From 5b06d7dea047795b882ead9896902bcf3d0bf4ea Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Mon, 17 Aug 2026 09:00:09 +0200 Subject: [PATCH 1/6] perf(vscode): optimize large session load times and eliminate reactive cascades --- .changeset/optimize-large-session-loading.md | 5 ++ .../src/kilo-provider/slim-metadata.ts | 1 + .../webview-ui/src/context/session-queue.ts | 2 +- .../webview-ui/src/context/session-utils.ts | 28 +++++++++- .../webview-ui/src/context/session.tsx | 56 +++++++------------ 5 files changed, 54 insertions(+), 38 deletions(-) create mode 100644 .changeset/optimize-large-session-loading.md diff --git a/.changeset/optimize-large-session-loading.md b/.changeset/optimize-large-session-loading.md new file mode 100644 index 0000000000..8bce7c260b --- /dev/null +++ b/.changeset/optimize-large-session-loading.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Optimize large session loading performance and reduce reactive re-render overhead in the webview. diff --git a/packages/kilo-vscode/src/kilo-provider/slim-metadata.ts b/packages/kilo-vscode/src/kilo-provider/slim-metadata.ts index 9175ea673d..3ce6b56fc2 100644 --- a/packages/kilo-vscode/src/kilo-provider/slim-metadata.ts +++ b/packages/kilo-vscode/src/kilo-provider/slim-metadata.ts @@ -218,6 +218,7 @@ const slimmers: Record) => Record Message["parts"], ) { + if (status.type === "idle") return undefined const id = active(messages, status, parts) if (id) return id - if (status.type === "idle") return undefined return pending(messages, parts) } diff --git a/packages/kilo-vscode/webview-ui/src/context/session-utils.ts b/packages/kilo-vscode/webview-ui/src/context/session-utils.ts index 245711850f..ec3ef9de42 100644 --- a/packages/kilo-vscode/webview-ui/src/context/session-utils.ts +++ b/packages/kilo-vscode/webview-ui/src/context/session-utils.ts @@ -1,5 +1,23 @@ import { reconcile } from "solid-js/store" -import type { Message, Part, ToolPart } from "../types/messages" +import type { Message, MessageLoadMode, Part, ToolPart } from "../types/messages" + +export const SNAPSHOT_PROGRESS_TEXT = "Initializing snapshot..." + +export type MessageMutation = Exclude | "append" | "update" + +export interface MessagePageState { + loadingInitial: boolean + loadingOlder: boolean + before?: string + hasMore: boolean + lastMutation?: MessageMutation +} + +export const emptyPageState: MessagePageState = { + loadingInitial: false, + loadingOlder: false, + hasMore: false, +} /** Remove ids from a Set immutably, returning the original when nothing changed. */ export function dropSet(prev: Set, ids: Iterable): Set { @@ -8,7 +26,13 @@ export function dropSet(prev: Set, ids: Iterable): Set { return next.size === prev.size ? prev : next } -export const SNAPSHOT_PROGRESS_TEXT = "Initializing snapshot..." +export function messageParts(messages: Message[]): Record { + const parts: Record = {} + for (const msg of messages) { + if (msg.parts && msg.parts.length > 0) parts[msg.id] = msg.parts + } + return parts +} type SnapshotPart = { type?: string diff --git a/packages/kilo-vscode/webview-ui/src/context/session.tsx b/packages/kilo-vscode/webview-ui/src/context/session.tsx index d8f0b44d24..c3923b8553 100644 --- a/packages/kilo-vscode/webview-ui/src/context/session.tsx +++ b/packages/kilo-vscode/webview-ui/src/context/session.tsx @@ -62,10 +62,14 @@ import { buildSessionToolParts, childID, dropSet, + emptyPageState, + messageParts, reconcileSessionToolParts, removeSessionToolPart, removeSessionToolPartsForMessage, upsertSessionToolPart, + type MessageMutation, + type MessagePageState, } from "./session-utils" import { Identifier } from "../utils/id" import { resolveModelSelection } from "./model-selection" @@ -91,22 +95,6 @@ import { createModelSelector } from "./session-model-selector" const RECENT_LIMIT = 5 const MESSAGE_PAGE_LIMIT = 80 -type MessageMutation = Exclude | "append" | "update" - -interface MessagePageState { - loadingInitial: boolean - loadingOlder: boolean - before?: string - hasMore: boolean - lastMutation?: MessageMutation -} - -const emptyPageState: MessagePageState = { - loadingInitial: false, - loadingOlder: false, - hasMore: false, -} - // Store structure for messages and parts interface SessionStore { sessions: Record @@ -1403,24 +1391,21 @@ export const SessionProvider: ParentComponent = (props) => { return [...merged, ...orphans] } - function setTools(sessionID: string, tools: ToolPart[]) { - setStore("toolParts", sessionID, reconcileSessionToolParts(tools)) + function setTools(sessionID: string, tools: ToolPart[], mode?: MessageLoadMode) { + setStore("toolParts", sessionID, mode === "replace" ? tools : reconcileSessionToolParts(tools)) } - function rebuildToolParts(sessionID: string, messages: Message[], parts?: Record) { + function rebuildToolParts( + sessionID: string, + messages: Message[], + parts?: Record, + mode?: MessageLoadMode, + ) { const tools = buildSessionToolParts( messages, - (msg) => parts?.[msg.id] ?? store.parts[msg.id] ?? stash.peek(msg.id) ?? msg.parts, + (msg) => parts?.[msg.id] ?? stash.peek(msg.id) ?? untrack(() => store.parts[msg.id]) ?? msg.parts, ) - setTools(sessionID, tools) - } - - function messageParts(messages: Message[]): Record { - const parts: Record = {} - for (const msg of messages) { - if (msg.parts && msg.parts.length > 0) parts[msg.id] = msg.parts - } - return parts + setTools(sessionID, tools, mode) } function patchToolPart(sessionID: string | undefined, messageID: string, part: Part) { @@ -1510,7 +1495,7 @@ export const SessionProvider: ParentComponent = (props) => { if (mode === "reconcile") stash.remove(msg.id) } - rebuildToolParts(sessionID, merged, loadedParts) + rebuildToolParts(sessionID, merged, loadedParts, mode) // "reconcile" is a background tail refresh, not a page navigation — // preserve the existing pagination cursor/hasMore so "load earlier" @@ -1855,8 +1840,10 @@ export const SessionProvider: ParentComponent = (props) => { } function visibleToolParts(sessionID: string, messages: Message[]): ToolPart[] { + const tools = store.toolParts[sessionID] + if (!tools || tools.length === 0 || messages.length === 0) return [] const ids = new Set(messages.map((msg) => msg.id)) - return (store.toolParts[sessionID] ?? []).filter((part) => !part.messageID || ids.has(part.messageID)) + return tools.filter((part) => !part.messageID || ids.has(part.messageID)) } /** @@ -1869,8 +1856,9 @@ export const SessionProvider: ParentComponent = (props) => { const queue = [rootID] while (queue.length > 0) { const sid = queue.pop()! + const tools = store.toolParts[sid] + if (!tools || tools.length === 0 || !tools.some((t) => t.tool === "task")) continue for (const p of visibleToolParts(sid, source(sid))) { - // Webview ToolState omits runtime metadata; task parts still carry it from the backend. const child = childID( p as { type: string @@ -2728,9 +2716,7 @@ export const SessionProvider: ParentComponent = (props) => { return id ? store.messages[id] || [] : [] } - const getParts = (messageID: string) => { - return store.parts[messageID] || stash.peek(messageID) || [] - } + const getParts = (messageID: string) => stash.peek(messageID) ?? untrack(() => store.parts[messageID]) ?? [] const getSessionToolParts = (sessionID: string) => store.toolParts[sessionID] ?? [] From cba6ebc6fa7ba313bc0aae51550389626621d9c5 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Mon, 17 Aug 2026 10:46:51 +0200 Subject: [PATCH 2/6] fix(ui): preserve streaming chat scroll position --- .changeset/fix-streaming-scroll-follow.md | 5 +++ .../src/hooks/create-auto-scroll.test.tsx | 33 +++++++++++++++++++ .../kilo-ui/src/hooks/create-auto-scroll.tsx | 6 +++- .../kilo-ui/src/hooks/scroll-user-activity.ts | 2 +- 4 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 .changeset/fix-streaming-scroll-follow.md diff --git a/.changeset/fix-streaming-scroll-follow.md b/.changeset/fix-streaming-scroll-follow.md new file mode 100644 index 0000000000..30600ba02e --- /dev/null +++ b/.changeset/fix-streaming-scroll-follow.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-ui": patch +--- + +Keep chat pinned to the latest streaming output through layout reflows and downward scrolling. diff --git a/packages/kilo-ui/src/hooks/create-auto-scroll.test.tsx b/packages/kilo-ui/src/hooks/create-auto-scroll.test.tsx index 6f1ec9e354..0a0334ade4 100644 --- a/packages/kilo-ui/src/hooks/create-auto-scroll.test.tsx +++ b/packages/kilo-ui/src/hooks/create-auto-scroll.test.tsx @@ -203,6 +203,39 @@ describe("createAutoScroll non-scrollable layouts", () => { ctx.dispose() }) + test("continues following streaming growth after a downward wheel at the bottom", () => { + const ctx = setup({ working: true }) + ctx.el.scrollHeight = 1000 + ctx.el.clientHeight = 200 + ctx.el.scrollTop = 800 + + ctx.el.fire("wheel", new FakeWheelEvent(50, ctx.el) as unknown as Event) + ctx.el.scrollHeight = 1048 + ctx.resize(0) + + expect(ctx.scroll.userScrolled()).toBe(false) + expect(ctx.el.scrollTop).toBe(1048) + ctx.dispose() + }) + + test("continues following when streaming reflow emits scroll before resize", () => { + const ctx = setup({ working: true }) + ctx.el.scrollHeight = 1000 + ctx.el.clientHeight = 200 + ctx.el.scrollTop = 800 + + ctx.el.scrollHeight = 1108 + ctx.scroll.handleScroll() + + expect(ctx.scroll.userScrolled()).toBe(false) + + ctx.resize(0) + + expect(ctx.scroll.userScrolled()).toBe(false) + expect(ctx.el.scrollTop).toBe(1108) + ctx.dispose() + }) + test("follows when initially short content starts overflowing", () => { const ctx = setup() ctx.resize() diff --git a/packages/kilo-ui/src/hooks/create-auto-scroll.tsx b/packages/kilo-ui/src/hooks/create-auto-scroll.tsx index 09ab785423..171dcb263f 100644 --- a/packages/kilo-ui/src/hooks/create-auto-scroll.tsx +++ b/packages/kilo-ui/src/hooks/create-auto-scroll.tsx @@ -99,7 +99,7 @@ export function createAutoScroll(options: AutoScrollOptions) { const handleScroll = () => { if (!scroll) return - userActivity.consumeScroll() + const input = userActivity.consumeScroll() const distance = distanceFromBottom(scroll) if (!canScroll(scroll)) return @@ -109,6 +109,10 @@ export function createAutoScroll(options: AutoScrollOptions) { return } + // Virtualizer and layout remeasurement can emit scroll before the + // ResizeObserver restores bottom-follow. Only user input should pause it. + if (!store.userScrolled && !input && !userActivity.isRecent()) return + stop() } diff --git a/packages/kilo-ui/src/hooks/scroll-user-activity.ts b/packages/kilo-ui/src/hooks/scroll-user-activity.ts index a8a33fc8a6..2cd5b8a76b 100644 --- a/packages/kilo-ui/src/hooks/scroll-user-activity.ts +++ b/packages/kilo-ui/src/hooks/scroll-user-activity.ts @@ -26,8 +26,8 @@ export const createUserActivity = (options: UserActivityOptions) => { const handleWheel = (event: WheelEvent) => { if (!isPotentialScrollInput(event)) return if (!scroll || scroll.scrollHeight - scroll.clientHeight <= 1) return - mark(event) if (event.deltaY >= 0 || scroll.scrollTop <= 0) return + mark(event) options.onWheelUp() } From 2f390b2b68da3c35377c0ff4dd12d95b0cbcd9bf Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Mon, 17 Aug 2026 10:47:52 +0200 Subject: [PATCH 3/6] fix(docs): exclude flaky Requesty marketplace link --- packages/kilo-docs/lychee.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/kilo-docs/lychee.toml b/packages/kilo-docs/lychee.toml index bfff8a2920..0e327f94ac 100644 --- a/packages/kilo-docs/lychee.toml +++ b/packages/kilo-docs/lychee.toml @@ -57,6 +57,8 @@ exclude = [ '^https?://aistudio\.google\.com/api-keys/?$', # Linear intermittently returns 503 to CI link checks. '^https?://linear\.app/?$', + # The Requesty Marketplace page intermittently returns 503 to automated link checks. + '^https://marketplace\.visualstudio\.com/items\?itemName=Requesty\.requesty$', # Consistently times out in CI '^https?://opncd\.ai', '^https?://zod\.dev/v4/changelog', From d4fd7934d2049db13933e7d3f3443c6d978f8728 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Mon, 17 Aug 2026 10:52:03 +0200 Subject: [PATCH 4/6] fix(docs): exclude flaky VS Code marketplace pages --- packages/kilo-docs/lychee.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/kilo-docs/lychee.toml b/packages/kilo-docs/lychee.toml index 0e327f94ac..bc3063ba15 100644 --- a/packages/kilo-docs/lychee.toml +++ b/packages/kilo-docs/lychee.toml @@ -57,8 +57,8 @@ exclude = [ '^https?://aistudio\.google\.com/api-keys/?$', # Linear intermittently returns 503 to CI link checks. '^https?://linear\.app/?$', - # The Requesty Marketplace page intermittently returns 503 to automated link checks. - '^https://marketplace\.visualstudio\.com/items\?itemName=Requesty\.requesty$', + # VS Code Marketplace item pages intermittently return 503 to automated link checks. + '^https://marketplace\.visualstudio\.com/items\?itemName=', # Consistently times out in CI '^https?://opncd\.ai', '^https?://zod\.dev/v4/changelog', From 2acdb46eeec7e577bc27cf589c0dfe16c4e83410 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Mon, 17 Aug 2026 10:55:01 +0200 Subject: [PATCH 5/6] fix(ui): preserve native scroll pauses --- .../src/hooks/create-auto-scroll.test.tsx | 19 +++++++++++++++++++ .../kilo-ui/src/hooks/create-auto-scroll.tsx | 9 ++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/packages/kilo-ui/src/hooks/create-auto-scroll.test.tsx b/packages/kilo-ui/src/hooks/create-auto-scroll.test.tsx index 0a0334ade4..463c58c08c 100644 --- a/packages/kilo-ui/src/hooks/create-auto-scroll.test.tsx +++ b/packages/kilo-ui/src/hooks/create-auto-scroll.test.tsx @@ -295,4 +295,23 @@ describe("createAutoScroll non-scrollable layouts", () => { expect(ctx.el.scrollTop).toBe(600) ctx.dispose() }) + + test("pauses when a native scrollbar drag changes scroll position without input events", () => { + const ctx = setup({ working: true }) + ctx.el.scrollHeight = 1000 + ctx.el.clientHeight = 200 + ctx.el.scrollTop = 800 + ctx.scroll.handleScroll() + + ctx.el.scrollTop = 600 + ctx.scroll.handleScroll() + + expect(ctx.scroll.userScrolled()).toBe(true) + + ctx.el.scrollHeight = 1050 + ctx.resize() + + expect(ctx.el.scrollTop).toBe(600) + ctx.dispose() + }) }) diff --git a/packages/kilo-ui/src/hooks/create-auto-scroll.tsx b/packages/kilo-ui/src/hooks/create-auto-scroll.tsx index 171dcb263f..a41cdbb0f0 100644 --- a/packages/kilo-ui/src/hooks/create-auto-scroll.tsx +++ b/packages/kilo-ui/src/hooks/create-auto-scroll.tsx @@ -25,6 +25,8 @@ export function createAutoScroll(options: AutoScrollOptions) { let settling = false let settleTimer: ReturnType | undefined let cleanup: (() => void) | undefined + let lastTop = 0 + let lastHeight = 0 const [store, setStore] = createStore({ contentRef: undefined as HTMLElement | undefined, @@ -101,6 +103,9 @@ export function createAutoScroll(options: AutoScrollOptions) { const input = userActivity.consumeScroll() const distance = distanceFromBottom(scroll) + const moved = Math.abs(scroll.scrollTop - lastTop) > 1 && Math.abs(scroll.scrollHeight - lastHeight) <= 1 + lastTop = scroll.scrollTop + lastHeight = scroll.scrollHeight if (!canScroll(scroll)) return @@ -111,7 +116,7 @@ export function createAutoScroll(options: AutoScrollOptions) { // Virtualizer and layout remeasurement can emit scroll before the // ResizeObserver restores bottom-follow. Only user input should pause it. - if (!store.userScrolled && !input && !userActivity.isRecent()) return + if (!store.userScrolled && !input && !userActivity.isRecent() && !moved) return stop() } @@ -204,6 +209,8 @@ export function createAutoScroll(options: AutoScrollOptions) { if (!el) return + lastTop = el.scrollTop + lastHeight = el.scrollHeight updateOverflowAnchor(el) cleanup = userActivity.listen(el) } From 1bc14d9b4f44473d9f5dfdb190f8b3fda48f56ec Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Mon, 17 Aug 2026 11:00:21 +0200 Subject: [PATCH 6/6] fix(docs): scope marketplace link exclusions --- packages/kilo-docs/lychee.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/kilo-docs/lychee.toml b/packages/kilo-docs/lychee.toml index bc3063ba15..0622d7cf07 100644 --- a/packages/kilo-docs/lychee.toml +++ b/packages/kilo-docs/lychee.toml @@ -57,8 +57,8 @@ exclude = [ '^https?://aistudio\.google\.com/api-keys/?$', # Linear intermittently returns 503 to CI link checks. '^https?://linear\.app/?$', - # VS Code Marketplace item pages intermittently return 503 to automated link checks. - '^https://marketplace\.visualstudio\.com/items\?itemName=', + # These VS Code Marketplace pages intermittently return 503 to automated link checks. + '^https://marketplace\.visualstudio\.com/items\?itemName=(connor4312\.esbuild-problem-matchers|dbaeumer\.vscode-eslint|esbenp\.prettier-vscode|Requesty\.requesty)$', # Consistently times out in CI '^https?://opncd\.ai', '^https?://zod\.dev/v4/changelog',