AI output
Tool call
One tool call made by an AI agent, from its arguments to its result, with a single status line that changes in place.
Live preview
Loading preview
A search_flights call that goes from Preparing (arguments arriving) to Running to Done with a small result, and a second call that fails and offers Try again. Replay restarts both.
Installation
Run this in your app after the one-time setup on the Getting started page.
$ npx @ganisigit/lunia-ds add tool-callUsage
The demo above, as code.
demo.tsx
'use client';
import * as React from 'react';
import { ToolCall, type ToolCallStatus } from './tool-call';
type Call = {
status: ToolCallStatus;
input: Record<string, string>;
output?: Record<string, string>;
duration?: number;
};
const INPUT = { from: 'Jakarta (CGK)', to: 'Singapore (SIN)', date: '2 Nov 2026', passengers: '2' };
const OUTPUT = { flights: '12 found', cheapest: '$84, leaves at 07:15' };
const HOTEL_INPUT = { city: 'Singapore', nights: '3', guests: '2' };
const FLIGHT_DONE: Call = { status: 'done', input: INPUT, output: OUTPUT, duration: 1400 };
const HOTEL_FAILED: Call = { status: 'error', input: HOTEL_INPUT };
export default function ToolCallDemo({ replay = 0 }: { replay?: number }) {
const [flight, setFlight] = React.useState<Call>(FLIGHT_DONE);
const [hotel, setHotel] = React.useState<Call>(HOTEL_FAILED);
const timers = React.useRef<number[]>([]);
const later = (ms: number, run: () => void) => timers.current.push(window.setTimeout(run, ms));
const clear = () => {
for (const id of timers.current) window.clearTimeout(id);
timers.current = [];
};
// biome-ignore lint/correctness/useExhaustiveDependencies: clears the timers once, on unmount
React.useEffect(() => clear, []);
const runHotel = (delay: number) => {
setHotel({ status: 'preparing', input: {} });
later(delay + 300, () => setHotel({ status: 'preparing', input: { city: 'Singapore' } }));
later(delay + 700, () => setHotel({ status: 'running', input: HOTEL_INPUT }));
later(delay + 2300, () => setHotel(HOTEL_FAILED));
};
const replayRef = React.useRef(() => {});
replayRef.current = () => {
clear();
const entries = Object.entries(INPUT);
setFlight({ status: 'preparing', input: {} });
for (let i = 0; i < entries.length; i++)
later(250 + i * 280, () =>
setFlight({ status: 'preparing', input: Object.fromEntries(entries.slice(0, i + 1)) }),
);
later(1500, () => setFlight({ status: 'running', input: INPUT }));
later(2900, () => setFlight(FLIGHT_DONE));
runHotel(600);
};
React.useEffect(() => {
if (replay > 0) replayRef.current();
}, [replay]);
return (
<div className="flex w-full max-w-[480px] flex-col gap-lg">
<ToolCall
name="search_flights"
title="Search flights"
status={flight.status}
input={flight.input}
output={flight.output}
duration={flight.duration}
defaultOpen
/>
<ToolCall
name="book_hotel"
title="Book a hotel"
status={hotel.status}
input={hotel.input}
errorText="The hotel service did not respond."
defaultOpen
onRetry={() => {
clear();
runHotel(0);
}}
/>
</div>
);
}
API
Parts: root, header (button), tool icon, title, status (icon slot + label), run time, chevron, details panel, input section, output section, error section with retry.
| Prop | Type | Default | Description |
|---|---|---|---|
| name | string | - | The tool name as the model calls it, for example "search_flights". |
| title | string | name | A readable label, for example "Search flights". |
| status | "preparing" | "running" | "done" | "error" | "denied" | - | Current step of the call. |
| input | unknown | undefined | Arguments; may be partial while preparing. |
| output | unknown | undefined | The result once done. |
| errorText | string | undefined | Message shown when the call failed. |
| duration | number | undefined | Run time in ms, shown when done. |
| open | boolean | undefined | Controlled open state of the details panel. |
| defaultOpen | boolean | false | Open state at first when not controlled. |
| Event | Description |
|---|---|
| openChange | The details panel opened or closed. Payload: the new open state. |
| retry | The person pressed Try again after an error. |
| Slot | Description |
|---|---|
| icon | Tool icon. |
| output | Custom view of the result instead of the default fields. |
Motion
Every row names a token from the Motion page. The last column says what happens when the user asks for reduced motion.
| ID | Trigger | Element | Property | From → to | Timing | Reduced motion |
|---|---|---|---|---|---|---|
| TC1 | While preparing | status label | sheen (mask position) | 200% → 0%, repeats | duration-loop (1600)ease-linear | No loop |
| TC2 | Preparing to running | status icon slot | width, gap | 0, 0 → 14px, 5px | duration-xl (380)ease-out | Resizes at once |
| TC3 | Status change | status icons | opacity, blur | 0, blur-swap → 1, 0 | duration-md (260)ease-out | Swaps at once |
| TC14 | Status change | status icons | scale | scale-swap → 1 | duration-xl (380)ease-out | Swaps at once |
| TC4 | Running to done | check mark | stroke drawn | 0% → 100% | duration-2xl (420)ease-outdelay 120ms | Shows at once |
| TC5 | Status change | status label | text morph | Preparing → Running, Failed | duration-3xl (460)ease-out | Swaps at once |
| TC6 | Running to done | run time | column width (grid columns) | 0fr → 1fr | duration-2xl (420)ease-out | Shows at once |
| TC15 | Running to done | run time | opacity | 0 → 1 | duration-lg (320)ease-out | Shows at once |
| TC7 | Running to done | run time | number roll | 0 → 1.4s | duration-6xl (900)ease-out | Shows at once |
| TC8 | Running to error | status label | text color | text-tertiary → text-error-primary | duration-lg (320)ease-out | Swaps at once |
| TC9 | Open or close | details panel | height | 0 → content height | duration-2xl (420)ease-out | Resizes at once |
| TC16 | Open or close | details panel | opacity | 0 → 1 | duration-lg (320)ease-out | Shows at once |
| TC10 | Arguments or output arrive | details panel | height | old height → new height | duration-2xl (420)ease-out | Resizes at once |
| TC11 | Output or error appears | new section | opacity, translate y | 0, distance-rise → 1, 0 | duration-lg (320)ease-settle | Shows at once |
| TC12 | Open or close | chevron | rotation | 0deg → 180deg | duration-lg (320)ease-out | Swaps at once |
| TC13 | Fails while closed | details panel | opens once by itself | closed → open (same as TC9) | duration-2xl (420)ease-out | Resizes at once |
Accessibility
- The header is a button with aria-expanded and aria-controls pointing to the details panel.
- The status label is in a role="status" aria-live="polite" region, so each step is announced once.
- Icons and the chevron are aria-hidden; the run time column is aria-hidden until it is shown.
- Error state uses red text plus the word Failed and an icon, never color alone.