import assert from "node:assert/strict"; import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import test from "node:test"; import { createBashTool, createEditTool, createFindTool, createGrepTool, createLsTool, createReadTool, createWriteTool, type ExtensionAPI, } from "@earendil-works/pi-coding-agent"; import { registerToolDisplayOverrides } from "../src/tool-overrides.ts"; import { DEFAULT_TOOL_DISPLAY_CONFIG } from "../src/types.ts"; const TOOL_DISPLAY_PENDING_DECORATIONS_KEY = Symbol.for("pi-tool-display.pendingDecorations.v1"); interface RegisteredToolLike { name: string; description: string; parameters: unknown; renderShell?: "default" | "self"; promptSnippet?: string; promptGuidelines?: string[]; renderCall?: (...args: unknown[]) => unknown; renderResult?: (...args: unknown[]) => unknown; } interface ToolEventHandlers { session_start?: () => Promise | void; before_agent_start?: () => Promise | void; } interface ExecutableToolLike extends RegisteredToolLike { execute: (...args: unknown[]) => Promise<{ content?: Array<{ type: string; text?: string }> }>; } async function withTempDir(name: string, run: (dir: string) => Promise | void): Promise { const dir = mkdtempSync(join(tmpdir(), name)); try { await run(dir); } finally { rmSync(dir, { recursive: true, force: true }); } } function getTextOutput(result: { content?: Array<{ type: string; text?: string }> }): string { return (result.content ?? []) .filter((entry) => entry.type === "text") .map((entry) => entry.text ?? "") .join(""); } function withDefaultReadEditOwners(tools: unknown[] = []): unknown[] { const names = new Set( tools .map((tool) => (tool as { name?: unknown }).name) .filter((name): name is string => typeof name === "string"), ); const defaults = ["read", "edit"] .filter((name) => !names.has(name)) .map((name) => ({ name, sourceInfo: { source: "builtin", path: `` } })); return [...defaults, ...tools]; } function createExtensionApiStub(allTools: unknown[] = []): { api: ExtensionAPI; registeredTools: RegisteredToolLike[]; eventHandlers: ToolEventHandlers; } { const registeredTools: RegisteredToolLike[] = []; const eventHandlers: ToolEventHandlers = {}; const api = { registerTool(tool: RegisteredToolLike): void { registeredTools.push(tool); }, on(event: keyof ToolEventHandlers, handler: () => Promise | void): void { eventHandlers[event] = handler; }, getAllTools(): unknown[] { return withDefaultReadEditOwners(allTools); }, } as unknown as ExtensionAPI; return { api, registeredTools, eventHandlers }; } test("registerToolDisplayOverrides copies built-in prompt metadata onto overridden tools", async () => { const { api, registeredTools, eventHandlers } = createExtensionApiStub(); registerToolDisplayOverrides(api, () => DEFAULT_TOOL_DISPLAY_CONFIG); assert.deepEqual( registeredTools.map((tool) => tool.name).sort(), ["bash", "edit", "find", "grep", "ls", "read", "write"], ); await eventHandlers.before_agent_start?.(); assert.equal(registeredTools.length, 7); const byName = new Map(registeredTools.map((tool) => [tool.name, tool])); const cwd = process.cwd(); const builtInTools = { read: createReadTool(cwd), grep: createGrepTool(cwd), find: createFindTool(cwd), ls: createLsTool(cwd), bash: createBashTool(cwd), edit: createEditTool(cwd), write: createWriteTool(cwd), }; for (const [name, builtInTool] of Object.entries(builtInTools)) { const registeredTool = byName.get(name); const builtInMetadata = builtInTool as unknown as RegisteredToolLike; assert.ok(registeredTool, `expected '${name}' to be registered`); assert.equal(registeredTool.promptSnippet, builtInMetadata.promptSnippet); } assert.deepEqual(byName.get("read")?.promptGuidelines, (builtInTools.read as unknown as RegisteredToolLike).promptGuidelines); assert.deepEqual(byName.get("edit")?.promptGuidelines, (builtInTools.edit as unknown as RegisteredToolLike).promptGuidelines); assert.deepEqual(byName.get("write")?.promptGuidelines, (builtInTools.write as unknown as RegisteredToolLike).promptGuidelines); assert.equal(byName.get("grep")?.promptGuidelines, undefined); assert.equal(byName.get("find")?.promptGuidelines, undefined); assert.equal(byName.get("ls")?.promptGuidelines, undefined); assert.equal(byName.get("bash")?.promptGuidelines, undefined); }); test("registerToolDisplayOverrides registers built-in display renderers during extension load for pre-bind history rendering", () => { const { api, registeredTools } = createExtensionApiStub(); registerToolDisplayOverrides(api, () => DEFAULT_TOOL_DISPLAY_CONFIG); const byName = new Map(registeredTools.map((tool) => [tool.name, tool])); for (const name of ["read", "grep", "find", "ls", "bash", "edit", "write"] as const) { const registeredTool = byName.get(name); assert.ok(registeredTool, `expected '${name}' to be available before session_start`); assert.equal(typeof registeredTool.renderCall, "function", `${name} has renderCall before session_start`); assert.equal(typeof registeredTool.renderResult, "function", `${name} has renderResult before session_start`); } }); test("registerToolDisplayOverrides clones built-in parameter schemas so Pi TUI keeps extension renderers active", async () => { const { api, registeredTools, eventHandlers } = createExtensionApiStub(); registerToolDisplayOverrides(api, () => DEFAULT_TOOL_DISPLAY_CONFIG); await eventHandlers.before_agent_start?.(); const byName = new Map(registeredTools.map((tool) => [tool.name, tool])); const cwd = process.cwd(); const builtInTools = { read: createReadTool(cwd), grep: createGrepTool(cwd), find: createFindTool(cwd), ls: createLsTool(cwd), bash: createBashTool(cwd), edit: createEditTool(cwd), write: createWriteTool(cwd), }; for (const [name, builtInTool] of Object.entries(builtInTools)) { const registeredTool = byName.get(name); assert.ok(registeredTool, `expected '${name}' to be registered`); assert.notEqual( registeredTool.parameters, builtInTool.parameters, `expected '${name}' to use a cloned parameter object`, ); assert.deepEqual(registeredTool.parameters, builtInTool.parameters); } }); test("registerToolDisplayOverrides renders tools self-framed via the upstream renderShell API", async () => { const { api, registeredTools, eventHandlers } = createExtensionApiStub(); registerToolDisplayOverrides(api, () => DEFAULT_TOOL_DISPLAY_CONFIG); await eventHandlers.before_agent_start?.(); const byName = new Map(registeredTools.map((tool) => [tool.name, tool])); for (const name of ["read", "grep", "find", "ls", "bash", "edit", "write"] as const) { assert.equal(byName.get(name)?.renderShell, "self", `${name} should render self-framed`); } }); test("registerToolDisplayOverrides leaves externally owned read/edit/grep tools active", async () => { const { api, registeredTools, eventHandlers } = createExtensionApiStub([ { name: "read", sourceInfo: { source: "local", path: "agent/extensions/example-read/src/read.ts" } }, { name: "edit", sourceInfo: { source: "local", path: "agent/extensions/example-edit/src/edit.ts" } }, { name: "grep", sourceInfo: { source: "local", path: "agent/extensions/example-grep/src/grep.ts" } }, ]); registerToolDisplayOverrides(api, () => DEFAULT_TOOL_DISPLAY_CONFIG); await eventHandlers.before_agent_start?.(); const registeredNames = new Set(registeredTools.map((tool) => tool.name)); assert.equal(registeredNames.has("read"), false); assert.equal(registeredNames.has("edit"), false); assert.equal(registeredNames.has("grep"), false); assert.equal(registeredNames.has("find"), true); assert.equal(registeredNames.has("ls"), true); assert.equal(registeredNames.has("bash"), true); assert.equal(registeredNames.has("write"), true); }); test("bash override uses shellPath from Pi settings", async () => { await withTempDir("pi-tool-display-shellpath-", async (dir) => { const previousAgentDir = process.env.PI_CODING_AGENT_DIR; process.env.PI_CODING_AGENT_DIR = dir; writeFileSync( join(dir, "settings.json"), JSON.stringify({ shellPath: "/definitely/missing/bash" }), "utf8", ); try { const { api, registeredTools, eventHandlers } = createExtensionApiStub(); registerToolDisplayOverrides(api, () => DEFAULT_TOOL_DISPLAY_CONFIG); await eventHandlers.before_agent_start?.(); const bashTool = registeredTools.find((tool) => tool.name === "bash") as ExecutableToolLike | undefined; assert.ok(bashTool, "expected bash override to be registered"); await assert.rejects( bashTool.execute("tool-call-1", { command: "printf test" }, undefined, undefined, { cwd: process.cwd() }), /custom shell path not found/i, ); assert.equal(bashTool.description.length > 0, true); } finally { if (previousAgentDir === undefined) { delete process.env.PI_CODING_AGENT_DIR; } else { process.env.PI_CODING_AGENT_DIR = previousAgentDir; } } }); }); test("bash override uses shellCommandPrefix from Pi settings", async () => { await withTempDir("pi-tool-display-shellprefix-", async (dir) => { const previousAgentDir = process.env.PI_CODING_AGENT_DIR; process.env.PI_CODING_AGENT_DIR = dir; writeFileSync( join(dir, "settings.json"), JSON.stringify({ shellCommandPrefix: "printf 'prefix-output\\n'" }), "utf8", ); try { const { api, registeredTools, eventHandlers } = createExtensionApiStub(); registerToolDisplayOverrides(api, () => DEFAULT_TOOL_DISPLAY_CONFIG); await eventHandlers.before_agent_start?.(); const bashTool = registeredTools.find((tool) => tool.name === "bash") as ExecutableToolLike | undefined; assert.ok(bashTool, "expected bash override to be registered"); const result = await bashTool.execute( "tool-call-2", { command: "printf 'command-output\\n'" }, undefined, undefined, { cwd: process.cwd() }, ); assert.equal(getTextOutput(result).trim(), "prefix-output\ncommand-output"); } finally { if (previousAgentDir === undefined) { delete process.env.PI_CODING_AGENT_DIR; } else { process.env.PI_CODING_AGENT_DIR = previousAgentDir; } } }); }); test("registerToolDisplayOverrides drains pending display decorations from early-loading extensions", () => { type GlobalWithPendingDecorations = typeof globalThis & { [TOOL_DISPLAY_PENDING_DECORATIONS_KEY]?: Array<{ tool: Record; adapter?: Record; }>; }; const globalWithPending = globalThis as GlobalWithPendingDecorations; const previousPending = globalWithPending[TOOL_DISPLAY_PENDING_DECORATIONS_KEY]; const queuedTool: Record = { name: "mcp", label: "MCP Proxy", description: "Unified MCP gateway.", parameters: {}, execute(): void { // No-op test stub. }, }; globalWithPending[TOOL_DISPLAY_PENDING_DECORATIONS_KEY] = [ { adapter: { kind: "mcp" }, tool: queuedTool, }, ]; try { const { api, registeredTools } = createExtensionApiStub(); registerToolDisplayOverrides(api, () => DEFAULT_TOOL_DISPLAY_CONFIG); assert.equal(registeredTools.some((tool) => tool.name === "mcp"), false); assert.equal(typeof queuedTool.renderCall, "function", "expected queued MCP tool to receive renderCall"); assert.equal(typeof queuedTool.renderResult, "function", "expected queued MCP tool to receive renderResult"); assert.equal(globalWithPending[TOOL_DISPLAY_PENDING_DECORATIONS_KEY]?.length, 0); } finally { if (previousPending) { globalWithPending[TOOL_DISPLAY_PENDING_DECORATIONS_KEY] = previousPending; } else { delete globalWithPending[TOOL_DISPLAY_PENDING_DECORATIONS_KEY]; } } });