mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-10-30 19:32:39 +08:00
Merge branch 'main' into visual-gruyere
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Use Kilo session share links when sharing conversations from the CLI.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
"@kilocode/kilo-ui": patch
|
||||
---
|
||||
|
||||
Reduce lag and gray placeholders in the diff and Changes views by enabling worker-backed highlighting and rendering patch-backed review hunks without rebuilding full source files.
|
||||
@@ -46,8 +46,10 @@ jobs:
|
||||
GH_REPO: ${{ github.repository }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
|
||||
- name: Save reviewed changelog
|
||||
run: cp packages/kilo-jetbrains/CHANGELOG.md "$RUNNER_TEMP/jetbrains-CHANGELOG.md"
|
||||
- name: Save reviewed release metadata
|
||||
run: |
|
||||
cp packages/kilo-jetbrains/CHANGELOG.md "$RUNNER_TEMP/jetbrains-CHANGELOG.md"
|
||||
cp packages/kilo-jetbrains/gradle.properties "$RUNNER_TEMP/jetbrains-gradle.properties"
|
||||
|
||||
- name: Checkout release tag
|
||||
uses: actions/checkout@v6
|
||||
@@ -55,8 +57,10 @@ jobs:
|
||||
fetch-depth: 0
|
||||
ref: ${{ steps.release.outputs.tag }}
|
||||
|
||||
- name: Restore reviewed changelog
|
||||
run: cp "$RUNNER_TEMP/jetbrains-CHANGELOG.md" packages/kilo-jetbrains/CHANGELOG.md
|
||||
- name: Restore reviewed release metadata
|
||||
run: |
|
||||
cp "$RUNNER_TEMP/jetbrains-CHANGELOG.md" packages/kilo-jetbrains/CHANGELOG.md
|
||||
cp "$RUNNER_TEMP/jetbrains-gradle.properties" packages/kilo-jetbrains/gradle.properties
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
@@ -107,7 +111,7 @@ jobs:
|
||||
|
||||
- name: Verify plugin
|
||||
working-directory: packages/kilo-jetbrains
|
||||
run: ./gradlew verifyPlugin -Pproduction=true -Pkilo.version="$VERSION" -Pkilo.channel="$CHANNEL"
|
||||
run: ./gradlew verifyPlugin -Pproduction=true -Pkilo.channel="$CHANNEL"
|
||||
env:
|
||||
VERSION: ${{ steps.release.outputs.version }}
|
||||
CHANNEL: ${{ steps.release.outputs.marketplace_channel }}
|
||||
@@ -125,7 +129,7 @@ jobs:
|
||||
|
||||
- name: Publish to JetBrains Marketplace
|
||||
working-directory: packages/kilo-jetbrains
|
||||
run: ./gradlew publishPlugin -Pproduction=true -Pkilo.version="$VERSION" -Pkilo.channel="$CHANNEL"
|
||||
run: ./gradlew publishPlugin -Pproduction=true -Pkilo.channel="$CHANNEL"
|
||||
env:
|
||||
VERSION: ${{ steps.release.outputs.version }}
|
||||
CHANNEL: ${{ steps.release.outputs.marketplace_channel }}
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
},
|
||||
"packages/core": {
|
||||
"name": "@opencode-ai/core",
|
||||
"version": "7.3.17",
|
||||
"version": "7.3.18",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -68,7 +68,7 @@
|
||||
},
|
||||
"packages/kilo-docs": {
|
||||
"name": "@kilocode/kilo-docs",
|
||||
"version": "7.3.17",
|
||||
"version": "7.3.18",
|
||||
"dependencies": {
|
||||
"@docsearch/css": "^4",
|
||||
"@docsearch/js": "^4",
|
||||
@@ -98,7 +98,7 @@
|
||||
},
|
||||
"packages/kilo-gateway": {
|
||||
"name": "@kilocode/kilo-gateway",
|
||||
"version": "7.3.17",
|
||||
"version": "7.3.18",
|
||||
"dependencies": {
|
||||
"@ai-sdk/alibaba": "1.0.17",
|
||||
"@ai-sdk/anthropic": "3.0.71",
|
||||
@@ -134,7 +134,7 @@
|
||||
},
|
||||
"packages/kilo-i18n": {
|
||||
"name": "@kilocode/kilo-i18n",
|
||||
"version": "7.3.17",
|
||||
"version": "7.3.18",
|
||||
"devDependencies": {
|
||||
"@tsconfig/node22": "catalog:",
|
||||
"@types/bun": "catalog:",
|
||||
@@ -144,7 +144,7 @@
|
||||
},
|
||||
"packages/kilo-indexing": {
|
||||
"name": "@kilocode/kilo-indexing",
|
||||
"version": "7.3.17",
|
||||
"version": "7.3.18",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-bedrock-runtime": "3.1005.0",
|
||||
"@aws-sdk/credential-provider-ini": "3.972.31",
|
||||
@@ -176,11 +176,11 @@
|
||||
},
|
||||
"packages/kilo-jetbrains": {
|
||||
"name": "@kilocode/kilo-jetbrains",
|
||||
"version": "7.3.17",
|
||||
"version": "7.3.18",
|
||||
},
|
||||
"packages/kilo-telemetry": {
|
||||
"name": "@kilocode/kilo-telemetry",
|
||||
"version": "7.3.17",
|
||||
"version": "7.3.18",
|
||||
"dependencies": {
|
||||
"@kilocode/kilo-gateway": "workspace:*",
|
||||
"posthog-node": "4.4.0",
|
||||
@@ -194,7 +194,7 @@
|
||||
},
|
||||
"packages/kilo-ui": {
|
||||
"name": "@kilocode/kilo-ui",
|
||||
"version": "7.3.17",
|
||||
"version": "7.3.18",
|
||||
"dependencies": {
|
||||
"@kilocode/sdk": "workspace:*",
|
||||
"@kobalte/core": "0.13.11",
|
||||
@@ -231,7 +231,7 @@
|
||||
},
|
||||
"packages/kilo-vscode": {
|
||||
"name": "kilo-code",
|
||||
"version": "7.3.17",
|
||||
"version": "7.3.18",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.39.0",
|
||||
"@kilocode/kilo-gateway": "workspace:*",
|
||||
@@ -296,7 +296,7 @@
|
||||
},
|
||||
"packages/opencode": {
|
||||
"name": "@kilocode/cli",
|
||||
"version": "7.3.17",
|
||||
"version": "7.3.18",
|
||||
"bin": {
|
||||
"kilo": "./bin/kilo",
|
||||
"kilocode": "./bin/kilo",
|
||||
@@ -456,7 +456,7 @@
|
||||
},
|
||||
"packages/plugin": {
|
||||
"name": "@kilocode/plugin",
|
||||
"version": "7.3.17",
|
||||
"version": "7.3.18",
|
||||
"dependencies": {
|
||||
"@kilocode/sdk": "workspace:*",
|
||||
"effect": "catalog:",
|
||||
@@ -481,7 +481,7 @@
|
||||
},
|
||||
"packages/script": {
|
||||
"name": "@opencode-ai/script",
|
||||
"version": "7.3.17",
|
||||
"version": "7.3.18",
|
||||
"dependencies": {
|
||||
"semver": "^7.6.3",
|
||||
},
|
||||
@@ -492,7 +492,7 @@
|
||||
},
|
||||
"packages/sdk/js": {
|
||||
"name": "@kilocode/sdk",
|
||||
"version": "7.3.17",
|
||||
"version": "7.3.18",
|
||||
"dependencies": {
|
||||
"cross-spawn": "catalog:",
|
||||
},
|
||||
@@ -507,7 +507,7 @@
|
||||
},
|
||||
"packages/storybook": {
|
||||
"name": "@opencode-ai/storybook",
|
||||
"version": "7.3.17",
|
||||
"version": "7.3.18",
|
||||
"devDependencies": {
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
"@solidjs/meta": "catalog:",
|
||||
@@ -530,7 +530,7 @@
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "7.3.17",
|
||||
"version": "7.3.18",
|
||||
"dependencies": {
|
||||
"@kilocode/sdk": "workspace:*",
|
||||
"@kobalte/core": "catalog:",
|
||||
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"nodeModules": {
|
||||
"x86_64-linux": "sha256-d+/jrUA/Di+8N2Hn1xPN4anKRQb9COTfTgnnf+2o5ew=",
|
||||
"aarch64-linux": "sha256-1tv43AwnfWGvuRtmM2Y08Zua7yRtemonjFkVlBkl9O8=",
|
||||
"aarch64-darwin": "sha256-D6FwbOxmYZLCPf2TCY02Qyu2Tu2D2TMzgAA0UroOWgA=",
|
||||
"x86_64-darwin": "sha256-WVw5WT8BqnAsieFj5ho+fLM2tmppJ2FyfEHm7BlD2y0="
|
||||
"x86_64-linux": "sha256-KqxasDpdyLQBZC+N85z4HFXLn/15D9qqDyEqgvWTY7c=",
|
||||
"aarch64-linux": "sha256-YEBF+S2+8imYWO4PfbCY5AOA5uIqAZY80GO6HK8k/sM=",
|
||||
"aarch64-darwin": "sha256-CxE5cdMcvcAENZLCilCU6Ndx+wiZVbFxtx0UNySdNDA=",
|
||||
"x86_64-darwin": "sha256-Z9eU77dkavAGAM0AlcnKXCXrrFuRyLnYwM/F9nrkwrk="
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -150,6 +150,6 @@
|
||||
"solid-js@1.9.10": "patches/solid-js@1.9.10.patch",
|
||||
"mammoth@1.12.0": "patches/mammoth@1.12.0.patch"
|
||||
},
|
||||
"version": "7.3.17",
|
||||
"version": "7.3.18",
|
||||
"peerDependencies": {}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "7.3.17",
|
||||
"version": "7.3.18",
|
||||
"name": "@opencode-ai/core",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
id = "kilo"
|
||||
name = "Kilo"
|
||||
description = "The open source coding agent."
|
||||
version = "7.3.17"
|
||||
version = "7.3.18"
|
||||
schema_version = 1
|
||||
authors = ["Anomaly"]
|
||||
repository = "https://github.com/Kilo-Org/kilocode"
|
||||
@@ -11,26 +11,26 @@ name = "Kilo"
|
||||
icon = "./icons/opencode.svg"
|
||||
|
||||
[agent_servers.opencode.targets.darwin-aarch64]
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.17/opencode-darwin-arm64.zip"
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.18/opencode-darwin-arm64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.darwin-x86_64]
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.17/opencode-darwin-x64.zip"
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.18/opencode-darwin-x64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.linux-aarch64]
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.17/opencode-linux-arm64.tar.gz"
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.18/opencode-linux-arm64.tar.gz"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.linux-x86_64]
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.17/opencode-linux-x64.tar.gz"
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.18/opencode-linux-x64.tar.gz"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.windows-x86_64]
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.17/opencode-windows-x64.zip"
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.18/opencode-windows-x64.zip"
|
||||
cmd = "./opencode.exe"
|
||||
args = ["acp"]
|
||||
|
||||
@@ -1,16 +1,46 @@
|
||||
import React, { useState, useEffect } from "react"
|
||||
import Link from "next/link"
|
||||
|
||||
const TAB_SYNC_EVENT = "kilo-tab-select"
|
||||
|
||||
function slugify(label: string) {
|
||||
return label
|
||||
.toLowerCase()
|
||||
.replace(/\s+/g, "-")
|
||||
.replace(/[^a-z0-9-]/g, "")
|
||||
}
|
||||
|
||||
export function TableOfContents({ toc }) {
|
||||
const items = toc.filter((item) => item.id && (item.level === 2 || item.level === 3))
|
||||
const [tab, setTab] = useState("")
|
||||
const [activeHash, setActiveHash] = useState("")
|
||||
const items = toc.filter((item) => {
|
||||
if (!item.id || (item.level !== 2 && item.level !== 3)) return false
|
||||
|
||||
return !item.tab || item.tab.slug === tab
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const updateHash = () => setActiveHash(window.location.hash)
|
||||
updateHash()
|
||||
window.addEventListener("hashchange", updateHash)
|
||||
return () => window.removeEventListener("hashchange", updateHash)
|
||||
}, [])
|
||||
const update = () => {
|
||||
const hash = window.location.hash.slice(1)
|
||||
const item = toc.find((entry) => entry.id === hash && entry.tab)
|
||||
setActiveHash(window.location.hash)
|
||||
setTab(item?.tab?.slug ?? (toc.some((entry) => entry.tab?.slug === hash) ? hash : ""))
|
||||
}
|
||||
|
||||
const sync = (e: Event) => {
|
||||
const label = (e as CustomEvent<string>).detail
|
||||
setActiveHash(window.location.hash)
|
||||
setTab(slugify(label))
|
||||
}
|
||||
|
||||
update()
|
||||
window.addEventListener("hashchange", update)
|
||||
window.addEventListener(TAB_SYNC_EVENT, sync)
|
||||
return () => {
|
||||
window.removeEventListener("hashchange", update)
|
||||
window.removeEventListener(TAB_SYNC_EVENT, sync)
|
||||
}
|
||||
}, [toc])
|
||||
|
||||
if (items.length <= 1) {
|
||||
return null
|
||||
@@ -22,14 +52,27 @@ export function TableOfContents({ toc }) {
|
||||
{items.map((item) => {
|
||||
const href = `#${item.id}`
|
||||
const active = activeHash === href
|
||||
const select = (e: React.MouseEvent<HTMLAnchorElement>) => {
|
||||
if (!item.tab) return
|
||||
|
||||
e.preventDefault()
|
||||
window.dispatchEvent(new CustomEvent(TAB_SYNC_EVENT, { detail: item.tab.label }))
|
||||
setActiveHash(href)
|
||||
setTab(item.tab.slug)
|
||||
history.pushState(null, "", href)
|
||||
requestAnimationFrame(() => document.getElementById(item.id)?.scrollIntoView())
|
||||
}
|
||||
|
||||
return (
|
||||
<li
|
||||
key={item.title}
|
||||
key={`${item.tab?.slug ?? "page"}-${item.id}`}
|
||||
className={[active ? "active" : undefined, item.level === 3 ? "padded" : undefined]
|
||||
.filter(Boolean)
|
||||
.join(" ")}
|
||||
>
|
||||
<Link href={href}>{item.title}</Link>
|
||||
<Link href={href} onClick={select}>
|
||||
{item.title}
|
||||
</Link>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect, Children, isValidElement, ReactNode, ReactElement } from "react"
|
||||
import React, { useState, useEffect, useRef, Children, isValidElement, ReactNode, ReactElement } from "react"
|
||||
|
||||
const TAB_SYNC_EVENT = "kilo-tab-select"
|
||||
|
||||
@@ -18,6 +18,17 @@ function slugify(label: string) {
|
||||
.replace(/[^a-z0-9-]/g, "")
|
||||
}
|
||||
|
||||
function contains(node: ReactNode, hash: string): boolean {
|
||||
return Children.toArray(node).some((child) => {
|
||||
if (!isValidElement(child)) return false
|
||||
|
||||
const props = child.props as { children?: ReactNode; id?: string }
|
||||
if (props.id === hash) return true
|
||||
|
||||
return contains(props.children, hash)
|
||||
})
|
||||
}
|
||||
|
||||
export function Tab({ children }: TabProps) {
|
||||
return <>{children}</>
|
||||
}
|
||||
@@ -32,19 +43,31 @@ export function Tabs({ children }: TabsProps) {
|
||||
const hash = window.location.hash.slice(1)
|
||||
if (!hash) return 0
|
||||
const found = tabs.findIndex((tab) => slugify(tab.props.label) === hash)
|
||||
return found >= 0 ? found : 0
|
||||
if (found >= 0) return found
|
||||
|
||||
const child = tabs.findIndex((tab) => contains(tab.props.children, hash))
|
||||
return child >= 0 ? child : 0
|
||||
}
|
||||
|
||||
const [activeIndex, setActiveIndex] = useState(0)
|
||||
const scroll = useRef(false)
|
||||
|
||||
useEffect(() => {
|
||||
setActiveIndex(indexFromHash())
|
||||
const onHashChange = () => setActiveIndex(indexFromHash())
|
||||
const activate = () => {
|
||||
const hash = window.location.hash.slice(1)
|
||||
const index = indexFromHash()
|
||||
scroll.current = Boolean(hash && contains(tabs[index]?.props.children, hash))
|
||||
setActiveIndex(index)
|
||||
}
|
||||
|
||||
activate()
|
||||
const onHashChange = activate
|
||||
window.addEventListener("hashchange", onHashChange)
|
||||
|
||||
const onSync = (e: Event) => {
|
||||
const label = (e as CustomEvent<string>).detail
|
||||
const found = tabs.findIndex((tab) => tab.props.label === label)
|
||||
scroll.current = false
|
||||
if (found >= 0) setActiveIndex(found)
|
||||
}
|
||||
window.addEventListener(TAB_SYNC_EVENT, onSync)
|
||||
@@ -55,6 +78,14 @@ export function Tabs({ children }: TabsProps) {
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const hash = window.location.hash.slice(1)
|
||||
if (!hash || !scroll.current) return
|
||||
|
||||
scroll.current = false
|
||||
requestAnimationFrame(() => document.getElementById(hash)?.scrollIntoView())
|
||||
}, [activeIndex])
|
||||
|
||||
const selectTab = (index: number) => {
|
||||
setActiveIndex(index)
|
||||
const label = tabs[index].props.label
|
||||
|
||||
@@ -11,7 +11,10 @@ Before installing the Kilo Code plugin, ensure you have:
|
||||
|
||||
2. **Node.js:**
|
||||
- Download LTS version from [https://nodejs.org/](https://nodejs.org/)
|
||||
- Required for the extension's backend services
|
||||
|
||||
{% callout type="tip" %}
|
||||
Try the [v7 Early Access Program plugin](#jetbrains-early-access) for a JetBrains-native experience that does not require Node.js or manual API key configuration.
|
||||
{% /callout %}
|
||||
|
||||
### Install directly
|
||||
|
||||
@@ -28,12 +31,12 @@ Before installing the Kilo Code plugin, ensure you have:
|
||||
4. Search for "Kilo Code"
|
||||
5. Click **Install** and restart your IDE
|
||||
|
||||
### Try the v7 Early Access Program plugin
|
||||
### Try the v7 Early Access Program plugin {% #jetbrains-early-access %}
|
||||
|
||||
The v7 EAP plugin is available for users who want to try the newest JetBrains experience before it reaches the default Marketplace channel. It uses a JetBrains-native UI and is designed to work well with JetBrains remote development.
|
||||
|
||||
{% callout type="info" %}
|
||||
EAP builds may update more frequently than the default Marketplace release. Share feedback in the JetBrains channel on the [Kilo Discord](https://kilo.ai/discord).
|
||||
The v7 EAP plugin supports JetBrains IDEs with 261+ builds. EAP builds may update more frequently than the default Marketplace release. Share feedback in the JetBrains channel on the [Kilo Discord](https://kilo.ai/discord).
|
||||
{% /callout %}
|
||||
|
||||
To install the EAP build and receive updates:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@kilocode/kilo-docs",
|
||||
"version": "7.3.17",
|
||||
"version": "7.3.18",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --webpack --port 3002",
|
||||
|
||||
@@ -28,27 +28,42 @@ import type { MarkdocNextJsPageProps } from "@markdoc/next.js"
|
||||
const TITLE = "Kilo Code Documentation"
|
||||
const DESCRIPTION = "Build, ship, and iterate faster with the most popular open source coding agent."
|
||||
|
||||
function collectHeadings(node, sections = []) {
|
||||
function slugify(label) {
|
||||
return label
|
||||
.toLowerCase()
|
||||
.replace(/\s+/g, "-")
|
||||
.replace(/[^a-z0-9-]/g, "")
|
||||
}
|
||||
|
||||
function collectHeadings(node, sections = [], tab = undefined) {
|
||||
if (node) {
|
||||
// Skip headings inside tabs - they're not always visible
|
||||
if (node.name === "Tabs") {
|
||||
for (const child of node.children || []) {
|
||||
const label = child.attributes?.label
|
||||
collectHeadings(
|
||||
child,
|
||||
sections,
|
||||
typeof label === "string" ? { label, slug: slugify(label) } : undefined,
|
||||
)
|
||||
}
|
||||
return sections
|
||||
}
|
||||
|
||||
if (node.name === "Heading") {
|
||||
const title = node.children[0]
|
||||
const title = typeof node.children[0] === "string" ? node.children[0].trim() : node.children[0]
|
||||
|
||||
if (typeof title === "string") {
|
||||
sections.push({
|
||||
...node.attributes,
|
||||
title,
|
||||
tab,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (node.children) {
|
||||
for (const child of node.children) {
|
||||
collectHeadings(child, sections)
|
||||
collectHeadings(child, sections, tab)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,49 +25,6 @@ The current Kilo Code extension is built on the [Kilo CLI](https://github.com/Ki
|
||||
The "pre-release" label is a VS Code Marketplace distribution channel — the extension is stable and recommended for all users.
|
||||
{% /callout %}
|
||||
|
||||
{% /tab %}
|
||||
{% tab label="CLI" %}
|
||||
|
||||
## Command Line Interface
|
||||
|
||||
{% partial file="install-cli.md" /%}
|
||||
|
||||
{% /tab %}
|
||||
{% tab label="VS Code (Legacy)" %}
|
||||
|
||||
## VS Code Legacy Extension
|
||||
|
||||
The legacy extension is the previous version of Kilo Code for VS Code. It is still available but is no longer actively developed. We recommend installing the current extension (see the **VS Code** tab).
|
||||
|
||||
To install or switch back to the legacy version:
|
||||
|
||||
1. Open VS Code
|
||||
2. Go to Extensions (`Ctrl+Shift+X` / `Cmd+Shift+X`)
|
||||
3. Search for "Kilo Code"
|
||||
4. Click the dropdown arrow next to **Install** and select **Switch to Release Version**
|
||||
|
||||
{% /tab %}
|
||||
{% tab label="JetBrains" %}
|
||||
|
||||
## JetBrains IDEs
|
||||
|
||||
{% partial file="install-jetbrains.md" /%}
|
||||
|
||||
{% /tab %}
|
||||
{% tab label="Slack" %}
|
||||
|
||||
## Slack Integration
|
||||
|
||||
{% partial file="install-slack.md" /%}
|
||||
|
||||
{% /tab %}
|
||||
{% tab label="Other IDEs" %}
|
||||
|
||||
{% partial file="install-other-ides.md" /%}
|
||||
|
||||
{% /tab %}
|
||||
{% /tabs %}
|
||||
|
||||
## Manual Installations
|
||||
|
||||
### Open VSX Registry
|
||||
@@ -136,6 +93,49 @@ If you plan to remain on that version for a while, you may also want to temporar
|
||||
3. Add: `C:\Windows\System32\WindowsPowerShell\v1.0\`
|
||||
4. Click **OK** and restart VS Code
|
||||
|
||||
{% /tab %}
|
||||
{% tab label="CLI" %}
|
||||
|
||||
## Command Line Interface
|
||||
|
||||
{% partial file="install-cli.md" /%}
|
||||
|
||||
{% /tab %}
|
||||
{% tab label="VS Code (Legacy)" %}
|
||||
|
||||
## VS Code Legacy Extension
|
||||
|
||||
The legacy extension is the previous version of Kilo Code for VS Code. It is still available but is no longer actively developed. We recommend installing the current extension (see the **VS Code** tab).
|
||||
|
||||
To install or switch back to the legacy version:
|
||||
|
||||
1. Open VS Code
|
||||
2. Go to Extensions (`Ctrl+Shift+X` / `Cmd+Shift+X`)
|
||||
3. Search for "Kilo Code"
|
||||
4. Click the dropdown arrow next to **Install** and select **Switch to Release Version**
|
||||
|
||||
{% /tab %}
|
||||
{% tab label="JetBrains" %}
|
||||
|
||||
## JetBrains IDEs
|
||||
|
||||
{% partial file="install-jetbrains.md" /%}
|
||||
|
||||
{% /tab %}
|
||||
{% tab label="Slack" %}
|
||||
|
||||
## Slack Integration
|
||||
|
||||
{% partial file="install-slack.md" /%}
|
||||
|
||||
{% /tab %}
|
||||
{% tab label="Other IDEs" %}
|
||||
|
||||
{% partial file="install-other-ides.md" /%}
|
||||
|
||||
{% /tab %}
|
||||
{% /tabs %}
|
||||
|
||||
## Next Steps
|
||||
|
||||
After installation, check out these resources to get started:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@kilocode/kilo-gateway",
|
||||
"version": "7.3.17",
|
||||
"version": "7.3.18",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"description": "Unified Kilo Gateway package for OpenCode - authentication, provider, and API integration",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@kilocode/kilo-i18n",
|
||||
"version": "7.3.17",
|
||||
"version": "7.3.18",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"description": "Kilo-specific i18n translations and overrides",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@kilocode/kilo-indexing",
|
||||
"version": "7.3.17",
|
||||
"version": "7.3.18",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"description": "Standalone indexing engine and host helpers for Kilo Code",
|
||||
|
||||
@@ -2,7 +2,12 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.0.0-dev]
|
||||
## [7.0.1-rc.4] - 2026-05-29
|
||||
|
||||
### Added
|
||||
- Initial JetBrains plugin development builds.
|
||||
- Initial JetBrains plugin release with a native Kilo Code tool window.
|
||||
- Chat sessions with streamed responses, tool output, reasoning, markdown, todos, and plan follow-ups.
|
||||
- Native mode/model selection, account sign-in, permission prompts, and question flows.
|
||||
- Local and cloud session history with search, reopen, rename/delete local sessions, and repository filtering.
|
||||
- Migration wizard for legacy JetBrains plugin settings and chat history.
|
||||
- Bundled Kilo CLI runtime for macOS, Linux, and Windows.
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
AI coding agent plugin for JetBrains IDEs.
|
||||
|
||||
To try the v7 Early Access Program plugin, follow the [JetBrains EAP installation guide](https://kilo.ai/docs/code-with-ai/platforms/jetbrains#jetbrains-early-access).
|
||||
|
||||
---
|
||||
|
||||
## Set up your environment
|
||||
|
||||
@@ -58,12 +58,12 @@ The PR updates:
|
||||
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| `packages/kilo-jetbrains/package.json` | JetBrains plugin package version. |
|
||||
| `packages/kilo-jetbrains/gradle.properties` | JetBrains plugin version in `kilo.jetbrains.version`. |
|
||||
| `packages/kilo-jetbrains/CHANGELOG.md` | Release notes packaged into the plugin. |
|
||||
|
||||
Review and edit `packages/kilo-jetbrains/CHANGELOG.md` before merging. This changelog entry is rendered into JetBrains `<change-notes>`, so it appears on the Marketplace and inside IntelliJ plugin UI.
|
||||
Review `packages/kilo-jetbrains/gradle.properties` and edit `packages/kilo-jetbrains/CHANGELOG.md` before merging. This changelog entry is rendered into JetBrains `<change-notes>`, so it appears on the Marketplace and inside IntelliJ plugin UI.
|
||||
|
||||
The PR can change release metadata such as `packages/kilo-jetbrains/package.json` and `packages/kilo-jetbrains/CHANGELOG.md`, but it does not change the tagged source code that will be built.
|
||||
The PR can change release metadata such as `packages/kilo-jetbrains/gradle.properties` and `packages/kilo-jetbrains/CHANGELOG.md`, but it does not change the tagged source code that will be built.
|
||||
|
||||
## Merge and Publish
|
||||
|
||||
@@ -84,7 +84,7 @@ Publishing behavior:
|
||||
| `x.y.z-rc.n` | `eap` | Prerelease |
|
||||
| `x.y.z` | default | Stable release |
|
||||
|
||||
The workflow checks out `jetbrains/v<version>` for verification, signing, and Marketplace publishing. It overlays the reviewed `packages/kilo-jetbrains/CHANGELOG.md` from the merged PR before rendering release notes and before `publishPlugin`, so Marketplace metadata and the GitHub Release use the reviewed changelog.
|
||||
The workflow checks out `jetbrains/v<version>` for verification, signing, and Marketplace publishing. It overlays the reviewed `packages/kilo-jetbrains/gradle.properties` and `packages/kilo-jetbrains/CHANGELOG.md` from the merged PR before rendering release notes and before `publishPlugin`, so the Marketplace plugin version, Marketplace notes, and GitHub Release use the reviewed metadata.
|
||||
|
||||
## Installing RC Builds
|
||||
|
||||
|
||||
+2
-2
@@ -542,14 +542,14 @@ class KiloBackendAppService private constructor(
|
||||
|
||||
private fun setAppReady(data: AppData) {
|
||||
warnings = data.warnings
|
||||
_appState.value = KiloAppState.Ready(data)
|
||||
if (data.warnings.isNotEmpty()) warnAppWarnings(data.warnings)
|
||||
_appState.value = KiloAppState.Ready(data)
|
||||
}
|
||||
|
||||
private fun setAppError(message: String, errors: List<LoadError>) {
|
||||
val state = KiloAppState.Error(message, errors)
|
||||
_appState.value = state
|
||||
warnAppError(state)
|
||||
_appState.value = state
|
||||
}
|
||||
|
||||
private fun warnAppError(state: KiloAppState.Error) {
|
||||
|
||||
+1
-1
@@ -291,8 +291,8 @@ class KiloBackendWorkspace(
|
||||
}
|
||||
|
||||
private fun setWorkspaceError(message: String, errors: List<LoadError>) {
|
||||
_state.value = KiloWorkspaceState.Error(message, errors)
|
||||
log.warn("Workspace error [$directory]: $message")
|
||||
_state.value = KiloWorkspaceState.Error(message, errors)
|
||||
}
|
||||
|
||||
private data class FetchResult<T>(val value: T?, val error: LoadError?) {
|
||||
|
||||
@@ -42,10 +42,11 @@ fun gitTag(): String? {
|
||||
|
||||
val release = providers.gradleProperty("production").map { it.toBoolean() }.orElse(false).get()
|
||||
val override = providers.gradleProperty("kilo.version").orNull?.trim()?.takeIf { it.isNotEmpty() }
|
||||
val ver = override?.let(::checked) ?: if (release) checked(
|
||||
gitTag()?.removePrefix("jetbrains/v")
|
||||
?: error("Missing JetBrains plugin version. Publish builds must run from a jetbrains/v<version> tag."),
|
||||
) else checked(gitTag()?.removePrefix("jetbrains/v") ?: "0.0.0-dev")
|
||||
val prop = providers.gradleProperty("kilo.jetbrains.version").orNull?.trim()?.takeIf { it.isNotEmpty() }
|
||||
val tag = gitTag()?.removePrefix("jetbrains/v")
|
||||
val ver = override?.let(::checked) ?: prop?.let(::checked) ?: if (release) checked(
|
||||
tag ?: error("Missing JetBrains plugin version. Publish builds must set kilo.jetbrains.version or run from a jetbrains/v<version> tag."),
|
||||
) else checked(tag ?: "0.0.0-dev")
|
||||
|
||||
val channel = providers.gradleProperty("kilo.channel").map { it.trim() }.orElse("default")
|
||||
val splitPort = providers.gradleProperty("kilo.splitModeServerPort").orNull?.let(::port) ?: fallback()
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
kotlin.stdlib.default.dependency=false
|
||||
kilo.jetbrains.version=7.0.1-rc.4
|
||||
org.gradle.configuration-cache=true
|
||||
org.gradle.caching=true
|
||||
org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=512m
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"test": "./gradlew test",
|
||||
"test:ci": "bun script/test-ci.ts"
|
||||
},
|
||||
"version": "7.3.17",
|
||||
"version": "7.3.18",
|
||||
"dependencies": {},
|
||||
"devDependencies": {},
|
||||
"peerDependencies": {}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@kilocode/kilo-telemetry",
|
||||
"version": "7.3.17",
|
||||
"version": "7.3.18",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"description": "Telemetry for Kilo CLI - PostHog analytics integration",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@kilocode/kilo-ui",
|
||||
"version": "7.3.17",
|
||||
"version": "7.3.18",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"exports": {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DIFFS_TAG_NAME, FileDiff, type SelectedLineRange, VirtualizedFileDiff } from "@pierre/diffs"
|
||||
import { DIFFS_TAG_NAME, FileDiff, processFile, type SelectedLineRange, VirtualizedFileDiff } from "@pierre/diffs"
|
||||
import { PreloadMultiFileDiffResult } from "@pierre/diffs/ssr"
|
||||
import { createEffect, onCleanup, onMount, Show, splitProps } from "solid-js"
|
||||
import { Dynamic, isServer } from "solid-js/web"
|
||||
@@ -16,11 +16,14 @@ export function Diff<T>(props: SSRDiffProps<T>) {
|
||||
const [local, others] = splitProps(props, [
|
||||
"before",
|
||||
"after",
|
||||
"patch",
|
||||
"fileDiff",
|
||||
"class",
|
||||
"classList",
|
||||
"annotations",
|
||||
"selectedLines",
|
||||
"commentedLines",
|
||||
"virtualized",
|
||||
])
|
||||
const workerPool = useWorkerPool(props.diffStyle)
|
||||
|
||||
@@ -227,7 +230,7 @@ export function Diff<T>(props: SSRDiffProps<T>) {
|
||||
onCleanup(() => monitor.disconnect())
|
||||
}
|
||||
|
||||
const virtualizer = getVirtualizer()
|
||||
const virtualizer = local.virtualized === false ? undefined : getVirtualizer()
|
||||
|
||||
fileDiffInstance = virtualizer
|
||||
? new VirtualizedFileDiff<T>(
|
||||
@@ -250,9 +253,12 @@ export function Diff<T>(props: SSRDiffProps<T>) {
|
||||
)
|
||||
// @ts-expect-error - fileContainer is private but needed for SSR hydration
|
||||
fileDiffInstance.fileContainer = fileDiffRef
|
||||
const patch = "patch" in local && typeof local.patch === "string" ? local.patch : ""
|
||||
const metadata = local.fileDiff ?? (patch ? processFile(patch, { cacheKey: patch }) : undefined)
|
||||
fileDiffInstance.hydrate({
|
||||
oldFile: local.before,
|
||||
newFile: local.after,
|
||||
oldFile: metadata ? undefined : local.before,
|
||||
newFile: metadata ? undefined : local.after,
|
||||
fileDiff: metadata,
|
||||
lineAnnotations: local.annotations,
|
||||
fileContainer: fileDiffRef,
|
||||
containerWrapper: container,
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import { sampledChecksum } from "@opencode-ai/core/util/encode"
|
||||
import { FileDiff, type FileDiffOptions, type SelectedLineRange, VirtualizedFileDiff } from "@pierre/diffs"
|
||||
import {
|
||||
FileDiff,
|
||||
type FileDiffMetadata,
|
||||
type FileDiffOptions,
|
||||
processFile,
|
||||
type SelectedLineRange,
|
||||
VirtualizedFileDiff,
|
||||
} from "@pierre/diffs"
|
||||
import { createMediaQuery } from "@solid-primitives/media"
|
||||
import { createEffect, createMemo, createSignal, on, onCleanup, splitProps, untrack } from "solid-js"
|
||||
import { createDefaultOptions, type DiffProps, styleVariables } from "../pierre"
|
||||
@@ -142,6 +149,7 @@ export function Diff<T>(props: DiffProps<T>) {
|
||||
let container!: HTMLDivElement
|
||||
let observer: MutationObserver | undefined
|
||||
let sharedVirtualizer: NonNullable<ReturnType<typeof acquireVirtualizer>> | undefined
|
||||
let parsed: { patch: string; diff: FileDiffMetadata } | undefined
|
||||
let renderToken = 0
|
||||
let selectionFrame: number | undefined
|
||||
let dragFrame: number | undefined
|
||||
@@ -156,6 +164,7 @@ export function Diff<T>(props: DiffProps<T>) {
|
||||
const [local, others] = splitProps(props, [
|
||||
"before",
|
||||
"after",
|
||||
"patch",
|
||||
"fileDiff",
|
||||
"class",
|
||||
"classList",
|
||||
@@ -163,6 +172,7 @@ export function Diff<T>(props: DiffProps<T>) {
|
||||
"selectedLines",
|
||||
"commentedLines",
|
||||
"onRendered",
|
||||
"virtualized",
|
||||
])
|
||||
|
||||
const mobile = createMediaQuery("(max-width: 640px)")
|
||||
@@ -178,11 +188,24 @@ export function Diff<T>(props: DiffProps<T>) {
|
||||
})
|
||||
|
||||
const estimate = createMemo(() => {
|
||||
const value = Math.max(lines(before()), lines(after())) * ESTIMATED_LINE_HEIGHT
|
||||
// A tracked detail response already carries a hunk-bounded git patch. Base
|
||||
// placeholder height on that patch instead of the full source file so a
|
||||
// tiny change in a large file does not reserve a large gray body.
|
||||
const patch = "patch" in local && typeof local.patch === "string" ? local.patch : ""
|
||||
const value = (patch ? lines(patch) : Math.max(lines(before()), lines(after()))) * ESTIMATED_LINE_HEIGHT
|
||||
if (value === 0) return MIN_PLACEHOLDER_HEIGHT
|
||||
return Math.max(MIN_PLACEHOLDER_HEIGHT, Math.min(value, MAX_PLACEHOLDER_HEIGHT))
|
||||
})
|
||||
|
||||
const patchDiff = () => {
|
||||
if (!("patch" in local) || typeof local.patch !== "string" || local.patch.length === 0) return
|
||||
if (parsed?.patch === local.patch) return parsed.diff
|
||||
const diff = processFile(local.patch, { cacheKey: local.patch })
|
||||
if (!diff) return
|
||||
parsed = { patch: local.patch, diff }
|
||||
return diff
|
||||
}
|
||||
|
||||
const large = createMemo(() => {
|
||||
return Math.max(before().length, after().length) > 500_000
|
||||
})
|
||||
@@ -681,8 +704,19 @@ export function Diff<T>(props: DiffProps<T>) {
|
||||
|
||||
const opts = options()
|
||||
const workerPool = large() ? getWorkerPool("unified") : getWorkerPool(props.diffStyle)
|
||||
const virtualizer = getVirtualizer()
|
||||
// Eager (non-virtualized) patch-backed diffs render their visible hunks once
|
||||
// and never re-render on scroll or height changes, avoiding Pierre's
|
||||
// re-render-all storms. Full-content or oversized diffs keep virtualizing.
|
||||
const virtualizer = local.virtualized === false ? undefined : getVirtualizer()
|
||||
if (local.virtualized === false && sharedVirtualizer) {
|
||||
sharedVirtualizer.release()
|
||||
sharedVirtualizer = undefined
|
||||
}
|
||||
const annotations = untrack(() => local.annotations)
|
||||
// Parse hunk-bounded patches only after the deferred visibility gate. This
|
||||
// preserves quick session switching while avoiding a full before/after diff
|
||||
// reconstruction for tiny changes inside large source files.
|
||||
const metadata = local.fileDiff ?? patchDiff()
|
||||
|
||||
// Preserve container height during re-render to prevent scroll jumps.
|
||||
// When Pierre tears down the DOM (innerHTML = ""), the container collapses
|
||||
@@ -699,9 +733,9 @@ export function Diff<T>(props: DiffProps<T>) {
|
||||
|
||||
container.innerHTML = ""
|
||||
|
||||
if (local.fileDiff) {
|
||||
if (metadata) {
|
||||
instance.render({
|
||||
fileDiff: local.fileDiff,
|
||||
fileDiff: metadata,
|
||||
lineAnnotations: annotations,
|
||||
containerWrapper: container,
|
||||
})
|
||||
|
||||
@@ -48,6 +48,11 @@ type DiffShared<T> = FileDiffOptions<T> & {
|
||||
commentedLines?: SelectedLineRange[]
|
||||
onLineNumberSelectionEnd?: (selection: SelectedLineRange | null) => void
|
||||
onRendered?: () => void
|
||||
// When false, render the supplied diff once instead of row-virtualizing it.
|
||||
// Callers should supply hunk-bounded `fileDiff`/`patch` data for large source
|
||||
// files so eager rendering does not expand full before/after content.
|
||||
// Defaults to virtualized.
|
||||
virtualized?: boolean
|
||||
class?: string
|
||||
classList?: ComponentProps<"div">["classList"]
|
||||
}
|
||||
@@ -55,6 +60,8 @@ type DiffShared<T> = FileDiffOptions<T> & {
|
||||
type DiffPair<T> = DiffShared<T> & {
|
||||
before: FileContents
|
||||
after: FileContents
|
||||
/** Unified patch used to parse only rendered hunks instead of full file contents. */
|
||||
patch?: string
|
||||
fileDiff?: undefined
|
||||
}
|
||||
|
||||
@@ -62,6 +69,7 @@ type DiffPatch<T> = DiffShared<T> & {
|
||||
fileDiff: FileDiffMetadata
|
||||
before?: undefined
|
||||
after?: undefined
|
||||
patch?: undefined
|
||||
}
|
||||
|
||||
export type DiffProps<T = {}> = DiffPair<T> | DiffPatch<T>
|
||||
|
||||
@@ -181,6 +181,12 @@ New webview features must use **`@kilocode/kilo-ui`** components instead of raw
|
||||
- **Prefer kilo-ui styles**: Always reuse existing kilo-ui CSS variables, tokens, and component styles instead of writing custom CSS. If a style doesn't exist in kilo-ui yet, add it there and reuse it rather than inlining or duplicating styles in the webview.
|
||||
- **Icons**: kilo-ui has 75+ custom SVG icons in [`packages/ui/src/components/icon.tsx`](../../packages/ui/src/components/icon.tsx). To list all available icon names: `node -e "const c=require('fs').readFileSync('../../packages/ui/src/components/icon.tsx','utf8');[...c.matchAll(/^\\s{2}[\"']?([\\w-]+)[\"']?:\\s*\x60/gm)].map(m=>m[1]).sort().forEach(n=>console.log(n))"`. Icon names use both hyphenated (`arrow-left`) and bare-word (`brain`, `console`, `providers`) keys.
|
||||
|
||||
### Diff Rendering Performance
|
||||
|
||||
- Preserve hunk-bounded unified `patch` data through Changes/review detail flows and pass patch-derived `FileDiffMetadata` to Pierre when available. Do not eagerly render Pierre from complete `before`/`after` contents based only on changed-line counts: a tiny patch in a large source file can otherwise parse and render the entire file while the user sees a placeholder.
|
||||
- Pierre workers can offload highlighted updates, but they do not make an expensive synchronous initial render safe. Keep initial rendering hunk-bounded, and keep patch parsing behind deferred visibility/activation where session-switch responsiveness depends on it.
|
||||
- When changing diff scheduling, verify both rapid session switching and fast scrolling through a review. Improving one by shifting work into the other is a regression, not an optimization.
|
||||
|
||||
## Docs Screenshot Stories
|
||||
|
||||
When adding or updating Storybook stories for screenshots used by docs, make the story content match the docs page closely before replacing the docs image. Do not replace screenshots from VSCode Legacy docs tabs or sections.
|
||||
@@ -191,6 +197,7 @@ Generated screenshot baselines live under `packages/kilo-docs/public/img/screens
|
||||
|
||||
- Extension logs: "Extension Host" output channel (not Debug Console)
|
||||
- Webview logs: Command Palette → "Developer: Open Webview Developer Tools"
|
||||
- In Chrome/VS Code performance traces, associate CPU `ProfileChunk` events to their `Profile.id` target before attributing work to a thread. `v8:ProfEvntProc` is a profile delivery thread, not evidence that application work ran off the webview main thread.
|
||||
- All debug output must be prepended with `[Kilo New]` for easy filtering
|
||||
|
||||
## Naming Conventions
|
||||
|
||||
@@ -56,45 +56,46 @@ const esbuildProblemMatcherPlugin = {
|
||||
}
|
||||
|
||||
/**
|
||||
* Stub the pierre worker module so the Diff/Code components work without
|
||||
* web workers in the VS Code webview. The `@pierre/diffs` library handles
|
||||
* undefined worker pools gracefully (renders without syntax highlighting).
|
||||
* Route the shared `@opencode-ai/ui/pierre/worker` module (and its relative
|
||||
* variants) to the Kilo implementation in `webview-ui/pierre-worker.ts`.
|
||||
*
|
||||
* We stub the entire worker module rather than just the URL import because
|
||||
* `new Worker('')` would throw at runtime.
|
||||
* The upstream module loads Pierre's Shiki worker via a Vite-only
|
||||
* `?worker&url` import that esbuild can't resolve. The Kilo replacement loads
|
||||
* the worker from the bundled `dist/shiki-worker.js` asset instead, so syntax
|
||||
* highlighting runs off the main thread. `@pierre/diffs/worker` (used by that
|
||||
* replacement) is left alone.
|
||||
*
|
||||
* @type {import('esbuild').Plugin}
|
||||
*/
|
||||
const pierreWorkerStubPlugin = {
|
||||
name: "pierre-worker-stub",
|
||||
const pierreWorkerAliasPlugin = {
|
||||
name: "pierre-worker-alias",
|
||||
setup(build) {
|
||||
// Stub the Vite-specific ?worker&url import
|
||||
build.onResolve({ filter: /\?worker&url$/ }, (args) => ({
|
||||
path: args.path,
|
||||
namespace: "worker-url-stub",
|
||||
}))
|
||||
build.onLoad({ filter: /.*/, namespace: "worker-url-stub" }, () => ({
|
||||
contents: "export default ''",
|
||||
loader: "js",
|
||||
}))
|
||||
|
||||
// Stub the pierre worker module so getWorkerPool always returns undefined
|
||||
build.onResolve({ filter: /pierre\/worker$/ }, (args) => {
|
||||
// Only stub the local UI worker module, not @pierre/diffs/worker
|
||||
if (args.path.includes("@pierre")) return
|
||||
return {
|
||||
path: args.path,
|
||||
namespace: "pierre-worker-stub",
|
||||
}
|
||||
return { path: path.join(__dirname, "webview-ui", "pierre-worker.ts") }
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the synthetic `kilo-shiki-worker` entry point to Pierre's Shiki worker
|
||||
* so esbuild can bundle it (and its inlined oniguruma WebAssembly) into a single
|
||||
* `dist/shiki-worker.js` asset loaded by `webview-ui/pierre-worker.ts`. Switch to
|
||||
* `worker-portable.js` to drop WebAssembly and use the JS regex engine instead.
|
||||
*
|
||||
* @type {import('esbuild').Plugin}
|
||||
*/
|
||||
const shikiWorkerEntryPlugin = {
|
||||
name: "shiki-worker-entry",
|
||||
setup(build) {
|
||||
build.onResolve({ filter: /^kilo-shiki-worker$/ }, async () => {
|
||||
const resolved = await build.resolve("@pierre/diffs/worker/worker.js", {
|
||||
kind: "import-statement",
|
||||
resolveDir: __dirname,
|
||||
})
|
||||
if (resolved.errors.length > 0) return { errors: resolved.errors }
|
||||
return { path: resolved.path }
|
||||
})
|
||||
build.onLoad({ filter: /.*/, namespace: "pierre-worker-stub" }, () => ({
|
||||
contents: `
|
||||
export function getWorkerPool() { return undefined }
|
||||
export function getWorkerPools() { return { unified: undefined, split: undefined } }
|
||||
export function workerFactory() { return undefined }
|
||||
`,
|
||||
loader: "js",
|
||||
}))
|
||||
},
|
||||
}
|
||||
|
||||
@@ -157,7 +158,7 @@ function createBrowserWebviewContext(entryPoint, outfile) {
|
||||
},
|
||||
plugins: [
|
||||
solidDedupePlugin,
|
||||
pierreWorkerStubPlugin,
|
||||
pierreWorkerAliasPlugin,
|
||||
svgSpritePlugin,
|
||||
cssPackageResolvePlugin,
|
||||
solidPlugin(),
|
||||
@@ -166,6 +167,23 @@ function createBrowserWebviewContext(entryPoint, outfile) {
|
||||
})
|
||||
}
|
||||
|
||||
// Bundle Pierre's Shiki worker into a single self-contained asset that the
|
||||
// webviews load off the main thread for syntax highlighting.
|
||||
function createShikiWorkerContext() {
|
||||
return esbuild.context({
|
||||
entryPoints: ["kilo-shiki-worker"],
|
||||
bundle: true,
|
||||
format: "iife",
|
||||
minify: production,
|
||||
sourcemap: !production,
|
||||
sourcesContent: false,
|
||||
platform: "browser",
|
||||
outfile: "dist/shiki-worker.js",
|
||||
logLevel: "silent",
|
||||
plugins: [shikiWorkerEntryPlugin, esbuildProblemMatcherPlugin],
|
||||
})
|
||||
}
|
||||
|
||||
async function main() {
|
||||
// Build extension
|
||||
const extensionCtx = await esbuild.context({
|
||||
@@ -200,6 +218,9 @@ async function main() {
|
||||
// Build webview
|
||||
const webviewCtx = await createBrowserWebviewContext("webview-ui/src/index.tsx", "dist/webview.js")
|
||||
|
||||
// Build the shared Shiki highlighting worker asset
|
||||
const shikiWorkerCtx = await createShikiWorkerContext()
|
||||
|
||||
if (watch) {
|
||||
await Promise.all([
|
||||
extensionCtx.watch(),
|
||||
@@ -208,6 +229,7 @@ async function main() {
|
||||
diffViewerCtx.watch(),
|
||||
diffVirtualCtx.watch(),
|
||||
kiloClawCtx.watch(),
|
||||
shikiWorkerCtx.watch(),
|
||||
])
|
||||
} else {
|
||||
await Promise.all([
|
||||
@@ -217,14 +239,16 @@ async function main() {
|
||||
kiloClawCtx.rebuild(),
|
||||
diffViewerCtx.rebuild(),
|
||||
diffVirtualCtx.rebuild(),
|
||||
shikiWorkerCtx.rebuild(),
|
||||
])
|
||||
await Promise.all([
|
||||
extensionCtx.dispose(),
|
||||
webviewCtx.dispose(),
|
||||
agentManagerCtx.dispose(),
|
||||
kiloClawCtx.dispose(),
|
||||
diffViewerCtx.dispose(),
|
||||
diffVirtualCtx.dispose(),
|
||||
kiloClawCtx.dispose(),
|
||||
shikiWorkerCtx.dispose(),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"webview-ui/diff-viewer/index.tsx",
|
||||
"webview-ui/diff-virtual/index.tsx",
|
||||
"webview-ui/kiloclaw/index.tsx",
|
||||
"webview-ui/pierre-worker.ts",
|
||||
"webview-ui/src/index.tsx",
|
||||
"src/**/__tests__/**/*.{ts,spec.ts}",
|
||||
"src/**/*.test.ts",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "kilo-code",
|
||||
"displayName": "Kilo Code: AI Coding Agent, Copilot, and Autocomplete",
|
||||
"description": "Open Source AI coding agent that generates code from natural language, automates tasks, and runs terminal commands. Features inline autocomplete, browser automation, automated refactoring, and custom modes for planning, coding, and debugging. Supports 500+ AI models including Claude (Anthropic), Gemini, Grok, GPT, Codex and GLM.",
|
||||
"version": "7.3.17",
|
||||
"version": "7.3.18",
|
||||
"icon": "assets/icons/logo-outline-black.png",
|
||||
"galleryBanner": {
|
||||
"color": "#FFFFFF",
|
||||
|
||||
@@ -113,6 +113,7 @@ export class DiffVirtualProvider implements vscode.Disposable {
|
||||
scriptUri: webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, "dist", "diff-virtual.js")),
|
||||
styleUri: webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, "dist", "diff-virtual.css")),
|
||||
iconsBaseUri: webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, "assets", "icons")),
|
||||
workerUri: webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, "dist", "shiki-worker.js")),
|
||||
title: "Diff Virtual",
|
||||
extraStyles: "#root { display: flex; flex-direction: column; height: 100%; }",
|
||||
})
|
||||
|
||||
@@ -3494,6 +3494,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
scriptUri: webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, "dist", "webview.js")),
|
||||
styleUri: webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, "dist", "webview.css")),
|
||||
iconsBaseUri: webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, "assets", "icons")),
|
||||
workerUri: webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, "dist", "shiki-worker.js")),
|
||||
title: "Kilo Code",
|
||||
port: this.connectionService.getServerInfo()?.port,
|
||||
extraStyles: `.container { height: 100%; display: flex; flex-direction: column; height: 100vh; border-right: 1px solid var(--border-weak-base); }`,
|
||||
|
||||
@@ -84,6 +84,7 @@ export class VscodeHost implements Host {
|
||||
scriptUri: panel.webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, "dist", "agent-manager.js")),
|
||||
styleUri: panel.webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, "dist", "agent-manager.css")),
|
||||
iconsBaseUri: panel.webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, "assets", "icons")),
|
||||
workerUri: panel.webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, "dist", "shiki-worker.js")),
|
||||
title: "Agent Manager",
|
||||
port,
|
||||
})
|
||||
|
||||
@@ -238,6 +238,7 @@ export class DiffViewerProvider implements vscode.Disposable {
|
||||
scriptUri: webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, "dist", "diff-viewer.js")),
|
||||
styleUri: webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, "dist", "diff-viewer.css")),
|
||||
iconsBaseUri: webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, "assets", "icons")),
|
||||
workerUri: webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, "dist", "shiki-worker.js")),
|
||||
title: "Changes",
|
||||
port: this.connection.getServerInfo()?.port,
|
||||
extraStyles: "#root { display: flex; flex-direction: column; }",
|
||||
|
||||
@@ -71,6 +71,7 @@ export function toSessionDiffFile(raw: SnapshotFileDiff): DiffFile {
|
||||
file: raw.file,
|
||||
before: view ? text(view, "deletions") : "",
|
||||
after: view ? text(view, "additions") : "",
|
||||
patch: raw.patch,
|
||||
additions: raw.additions,
|
||||
deletions: raw.deletions,
|
||||
status: raw.status,
|
||||
|
||||
@@ -90,11 +90,17 @@ export function createStagedDiffSource(): DiffSource {
|
||||
// For added: HEAD has no blob. For deleted: index has no blob.
|
||||
const before = entry.status === "added" ? "" : await showBlob(git, dir, "HEAD", file)
|
||||
const after = entry.status === "deleted" ? "" : await showBlob(git, dir, INDEX_REF, file)
|
||||
const result = await git.execGit(
|
||||
["-c", "core.quotepath=false", "diff", "--cached", "--no-ext-diff", "--no-renames", "HEAD", "--", file],
|
||||
dir,
|
||||
)
|
||||
const patch = result.code === 0 ? result.stdout : undefined
|
||||
const summarized = before === "" && after === "" && entry.status === "modified"
|
||||
return {
|
||||
file,
|
||||
before,
|
||||
after,
|
||||
patch,
|
||||
additions: entry.additions,
|
||||
deletions: entry.deletions,
|
||||
status: entry.status,
|
||||
|
||||
@@ -130,6 +130,10 @@ export function createUnstagedDiffSource(): DiffSource {
|
||||
// after = disk content (or "" for deleted).
|
||||
const before = !entry.tracked || entry.status === "added" ? "" : await showBlob(git, dir, INDEX_REF, file)
|
||||
const after = entry.status === "deleted" ? "" : await readDisk(dir, file)
|
||||
const result = entry.tracked
|
||||
? await git.execGit(["-c", "core.quotepath=false", "diff", "--no-ext-diff", "--no-renames", "--", file], dir)
|
||||
: undefined
|
||||
const patch = result?.code === 0 ? result.stdout : undefined
|
||||
const summarized = before === "" && after === "" && entry.status === "modified"
|
||||
|
||||
// For untracked added files numstat doesn't return counts, so backfill
|
||||
@@ -139,6 +143,7 @@ export function createUnstagedDiffSource(): DiffSource {
|
||||
file,
|
||||
before,
|
||||
after,
|
||||
patch,
|
||||
additions,
|
||||
deletions: entry.deletions,
|
||||
status: entry.status,
|
||||
|
||||
@@ -139,15 +139,16 @@ async function resolveOverrideRef(
|
||||
|
||||
/**
|
||||
* Project a `WorktreeDiffEntry` from `local-diff.ts` onto the `DiffFile` shape
|
||||
* expected by the diff viewer. Drops `patch` (the webview rebuilds before/after
|
||||
* for itself) and coerces optional `before`/`after` to empty strings when the
|
||||
* entry is summarized.
|
||||
* expected by the diff viewer. Preserve its hunk-bounded `patch` so Pierre can
|
||||
* parse the git diff directly rather than recomputing a diff from full source
|
||||
* contents; summarized entries still coerce optional content to empty strings.
|
||||
*/
|
||||
function toDiffFile(entry: WorktreeDiffEntry): DiffFile {
|
||||
return {
|
||||
file: entry.file,
|
||||
before: entry.before ?? "",
|
||||
after: entry.after ?? "",
|
||||
patch: entry.patch,
|
||||
additions: entry.additions,
|
||||
deletions: entry.deletions,
|
||||
status: entry.status,
|
||||
|
||||
@@ -17,6 +17,8 @@ export interface DiffFile {
|
||||
file: string
|
||||
before: string
|
||||
after: string
|
||||
/** Hunk-bounded unified patch used by Pierre to avoid re-diffing full files. */
|
||||
patch?: string
|
||||
additions: number
|
||||
deletions: number
|
||||
status?: "added" | "deleted" | "modified"
|
||||
|
||||
@@ -144,6 +144,7 @@ export class KiloClawProvider implements vscode.Disposable {
|
||||
scriptUri: panel.webview.asWebviewUri(vscode.Uri.joinPath(this.uri, "dist", "kiloclaw.js")),
|
||||
styleUri: panel.webview.asWebviewUri(vscode.Uri.joinPath(this.uri, "dist", "kiloclaw.css")),
|
||||
iconsBaseUri: panel.webview.asWebviewUri(vscode.Uri.joinPath(this.uri, "assets", "icons")),
|
||||
workerUri: panel.webview.asWebviewUri(vscode.Uri.joinPath(this.uri, "dist", "shiki-worker.js")),
|
||||
title: "KiloClaw",
|
||||
})
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ export function buildWebviewHtml(
|
||||
scriptUri: vscode.Uri
|
||||
styleUri: vscode.Uri
|
||||
iconsBaseUri: vscode.Uri
|
||||
workerUri: vscode.Uri
|
||||
title: string
|
||||
port?: number
|
||||
extraStyles?: string
|
||||
@@ -81,7 +82,7 @@ export function buildWebviewHtml(
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script nonce="${nonce}">window.ICONS_BASE_URI = "${opts.iconsBaseUri}";</script>
|
||||
<script nonce="${nonce}">window.ICONS_BASE_URI = "${opts.iconsBaseUri}"; window.KILO_SHIKI_WORKER_URI = "${opts.workerUri}";</script>
|
||||
<script nonce="${nonce}" src="${opts.scriptUri}"></script>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
@@ -26,6 +26,8 @@ export function buildCspString(cspSource: string, nonce: string, port?: number):
|
||||
"default-src 'none'",
|
||||
`style-src 'unsafe-inline' ${cspSource}`,
|
||||
`script-src 'nonce-${nonce}' 'wasm-unsafe-eval'`,
|
||||
// Allow the bundled Shiki highlighting worker (loaded as a webview resource).
|
||||
`worker-src ${cspSource}`,
|
||||
`font-src ${cspSource}`,
|
||||
`connect-src ${cspSource} ${connectSrc}`,
|
||||
`img-src ${cspSource} data: https:`,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"type": "module",
|
||||
"version": "7.3.17",
|
||||
"version": "7.3.18",
|
||||
"dependencies": {},
|
||||
"devDependencies": {},
|
||||
"peerDependencies": {}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { describe, expect, it } from "bun:test"
|
||||
import { mergeWorktreeDiffs } from "../../webview-ui/agent-manager/diff-state"
|
||||
import {
|
||||
EAGER_DIFF_REVIEW_LINES,
|
||||
EXTREME_DIFF_CHANGED_LINES,
|
||||
allOpenFiles,
|
||||
eagerDiffFiles,
|
||||
expandableOpenFiles,
|
||||
initialOpenFiles,
|
||||
toggleOpenFiles,
|
||||
@@ -26,16 +28,27 @@ function diff(overrides: Partial<WorktreeFileDiff>): WorktreeFileDiff {
|
||||
}
|
||||
|
||||
describe("agent manager diff state", () => {
|
||||
it("preserves loaded detail when summary metadata is unchanged", () => {
|
||||
const prev = [diff({ summarized: false, before: "old\n", after: "new\n" })]
|
||||
it("preserves loaded detail and patch when summary metadata is unchanged", () => {
|
||||
const prev = [diff({ summarized: false, before: "old\n", after: "new\n", patch: "@@ -1 +1 @@\n-old\n+new\n" })]
|
||||
const next = [diff({ summarized: true })]
|
||||
const result = mergeWorktreeDiffs(prev, next)
|
||||
|
||||
expect(result.diffs).toEqual([diff({ summarized: false, before: "old\n", after: "new\n" })])
|
||||
expect(result.diffs).toEqual([
|
||||
diff({ summarized: false, before: "old\n", after: "new\n", patch: "@@ -1 +1 @@\n-old\n+new\n" }),
|
||||
])
|
||||
expect(result.diffs[0]).toBe(prev[0])
|
||||
expect(result.stale.size).toBe(0)
|
||||
})
|
||||
|
||||
it("replaces detailed content when patch anchors change", () => {
|
||||
const prev = [diff({ summarized: false, before: "old\n", after: "new\n", patch: "@@ -1 +1 @@\n-old\n+new\n" })]
|
||||
const next = [diff({ summarized: false, before: "old\n", after: "new\n", patch: "@@ -100 +100 @@\n-old\n+new\n" })]
|
||||
const result = mergeWorktreeDiffs(prev, next)
|
||||
|
||||
expect(result.diffs[0]).toBe(next[0])
|
||||
expect(result.diffs[0]?.patch).toContain("@@ -100 +100 @@")
|
||||
})
|
||||
|
||||
it("preserves cached content and marks stale when summary metadata changes", () => {
|
||||
const prev = [diff({ summarized: false, before: "old\n", after: "new\n", additions: 1 })]
|
||||
const next = [diff({ summarized: true, additions: 2 })]
|
||||
@@ -97,3 +110,46 @@ describe("agent manager diff state", () => {
|
||||
expect(toggleOpenFiles(diffs, ["src/app.ts", "src/panel.ts"])).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe("eager diff files", () => {
|
||||
it("renders hunk-bounded detailed patches eagerly", () => {
|
||||
const diffs = [
|
||||
diff({ file: "src/a.ts", patch: "@@ -1 +1 @@\n-a\n+b\n", additions: 10, deletions: 5 }),
|
||||
diff({ file: "src/b.ts", patch: "@@ -1 +1 @@\n-a\n+b\n", additions: 3, deletions: 0 }),
|
||||
]
|
||||
expect(eagerDiffFiles(diffs)).toEqual(new Set(["src/a.ts", "src/b.ts"]))
|
||||
})
|
||||
|
||||
it("virtualizes a full-content detail without a hunk-bounded patch", () => {
|
||||
const diffs = [diff({ file: "src/large-source.ts", before: "a\n".repeat(4000), after: "b\n", additions: 1 })]
|
||||
expect(eagerDiffFiles(diffs)).toEqual(new Set())
|
||||
})
|
||||
|
||||
it("virtualizes files larger than the large-file threshold", () => {
|
||||
const diffs = [
|
||||
diff({ file: "src/big.ts", patch: "large", additions: EXTREME_DIFF_CHANGED_LINES + 1, deletions: 0 }),
|
||||
diff({ file: "src/small.ts", patch: "small", additions: 5, deletions: 0 }),
|
||||
]
|
||||
expect(eagerDiffFiles(diffs)).toEqual(new Set(["src/small.ts"]))
|
||||
})
|
||||
|
||||
it("stops rendering eagerly once the review budget is exhausted", () => {
|
||||
// Each file is under the large-file threshold, but together they exceed the
|
||||
// aggregate budget, so the overflow falls back to virtualization.
|
||||
const diffs = [
|
||||
diff({ file: "src/a.ts", patch: "a", additions: 2000, deletions: 0 }),
|
||||
diff({ file: "src/b.ts", patch: "b", additions: 2000, deletions: 0 }),
|
||||
diff({ file: "src/c.ts", patch: "c", additions: 2000, deletions: 0 }),
|
||||
diff({ file: "src/d.ts", patch: "d", additions: EAGER_DIFF_REVIEW_LINES - 6005, deletions: 0 }),
|
||||
diff({ file: "src/e.ts", patch: "e", additions: 2000, deletions: 0 }),
|
||||
diff({ file: "src/f.ts", patch: "f", additions: 5, deletions: 0 }),
|
||||
]
|
||||
const eager = eagerDiffFiles(diffs)
|
||||
expect(eager.has("src/a.ts")).toBe(true)
|
||||
expect(eager.has("src/d.ts")).toBe(true)
|
||||
// Budget exhausted, so the next sizeable file virtualizes.
|
||||
expect(eager.has("src/e.ts")).toBe(false)
|
||||
// A smaller later file still fits within the remaining budget.
|
||||
expect(eager.has("src/f.ts")).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -69,6 +69,7 @@ describe("createSessionDiffSource.fetch", () => {
|
||||
expect(foo.file).toBe("foo.ts")
|
||||
expect(foo.before).toBe("keep\nold\n")
|
||||
expect(foo.after).toBe("keep\nnew\n")
|
||||
expect(foo.patch).toBe(modifiedPatch)
|
||||
expect(foo.additions).toBe(1)
|
||||
expect(foo.deletions).toBe(1)
|
||||
expect(foo.status).toBe("modified")
|
||||
|
||||
@@ -48,6 +48,12 @@ describe("buildCspString", () => {
|
||||
expect(result).toContain(`font-src ${cspSource}`)
|
||||
})
|
||||
|
||||
it("allows only webview resources for the Shiki worker", () => {
|
||||
const result = buildCspString(cspSource, nonce)
|
||||
expect(result).toContain(`worker-src ${cspSource}`)
|
||||
expect(result).not.toContain(`worker-src ${cspSource} blob:`)
|
||||
})
|
||||
|
||||
it("includes cspSource and https: in img-src", () => {
|
||||
const result = buildCspString(cspSource, nonce)
|
||||
expect(result).toContain("img-src")
|
||||
|
||||
@@ -33,6 +33,7 @@ import { createReviewAnnotationSpeechRenderer } from "./review-annotation-speech
|
||||
import {
|
||||
LONG_DIFF_MARKER_FILE_COUNT,
|
||||
allOpenFiles,
|
||||
eagerDiffFiles,
|
||||
initialOpenFiles,
|
||||
isLargeDiffFile,
|
||||
toggleOpenFiles,
|
||||
@@ -119,6 +120,7 @@ export const DiffPanel: Component<DiffPanelProps> = (props) => {
|
||||
// Reorder diffs to match the file-tree's depth-first visual order so
|
||||
// scrolling through the accordion matches the tree grouping.
|
||||
const sorted = createMemo(() => treeOrder(props.diffs))
|
||||
const eager = createMemo(() => eagerDiffFiles(sorted()))
|
||||
|
||||
const comments = () => props.comments
|
||||
const setComments = (next: ReviewComment[]) => props.onCommentsChange(next)
|
||||
@@ -611,7 +613,9 @@ export const DiffPanel: Component<DiffPanelProps> = (props) => {
|
||||
<Diff<AnnotationMeta>
|
||||
before={{ name: diff.file, contents: diff.before }}
|
||||
after={{ name: diff.file, contents: diff.after }}
|
||||
patch={diff.patch}
|
||||
diffStyle={props.diffStyle ?? "unified"}
|
||||
virtualized={!eager().has(diff.file)}
|
||||
annotations={annotationsForFile(diff.file)}
|
||||
renderAnnotation={buildAnnotation}
|
||||
enableGutterUtility={true}
|
||||
|
||||
@@ -41,6 +41,7 @@ import { createReviewAnnotationSpeechRenderer } from "./review-annotation-speech
|
||||
import {
|
||||
LONG_DIFF_MARKER_FILE_COUNT,
|
||||
allOpenFiles,
|
||||
eagerDiffFiles,
|
||||
initialOpenFiles,
|
||||
isLargeDiffFile,
|
||||
toggleOpenFiles,
|
||||
@@ -135,6 +136,7 @@ export const FullScreenDiffView: Component<FullScreenDiffViewProps> = (props) =>
|
||||
// Reorder diffs to match the file-tree's depth-first visual order so
|
||||
// scrolling through the diff panel matches the tree on the left.
|
||||
const sorted = createMemo(() => treeOrder(props.diffs))
|
||||
const eager = createMemo(() => eagerDiffFiles(sorted()))
|
||||
|
||||
const comments = () => props.comments
|
||||
const setComments = (next: ReviewComment[]) => props.onCommentsChange(next)
|
||||
@@ -708,7 +710,9 @@ export const FullScreenDiffView: Component<FullScreenDiffViewProps> = (props) =>
|
||||
<Diff<AnnotationMeta>
|
||||
before={{ name: diff.file, contents: diff.before }}
|
||||
after={{ name: diff.file, contents: diff.after }}
|
||||
patch={diff.patch}
|
||||
diffStyle={props.diffStyle}
|
||||
virtualized={!eager().has(diff.file)}
|
||||
annotations={annotationsForFile(diff.file)}
|
||||
renderAnnotation={buildAnnotation}
|
||||
enableGutterUtility={props.canComment !== false}
|
||||
|
||||
@@ -2,11 +2,32 @@ import type { WorktreeFileDiff } from "../src/types/messages"
|
||||
|
||||
export const LONG_DIFF_MARKER_FILE_COUNT = 50
|
||||
export const EXTREME_DIFF_CHANGED_LINES = 2_000
|
||||
// Total changed rows rendered eagerly (non-virtualized) across one review before
|
||||
// the remainder falls back to virtualization. Bounds eager DOM for huge reviews
|
||||
// while letting normal reviews render fully so scrolling never shows gap buffers.
|
||||
export const EAGER_DIFF_REVIEW_LINES = 8_000
|
||||
|
||||
export function isLargeDiffFile(diff: WorktreeFileDiff): boolean {
|
||||
return diff.additions + diff.deletions > EXTREME_DIFF_CHANGED_LINES
|
||||
}
|
||||
|
||||
// Files whose hunk-bounded patches should render eagerly (no row virtualization)
|
||||
// so their rows stay mounted and never re-render on scroll. Never eager-render a
|
||||
// detail without a patch: a tiny change in a very large source file would make
|
||||
// Pierre re-diff and render full before/after contents on the main thread.
|
||||
export function eagerDiffFiles(diffs: WorktreeFileDiff[]): Set<string> {
|
||||
const eager = new Set<string>()
|
||||
let used = 0
|
||||
for (const diff of diffs) {
|
||||
if (!diff.patch || isLargeDiffFile(diff)) continue
|
||||
const size = diff.additions + diff.deletions
|
||||
if (used + size > EAGER_DIFF_REVIEW_LINES) continue
|
||||
used += size
|
||||
eager.add(diff.file)
|
||||
}
|
||||
return eager
|
||||
}
|
||||
|
||||
export function expandableOpenFiles(diffs: WorktreeFileDiff[]): string[] {
|
||||
return diffs.filter((diff) => !isLargeDiffFile(diff) && diff.generatedLike !== true).map((diff) => diff.file)
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ export function mergeWorktreeDiffs(prev: WorktreeFileDiff[], next: WorktreeFileD
|
||||
existing.file === diff.file &&
|
||||
existing.before === diff.before &&
|
||||
existing.after === diff.after &&
|
||||
existing.patch === diff.patch &&
|
||||
sameDiffMeta(existing, diff)
|
||||
)
|
||||
return existing
|
||||
@@ -45,8 +46,19 @@ export function mergeWorktreeDiffs(prev: WorktreeFileDiff[], next: WorktreeFileD
|
||||
if (!diff.summarized) return diff
|
||||
// Metadata matches — restore cached content as before.
|
||||
if (sameDiffMeta({ ...existing, summarized: true }, diff)) {
|
||||
const merged = { ...diff, before: existing.before, after: existing.after, summarized: false }
|
||||
if (existing.before === merged.before && existing.after === merged.after && sameDiffMeta(existing, merged))
|
||||
const merged = {
|
||||
...diff,
|
||||
before: existing.before,
|
||||
after: existing.after,
|
||||
patch: existing.patch,
|
||||
summarized: false,
|
||||
}
|
||||
if (
|
||||
existing.before === merged.before &&
|
||||
existing.after === merged.after &&
|
||||
existing.patch === merged.patch &&
|
||||
sameDiffMeta(existing, merged)
|
||||
)
|
||||
return existing
|
||||
return merged
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
// Kilo-specific replacement for the shared `@opencode-ai/ui/pierre/worker`
|
||||
// module (wired up by the pierre-worker-alias plugin in esbuild.js).
|
||||
//
|
||||
// The upstream module loads Pierre's Shiki worker through a Vite-only
|
||||
// `?worker&url` import, which esbuild cannot resolve and which the VS Code
|
||||
// webview CSP blocks. As a result the webview previously ran syntax
|
||||
// highlighting synchronously on the main thread (`findNextMatchSync`), which
|
||||
// froze scrolling on large diffs.
|
||||
//
|
||||
// Here we instead load the worker from a real dist asset (`dist/shiki-worker.js`,
|
||||
// also produced by esbuild.js) using the webview URI the extension injects into
|
||||
// the page. Pierre can offload highlighted updates to the pool after its initial
|
||||
// plain render. The diff wrapper still needs to keep that initial render cheap,
|
||||
// which is why review surfaces pass hunk-bounded patches instead of full files.
|
||||
import { WorkerPoolManager } from "@pierre/diffs/worker"
|
||||
|
||||
export type WorkerPoolStyle = "unified" | "split"
|
||||
|
||||
// Oniguruma WebAssembly engine (matches the highlighting quality of the rest of
|
||||
// the app). The CSP already allows `wasm-unsafe-eval`, and if the worker fails to
|
||||
// instantiate, WorkerPoolManager falls back to the main-thread highlighter. To
|
||||
// avoid WebAssembly entirely, build the portable worker and use "shiki-js".
|
||||
const ENGINE = "shiki-wasm"
|
||||
|
||||
function uri(): string | undefined {
|
||||
if (typeof window === "undefined") return undefined
|
||||
return (window as { KILO_SHIKI_WORKER_URI?: string }).KILO_SHIKI_WORKER_URI
|
||||
}
|
||||
|
||||
export function workerFactory(): Worker {
|
||||
const url = uri()
|
||||
if (!url) throw new Error("KILO_SHIKI_WORKER_URI is not set")
|
||||
return new Worker(url)
|
||||
}
|
||||
|
||||
function createPool(lineDiffType: "none" | "word-alt") {
|
||||
const pool = new WorkerPoolManager(
|
||||
{ workerFactory, poolSize: 2 },
|
||||
{ theme: "Kilo", lineDiffType, preferredHighlighter: ENGINE },
|
||||
)
|
||||
void pool.initialize()
|
||||
return pool
|
||||
}
|
||||
|
||||
let unified: WorkerPoolManager | undefined
|
||||
let split: WorkerPoolManager | undefined
|
||||
|
||||
export function getWorkerPool(style: WorkerPoolStyle | undefined): WorkerPoolManager | undefined {
|
||||
// No injected worker URI means we can't spawn the worker; returning undefined
|
||||
// makes Pierre fall back to the existing main-thread highlighter.
|
||||
if (!uri()) return undefined
|
||||
|
||||
if (style === "split") {
|
||||
if (!split) split = createPool("word-alt")
|
||||
return split
|
||||
}
|
||||
|
||||
if (!unified) unified = createPool("none")
|
||||
return unified
|
||||
}
|
||||
|
||||
export function getWorkerPools() {
|
||||
return {
|
||||
unified: getWorkerPool("unified"),
|
||||
split: getWorkerPool("split"),
|
||||
}
|
||||
}
|
||||
@@ -123,6 +123,8 @@ export interface WorktreeFileDiff {
|
||||
file: string
|
||||
before: string
|
||||
after: string
|
||||
/** Hunk-bounded unified patch used by Pierre to avoid re-diffing full files. */
|
||||
patch?: string
|
||||
additions: number
|
||||
deletions: number
|
||||
status?: "added" | "deleted" | "modified"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "7.3.17",
|
||||
"version": "7.3.18",
|
||||
"name": "@kilocode/cli",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
// kilocode_change - new file
|
||||
import { remapChildren as _remapChildren } from "./fork"
|
||||
import z from "zod"
|
||||
import { Effect, Schema } from "effect"
|
||||
import { Cause, Effect, Schema } from "effect"
|
||||
import { BusEvent } from "@/bus/bus-event"
|
||||
import { EffectBridge } from "@/effect/bridge"
|
||||
import { Session } from "@/session/session"
|
||||
import { MessageID, SessionID } from "@/session/schema"
|
||||
import { fn } from "@/util/fn"
|
||||
@@ -230,14 +231,18 @@ export namespace KiloSession {
|
||||
// Session lifecycle hooks (share, unshare, remove)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export async function shareSession(id: string): Promise<{ url: string }> {
|
||||
const { KiloSessions } = await import("@/kilo-sessions/kilo-sessions")
|
||||
return KiloSessions.share(id)
|
||||
export function shareSession(id: SessionID) {
|
||||
return EffectBridge.fromPromise(async () => {
|
||||
const { KiloSessions } = await import("@/kilo-sessions/kilo-sessions")
|
||||
return KiloSessions.share(id)
|
||||
}).pipe(Effect.catchCause((cause) => Effect.fail(Cause.squash(cause))))
|
||||
}
|
||||
|
||||
export async function unshareSession(id: string): Promise<void> {
|
||||
const { KiloSessions } = await import("@/kilo-sessions/kilo-sessions")
|
||||
await KiloSessions.unshare(id)
|
||||
export function unshareSession(id: SessionID) {
|
||||
return EffectBridge.fromPromise(async () => {
|
||||
const { KiloSessions } = await import("@/kilo-sessions/kilo-sessions")
|
||||
await KiloSessions.unshare(id)
|
||||
}).pipe(Effect.catchCause((cause) => Effect.fail(Cause.squash(cause))))
|
||||
}
|
||||
|
||||
export async function removeSession(id: string): Promise<void> {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { SyncEvent } from "@/sync"
|
||||
import { Effect, Layer, Scope, Context } from "effect"
|
||||
import { Config } from "@/config/config"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import * as ShareNext from "./share-next"
|
||||
import { KiloSession } from "@/kilocode/session" // kilocode_change
|
||||
|
||||
export interface Interface {
|
||||
readonly create: (input?: Session.CreateInput) => Effect.Effect<Session.Info>
|
||||
@@ -19,20 +19,19 @@ export const layer = Layer.effect(
|
||||
Effect.gen(function* () {
|
||||
const cfg = yield* Config.Service
|
||||
const session = yield* Session.Service
|
||||
const shareNext = yield* ShareNext.Service
|
||||
const scope = yield* Scope.Scope
|
||||
const sync = yield* SyncEvent.Service
|
||||
|
||||
const share = Effect.fn("SessionShare.share")(function* (sessionID: SessionID) {
|
||||
const conf = yield* cfg.get()
|
||||
if (conf.share === "disabled") throw new Error("Sharing is disabled in configuration")
|
||||
const result = yield* shareNext.create(sessionID)
|
||||
const result = yield* KiloSession.shareSession(sessionID) // kilocode_change - use Kilo public share URLs
|
||||
yield* sync.run(Session.Event.Updated, { sessionID, info: { share: { url: result.url } } })
|
||||
return result
|
||||
})
|
||||
|
||||
const unshare = Effect.fn("SessionShare.unshare")(function* (sessionID: SessionID) {
|
||||
yield* shareNext.remove(sessionID)
|
||||
yield* KiloSession.unshareSession(sessionID) // kilocode_change - use Kilo public share URLs
|
||||
yield* sync.run(Session.Event.Updated, { sessionID, info: { share: { url: null } } })
|
||||
})
|
||||
|
||||
@@ -50,7 +49,6 @@ export const layer = Layer.effect(
|
||||
)
|
||||
|
||||
export const defaultLayer = layer.pipe(
|
||||
Layer.provide(ShareNext.defaultLayer),
|
||||
Layer.provide(Session.defaultLayer),
|
||||
Layer.provide(Config.defaultLayer),
|
||||
Layer.provide(SyncEvent.defaultLayer),
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { expect, spyOn } from "bun:test"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||
import { Auth } from "../../src/auth"
|
||||
import { Config } from "../../src/config/config"
|
||||
import { Session } from "../../src/session/session"
|
||||
import { SessionShare } from "../../src/share/session"
|
||||
import { Storage } from "../../src/storage/storage"
|
||||
import { SyncEvent } from "../../src/sync"
|
||||
import { testEffect } from "../lib/effect"
|
||||
|
||||
const it = testEffect(Layer.mergeAll(Auth.defaultLayer, Storage.defaultLayer, CrossSpawnSpawner.defaultLayer))
|
||||
|
||||
const layer = SessionShare.layer.pipe(
|
||||
Layer.provideMerge(Session.defaultLayer),
|
||||
Layer.provide(Config.defaultLayer),
|
||||
Layer.provide(SyncEvent.defaultLayer),
|
||||
)
|
||||
|
||||
it.instance("shares and unshares sessions through Kilo public URLs", () => {
|
||||
const urls: string[] = []
|
||||
const fetch: typeof globalThis.fetch = Object.assign(
|
||||
async (input: RequestInfo | URL) => {
|
||||
const url = String(input)
|
||||
urls.push(url)
|
||||
if (url.endsWith("/api/user")) return new Response("{}", { status: 200 })
|
||||
if (url.endsWith("/share")) return Response.json({ public_id: "public-1" })
|
||||
if (url.endsWith("/unshare")) return new Response(null, { status: 200 })
|
||||
return new Response("{}", { status: 200 })
|
||||
},
|
||||
{ preconnect: globalThis.fetch.preconnect },
|
||||
)
|
||||
const request = spyOn(globalThis, "fetch").mockImplementation(fetch)
|
||||
|
||||
return Effect.gen(function* () {
|
||||
const auth = yield* Auth.Service
|
||||
const share = yield* SessionShare.Service
|
||||
const session = yield* Session.Service
|
||||
const storage = yield* Storage.Service
|
||||
yield* auth.set("kilo", { type: "api", key: "test-token" })
|
||||
|
||||
const info = yield* share.create({ title: "share-test" })
|
||||
yield* storage.write(["session_share", info.id], { id: "remote-1", ingestPath: "/api/ingest/session-1" })
|
||||
|
||||
const result = yield* share.share(info.id)
|
||||
expect(result.url).toBe("https://app.kilo.ai/s/public-1")
|
||||
expect((yield* session.get(info.id)).share?.url).toBe("https://app.kilo.ai/s/public-1")
|
||||
|
||||
yield* share.unshare(info.id)
|
||||
expect((yield* session.get(info.id)).share).toBeUndefined()
|
||||
expect(urls.some((url) => url.endsWith(`/api/session/${info.id}/share`))).toBe(true)
|
||||
expect(urls.some((url) => url.endsWith(`/api/session/${info.id}/unshare`))).toBe(true)
|
||||
}).pipe(
|
||||
Effect.ensuring(
|
||||
Effect.gen(function* () {
|
||||
const auth = yield* Auth.Service
|
||||
yield* auth.remove("kilo").pipe(Effect.ignore)
|
||||
request.mockRestore()
|
||||
}),
|
||||
),
|
||||
Effect.provide(layer),
|
||||
)
|
||||
})
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@kilocode/plugin",
|
||||
"version": "7.3.17",
|
||||
"version": "7.3.18",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -12,6 +12,6 @@
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"version": "7.3.17",
|
||||
"version": "7.3.18",
|
||||
"peerDependencies": {}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@kilocode/sdk",
|
||||
"version": "7.3.17",
|
||||
"version": "7.3.18",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
"typescript": "catalog:",
|
||||
"vite": "catalog:"
|
||||
},
|
||||
"version": "7.3.17",
|
||||
"version": "7.3.18",
|
||||
"dependencies": {},
|
||||
"peerDependencies": {}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "7.3.17",
|
||||
"version": "7.3.18",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"exports": {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { $ } from "bun"
|
||||
import semver from "semver"
|
||||
import { parseArgs } from "util"
|
||||
|
||||
const pkgfile = new URL("../packages/kilo-jetbrains/package.json", import.meta.url).pathname
|
||||
const props = new URL("../packages/kilo-jetbrains/gradle.properties", import.meta.url).pathname
|
||||
const log = new URL("../packages/kilo-jetbrains/CHANGELOG.md", import.meta.url).pathname
|
||||
const repo = process.env.GH_REPO ?? process.env.GITHUB_REPOSITORY ?? "Kilo-Org/kilocode"
|
||||
|
||||
@@ -67,9 +67,9 @@ if (dry) {
|
||||
}
|
||||
|
||||
await $`git checkout -B ${branch} ${sha}`
|
||||
await writepkg(ver)
|
||||
await writeprops(ver)
|
||||
await writelog(ver, entry)
|
||||
await $`git add packages/kilo-jetbrains/package.json packages/kilo-jetbrains/CHANGELOG.md`
|
||||
await $`git add packages/kilo-jetbrains/gradle.properties packages/kilo-jetbrains/CHANGELOG.md`
|
||||
|
||||
const changed = await $`git diff --cached --quiet`.nothrow()
|
||||
if (changed.exitCode !== 0) await $`git commit -m ${`release(jetbrains): v${ver}`}`
|
||||
@@ -203,10 +203,13 @@ function entries(notes: string) {
|
||||
return groups
|
||||
}
|
||||
|
||||
async function writepkg(ver: string) {
|
||||
const pkg = await Bun.file(pkgfile).json()
|
||||
pkg.version = ver
|
||||
await Bun.write(pkgfile, `${JSON.stringify(pkg, null, 2)}\n`)
|
||||
async function writeprops(ver: string) {
|
||||
const current = await Bun.file(props).text()
|
||||
const line = `kilo.jetbrains.version=${ver}`
|
||||
const next = current.match(/^kilo\.jetbrains\.version=/m)
|
||||
? current.replace(/^kilo\.jetbrains\.version=.*$/m, line)
|
||||
: `${current.trim()}\n${line}\n`
|
||||
await Bun.write(props, next.endsWith("\n") ? next : `${next}\n`)
|
||||
}
|
||||
|
||||
async function writelog(ver: string, entry: string) {
|
||||
@@ -229,7 +232,7 @@ function regex(ver: string) {
|
||||
function body(ver: string, kind: string, from: string, tag: string, sha: string, notes: string) {
|
||||
return `## Summary
|
||||
- Prepare JetBrains ${kind} release ${ver}.
|
||||
- Review and edit \`packages/kilo-jetbrains/CHANGELOG.md\` before merging.
|
||||
- Review \`packages/kilo-jetbrains/gradle.properties\` and edit \`packages/kilo-jetbrains/CHANGELOG.md\` before merging.
|
||||
|
||||
JetBrains-Version: ${ver}
|
||||
JetBrains-Kind: ${kind}
|
||||
|
||||
@@ -69,8 +69,8 @@ if (existing.exitCode !== 0) throw new Error(`${tag} does not exist`)
|
||||
const sha = (await $`git rev-list -n 1 ${tag}`.text()).trim()
|
||||
if (sha !== commit) throw new Error(`${tag} points at ${sha}, expected ${commit}`)
|
||||
|
||||
const pkg = await Bun.file("packages/kilo-jetbrains/package.json").json()
|
||||
if (pkg.version !== ver) throw new Error(`packages/kilo-jetbrains/package.json version is ${pkg.version}, expected ${ver}`)
|
||||
const prop = await props()
|
||||
if (prop !== ver) throw new Error(`packages/kilo-jetbrains/gradle.properties kilo.jetbrains.version is ${prop}, expected ${ver}`)
|
||||
|
||||
const changelog = await Bun.file("packages/kilo-jetbrains/CHANGELOG.md").text()
|
||||
if (!changelog.includes(`## [${ver}]`)) throw new Error(`CHANGELOG.md is missing section for ${ver}`)
|
||||
@@ -102,3 +102,11 @@ function need(body: string, key: string) {
|
||||
if (!value) throw new Error(`PR body is missing ${key}`)
|
||||
return value
|
||||
}
|
||||
|
||||
async function props() {
|
||||
const text = await Bun.file("packages/kilo-jetbrains/gradle.properties").text()
|
||||
const line = text.split(/\r?\n/).find((item) => item.startsWith("kilo.jetbrains.version="))
|
||||
const value = line?.split("=", 2)[1]?.trim()
|
||||
if (!value) throw new Error("packages/kilo-jetbrains/gradle.properties is missing kilo.jetbrains.version")
|
||||
return value
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@kilocode/upstream-merge",
|
||||
"version": "7.3.17",
|
||||
"version": "7.3.18",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "Scripts for automating upstream opencode merges into Kilo",
|
||||
|
||||
Reference in New Issue
Block a user