termino
~/docs/custom/tool-call

ToolCallcustom

Invocation card: state glyph, args, duration and collapsible output

~/custom/tool-call
live
$ 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
proptypedefaultdescription
namestring-Tool name
argsstring-Argument summary
stateToolState"pending"pending | running | done | error
durationMsnumber-Right-aligned duration
outputstring[][]Lines under the header when expanded
expanded / defaultExpandedboolean- / falseControlled or initial expansion
onToggle / focused() => void / boolean- / trueCallback + keyboard gate
colorsPartial<NeoColors>NEOBevel palette

##Keymap

~/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.

tsxtool-call.tsx
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";
6
7export type ToolState = "pending" | "running" | "done" | "error";
8
9export 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}
22
23export 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";
41
42 useEffect(() => {
43 if (!running) return;
44 const timer = setInterval(() => setFrame((f) => (f + 1) % SPIN.length), 100);
45 return () => clearInterval(timer);
46 }, [running]);
47
48 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 });
56
57 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 });
74
75 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-deps
84 }, [name, args, state, durationMs, output, expanded, frame, color, colors, running]);
85
86 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