▚ToolCallcustom
Invocation card: state glyph, args, duration and collapsible output
$ termino tool-call — agent invocation cards
╭─────────────────────────────────────────────╮
│▾ ✓ read_file lib/custom/neo.ts 12ms│
│ │
│ +114 · 1 export const NEO │
│ span 234 — surface #2e3550 │
╰─────────────────────────────────────────────╯
╭─────────────────────────────────────────────╮
│▸ ✓ search_files pattern: neoPanel 4ms│
╰─────────────────────────────────────────────╯
╭─────────────────────────────────────────────╮
│▾ ⠋ edit_file lib/custom/tool-call.tsx 900ms│
╰─────────────────────────────────────────────╯
╭──────────────────────────────────────────────╮
│▸ · run_tests npm run lint │
╰──────────────────────────────────────────────╯
live running call accumulates ms — enter/space expands in terminal
##API
Beveled panel per tool invocation. Header row: ▾/▸ chevron, state glyph (· pending, spinner running, ✓ done, ✗ error), tool name, dimmed args, right-aligned ms duration. Enter/Space toggles an output section when focused.
properties
| prop | type | default | description |
|---|---|---|---|
| name | string | - | Tool name |
| args | string | - | Argument summary |
| state | ToolState | "pending" | pending | running | done | error |
| durationMs | number | - | Right-aligned duration |
| output | string[] | [] | Lines under the header when expanded |
| expanded / defaultExpanded | boolean | - / false | Controlled or initial expansion |
| onToggle / focused | () => void / boolean | - / true | Callback + keyboard gate |
| colors | Partial<NeoColors> | NEO | Bevel palette |
##Keymap
return / spaceExpand or collapse output (when focused)
state proppending | running | done | error
##Notes
- ▸The exact shape of Claude Code / opencode tool invocation logs, restyled as extruded cards.
- ▸Map a queue of ToolCalls to render the full tool-calling session.
##Source
Real implementation — this is the code that runs in your terminal.
1import { createElement as h, useEffect, useMemo, useState } from "react";2import { useKeyboard } from "@opentui/react";3import { Canvas } from "./canvas";4import { cellsWidth, type CursorCell } from "./chart";5import { NEO, SPIN, neoPanel, stateGlyph, type NeoColors } from "./neo";67export type ToolState = "pending" | "running" | "done" | "error";89export interface ToolCallProps {10 name: string;11 args?: string;12 state?: ToolState;13 durationMs?: number;14 output?: string[];15 expanded?: boolean;16 defaultExpanded?: boolean;17 onToggle?: () => void;18 focused?: boolean;19 color?: string;20 colors?: Partial<NeoColors>;21}2223export function ToolCall({24 name,25 args,26 state = "pending",27 durationMs,28 output,29 expanded: expandedProp,30 defaultExpanded,31 onToggle,32 focused = true,33 color = "#a5d98f",34 colors,35}: Readonly<ToolCallProps>) {36 const c = { ...NEO, ...colors };37 const [internal, setInternal] = useState(!!defaultExpanded);38 const expanded = expandedProp ?? internal;39 const [frame, setFrame] = useState(0);40 const running = state === "running";4142 useEffect(() => {43 if (!running) return;44 const timer = setInterval(() => setFrame((f) => (f + 1) % SPIN.length), 100);45 return () => clearInterval(timer);46 }, [running]);4748 useKeyboard((key) => {49 if (!focused) return;50 if (key.name === "return" || key.name === "space") {51 key.preventDefault?.();52 setInternal((v) => !v);53 onToggle?.();54 }55 });5657 const rows = useMemo(() => {58 const glyph = stateGlyph(state, { ch: SPIN[frame % SPIN.length] ?? SPIN[0], fg: color }, c);59 const body: CursorCell[] = [60 { ch: expanded ? "▾ " : "▸ ", fg: c.dim },61 glyph,62 { ch: ` ${name}`, fg: c.text },63 ];64 if (args) {65 body.push({ ch: ` ${args}`, fg: c.dim });66 }67 const right = durationMs !== undefined ? `${durationMs}ms` : "";68 let room = 48 - cellsWidth(body) - (right ? right.length + 2 : 0);69 while (room > 0) {70 body.push({ ch: " ", fg: c.text });71 room--;72 }73 if (right) body.push({ ch: ` ${right}`, fg: c.dim });7475 const inner: CursorCell[][] = [body];76 if (expanded && output?.length) {77 inner.push([{ ch: " ", fg: c.dim }]);78 for (const line of output) {79 inner.push([{ ch: ` ${line}`, fg: c.dim }]);80 }81 }82 return inner;83 // eslint-disable-next-line react-hooks/exhaustive-deps84 }, [name, args, state, durationMs, output, expanded, frame, color, colors, running]);8586 const panel = neoPanel(rows, c);87 const width = cellsWidth(panel[0] ?? []);88 return h("box", { flexDirection: "column", gap: 0, width }, h(Canvas, { rows: panel, width }));89}90