+
+
+ {action}
+
+ {url && {url}}
+
+
+ {(selector || text) && (
+
+ {selector &&
Selector: {selector}
}
+ {text &&
Text: {text}
}
+
+ )}
+
+ {images && images.length > 0 && (
+
+
+
+ )}
+
+ {status === "running" && !result && (!images || images.length === 0) && (
+
Browser active…
+ )}
+
+ {result && (
+
+ {result}
+
+ )}
+
+ );
+}
diff --git a/packages/client/src/components/tool-renderers/EvalToolRenderer.tsx b/packages/client/src/components/tool-renderers/EvalToolRenderer.tsx
new file mode 100644
index 000000000..d0631e74c
--- /dev/null
+++ b/packages/client/src/components/tool-renderers/EvalToolRenderer.tsx
@@ -0,0 +1,60 @@
+import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
+import { getSyntaxTheme } from "../../lib/syntax-theme.js";
+import { useThemeContext } from "../ThemeProvider.js";
+import type { ToolRendererProps } from "./types.js";
+
+export function EvalToolRenderer({ args, status, result }: ToolRendererProps) {
+ let syntaxStyle;
+ try {
+ const { resolved: theme, themeName } = useThemeContext();
+ syntaxStyle = getSyntaxTheme(theme, themeName);
+ } catch {
+ syntaxStyle = undefined;
+ }
+
+ const language = (args?.language as string) || "py";
+ const code = (args?.code as string) || (args?.raw as string);
+ const title = args?.title as string | undefined;
+
+ return (
+
+
+
+ github {op ?? ""}
+
+ {repo && {repo}}
+ {issueOrPr && #{issueOrPr}}
+
+
+ {title && (
+
+ {title}
+
+ )}
+
+ {status === "running" && !result && (
+
GitHub request in progress…
+ )}
+
+ {result && (
+
+ {result}
+
+ )}
+
+ );
+}
diff --git a/packages/client/src/components/tool-renderers/GoalToolRenderer.tsx b/packages/client/src/components/tool-renderers/GoalToolRenderer.tsx
new file mode 100644
index 000000000..e12c2a54b
--- /dev/null
+++ b/packages/client/src/components/tool-renderers/GoalToolRenderer.tsx
@@ -0,0 +1,39 @@
+import type { ToolRendererProps } from "./types.js";
+
+export function GoalToolRenderer({ args, status, result }: ToolRendererProps) {
+ const objective = (args?.objective ?? args?.goal ?? args?.title) as string | undefined;
+ const goalStatus = (args?.status as string) || "active";
+
+ const statusStyles: Record = {
+ active: "bg-sky-500/20 text-sky-300",
+ accomplished: "bg-emerald-500/20 text-emerald-300",
+ abandoned: "bg-rose-500/20 text-rose-300",
+ };
+
+ return (
+
+
+
+ {goalStatus}
+
+ Goal
+
+
+ {objective && (
+
+ {objective}
+
+ )}
+
+ {status === "running" && !result && (
+
Evaluating goal…
+ )}
+
+ {result && (
+
+ {result}
+
+ )}
+
+ );
+}
diff --git a/packages/client/src/components/tool-renderers/HubToolRenderer.tsx b/packages/client/src/components/tool-renderers/HubToolRenderer.tsx
new file mode 100644
index 000000000..ea92b34ad
--- /dev/null
+++ b/packages/client/src/components/tool-renderers/HubToolRenderer.tsx
@@ -0,0 +1,38 @@
+import type { ToolRendererProps } from "./types.js";
+
+export function HubToolRenderer({ args, status, result }: ToolRendererProps) {
+ const op = (args?.op as string) || "hub";
+ const target = (args?.to as string) || (args?.from as string) || (args?.name as string) || (args?.id as string);
+ const message = (args?.message as string) || (args?.text as string);
+
+ return (
+
+
+
+ {op}
+
+ {target && (
+
+ target: {target}
+
+ )}
+
+
+ {message && (
+
+ {message}
+
+ )}
+
+ {status === "running" && !result && (
+
Hub operating…
+ )}
+
+ {result && (
+
+ {result}
+
+ )}
+
+ );
+}
diff --git a/packages/client/src/components/tool-renderers/ImageToolRenderer.tsx b/packages/client/src/components/tool-renderers/ImageToolRenderer.tsx
new file mode 100644
index 000000000..7fab6926b
--- /dev/null
+++ b/packages/client/src/components/tool-renderers/ImageToolRenderer.tsx
@@ -0,0 +1,33 @@
+import { ToolResultImages } from "./ToolResultImages.js";
+import type { ToolRendererProps } from "./types.js";
+
+export function ImageToolRenderer({ toolName, args, status, result, images }: ToolRendererProps) {
+ const promptOrPath = (args?.prompt ?? args?.path ?? args?.url) as string | undefined;
+
+ return (
+
+
+
+ {toolName}
+
+ {promptOrPath && "{promptOrPath}"}
+
+
+ {images && images.length > 0 && (
+
+
+
+ )}
+
+ {status === "running" && !result && (!images || images.length === 0) && (
+
Processing image…
+ )}
+
+ {result && (
+
+ {result}
+
+ )}
+
+ );
+}
diff --git a/packages/client/src/components/tool-renderers/KnowledgeToolRenderer.tsx b/packages/client/src/components/tool-renderers/KnowledgeToolRenderer.tsx
new file mode 100644
index 000000000..1a0954d57
--- /dev/null
+++ b/packages/client/src/components/tool-renderers/KnowledgeToolRenderer.tsx
@@ -0,0 +1,33 @@
+import type { ToolRendererProps } from "./types.js";
+
+export function KnowledgeToolRenderer({ toolName, args, status, result }: ToolRendererProps) {
+ const keyOrQuery = (args?.key ?? args?.query ?? args?.topic ?? args?.subject) as string | undefined;
+ const value = (args?.value ?? args?.content ?? args?.memory) as string | undefined;
+
+ return (
+
+
+
+ {toolName}
+
+ {keyOrQuery && {keyOrQuery}}
+
+
+ {value && (
+
+ {value}
+
+ )}
+
+ {status === "running" && !result && !value && (
+
Accessing knowledge base…
+ )}
+
+ {result && (
+
+ {result}
+
+ )}
+
+ );
+}
diff --git a/packages/client/src/components/tool-renderers/LspToolRenderer.tsx b/packages/client/src/components/tool-renderers/LspToolRenderer.tsx
new file mode 100644
index 000000000..9bc3b6c0c
--- /dev/null
+++ b/packages/client/src/components/tool-renderers/LspToolRenderer.tsx
@@ -0,0 +1,40 @@
+import { OpenFileButton } from "./OpenFileButton.js";
+import type { ToolRendererProps } from "./types.js";
+
+export function LspToolRenderer({ args, status, result, context }: ToolRendererProps) {
+ const command = (args?.command ?? args?.action ?? args?.method) as string | undefined;
+ const filePath = (args?.path ?? args?.uri ?? args?.filePath) as string | undefined;
+ const symbol = (args?.symbol ?? args?.query) as string | undefined;
+
+ return (
+
+
+
+ LSP {command ?? ""}
+
+ {filePath && (
+
+ {filePath}
+
+
+ )}
+
+
+ {symbol && (
+
+ symbol: {symbol}
+
+ )}
+
+ {status === "running" && !result && (
+
Querying LSP server…
+ )}
+
+ {result && (
+
+ {result}
+
+ )}
+
+ );
+}
diff --git a/packages/client/src/components/tool-renderers/ResolveRejectToolRenderer.tsx b/packages/client/src/components/tool-renderers/ResolveRejectToolRenderer.tsx
new file mode 100644
index 000000000..008d7a9e3
--- /dev/null
+++ b/packages/client/src/components/tool-renderers/ResolveRejectToolRenderer.tsx
@@ -0,0 +1,32 @@
+import { mdiAlertCircle, mdiCheckCircle } from "@mdi/js";
+import { Icon } from "@mdi/react";
+import type { ToolRendererProps } from "./types.js";
+
+export function ResolveRejectToolRenderer({ toolName, args, result }: ToolRendererProps) {
+ const isResolve = toolName === "resolve";
+ const reason = (args?.reason ?? args?.label ?? args?.action ?? args?.summary) as string | undefined;
+
+ return (
+
+
+
+
+ {isResolve ? "Task Resolved" : "Task Rejected"}
+
+ {reason &&
{reason}
}
+ {result &&
{result}
}
+
+
+ );
+}
diff --git a/packages/client/src/components/tool-renderers/SearchToolRenderer.tsx b/packages/client/src/components/tool-renderers/SearchToolRenderer.tsx
new file mode 100644
index 000000000..9c52ec923
--- /dev/null
+++ b/packages/client/src/components/tool-renderers/SearchToolRenderer.tsx
@@ -0,0 +1,29 @@
+import { LinkifiedText } from "./LinkifiedText.js";
+import type { ToolRendererProps } from "./types.js";
+
+export function SearchToolRenderer({ toolName, args, status, result }: ToolRendererProps) {
+ const query = (args?.query ?? args?.pattern ?? args?.glob ?? args?.search) as string | undefined;
+ const path = (args?.path ?? args?.domain ?? args?.dir) as string | undefined;
+
+ return (
+
+
+
+ {toolName}
+
+ {query && "{query}"}
+ {path && in {path}}
+
+
+ {status === "running" && !result && (
+
Searching…
+ )}
+
+ {result && (
+
+
+
+ )}
+
+ );
+}
diff --git a/packages/client/src/components/tool-renderers/TaskToolRenderer.tsx b/packages/client/src/components/tool-renderers/TaskToolRenderer.tsx
new file mode 100644
index 000000000..c2e2f7805
--- /dev/null
+++ b/packages/client/src/components/tool-renderers/TaskToolRenderer.tsx
@@ -0,0 +1,37 @@
+import type { ToolRendererProps } from "./types.js";
+
+export function TaskToolRenderer({ args, status, result }: ToolRendererProps) {
+ const name = args?.name as string | undefined;
+ const agent = (args?.agent as string) || "task";
+ const taskDesc = (args?.task as string) || (args?.description as string);
+
+ return (
+
+
+
+ {agent}
+
+ {name && {name}}
+
+
+ {taskDesc && (
+
+ {taskDesc}
+
+ )}
+
+ {status === "running" && !result && (
+
Task running…
+ )}
+
+ {result && (
+
+
Result
+
+ {result}
+
+
+ )}
+
+ );
+}
diff --git a/packages/client/src/components/tool-renderers/ThinkToolRenderer.tsx b/packages/client/src/components/tool-renderers/ThinkToolRenderer.tsx
new file mode 100644
index 000000000..ae8c32cdd
--- /dev/null
+++ b/packages/client/src/components/tool-renderers/ThinkToolRenderer.tsx
@@ -0,0 +1,27 @@
+import type { ToolRendererProps } from "./types.js";
+
+export function ThinkToolRenderer({ args, status, result }: ToolRendererProps) {
+ const thought = (args?.thought ?? args?.reasoning ?? args?.text) as string | undefined;
+
+ return (
+
+
+ Reasoning / Thought
+
+
+ {thought && (
+
+ {thought}
+
+ )}
+
+ {status === "running" && !thought && !result && (
+
Thinking…
+ )}
+
+ {result && (
+
{result}
+ )}
+
+ );
+}
diff --git a/packages/client/src/components/tool-renderers/TodoToolRenderer.tsx b/packages/client/src/components/tool-renderers/TodoToolRenderer.tsx
new file mode 100644
index 000000000..a273332d8
--- /dev/null
+++ b/packages/client/src/components/tool-renderers/TodoToolRenderer.tsx
@@ -0,0 +1,57 @@
+import { mdiCheckboxBlankOutline, mdiCheckboxMarkedOutline } from "@mdi/js";
+import { Icon } from "@mdi/react";
+import type { ToolRendererProps } from "./types.js";
+
+interface TodoItem {
+ id?: string;
+ text?: string;
+ title?: string;
+ completed?: boolean;
+ status?: string;
+}
+
+export function TodoToolRenderer({ args, status, result }: ToolRendererProps) {
+ const action = (args?.action as string) || "todo";
+ const items = (Array.isArray(args?.todos) ? args.todos : Array.isArray(args?.items) ? args.items : []) as TodoItem[];
+ const singleTitle = (args?.title ?? args?.text ?? args?.item) as string | undefined;
+
+ return (
+
+
+
+ {action}
+
+ {singleTitle && {singleTitle}}
+
+
+ {items.length > 0 && (
+
+ {items.map((item, idx) => {
+ const isDone = item.completed || item.status === "completed" || item.status === "done";
+ const label = item.text ?? item.title ?? item.id ?? `Item ${idx + 1}`;
+ return (
+
+
+ {label}
+
+ );
+ })}
+
+ )}
+
+ {status === "running" && !result && items.length === 0 && (
+
Updating todo list…
+ )}
+
+ {result && (
+
+ {result}
+
+ )}
+
+ );
+}
diff --git a/packages/client/src/components/tool-renderers/__tests__/SpecableToolRenderers.test.tsx b/packages/client/src/components/tool-renderers/__tests__/SpecableToolRenderers.test.tsx
new file mode 100644
index 000000000..bf51ce34d
--- /dev/null
+++ b/packages/client/src/components/tool-renderers/__tests__/SpecableToolRenderers.test.tsx
@@ -0,0 +1,238 @@
+import { render, screen } from "@testing-library/react";
+import { describe, expect, it } from "vitest";
+import { AstToolRenderer } from "../AstToolRenderer.js";
+import { BrowserToolRenderer } from "../BrowserToolRenderer.js";
+import { EditToolRenderer } from "../EditToolRenderer.js";
+import { EvalToolRenderer } from "../EvalToolRenderer.js";
+import { GithubToolRenderer } from "../GithubToolRenderer.js";
+import { GoalToolRenderer } from "../GoalToolRenderer.js";
+import { HubToolRenderer } from "../HubToolRenderer.js";
+import { ImageToolRenderer } from "../ImageToolRenderer.js";
+import { KnowledgeToolRenderer } from "../KnowledgeToolRenderer.js";
+import { LspToolRenderer } from "../LspToolRenderer.js";
+import { ReadToolRenderer } from "../ReadToolRenderer.js";
+import { ResolveRejectToolRenderer } from "../ResolveRejectToolRenderer.js";
+import { getToolRenderer } from "../registry.js";
+import { SearchToolRenderer } from "../SearchToolRenderer.js";
+import { TaskToolRenderer } from "../TaskToolRenderer.js";
+import { ThinkToolRenderer } from "../ThinkToolRenderer.js";
+import { TodoToolRenderer } from "../TodoToolRenderer.js";
+import type { ToolContext } from "../types.js";
+
+const ctx: ToolContext = { cwd: "/root" };
+
+describe("SpecableToolRenderers", () => {
+ it("renders resolve banner with green check and reason", () => {
+ const { container } = render(
+ ,
+ );
+ expect(screen.getByTestId("resolve-banner")).toBeDefined();
+ expect(container.textContent).toContain("Task Resolved");
+ expect(container.textContent).toContain("All tests passing");
+ });
+
+ it("renders reject banner with alert icon and reason", () => {
+ const { container } = render(
+ ,
+ );
+ expect(screen.getByTestId("reject-banner")).toBeDefined();
+ expect(container.textContent).toContain("Task Rejected");
+ expect(container.textContent).toContain("Build failed");
+ });
+
+ it("renders eval card with language badge and code snippet", () => {
+ const { container } = render(
+ ,
+ );
+ expect(container.textContent?.toLowerCase()).toContain("py");
+ expect(container.textContent).toContain("print('hello')");
+ expect(container.textContent).toContain("hello");
+ });
+
+ it("renders browser card with action badge and url", () => {
+ const { container } = render(
+ ,
+ );
+ expect(container.textContent?.toLowerCase()).toContain("navigate");
+ expect(container.textContent).toContain("https://example.com");
+ });
+
+ it("renders task card with agent type and description", () => {
+ const { container } = render(
+ ,
+ );
+ expect(container.textContent?.toLowerCase()).toContain("reviewer");
+ expect(container.textContent).toContain("PR Check");
+ expect(container.textContent).toContain("Audit code");
+ });
+
+ it("renders hub card with operation badge and target", () => {
+ const { container } = render(
+ ,
+ );
+ expect(container.textContent?.toLowerCase()).toContain("send");
+ expect(container.textContent).toContain("worker1");
+ expect(container.textContent).toContain("Ping");
+ });
+
+ it("renders todo card with checkbox list", () => {
+ const { container } = render(
+ ,
+ );
+ expect(container.textContent?.toLowerCase()).toContain("list");
+ expect(container.textContent).toContain("First task");
+ expect(container.textContent).toContain("Second task");
+ });
+
+ it("renders goal card with objective and status", () => {
+ const { container } = render(
+ ,
+ );
+ expect(container.textContent?.toLowerCase()).toContain("accomplished");
+ expect(container.textContent).toContain("Finish feature");
+ });
+
+ it("renders think card with reasoning block", () => {
+ const { container } = render(
+ ,
+ );
+ expect(container.textContent).toContain("Reasoning / Thought");
+ expect(container.textContent).toContain("I should analyze the codebase first");
+ });
+
+ it("renders ast card with pattern and path", () => {
+ const { container } = render(
+ ,
+ );
+ expect(container.textContent).toContain("ast_grep");
+ expect(container.textContent).toContain("src/index.ts");
+ expect(container.textContent).toContain("function foo($A)");
+ });
+
+ it("renders lsp card with command and file", () => {
+ const { container } = render(
+ ,
+ );
+ expect(container.textContent).toContain("LSP definition");
+ expect(container.textContent).toContain("src/main.ts");
+ });
+
+ it("renders search card for web_search/grep/glob/find", () => {
+ const { container } = render(
+ ,
+ );
+ expect(container.textContent).toContain("web_search");
+ expect(container.textContent).toContain("vitest react");
+ });
+
+ it("renders github card with operation and issue number", () => {
+ const { container } = render(
+ ,
+ );
+ expect(container.textContent).toContain("github pr_view");
+ expect(container.textContent).toContain("ZGEnergy/omp-dashboard");
+ expect(container.textContent).toContain("#120");
+ });
+
+ it("renders knowledge card with key and memory text", () => {
+ const { container } = render(
+ ,
+ );
+ expect(container.textContent).toContain("retain");
+ expect(container.textContent).toContain("user_pref");
+ expect(container.textContent).toContain("dark mode");
+ });
+
+ it("renders image card with prompt and images", () => {
+ const { container } = render(
+ ,
+ );
+ expect(container.textContent).toContain("generate_image");
+ expect(container.textContent).toContain("A glowing red cube");
+ });
+
+ it("maps registry aliases correctly", () => {
+ expect(getToolRenderer("fetch")).toBe(ReadToolRenderer);
+ expect(getToolRenderer("apply_patch")).toBe(EditToolRenderer);
+ expect(getToolRenderer("resolve")).toBe(ResolveRejectToolRenderer);
+ expect(getToolRenderer("reject")).toBe(ResolveRejectToolRenderer);
+ expect(getToolRenderer("eval")).toBe(EvalToolRenderer);
+ expect(getToolRenderer("browser")).toBe(BrowserToolRenderer);
+ });
+});
diff --git a/packages/client/src/components/tool-renderers/index.ts b/packages/client/src/components/tool-renderers/index.ts
index 5acb229a5..b705b9285 100644
--- a/packages/client/src/components/tool-renderers/index.ts
+++ b/packages/client/src/components/tool-renderers/index.ts
@@ -1,2 +1,16 @@
+export { AstToolRenderer } from "./AstToolRenderer.js";
+export { BrowserToolRenderer } from "./BrowserToolRenderer.js";
+export { EvalToolRenderer } from "./EvalToolRenderer.js";
+export { GithubToolRenderer } from "./GithubToolRenderer.js";
+export { GoalToolRenderer } from "./GoalToolRenderer.js";
+export { HubToolRenderer } from "./HubToolRenderer.js";
+export { ImageToolRenderer } from "./ImageToolRenderer.js";
+export { KnowledgeToolRenderer } from "./KnowledgeToolRenderer.js";
+export { LspToolRenderer } from "./LspToolRenderer.js";
+export { ResolveRejectToolRenderer } from "./ResolveRejectToolRenderer.js";
export { getToolRenderer, registerToolRenderer } from "./registry.js";
-export type { ToolContext, ToolRendererProps, ToolRenderer } from "./types.js";
+export { SearchToolRenderer } from "./SearchToolRenderer.js";
+export { TaskToolRenderer } from "./TaskToolRenderer.js";
+export { ThinkToolRenderer } from "./ThinkToolRenderer.js";
+export { TodoToolRenderer } from "./TodoToolRenderer.js";
+export type { ToolContext, ToolRenderer, ToolRendererProps } from "./types.js";
diff --git a/packages/client/src/components/tool-renderers/registry.ts b/packages/client/src/components/tool-renderers/registry.ts
index b9a516b27..8068675ad 100644
--- a/packages/client/src/components/tool-renderers/registry.ts
+++ b/packages/client/src/components/tool-renderers/registry.ts
@@ -1,12 +1,26 @@
-import type { ToolRenderer } from "./types.js";
-import { ReadToolRenderer } from "./ReadToolRenderer.js";
-import { EditToolRenderer } from "./EditToolRenderer.js";
-import { WriteToolRenderer } from "./WriteToolRenderer.js";
-import { BashToolRenderer } from "./BashToolRenderer.js";
import { AgentToolRenderer } from "./AgentToolRenderer.js";
-import { GenericToolRenderer } from "./GenericToolRenderer.js";
import { AskUserToolRenderer } from "./AskUserToolRenderer.js";
+import { AstToolRenderer } from "./AstToolRenderer.js";
+import { BashToolRenderer } from "./BashToolRenderer.js";
+import { BrowserToolRenderer } from "./BrowserToolRenderer.js";
import { CtxToolRenderer } from "./CtxToolRenderer.js";
+import { EditToolRenderer } from "./EditToolRenderer.js";
+import { EvalToolRenderer } from "./EvalToolRenderer.js";
+import { GenericToolRenderer } from "./GenericToolRenderer.js";
+import { GithubToolRenderer } from "./GithubToolRenderer.js";
+import { GoalToolRenderer } from "./GoalToolRenderer.js";
+import { HubToolRenderer } from "./HubToolRenderer.js";
+import { ImageToolRenderer } from "./ImageToolRenderer.js";
+import { KnowledgeToolRenderer } from "./KnowledgeToolRenderer.js";
+import { LspToolRenderer } from "./LspToolRenderer.js";
+import { ReadToolRenderer } from "./ReadToolRenderer.js";
+import { ResolveRejectToolRenderer } from "./ResolveRejectToolRenderer.js";
+import { SearchToolRenderer } from "./SearchToolRenderer.js";
+import { TaskToolRenderer } from "./TaskToolRenderer.js";
+import { ThinkToolRenderer } from "./ThinkToolRenderer.js";
+import { TodoToolRenderer } from "./TodoToolRenderer.js";
+import type { ToolRenderer } from "./types.js";
+import { WriteToolRenderer } from "./WriteToolRenderer.js";
const renderers = new Map([
["read", ReadToolRenderer],
@@ -23,6 +37,31 @@ const renderers = new Map([
["ctx_index", CtxToolRenderer],
["ctx_fetch_and_index", CtxToolRenderer],
["ctx_insight", CtxToolRenderer],
+ // Specable Tool Card Mappings (#120)
+ ["apply_patch", EditToolRenderer],
+ ["fetch", ReadToolRenderer],
+ ["resolve", ResolveRejectToolRenderer],
+ ["reject", ResolveRejectToolRenderer],
+ ["eval", EvalToolRenderer],
+ ["browser", BrowserToolRenderer],
+ ["task", TaskToolRenderer],
+ ["hub", HubToolRenderer],
+ ["todo", TodoToolRenderer],
+ ["goal", GoalToolRenderer],
+ ["think", ThinkToolRenderer],
+ ["ast_edit", AstToolRenderer],
+ ["ast_grep", AstToolRenderer],
+ ["lsp", LspToolRenderer],
+ ["web_search", SearchToolRenderer],
+ ["grep", SearchToolRenderer],
+ ["glob", SearchToolRenderer],
+ ["find", SearchToolRenderer],
+ ["github", GithubToolRenderer],
+ ["retain", KnowledgeToolRenderer],
+ ["recall", KnowledgeToolRenderer],
+ ["reflect", KnowledgeToolRenderer],
+ ["inspect_image", ImageToolRenderer],
+ ["generate_image", ImageToolRenderer],
]);
/** Register a custom renderer for a tool name */
diff --git a/packages/client/src/lib/__tests__/unwrap-xd.test.ts b/packages/client/src/lib/__tests__/unwrap-xd.test.ts
new file mode 100644
index 000000000..eb006b011
--- /dev/null
+++ b/packages/client/src/lib/__tests__/unwrap-xd.test.ts
@@ -0,0 +1,91 @@
+import { describe, expect, it } from "vitest";
+import { unwrapXdToolCall } from "../unwrap-xd.js";
+
+describe("unwrapXdToolCall", () => {
+ it("prioritizes toolDetails.xdev when present", () => {
+ const result = unwrapXdToolCall(
+ "write",
+ { path: "xd://ignored", content: '{"foo": "bar"}' },
+ { xdev: { tool: "eval", inner: { code: "console.log(1)" } } },
+ );
+ expect(result).toEqual({
+ effectiveToolName: "eval",
+ effectiveArgs: { code: "console.log(1)" },
+ });
+ });
+
+ it("supports xdev.args fallback when xdev.inner is missing", () => {
+ const result = unwrapXdToolCall(
+ "write",
+ { path: "xd://ignored" },
+ { xdev: { tool: "browser", args: { url: "https://example.com" } } },
+ );
+ expect(result).toEqual({
+ effectiveToolName: "browser",
+ effectiveArgs: { url: "https://example.com" },
+ });
+ });
+
+ it("unwraps write tool calls to xd:// with valid JSON content", () => {
+ const result = unwrapXdToolCall("write", {
+ path: "xd://eval",
+ content: '{"language": "py", "code": "print(42)"}',
+ });
+ expect(result).toEqual({
+ effectiveToolName: "eval",
+ effectiveArgs: { language: "py", code: "print(42)" },
+ });
+ });
+
+ it("unwraps xd:// path for MCP tools", () => {
+ const result = unwrapXdToolCall("write", {
+ path: "xd://mcp__context_mode_context_mode_ctx_execute",
+ content: '{"command": "ls"}',
+ });
+ expect(result).toEqual({
+ effectiveToolName: "ctx_execute",
+ effectiveArgs: { command: "ls" },
+ });
+ });
+
+ it("handles double-underscore MCP paths", () => {
+ const result = unwrapXdToolCall("write", {
+ path: "xd://mcp__server__custom_tool",
+ content: '{"a": 1}',
+ });
+ expect(result).toEqual({
+ effectiveToolName: "custom_tool",
+ effectiveArgs: { a: 1 },
+ });
+ });
+
+ it("falls back to raw string content on invalid or truncated JSON", () => {
+ const result = unwrapXdToolCall("write", {
+ path: "xd://eval",
+ content: '{"code": "print(',
+ });
+ expect(result).toEqual({
+ effectiveToolName: "eval",
+ effectiveArgs: { raw: '{"code": "print(' },
+ });
+ });
+
+ it("maps alias names (puppeteer -> browser, irc -> hub)", () => {
+ const res1 = unwrapXdToolCall("puppeteer", { url: "https://a.b" });
+ expect(res1.effectiveToolName).toBe("browser");
+
+ const res2 = unwrapXdToolCall("write", {
+ path: "xd://irc",
+ content: '{"op": "send"}',
+ });
+ expect(res2.effectiveToolName).toBe("hub");
+ });
+
+ it("passes through standard non-xd tool calls unchanged", () => {
+ const result = unwrapXdToolCall("bash", { command: "npm test" });
+ expect(result).toEqual({
+ effectiveToolName: "bash",
+ effectiveArgs: { command: "npm test" },
+ });
+ });
+});
diff --git a/packages/client/src/lib/tool-summary.ts b/packages/client/src/lib/tool-summary.ts
index 2eae6497d..f49693a86 100644
--- a/packages/client/src/lib/tool-summary.ts
+++ b/packages/client/src/lib/tool-summary.ts
@@ -10,14 +10,27 @@
*/
import {
+ mdiAccountGroupOutline,
mdiAccountQuestionOutline,
+ mdiBrain,
+ mdiCheckCircleOutline,
+ mdiCloseCircleOutline,
mdiCodeBraces,
mdiConsoleLine,
+ mdiDatabasePlusOutline,
mdiDatabaseSearchOutline,
+ mdiDownload,
+ mdiFileDocumentEditOutline,
mdiFileDocumentOutline,
mdiFilePlusOutline,
+ mdiFlagOutline,
mdiFolderSearchOutline,
mdiFormatListBulleted,
+ mdiFormatListCheckbox,
+ mdiGithub,
+ mdiImageOutline,
+ mdiImagePlusOutline,
+ mdiLightbulbOutline,
mdiMagnify,
mdiPencil,
mdiRobotOutline,
@@ -52,6 +65,27 @@ export const toolSummaries: Record) => s
ctx_index: (args) => `ctx_index ${String(args?.source ?? args?.path ?? "")}`.trim(),
ctx_fetch_and_index: (args) => `ctx_fetch_and_index ${String(args?.url ?? args?.source ?? "")}`.trim(),
ctx_insight: () => `ctx_insight`,
+ resolve: (args) => `Resolve ${args?.reason ? String(args.reason) : args?.label ? String(args.label) : ""}`.trim(),
+ reject: (args) => `Reject ${args?.reason ? String(args.reason) : args?.label ? String(args.label) : ""}`.trim(),
+ eval: (args) => `eval ${args?.language ? `${args.language} ` : ""}${args?.title ? `(${args.title})` : ""}`.trim(),
+ browser: (args) => `browser ${args?.action ?? args?.url ?? ""}`.trim(),
+ task: (args) => `task ${args?.name ?? args?.agent ?? ""}`.trim(),
+ hub: (args) => `hub ${args?.op ?? args?.to ?? ""}`.trim(),
+ todo: (args) => `todo ${args?.action ?? ""}`.trim(),
+ goal: (args) => `goal ${args?.objective ?? args?.goal ?? ""}`.trim(),
+ think: (args) => `think ${args?.thought ? String(args.thought).slice(0, 40) : ""}`.trim(),
+ ast_edit: (args) => `ast_edit ${args?.path ?? ""}`.trim(),
+ ast_grep: (args) => `ast_grep ${args?.pattern ?? args?.path ?? ""}`.trim(),
+ lsp: (args) => `lsp ${args?.command ?? args?.action ?? ""}`.trim(),
+ web_search: (args) => `web_search ${args?.query ?? ""}`.trim(),
+ fetch: (args) => `fetch ${args?.url ?? args?.path ?? ""}`.trim(),
+ github: (args) => `github ${args?.op ?? args?.action ?? ""}`.trim(),
+ apply_patch: (args) => `apply_patch ${args?.path ?? ""}`.trim(),
+ retain: (args) => `retain ${args?.key ?? args?.topic ?? ""}`.trim(),
+ recall: (args) => `recall ${args?.query ?? args?.key ?? ""}`.trim(),
+ reflect: (args) => `reflect ${args?.topic ?? ""}`.trim(),
+ inspect_image: (args) => `inspect_image ${args?.path ?? args?.url ?? ""}`.trim(),
+ generate_image: (args) => `generate_image ${args?.prompt ? String(args.prompt).slice(0, 30) : ""}`.trim(),
};
export function getSummary(toolName: string, args?: Record): string {
@@ -89,6 +123,27 @@ export const toolIcons: Record = {
ctx_index: mdiDatabaseSearchOutline,
ctx_fetch_and_index: mdiWeb,
ctx_insight: mdiFormatListBulleted,
+ resolve: mdiCheckCircleOutline,
+ reject: mdiCloseCircleOutline,
+ eval: mdiCodeBraces,
+ browser: mdiWeb,
+ task: mdiFormatListCheckbox,
+ hub: mdiAccountGroupOutline,
+ todo: mdiFormatListCheckbox,
+ goal: mdiFlagOutline,
+ think: mdiBrain,
+ ast_edit: mdiFileDocumentEditOutline,
+ ast_grep: mdiMagnify,
+ lsp: mdiCodeBraces,
+ web_search: mdiWeb,
+ fetch: mdiDownload,
+ github: mdiGithub,
+ apply_patch: mdiPencil,
+ retain: mdiDatabasePlusOutline,
+ recall: mdiDatabaseSearchOutline,
+ reflect: mdiLightbulbOutline,
+ inspect_image: mdiImageOutline,
+ generate_image: mdiImagePlusOutline,
};
/** mdi icon path for a tool kind; generic wrench for unknown kinds. */
diff --git a/packages/client/src/lib/unwrap-xd.ts b/packages/client/src/lib/unwrap-xd.ts
new file mode 100644
index 000000000..1d031a55d
--- /dev/null
+++ b/packages/client/src/lib/unwrap-xd.ts
@@ -0,0 +1,68 @@
+const TOOL_ALIASES: Record = {
+ puppeteer: "browser",
+ irc: "hub",
+ job: "hub",
+ await: "hub",
+ poll: "hub",
+};
+
+export interface UnwrappedToolCall {
+ effectiveToolName: string;
+ effectiveArgs: Record;
+}
+
+function parseXdPath(pathStr: string): string {
+ const rawPath = pathStr.slice(5).split("?")[0].split("#")[0];
+ if (!rawPath.startsWith("mcp__")) return rawPath;
+ const inner = rawPath.slice(5);
+ if (inner.includes("__")) return inner.split("__").pop() ?? inner;
+ if (inner.includes("_ctx_")) return inner.slice(inner.indexOf("_ctx_") + 1);
+ return inner;
+}
+
+function parseXdContent(args?: Record): Record {
+ if (!args) return {};
+ if (typeof args.content === "string") {
+ try {
+ const parsed = JSON.parse(args.content);
+ return typeof parsed === "object" && parsed !== null ? (parsed as Record) : { raw: args.content };
+ } catch {
+ return { raw: args.content };
+ }
+ }
+ if (typeof args.content === "object" && args.content !== null) {
+ return args.content as Record;
+ }
+ return args;
+}
+
+function getXdevUnwrapped(toolDetails?: Record, fallbackArgs?: Record): UnwrappedToolCall | null {
+ const xdev = toolDetails?.xdev;
+ if (xdev && typeof xdev === "object") {
+ const obj = xdev as Record;
+ if (typeof obj.tool === "string" && obj.tool.length > 0) {
+ const inner = (obj.inner ?? obj.args ?? fallbackArgs ?? {}) as Record;
+ return { effectiveToolName: TOOL_ALIASES[obj.tool] ?? obj.tool, effectiveArgs: inner };
+ }
+ }
+ return null;
+}
+
+export function unwrapXdToolCall(
+ toolName: string,
+ args?: Record,
+ toolDetails?: Record,
+): UnwrappedToolCall {
+ const xdevResult = getXdevUnwrapped(toolDetails, args);
+ if (xdevResult) return xdevResult;
+
+ const lowerName = toolName.toLowerCase();
+ const isWrite = lowerName === "write" || lowerName === "write_file";
+ if (isWrite && typeof args?.path === "string" && args.path.startsWith("xd://")) {
+ const extractedTool = parseXdPath(args.path);
+ const effectiveArgs = parseXdContent(args);
+ return { effectiveToolName: TOOL_ALIASES[extractedTool] ?? extractedTool, effectiveArgs };
+ }
+
+ return { effectiveToolName: TOOL_ALIASES[toolName] ?? toolName, effectiveArgs: args ?? {} };
+}
From c704aa51ec89fb8e7af116be9108b86fb60bedc9 Mon Sep 17 00:00:00 2001
From: Joe Shull <42043763+joeshull@users.noreply.github.com>
Date: Thu, 13 Aug 2026 16:38:30 -0600
Subject: [PATCH 2/2] fix: resolve CI failure (attempt 1)
---
.../src/components/tool-renderers/SearchToolRenderer.tsx | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/packages/client/src/components/tool-renderers/SearchToolRenderer.tsx b/packages/client/src/components/tool-renderers/SearchToolRenderer.tsx
index 9c52ec923..8433be7de 100644
--- a/packages/client/src/components/tool-renderers/SearchToolRenderer.tsx
+++ b/packages/client/src/components/tool-renderers/SearchToolRenderer.tsx
@@ -1,7 +1,7 @@
import { LinkifiedText } from "./LinkifiedText.js";
import type { ToolRendererProps } from "./types.js";
-export function SearchToolRenderer({ toolName, args, status, result }: ToolRendererProps) {
+export function SearchToolRenderer({ toolName, args, status, result, context }: ToolRendererProps) {
const query = (args?.query ?? args?.pattern ?? args?.glob ?? args?.search) as string | undefined;
const path = (args?.path ?? args?.domain ?? args?.dir) as string | undefined;
@@ -21,7 +21,7 @@ export function SearchToolRenderer({ toolName, args, status, result }: ToolRende
{result && (
-
+
)}