feat: Add diff view for edit tool

This commit is contained in:
Johnny Amancio
2026-04-27 12:18:36 +02:00
committed by Johnny Eric Amancio
parent 83b153d6bb
commit e3476be3bb
5 changed files with 81 additions and 12 deletions
@@ -2022,11 +2022,28 @@ 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 canOpenDiff = () => !!data.openDiff && !!path() && (before() !== "" || after() !== "")
const canOpenFile = () => !!data.openFile && !!path()
const handleFileClick = (e: MouseEvent) => {
if (!data.openFile || !props.input.filePath) return
e.stopPropagation()
data.openFile(props.input.filePath)
if (canOpenDiff()) {
data.openDiff!({
file: path(),
before: before(),
after: after(),
additions: props.metadata?.filediff?.additions ?? 0,
deletions: props.metadata?.filediff?.deletions ?? 0,
})
return
}
if (canOpenFile()) {
data.openFile!(path())
}
}
return (
@@ -2049,7 +2066,7 @@ ToolRegistry.register({
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={canOpenDiff() || canOpenFile() ? handleFileClick : undefined}
/>
)}
</Show>
@@ -2072,12 +2089,12 @@ ToolRegistry.register({
component={fileComponent}
mode="diff"
before={{
name: props.metadata?.filediff?.file || props.input.filePath,
contents: props.metadata?.filediff?.before || props.input.oldString,
name: path(),
contents: before(),
}}
after={{
name: props.metadata?.filediff?.file || props.input.filePath,
contents: props.metadata?.filediff?.after || props.input.newString,
name: path(),
contents: after(),
}}
/>
</div>
@@ -2,6 +2,9 @@ import { describe, it, expect } from "bun:test"
import { readFileSync } from "node:fs"
import { join } from "node:path"
const APP_FILE = join(__dirname, "..", "..", "webview-ui", "src", "App.tsx")
const src = readFileSync(APP_FILE, "utf8")
/**
* Static guard against the perf regression fixed in this PR.
*
@@ -28,9 +31,6 @@ import { join } from "node:path"
* `tests/webview-reactivity/databridge-reactivity.test.ts`.
*/
describe("DataBridge shape (perf regression guard)", () => {
const path = join(__dirname, "..", "..", "webview-ui", "src", "App.tsx")
const src = readFileSync(path, "utf8")
it("DataBridge exists in App.tsx", () => {
expect(src).toMatch(/export const DataBridge/)
})
@@ -59,3 +59,16 @@ describe("DataBridge shape (perf regression guard)", () => {
expect(block).toMatch(/get\s+part\s*\(\s*\)\s*\{/)
})
})
describe("DataBridge openDiff wiring (regression guard)", () => {
const openDiffBlock = () => {
const match = src.match(/const\s+openDiff\s*=\s*\(diff:\s*\{[\s\S]*?\n\s*\}\n\n\s*const\s+openUrl/)
expect(match).toBeTruthy()
return match![0]
}
it("wires openDiff to the openDiffVirtual webview message", () => {
expect(openDiffBlock()).toMatch(/postMessage\(\{\s*type:\s*["']openDiffVirtual["']\s*,\s*diff\s*\}\)/)
expect(src).toContain("onOpenDiff={openDiff}")
})
})
@@ -21,6 +21,7 @@ const MONOREPO_ROOT = path.resolve(import.meta.dir, "../../../..")
const KILO_UI_DIR = path.join(MONOREPO_ROOT, "packages/kilo-ui")
const DATA_CONTEXT_FILE = path.join(MONOREPO_ROOT, "packages/ui/src/context/data.tsx")
const MESSAGE_PART_FILE = path.join(MONOREPO_ROOT, "packages/ui/src/components/message-part.tsx")
const KILO_MESSAGE_PART_FILE = path.join(MONOREPO_ROOT, "packages/kilo-ui/src/components/message-part.tsx")
function check(code: string): { ok: boolean; output: string } {
const result = Bun.spawnSync(["bun", "--conditions=browser", "-e", code], {
@@ -118,6 +119,27 @@ describe("DataProvider contract (runtime)", () => {
expect(src).toContain("OpenFileFn")
expect(src).toMatch(/openFile:\s*props\.onOpenFile/)
})
it("DataProvider accepts onOpenDiff prop and exports OpenDiffFn (source)", () => {
// onOpenDiff and OpenDiffFn are `kilocode_change` additions — TypeScript types
// erased at runtime, so we verify via source analysis
const src = fs.readFileSync(DATA_CONTEXT_FILE, "utf-8")
expect(src).toContain("onOpenDiff")
expect(src).toContain("OpenDiffFn")
expect(src).toMatch(/openDiff:\s*props\.onOpenDiff/)
})
})
describe("Edit tool diff-first click contract (source)", () => {
const src = fs.readFileSync(KILO_MESSAGE_PART_FILE, "utf-8")
const editBlockMatch = src.match(/ToolRegistry\.register\(\{\s*name:\s*"edit"[\s\S]*?(?=ToolRegistry\.register\(|$)/)
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/)
})
})
describe("BasicTool export contract (runtime)", () => {
@@ -132,6 +132,10 @@ export const DataBridge: Component<{ children: any }> = (props) => {
vscode.postMessage({ type: "openFile", filePath, line, column })
}
const openDiff = (diff: { file: string; before: string; after: string; additions: number; deletions: number }) => {
vscode.postMessage({ type: "openDiffVirtual", diff })
}
const openUrl = (url: string) => {
vscode.postMessage({ type: "openExternal", url })
}
@@ -151,6 +155,7 @@ export const DataBridge: Component<{ children: any }> = (props) => {
onQuestionReply={reply}
onQuestionReject={reject}
onOpenFile={open}
onOpenDiff={openDiff}
onOpenUrl={openUrl}
>
{props.children}
+14 -2
View File
@@ -30,9 +30,19 @@ export type NavigateToSessionFn = (sessionID: string) => void
export type SessionHrefFn = (sessionID: string) => string
export type OpenFileFn = (filePath: string, line?: number, column?: number) => void // kilocode_change
// kilocode_change start
export type OpenFileFn = (filePath: string, line?: number, column?: number) => void
export type OpenUrlFn = (url: string) => void // kilocode_change
export type OpenDiffFn = (diff: {
file: string
before: string
after: string
additions: number
deletions: number
}) => void
export type OpenUrlFn = (url: string) => void
// kilocode_change end
export const { use: useData, provider: DataProvider } = createSimpleContext({
name: "Data",
@@ -42,6 +52,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
onNavigateToSession?: NavigateToSessionFn
onSessionHref?: SessionHrefFn
onOpenFile?: OpenFileFn // kilocode_change
onOpenDiff?: OpenDiffFn // kilocode_change
onOpenUrl?: OpenUrlFn // kilocode_change
}) => {
return {
@@ -54,6 +65,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
navigateToSession: props.onNavigateToSession,
sessionHref: props.onSessionHref,
openFile: props.onOpenFile, // kilocode_change
openDiff: props.onOpenDiff, // kilocode_change
openUrl: props.onOpenUrl, // kilocode_change
}
},