工具渲染器注册表
pi-web-ui 把工具调用的 UI 渲染解耦:工具的 execute 在 AgentTool 里定义,渲染由 ToolRenderer 接口实现,两者通过工具名在 toolRenderers Map 里配对。renderTool 是统一入口,按工具名查 registry,命中就用自定义 renderer,没命中就走 DefaultRenderer。内置 bash、extract_document、javascript_repl 等渲染器,host 也可以通过 registerToolRenderer 注入业务专属渲染。
职责
- 全局 registry:
toolRenderersMap 按 toolName 存ToolRenderer实例,见packages/web-ui/src/tools/renderer-registry.ts:9-16。 - 统一入口:
renderTool(toolName, params, result, isStreaming)先查 registry,有则用,无则 fallback 到DefaultRenderer,还支持showJsonMode全局开关强制用默认 JSON 渲染,见packages/web-ui/src/tools/index.ts:28-44。 - 头部辅助:
renderHeader与renderCollapsibleHeader两个 helper 统一三种状态(inprogress/complete/error)的图标 + 文案 + 折叠交互,见packages/web-ui/src/tools/renderer-registry.ts:29-130。 - 内联渲染:
ToolMessage.render调renderTool后,根据isCustom决定是否套卡片,见packages/web-ui/src/components/Messages.ts:258-276。 - 内置渲染器:
extract-document.ts与javascript-repl.ts在文件末尾registerToolRenderer(name, renderer)自动注册,见packages/web-ui/src/tools/extract-document.ts:275与packages/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: true 让 ToolMessage 不套卡片 wrapper,直接返回 content;isCustom: false 则套一个 border rounded-md 卡片。这种「自定义内容 + 默认外壳」的二选一,既给渲染器自由,又保证大多数工具有统一外观。
为什么 extract-document 与 javascript-repl 把 renderer 和 tool 放同一文件?因为它们是配对的:tool 的 result.details 形状由 tool 定义,renderer 直接消费。同文件维护减少不一致风险,文件末尾的 registerToolRenderer 是「自动注册」——tools/index.ts 用 import "./javascript-repl.js" 触发副作用完成注册,host 不需要手动调用。
关键文件
packages/web-ui/src/tools/types.ts:4-15—ToolRenderResult与ToolRenderer接口,定义 render 签名。packages/web-ui/src/tools/renderer-registry.ts:9-23—toolRenderersMap、registerToolRenderer、getToolRenderer。packages/web-ui/src/tools/renderer-registry.ts:29-63—renderHeader:三态图标 + 文案。packages/web-ui/src/tools/renderer-registry.ts:69-130—renderCollapsibleHeader:带 chevron 折叠交互。packages/web-ui/src/tools/index.ts:9-13— 注册bashrenderer,初始化defaultRenderer,引入两个副作用 import。packages/web-ui/src/tools/index.ts:28-44—renderTool入口,处理showJsonMode与 fallback。packages/web-ui/src/tools/extract-document.ts:36-181—createExtractDocumentTool,直连 fetch 失败时 fallback 到 proxy。packages/web-ui/src/tools/extract-document.ts:190-272—extractDocumentRenderer:有 result/只有 params/无 params 三态渲染。packages/web-ui/src/tools/javascript-repl.ts:128-195—createJavaScriptReplTool,在沙箱 iframe 里执行代码并返回文件。packages/web-ui/src/tools/javascript-repl.ts:200-290—javascriptReplRenderer:可折叠代码块 + console 输出 + 附件 tile。packages/web-ui/src/components/Messages.ts:258-276—ToolMessage.render调renderTool,根据isCustom决定是否套卡片。
renderTool 是统一入口,逻辑很短:
// 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 即注册」模式:
// packages/web-ui/src/tools/javascript-repl.ts:292-293
// Auto-register the renderer
registerToolRenderer(javascriptReplTool.name, javascriptReplRenderer);数据流
工具调用的渲染路径,从 AssistantMessage 触发到 renderer 返回:
边界与失败
- 工具没注册 renderer:
renderToolfallback 到DefaultRenderer,用 JSON 展示 params 与 result,不至于空白,见packages/web-ui/src/tools/index.ts:39-43。 - 流式中 result 还没到:renderer 收到
result === undefined,用renderHeader(state="inprogress", ...)显示 spinner + 文案,见packages/web-ui/src/tools/extract-document.ts:249-264。 - aborted 工具调用:
ToolMessage在aborted时合成一个 isError 的ToolResultMessage占位,renderer 渲染为 error 态,见packages/web-ui/src/components/Messages.ts:248-258。 - CORS 失败降级:
extract_document直连 fetch 失败时,isCorsError命中且配了corsProxyUrl就重试代理,见packages/web-ui/src/tools/extract-document.ts:103-135。 - showJsonMode 调试开关:
setShowJsonMode(true)后所有工具强制走DefaultRenderer的 JSON 视图,便于排查 tool 返回值,见packages/web-ui/src/tools/index.ts:21-23。 - renderer 重复注册:
registerToolRenderer直接Map.set覆盖,后注册的生效,不报错,host 注入自定义 renderer 可覆盖内置实现。
小结
renderer-registry 把工具的 execute 与渲染解耦:renderTool 按 toolName 查 Map,命中用自定义 renderer,否则走 DefaultRenderer。extract_document 与 javascript_repl 用「import 即注册」模式自动登记。渲染入口在 消息渲染组件 的 ToolMessage,工具实例的装配在 ChatPanel 顶层元素。