فهرست منبع

fix(app): patch tool diff rendering

Adam 1 هفته پیش
والد
کامیت
d98be39344

+ 43 - 0
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")
+  })
+})

+ 78 - 0
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)
+}

+ 4 - 25
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({
                           <Accordion.Content>
                             <Show when={visible()}>
                               <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 }}
-                                />
+                                <Dynamic component={fileComponent} mode="diff" fileDiff={file.view.fileDiff} />
                               </div>
                             </Show>
                           </Accordion.Content>
@@ -2212,12 +2196,7 @@ ToolRegistry.register({
               }
             >
               <div data-component="apply-patch-file-diff">
-                <Dynamic
-                  component={fileComponent}
-                  mode="diff"
-                  before={{ name: single()!.filePath, contents: single()!.before }}
-                  after={{ name: single()!.movePath ?? single()!.filePath, contents: single()!.after }}
-                />
+                <Dynamic component={fileComponent} mode="diff" fileDiff={single()!.view.fileDiff} />
               </div>
             </ToolFileAccordion>
           </BasicTool>