Tool renderer registry
pi-web-ui decouples the UI rendering of tool calls: a tool's execute is defined in AgentTool, while rendering is implemented by a ToolRenderer interface — the two are paired by tool name in the toolRenderers Map. renderTool is the single entry point that looks up the registry by tool name; on a hit it uses the custom renderer, on a miss it falls back to DefaultRenderer. Built-in renderers cover bash, extract_document, javascript_repl, and others; hosts can also inject business-specific renderers via registerToolRenderer.
Responsibilities
- Global registry: the
toolRenderersMap storesToolRendererinstances by toolName, seepackages/web-ui/src/tools/renderer-registry.ts:9-16. - Single entry point:
renderTool(toolName, params, result, isStreaming)queries the registry first, uses it if found, falls back toDefaultRendererotherwise; it also supports ashowJsonModeglobal toggle that forces the default JSON renderer, seepackages/web-ui/src/tools/index.ts:28-44. - Header helpers:
renderHeaderandrenderCollapsibleHeaderunify the icon, text, and collapse interaction across three states (inprogress/complete/error), seepackages/web-ui/src/tools/renderer-registry.ts:29-130. - Inline rendering:
ToolMessage.rendercallsrenderTool, then decides whether to wrap the result in a card based onisCustom, seepackages/web-ui/src/components/Messages.ts:258-276. - Built-in renderers:
extract-document.tsandjavascript-repl.tsauto-register at the end of their files viaregisterToolRenderer(name, renderer), seepackages/web-ui/src/tools/extract-document.ts:275andpackages/web-ui/src/tools/javascript-repl.ts:293.
Design motivation
Why not just add a render method on AgentTool? Because the tool's execute lives in the pi-agent-core layer as backend logic, while rendering lives in the pi-web-ui layer as frontend logic. The two cannot be coupled — the same bash tool might be used in CLI, TUI, and web UIs; execute is reused, rendering is written per UI. The registry pattern lets web-ui register a renderer for any tool name, even if the tool itself is in another package.
Why does renderTool return {content, isCustom} instead of a TemplateResult? Because some renderers want to control the entire layout themselves (e.g. the artifacts tool is inlined onto the canvas and does not want a card frame); isCustom: true makes ToolMessage skip the card wrapper and return the content directly; isCustom: false wraps it in a border rounded-md card. This "custom content plus default shell" choice gives renderers freedom while keeping a consistent look for most tools.
Why do extract-document and javascript-repl keep renderer and tool in the same file? Because they are paired: the shape of the tool's result.details is defined by the tool, and the renderer consumes it directly. Co-locating them reduces the risk of drift, and the registerToolRenderer at the end of the file is "auto-register" — tools/index.ts uses import "./javascript-repl.js" to trigger the side effect; the host does not need to call anything manually.
Key files
packages/web-ui/src/tools/types.ts:4-15—ToolRenderResultand theToolRendererinterface, defining the render signature.packages/web-ui/src/tools/renderer-registry.ts:9-23—toolRenderersMap,registerToolRenderer,getToolRenderer.packages/web-ui/src/tools/renderer-registry.ts:29-63—renderHeader: three-state icons and text.packages/web-ui/src/tools/renderer-registry.ts:69-130—renderCollapsibleHeader: chevron collapse interaction.packages/web-ui/src/tools/index.ts:9-13— register thebashrenderer, initializedefaultRenderer, pull in two side-effect imports.packages/web-ui/src/tools/index.ts:28-44—renderToolentry point, handlesshowJsonModeand fallback.packages/web-ui/src/tools/extract-document.ts:36-181—createExtractDocumentTool; on direct fetch failure it falls back to the proxy.packages/web-ui/src/tools/extract-document.ts:190-272—extractDocumentRenderer: three-state rendering for has-result / params-only / no-params.packages/web-ui/src/tools/javascript-repl.ts:128-195—createJavaScriptReplTool, runs code in a sandboxed iframe and returns files.packages/web-ui/src/tools/javascript-repl.ts:200-290—javascriptReplRenderer: collapsible code block plus console output plus attachment tile.packages/web-ui/src/components/Messages.ts:258-276—ToolMessage.rendercallsrenderTool, decides whether to wrap in a card byisCustom.
renderTool is the single entry point, with very short logic:
// packages/web-ui/src/tools/index.ts:28-44
export function renderTool(
toolName: string,
params: any | undefined,
result: ToolResultMessage | undefined,
isStreaming?: boolean,
): ToolRenderResult {
if (showJsonMode) {
return defaultRenderer.render(params, result, isStreaming);
}
const renderer = getToolRenderer(toolName);
if (renderer) {
return renderer.render(params, result, isStreaming);
}
return defaultRenderer.render(params, result, isStreaming);
}The auto-register at the end of javascript-repl.ts is the "import as registration" pattern:
// packages/web-ui/src/tools/javascript-repl.ts:292-293
// Auto-register the renderer
registerToolRenderer(javascriptReplTool.name, javascriptReplRenderer);Data flow
The rendering path of a tool call, from AssistantMessage triggering it to the renderer returning:
Edges and failures
- Tool has no registered renderer:
renderToolfalls back toDefaultRenderer, displaying params and result as JSON so the slot is not empty, seepackages/web-ui/src/tools/index.ts:39-43. - Result not yet arrived during streaming: the renderer receives
result === undefinedand usesrenderHeader(state="inprogress", ...)to show a spinner plus text, seepackages/web-ui/src/tools/extract-document.ts:249-264. - Aborted tool call:
ToolMessagesynthesizes an isErrorToolResultMessageas a placeholder whenaborted, and the renderer renders it in the error state, seepackages/web-ui/src/components/Messages.ts:248-258. - CORS failure fallback: when
extract_documentdirect fetch fails,isCorsErrormatches and acorsProxyUrlis configured, it retries via the proxy, seepackages/web-ui/src/tools/extract-document.ts:103-135. - showJsonMode debug toggle: after
setShowJsonMode(true), every tool is forced throughDefaultRenderer's JSON view, which helps inspect tool return values, seepackages/web-ui/src/tools/index.ts:21-23. - Duplicate renderer registration:
registerToolRendererjust doesMap.set, overwriting; the later registration wins, no error. A host injecting a custom renderer can override the built-in implementation.
Summary
renderer-registry decouples a tool's execute from its rendering: renderTool looks up the Map by toolName, uses the custom renderer on a hit, otherwise DefaultRenderer. extract_document and javascript_repl use the "import as registration" pattern to auto-register. The rendering entry point is ToolMessage in Message rendering components; the assembly of tool instances happens in ChatPanel top-level element.