From 6f211de60340e68f939f2733712b10b10fa31cec Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 7 Aug 2026 22:45:22 -0400 Subject: [PATCH] fix(core): reuse shared patch diff --- packages/core/src/tool/plugin/patch.ts | 24 +++++++++-------------- packages/core/test/tool-patch.test.ts | 27 ++++++++++++++++++++++++-- 2 files changed, 34 insertions(+), 17 deletions(-) diff --git a/packages/core/src/tool/plugin/patch.ts b/packages/core/src/tool/plugin/patch.ts index 9c59d2e116ae..0c22b6bcbf96 100644 --- a/packages/core/src/tool/plugin/patch.ts +++ b/packages/core/src/tool/plugin/patch.ts @@ -3,7 +3,6 @@ export * as PatchTool from "./patch" import type { Context as PluginContext } from "@opencode-ai/plugin/effect/plugin" import { ToolFailure } from "@opencode-ai/ai" import { FileDiff } from "@opencode-ai/schema/file-diff" -import { createTwoFilesPatch, diffLines } from "diff" import { Effect, Result, Schema } from "effect" import path from "path" import { Bom } from "@opencode-ai/util/bom" @@ -15,6 +14,7 @@ import { Location } from "../../location" import { Patch } from "@opencode-ai/util/patch" import { Permission } from "../../permission" import DESCRIPTION from "../patch.txt" +import { fileDiff } from "./file-diff" export const name = "patch" @@ -353,22 +353,16 @@ function errorMessage(error: unknown) { function patchFile(change: Prepared, after = change.after): typeof FileDiff.Info.Type { const target = (change.type === "update" ? change.moveTarget : undefined)?.resource ?? change.target.resource - const patch = trimDiff(createTwoFilesPatch(change.target.absolute, change.target.absolute, change.before, after)) - const counts = - change.type === "delete" - ? { additions: 0, deletions: change.before.split("\n").length } - : diffLines(change.before, after).reduce( - (result, item) => ({ - additions: result.additions + (item.added ? (item.count ?? 0) : 0), - deletions: result.deletions + (item.removed ? (item.count ?? 0) : 0), - }), - { additions: 0, deletions: 0 }, - ) + const diff = fileDiff( + change.target.absolute, + change.before, + after, + change.type === "add" ? "added" : change.type === "delete" ? "deleted" : "modified", + ) return { + ...diff, file: target, - patch, - status: change.type === "add" ? "added" : change.type === "delete" ? "deleted" : "modified", - ...counts, + patch: trimDiff(diff.patch), } } diff --git a/packages/core/test/tool-patch.test.ts b/packages/core/test/tool-patch.test.ts index 053dce63bad0..f6cac4e4f381 100644 --- a/packages/core/test/tool-patch.test.ts +++ b/packages/core/test/tool-patch.test.ts @@ -215,7 +215,7 @@ describe("PatchTool", () => { file: "remove.txt", status: "deleted", additions: 0, - deletions: 2, + deletions: 1, patch: expect.stringContaining("-remove"), }, ], @@ -248,6 +248,29 @@ describe("PatchTool", () => { ), ) + it.live("counts deleted lines with and without a trailing newline", () => + withTempTool((directory, registry) => + Effect.gen(function* () { + yield* Effect.promise(() => + Promise.all([ + fs.writeFile(path.join(directory, "trailing.txt"), "remove\n"), + fs.writeFile(path.join(directory, "unterminated.txt"), "remove"), + ]), + ) + const settled = yield* executeTool( + registry, + call("*** Begin Patch\n*** Delete File: trailing.txt\n*** Delete File: unterminated.txt\n*** End Patch"), + ) + expect(settled.status).toBe("completed") + if (settled.status !== "completed") return + expect(settled.output.files).toMatchObject([ + { file: "trailing.txt", additions: 0, deletions: 1 }, + { file: "unterminated.txt", additions: 0, deletions: 1 }, + ]) + }), + ), + ) + it.live("serializes concurrent patch transactions", () => withTempTool((directory, registry) => { const target = path.join(directory, "concurrent.txt") @@ -446,7 +469,7 @@ describe("PatchTool", () => { { file: "renamed/dir/name.txt", status: "modified", - patch: expect.stringContaining("-old content\n+new content"), + patch: expect.stringContaining(`Index: ${source}`), }, ], })