From c54a5b370c5462a55ca859325f31bd254b643f7a Mon Sep 17 00:00:00 2001 From: "kilo-maintainer[bot]" Date: Fri, 29 May 2026 01:10:57 +0000 Subject: [PATCH 01/19] release(jetbrains): v7.0.1-rc.4 --- packages/kilo-jetbrains/CHANGELOG.md | 9 +++++++++ packages/kilo-jetbrains/package.json | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/kilo-jetbrains/CHANGELOG.md b/packages/kilo-jetbrains/CHANGELOG.md index 6e145106e57..bc5ab10de04 100644 --- a/packages/kilo-jetbrains/CHANGELOG.md +++ b/packages/kilo-jetbrains/CHANGELOG.md @@ -2,6 +2,15 @@ ## [Unreleased] +## [7.0.1-rc.4] - 2026-05-29 + +### Added +- feat: add Italian language support by @ale-saglia in https://github.com/Kilo-Org/kilocode/pull/9796 + +### Changed +- ci(jetbrains): lock release source before reviewed publish by @kirillk in https://github.com/Kilo-Org/kilocode/pull/10627 + + ## [0.0.0-dev] ### Added diff --git a/packages/kilo-jetbrains/package.json b/packages/kilo-jetbrains/package.json index c7261a02272..01f1472a7a0 100644 --- a/packages/kilo-jetbrains/package.json +++ b/packages/kilo-jetbrains/package.json @@ -8,7 +8,7 @@ "test": "./gradlew test", "test:ci": "bun script/test-ci.ts" }, - "version": "7.3.16", + "version": "7.0.1-rc.4", "dependencies": {}, "devDependencies": {}, "peerDependencies": {} From b28d5905d54f0e5c6ab5c62d057611f8fb5e8cee Mon Sep 17 00:00:00 2001 From: Kirill Kalishev Date: Thu, 28 May 2026 23:27:03 -0400 Subject: [PATCH 02/19] Update CHANGELOG.md --- packages/kilo-jetbrains/CHANGELOG.md | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/packages/kilo-jetbrains/CHANGELOG.md b/packages/kilo-jetbrains/CHANGELOG.md index bc5ab10de04..b4fadf8106d 100644 --- a/packages/kilo-jetbrains/CHANGELOG.md +++ b/packages/kilo-jetbrains/CHANGELOG.md @@ -1,17 +1,11 @@ # Changelog -## [Unreleased] - ## [7.0.1-rc.4] - 2026-05-29 ### Added -- feat: add Italian language support by @ale-saglia in https://github.com/Kilo-Org/kilocode/pull/9796 - -### Changed -- ci(jetbrains): lock release source before reviewed publish by @kirillk in https://github.com/Kilo-Org/kilocode/pull/10627 - - -## [0.0.0-dev] - -### 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. From 51ce1b82b3374a9d573e3fe5ecbe19f4a22db9a4 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Fri, 29 May 2026 12:21:15 +0200 Subject: [PATCH 03/19] fix(vscode): offload diff highlighting to a worker and render review diffs eagerly Diff syntax highlighting ran synchronously on the webview main thread because the Shiki worker was stubbed out (the webview CSP blocked workers), so scrolling large diffs froze the UI. Bundle the Shiki worker as a dist asset, load it from an injected webview URI, and allow it via a scoped worker-src CSP directive so highlighting runs off the main thread. With highlighting off-thread, normal-sized review files now render their diffs eagerly instead of going through Pierre's per-instance virtualizer, which avoids the re-render-all storms that rebuilt thousands of rows on every height change. Large files and an aggregate per-review budget keep eager DOM bounded. --- .changeset/smooth-diff-worker-highlight.md | 5 ++ packages/kilo-ui/src/components/diff-ssr.tsx | 3 +- packages/kilo-ui/src/components/diff.tsx | 11 ++- packages/kilo-ui/src/pierre/index.ts | 5 ++ packages/kilo-vscode/esbuild.js | 90 ++++++++++++------- .../kilo-vscode/src/DiffVirtualProvider.ts | 1 + packages/kilo-vscode/src/KiloProvider.ts | 1 + .../src/agent-manager/vscode-host.ts | 1 + .../src/diff/DiffViewerProvider.ts | 1 + .../src/kiloclaw/KiloClawProvider.ts | 1 + packages/kilo-vscode/src/utils.ts | 3 +- .../kilo-vscode/src/webview-html-utils.ts | 2 + .../unit/agent-manager-diff-state.test.ts | 40 +++++++++ .../tests/unit/webview-html.test.ts | 5 ++ .../webview-ui/agent-manager/DiffPanel.tsx | 3 + .../agent-manager/FullScreenDiffView.tsx | 3 + .../agent-manager/diff-open-policy.ts | 20 +++++ .../kilo-vscode/webview-ui/pierre-worker.ts | 67 ++++++++++++++ 18 files changed, 226 insertions(+), 36 deletions(-) create mode 100644 .changeset/smooth-diff-worker-highlight.md create mode 100644 packages/kilo-vscode/webview-ui/pierre-worker.ts diff --git a/.changeset/smooth-diff-worker-highlight.md b/.changeset/smooth-diff-worker-highlight.md new file mode 100644 index 00000000000..5c3233e95a7 --- /dev/null +++ b/.changeset/smooth-diff-worker-highlight.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Fix laggy scrolling and gray flashes in the diff and Changes views. Syntax highlighting now runs in a web worker instead of blocking the main thread, and normal-sized review files render their diffs up front instead of re-rendering while scrolling. Scrolling large diffs stays smooth even when scrolling fast. diff --git a/packages/kilo-ui/src/components/diff-ssr.tsx b/packages/kilo-ui/src/components/diff-ssr.tsx index 84b862f8d78..4dd808b4b48 100644 --- a/packages/kilo-ui/src/components/diff-ssr.tsx +++ b/packages/kilo-ui/src/components/diff-ssr.tsx @@ -21,6 +21,7 @@ export function Diff(props: SSRDiffProps) { "annotations", "selectedLines", "commentedLines", + "virtualized", ]) const workerPool = useWorkerPool(props.diffStyle) @@ -227,7 +228,7 @@ export function Diff(props: SSRDiffProps) { onCleanup(() => monitor.disconnect()) } - const virtualizer = getVirtualizer() + const virtualizer = local.virtualized === false ? undefined : getVirtualizer() fileDiffInstance = virtualizer ? new VirtualizedFileDiff( diff --git a/packages/kilo-ui/src/components/diff.tsx b/packages/kilo-ui/src/components/diff.tsx index febb70ca813..3834adde70d 100644 --- a/packages/kilo-ui/src/components/diff.tsx +++ b/packages/kilo-ui/src/components/diff.tsx @@ -163,6 +163,7 @@ export function Diff(props: DiffProps) { "selectedLines", "commentedLines", "onRendered", + "virtualized", ]) const mobile = createMediaQuery("(max-width: 640px)") @@ -681,7 +682,15 @@ export function Diff(props: DiffProps) { const opts = options() const workerPool = large() ? getWorkerPool("unified") : getWorkerPool(props.diffStyle) - const virtualizer = getVirtualizer() + // Eager (non-virtualized) diffs render once and never re-render on scroll or + // height changes, avoiding Pierre's re-render-all storms. Highlighting still + // runs in the worker, so the one-time row build stays cheap. Large diffs keep + // virtualizing to bound DOM size. + const virtualizer = local.virtualized === false ? undefined : getVirtualizer() + if (local.virtualized === false && sharedVirtualizer) { + sharedVirtualizer.release() + sharedVirtualizer = undefined + } const annotations = untrack(() => local.annotations) // Preserve container height during re-render to prevent scroll jumps. diff --git a/packages/kilo-ui/src/pierre/index.ts b/packages/kilo-ui/src/pierre/index.ts index 0d2edfeea93..1b9a5bc7f98 100644 --- a/packages/kilo-ui/src/pierre/index.ts +++ b/packages/kilo-ui/src/pierre/index.ts @@ -48,6 +48,11 @@ type DiffShared = FileDiffOptions & { commentedLines?: SelectedLineRange[] onLineNumberSelectionEnd?: (selection: SelectedLineRange | null) => void onRendered?: () => void + // When false, render the whole diff up front instead of row-virtualizing it. + // With highlighting offloaded to a worker, eager diffs render their rows once + // and never re-render on scroll, so fast scrolling never shows gap buffers. + // Defaults to virtualized. + virtualized?: boolean class?: string classList?: ComponentProps<"div">["classList"] } diff --git a/packages/kilo-vscode/esbuild.js b/packages/kilo-vscode/esbuild.js index 704953109de..a82df0e7d23 100644 --- a/packages/kilo-vscode/esbuild.js +++ b/packages/kilo-vscode/esbuild.js @@ -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(), ]) } } diff --git a/packages/kilo-vscode/src/DiffVirtualProvider.ts b/packages/kilo-vscode/src/DiffVirtualProvider.ts index 90a170559dc..721cb71c3fd 100644 --- a/packages/kilo-vscode/src/DiffVirtualProvider.ts +++ b/packages/kilo-vscode/src/DiffVirtualProvider.ts @@ -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%; }", }) diff --git a/packages/kilo-vscode/src/KiloProvider.ts b/packages/kilo-vscode/src/KiloProvider.ts index 8657c062cf2..7f72caf1221 100644 --- a/packages/kilo-vscode/src/KiloProvider.ts +++ b/packages/kilo-vscode/src/KiloProvider.ts @@ -3492,6 +3492,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); }`, diff --git a/packages/kilo-vscode/src/agent-manager/vscode-host.ts b/packages/kilo-vscode/src/agent-manager/vscode-host.ts index 94f28cba3b1..74b968870c1 100644 --- a/packages/kilo-vscode/src/agent-manager/vscode-host.ts +++ b/packages/kilo-vscode/src/agent-manager/vscode-host.ts @@ -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, }) diff --git a/packages/kilo-vscode/src/diff/DiffViewerProvider.ts b/packages/kilo-vscode/src/diff/DiffViewerProvider.ts index 476cdb92034..7f773cafcfa 100644 --- a/packages/kilo-vscode/src/diff/DiffViewerProvider.ts +++ b/packages/kilo-vscode/src/diff/DiffViewerProvider.ts @@ -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; }", diff --git a/packages/kilo-vscode/src/kiloclaw/KiloClawProvider.ts b/packages/kilo-vscode/src/kiloclaw/KiloClawProvider.ts index fb0c4beebfc..b6530ccc70a 100644 --- a/packages/kilo-vscode/src/kiloclaw/KiloClawProvider.ts +++ b/packages/kilo-vscode/src/kiloclaw/KiloClawProvider.ts @@ -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", }) diff --git a/packages/kilo-vscode/src/utils.ts b/packages/kilo-vscode/src/utils.ts index 1936575c222..87ff464e251 100644 --- a/packages/kilo-vscode/src/utils.ts +++ b/packages/kilo-vscode/src/utils.ts @@ -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(
- + ` diff --git a/packages/kilo-vscode/src/webview-html-utils.ts b/packages/kilo-vscode/src/webview-html-utils.ts index afca8884703..f9ce363bb8e 100644 --- a/packages/kilo-vscode/src/webview-html-utils.ts +++ b/packages/kilo-vscode/src/webview-html-utils.ts @@ -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} blob:`, `font-src ${cspSource}`, `connect-src ${cspSource} ${connectSrc}`, `img-src ${cspSource} data: https:`, diff --git a/packages/kilo-vscode/tests/unit/agent-manager-diff-state.test.ts b/packages/kilo-vscode/tests/unit/agent-manager-diff-state.test.ts index 5a12d4aaa39..a1d401dc1f9 100644 --- a/packages/kilo-vscode/tests/unit/agent-manager-diff-state.test.ts +++ b/packages/kilo-vscode/tests/unit/agent-manager-diff-state.test.ts @@ -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, @@ -97,3 +99,41 @@ describe("agent manager diff state", () => { expect(toggleOpenFiles(diffs, ["src/app.ts", "src/panel.ts"])).toEqual([]) }) }) + +describe("eager diff files", () => { + it("renders normal files eagerly", () => { + const diffs = [ + diff({ file: "src/a.ts", additions: 10, deletions: 5 }), + diff({ file: "src/b.ts", additions: 3, deletions: 0 }), + ] + expect(eagerDiffFiles(diffs)).toEqual(new Set(["src/a.ts", "src/b.ts"])) + }) + + it("virtualizes files larger than the large-file threshold", () => { + const diffs = [ + diff({ file: "src/big.ts", additions: EXTREME_DIFF_CHANGED_LINES + 1, deletions: 0 }), + diff({ file: "src/small.ts", 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", additions: 2000, deletions: 0 }), + diff({ file: "src/b.ts", additions: 2000, deletions: 0 }), + diff({ file: "src/c.ts", additions: 2000, deletions: 0 }), + diff({ file: "src/d.ts", additions: EAGER_DIFF_REVIEW_LINES - 6005, deletions: 0 }), + diff({ file: "src/e.ts", additions: 2000, deletions: 0 }), + diff({ file: "src/f.ts", 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) + }) +}) diff --git a/packages/kilo-vscode/tests/unit/webview-html.test.ts b/packages/kilo-vscode/tests/unit/webview-html.test.ts index 465588e7cde..b3c7e7c7d28 100644 --- a/packages/kilo-vscode/tests/unit/webview-html.test.ts +++ b/packages/kilo-vscode/tests/unit/webview-html.test.ts @@ -48,6 +48,11 @@ describe("buildCspString", () => { expect(result).toContain(`font-src ${cspSource}`) }) + it("allows the Shiki worker via worker-src", () => { + const result = buildCspString(cspSource, nonce) + expect(result).toContain(`worker-src ${cspSource} blob:`) + }) + it("includes cspSource and https: in img-src", () => { const result = buildCspString(cspSource, nonce) expect(result).toContain("img-src") diff --git a/packages/kilo-vscode/webview-ui/agent-manager/DiffPanel.tsx b/packages/kilo-vscode/webview-ui/agent-manager/DiffPanel.tsx index 5ecd82318f4..a65aaa8b20e 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/DiffPanel.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/DiffPanel.tsx @@ -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 = (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) @@ -612,6 +614,7 @@ export const DiffPanel: Component = (props) => { before={{ name: diff.file, contents: diff.before }} after={{ name: diff.file, contents: diff.after }} diffStyle={props.diffStyle ?? "unified"} + virtualized={!eager().has(diff.file)} annotations={annotationsForFile(diff.file)} renderAnnotation={buildAnnotation} enableGutterUtility={true} diff --git a/packages/kilo-vscode/webview-ui/agent-manager/FullScreenDiffView.tsx b/packages/kilo-vscode/webview-ui/agent-manager/FullScreenDiffView.tsx index 4dd0936b991..2b1fb225e5d 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/FullScreenDiffView.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/FullScreenDiffView.tsx @@ -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 = (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) @@ -709,6 +711,7 @@ export const FullScreenDiffView: Component = (props) => before={{ name: diff.file, contents: diff.before }} after={{ name: diff.file, contents: diff.after }} diffStyle={props.diffStyle} + virtualized={!eager().has(diff.file)} annotations={annotationsForFile(diff.file)} renderAnnotation={buildAnnotation} enableGutterUtility={props.canComment !== false} diff --git a/packages/kilo-vscode/webview-ui/agent-manager/diff-open-policy.ts b/packages/kilo-vscode/webview-ui/agent-manager/diff-open-policy.ts index dc6f22d9553..43362661ee8 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/diff-open-policy.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/diff-open-policy.ts @@ -2,11 +2,31 @@ 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 diffs should render eagerly (no row virtualization) so their rows +// stay mounted and never re-render on scroll. Large files keep virtualizing, and +// an aggregate budget keeps eager DOM bounded for very large reviews. +export function eagerDiffFiles(diffs: WorktreeFileDiff[]): Set { + const eager = new Set() + let used = 0 + for (const diff of diffs) { + if (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) } diff --git a/packages/kilo-vscode/webview-ui/pierre-worker.ts b/packages/kilo-vscode/webview-ui/pierre-worker.ts new file mode 100644 index 00000000000..1d6ba9b7b84 --- /dev/null +++ b/packages/kilo-vscode/webview-ui/pierre-worker.ts @@ -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. Highlighting then runs off the main thread and scrolling stays +// smooth. If the URI is missing (or the worker fails to spawn) Pierre falls back +// to its main-thread highlighter, so behaviour is never worse than before. +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"), + } +} From 9df433cc27258148dff6155eefd33c416855a3a9 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Fri, 29 May 2026 14:59:35 +0200 Subject: [PATCH 04/19] fix(vscode): render review diffs from hunk patches Eager review rendering must not rebuild complete before/after contents when a tiny patch occurs inside a large source file. Preserve existing unified patches through Changes sources, parse them behind the deferred visibility gate, and use eager rendering only for patch-backed detail so gray placeholders clear without regressing session switching. --- .changeset/smooth-diff-worker-highlight.md | 3 +- packages/kilo-ui/src/components/diff-ssr.tsx | 11 +++-- packages/kilo-ui/src/components/diff.tsx | 41 +++++++++++++---- packages/kilo-ui/src/pierre/index.ts | 9 ++-- .../kilo-vscode/src/diff/sources/session.ts | 1 + .../kilo-vscode/src/diff/sources/staged.ts | 6 +++ .../kilo-vscode/src/diff/sources/unstaged.ts | 5 +++ .../kilo-vscode/src/diff/sources/worktree.ts | 7 +-- packages/kilo-vscode/src/diff/types.ts | 2 + .../unit/agent-manager-diff-state.test.ts | 44 +++++++++++++------ .../tests/unit/diff-session-source.test.ts | 1 + .../webview-ui/agent-manager/DiffPanel.tsx | 1 + .../agent-manager/FullScreenDiffView.tsx | 1 + .../agent-manager/diff-open-policy.ts | 9 ++-- .../webview-ui/agent-manager/diff-state.ts | 16 ++++++- .../kilo-vscode/webview-ui/pierre-worker.ts | 6 +-- .../src/types/messages/agent-manager.ts | 2 + 17 files changed, 124 insertions(+), 41 deletions(-) diff --git a/.changeset/smooth-diff-worker-highlight.md b/.changeset/smooth-diff-worker-highlight.md index 5c3233e95a7..0bb9e666c17 100644 --- a/.changeset/smooth-diff-worker-highlight.md +++ b/.changeset/smooth-diff-worker-highlight.md @@ -1,5 +1,6 @@ --- "kilo-code": patch +"@kilocode/kilo-ui": patch --- -Fix laggy scrolling and gray flashes in the diff and Changes views. Syntax highlighting now runs in a web worker instead of blocking the main thread, and normal-sized review files render their diffs up front instead of re-rendering while scrolling. Scrolling large diffs stays smooth even when scrolling fast. +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. diff --git a/packages/kilo-ui/src/components/diff-ssr.tsx b/packages/kilo-ui/src/components/diff-ssr.tsx index 4dd808b4b48..a233514c87a 100644 --- a/packages/kilo-ui/src/components/diff-ssr.tsx +++ b/packages/kilo-ui/src/components/diff-ssr.tsx @@ -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,6 +16,8 @@ export function Diff(props: SSRDiffProps) { const [local, others] = splitProps(props, [ "before", "after", + "patch", + "fileDiff", "class", "classList", "annotations", @@ -251,9 +253,12 @@ export function Diff(props: SSRDiffProps) { ) // @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, diff --git a/packages/kilo-ui/src/components/diff.tsx b/packages/kilo-ui/src/components/diff.tsx index 3834adde70d..87db2bb4a70 100644 --- a/packages/kilo-ui/src/components/diff.tsx +++ b/packages/kilo-ui/src/components/diff.tsx @@ -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(props: DiffProps) { let container!: HTMLDivElement let observer: MutationObserver | undefined let sharedVirtualizer: NonNullable> | 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(props: DiffProps) { const [local, others] = splitProps(props, [ "before", "after", + "patch", "fileDiff", "class", "classList", @@ -179,11 +188,24 @@ export function Diff(props: DiffProps) { }) 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 }) @@ -682,16 +704,19 @@ export function Diff(props: DiffProps) { const opts = options() const workerPool = large() ? getWorkerPool("unified") : getWorkerPool(props.diffStyle) - // Eager (non-virtualized) diffs render once and never re-render on scroll or - // height changes, avoiding Pierre's re-render-all storms. Highlighting still - // runs in the worker, so the one-time row build stays cheap. Large diffs keep - // virtualizing to bound DOM size. + // 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 @@ -708,9 +733,9 @@ export function Diff(props: DiffProps) { container.innerHTML = "" - if (local.fileDiff) { + if (metadata) { instance.render({ - fileDiff: local.fileDiff, + fileDiff: metadata, lineAnnotations: annotations, containerWrapper: container, }) diff --git a/packages/kilo-ui/src/pierre/index.ts b/packages/kilo-ui/src/pierre/index.ts index 1b9a5bc7f98..189a3147bf1 100644 --- a/packages/kilo-ui/src/pierre/index.ts +++ b/packages/kilo-ui/src/pierre/index.ts @@ -48,9 +48,9 @@ type DiffShared = FileDiffOptions & { commentedLines?: SelectedLineRange[] onLineNumberSelectionEnd?: (selection: SelectedLineRange | null) => void onRendered?: () => void - // When false, render the whole diff up front instead of row-virtualizing it. - // With highlighting offloaded to a worker, eager diffs render their rows once - // and never re-render on scroll, so fast scrolling never shows gap buffers. + // 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 @@ -60,6 +60,8 @@ type DiffShared = FileDiffOptions & { type DiffPair = DiffShared & { before: FileContents after: FileContents + /** Unified patch used to parse only rendered hunks instead of full file contents. */ + patch?: string fileDiff?: undefined } @@ -67,6 +69,7 @@ type DiffPatch = DiffShared & { fileDiff: FileDiffMetadata before?: undefined after?: undefined + patch?: undefined } export type DiffProps = DiffPair | DiffPatch diff --git a/packages/kilo-vscode/src/diff/sources/session.ts b/packages/kilo-vscode/src/diff/sources/session.ts index cf10f0e2a2a..4df81854a17 100644 --- a/packages/kilo-vscode/src/diff/sources/session.ts +++ b/packages/kilo-vscode/src/diff/sources/session.ts @@ -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, diff --git a/packages/kilo-vscode/src/diff/sources/staged.ts b/packages/kilo-vscode/src/diff/sources/staged.ts index 9af28badce3..03a54198ff7 100644 --- a/packages/kilo-vscode/src/diff/sources/staged.ts +++ b/packages/kilo-vscode/src/diff/sources/staged.ts @@ -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, diff --git a/packages/kilo-vscode/src/diff/sources/unstaged.ts b/packages/kilo-vscode/src/diff/sources/unstaged.ts index 26f1210b928..2441bb7f85b 100644 --- a/packages/kilo-vscode/src/diff/sources/unstaged.ts +++ b/packages/kilo-vscode/src/diff/sources/unstaged.ts @@ -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, diff --git a/packages/kilo-vscode/src/diff/sources/worktree.ts b/packages/kilo-vscode/src/diff/sources/worktree.ts index fdeea76a44a..f7e2a0e4d1a 100644 --- a/packages/kilo-vscode/src/diff/sources/worktree.ts +++ b/packages/kilo-vscode/src/diff/sources/worktree.ts @@ -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, diff --git a/packages/kilo-vscode/src/diff/types.ts b/packages/kilo-vscode/src/diff/types.ts index 59dd384f5a4..a6d9def400f 100644 --- a/packages/kilo-vscode/src/diff/types.ts +++ b/packages/kilo-vscode/src/diff/types.ts @@ -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" diff --git a/packages/kilo-vscode/tests/unit/agent-manager-diff-state.test.ts b/packages/kilo-vscode/tests/unit/agent-manager-diff-state.test.ts index a1d401dc1f9..4c88fdceb7a 100644 --- a/packages/kilo-vscode/tests/unit/agent-manager-diff-state.test.ts +++ b/packages/kilo-vscode/tests/unit/agent-manager-diff-state.test.ts @@ -28,16 +28,27 @@ function diff(overrides: Partial): 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 })] @@ -101,18 +112,23 @@ describe("agent manager diff state", () => { }) describe("eager diff files", () => { - it("renders normal files eagerly", () => { + it("renders hunk-bounded detailed patches eagerly", () => { const diffs = [ - diff({ file: "src/a.ts", additions: 10, deletions: 5 }), - diff({ file: "src/b.ts", additions: 3, deletions: 0 }), + 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", additions: EXTREME_DIFF_CHANGED_LINES + 1, deletions: 0 }), - diff({ file: "src/small.ts", additions: 5, deletions: 0 }), + 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"])) }) @@ -121,12 +137,12 @@ describe("eager diff files", () => { // 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", additions: 2000, deletions: 0 }), - diff({ file: "src/b.ts", additions: 2000, deletions: 0 }), - diff({ file: "src/c.ts", additions: 2000, deletions: 0 }), - diff({ file: "src/d.ts", additions: EAGER_DIFF_REVIEW_LINES - 6005, deletions: 0 }), - diff({ file: "src/e.ts", additions: 2000, deletions: 0 }), - diff({ file: "src/f.ts", additions: 5, deletions: 0 }), + 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) diff --git a/packages/kilo-vscode/tests/unit/diff-session-source.test.ts b/packages/kilo-vscode/tests/unit/diff-session-source.test.ts index 3aa9be076d5..f8a24d24c5c 100644 --- a/packages/kilo-vscode/tests/unit/diff-session-source.test.ts +++ b/packages/kilo-vscode/tests/unit/diff-session-source.test.ts @@ -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") diff --git a/packages/kilo-vscode/webview-ui/agent-manager/DiffPanel.tsx b/packages/kilo-vscode/webview-ui/agent-manager/DiffPanel.tsx index a65aaa8b20e..acef4a9c2f1 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/DiffPanel.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/DiffPanel.tsx @@ -613,6 +613,7 @@ export const DiffPanel: Component = (props) => { 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)} diff --git a/packages/kilo-vscode/webview-ui/agent-manager/FullScreenDiffView.tsx b/packages/kilo-vscode/webview-ui/agent-manager/FullScreenDiffView.tsx index 2b1fb225e5d..d609816367e 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/FullScreenDiffView.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/FullScreenDiffView.tsx @@ -710,6 +710,7 @@ export const FullScreenDiffView: Component = (props) => 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)} diff --git a/packages/kilo-vscode/webview-ui/agent-manager/diff-open-policy.ts b/packages/kilo-vscode/webview-ui/agent-manager/diff-open-policy.ts index 43362661ee8..64a52ac71ef 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/diff-open-policy.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/diff-open-policy.ts @@ -11,14 +11,15 @@ export function isLargeDiffFile(diff: WorktreeFileDiff): boolean { return diff.additions + diff.deletions > EXTREME_DIFF_CHANGED_LINES } -// Files whose diffs should render eagerly (no row virtualization) so their rows -// stay mounted and never re-render on scroll. Large files keep virtualizing, and -// an aggregate budget keeps eager DOM bounded for very large reviews. +// 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 { const eager = new Set() let used = 0 for (const diff of diffs) { - if (isLargeDiffFile(diff)) continue + if (!diff.patch || isLargeDiffFile(diff)) continue const size = diff.additions + diff.deletions if (used + size > EAGER_DIFF_REVIEW_LINES) continue used += size diff --git a/packages/kilo-vscode/webview-ui/agent-manager/diff-state.ts b/packages/kilo-vscode/webview-ui/agent-manager/diff-state.ts index 05a9657172f..e54e156e0ff 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/diff-state.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/diff-state.ts @@ -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 } diff --git a/packages/kilo-vscode/webview-ui/pierre-worker.ts b/packages/kilo-vscode/webview-ui/pierre-worker.ts index 1d6ba9b7b84..25c126394cb 100644 --- a/packages/kilo-vscode/webview-ui/pierre-worker.ts +++ b/packages/kilo-vscode/webview-ui/pierre-worker.ts @@ -9,9 +9,9 @@ // // 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. Highlighting then runs off the main thread and scrolling stays -// smooth. If the URI is missing (or the worker fails to spawn) Pierre falls back -// to its main-thread highlighter, so behaviour is never worse than before. +// 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" diff --git a/packages/kilo-vscode/webview-ui/src/types/messages/agent-manager.ts b/packages/kilo-vscode/webview-ui/src/types/messages/agent-manager.ts index 6a5a21e3ccb..c19d22c9dd5 100644 --- a/packages/kilo-vscode/webview-ui/src/types/messages/agent-manager.ts +++ b/packages/kilo-vscode/webview-ui/src/types/messages/agent-manager.ts @@ -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" From dbb7215cf95de75f5e63d7de0f0ab6c88063a391 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Fri, 29 May 2026 15:01:38 +0200 Subject: [PATCH 05/19] docs(vscode): record diff rendering performance invariants Document the patch-bounded rendering requirement and correct Chrome trace attribution so future optimizations do not reintroduce full-file gray placeholders or mistake profiler delivery threads for execution threads. --- packages/kilo-vscode/AGENTS.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/kilo-vscode/AGENTS.md b/packages/kilo-vscode/AGENTS.md index a1583f8dbc4..19deadaa14a 100644 --- a/packages/kilo-vscode/AGENTS.md +++ b/packages/kilo-vscode/AGENTS.md @@ -180,6 +180,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. @@ -190,6 +196,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 From 830de58395f0dd296c676a118490bc3c27cb7e96 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Fri, 29 May 2026 15:10:16 +0200 Subject: [PATCH 06/19] fix(vscode): register diff worker for knip The webview worker adapter is reached through an esbuild alias rather than a TypeScript import, so Knip must treat it as an explicit entrypoint for the VS Code CI check to pass. --- packages/kilo-vscode/knip.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/kilo-vscode/knip.json b/packages/kilo-vscode/knip.json index 79b8eb7da65..98c83d5a1f0 100644 --- a/packages/kilo-vscode/knip.json +++ b/packages/kilo-vscode/knip.json @@ -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", From 6c6e511e4a1f8c004e200bb477de3e2d3842864f Mon Sep 17 00:00:00 2001 From: Kirill Kalishev Date: Fri, 29 May 2026 09:28:28 -0400 Subject: [PATCH 07/19] initial release change notes From f743b545b1335f3bfec48eb5a9fba5fc703994e2 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Fri, 29 May 2026 16:10:33 +0200 Subject: [PATCH 08/19] fix(vscode): restrict diff worker CSP source The bundled Shiki worker is created only from its injected webview resource URI, and Pierre does not construct blob-backed workers. Restrict worker-src to the webview source and lock the narrower policy in unit coverage. --- packages/kilo-vscode/src/webview-html-utils.ts | 2 +- packages/kilo-vscode/tests/unit/webview-html.test.ts | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/kilo-vscode/src/webview-html-utils.ts b/packages/kilo-vscode/src/webview-html-utils.ts index f9ce363bb8e..ffe6e0b7168 100644 --- a/packages/kilo-vscode/src/webview-html-utils.ts +++ b/packages/kilo-vscode/src/webview-html-utils.ts @@ -27,7 +27,7 @@ export function buildCspString(cspSource: string, nonce: string, port?: number): `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} blob:`, + `worker-src ${cspSource}`, `font-src ${cspSource}`, `connect-src ${cspSource} ${connectSrc}`, `img-src ${cspSource} data: https:`, diff --git a/packages/kilo-vscode/tests/unit/webview-html.test.ts b/packages/kilo-vscode/tests/unit/webview-html.test.ts index b3c7e7c7d28..d54c99ff8cf 100644 --- a/packages/kilo-vscode/tests/unit/webview-html.test.ts +++ b/packages/kilo-vscode/tests/unit/webview-html.test.ts @@ -48,9 +48,10 @@ describe("buildCspString", () => { expect(result).toContain(`font-src ${cspSource}`) }) - it("allows the Shiki worker via worker-src", () => { + it("allows only webview resources for the Shiki worker", () => { const result = buildCspString(cspSource, nonce) - expect(result).toContain(`worker-src ${cspSource} blob:`) + expect(result).toContain(`worker-src ${cspSource}`) + expect(result).not.toContain(`worker-src ${cspSource} blob:`) }) it("includes cspSource and https: in img-src", () => { From 4ee8b4b63f37abaa07e504a600c46d476501b016 Mon Sep 17 00:00:00 2001 From: kirillk Date: Fri, 29 May 2026 11:29:49 -0400 Subject: [PATCH 09/19] release(jetbrains): restore unreleased changelog marker --- packages/kilo-jetbrains/CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/kilo-jetbrains/CHANGELOG.md b/packages/kilo-jetbrains/CHANGELOG.md index b4fadf8106d..e30af42193d 100644 --- a/packages/kilo-jetbrains/CHANGELOG.md +++ b/packages/kilo-jetbrains/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [7.0.1-rc.4] - 2026-05-29 ### Added From dfe3961087b1c33bd3a1bbdc27d21dccf1c8a9f8 Mon Sep 17 00:00:00 2001 From: kirillk Date: Fri, 29 May 2026 11:45:33 -0400 Subject: [PATCH 10/19] fix(jetbrains): preserve plugin package version --- .github/workflows/publish-jetbrains.yml | 12 ++++++++---- script/publish-start.ts | 9 ++++++++- script/publish.ts | 9 ++++++++- script/upstream/transforms/preserve-versions.ts | 2 ++ 4 files changed, 26 insertions(+), 6 deletions(-) diff --git a/.github/workflows/publish-jetbrains.yml b/.github/workflows/publish-jetbrains.yml index bc1094b11fd..d72664c238c 100644 --- a/.github/workflows/publish-jetbrains.yml +++ b/.github/workflows/publish-jetbrains.yml @@ -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/package.json "$RUNNER_TEMP/jetbrains-package.json" - 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-package.json" packages/kilo-jetbrains/package.json - name: Setup Node uses: actions/setup-node@v4 diff --git a/script/publish-start.ts b/script/publish-start.ts index 41333c6e645..fa6144f1649 100755 --- a/script/publish-start.ts +++ b/script/publish-start.ts @@ -50,7 +50,14 @@ const pkgjsons = await Array.fromAsync( new Bun.Glob("**/package.json").scan({ absolute: true, }), -).then((arr) => arr.filter((x) => !x.includes("node_modules") && !x.includes("dist"))) +).then((arr) => + arr.filter( + (x) => + !x.includes("node_modules") && + !x.includes("dist") && + !x.endsWith("/packages/kilo-jetbrains/package.json"), // kilocode_change + ), +) for (const file of pkgjsons) { let pkg = await Bun.file(file).text() diff --git a/script/publish.ts b/script/publish.ts index 9e676d0a13a..e9f1c8b7136 100755 --- a/script/publish.ts +++ b/script/publish.ts @@ -42,7 +42,14 @@ const pkgjsons = await Array.fromAsync( new Bun.Glob("**/package.json").scan({ absolute: true, }), -).then((arr) => arr.filter((x) => !x.includes("node_modules") && !x.includes("dist"))) +).then((arr) => + arr.filter( + (x) => + !x.includes("node_modules") && + !x.includes("dist") && + !x.endsWith("/packages/kilo-jetbrains/package.json"), // kilocode_change + ), +) for (const file of pkgjsons) { let pkg = await Bun.file(file).text() diff --git a/script/upstream/transforms/preserve-versions.ts b/script/upstream/transforms/preserve-versions.ts index 4fdd568ec8a..0c5e9229f6e 100644 --- a/script/upstream/transforms/preserve-versions.ts +++ b/script/upstream/transforms/preserve-versions.ts @@ -76,6 +76,8 @@ export async function preserveAllVersions(options: PreserveOptions = {}): Promis info(`Target version: ${targetVersion}`) for await (const path of glob.scan({ absolute: true })) { + if (path.endsWith("/packages/kilo-jetbrains/package.json")) continue + // Skip excluded paths if (excludes.some((ex) => path.includes(ex.replace(/\*\*/g, "")))) { continue From fb6e241b9756d804a4d58359b45d1ab8a769f8c1 Mon Sep 17 00:00:00 2001 From: kirillk Date: Fri, 29 May 2026 12:14:43 -0400 Subject: [PATCH 11/19] fix(jetbrains): use gradle release metadata --- .github/workflows/publish-jetbrains.yml | 8 ++++---- packages/kilo-jetbrains/RELEASING.md | 8 ++++---- packages/kilo-jetbrains/build.gradle.kts | 9 +++++---- packages/kilo-jetbrains/gradle.properties | 1 + packages/kilo-jetbrains/package.json | 2 +- script/jetbrains-release-pr.ts | 19 +++++++++++-------- script/jetbrains-release-validate.ts | 12 ++++++++++-- script/publish-start.ts | 9 +-------- script/publish.ts | 9 +-------- .../upstream/transforms/preserve-versions.ts | 2 -- 10 files changed, 38 insertions(+), 41 deletions(-) diff --git a/.github/workflows/publish-jetbrains.yml b/.github/workflows/publish-jetbrains.yml index d72664c238c..0b0f9244445 100644 --- a/.github/workflows/publish-jetbrains.yml +++ b/.github/workflows/publish-jetbrains.yml @@ -49,7 +49,7 @@ jobs: - name: Save reviewed release metadata run: | cp packages/kilo-jetbrains/CHANGELOG.md "$RUNNER_TEMP/jetbrains-CHANGELOG.md" - cp packages/kilo-jetbrains/package.json "$RUNNER_TEMP/jetbrains-package.json" + cp packages/kilo-jetbrains/gradle.properties "$RUNNER_TEMP/jetbrains-gradle.properties" - name: Checkout release tag uses: actions/checkout@v6 @@ -60,7 +60,7 @@ jobs: - name: Restore reviewed release metadata run: | cp "$RUNNER_TEMP/jetbrains-CHANGELOG.md" packages/kilo-jetbrains/CHANGELOG.md - cp "$RUNNER_TEMP/jetbrains-package.json" packages/kilo-jetbrains/package.json + cp "$RUNNER_TEMP/jetbrains-gradle.properties" packages/kilo-jetbrains/gradle.properties - name: Setup Node uses: actions/setup-node@v4 @@ -111,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 }} @@ -129,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 }} diff --git a/packages/kilo-jetbrains/RELEASING.md b/packages/kilo-jetbrains/RELEASING.md index 69bea003546..e08d34d98c0 100644 --- a/packages/kilo-jetbrains/RELEASING.md +++ b/packages/kilo-jetbrains/RELEASING.md @@ -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 ``, 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 ``, 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` 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` 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 diff --git a/packages/kilo-jetbrains/build.gradle.kts b/packages/kilo-jetbrains/build.gradle.kts index cfba42d354d..06e65495e9d 100644 --- a/packages/kilo-jetbrains/build.gradle.kts +++ b/packages/kilo-jetbrains/build.gradle.kts @@ -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 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 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() diff --git a/packages/kilo-jetbrains/gradle.properties b/packages/kilo-jetbrains/gradle.properties index d5066fb4c41..ec656f922ff 100644 --- a/packages/kilo-jetbrains/gradle.properties +++ b/packages/kilo-jetbrains/gradle.properties @@ -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 diff --git a/packages/kilo-jetbrains/package.json b/packages/kilo-jetbrains/package.json index 01f1472a7a0..b8a39255de7 100644 --- a/packages/kilo-jetbrains/package.json +++ b/packages/kilo-jetbrains/package.json @@ -8,7 +8,7 @@ "test": "./gradlew test", "test:ci": "bun script/test-ci.ts" }, - "version": "7.0.1-rc.4", + "version": "7.3.17", "dependencies": {}, "devDependencies": {}, "peerDependencies": {} diff --git a/script/jetbrains-release-pr.ts b/script/jetbrains-release-pr.ts index 2c2c4d859b8..db9d329e7ba 100644 --- a/script/jetbrains-release-pr.ts +++ b/script/jetbrains-release-pr.ts @@ -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} diff --git a/script/jetbrains-release-validate.ts b/script/jetbrains-release-validate.ts index f56f046d6a5..2cb64e6eb9c 100644 --- a/script/jetbrains-release-validate.ts +++ b/script/jetbrains-release-validate.ts @@ -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 +} diff --git a/script/publish-start.ts b/script/publish-start.ts index fa6144f1649..41333c6e645 100755 --- a/script/publish-start.ts +++ b/script/publish-start.ts @@ -50,14 +50,7 @@ const pkgjsons = await Array.fromAsync( new Bun.Glob("**/package.json").scan({ absolute: true, }), -).then((arr) => - arr.filter( - (x) => - !x.includes("node_modules") && - !x.includes("dist") && - !x.endsWith("/packages/kilo-jetbrains/package.json"), // kilocode_change - ), -) +).then((arr) => arr.filter((x) => !x.includes("node_modules") && !x.includes("dist"))) for (const file of pkgjsons) { let pkg = await Bun.file(file).text() diff --git a/script/publish.ts b/script/publish.ts index e9f1c8b7136..9e676d0a13a 100755 --- a/script/publish.ts +++ b/script/publish.ts @@ -42,14 +42,7 @@ const pkgjsons = await Array.fromAsync( new Bun.Glob("**/package.json").scan({ absolute: true, }), -).then((arr) => - arr.filter( - (x) => - !x.includes("node_modules") && - !x.includes("dist") && - !x.endsWith("/packages/kilo-jetbrains/package.json"), // kilocode_change - ), -) +).then((arr) => arr.filter((x) => !x.includes("node_modules") && !x.includes("dist"))) for (const file of pkgjsons) { let pkg = await Bun.file(file).text() diff --git a/script/upstream/transforms/preserve-versions.ts b/script/upstream/transforms/preserve-versions.ts index 0c5e9229f6e..4fdd568ec8a 100644 --- a/script/upstream/transforms/preserve-versions.ts +++ b/script/upstream/transforms/preserve-versions.ts @@ -76,8 +76,6 @@ export async function preserveAllVersions(options: PreserveOptions = {}): Promis info(`Target version: ${targetVersion}`) for await (const path of glob.scan({ absolute: true })) { - if (path.endsWith("/packages/kilo-jetbrains/package.json")) continue - // Skip excluded paths if (excludes.some((ex) => path.includes(ex.replace(/\*\*/g, "")))) { continue From 57bc6eea583e22e4c3b8b00ad1c64fed62dc85e8 Mon Sep 17 00:00:00 2001 From: Imanol Maiztegui Date: Fri, 29 May 2026 18:15:42 +0200 Subject: [PATCH 12/19] fix(cli): route share links through Kilo session API (#10736) * fix(cli): route share links through Kilo session API Refactor shareSession and unshareSession to return Effect values using EffectBridge.fromPromise and replace the ShareNext dependency in SessionShare with KiloSession calls so CLI-generated share URLs point to app.kilo.ai/s/ public links. * test(session-share): replace EffectBridge usage with direct Storage service calls Update session-share test to use Storage.Service directly instead of wrapping Storage.write in EffectBridge.fromPromise, aligning test code with the refactored service-based architecture. Add Storage.defaultLayer to the test effect layer composition. --- .changeset/fix-cli-share-links.md | 5 ++ .../opencode/src/kilocode/session/index.ts | 19 +++--- packages/opencode/src/share/session.ts | 8 +-- .../test/kilocode/session-share.test.ts | 63 +++++++++++++++++++ 4 files changed, 83 insertions(+), 12 deletions(-) create mode 100644 .changeset/fix-cli-share-links.md create mode 100644 packages/opencode/test/kilocode/session-share.test.ts diff --git a/.changeset/fix-cli-share-links.md b/.changeset/fix-cli-share-links.md new file mode 100644 index 00000000000..e96b25e6f51 --- /dev/null +++ b/.changeset/fix-cli-share-links.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": patch +--- + +Use Kilo session share links when sharing conversations from the CLI. diff --git a/packages/opencode/src/kilocode/session/index.ts b/packages/opencode/src/kilocode/session/index.ts index 34389450bda..c9d5e90213b 100644 --- a/packages/opencode/src/kilocode/session/index.ts +++ b/packages/opencode/src/kilocode/session/index.ts @@ -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 { - 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 { diff --git a/packages/opencode/src/share/session.ts b/packages/opencode/src/share/session.ts index f66341d97a3..ebd198ebe84 100644 --- a/packages/opencode/src/share/session.ts +++ b/packages/opencode/src/share/session.ts @@ -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 @@ -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), diff --git a/packages/opencode/test/kilocode/session-share.test.ts b/packages/opencode/test/kilocode/session-share.test.ts new file mode 100644 index 00000000000..280c0147e6f --- /dev/null +++ b/packages/opencode/test/kilocode/session-share.test.ts @@ -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), + ) +}) From 7bbac24c696403e658c78e7dfb494b0a5c9c5944 Mon Sep 17 00:00:00 2001 From: "kilo-maintainer[bot]" Date: Fri, 29 May 2026 16:16:02 +0000 Subject: [PATCH 13/19] chore: update nix node_modules hashes --- nix/hashes.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nix/hashes.json b/nix/hashes.json index 7a0d099d63c..82e6694687d 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -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=" } } From 9721cf9f4e366a8791fa87aa2473be5fb65c5db6 Mon Sep 17 00:00:00 2001 From: "kilo-maintainer[bot]" Date: Fri, 29 May 2026 16:27:33 +0000 Subject: [PATCH 14/19] release: v7.3.18 --- bun.lock | 30 ++++++++++++------------- package.json | 2 +- packages/core/package.json | 2 +- packages/extensions/zed/extension.toml | 12 +++++----- packages/kilo-docs/package.json | 2 +- packages/kilo-gateway/package.json | 2 +- packages/kilo-i18n/package.json | 2 +- packages/kilo-indexing/package.json | 2 +- packages/kilo-jetbrains/package.json | 2 +- packages/kilo-telemetry/package.json | 2 +- packages/kilo-ui/package.json | 2 +- packages/kilo-vscode/package.json | 2 +- packages/kilo-vscode/tests/package.json | 2 +- packages/opencode/package.json | 2 +- packages/plugin/package.json | 2 +- packages/script/package.json | 2 +- packages/sdk/js/package.json | 2 +- packages/storybook/package.json | 2 +- packages/ui/package.json | 2 +- script/upstream/package.json | 2 +- 20 files changed, 39 insertions(+), 39 deletions(-) diff --git a/bun.lock b/bun.lock index 0983fc24172..0b944528095 100644 --- a/bun.lock +++ b/bun.lock @@ -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:", diff --git a/package.json b/package.json index b11c4723477..43c299e393e 100644 --- a/package.json +++ b/package.json @@ -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": {} } diff --git a/packages/core/package.json b/packages/core/package.json index 1ccc8b551eb..3875f8a167a 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -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", diff --git a/packages/extensions/zed/extension.toml b/packages/extensions/zed/extension.toml index 70f83fcdf2e..f1f65868847 100644 --- a/packages/extensions/zed/extension.toml +++ b/packages/extensions/zed/extension.toml @@ -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"] diff --git a/packages/kilo-docs/package.json b/packages/kilo-docs/package.json index 38523df9c64..d1203040c55 100644 --- a/packages/kilo-docs/package.json +++ b/packages/kilo-docs/package.json @@ -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", diff --git a/packages/kilo-gateway/package.json b/packages/kilo-gateway/package.json index 93a75fd0ad8..c55f7888d58 100644 --- a/packages/kilo-gateway/package.json +++ b/packages/kilo-gateway/package.json @@ -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", diff --git a/packages/kilo-i18n/package.json b/packages/kilo-i18n/package.json index 7d9b5134848..ba2736e9fac 100644 --- a/packages/kilo-i18n/package.json +++ b/packages/kilo-i18n/package.json @@ -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", diff --git a/packages/kilo-indexing/package.json b/packages/kilo-indexing/package.json index 57af8763e65..43409aa35b9 100644 --- a/packages/kilo-indexing/package.json +++ b/packages/kilo-indexing/package.json @@ -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", diff --git a/packages/kilo-jetbrains/package.json b/packages/kilo-jetbrains/package.json index b8a39255de7..219822bd0e4 100644 --- a/packages/kilo-jetbrains/package.json +++ b/packages/kilo-jetbrains/package.json @@ -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": {} diff --git a/packages/kilo-telemetry/package.json b/packages/kilo-telemetry/package.json index c9781d6f07b..2161a1b81ba 100644 --- a/packages/kilo-telemetry/package.json +++ b/packages/kilo-telemetry/package.json @@ -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", diff --git a/packages/kilo-ui/package.json b/packages/kilo-ui/package.json index 3de9ee70d26..4eadd6e4a9a 100644 --- a/packages/kilo-ui/package.json +++ b/packages/kilo-ui/package.json @@ -1,6 +1,6 @@ { "name": "@kilocode/kilo-ui", - "version": "7.3.17", + "version": "7.3.18", "type": "module", "license": "MIT", "exports": { diff --git a/packages/kilo-vscode/package.json b/packages/kilo-vscode/package.json index df5f1328a5e..310f8379cb9 100644 --- a/packages/kilo-vscode/package.json +++ b/packages/kilo-vscode/package.json @@ -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", diff --git a/packages/kilo-vscode/tests/package.json b/packages/kilo-vscode/tests/package.json index 200c0a51008..ac10c6526d4 100644 --- a/packages/kilo-vscode/tests/package.json +++ b/packages/kilo-vscode/tests/package.json @@ -1,6 +1,6 @@ { "type": "module", - "version": "7.3.17", + "version": "7.3.18", "dependencies": {}, "devDependencies": {}, "peerDependencies": {} diff --git a/packages/opencode/package.json b/packages/opencode/package.json index deea8eb894f..ffd2c0a73a4 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -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", diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 76cfdd4d68a..bf9d07ee59a 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -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": { diff --git a/packages/script/package.json b/packages/script/package.json index 9681da73979..74d5106dcd7 100644 --- a/packages/script/package.json +++ b/packages/script/package.json @@ -12,6 +12,6 @@ "exports": { ".": "./src/index.ts" }, - "version": "7.3.17", + "version": "7.3.18", "peerDependencies": {} } diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index 0ccd7c9a6a3..e0190ed33ec 100644 --- a/packages/sdk/js/package.json +++ b/packages/sdk/js/package.json @@ -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": { diff --git a/packages/storybook/package.json b/packages/storybook/package.json index 3f3854f053e..6999aa7ba65 100644 --- a/packages/storybook/package.json +++ b/packages/storybook/package.json @@ -26,7 +26,7 @@ "typescript": "catalog:", "vite": "catalog:" }, - "version": "7.3.17", + "version": "7.3.18", "dependencies": {}, "peerDependencies": {} } diff --git a/packages/ui/package.json b/packages/ui/package.json index 3b52909318f..7f265caddc7 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/ui", - "version": "7.3.17", + "version": "7.3.18", "type": "module", "license": "MIT", "exports": { diff --git a/script/upstream/package.json b/script/upstream/package.json index b32337b20d6..f9f4109cce0 100644 --- a/script/upstream/package.json +++ b/script/upstream/package.json @@ -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", From 69f8307da188a7705694c06de0fb22f29e77615c Mon Sep 17 00:00:00 2001 From: Imanol Maiztegui Date: Fri, 29 May 2026 18:30:01 +0200 Subject: [PATCH 15/19] fix(jetbrains): emit log warnings before updating state values (#10743) Reorder statements in KiloBackendAppService and KiloBackendWorkspace so that warning/error logging occurs before the reactive state is updated. This ensures observers reacting to state changes can rely on warnings already being recorded in logs. --- .../kotlin/ai/kilocode/backend/app/KiloBackendAppService.kt | 4 ++-- .../ai/kilocode/backend/workspace/KiloBackendWorkspace.kt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendAppService.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendAppService.kt index 468760d8321..5a0c14126ea 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendAppService.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendAppService.kt @@ -495,14 +495,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) { val state = KiloAppState.Error(message, errors) - _appState.value = state warnAppError(state) + _appState.value = state } private fun warnAppError(state: KiloAppState.Error) { diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/workspace/KiloBackendWorkspace.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/workspace/KiloBackendWorkspace.kt index d18a86d831e..2d9bd74d8b7 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/workspace/KiloBackendWorkspace.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/workspace/KiloBackendWorkspace.kt @@ -291,8 +291,8 @@ class KiloBackendWorkspace( } private fun setWorkspaceError(message: String, errors: List) { - _state.value = KiloWorkspaceState.Error(message, errors) log.warn("Workspace error [$directory]: $message") + _state.value = KiloWorkspaceState.Error(message, errors) } private data class FetchResult(val value: T?, val error: LoadError?) { From 2f7996c600d6378d7ca901bb2bf0dff045cb8e90 Mon Sep 17 00:00:00 2001 From: kirillk Date: Fri, 29 May 2026 13:24:15 -0400 Subject: [PATCH 16/19] docs(jetbrains): link v7 early access guide --- packages/kilo-docs/markdoc/partials/install-jetbrains.md | 2 ++ packages/kilo-jetbrains/README.md | 2 ++ 2 files changed, 4 insertions(+) diff --git a/packages/kilo-docs/markdoc/partials/install-jetbrains.md b/packages/kilo-docs/markdoc/partials/install-jetbrains.md index 2f9800f869e..939332a08c0 100644 --- a/packages/kilo-docs/markdoc/partials/install-jetbrains.md +++ b/packages/kilo-docs/markdoc/partials/install-jetbrains.md @@ -28,6 +28,8 @@ 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 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. diff --git a/packages/kilo-jetbrains/README.md b/packages/kilo-jetbrains/README.md index 146feaba28e..8bf4543d3f4 100644 --- a/packages/kilo-jetbrains/README.md +++ b/packages/kilo-jetbrains/README.md @@ -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#v7-early-access). + --- ## Set up your environment From 96bbac6a7a98ee35df357812b536a385a8f5cf84 Mon Sep 17 00:00:00 2001 From: kirillk Date: Fri, 29 May 2026 13:27:01 -0400 Subject: [PATCH 17/19] docs(jetbrains): rename early access anchor --- packages/kilo-docs/markdoc/partials/install-jetbrains.md | 2 +- packages/kilo-jetbrains/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/kilo-docs/markdoc/partials/install-jetbrains.md b/packages/kilo-docs/markdoc/partials/install-jetbrains.md index 939332a08c0..f12f3d23cb9 100644 --- a/packages/kilo-docs/markdoc/partials/install-jetbrains.md +++ b/packages/kilo-docs/markdoc/partials/install-jetbrains.md @@ -28,7 +28,7 @@ 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 diff --git a/packages/kilo-jetbrains/README.md b/packages/kilo-jetbrains/README.md index 8bf4543d3f4..2d704f9fee5 100644 --- a/packages/kilo-jetbrains/README.md +++ b/packages/kilo-jetbrains/README.md @@ -2,7 +2,7 @@ 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#v7-early-access). +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). --- From 91bbeca69172921cff29b4c0ba72652b70297450 Mon Sep 17 00:00:00 2001 From: kirillk Date: Fri, 29 May 2026 14:49:19 -0400 Subject: [PATCH 18/19] docs(kilo-docs): improve JetBrains install anchors --- .../kilo-docs/components/TableOfContents.tsx | 59 +++++++++++-- packages/kilo-docs/components/Tabs.tsx | 23 ++++- .../markdoc/partials/install-jetbrains.md | 11 +-- packages/kilo-docs/pages/_app.tsx | 23 ++++- .../pages/getting-started/installing.md | 86 +++++++++---------- 5 files changed, 141 insertions(+), 61 deletions(-) diff --git a/packages/kilo-docs/components/TableOfContents.tsx b/packages/kilo-docs/components/TableOfContents.tsx index c6b3f5b723f..735383abf88 100644 --- a/packages/kilo-docs/components/TableOfContents.tsx +++ b/packages/kilo-docs/components/TableOfContents.tsx @@ -1,16 +1,46 @@ import React, { useState, useEffect } from "react" import Link from "next/link" +const TAB_SYNC_EVENT = "kilo-tab-select" + 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 + if ( + tab === "jetbrains" && + ["manual-installations", "open-vsx-registry", "via-vsix", "troubleshooting"].includes(item.id) + ) { + 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).detail + const item = toc.find((entry) => entry.tab?.label === label) + setActiveHash(window.location.hash) + setTab(item?.tab?.slug ?? "") + } + + 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) => { + 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 (
  • - {item.title} + + {item.title} +
  • ) })} diff --git a/packages/kilo-docs/components/Tabs.tsx b/packages/kilo-docs/components/Tabs.tsx index ecffe35dc6d..16618445f8e 100644 --- a/packages/kilo-docs/components/Tabs.tsx +++ b/packages/kilo-docs/components/Tabs.tsx @@ -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,7 +43,10 @@ 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) @@ -55,6 +69,13 @@ export function Tabs({ children }: TabsProps) { } }, []) + useEffect(() => { + const hash = window.location.hash.slice(1) + if (!hash) return + + requestAnimationFrame(() => document.getElementById(hash)?.scrollIntoView()) + }, [activeIndex]) + const selectTab = (index: number) => { setActiveIndex(index) const label = tabs[index].props.label diff --git a/packages/kilo-docs/markdoc/partials/install-jetbrains.md b/packages/kilo-docs/markdoc/partials/install-jetbrains.md index f12f3d23cb9..3fdcdff6d11 100644 --- a/packages/kilo-docs/markdoc/partials/install-jetbrains.md +++ b/packages/kilo-docs/markdoc/partials/install-jetbrains.md @@ -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,14 +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: diff --git a/packages/kilo-docs/pages/_app.tsx b/packages/kilo-docs/pages/_app.tsx index 2739752f3a6..a355d72bc48 100644 --- a/packages/kilo-docs/pages/_app.tsx +++ b/packages/kilo-docs/pages/_app.tsx @@ -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) } } } diff --git a/packages/kilo-docs/pages/getting-started/installing.md b/packages/kilo-docs/pages/getting-started/installing.md index e63c864adb5..095d1687af7 100644 --- a/packages/kilo-docs/pages/getting-started/installing.md +++ b/packages/kilo-docs/pages/getting-started/installing.md @@ -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: From c23be8a170b9b7eaa589f75a36d158ef5f3e0ce8 Mon Sep 17 00:00:00 2001 From: kirillk Date: Fri, 29 May 2026 15:00:48 -0400 Subject: [PATCH 19/19] docs(kilo-docs): address tab TOC review --- .../kilo-docs/components/TableOfContents.tsx | 16 ++++++++-------- packages/kilo-docs/components/Tabs.tsx | 18 ++++++++++++++---- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/packages/kilo-docs/components/TableOfContents.tsx b/packages/kilo-docs/components/TableOfContents.tsx index 735383abf88..f0e9157eddc 100644 --- a/packages/kilo-docs/components/TableOfContents.tsx +++ b/packages/kilo-docs/components/TableOfContents.tsx @@ -3,17 +3,18 @@ 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 [tab, setTab] = useState("") const [activeHash, setActiveHash] = useState("") const items = toc.filter((item) => { if (!item.id || (item.level !== 2 && item.level !== 3)) return false - if ( - tab === "jetbrains" && - ["manual-installations", "open-vsx-registry", "via-vsix", "troubleshooting"].includes(item.id) - ) { - return false - } return !item.tab || item.tab.slug === tab }) @@ -28,9 +29,8 @@ export function TableOfContents({ toc }) { const sync = (e: Event) => { const label = (e as CustomEvent).detail - const item = toc.find((entry) => entry.tab?.label === label) setActiveHash(window.location.hash) - setTab(item?.tab?.slug ?? "") + setTab(slugify(label)) } update() diff --git a/packages/kilo-docs/components/Tabs.tsx b/packages/kilo-docs/components/Tabs.tsx index 16618445f8e..d826b818c2f 100644 --- a/packages/kilo-docs/components/Tabs.tsx +++ b/packages/kilo-docs/components/Tabs.tsx @@ -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" @@ -50,15 +50,24 @@ export function Tabs({ children }: TabsProps) { } 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).detail const found = tabs.findIndex((tab) => tab.props.label === label) + scroll.current = false if (found >= 0) setActiveIndex(found) } window.addEventListener(TAB_SYNC_EVENT, onSync) @@ -71,8 +80,9 @@ export function Tabs({ children }: TabsProps) { useEffect(() => { const hash = window.location.hash.slice(1) - if (!hash) return + if (!hash || !scroll.current) return + scroll.current = false requestAnimationFrame(() => document.getElementById(hash)?.scrollIntoView()) }, [activeIndex])