import { useState, type CSSProperties, type ReactNode } from 'react'; import type { ToolCallStatus, ToolInvocation } from './types.js'; export interface ToolCallCardProps { invocation: ToolInvocation; /** Initially expanded? Default: false. */ defaultExpanded?: boolean; /** Override the title rendered to the right of the status pill. */ title?: ReactNode; /** Render a custom output preview (overrides default JSON pretty-print). */ renderOutput?: (output: unknown) => ReactNode; className?: string; style?: CSSProperties; } /** * Disclosure card showing a single LLM tool call. Collapsed by default, * expands to show the input + output JSON. Status pill + name are always * visible. * * Visual states: * pending — gray pill, dotted spinner * streaming — accent pill, animated dots * success — success-token pill, ✓ * error — danger-token pill, ✕ + error string in body */ export function ToolCallCard({ invocation, defaultExpanded = false, title, renderOutput, className, style, }: ToolCallCardProps) { const [open, setOpen] = useState(defaultExpanded); const { name, status, input, output, error, durationMs } = invocation; return (
{open && (
{input !== undefined && (
{stringifyShort(input)}
)} {status === 'error' ? (
{error ?? 'Tool call failed.'}
) : output !== undefined ? (
{renderOutput ? renderOutput(output) :
{stringifyShort(output)}
}
) : null}
)}
); } function StatusPill({ status }: { status: ToolCallStatus }) { const map: Record = { pending: { bg: 'var(--bl-surface-muted, rgba(0,0,0,0.04))', fg: 'var(--bl-text-tertiary, #888)', label: '…', }, streaming: { bg: 'var(--bl-accent-muted, rgba(99,102,241,0.12))', fg: 'var(--bl-accent, #6366f1)', label: '⋯', }, success: { bg: 'var(--bl-success-muted, rgba(34,197,94,0.12))', fg: 'var(--bl-success, #22c55e)', label: '✓', }, error: { bg: 'var(--bl-danger-muted, rgba(239,68,68,0.12))', fg: 'var(--bl-danger, #ef4444)', label: '✕', }, }; const v = map[status]; return ( {v.label} ); } function Section({ label, tone, children, }: { label: string; tone?: 'danger'; children: ReactNode; }) { return (
{label}
{children}
); } function Pre({ children }: { children: ReactNode }) { return (
      {children}
    
); } function stringifyShort(value: unknown): string { if (typeof value === 'string') return value; try { return JSON.stringify(value, null, 2); } catch { return String(value); } }