ツールレンダラーレジストリ
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 を引き、あればそれを使い、無ければDefaultRendererにフォールバックする。showJsonModeのグローバルスイッチで強制的にデフォルトの JSON 描画にすることも可能。packages/web-ui/src/tools/index.ts:28-44参照。 - ヘッダー補助:
renderHeaderとrenderCollapsibleHeaderの 2 ヘルパーが、3 状態(inprogress/complete/error)のアイコン + 文言 + 折りたたみ操作を統一する。packages/web-ui/src/tools/renderer-registry.ts:29-130参照。 - インライン描画:
ToolMessage.renderはrenderToolを呼んだあと、isCustomに応じてカード wrapper の有無を決める。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 の 3 種の UI で使われるかもしれず、execute は再利用し、描画は各 UI で別々に書く。registry パターンにより、web-ui は任意のツール名にレンダラーを登録でき、ツール自身が別パッケージにあっても構わない。
なぜ renderTool は TemplateResult ではなく {content, isCustom} を返すのか。一部のレンダラーはレイアウト全体を自分で制御したいからだ(例えば 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:3 状態のアイコン + 文言。packages/web-ui/src/tools/renderer-registry.ts:69-130—renderCollapsibleHeader:chevron 付き折りたたみ操作。packages/web-ui/src/tools/index.ts:9-13—bashrenderer の登録、defaultRendererの初期化、2 つの副作用 import。packages/web-ui/src/tools/index.ts:28-44—renderTool入口、showJsonModeとフォールバックを処理。packages/web-ui/src/tools/extract-document.ts:36-181—createExtractDocumentTool、直結 fetch 失敗時にプロキシへフォールバック。packages/web-ui/src/tools/extract-document.ts:190-272—extractDocumentRenderer:result あり/params のみ/params 無しの 3 状態描画。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 未登録:
renderToolは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 最上位要素 で行われる。