mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-31 01:37:28 +08:00
fix(vscode): restore inline tool diffs (#9688)
* fix(vscode): restore inline tool diffs * fix(vscode): render tool patches in kilo ui * style: format long regex assignments and add change marker comment Reformat multi-line regex match assignments in kilo-ui-contract test to satisfy line length limits, and annotate the `contents(diff)` call in session-diff with a kilocode_change tracking comment. * fix(kilo-vscode): append trailing newlines to expected diff content assertions Update test expectations in diff-session-source to include trailing newlines in before/after content, matching actual file content behavior. * fix(vscode): guard empty-patch diffs in session turn accordion Match diff-session-source.ts:99 behavior by short-circuiting contents() when the patch is empty (binary or summarized files), so the accordion content stays empty instead of rendering a confusing whitespace-only diff. --------- Co-authored-by: Imanol Maiztegui <imanol.mzd@gmail.com>
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
"@opencode-ai/ui": patch
|
||||
---
|
||||
|
||||
Restore inline diff previews after session reloads while keeping large file contents stripped from VS Code webview messages.
|
||||
@@ -53,6 +53,7 @@ import { busy, createThrottledValue, useToolFade, useContextToolPending } from "
|
||||
import { ContextToolGroupHeader, ContextToolExpandedList, ContextToolRollingResults } from "./context-tool-results"
|
||||
import { ShellRollingResults } from "./shell-rolling-results"
|
||||
import { extractFilePathFromHref } from "../file-path"
|
||||
import { contents } from "./session-diff"
|
||||
|
||||
// Windows CLI tools (e.g. winget) use \r to overwrite progress bars in-place.
|
||||
// Without this, every progress frame renders as a separate visual line.
|
||||
@@ -2118,8 +2119,13 @@ ToolRegistry.register({
|
||||
const filename = () => getFilename(props.input.filePath ?? "")
|
||||
const pending = () => busy(props.status)
|
||||
const reveal = useToolReveal(pending, () => props.reveal !== false)
|
||||
const before = () => props.metadata?.filediff?.before ?? props.input.oldString ?? ""
|
||||
const after = () => props.metadata?.filediff?.after ?? props.input.newString ?? ""
|
||||
const view = createMemo(() => {
|
||||
const diff = props.metadata?.filediff
|
||||
if (!diff?.patch) return
|
||||
return contents(diff)
|
||||
})
|
||||
const before = () => view()?.before ?? props.metadata?.filediff?.before ?? props.input.oldString ?? ""
|
||||
const after = () => view()?.after ?? props.metadata?.filediff?.after ?? props.input.newString ?? ""
|
||||
const canOpenDiff = () => !!data.openDiff && !!path() && (before() !== "" || after() !== "")
|
||||
const canOpenFile = () => !!data.openFile && !!path()
|
||||
|
||||
@@ -2238,10 +2244,25 @@ ToolRegistry.register({
|
||||
const filename = () => getFilename(props.input.filePath ?? "")
|
||||
const pending = () => busy(props.status)
|
||||
const reveal = useToolReveal(pending, () => props.reveal !== false)
|
||||
const view = createMemo(() => {
|
||||
const diff = props.metadata?.filediff
|
||||
if (!diff?.patch) return
|
||||
return contents(diff)
|
||||
})
|
||||
|
||||
const handleFileClick = (e: MouseEvent) => {
|
||||
if (!data.openFile || !props.input.filePath) return
|
||||
e.stopPropagation()
|
||||
if (data.openDiff && view()) {
|
||||
data.openDiff({
|
||||
file: props.metadata?.filediff?.file || props.input.filePath,
|
||||
before: view()!.before,
|
||||
after: view()!.after,
|
||||
additions: props.metadata?.filediff?.additions ?? 0,
|
||||
deletions: props.metadata?.filediff?.deletions ?? 0,
|
||||
})
|
||||
return
|
||||
}
|
||||
if (!data.openFile || !props.input.filePath) return
|
||||
data.openFile(props.input.filePath)
|
||||
}
|
||||
|
||||
@@ -2263,8 +2284,13 @@ ToolRegistry.register({
|
||||
<ToolMetaLine
|
||||
filename={name()}
|
||||
path={props.input.filePath?.includes("/") ? getDirectory(props.input.filePath!) : undefined}
|
||||
changes={props.metadata.filediff}
|
||||
animate={reveal()}
|
||||
onClick={data.openFile && props.input.filePath ? handleFileClick : undefined}
|
||||
onClick={
|
||||
(view() && data.openDiff) || (data.openFile && props.input.filePath)
|
||||
? handleFileClick
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Show>
|
||||
@@ -2273,19 +2299,40 @@ ToolRegistry.register({
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Show when={props.input.content && path()}>
|
||||
<ToolFileAccordion path={path()}>
|
||||
<Show when={(props.input.content || view()) && path()}>
|
||||
<ToolFileAccordion
|
||||
path={path()}
|
||||
actions={
|
||||
<Show when={!pending() && props.metadata.filediff}>
|
||||
{(diff) => <ToolChanges changes={diff()} animate={reveal()} />}
|
||||
</Show>
|
||||
}
|
||||
>
|
||||
<div data-component="write-content">
|
||||
<Dynamic
|
||||
component={fileComponent}
|
||||
mode="text"
|
||||
file={{
|
||||
name: props.input.filePath,
|
||||
contents: props.input.content,
|
||||
cacheKey: checksum(props.input.content),
|
||||
}}
|
||||
overflow="scroll"
|
||||
/>
|
||||
<Show
|
||||
when={view()}
|
||||
fallback={
|
||||
<Dynamic
|
||||
component={fileComponent}
|
||||
mode="text"
|
||||
file={{
|
||||
name: props.input.filePath,
|
||||
contents: props.input.content,
|
||||
cacheKey: checksum(props.input.content),
|
||||
}}
|
||||
overflow="scroll"
|
||||
/>
|
||||
}
|
||||
>
|
||||
{(diff) => (
|
||||
<Dynamic
|
||||
component={fileComponent}
|
||||
mode="diff"
|
||||
before={{ name: props.metadata?.filediff?.file || props.input.filePath, contents: diff().before }}
|
||||
after={{ name: props.metadata?.filediff?.file || props.input.filePath, contents: diff().after }}
|
||||
/>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
</ToolFileAccordion>
|
||||
</Show>
|
||||
@@ -2300,6 +2347,7 @@ interface ApplyPatchFile {
|
||||
filePath: string
|
||||
relativePath: string
|
||||
type: "add" | "update" | "delete" | "move"
|
||||
patch?: string
|
||||
diff: string
|
||||
before?: string
|
||||
after?: string
|
||||
@@ -2315,6 +2363,13 @@ ToolRegistry.register({
|
||||
const i18n = useI18n()
|
||||
const fileComponent = useFileComponent()
|
||||
const files = createMemo(() => (props.metadata.files ?? []) as ApplyPatchFile[])
|
||||
const view = (file: ApplyPatchFile) => {
|
||||
if (file.patch)
|
||||
return contents({ file: file.relativePath, patch: file.patch, additions: file.additions, deletions: file.deletions })
|
||||
if (file.diff)
|
||||
return contents({ file: file.relativePath, patch: file.diff, additions: file.additions, deletions: file.deletions })
|
||||
if (file.before !== undefined || file.after !== undefined) return { before: file.before ?? "", after: file.after ?? "" }
|
||||
}
|
||||
const pending = createMemo(() => busy(props.status))
|
||||
const reveal = useToolReveal(pending, () => props.reveal !== false)
|
||||
const single = createMemo(() => {
|
||||
@@ -2452,15 +2507,17 @@ ToolRegistry.register({
|
||||
</Accordion.Trigger>
|
||||
</StickyAccordionHeader>
|
||||
<Accordion.Content>
|
||||
<Show when={visible() && file.before !== undefined}>
|
||||
<div data-component="apply-patch-file-diff">
|
||||
<Dynamic
|
||||
component={fileComponent}
|
||||
mode="diff"
|
||||
before={{ name: file.filePath, contents: file.before }}
|
||||
after={{ name: file.movePath ?? file.filePath, contents: file.after }}
|
||||
/>
|
||||
</div>
|
||||
<Show when={visible() && view(file)}>
|
||||
{(diff) => (
|
||||
<div data-component="apply-patch-file-diff">
|
||||
<Dynamic
|
||||
component={fileComponent}
|
||||
mode="diff"
|
||||
before={{ name: file.filePath, contents: diff().before }}
|
||||
after={{ name: file.movePath ?? file.filePath, contents: diff().after }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
</Accordion.Content>
|
||||
</Accordion.Item>
|
||||
@@ -2500,15 +2557,17 @@ ToolRegistry.register({
|
||||
</Switch>
|
||||
}
|
||||
>
|
||||
<Show when={file().before !== undefined}>
|
||||
<div data-component="apply-patch-file-diff">
|
||||
<Dynamic
|
||||
component={fileComponent}
|
||||
mode="diff"
|
||||
before={{ name: file().filePath, contents: file().before }}
|
||||
after={{ name: file().movePath ?? file().filePath, contents: file().after }}
|
||||
/>
|
||||
</div>
|
||||
<Show when={view(file())}>
|
||||
{(diff) => (
|
||||
<div data-component="apply-patch-file-diff">
|
||||
<Dynamic
|
||||
component={fileComponent}
|
||||
mode="diff"
|
||||
before={{ name: file().filePath, contents: diff().before }}
|
||||
after={{ name: file().movePath ?? file().filePath, contents: diff().after }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
</ToolFileAccordion>
|
||||
)}
|
||||
|
||||
@@ -1 +1 @@
|
||||
export { normalize, text, type ViewDiff } from "../../../ui/src/components/session-diff"
|
||||
export { contents, normalize, text, type DiffText, type ViewDiff } from "../../../ui/src/components/session-diff"
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
// Max chars to keep for truncated output fields (bash metadata.output etc.)
|
||||
const OUTPUT_CAP = 4000
|
||||
const PATCH_CAP = 64_000
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
@@ -32,11 +33,22 @@ function cap(v: unknown, limit = OUTPUT_CAP): string | undefined {
|
||||
return v.slice(0, limit) + `\n… (truncated, ${v.length - limit} chars omitted)`
|
||||
}
|
||||
|
||||
function patch(v: unknown): string | undefined {
|
||||
if (typeof v !== "string") return undefined
|
||||
if (v.length > PATCH_CAP) return undefined
|
||||
return v
|
||||
}
|
||||
|
||||
function withPatch(v: unknown): { patch: string } | {} {
|
||||
const kept = patch(v)
|
||||
return kept ? { patch: kept } : {}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Per-tool slimmers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** edit: strip filediff.before/after (webview falls back to input.oldString/newString). */
|
||||
/** edit: strip filediff.before/after while preserving bounded patches for inline diffs. */
|
||||
function slimEdit(state: Record<string, unknown>): Record<string, unknown> {
|
||||
const next = { ...state }
|
||||
const meta = state.metadata
|
||||
@@ -50,6 +62,7 @@ function slimEdit(state: Record<string, unknown>): Record<string, unknown> {
|
||||
if (isObj(fd)) {
|
||||
result.filediff = {
|
||||
...(typeof fd.file === "string" ? { file: fd.file } : {}),
|
||||
...withPatch(fd.patch),
|
||||
additions: typeof fd.additions === "number" ? fd.additions : 0,
|
||||
deletions: typeof fd.deletions === "number" ? fd.deletions : 0,
|
||||
}
|
||||
@@ -59,7 +72,7 @@ function slimEdit(state: Record<string, unknown>): Record<string, unknown> {
|
||||
return next
|
||||
}
|
||||
|
||||
/** apply_patch: strip files[].before/after/diff, metadata.diff, + input.patchText. */
|
||||
/** apply_patch: strip full file contents and input patch text while preserving bounded rendered patches. */
|
||||
function slimPatch(state: Record<string, unknown>): Record<string, unknown> {
|
||||
const next = { ...state }
|
||||
const meta = state.metadata
|
||||
@@ -67,14 +80,18 @@ function slimPatch(state: Record<string, unknown>): Record<string, unknown> {
|
||||
const slim: Record<string, unknown> = {}
|
||||
if (meta.diagnostics) slim.diagnostics = meta.diagnostics
|
||||
if (Array.isArray(meta.files)) {
|
||||
slim.files = (meta.files as Record<string, unknown>[]).map((f) => ({
|
||||
filePath: f.filePath,
|
||||
relativePath: f.relativePath,
|
||||
type: f.type,
|
||||
additions: f.additions,
|
||||
deletions: f.deletions,
|
||||
movePath: f.movePath,
|
||||
}))
|
||||
slim.files = (meta.files as Record<string, unknown>[]).map((f) => {
|
||||
const diff = patch(f.patch) ?? patch(f.diff)
|
||||
return {
|
||||
filePath: f.filePath,
|
||||
relativePath: f.relativePath,
|
||||
type: f.type,
|
||||
...withPatch(diff),
|
||||
additions: f.additions,
|
||||
deletions: f.deletions,
|
||||
movePath: f.movePath,
|
||||
}
|
||||
})
|
||||
}
|
||||
next.metadata = slim
|
||||
}
|
||||
@@ -101,6 +118,7 @@ function slimMultiedit(state: Record<string, unknown>): Record<string, unknown>
|
||||
if (isObj(fd)) {
|
||||
rs.filediff = {
|
||||
...(typeof fd.file === "string" ? { file: fd.file } : {}),
|
||||
...withPatch(fd.patch),
|
||||
additions: typeof fd.additions === "number" ? fd.additions : 0,
|
||||
deletions: typeof fd.deletions === "number" ? fd.deletions : 0,
|
||||
}
|
||||
@@ -130,6 +148,7 @@ function slimWrite(state: Record<string, unknown>): Record<string, unknown> {
|
||||
if (isObj(fd)) {
|
||||
slim.filediff = {
|
||||
...(typeof fd.file === "string" ? { file: fd.file } : {}),
|
||||
...withPatch(fd.patch),
|
||||
additions: typeof fd.additions === "number" ? fd.additions : 0,
|
||||
deletions: typeof fd.deletions === "number" ? fd.deletions : 0,
|
||||
}
|
||||
|
||||
@@ -75,8 +75,8 @@ describe("SessionDiffSource.initialFetch", () => {
|
||||
|
||||
const foo = diffsMsg.diffs[0]!
|
||||
expect(foo.file).toBe("foo.ts")
|
||||
expect(foo.before).toBe("keep\nold")
|
||||
expect(foo.after).toBe("keep\nnew")
|
||||
expect(foo.before).toBe("keep\nold\n")
|
||||
expect(foo.after).toBe("keep\nnew\n")
|
||||
expect(foo.additions).toBe(1)
|
||||
expect(foo.deletions).toBe(1)
|
||||
expect(foo.status).toBe("modified")
|
||||
|
||||
@@ -137,8 +137,29 @@ describe("Edit tool diff-first click contract (source)", () => {
|
||||
const editBlock = editBlockMatch?.[0] ?? ""
|
||||
|
||||
it("edit tool derives before/after content from filediff or input", () => {
|
||||
expect(editBlock).toMatch(/filediff\?\.before\s*\?\?.*oldString/)
|
||||
expect(editBlock).toMatch(/filediff\?\.after\s*\?\?.*newString/)
|
||||
expect(editBlock).toContain("contents(diff)")
|
||||
expect(editBlock).toMatch(/view\(\)\?\.before\s*\?\?.*filediff\?\.before\s*\?\?.*oldString/)
|
||||
expect(editBlock).toMatch(/view\(\)\?\.after\s*\?\?.*filediff\?\.after\s*\?\?.*newString/)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Write and apply_patch patch rendering contracts (source)", () => {
|
||||
const src = fs.readFileSync(KILO_MESSAGE_PART_FILE, "utf-8")
|
||||
const writeBlock =
|
||||
src.match(/ToolRegistry\.register\(\{\s*name:\s*"write"[\s\S]*?(?=ToolRegistry\.register\(|$)/)?.[0] ?? ""
|
||||
const patchBlock =
|
||||
src.match(/ToolRegistry\.register\(\{\s*name:\s*"apply_patch"[\s\S]*?(?=ToolRegistry\.register\(|$)/)?.[0] ?? ""
|
||||
|
||||
it("write tool can render from filediff.patch when input.content is stripped", () => {
|
||||
expect(writeBlock).toContain("contents(diff)")
|
||||
expect(writeBlock).toContain("props.input.content || view()")
|
||||
expect(writeBlock).toContain('mode="diff"')
|
||||
})
|
||||
|
||||
it("apply_patch tool can render from patch metadata without before/after", () => {
|
||||
expect(patchBlock).toContain("file.patch")
|
||||
expect(patchBlock).toContain("contents({ file: file.relativePath, patch: file.patch")
|
||||
expect(patchBlock).toContain('mode="diff"')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -13,12 +13,18 @@ function bytes(obj: unknown): number {
|
||||
return JSON.stringify(obj).length
|
||||
}
|
||||
|
||||
function bigPatch() {
|
||||
return `Index: a.ts\n===================================================================\n--- a.ts\t\n+++ a.ts\t\n@@ -1,1 +1,1 @@\n-${"x".repeat(70_000)}\n+${"y".repeat(70_000)}\n`
|
||||
}
|
||||
|
||||
/**
|
||||
* Hard ceiling per slimmed tool state (JSON bytes). Real slimmed parts
|
||||
* should be well under this. If a slimmer leaks even one file-content
|
||||
* field (~50-500 KB each) the test blows past this immediately.
|
||||
*/
|
||||
const MAX_SLIM_BYTES = 10_000
|
||||
const PATCH =
|
||||
"Index: a.ts\n===================================================================\n--- a.ts\t\n+++ a.ts\t\n@@ -1,2 +1,2 @@\n one\n-two\n+three\n"
|
||||
|
||||
const BIG = "x".repeat(200_000) // 200 KB — typical file content size
|
||||
const DIAG = [
|
||||
@@ -50,7 +56,7 @@ describe("slimPart", () => {
|
||||
output: "Edit applied successfully.",
|
||||
metadata: {
|
||||
diff: BIG,
|
||||
filediff: { file: "/a.ts", before: BIG, after: BIG, additions: 3, deletions: 1 },
|
||||
filediff: { file: "/a.ts", patch: PATCH, before: BIG, after: BIG, additions: 3, deletions: 1 },
|
||||
diagnostics: { "/a.ts": DIAG },
|
||||
},
|
||||
})
|
||||
@@ -63,6 +69,7 @@ describe("slimPart", () => {
|
||||
const slim = slimPart(heavy) as Record<string, any>
|
||||
const meta = slim.state.metadata
|
||||
expect(meta.filediff.file).toBe("/a.ts")
|
||||
expect(meta.filediff.patch).toBe(PATCH)
|
||||
expect(meta.filediff.additions).toBe(3)
|
||||
expect(meta.filediff.deletions).toBe(1)
|
||||
expect(meta.diagnostics).toEqual({ "/a.ts": DIAG })
|
||||
@@ -81,6 +88,19 @@ describe("slimPart", () => {
|
||||
})
|
||||
expect(bytes(slimPart(withUnknown))).toBeLessThan(MAX_SLIM_BYTES)
|
||||
})
|
||||
|
||||
it("drops oversized filediff patches", () => {
|
||||
const wide = part("edit", {
|
||||
...heavy.state,
|
||||
metadata: {
|
||||
...(heavy.state.metadata as object),
|
||||
filediff: { file: "/a.ts", patch: bigPatch(), before: BIG, after: BIG, additions: 1, deletions: 1 },
|
||||
},
|
||||
})
|
||||
const slim = slimPart(wide) as Record<string, any>
|
||||
expect(slim.state.metadata.filediff.patch).toBeUndefined()
|
||||
expect(bytes(slim)).toBeLessThan(MAX_SLIM_BYTES)
|
||||
})
|
||||
})
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
@@ -100,6 +120,7 @@ describe("slimPart", () => {
|
||||
type: "update",
|
||||
before: BIG,
|
||||
after: BIG,
|
||||
patch: PATCH,
|
||||
diff: BIG,
|
||||
additions: 5,
|
||||
deletions: 2,
|
||||
@@ -130,6 +151,7 @@ describe("slimPart", () => {
|
||||
expect(meta.files[0].filePath).toBe("/a.ts")
|
||||
expect(meta.files[0].relativePath).toBe("a.ts")
|
||||
expect(meta.files[0].type).toBe("update")
|
||||
expect(meta.files[0].patch).toBe(PATCH)
|
||||
expect(meta.files[0].additions).toBe(5)
|
||||
expect(meta.files[1].type).toBe("add")
|
||||
expect(meta.diagnostics).toEqual({ "/a.ts": DIAG })
|
||||
@@ -163,6 +185,28 @@ describe("slimPart", () => {
|
||||
})
|
||||
expect(bytes(slimPart(withUnknown))).toBeLessThan(MAX_SLIM_BYTES)
|
||||
})
|
||||
|
||||
it("drops oversized per-file patches", () => {
|
||||
const wide = part("apply_patch", {
|
||||
...heavy.state,
|
||||
metadata: {
|
||||
...(heavy.state.metadata as Record<string, unknown>),
|
||||
files: [
|
||||
{
|
||||
filePath: "/a.ts",
|
||||
relativePath: "a.ts",
|
||||
type: "update",
|
||||
patch: bigPatch(),
|
||||
additions: 1,
|
||||
deletions: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
const slim = slimPart(wide) as Record<string, any>
|
||||
expect(slim.state.metadata.files[0].patch).toBeUndefined()
|
||||
expect(bytes(slim)).toBeLessThan(MAX_SLIM_BYTES)
|
||||
})
|
||||
})
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
@@ -178,7 +222,7 @@ describe("slimPart", () => {
|
||||
diagnostics: { "/a.ts": DIAG },
|
||||
results: [
|
||||
{
|
||||
filediff: { file: "/a.ts", before: BIG, after: BIG, additions: 1, deletions: 1 },
|
||||
filediff: { file: "/a.ts", patch: PATCH, before: BIG, after: BIG, additions: 1, deletions: 1 },
|
||||
diagnostics: { "/a.ts": DIAG },
|
||||
diff: BIG,
|
||||
},
|
||||
@@ -195,6 +239,7 @@ describe("slimPart", () => {
|
||||
const slim = slimPart(heavy) as Record<string, any>
|
||||
const meta = slim.state.metadata
|
||||
expect(meta.results[0].filediff.file).toBe("/a.ts")
|
||||
expect(meta.results[0].filediff.patch).toBe(PATCH)
|
||||
expect(meta.results[0].filediff.additions).toBe(1)
|
||||
expect(meta.results[0].diagnostics).toEqual({ "/a.ts": DIAG })
|
||||
expect(meta.results[1].filediff.file).toBe("/b.ts")
|
||||
@@ -233,7 +278,7 @@ describe("slimPart", () => {
|
||||
filepath: "/a.ts",
|
||||
exists: true,
|
||||
diff: BIG,
|
||||
filediff: { file: "/a.ts", before: BIG, after: BIG, additions: 100, deletions: 0 },
|
||||
filediff: { file: "/a.ts", patch: PATCH, before: BIG, after: BIG, additions: 100, deletions: 0 },
|
||||
diagnostics: { "/a.ts": DIAG },
|
||||
},
|
||||
})
|
||||
@@ -248,6 +293,7 @@ describe("slimPart", () => {
|
||||
expect(meta.filepath).toBe("/a.ts")
|
||||
expect(meta.exists).toBe(true)
|
||||
expect(meta.filediff.file).toBe("/a.ts")
|
||||
expect(meta.filediff.patch).toBe(PATCH)
|
||||
expect(meta.filediff.additions).toBe(100)
|
||||
expect(meta.filediff.deletions).toBe(0)
|
||||
expect(meta.diagnostics).toEqual({ "/a.ts": DIAG })
|
||||
|
||||
@@ -12,7 +12,7 @@ import { IconButton } from "@kilocode/kilo-ui/icon-button"
|
||||
import { RadioGroup } from "@kilocode/kilo-ui/radio-group"
|
||||
import { ThemeProvider } from "@kilocode/kilo-ui/theme"
|
||||
import { Tooltip } from "@kilocode/kilo-ui/tooltip"
|
||||
import { normalize, text } from "@kilocode/kilo-ui/session-diff"
|
||||
import { contents } from "@kilocode/kilo-ui/session-diff"
|
||||
import { LanguageProvider, useLanguage } from "../src/context/language"
|
||||
import { ServerProvider, useServer } from "../src/context/server"
|
||||
import { getVSCodeAPI, VSCodeProvider } from "../src/context/vscode"
|
||||
@@ -68,8 +68,8 @@ const DiffVirtualContent: Component = () => {
|
||||
if (!d) return { before: "", after: "" }
|
||||
if (d.before !== undefined || d.after !== undefined) return { before: d.before ?? "", after: d.after ?? "" }
|
||||
if (d.patch) {
|
||||
const view = normalize(d as { file: string; patch: string; additions: number; deletions: number })
|
||||
return { before: text(view, "deletions"), after: text(view, "additions") }
|
||||
const view = contents(d as { file: string; patch: string; additions: number; deletions: number })
|
||||
return { before: view.before, after: view.after }
|
||||
}
|
||||
return { before: "", after: "" }
|
||||
})
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Diff } from "@kilocode/kilo-ui/diff"
|
||||
import { DiffChanges } from "@kilocode/kilo-ui/diff-changes"
|
||||
import { IconButton } from "@kilocode/kilo-ui/icon-button"
|
||||
import { Tooltip } from "@kilocode/kilo-ui/tooltip"
|
||||
import { parsePatch } from "diff"
|
||||
import { contents } from "@kilocode/kilo-ui/session-diff"
|
||||
import type { PermissionFileDiff } from "../../types/messages"
|
||||
import { useVSCode } from "../../context/vscode"
|
||||
|
||||
@@ -11,29 +11,6 @@ interface PermissionDiffProps {
|
||||
filediff: PermissionFileDiff
|
||||
}
|
||||
|
||||
function patchText(patch: string) {
|
||||
const parsed = parsePatch(patch)[0]
|
||||
if (!parsed) return { before: "", after: "" }
|
||||
|
||||
const before: string[] = []
|
||||
const after: string[] = []
|
||||
for (const hunk of parsed.hunks) {
|
||||
for (const line of hunk.lines) {
|
||||
if (line.startsWith("-")) {
|
||||
before.push(line.slice(1))
|
||||
continue
|
||||
}
|
||||
if (line.startsWith("+")) {
|
||||
after.push(line.slice(1))
|
||||
continue
|
||||
}
|
||||
before.push(line.slice(1))
|
||||
after.push(line.slice(1))
|
||||
}
|
||||
}
|
||||
return { before: before.join("\n"), after: after.join("\n") }
|
||||
}
|
||||
|
||||
export const PermissionDiff: Component<PermissionDiffProps> = (props) => {
|
||||
const vscode = useVSCode()
|
||||
const filename = createMemo(() => {
|
||||
@@ -50,7 +27,10 @@ export const PermissionDiff: Component<PermissionDiffProps> = (props) => {
|
||||
const resolved = createMemo(() => {
|
||||
const fd = props.filediff
|
||||
if (fd.before !== undefined || fd.after !== undefined) return { before: fd.before ?? "", after: fd.after ?? "" }
|
||||
if (fd.patch) return patchText(fd.patch)
|
||||
if (fd.patch) {
|
||||
const view = contents(fd)
|
||||
return { before: view.before, after: view.after }
|
||||
}
|
||||
return { before: "", after: "" }
|
||||
})
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ import { Icon } from "@kilocode/kilo-ui/icon"
|
||||
import { StickyAccordionHeader } from "@kilocode/kilo-ui/sticky-accordion-header"
|
||||
import { useData } from "@kilocode/kilo-ui/context/data"
|
||||
import { useFileComponent } from "@kilocode/kilo-ui/context/file"
|
||||
import { normalize } from "@kilocode/kilo-ui/session-diff"
|
||||
import { contents } from "@kilocode/kilo-ui/session-diff"
|
||||
import { useI18n } from "@kilocode/kilo-ui/context/i18n"
|
||||
import { AssistantMessage } from "./AssistantMessage"
|
||||
import type {
|
||||
@@ -280,11 +280,17 @@ export const VscodeSessionTurn: Component<VscodeSessionTurnProps> = (props) => {
|
||||
<Accordion.Content>
|
||||
<Show when={visible()}>
|
||||
<div data-slot="session-turn-diff-view" data-scrollable>
|
||||
<Dynamic
|
||||
component={fileComponent}
|
||||
mode="diff"
|
||||
fileDiff={normalize(diff).fileDiff}
|
||||
/>
|
||||
{(() => {
|
||||
const view = diff.patch === "" ? { before: "", after: "" } : contents(diff)
|
||||
return (
|
||||
<Dynamic
|
||||
component={fileComponent}
|
||||
mode="diff"
|
||||
before={{ name: diff.file, contents: view.before }}
|
||||
after={{ name: diff.file, contents: view.after }}
|
||||
/>
|
||||
)
|
||||
})()}
|
||||
</div>
|
||||
</Show>
|
||||
</Accordion.Content>
|
||||
|
||||
@@ -692,6 +692,17 @@ export const cursor = {
|
||||
|
||||
// kilocode_change start - strip bloated metadata fields from stored parts to prevent multi-MB payloads
|
||||
// This handles both legacy data that was stored with full file contents and keeps the API response lean.
|
||||
function stripPatch(value: unknown) {
|
||||
if (typeof value !== "string") return undefined
|
||||
if (Buffer.byteLength(value) > Snapshot.MAX_DIFF_SIZE) return undefined
|
||||
return value
|
||||
}
|
||||
|
||||
function withPatch(value: unknown) {
|
||||
const kept = stripPatch(value)
|
||||
return kept ? { patch: kept } : {}
|
||||
}
|
||||
|
||||
export function stripPartMetadata(part: Part): Part {
|
||||
// kilocode_change - exported for testing
|
||||
if (part.type !== "tool") return part
|
||||
@@ -703,20 +714,41 @@ export function stripPartMetadata(part: Part): Part {
|
||||
let changed = false
|
||||
let next = meta
|
||||
|
||||
// Strip edit tool's filediff.before/after (full file contents)
|
||||
if (meta.filediff && (meta.filediff.before || meta.filediff.after)) {
|
||||
const { before, after, ...rest } = meta.filediff
|
||||
next = { ...next, filediff: rest }
|
||||
if (meta.diff !== undefined) {
|
||||
const { diff, ...rest } = next
|
||||
next = rest
|
||||
changed = true
|
||||
}
|
||||
|
||||
// Strip apply_patch tool's files[].before/after (full file contents per file)
|
||||
if (Array.isArray(meta.files) && meta.files.length > 0 && meta.files[0]?.before !== undefined) {
|
||||
// Strip edit/write tool filediff.before/after (full file contents) and cap patches.
|
||||
if (meta.filediff) {
|
||||
const { before, after, patch, ...rest } = meta.filediff
|
||||
next = { ...next, filediff: { ...rest, ...withPatch(patch) } }
|
||||
changed = true
|
||||
}
|
||||
|
||||
// Strip apply_patch tool's files[].before/after (full file contents per file) and cap per-file patches.
|
||||
if (Array.isArray(meta.files) && meta.files.length > 0) {
|
||||
next = {
|
||||
...next,
|
||||
files: meta.files.map((f: Record<string, unknown>) => {
|
||||
const { before, after, ...rest } = f
|
||||
return rest
|
||||
const { before, after, patch, diff, ...rest } = f
|
||||
const kept = stripPatch(patch) ?? stripPatch(diff)
|
||||
return { ...rest, ...(kept ? { patch: kept } : {}) }
|
||||
}),
|
||||
}
|
||||
changed = true
|
||||
}
|
||||
|
||||
if (Array.isArray(meta.results) && meta.results.length > 0) {
|
||||
next = {
|
||||
...next,
|
||||
results: meta.results.map((r: Record<string, unknown>) => {
|
||||
const { diff, ...rest } = r
|
||||
if (!r.filediff || typeof r.filediff !== "object") return rest
|
||||
const fd = r.filediff as Record<string, unknown>
|
||||
const { before, after, patch, ...file } = fd
|
||||
return { ...rest, filediff: { ...file, ...withPatch(patch) } }
|
||||
}),
|
||||
}
|
||||
changed = true
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
// kilocode_change - new file
|
||||
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { MessageV2 } from "../../src/session/message-v2"
|
||||
import { SessionID, MessageID, PartID } from "../../src/session/schema"
|
||||
import { Snapshot } from "../../src/snapshot"
|
||||
|
||||
const sessionID = SessionID.make("session")
|
||||
const patch =
|
||||
"Index: a.ts\n===================================================================\n--- a.ts\t\n+++ a.ts\t\n@@ -1,2 +1,2 @@\n one\n-two\n+three\n"
|
||||
|
||||
function blob(size: number) {
|
||||
return "x".repeat(size)
|
||||
}
|
||||
|
||||
function part(tool: string, metadata: Record<string, unknown>): MessageV2.Part {
|
||||
return {
|
||||
id: PartID.make(`p-${tool}`),
|
||||
sessionID,
|
||||
messageID: MessageID.make("m-assistant"),
|
||||
type: "tool",
|
||||
callID: `call-${tool}`,
|
||||
tool,
|
||||
state: {
|
||||
status: "completed",
|
||||
input: {},
|
||||
output: "ok",
|
||||
title: tool,
|
||||
metadata,
|
||||
time: { start: 0, end: 1 },
|
||||
},
|
||||
} as MessageV2.Part
|
||||
}
|
||||
|
||||
describe("session message metadata stripping", () => {
|
||||
test("keeps bounded edit filediff patches and strips heavy fields", () => {
|
||||
const input = part("edit", {
|
||||
diff: blob(200_000),
|
||||
filediff: {
|
||||
file: "a.ts",
|
||||
patch,
|
||||
before: blob(200_000),
|
||||
after: blob(200_000),
|
||||
additions: 1,
|
||||
deletions: 1,
|
||||
},
|
||||
diagnostics: {},
|
||||
})
|
||||
const stripped = MessageV2.stripPartMetadata(input) as Extract<MessageV2.Part, { type: "tool" }>
|
||||
const meta = stripped.state.status === "completed" ? stripped.state.metadata : {}
|
||||
|
||||
expect(meta.diff).toBeUndefined()
|
||||
expect(meta.filediff.before).toBeUndefined()
|
||||
expect(meta.filediff.after).toBeUndefined()
|
||||
expect(meta.filediff.patch).toBe(patch)
|
||||
expect(JSON.stringify(stripped).length).toBeLessThan(10_000)
|
||||
})
|
||||
|
||||
test("keeps bounded write filediff patches and strips heavy fields", () => {
|
||||
const input = part("write", {
|
||||
diff: blob(200_000),
|
||||
filediff: {
|
||||
file: "README.md",
|
||||
patch,
|
||||
before: blob(200_000),
|
||||
after: blob(200_000),
|
||||
additions: 1,
|
||||
deletions: 1,
|
||||
},
|
||||
diagnostics: {},
|
||||
})
|
||||
const stripped = MessageV2.stripPartMetadata(input) as Extract<MessageV2.Part, { type: "tool" }>
|
||||
const meta = stripped.state.status === "completed" ? stripped.state.metadata : {}
|
||||
|
||||
expect(meta.diff).toBeUndefined()
|
||||
expect(meta.filediff.before).toBeUndefined()
|
||||
expect(meta.filediff.after).toBeUndefined()
|
||||
expect(meta.filediff.patch).toBe(patch)
|
||||
expect(JSON.stringify(stripped).length).toBeLessThan(10_000)
|
||||
})
|
||||
|
||||
test("keeps bounded apply_patch per-file patches and strips heavy fields", () => {
|
||||
const input = part("apply_patch", {
|
||||
diff: blob(200_000),
|
||||
files: [
|
||||
{
|
||||
filePath: "/tmp/a.ts",
|
||||
relativePath: "a.ts",
|
||||
type: "update",
|
||||
patch,
|
||||
before: blob(200_000),
|
||||
after: blob(200_000),
|
||||
additions: 1,
|
||||
deletions: 1,
|
||||
},
|
||||
],
|
||||
diagnostics: {},
|
||||
})
|
||||
const stripped = MessageV2.stripPartMetadata(input) as Extract<MessageV2.Part, { type: "tool" }>
|
||||
const meta = stripped.state.status === "completed" ? stripped.state.metadata : {}
|
||||
|
||||
expect(meta.diff).toBeUndefined()
|
||||
expect(meta.files[0].before).toBeUndefined()
|
||||
expect(meta.files[0].after).toBeUndefined()
|
||||
expect(meta.files[0].patch).toBe(patch)
|
||||
expect(JSON.stringify(stripped).length).toBeLessThan(10_000)
|
||||
})
|
||||
|
||||
test("drops oversized tool patches from hydrated session metadata", () => {
|
||||
const wide = `Index: a.ts\n--- a.ts\n+++ a.ts\n@@ -1,1 +1,1 @@\n-${blob(Snapshot.MAX_DIFF_SIZE)}\n+${blob(Snapshot.MAX_DIFF_SIZE)}\n`
|
||||
const input = part("apply_patch", {
|
||||
files: [
|
||||
{
|
||||
filePath: "/tmp/a.ts",
|
||||
relativePath: "a.ts",
|
||||
type: "update",
|
||||
patch: wide,
|
||||
additions: 1,
|
||||
deletions: 1,
|
||||
},
|
||||
],
|
||||
})
|
||||
const stripped = MessageV2.stripPartMetadata(input) as Extract<MessageV2.Part, { type: "tool" }>
|
||||
const meta = stripped.state.status === "completed" ? stripped.state.metadata : {}
|
||||
|
||||
expect(meta.files[0].patch).toBeUndefined()
|
||||
expect(JSON.stringify(stripped).length).toBeLessThan(10_000)
|
||||
})
|
||||
})
|
||||
@@ -14,9 +14,18 @@ type LegacyDiff = {
|
||||
|
||||
type ReviewDiff = SnapshotFileDiff | VcsFileDiff | LegacyDiff
|
||||
|
||||
// kilocode_change start - expose patch text extraction without building FileDiffMetadata on the UI thread
|
||||
export type DiffText = {
|
||||
before: string
|
||||
after: string
|
||||
patch: string
|
||||
}
|
||||
|
||||
export type ViewDiff = {
|
||||
file: string
|
||||
patch: string
|
||||
before: string // kilocode_change
|
||||
after: string // kilocode_change
|
||||
additions: number
|
||||
deletions: number
|
||||
status?: "added" | "deleted" | "modified"
|
||||
@@ -25,7 +34,7 @@ export type ViewDiff = {
|
||||
|
||||
const cache = new Map<string, FileDiffMetadata>()
|
||||
|
||||
function patch(diff: ReviewDiff) {
|
||||
export function contents(diff: ReviewDiff): DiffText {
|
||||
if (typeof diff.patch === "string") {
|
||||
const [patch] = parsePatch(diff.patch)
|
||||
|
||||
@@ -46,7 +55,7 @@ function patch(diff: ReviewDiff) {
|
||||
}
|
||||
}
|
||||
|
||||
return { before: beforeLines.join("\n"), after: afterLines.join("\n"), patch: diff.patch }
|
||||
return { before: beforeLines.join("\n") + "\n", after: afterLines.join("\n") + "\n", patch: diff.patch }
|
||||
}
|
||||
return {
|
||||
before: "before" in diff && typeof diff.before === "string" ? diff.before : "",
|
||||
@@ -64,6 +73,7 @@ function patch(diff: ReviewDiff) {
|
||||
),
|
||||
}
|
||||
}
|
||||
// kilocode_change end
|
||||
|
||||
function file(file: string, patch: string, before: string, after: string) {
|
||||
const hit = cache.get(patch)
|
||||
@@ -75,10 +85,12 @@ function file(file: string, patch: string, before: string, after: string) {
|
||||
}
|
||||
|
||||
export function normalize(diff: ReviewDiff): ViewDiff {
|
||||
const next = patch(diff)
|
||||
const next = contents(diff) // kilocode_change
|
||||
return {
|
||||
file: diff.file,
|
||||
patch: next.patch,
|
||||
file: diff.file, // kilocode_change
|
||||
patch: next.patch, // kilocode_change
|
||||
before: next.before, // kilocode_change
|
||||
after: next.after, // kilocode_change
|
||||
additions: diff.additions,
|
||||
deletions: diff.deletions,
|
||||
status: diff.status,
|
||||
|
||||
Reference in New Issue
Block a user