fix(coding-agent): handled xdev execution errors with mounted tool renderer

- Catch execution errors in XdevRegistry and render them using the mounted tool's error handling.
- Preserve xdev dispatch context when device execution fails.
This commit is contained in:
can1357
2026-07-24 08:02:31 +02:00
parent af9e8546a9
commit 024f49220e
3 changed files with 84 additions and 20 deletions
@@ -6,9 +6,12 @@ import { Settings } from "@oh-my-pi/pi-coding-agent/config/settings";
import * as themeModule from "@oh-my-pi/pi-coding-agent/modes/theme/theme";
import { ToolChoiceQueue } from "@oh-my-pi/pi-coding-agent/session/tool-choice-queue";
import { createTools, type ToolSession } from "@oh-my-pi/pi-coding-agent/tools";
import { writeToolRenderer } from "@oh-my-pi/pi-coding-agent/tools/write";
import { githubToolRenderer } from "@oh-my-pi/pi-coding-agent/tools/gh-renderer";
import { ToolError } from "@oh-my-pi/pi-coding-agent/tools/tool-errors";
import { WriteTool, writeToolRenderer } from "@oh-my-pi/pi-coding-agent/tools/write";
import { XdevRegistry } from "@oh-my-pi/pi-coding-agent/tools/xdev";
import { removeWithRetries } from "@oh-my-pi/pi-utils";
import { type } from "arktype";
// xdev mounting is default-on: discoverable tools like ast_edit unmount into
// xd://, and a plain `write xd://ast_edit` dispatches them. These guard the
@@ -182,6 +185,49 @@ describe("read and write route xd:// device URLs", () => {
expect(rendered).toBeDefined();
});
it("renders device execution errors as the mounted tool instead of write", async () => {
await themeModule.initTheme();
const uiTheme = (await themeModule.getThemeByName("dark")) ?? (await themeModule.getThemeByName("light"));
if (!uiTheme) throw new Error("expected an initialized theme");
const githubDevice = {
name: "github",
label: "GitHub",
description: "fixture",
parameters: type({ op: "string" }),
...githubToolRenderer,
async execute() {
throw new ToolError("gh: Not Found (HTTP 404)");
},
};
const registry = new XdevRegistry([githubDevice]);
const write = new WriteTool(xdevSession(process.cwd(), { xdevRegistry: registry }));
const content = JSON.stringify({ op: "repo_view" });
const result = await write.execute("write-xdev-error", { path: "xd://github", content });
expect(result.isError).toBe(true);
expect(result.details?.xdev).toMatchObject({
tool: "github",
mode: "execute",
args: { op: "repo_view" },
});
const component = writeToolRenderer.renderResult(
result,
{
expanded: false,
isPartial: false,
renderContext: { resolveXdevMounted: name => registry.get(name) },
},
uiTheme,
{ path: "xd://github", content },
);
const rendered = Bun.stripANSI(component.render(80).join("\n"));
expect(rendered).toContain("GitHub Repo");
expect(rendered).toContain("gh: Not Found (HTTP 404)");
expect(rendered).not.toContain("Write");
});
it("docsAll inlines small device docs and falls back to a listing past the caps", async () => {
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "write-xdev-docs-"));
try {