Skip to content

工具渲染器注册表

源码版本v0.73.1

pi-web-ui 把工具调用的 UI 渲染解耦:工具的 execute 在 AgentTool 里定义,渲染由 ToolRenderer 接口实现,两者通过工具名在 toolRenderers Map 里配对。renderTool 是统一入口,按工具名查 registry,命中就用自定义 renderer,没命中就走 DefaultRenderer。内置 bashextract_documentjavascript_repl 等渲染器,host 也可以通过 registerToolRenderer 注入业务专属渲染。

职责

  1. 全局 registry:toolRenderers Map 按 toolName 存 ToolRenderer 实例,见 packages/web-ui/src/tools/renderer-registry.ts:9-16
  2. 统一入口:renderTool(toolName, params, result, isStreaming) 先查 registry,有则用,无则 fallback 到 DefaultRenderer,还支持 showJsonMode 全局开关强制用默认 JSON 渲染,见 packages/web-ui/src/tools/index.ts:28-44
  3. 头部辅助:renderHeaderrenderCollapsibleHeader 两个 helper 统一三种状态(inprogress/complete/error)的图标 + 文案 + 折叠交互,见 packages/web-ui/src/tools/renderer-registry.ts:29-130
  4. 内联渲染:ToolMessage.renderrenderTool 后,根据 isCustom 决定是否套卡片,见 packages/web-ui/src/components/Messages.ts:258-276
  5. 内置渲染器:extract-document.tsjavascript-repl.ts 在文件末尾 registerToolRenderer(name, renderer) 自动注册,见 packages/web-ui/src/tools/extract-document.ts:275packages/web-ui/src/tools/javascript-repl.ts:293

设计动机

为什么不直接在 AgentTool 上加 render 方法?因为工具的 execute 在 pi-agent-core 层定义,是后端逻辑;渲染在 pi-web-ui 层定义,是前端逻辑。两层不能耦合——同一个 bash 工具可能在 CLI、TUI、web 三种 UI 里用,execute 复用,渲染各写各的。registry 模式让 web-ui 可以给任意工具名注册渲染器,即使工具本身在另一个包里。

为什么 renderTool 返回 {content, isCustom} 而不是 TemplateResult?因为有些渲染器希望自己控制整个布局(比如 artifacts 工具内联在画布上,不要卡片框),isCustom: trueToolMessage 不套卡片 wrapper,直接返回 content;isCustom: false 则套一个 border rounded-md 卡片。这种「自定义内容 + 默认外壳」的二选一,既给渲染器自由,又保证大多数工具有统一外观。

为什么 extract-documentjavascript-repl 把 renderer 和 tool 放同一文件?因为它们是配对的:tool 的 result.details 形状由 tool 定义,renderer 直接消费。同文件维护减少不一致风险,文件末尾的 registerToolRenderer 是「自动注册」——tools/index.tsimport "./javascript-repl.js" 触发副作用完成注册,host 不需要手动调用。

关键文件

renderTool 是统一入口,逻辑很短:

typescript
// 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);
}

javascript-repl.ts 末尾的自动注册,是「import 即注册」模式:

typescript
// packages/web-ui/src/tools/javascript-repl.ts:292-293
// Auto-register the renderer
registerToolRenderer(javascriptReplTool.name, javascriptReplRenderer);

数据流

工具调用的渲染路径,从 AssistantMessage 触发到 renderer 返回:

边界与失败

小结

renderer-registry 把工具的 execute 与渲染解耦:renderTool 按 toolName 查 Map,命中用自定义 renderer,否则走 DefaultRendererextract_documentjavascript_repl 用「import 即注册」模式自动登记。渲染入口在 消息渲染组件ToolMessage,工具实例的装配在 ChatPanel 顶层元素