Jelajahi Sumber

fix(ui): better handle patch file support when rendering patch/edit tools (#26828)

Brendan Allan 3 bulan lalu
induk
melakukan
c933504d9c

+ 33 - 13
packages/ui/src/components/message-part.tsx

@@ -264,6 +264,7 @@ function getDirectory(path: string | undefined) {
 }
 
 import type { IconProps } from "./icon"
+import { normalize } from "./session-diff"
 
 export type ToolInfo = {
   icon: IconProps["name"]
@@ -1878,6 +1879,31 @@ ToolRegistry.register({
     const path = createMemo(() => props.metadata?.filediff?.file || props.input.filePath || "")
     const filename = () => getFilename(props.input.filePath ?? "")
     const pending = () => props.status === "pending" || props.status === "running"
+
+    const fileCompProps = createMemo(() => {
+      try {
+        if (props.metadata?.filediff) {
+          const diff = normalize({
+            ...props.metadata?.filediff,
+            status: "modified",
+          })
+          const fileDiff = diff.fileDiff
+          if (fileDiff) return { fileDiff, hunkSeparators: fileDiff.isPartial ? "simple" : "line-info-basic" }
+        }
+      } catch {}
+
+      return {
+        before: {
+          name: props.metadata?.filediff?.file || props.input.filePath,
+          contents: props.metadata?.filediff?.before || props.input.oldString || "",
+        },
+        after: {
+          name: props.metadata?.filediff?.file || props.input.filePath,
+          contents: props.metadata?.filediff?.after || props.input.newString || "",
+        },
+      }
+    })
+
     return (
       <div data-component="edit-tool">
         <BasicTool
@@ -1919,18 +1945,7 @@ ToolRegistry.register({
               }
             >
               <div data-component="edit-content">
-                <Dynamic
-                  component={fileComponent}
-                  mode="diff"
-                  before={{
-                    name: props.metadata?.filediff?.file || props.input.filePath,
-                    contents: props.metadata?.filediff?.before || props.input.oldString || "",
-                  }}
-                  after={{
-                    name: props.metadata?.filediff?.file || props.input.filePath,
-                    contents: props.metadata?.filediff?.after || props.input.newString || "",
-                  }}
-                />
+                <Dynamic component={fileComponent} mode="diff" {...fileCompProps()} />
               </div>
             </ToolFileAccordion>
           </Show>
@@ -2111,7 +2126,12 @@ ToolRegistry.register({
                           <Accordion.Content>
                             <Show when={visible()}>
                               <div data-component="apply-patch-file-diff">
-                                <Dynamic component={fileComponent} mode="diff" fileDiff={file.view.fileDiff} />
+                                <Dynamic
+                                  component={fileComponent}
+                                  mode="diff"
+                                  fileDiff={file.view.fileDiff}
+                                  hunkSeparators={file.view.fileDiff.isPartial ? "simple" : "line-info-basic"}
+                                />
                               </div>
                             </Show>
                           </Accordion.Content>

+ 13 - 5
packages/ui/src/components/session-diff.ts

@@ -1,4 +1,4 @@
-import { parseDiffFromFile, type FileDiffMetadata } from "@pierre/diffs"
+import { parseDiffFromFile, parsePatchFiles, type FileDiffMetadata } from "@pierre/diffs"
 import { formatPatch, parsePatch, structuredPatch } from "diff"
 import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2"
 
@@ -34,6 +34,8 @@ function patch(diff: ReviewDiff) {
       const afterLines: Array<{ text: string; newline: boolean }> = []
       let previous: "-" | "+" | " " | undefined
 
+      const patchIsPartial = patch.hunks.every((h) => h.oldStart > 1)
+
       for (const hunk of patch.hunks) {
         for (const line of hunk.lines) {
           if (line.startsWith("\\")) {
@@ -67,9 +69,10 @@ function patch(diff: ReviewDiff) {
         before: beforeLines.map((line) => line.text + (line.newline ? "\n" : "")).join(""),
         after: afterLines.map((line) => line.text + (line.newline ? "\n" : "")).join(""),
         patch: diff.patch,
+        patchIsPartial,
       }
     } catch {
-      return { before: "", after: "", patch: diff.patch }
+      return { before: "", after: "", patch: diff.patch, patchIsPartial: false }
     }
   }
   return {
@@ -86,27 +89,32 @@ function patch(diff: ReviewDiff) {
         { context: Number.MAX_SAFE_INTEGER },
       ),
     ),
+    patchIsPartial: false,
   }
 }
 
-function file(file: string, patch: string, before: string, after: string) {
+function file(file: string, patch: string, before: string, after: string, partial = false) {
   const hit = cache.get(patch)
   if (hit) return hit
 
-  const value = parseDiffFromFile({ name: file, contents: before }, { name: file, contents: after })
+  let value: FileDiffMetadata | undefined
+  if (partial) value = parsePatchFiles(patch)[0]?.files[0]
+  if (value === undefined) value = parseDiffFromFile({ name: file, contents: before }, { name: file, contents: after })
+
   cache.set(patch, value)
   return value
 }
 
 export function normalize(diff: ReviewDiff): ViewDiff {
   const next = patch(diff)
+  const fileDiff = file(diff.file, next.patch, next.before, next.after, next.patchIsPartial)
   return {
     file: diff.file,
     patch: next.patch,
     additions: diff.additions,
     deletions: diff.deletions,
     status: diff.status,
-    fileDiff: file(diff.file, next.patch, next.before, next.after),
+    fileDiff,
   }
 }