Render opencode Edit tool calls as diffs

opencode's Edit tool sends camelCase input keys (filePath, oldString,
newString) and a plain-string success ack on output. The shared edit
schemas expect snake_case input and an object output, so calls fell
through to the generic "unknown" renderer (raw JSON Input/Output).

Normalize both at the opencode boundary so the quirk does not leak into
the cross-provider schema.
This commit is contained in:
Mohamed Boudra
2026-05-27 22:43:08 +07:00
parent a025f17a73
commit e3eb333ddc
2 changed files with 52 additions and 5 deletions
@@ -219,6 +219,25 @@ function deriveOpencodeTaskDetail(
}; };
} }
const OpencodeEditInputSchema = z.union([
z
.object({
filePath: z.string(),
oldString: z.string().optional(),
newString: z.string().optional(),
})
.passthrough()
.transform((value) => ({
filePath: value.filePath,
oldString: nonEmptyString(value.oldString),
newString: nonEmptyString(value.newString),
unifiedDiff: undefined,
})),
ToolEditInputSchema,
]);
const OpencodeEditOutputSchema = z.union([z.string().transform(() => null), ToolEditOutputSchema]);
const OpencodeKnownToolDetailSchema = z.union([ const OpencodeKnownToolDetailSchema = z.union([
toolDetailBranchByToolName( toolDetailBranchByToolName(
"shell", "shell",
@@ -264,11 +283,16 @@ const OpencodeKnownToolDetailSchema = z.union([
ToolWriteOutputSchema, ToolWriteOutputSchema,
toWriteToolDetail, toWriteToolDetail,
), ),
toolDetailBranchByToolName("edit", ToolEditInputSchema, ToolEditOutputSchema, toEditToolDetail), toolDetailBranchByToolName(
"edit",
OpencodeEditInputSchema,
OpencodeEditOutputSchema,
toEditToolDetail,
),
toolDetailBranchByToolName( toolDetailBranchByToolName(
"apply_patch", "apply_patch",
ToolEditInputSchema, OpencodeEditInputSchema,
ToolEditOutputSchema, OpencodeEditOutputSchema,
toEditToolDetail, toEditToolDetail,
), ),
toolDetailBranchByToolName( toolDetailBranchByToolName(
@@ -279,8 +303,8 @@ const OpencodeKnownToolDetailSchema = z.union([
), ),
toolDetailBranchByToolName( toolDetailBranchByToolName(
"apply_diff", "apply_diff",
ToolEditInputSchema, OpencodeEditInputSchema,
ToolEditOutputSchema, OpencodeEditOutputSchema,
toEditToolDetail, toEditToolDetail,
), ),
toolDetailBranchByToolName("search", ToolSearchInputSchema, z.unknown(), (input) => toolDetailBranchByToolName("search", ToolSearchInputSchema, z.unknown(), (input) =>
@@ -296,6 +296,29 @@ describe("opencode tool-call mapper", () => {
}); });
}); });
it("maps completed edit calls with OpenCode camelCase input and success text", () => {
const item = expectMapped(
mapOpencodeToolCall({
toolName: "edit",
callId: "opencode-edit-camel",
status: "completed",
input: {
filePath: "/Users/moboudra/dev/paseo/packages/website/src/data/agent-pages.ts",
oldString: 'metaTitle: "Junie agent Mobile and Desktop App, Open Source"',
newString: 'metaTitle: "Junie Agent Mobile and Desktop App, Open Source"',
},
output: "Edit applied successfully.",
}),
);
expect(item.detail).toEqual({
type: "edit",
filePath: "/Users/moboudra/dev/paseo/packages/website/src/data/agent-pages.ts",
oldString: 'metaTitle: "Junie agent Mobile and Desktop App, Open Source"',
newString: 'metaTitle: "Junie Agent Mobile and Desktop App, Open Source"',
});
});
it("maps skill calls to plain text detail with the loaded skill name", () => { it("maps skill calls to plain text detail with the loaded skill name", () => {
const item = expectMapped( const item = expectMapped(
mapOpencodeToolCall({ mapOpencodeToolCall({