工具渲染器註冊表
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 頂層元素。