Skip to content

工具渲染器註冊表 ​

源码版本v0.73.1

pi-web-ui 把工具呼叫的 UI 渲染解耦:工具的 execute 在 AgentTool 裡定義,渲染由 ToolRenderer 介面實作,兩者透過工具名在 toolRenderers Map 裡配對。renderTool 是統一入口,按工具名查 registry,命中就用自訂 renderer,沒命中就走 DefaultRenderer。內建 bash、extract_document、javascript_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. 標頭輔助:renderHeader 與 renderCollapsibleHeader 兩個 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.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 不需要手動呼叫。

關鍵檔案 ​

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,否則走 DefaultRenderer。extract_document 與 javascript_repl 用「import 即註冊」模式自動登記。渲染入口在 訊息渲染元件 的 ToolMessage,工具實例的裝配在 ChatPanel 頂層元素。