From d98be39344b8a39d16b62ce927be71a2c6a61a53 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Wed, 8 Apr 2026 13:49:04 -0500 Subject: [PATCH] fix(app): patch tool diff rendering --- .../src/components/apply-patch-file.test.ts | 43 ++++++++++ .../ui/src/components/apply-patch-file.ts | 78 +++++++++++++++++++ packages/ui/src/components/message-part.tsx | 29 +------ 3 files changed, 125 insertions(+), 25 deletions(-) create mode 100644 packages/ui/src/components/apply-patch-file.test.ts create mode 100644 packages/ui/src/components/apply-patch-file.ts diff --git a/packages/ui/src/components/apply-patch-file.test.ts b/packages/ui/src/components/apply-patch-file.test.ts new file mode 100644 index 0000000000..6c58581564 --- /dev/null +++ b/packages/ui/src/components/apply-patch-file.test.ts @@ -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") + }) +}) diff --git a/packages/ui/src/components/apply-patch-file.ts b/packages/ui/src/components/apply-patch-file.ts new file mode 100644 index 0000000000..8e0c540826 --- /dev/null +++ b/packages/ui/src/components/apply-patch-file.ts @@ -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) +} diff --git a/packages/ui/src/components/message-part.tsx b/packages/ui/src/components/message-part.tsx index 3627eca409..02bd80ac9c 100644 --- a/packages/ui/src/components/message-part.tsx +++ b/packages/ui/src/components/message-part.tsx @@ -54,6 +54,7 @@ import { Spinner } from "./spinner" import { TextShimmer } from "./text-shimmer" import { AnimatedCountList } from "./tool-count-summary" import { ToolStatusTitle } from "./tool-status-title" +import { patchFiles } from "./apply-patch-file" import { animate } from "motion" import { useLocation } from "@solidjs/router" 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({ name: "apply_patch", render(props) { const i18n = useI18n() 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 single = createMemo(() => { const list = files() @@ -2137,12 +2126,7 @@ ToolRegistry.register({
- +
@@ -2212,12 +2196,7 @@ ToolRegistry.register({ } >
- +