fix(app): patch tool diff rendering

This commit is contained in:
Adam 2026-04-08 13:49:04 -05:00
commit d98be39344
No known key found for this signature in database
GPG key ID: 9CB48779AF150E75
3 changed files with 125 additions and 25 deletions

View file

@ -0,0 +1,43 @@
import { describe, expect, test } from "bun:test"
import { patchFiles } from "./apply-patch-file"
import { text } from "./session-diff"
describe("apply patch file", () => {
test("parses patch metadata from the server", () => {
const file = patchFiles([
{
filePath: "/tmp/a.ts",
relativePath: "a.ts",
type: "update",
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",
additions: 1,
deletions: 1,
},
])[0]
expect(file).toBeDefined()
expect(file?.view.fileDiff.name).toBe("a.ts")
expect(text(file!.view, "deletions")).toBe("one\ntwo\n")
expect(text(file!.view, "additions")).toBe("one\nthree\n")
})
test("keeps legacy before and after payloads working", () => {
const file = patchFiles([
{
filePath: "/tmp/a.ts",
relativePath: "a.ts",
type: "update",
before: "one\n",
after: "two\n",
additions: 1,
deletions: 1,
},
])[0]
expect(file).toBeDefined()
expect(file?.view.patch).toContain("@@ -1,1 +1,1 @@")
expect(text(file!.view, "deletions")).toBe("one\n")
expect(text(file!.view, "additions")).toBe("two\n")
})
})

View file

@ -0,0 +1,78 @@
import { normalize, type ViewDiff } from "./session-diff"
type Kind = "add" | "update" | "delete" | "move"
type Raw = {
filePath?: string
relativePath?: string
type?: Kind
patch?: string
diff?: string
before?: string
after?: string
additions?: number
deletions?: number
movePath?: string
}
export type ApplyPatchFile = {
filePath: string
relativePath: string
type: Kind
additions: number
deletions: number
movePath?: string
view: ViewDiff
}
function kind(value: unknown) {
if (value === "add" || value === "update" || value === "delete" || value === "move") return value
}
function status(type: Kind): "added" | "deleted" | "modified" {
if (type === "add") return "added"
if (type === "delete") return "deleted"
return "modified"
}
export function patchFile(raw: unknown): ApplyPatchFile | undefined {
if (!raw || typeof raw !== "object") return
const value = raw as Raw
const type = kind(value.type)
const filePath = typeof value.filePath === "string" ? value.filePath : undefined
const relativePath = typeof value.relativePath === "string" ? value.relativePath : filePath
const patch = typeof value.patch === "string" ? value.patch : typeof value.diff === "string" ? value.diff : undefined
const before = typeof value.before === "string" ? value.before : undefined
const after = typeof value.after === "string" ? value.after : undefined
if (!type || !filePath || !relativePath) return
if (!patch && before === undefined && after === undefined) return
const additions = typeof value.additions === "number" ? value.additions : 0
const deletions = typeof value.deletions === "number" ? value.deletions : 0
const movePath = typeof value.movePath === "string" ? value.movePath : undefined
return {
filePath,
relativePath,
type,
additions,
deletions,
movePath,
view: normalize({
file: relativePath,
patch,
before,
after,
additions,
deletions,
status: status(type),
}),
}
}
export function patchFiles(raw: unknown) {
if (!Array.isArray(raw)) return []
return raw.map(patchFile).filter((file): file is ApplyPatchFile => !!file)
}

View file

@ -54,6 +54,7 @@ import { Spinner } from "./spinner"
import { TextShimmer } from "./text-shimmer" import { TextShimmer } from "./text-shimmer"
import { AnimatedCountList } from "./tool-count-summary" import { AnimatedCountList } from "./tool-count-summary"
import { ToolStatusTitle } from "./tool-status-title" import { ToolStatusTitle } from "./tool-status-title"
import { patchFiles } from "./apply-patch-file"
import { animate } from "motion" import { animate } from "motion"
import { useLocation } from "@solidjs/router" import { useLocation } from "@solidjs/router"
import { attached, inline, kind } from "./message-file" import { attached, inline, kind } from "./message-file"
@ -2014,24 +2015,12 @@ ToolRegistry.register({
}, },
}) })
interface ApplyPatchFile {
filePath: string
relativePath: string
type: "add" | "update" | "delete" | "move"
diff: string
before: string
after: string
additions: number
deletions: number
movePath?: string
}
ToolRegistry.register({ ToolRegistry.register({
name: "apply_patch", name: "apply_patch",
render(props) { render(props) {
const i18n = useI18n() const i18n = useI18n()
const fileComponent = useFileComponent() const fileComponent = useFileComponent()
const files = createMemo(() => (props.metadata.files ?? []) as ApplyPatchFile[]) const files = createMemo(() => patchFiles(props.metadata.files))
const pending = createMemo(() => props.status === "pending" || props.status === "running") const pending = createMemo(() => props.status === "pending" || props.status === "running")
const single = createMemo(() => { const single = createMemo(() => {
const list = files() const list = files()
@ -2137,12 +2126,7 @@ ToolRegistry.register({
<Accordion.Content> <Accordion.Content>
<Show when={visible()}> <Show when={visible()}>
<div data-component="apply-patch-file-diff"> <div data-component="apply-patch-file-diff">
<Dynamic <Dynamic component={fileComponent} mode="diff" fileDiff={file.view.fileDiff} />
component={fileComponent}
mode="diff"
before={{ name: file.filePath, contents: file.before }}
after={{ name: file.movePath ?? file.filePath, contents: file.after }}
/>
</div> </div>
</Show> </Show>
</Accordion.Content> </Accordion.Content>
@ -2212,12 +2196,7 @@ ToolRegistry.register({
} }
> >
<div data-component="apply-patch-file-diff"> <div data-component="apply-patch-file-diff">
<Dynamic <Dynamic component={fileComponent} mode="diff" fileDiff={single()!.view.fileDiff} />
component={fileComponent}
mode="diff"
before={{ name: single()!.filePath, contents: single()!.before }}
after={{ name: single()!.movePath ?? single()!.filePath, contents: single()!.after }}
/>
</div> </div>
</ToolFileAccordion> </ToolFileAccordion>
</BasicTool> </BasicTool>