Clean up read tool activity content

This commit is contained in:
Mohamed Boudra
2026-05-25 21:08:11 +07:00
parent 893dd6d1fe
commit d271597ee8
2 changed files with 92 additions and 1 deletions
@@ -182,6 +182,36 @@ describe("opencode tool-call mapper", () => {
}
});
it("unwraps OpenCode XML read output into file content", () => {
const item = expectMapped(
mapOpencodeToolCall({
toolName: "read",
callId: "opencode-read-xml",
status: "completed",
input: { filePath: "/Users/moboudra/dev/paseo/docs/release.md" },
output: [
"<path>/Users/moboudra/dev/paseo/docs/release.md</path>",
"<type>file</type>",
"<content>",
"1: # Release",
"2:",
"3: All workspaces share one version and release together.",
"</content>",
].join("\n"),
}),
);
expect(item.detail).toEqual({
type: "read",
filePath: "/Users/moboudra/dev/paseo/docs/release.md",
content: [
"1: # Release",
"2:",
"3: All workspaces share one version and release together.",
].join("\n"),
});
});
it("maps failed calls with required error", () => {
const item = expectMapped(
mapOpencodeToolCall({
@@ -260,8 +260,69 @@ function flattenReadContent(
return flattenToolReadContent(value);
}
function readXmlTag(value: string, tagName: "path" | "content"): string | undefined {
const match = value.match(new RegExp(`<${tagName}>([\\s\\S]*?)</${tagName}>`, "i"));
return match?.[1];
}
function decodeXmlText(value: string): string {
return value.replace(
/&(?:#(\d+)|#x([0-9a-fA-F]+)|amp|lt|gt|quot|apos);/g,
(entity, decimal, hex) => {
if (decimal) {
return String.fromCodePoint(Number.parseInt(decimal, 10));
}
if (hex) {
return String.fromCodePoint(Number.parseInt(hex, 16));
}
switch (entity) {
case "&amp;":
return "&";
case "&lt;":
return "<";
case "&gt;":
return ">";
case "&quot;":
return '"';
case "&apos;":
return "'";
default:
return entity;
}
},
);
}
function trimOuterLineBreaks(value: string): string {
return value.replace(/^\r?\n/, "").replace(/\r?\n$/, "");
}
function parseXmlReadOutput(value: string): ToolReadOutputValue | undefined {
const trimmed = value.trim();
if (!trimmed.startsWith("<path>") || !/<content>/i.test(trimmed)) {
return undefined;
}
const filePath = nonEmptyString(readXmlTag(trimmed, "path")?.trim());
const content = readXmlTag(trimmed, "content");
if (!filePath && content === undefined) {
return undefined;
}
return {
filePath: filePath ? decodeXmlText(filePath) : undefined,
content: content === undefined ? undefined : decodeXmlText(trimOuterLineBreaks(content)),
};
}
const ToolReadOutputContentSchema = z.union([
z.string().transform((value) => ({ filePath: undefined, content: nonEmptyString(value) })),
z.string().transform((value) => {
const xmlReadOutput = parseXmlReadOutput(value);
if (xmlReadOutput) {
return xmlReadOutput;
}
return { filePath: undefined, content: nonEmptyString(value) };
}),
ToolReadChunkSchema.transform((value) => ({
filePath: undefined,
content: flattenReadContent(value),