diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
index 082eee14cf..cc966b58f8 100644
--- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
+++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
@@ -1385,8 +1385,8 @@ function ToolPart(props: { last: boolean; part: ToolPart; message: AssistantMess
-
-
+
+
@@ -1835,20 +1835,74 @@ function Edit(props: ToolProps) {
)
}
-function Patch(props: ToolProps) {
- const { theme } = useTheme()
+function ApplyPatch(props: ToolProps) {
+ 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 (
+
+
+
+ )
+ }
+
+ 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 (
-
-
-
- {props.output?.trim()}
-
-
+ 0}>
+
+ {(file) => (
+
+
+ -{file.deletions} line{file.deletions !== 1 ? "s" : ""}
+
+ }
+ >
+
+
+
+ )}
+
-
- Patch
+
+ apply_patch
diff --git a/packages/opencode/src/tool/apply_patch.ts b/packages/opencode/src/tool/apply_patch.ts
index 96c27f298e..1eebda697a 100644
--- a/packages/opencode/src/tool/apply_patch.ts
+++ b/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,
}