'use client'; /** * ToolPayloadValue — the single, shared renderer for a tool call's * `input` / `output` / live `streaming` payload. * * Routing (one implementation, reused everywhere): * - object / array (and NOT streaming) → interactive collapsible * `LazyJsonTree` (the same viewer the chat `json` block + document * inspector use). It renders values as React children — never * `innerHTML` — so a tool result carrying HTML/markdown (e.g. a fetched * web page) is shown as text, not executed: XSS-safe. * - string / scalar / streaming → plain `
` text. A streaming payload * arrives as a partial string and must NOT be parsed mid-flight; prose / * scalars have no tree to show. * * Both the default `DefaultPayload` (inside `ToolCalls`) AND a host that owns * its own tool-call card (e.g. cmdop's `` action card, reached via * the `renderToolCall` override) render THROUGH this — so the object→tree * behaviour can't drift between the two. `surfaceClassName` lets a host match * its own card's surface token (the only thing that differed when the two * implementations were separate). */ import { cn } from '@djangocfg/ui-core/lib'; import { LazyJsonTree } from '../../data/JsonTree/lazy'; // Defined locally (NOT imported from ./ToolCalls) on purpose: ToolCalls // imports ToolPayloadValue, so importing the type back would form an import // cycle. Under the `'use client'` + subpath-barrel (`./chat` → lazy.tsx) setup // that cycle makes Vite drop this module's symbol from the re-export graph at // runtime (the export typechecks via index.ts but fails to bundle — the exact // `MarkdownMessage` gotcha documented in lazy.tsx). The union is the SAME shape // ToolCalls re-exports as `ToolPayloadKind`; keep them in sync (trivial). export type ToolPayloadKind = 'input' | 'output' | 'streaming'; /** A structured value worth showing in the JSON tree (object or array). */ function isStructured(value: unknown): value is object { return value !== null && typeof value === 'object'; } function safeStringify(value: unknown): string { try { return JSON.stringify(value, null, 2); } catch { return String(value); } } export interface ToolPayloadValueProps { value: unknown; /** Drives expand depth (input collapsed, output 1 level), muted text, and * whether a streaming partial is parsed (it never is). */ kind: ToolPayloadKind; /** Surface token for the tree wrapper + ` ` background. Defaults to the * chat panel's `bg-background/60`; a host card can pass e.g. `bg-muted/40`. */ surfaceClassName?: string; /** * Show the JsonTree toolbar (search / expand / copy). Default **false**: * the tool-call panel already has its own header (tool name + status), so a * second toolbar is visual noise — and in `'auto'` mode JsonTree RESERVES the * toolbar's height even while hidden, leaving an empty gap above the tree. * Opt in (`toolbar`) only when copy/search on the raw payload is worth the * extra chrome. When true the toolbar is hover-revealed (`'auto'`). */ toolbar?: boolean; } export function ToolPayloadValue({ value, kind, surfaceClassName = 'bg-background/60', toolbar = false, }: ToolPayloadValueProps) { if (kind !== 'streaming' && isStructured(value)) { return (); } return ({typeof value === 'string' ? value : safeStringify(value)}); }