diff --git a/packages/server/src/server/agent/providers/opencode/tool-call-detail-parser.ts b/packages/server/src/server/agent/providers/opencode/tool-call-detail-parser.ts index afe89b85b4..39a7deb8c9 100644 --- a/packages/server/src/server/agent/providers/opencode/tool-call-detail-parser.ts +++ b/packages/server/src/server/agent/providers/opencode/tool-call-detail-parser.ts @@ -221,6 +221,15 @@ function deriveOpencodeTaskDetail( }; } +function toOpencodeEditInput(value: { filePath: string; oldString?: string; newString?: string }) { + return { + filePath: value.filePath, + oldString: nonEmptyString(value.oldString), + newString: nonEmptyString(value.newString), + unifiedDiff: undefined, + }; +} + const OpencodeEditInputSchema = z.union([ z .object({ @@ -229,12 +238,12 @@ const OpencodeEditInputSchema = z.union([ newString: z.string().optional(), }) .passthrough() - .transform((value) => ({ - filePath: value.filePath, - oldString: nonEmptyString(value.oldString), - newString: nonEmptyString(value.newString), - unifiedDiff: undefined, - })), + .transform(toOpencodeEditInput), + // OpenCode v2's edit tool names the file `path`. + z + .object({ path: z.string(), oldString: z.string(), newString: z.string() }) + .passthrough() + .transform((value) => toOpencodeEditInput({ ...value, filePath: value.path })), ToolEditInputSchema, ]); diff --git a/packages/server/src/server/agent/providers/opencode/v2/timeline.test.ts b/packages/server/src/server/agent/providers/opencode/v2/timeline.test.ts new file mode 100644 index 0000000000..1bbd86490a --- /dev/null +++ b/packages/server/src/server/agent/providers/opencode/v2/timeline.test.ts @@ -0,0 +1,56 @@ +import type { SessionMessageAssistant } from "@opencode/client"; +import { expect, test } from "vitest"; +import { V2Timeline } from "./timeline.js"; + +function assistant(content: SessionMessageAssistant["content"]): SessionMessageAssistant { + return { + id: "answer", + type: "assistant", + agent: "build", + model: { providerID: "test", id: "model" }, + time: { created: 2 }, + content, + }; +} + +test("a completed edit carries the replaced and replacement text", () => { + // Captured from OpenCode 2.0.18, whose edit tool takes `path` instead of `filePath`. + const edit = assistant([ + { + type: "tool", + id: "call-edit", + name: "edit", + executed: false, + state: { + status: "completed", + input: { path: "a.txt", oldString: "beta", newString: "BETA" }, + content: [{ type: "text", text: "Edited a.txt (1 replacement)" }], + metadata: { + files: [ + { + file: "a.txt", + patch: + "Index: a.txt\n===================================================================\n--- a.txt\n+++ a.txt\n@@ -1,3 +1,3 @@\n alpha\n-beta\n+BETA\n gamma\n", + status: "modified", + additions: 1, + deletions: 1, + }, + ], + truncated: false, + }, + }, + time: { created: 2, ran: 2, completed: 2 }, + }, + ]); + + expect(new V2Timeline().messages([edit])).toMatchObject([ + { + item: { + type: "tool_call", + name: "edit", + status: "completed", + detail: { type: "edit", filePath: "a.txt", oldString: "beta", newString: "BETA" }, + }, + }, + ]); +});