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.render 調 renderTool 後,根據 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 頂層元素