Aiden Cline hai 3 meses
pai
achega
f1ec28176f

+ 66 - 12
packages/opencode/src/cli/cmd/tui/routes/session/index.tsx

@@ -1385,8 +1385,8 @@ function ToolPart(props: { last: boolean; part: ToolPart; message: AssistantMess
         <Match when={props.part.tool === "task"}>
           <Task {...toolprops} />
         </Match>
-        <Match when={props.part.tool === "patch"}>
-          <Patch {...toolprops} />
+        <Match when={props.part.tool === "apply_patch"}>
+          <ApplyPatch {...toolprops} />
         </Match>
         <Match when={props.part.tool === "todowrite"}>
           <TodoWrite {...toolprops} />
@@ -1835,20 +1835,74 @@ function Edit(props: ToolProps<typeof EditTool>) {
   )
 }
 
-function Patch(props: ToolProps<typeof ApplyPatchTool>) {
-  const { theme } = useTheme()
+function ApplyPatch(props: ToolProps<typeof ApplyPatchTool>) {
+  const ctx = use()
+  const { theme, syntax } = useTheme()
+
+  const files = createMemo(() => props.metadata.files ?? [])
+
+  const view = createMemo(() => {
+    const diffStyle = ctx.sync.data.config.tui?.diff_style
+    if (diffStyle === "stacked") return "unified"
+    return ctx.width > 120 ? "split" : "unified"
+  })
+
+  function Diff(p: { diff: string; filePath: string }) {
+    return (
+      <box paddingLeft={1}>
+        <diff
+          diff={p.diff}
+          view={view()}
+          filetype={filetype(p.filePath)}
+          syntaxStyle={syntax()}
+          showLineNumbers={true}
+          width="100%"
+          wrapMode={ctx.diffWrapMode()}
+          fg={theme.text}
+          addedBg={theme.diffAddedBg}
+          removedBg={theme.diffRemovedBg}
+          contextBg={theme.diffContextBg}
+          addedSignColor={theme.diffHighlightAdded}
+          removedSignColor={theme.diffHighlightRemoved}
+          lineNumberFg={theme.diffLineNumber}
+          lineNumberBg={theme.diffContextBg}
+          addedLineNumberBg={theme.diffAddedLineNumberBg}
+          removedLineNumberBg={theme.diffRemovedLineNumberBg}
+        />
+      </box>
+    )
+  }
+
+  function title(file: { type: string; relativePath: string; filePath: string; deletions: number }) {
+    if (file.type === "delete") return "# Deleted " + file.relativePath
+    if (file.type === "add") return "# Created " + file.relativePath
+    if (file.type === "move") return "# Moved " + normalizePath(file.filePath) + " → " + file.relativePath
+    return "← Edit " + file.relativePath
+  }
+
   return (
     <Switch>
-      <Match when={props.output !== undefined}>
-        <BlockTool title="# Patch" part={props.part}>
-          <box>
-            <text fg={theme.text}>{props.output?.trim()}</text>
-          </box>
-        </BlockTool>
+      <Match when={files().length > 0}>
+        <For each={files()}>
+          {(file) => (
+            <BlockTool title={title(file)} part={props.part}>
+              <Show
+                when={file.type !== "delete"}
+                fallback={
+                  <text fg={theme.diffRemoved}>
+                    -{file.deletions} line{file.deletions !== 1 ? "s" : ""}
+                  </text>
+                }
+              >
+                <Diff diff={file.diff} filePath={file.filePath} />
+              </Show>
+            </BlockTool>
+          )}
+        </For>
       </Match>
       <Match when={true}>
-        <InlineTool icon="%" pending="Preparing patch..." complete={false} part={props.part}>
-          Patch
+        <InlineTool icon="%" pending="Preparing apply_patch..." complete={false} part={props.part}>
+          apply_patch
         </InlineTool>
       </Match>
     </Switch>

+ 49 - 6
packages/opencode/src/tool/apply_patch.ts

@@ -7,8 +7,9 @@ import { Bus } from "../bus"
 import { FileWatcher } from "../file/watcher"
 import { Instance } from "../project/instance"
 import { Patch } from "../patch"
-import { createTwoFilesPatch } from "diff"
+import { createTwoFilesPatch, diffLines } from "diff"
 import { assertExternalDirectory } from "./external-directory"
+import { trimDiff } from "./edit"
 
 const PatchParams = z.object({
   patchText: z.string().describe("The full patch text that describes all changes to be made"),
@@ -46,6 +47,9 @@ export const ApplyPatchTool = Tool.define("apply_patch", {
       newContent: string
       type: "add" | "update" | "delete" | "move"
       movePath?: string
+      diff: string
+      additions: number
+      deletions: number
     }> = []
 
     let totalDiff = ""
@@ -59,20 +63,30 @@ export const ApplyPatchTool = Tool.define("apply_patch", {
           const oldContent = ""
           const newContent =
             hunk.contents.length === 0 || hunk.contents.endsWith("\n") ? hunk.contents : `${hunk.contents}\n`
-          const diff = createTwoFilesPatch(filePath, filePath, oldContent, newContent)
+          const diff = trimDiff(createTwoFilesPatch(filePath, filePath, oldContent, newContent))
+
+          let additions = 0
+          let deletions = 0
+          for (const change of diffLines(oldContent, newContent)) {
+            if (change.added) additions += change.count || 0
+            if (change.removed) deletions += change.count || 0
+          }
 
           fileChanges.push({
             filePath,
             oldContent,
             newContent,
             type: "add",
+            diff,
+            additions,
+            deletions,
           })
 
           totalDiff += diff + "\n"
           break
         }
 
-        case "update":
+        case "update": {
           // Check if file exists for update
           const stats = await fs.stat(filePath).catch(() => null)
           if (!stats || stats.isDirectory()) {
@@ -92,7 +106,14 @@ export const ApplyPatchTool = Tool.define("apply_patch", {
             throw new Error(`apply_patch verification failed: ${error}`)
           }
 
-          const diff = createTwoFilesPatch(filePath, filePath, oldContent, newContent)
+          const diff = trimDiff(createTwoFilesPatch(filePath, filePath, oldContent, newContent))
+
+          let additions = 0
+          let deletions = 0
+          for (const change of diffLines(oldContent, newContent)) {
+            if (change.added) additions += change.count || 0
+            if (change.removed) deletions += change.count || 0
+          }
 
           const movePath = hunk.move_path ? path.resolve(Instance.directory, hunk.move_path) : undefined
           await assertExternalDirectory(ctx, movePath)
@@ -103,28 +124,38 @@ export const ApplyPatchTool = Tool.define("apply_patch", {
             newContent,
             type: hunk.move_path ? "move" : "update",
             movePath,
+            diff,
+            additions,
+            deletions,
           })
 
           totalDiff += diff + "\n"
           break
+        }
 
-        case "delete":
+        case "delete": {
           // Check if file exists for deletion
           await FileTime.assert(ctx.sessionID, filePath)
           const contentToDelete = await fs.readFile(filePath, "utf-8").catch((error) => {
             throw new Error(`apply_patch verification failed: ${error}`)
           })
-          const deleteDiff = createTwoFilesPatch(filePath, filePath, contentToDelete, "")
+          const deleteDiff = trimDiff(createTwoFilesPatch(filePath, filePath, contentToDelete, ""))
+
+          const deletions = contentToDelete.split("\n").length
 
           fileChanges.push({
             filePath,
             oldContent: contentToDelete,
             newContent: "",
             type: "delete",
+            diff: deleteDiff,
+            additions: 0,
+            deletions,
           })
 
           totalDiff += deleteDiff + "\n"
           break
+        }
       }
     }
 
@@ -196,10 +227,22 @@ export const ApplyPatchTool = Tool.define("apply_patch", {
     })
     const summary = `Success. Updated the following files:\n${summaryLines.join("\n")}`
 
+    // Build per-file metadata for UI rendering
+    const files = fileChanges.map((change) => ({
+      filePath: change.filePath,
+      relativePath: path.relative(Instance.worktree, change.movePath ?? change.filePath),
+      type: change.type,
+      diff: change.diff,
+      additions: change.additions,
+      deletions: change.deletions,
+      movePath: change.movePath,
+    }))
+
     return {
       title: summary,
       metadata: {
         diff: totalDiff,
+        files,
       },
       output: summary,
     }