Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/optimize-large-session-loading.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---

Optimize large session loading performance and reduce reactive re-render overhead in the webview.
1 change: 1 addition & 0 deletions packages/kilo-vscode/src/kilo-provider/slim-metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ const slimmers: Record<string, (state: Record<string, unknown>) => Record<string
multiedit: slimMultiedit,
write: slimWrite,
bash: slimBash,
task: slimOutput,
}

/** Strip provider metadata that the webview never reads from reasoning parts. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,9 +199,9 @@ export function activeUserMessageID(
status: SessionStatusInfo,
parts?: (msg: Message) => 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)
}

Expand Down
28 changes: 26 additions & 2 deletions packages/kilo-vscode/webview-ui/src/context/session-utils.ts
Original file line number Diff line number Diff line change
@@ -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<MessageLoadMode, "focus"> | "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<string>, ids: Iterable<string>): Set<string> {
Expand All @@ -8,7 +26,13 @@ export function dropSet(prev: Set<string>, ids: Iterable<string>): Set<string> {
return next.size === prev.size ? prev : next
}

export const SNAPSHOT_PROGRESS_TEXT = "Initializing snapshot..."
export function messageParts(messages: Message[]): Record<string, Part[]> {
const parts: Record<string, Part[]> = {}
for (const msg of messages) {
if (msg.parts && msg.parts.length > 0) parts[msg.id] = msg.parts
}
return parts
}

type SnapshotPart = {
type?: string
Expand Down
56 changes: 21 additions & 35 deletions packages/kilo-vscode/webview-ui/src/context/session.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -91,22 +95,6 @@ import { createModelSelector } from "./session-model-selector"
const RECENT_LIMIT = 5
const MESSAGE_PAGE_LIMIT = 80

type MessageMutation = Exclude<MessageLoadMode, "focus"> | "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<string, SessionInfo>
Expand Down Expand Up @@ -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<string, Part[]>) {
function rebuildToolParts(
sessionID: string,
messages: Message[],
parts?: Record<string, Part[]>,
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<string, Part[]> {
const parts: Record<string, Part[]> = {}
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) {
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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))
}

/**
Expand All @@ -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
Expand Down Expand Up @@ -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] ?? []

Expand Down
Loading