| import { useState } from 'react' |
|
|
| const TOOL_META = { |
| write_file: { icon: '✎', label: 'Write file', summarize: (a) => a.path }, |
| read_file: { icon: '↗', label: 'Read file', summarize: (a) => a.path }, |
| delete_file: { icon: '✕', label: 'Delete file', summarize: (a) => a.path }, |
| list_dir: { icon: 'â–¤', label: 'List dir', summarize: (a) => a.path || '/' }, |
| read_logs: { icon: '⌗', label: 'Read logs', summarize: (a) => `${a.kind}${a.lines ? ` (${a.lines})` : ''}` }, |
| } |
|
|
| const STATUS_DOT = { |
| running: '#f59e0b', |
| ok: '#22c55e', |
| error: '#ef4444', |
| } |
|
|
| function tryPretty(s) { |
| if (typeof s !== 'string') return '' |
| try { |
| return JSON.stringify(JSON.parse(s), null, 2) |
| } catch { |
| return s |
| } |
| } |
|
|
| function ResultSummary({ result }) { |
| if (!result) return null |
| try { |
| const parsed = JSON.parse(result) |
| if (parsed.ok === false) return <span style={{ color: '#ef4444' }}>{parsed.error}</span> |
| |
| if (parsed.bytes !== undefined) return <span>{parsed.bytes} bytes written</span> |
| if (parsed.deleted) return <span>deleted</span> |
| if (parsed.entries) return <span>{parsed.count} entries</span> |
| if (parsed.lines !== undefined && parsed.content !== undefined) { |
| return <span>{parsed.lines} lines{parsed.truncated ? ' (truncated)' : ''}</span> |
| } |
| if (parsed.lines_shown !== undefined) { |
| return <span>{parsed.lines_shown}/{parsed.lines_total} log lines</span> |
| } |
| return <span>ok</span> |
| } catch { |
| return <span>ok</span> |
| } |
| } |
|
|
| export default function ToolCallBlock({ tool }) { |
| const [open, setOpen] = useState(false) |
| const meta = TOOL_META[tool.name] || { icon: 'âš™', label: tool.name, summarize: () => '' } |
| const args = tool.args || {} |
| const status = tool.status || 'running' |
| const dot = STATUS_DOT[status] || STATUS_DOT.running |
|
|
| return ( |
| <div |
| className="my-1.5 rounded-md text-[11.5px]" |
| style={{ |
| background: 'var(--surface-3)', |
| border: '1px solid var(--border)', |
| overflow: 'hidden', |
| }} |
| > |
| {/* Header — clickable */} |
| <button |
| onClick={() => setOpen((v) => !v)} |
| className="w-full flex items-center gap-2 px-2.5 py-1.5 mono text-left transition-colors hover:bg-[var(--surface-2)]" |
| style={{ color: 'var(--text-dim)' }} |
| > |
| <span |
| className="w-1.5 h-1.5 rounded-full shrink-0" |
| style={{ |
| background: dot, |
| animation: status === 'running' ? 'pulse-live 1.4s ease-in-out infinite' : undefined, |
| }} |
| /> |
| <span style={{ color: 'var(--accent-hi)', minWidth: 16 }}>{meta.icon}</span> |
| <span style={{ color: 'var(--text-dim)' }}>{meta.label}</span> |
| <span className="truncate" style={{ color: 'var(--text)', flex: 1 }}> |
| {meta.summarize(args)} |
| </span> |
| <span className="text-[10px]" style={{ color: 'var(--text-faint)' }}> |
| {status === 'running' ? '…' : <ResultSummary result={tool.result} />} |
| </span> |
| <span style={{ color: 'var(--text-faint)', fontSize: 9 }}> |
| {open ? 'â–¾' : 'â–¸'} |
| </span> |
| </button> |
| |
| {/* Body — args + result */} |
| {open && ( |
| <div className="px-2.5 py-2 border-t mono text-[10.5px] leading-relaxed" |
| style={{ borderColor: 'var(--border)' }}> |
| <div className="mb-1.5 uppercase tracking-wider text-[9px]" style={{ color: 'var(--text-faint)' }}> |
| args |
| </div> |
| <pre |
| className="whitespace-pre-wrap break-all" |
| style={{ color: 'var(--text-dim)', maxHeight: 200, overflow: 'auto' }} |
| >{JSON.stringify(args, null, 2)}</pre> |
| |
| {tool.result && ( |
| <> |
| <div className="mt-3 mb-1.5 uppercase tracking-wider text-[9px]" style={{ color: 'var(--text-faint)' }}> |
| result |
| </div> |
| <pre |
| className="whitespace-pre-wrap break-all" |
| style={{ color: 'var(--text-dim)', maxHeight: 240, overflow: 'auto' }} |
| >{tryPretty(tool.result)}</pre> |
| </> |
| )} |
| </div> |
| )} |
| </div> |
| ) |
| } |
|
|