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, }