fix(coding-agent): built-in tools work like extension tools

Export readToolDefinition / createReadToolDefinition and the equivalent built-in ToolDefinition APIs from @mariozechner/pi-coding-agent.
This commit is contained in:
Mario Zechner
2026-03-22 04:20:28 +01:00
parent 80f527ec22
commit 235b247f1f
32 changed files with 2594 additions and 1408 deletions

View File

@@ -13,9 +13,10 @@ import { SessionManager } from "../session-manager.js";
*/
export interface ToolHtmlRenderer {
/** Render a tool call to HTML. Returns undefined if tool has no custom renderer. */
renderCall(toolName: string, args: unknown): string | undefined;
renderCall(toolCallId: string, toolName: string, args: unknown): string | undefined;
/** Render a tool result to HTML. Returns collapsed/expanded or undefined if tool has no custom renderer. */
renderResult(
toolCallId: string,
toolName: string,
result: Array<{ type: string; text?: string; data?: string; mimeType?: string }>,
details: unknown,
@@ -191,7 +192,7 @@ function preRenderCustomTools(
if (msg.role === "assistant" && Array.isArray(msg.content)) {
for (const block of msg.content) {
if (block.type === "toolCall" && !BUILTIN_TOOLS.has(block.name)) {
const callHtml = toolRenderer.renderCall(block.name, block.arguments);
const callHtml = toolRenderer.renderCall(block.id, block.name, block.arguments);
if (callHtml) {
renderedTools[block.id] = { callHtml };
}
@@ -205,7 +206,13 @@ function preRenderCustomTools(
// Only render if we have a pre-rendered call OR it's not a built-in tool
const existing = renderedTools[msg.toolCallId];
if (existing || !BUILTIN_TOOLS.has(toolName)) {
const rendered = toolRenderer.renderResult(toolName, msg.content, msg.details, msg.isError || false);
const rendered = toolRenderer.renderResult(
msg.toolCallId,
toolName,
msg.content,
msg.details,
msg.isError || false,
);
if (rendered) {
renderedTools[msg.toolCallId] = {
...existing,

View File

@@ -6,8 +6,9 @@
*/
import type { ImageContent, TextContent } from "@mariozechner/pi-ai";
import type { Component } from "@mariozechner/pi-tui";
import type { Theme } from "../../modes/interactive/theme/theme.js";
import type { ToolDefinition } from "../extensions/types.js";
import type { ToolDefinition, ToolRenderContext } from "../extensions/types.js";
import { ansiLinesToHtml } from "./ansi-to-html.js";
export interface ToolHtmlRendererDeps {
@@ -21,9 +22,10 @@ export interface ToolHtmlRendererDeps {
export interface ToolHtmlRenderer {
/** Render a tool call to HTML. Returns undefined if tool has no custom renderer. */
renderCall(toolName: string, args: unknown): string | undefined;
renderCall(toolCallId: string, toolName: string, args: unknown): string | undefined;
/** Render a tool result to collapsed/expanded HTML. Returns undefined if tool has no custom renderer. */
renderResult(
toolCallId: string,
toolName: string,
result: Array<{ type: string; text?: string; data?: string; mimeType?: string }>,
details: unknown,
@@ -40,27 +42,68 @@ export interface ToolHtmlRenderer {
export function createToolHtmlRenderer(deps: ToolHtmlRendererDeps): ToolHtmlRenderer {
const { getToolDefinition, theme, width = 100 } = deps;
const renderedCallComponents = new Map<string, Component>();
const renderedResultComponents = new Map<string, Component>();
const renderedStates = new Map<string, any>();
const renderedArgs = new Map<string, unknown>();
const getState = (toolCallId: string): any => {
let state = renderedStates.get(toolCallId);
if (!state) {
state = {};
renderedStates.set(toolCallId, state);
}
return state;
};
const createRenderContext = (
toolCallId: string,
lastComponent: Component | undefined,
expanded: boolean,
isPartial: boolean,
isError: boolean,
): ToolRenderContext => {
return {
args: renderedArgs.get(toolCallId),
toolCallId,
invalidate: () => {},
lastComponent,
state: getState(toolCallId),
cwd: process.cwd(),
executionStarted: true,
argsComplete: true,
isPartial,
expanded,
showImages: false,
isError,
};
};
return {
renderCall(toolName: string, args: unknown): string | undefined {
renderCall(toolCallId: string, toolName: string, args: unknown): string | undefined {
try {
renderedArgs.set(toolCallId, args);
const toolDef = getToolDefinition(toolName);
if (!toolDef?.renderCall) {
return undefined;
}
const component = toolDef.renderCall(args, theme);
if (!component) {
return undefined;
}
const component = toolDef.renderCall(
args,
theme,
createRenderContext(toolCallId, renderedCallComponents.get(toolCallId), false, true, false),
);
renderedCallComponents.set(toolCallId, component);
const lines = component.render(width);
return ansiLinesToHtml(lines);
} catch {
// On error, return undefined to trigger JSON fallback
// On error, return undefined so HTML export can fall back to structured result rendering
return undefined;
}
},
renderResult(
toolCallId: string,
toolName: string,
result: Array<{ type: string; text?: string; data?: string; mimeType?: string }>,
details: unknown,
@@ -85,28 +128,27 @@ export function createToolHtmlRenderer(deps: ToolHtmlRendererDeps): ToolHtmlRend
agentToolResult,
{ expanded: false, isPartial: false },
theme,
createRenderContext(toolCallId, renderedResultComponents.get(toolCallId), false, false, isError),
);
const collapsed = collapsedComponent ? ansiLinesToHtml(collapsedComponent.render(width)) : undefined;
renderedResultComponents.set(toolCallId, collapsedComponent);
const collapsed = ansiLinesToHtml(collapsedComponent.render(width));
// Render expanded
const expandedComponent = toolDef.renderResult(
agentToolResult,
{ expanded: true, isPartial: false },
theme,
createRenderContext(toolCallId, renderedResultComponents.get(toolCallId), true, false, isError),
);
const expanded = expandedComponent ? ansiLinesToHtml(expandedComponent.render(width)) : undefined;
// Return collapsed only if it exists and differs from expanded
if (!expanded) {
return undefined;
}
renderedResultComponents.set(toolCallId, expandedComponent);
const expanded = ansiLinesToHtml(expandedComponent.render(width));
return {
...(collapsed && collapsed !== expanded ? { collapsed } : {}),
expanded,
};
} catch {
// On error, return undefined to trigger JSON fallback
// On error, return undefined so HTML export can fall back to structured result rendering
return undefined;
}
},