From 5b06d7dea047795b882ead9896902bcf3d0bf4ea Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Mon, 17 Aug 2026 09:00:09 +0200 Subject: [PATCH] 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 00000000000..8bce7c260bc --- /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 9175ea673dc..3ce6b56fc2f 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 245711850f6..ec3ef9de426 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 d8f0b44d24b..c3923b85530 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] ?? []