mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-01 04:46:43 +08:00
perf(vscode): slim transcript payloads
This commit is contained in:
@@ -2,4 +2,4 @@
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Speed up Agent Manager switching for long sessions by lazily mounting collapsed historical tool details and sharing timeline hover infrastructure across activity bars.
|
||||
Speed up Agent Manager switching for long sessions by lazily mounting collapsed historical tool details, sharing timeline hover infrastructure across activity bars, and omitting transcript metadata that the webview does not use.
|
||||
|
||||
@@ -58,7 +58,7 @@ import { resolveProjectDirectory } from "./project-directory"
|
||||
import { getBusySessionCount, seedSessionStatuses } from "./session-status"
|
||||
import { normalizeEnhancePromptErrorMessage } from "./enhance-prompt-error"
|
||||
import { retry } from "./services/cli-backend/retry"
|
||||
import { slimPart, slimParts } from "./kilo-provider/slim-metadata"
|
||||
import { slimInfo, slimPart, slimParts } from "./kilo-provider/slim-metadata"
|
||||
import { handleSidebarWorktreeMessage } from "./kilo-provider/sidebar-worktree"
|
||||
import { parseMessageFiles, type MessageFile } from "./kilo-provider/message-files"
|
||||
import { renameSession } from "./kilo-provider/rename-session"
|
||||
@@ -356,8 +356,13 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
})
|
||||
}
|
||||
|
||||
// Strip edit-tool metadata.filediff.before/after (multi-MB for edit-heavy
|
||||
// sessions) to keep session switches fast. Logic in kilo-provider/slim-metadata.ts.
|
||||
// Strip metadata unused by the webview to keep session switches fast.
|
||||
// Logic in kilo-provider/slim-metadata.ts.
|
||||
private slimInfo<T>(info: T): T {
|
||||
if (!this.slimEditMetadata) return info
|
||||
return slimInfo(info)
|
||||
}
|
||||
|
||||
private slimPart<T>(part: T): T {
|
||||
if (!this.slimEditMetadata) return part
|
||||
return slimPart(part)
|
||||
@@ -1466,7 +1471,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
// no abort controller, so this guard prevents ghost entries.
|
||||
if (!this.trackedSessionIds.has(sessionID)) return
|
||||
const messages = page.items.map((m) => ({
|
||||
...m.info,
|
||||
...this.slimInfo(m.info),
|
||||
parts: this.slimParts(m.parts),
|
||||
createdAt: new Date(m.info.time.created).toISOString(),
|
||||
}))
|
||||
@@ -1524,7 +1529,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
)
|
||||
|
||||
const messages = messagesData.map((m) => ({
|
||||
...m.info,
|
||||
...this.slimInfo(m.info),
|
||||
parts: this.slimParts(m.parts),
|
||||
createdAt: new Date(m.info.time.created).toISOString(),
|
||||
}))
|
||||
@@ -3159,11 +3164,12 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
this.streams.push({ ...msg, part: this.slimPart(msg.part) })
|
||||
return
|
||||
}
|
||||
if (msg.type === "indexingStatusLoaded") {
|
||||
this.cachedIndexingStatusMessage = msg
|
||||
const next = msg.type === "messageCreated" ? { ...msg, message: this.slimInfo(msg.message) } : msg
|
||||
if (next.type === "indexingStatusLoaded") {
|
||||
this.cachedIndexingStatusMessage = next
|
||||
}
|
||||
this.streams.flush(sessionID)
|
||||
this.postMessage(msg)
|
||||
this.postMessage(next)
|
||||
}
|
||||
|
||||
/** Wait until the webview has sent "webviewReady". Resolves immediately when already ready. */
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
/**
|
||||
* Pure data-transform helpers that strip heavy tool metadata from
|
||||
* message parts before sending them to the webview via postMessage.
|
||||
* Pure data-transform helpers that strip heavy message metadata before
|
||||
* sending transcripts to the webview via postMessage.
|
||||
*
|
||||
* The webview communicates with the extension over VS Code's IPC bridge.
|
||||
* Every message is JSON-serialised → deserialised on each side. Tool parts
|
||||
* Every message is JSON-serialised → deserialised on each side. Tool parts
|
||||
* from edit, apply_patch, multiedit and write often carry full file contents
|
||||
* (before/after snapshots, patch text, written content). Sending those on
|
||||
* every session switch makes serialisation the dominant bottleneck.
|
||||
* (before/after snapshots, patch text, written content). User message summaries
|
||||
* and reasoning parts can also carry patches and encrypted provider metadata
|
||||
* that the webview does not use. Sending those on every session switch makes
|
||||
* serialisation the dominant bottleneck.
|
||||
*
|
||||
* This module strips fields the webview never (or rarely) needs while keeping
|
||||
* everything required to render collapsed tool-part headers and diagnostics.
|
||||
* everything required to render transcript summaries, tool details and
|
||||
* diagnostics.
|
||||
*
|
||||
* No vscode dependency — safe to unit-test in isolation.
|
||||
*/
|
||||
@@ -181,6 +184,24 @@ function slimBash(state: Record<string, unknown>): Record<string, unknown> {
|
||||
// Public API
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Strip patches used only by the explicit turn-diff fetch from message summaries. */
|
||||
export function slimInfo<T>(info: T): T {
|
||||
if (!info || typeof info !== "object") return info
|
||||
|
||||
const obj = info as Record<string, unknown>
|
||||
const summary = obj.summary
|
||||
if (!isObj(summary) || !Array.isArray(summary.diffs)) return info
|
||||
if (!summary.diffs.some((diff) => isObj(diff) && "patch" in diff)) return info
|
||||
|
||||
const diffs = summary.diffs.map((diff) => {
|
||||
if (!isObj(diff) || !("patch" in diff)) return diff
|
||||
const next = { ...diff }
|
||||
delete next.patch
|
||||
return next
|
||||
})
|
||||
return { ...obj, summary: { ...summary, diffs } } as T
|
||||
}
|
||||
|
||||
const slimmers: Record<string, (state: Record<string, unknown>) => Record<string, unknown>> = {
|
||||
read: slimOutput,
|
||||
list: slimOutput,
|
||||
@@ -193,11 +214,24 @@ const slimmers: Record<string, (state: Record<string, unknown>) => Record<string
|
||||
bash: slimBash,
|
||||
}
|
||||
|
||||
/** Strip heavy metadata from a single tool part; pass-through for non-tool parts. */
|
||||
/** Strip provider metadata that the webview never reads from reasoning parts. */
|
||||
function slimReasoning<T>(part: T, obj: Record<string, unknown>): T {
|
||||
const meta = obj.metadata
|
||||
if (!isObj(meta)) return part
|
||||
const openai = meta.openai
|
||||
if (!isObj(openai) || !("reasoningEncryptedContent" in openai)) return part
|
||||
|
||||
const next = { ...openai }
|
||||
delete next.reasoningEncryptedContent
|
||||
return { ...obj, metadata: { ...meta, openai: next } } as T
|
||||
}
|
||||
|
||||
/** Strip heavy metadata from a single transcript part. */
|
||||
export function slimPart<T>(part: T): T {
|
||||
if (!part || typeof part !== "object") return part
|
||||
|
||||
const obj = part as Record<string, unknown>
|
||||
if (obj.type === "reasoning") return slimReasoning(part, obj)
|
||||
if (obj.type !== "tool") return part
|
||||
|
||||
const tool = obj.tool
|
||||
|
||||
@@ -116,6 +116,7 @@ type ProviderInternals = {
|
||||
sessionDirectories: Map<string, string>
|
||||
trackedSessionIds: Set<string>
|
||||
stopCurrentSessionProcesses: (next?: string) => void
|
||||
handleEvent: (event: unknown) => void
|
||||
handleLoadMessages: (sid: string, opts?: { mode?: string; before?: string; limit?: number }) => Promise<void>
|
||||
handleDeleteSession: (sid: string) => Promise<void>
|
||||
}
|
||||
@@ -302,6 +303,74 @@ describe("KiloProvider.handleDeleteSession / background processes", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("KiloProvider.handleLoadMessages / slim payload", () => {
|
||||
it("strips transcript-only metadata before posting messages to the webview", async () => {
|
||||
const user = mkMessage("m1", "user", 1)
|
||||
const assistant = mkMessage("m2", "assistant", 2)
|
||||
const client = createClient({
|
||||
messagesData: [
|
||||
{
|
||||
...user,
|
||||
info: {
|
||||
...user.info,
|
||||
summary: { diffs: [{ file: "a.ts", patch: "full patch", additions: 2, deletions: 1 }] },
|
||||
},
|
||||
},
|
||||
{
|
||||
...assistant,
|
||||
parts: [
|
||||
{
|
||||
type: "reasoning",
|
||||
id: "r1",
|
||||
text: "Considering options",
|
||||
metadata: { openai: { reasoningEncryptedContent: "encrypted", itemId: "item-1" } },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
const { provider, sent } = makeProvider(client)
|
||||
|
||||
await provider.loadMessages("s1")
|
||||
|
||||
const loaded = sent.find(
|
||||
(msg) => typeof msg === "object" && msg && (msg as { type?: unknown }).type === "messagesLoaded",
|
||||
) as
|
||||
| {
|
||||
messages: Array<{
|
||||
summary?: { diffs?: Array<Record<string, unknown>> }
|
||||
parts: Array<{ metadata?: { openai?: Record<string, unknown> } }>
|
||||
}>
|
||||
}
|
||||
| undefined
|
||||
expect(loaded?.messages[0]?.summary?.diffs?.[0]).toEqual({ file: "a.ts", additions: 2, deletions: 1 })
|
||||
expect(loaded?.messages[1]?.parts[0]?.metadata?.openai).toEqual({ itemId: "item-1" })
|
||||
})
|
||||
|
||||
it("strips summary patches from live message updates", () => {
|
||||
const client = createClient()
|
||||
const { internal, sent } = makeProvider(client)
|
||||
|
||||
internal.handleEvent({
|
||||
type: "message.updated",
|
||||
properties: {
|
||||
info: {
|
||||
id: "m1",
|
||||
sessionID: "s1",
|
||||
role: "user",
|
||||
time: { created: 1 },
|
||||
summary: { diffs: [{ file: "a.ts", patch: "full patch", additions: 2, deletions: 1 }] },
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const created = sent.find(
|
||||
(msg) => typeof msg === "object" && msg && (msg as { type?: unknown }).type === "messageCreated",
|
||||
) as { message?: { summary?: { diffs?: Array<Record<string, unknown>> } } } | undefined
|
||||
expect(created?.message?.summary?.diffs?.[0]).toEqual({ file: "a.ts", additions: 2, deletions: 1 })
|
||||
})
|
||||
})
|
||||
|
||||
describe("KiloProvider.loadMessages / sub-agent viewer full history", () => {
|
||||
it("loads all messages without the MESSAGE_PAGE_LIMIT cap (sub-agent viewer needs full turn history)", async () => {
|
||||
// Regression: SubAgentViewerProvider used to call client.session.messages
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect } from "bun:test"
|
||||
import { slimPart } from "../../src/kilo-provider/slim-metadata"
|
||||
import { slimInfo, slimPart } from "../../src/kilo-provider/slim-metadata"
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
@@ -35,6 +35,28 @@ const DIAG = [
|
||||
// Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("slimInfo", () => {
|
||||
it("drops summary patches while keeping visible diff fields", () => {
|
||||
const info = {
|
||||
role: "user",
|
||||
summary: {
|
||||
title: "Updated files",
|
||||
diffs: [{ file: "a.ts", patch: BIG, additions: 3, deletions: 1, status: "modified" }],
|
||||
},
|
||||
}
|
||||
|
||||
const slim = slimInfo(info)
|
||||
expect(slim.summary.title).toBe("Updated files")
|
||||
expect(slim.summary.diffs).toEqual([{ file: "a.ts", additions: 3, deletions: 1, status: "modified" }])
|
||||
expect(info.summary.diffs[0]?.patch).toBe(BIG)
|
||||
})
|
||||
|
||||
it("passes through summaries without patches unchanged", () => {
|
||||
const info = { role: "user", summary: { diffs: [{ file: "a.ts", additions: 1, deletions: 0 }] } }
|
||||
expect(slimInfo(info)).toBe(info)
|
||||
})
|
||||
})
|
||||
|
||||
describe("slimPart", () => {
|
||||
it("passes through non-tool parts unchanged", () => {
|
||||
const text = { type: "text", id: "t1", content: "hello" }
|
||||
@@ -46,6 +68,22 @@ describe("slimPart", () => {
|
||||
expect(slimPart(p)).toBe(p)
|
||||
})
|
||||
|
||||
it("drops encrypted OpenAI reasoning metadata while keeping other provider fields", () => {
|
||||
const reasoning = {
|
||||
type: "reasoning",
|
||||
id: "r1",
|
||||
text: "Considering options",
|
||||
metadata: {
|
||||
openai: { reasoningEncryptedContent: BIG, itemId: "item-1" },
|
||||
anthropic: { signature: "sig-1" },
|
||||
},
|
||||
}
|
||||
|
||||
const slim = slimPart(reasoning)
|
||||
expect(slim.metadata).toEqual({ openai: { itemId: "item-1" }, anthropic: { signature: "sig-1" } })
|
||||
expect(reasoning.metadata.openai.reasoningEncryptedContent).toBe(BIG)
|
||||
})
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// edit
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user