mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-10-29 18:52:45 +08:00
perf(vscode): fix performance issues with write tool (#7211)
* perf(vscode): fix performance issues with write tool * refactor: remove packages/ui changes, defer to follow-up issue * fix: preserve metadata.diagnostics in slimMeta
This commit is contained in:
@@ -40,6 +40,7 @@ import { resolveProjectDirectory } from "./project-directory"
|
||||
|
||||
type KiloProviderOptions = {
|
||||
projectDirectory?: string | null
|
||||
slimEditMetadata?: boolean
|
||||
}
|
||||
|
||||
export class KiloProvider implements vscode.WebviewViewProvider, TelemetryPropertiesProvider {
|
||||
@@ -90,6 +91,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
private ignoreControllerDir: string | null = null
|
||||
private marketplace: MarketplaceService | null = null
|
||||
private projectDirectory: string | null | undefined
|
||||
private slimEditMetadata = true
|
||||
|
||||
/** Optional interceptor called before the standard message handler.
|
||||
* Return null to consume the message, or return a (possibly transformed) message. */
|
||||
@@ -102,6 +104,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
options?: KiloProviderOptions,
|
||||
) {
|
||||
this.projectDirectory = options?.projectDirectory
|
||||
this.slimEditMetadata = options?.slimEditMetadata ?? true
|
||||
TelemetryProxy.getInstance().setProvider(this)
|
||||
}
|
||||
|
||||
@@ -135,6 +138,63 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
}
|
||||
}
|
||||
|
||||
// Edit tool parts carry full file contents in metadata.filediff.before/after.
|
||||
// A session with many edits can produce multi-MB payloads serialized through
|
||||
// postMessage on every session switch. Stripping those strings down to just
|
||||
// file path + addition/deletion counts eliminates the dominant cost.
|
||||
|
||||
private slimMeta(meta: unknown) {
|
||||
if (!this.slimEditMetadata) return meta
|
||||
if (!meta || typeof meta !== "object") return undefined
|
||||
|
||||
const obj = meta as Record<string, unknown>
|
||||
const filediff = obj.filediff
|
||||
if (!filediff || typeof filediff !== "object") return undefined
|
||||
|
||||
const diff = filediff as Record<string, unknown>
|
||||
const file = typeof diff.file === "string" ? diff.file : undefined
|
||||
const additions = typeof diff.additions === "number" ? diff.additions : 0
|
||||
const deletions = typeof diff.deletions === "number" ? diff.deletions : 0
|
||||
|
||||
const result: Record<string, unknown> = {
|
||||
filediff: {
|
||||
...(file ? { file } : {}),
|
||||
additions,
|
||||
deletions,
|
||||
},
|
||||
}
|
||||
// Preserve diagnostics so post-edit LSP errors still render
|
||||
if (obj.diagnostics) result.diagnostics = obj.diagnostics
|
||||
return result
|
||||
}
|
||||
|
||||
/** Strip heavy metadata from a single edit tool part; pass-through for all other part types. */
|
||||
private slimPart<T>(part: T): T {
|
||||
if (!this.slimEditMetadata) return part
|
||||
if (!part || typeof part !== "object") return part
|
||||
|
||||
const obj = part as Record<string, unknown>
|
||||
if (obj.type !== "tool" || obj.tool !== "edit") return part
|
||||
|
||||
const state = obj.state
|
||||
if (!state || typeof state !== "object") return part
|
||||
|
||||
const next = { ...(state as Record<string, unknown>) }
|
||||
const meta = this.slimMeta(next.metadata)
|
||||
if (meta) next.metadata = meta
|
||||
else delete next.metadata
|
||||
|
||||
return {
|
||||
...obj,
|
||||
state: next,
|
||||
} as T
|
||||
}
|
||||
|
||||
private slimParts<T>(parts: T[]) {
|
||||
if (!this.slimEditMetadata) return parts
|
||||
return parts.map((part) => this.slimPart(part))
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronize current extension-side state to the webview.
|
||||
* This is primarily used after a webview refresh where early postMessage calls
|
||||
@@ -939,7 +999,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
|
||||
const messages = messagesData.map((m) => ({
|
||||
...m.info,
|
||||
parts: m.parts,
|
||||
parts: this.slimParts(m.parts),
|
||||
createdAt: new Date(m.info.time.created).toISOString(),
|
||||
}))
|
||||
|
||||
@@ -988,7 +1048,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
|
||||
const messages = messagesData.map((m) => ({
|
||||
...m.info,
|
||||
parts: m.parts,
|
||||
parts: this.slimParts(m.parts),
|
||||
createdAt: new Date(m.info.time.created).toISOString(),
|
||||
}))
|
||||
|
||||
@@ -1002,6 +1062,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
messages,
|
||||
})
|
||||
} catch (err) {
|
||||
this.syncedChildSessions.delete(sessionID)
|
||||
console.error("[Kilo New] KiloProvider: Failed to sync child session:", err)
|
||||
}
|
||||
}
|
||||
@@ -2281,6 +2342,13 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
|
||||
const msg = mapSSEEventToWebviewMessage(event, sessionID)
|
||||
if (msg) {
|
||||
if (msg.type === "partUpdated") {
|
||||
this.postMessage({
|
||||
...msg,
|
||||
part: this.slimPart(msg.part),
|
||||
})
|
||||
return
|
||||
}
|
||||
this.postMessage(msg)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,7 +148,9 @@ export class AgentManagerProvider implements vscode.Disposable {
|
||||
|
||||
panel.webview.html = this.getHtml(panel.webview)
|
||||
|
||||
this.provider = new KiloProvider(this.extensionUri, this.connectionService, this.context)
|
||||
this.provider = new KiloProvider(this.extensionUri, this.connectionService, this.context, {
|
||||
slimEditMetadata: true,
|
||||
})
|
||||
this.provider.attachToWebview(panel.webview, {
|
||||
onBeforeMessage: (msg) => this.onMessage(msg),
|
||||
})
|
||||
|
||||
@@ -67,25 +67,16 @@ export const DataBridge: Component<{ children: any }> = (props) => {
|
||||
|
||||
const data = createMemo(() => {
|
||||
const id = session.currentSessionID()
|
||||
const allParts = session.allParts()
|
||||
// Expose ALL session messages (including child sessions from sub-agents),
|
||||
// not just the current session. This lets VscodeSessionTurn and
|
||||
// TaskToolExpanded read child session data from the DataProvider store.
|
||||
const allMessages = Object.fromEntries(
|
||||
Object.entries(session.allMessages() as Record<string, SDKMessage[]>)
|
||||
.filter(([, msgs]) => (msgs as SDKMessage[]).length > 0)
|
||||
.map(([sid, msgs]) => [sid, msgs as SDKMessage[]]),
|
||||
)
|
||||
const family = session.familyData(id)
|
||||
return {
|
||||
session: session.sessions().map((s) => ({ ...s, id: s.id, role: "user" as const })) as unknown as any[],
|
||||
session_status: session.allStatusMap() as unknown as Record<string, any>,
|
||||
session_status: family.status as unknown as Record<string, any>,
|
||||
session_diff: {} as Record<string, any[]>,
|
||||
message: allMessages,
|
||||
part: Object.fromEntries(
|
||||
Object.entries(allParts)
|
||||
.filter(([, parts]) => (parts as SDKPart[]).length > 0)
|
||||
.map(([msgId, parts]) => [msgId, parts as unknown as SDKPart[]]),
|
||||
),
|
||||
// Restrict chat data to the selected session family (self + subagents).
|
||||
// This keeps unrelated tracked sessions from invalidating the visible
|
||||
// chat tree during streaming or background updates.
|
||||
message: family.messages as Record<string, SDKMessage[]>,
|
||||
part: family.parts as Record<string, SDKPart[]>,
|
||||
permission: (() => {
|
||||
const grouped: Record<string, any[]> = {}
|
||||
for (const p of session.permissions()) {
|
||||
|
||||
@@ -49,7 +49,8 @@ const TaskToolRenderer: Component<ToolProps> = (props) => {
|
||||
|
||||
const running = createMemo(() => props.status === "pending" || props.status === "running")
|
||||
|
||||
// Sync child session into store whenever we have a sessionId
|
||||
// Warm child session data immediately so completed task tools already have
|
||||
// their compact child tool list available when the user expands them.
|
||||
createEffect(() => {
|
||||
const id = childSessionId()
|
||||
if (!id) return
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
Accessor,
|
||||
batch,
|
||||
} from "solid-js"
|
||||
import { createStore, produce } from "solid-js/store"
|
||||
import { createStore, produce, reconcile } from "solid-js/store"
|
||||
import { useVSCode } from "./vscode"
|
||||
import { useServer } from "./server"
|
||||
import { useProvider } from "./provider"
|
||||
@@ -87,6 +87,13 @@ interface SessionContextValue {
|
||||
// All session statuses keyed by sessionID (for DataBridge)
|
||||
allStatusMap: () => Record<string, SessionStatusInfo>
|
||||
|
||||
// Current session family data (self + subagents) for DataBridge
|
||||
familyData: (sessionID: string | undefined) => {
|
||||
messages: Record<string, Message[]>
|
||||
parts: Record<string, Part[]>
|
||||
status: Record<string, SessionStatusInfo>
|
||||
}
|
||||
|
||||
// Parts for a specific message
|
||||
getParts: (messageID: string) => Part[]
|
||||
|
||||
@@ -197,6 +204,7 @@ export const SessionProvider: ParentComponent = (props) => {
|
||||
}
|
||||
|
||||
const [loading, setLoading] = createSignal(false)
|
||||
const [loaded, setLoaded] = createSignal<Set<string>>(new Set())
|
||||
|
||||
// Pending permissions
|
||||
const [permissions, setPermissions] = createSignal<PermissionRequest[]>([])
|
||||
@@ -566,6 +574,12 @@ export const SessionProvider: ParentComponent = (props) => {
|
||||
|
||||
function handleMessagesLoaded(sessionID: string, messages: Message[]) {
|
||||
batch(() => {
|
||||
setLoaded((prev) => {
|
||||
if (prev.has(sessionID)) return prev
|
||||
const next = new Set(prev)
|
||||
next.add(sessionID)
|
||||
return next
|
||||
})
|
||||
if (sessionID === currentSessionID()) setLoading(false)
|
||||
|
||||
// Preserve optimistic messages that haven't been confirmed yet.
|
||||
@@ -576,15 +590,15 @@ export const SessionProvider: ParentComponent = (props) => {
|
||||
const loadedIds = new Set(messages.map((m) => m.id))
|
||||
const current = store.messages[sessionID] ?? []
|
||||
const orphans = current.filter((m) => pending.has(m.id) && !loadedIds.has(m.id))
|
||||
setStore("messages", sessionID, [...messages, ...orphans])
|
||||
setStore("messages", sessionID, reconcile([...messages, ...orphans], { key: "id" }))
|
||||
} else {
|
||||
setStore("messages", sessionID, messages)
|
||||
setStore("messages", sessionID, reconcile(messages, { key: "id" }))
|
||||
}
|
||||
|
||||
// Also extract parts from messages
|
||||
for (const msg of messages) {
|
||||
if (msg.parts && msg.parts.length > 0) {
|
||||
setStore("parts", msg.id, msg.parts)
|
||||
setStore("parts", msg.id, reconcile(msg.parts, { key: "id" }))
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -806,6 +820,42 @@ export const SessionProvider: ParentComponent = (props) => {
|
||||
return family
|
||||
}
|
||||
|
||||
function familyData(sessionID: string | undefined) {
|
||||
if (!sessionID) {
|
||||
return {
|
||||
messages: {},
|
||||
parts: {},
|
||||
status: {},
|
||||
}
|
||||
}
|
||||
|
||||
const family = sessionFamily(sessionID)
|
||||
const messages: Record<string, Message[]> = {}
|
||||
const parts: Record<string, Part[]> = {}
|
||||
const status: Record<string, SessionStatusInfo> = {}
|
||||
|
||||
for (const sid of family) {
|
||||
const msgs = store.messages[sid]
|
||||
if (msgs?.length) {
|
||||
messages[sid] = msgs
|
||||
for (const msg of msgs) {
|
||||
const item = store.parts[msg.id]
|
||||
if (!item?.length) continue
|
||||
parts[msg.id] = item
|
||||
}
|
||||
}
|
||||
|
||||
const info = statusMap[sid]
|
||||
if (info) status[sid] = info
|
||||
}
|
||||
|
||||
return {
|
||||
messages,
|
||||
parts,
|
||||
status,
|
||||
}
|
||||
}
|
||||
|
||||
/** Return permissions scoped to the given session's family (self + subagents). */
|
||||
function scopedPermissions(sessionID: string | undefined): PermissionRequest[] {
|
||||
if (!sessionID) return []
|
||||
@@ -930,6 +980,12 @@ export const SessionProvider: ParentComponent = (props) => {
|
||||
if (cloudPreviewId() !== cloudSessionId) return
|
||||
const key = `cloud:${cloudSessionId}`
|
||||
batch(() => {
|
||||
setLoaded((prev) => {
|
||||
if (prev.has(key)) return prev
|
||||
const next = new Set(prev)
|
||||
next.add(key)
|
||||
return next
|
||||
})
|
||||
setStore("sessions", key, {
|
||||
id: key,
|
||||
title,
|
||||
@@ -951,6 +1007,12 @@ export const SessionProvider: ParentComponent = (props) => {
|
||||
const cloudKey = `cloud:${cloudSessionId}`
|
||||
const cloudMessages = store.messages[cloudKey] ?? []
|
||||
batch(() => {
|
||||
setLoaded((prev) => {
|
||||
const next = new Set(prev)
|
||||
next.add(session.id)
|
||||
next.delete(cloudKey)
|
||||
return next
|
||||
})
|
||||
setStore("sessions", session.id, session)
|
||||
|
||||
const pendingAgent = pendingAgentSelection()
|
||||
@@ -1223,7 +1285,7 @@ export const SessionProvider: ParentComponent = (props) => {
|
||||
return
|
||||
}
|
||||
setCurrentSessionID(id)
|
||||
setLoading(true)
|
||||
setLoading(!loaded().has(id))
|
||||
vscode.postMessage({ type: "loadMessages", sessionID: id })
|
||||
}
|
||||
|
||||
@@ -1251,6 +1313,12 @@ export const SessionProvider: ParentComponent = (props) => {
|
||||
delete sessions[id]
|
||||
}),
|
||||
)
|
||||
setLoaded((prev) => {
|
||||
if (!prev.has(id)) return prev
|
||||
const next = new Set(prev)
|
||||
next.delete(id)
|
||||
return next
|
||||
})
|
||||
vscode.postMessage({ type: "deleteSession", sessionID: id })
|
||||
}
|
||||
|
||||
@@ -1421,6 +1489,7 @@ export const SessionProvider: ParentComponent = (props) => {
|
||||
allMessages,
|
||||
allParts,
|
||||
allStatusMap,
|
||||
familyData,
|
||||
variantList,
|
||||
currentVariant,
|
||||
selectVariant,
|
||||
|
||||
@@ -131,6 +131,7 @@ export function mockSessionValue(overrides?: {
|
||||
allMessages: () => ({}),
|
||||
allParts: () => ({}),
|
||||
allStatusMap: () => ({}),
|
||||
familyData: () => ({ messages: {}, parts: {}, status: {} }),
|
||||
getParts: () => [],
|
||||
todos: () => [],
|
||||
permissions: () => permissions,
|
||||
|
||||
@@ -61,6 +61,7 @@ const WithSessions: ParentComponent<{ sessions?: typeof mockSessions }> = (props
|
||||
allMessages: () => ({}),
|
||||
allParts: () => ({}),
|
||||
allStatusMap: () => ({}),
|
||||
familyData: () => ({ messages: {}, parts: {}, status: {} }),
|
||||
getParts: () => [],
|
||||
todos: () => [],
|
||||
permissions: () => [],
|
||||
|
||||
Reference in New Issue
Block a user