Lunia DS

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.

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-call

Usage

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.

PropTypeDefaultDescription
namestring-The tool name as the model calls it, for example "search_flights".
titlestringnameA readable label, for example "Search flights".
status"preparing" | "running" | "done" | "error" | "denied"-Current step of the call.
inputunknownundefinedArguments; may be partial while preparing.
outputunknownundefinedThe result once done.
errorTextstringundefinedMessage shown when the call failed.
durationnumberundefinedRun time in ms, shown when done.
openbooleanundefinedControlled open state of the details panel.
defaultOpenbooleanfalseOpen state at first when not controlled.
EventDescription
openChangeThe details panel opened or closed. Payload: the new open state.
retryThe person pressed Try again after an error.
SlotDescription
iconTool icon.
outputCustom 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.

IDTriggerElementPropertyFrom → toTimingReduced motion
TC1While preparingstatus labelsheen (mask position)200% → 0%, repeatsduration-loop (1600)ease-linearNo loop
TC2Preparing to runningstatus icon slotwidth, gap0, 0 → 14px, 5pxduration-xl (380)ease-outResizes at once
TC3Status changestatus iconsopacity, blur0, blur-swap → 1, 0duration-md (260)ease-outSwaps at once
TC14Status changestatus iconsscalescale-swap → 1duration-xl (380)ease-outSwaps at once
TC4Running to donecheck markstroke drawn0% → 100%duration-2xl (420)ease-outdelay 120msShows at once
TC5Status changestatus labeltext morphPreparing → Running, Failedduration-3xl (460)ease-outSwaps at once
TC6Running to donerun timecolumn width (grid columns)0fr → 1frduration-2xl (420)ease-outShows at once
TC15Running to donerun timeopacity0 → 1duration-lg (320)ease-outShows at once
TC7Running to donerun timenumber roll0 → 1.4sduration-6xl (900)ease-outShows at once
TC8Running to errorstatus labeltext colortext-tertiary → text-error-primaryduration-lg (320)ease-outSwaps at once
TC9Open or closedetails panelheight0 → content heightduration-2xl (420)ease-outResizes at once
TC16Open or closedetails panelopacity0 → 1duration-lg (320)ease-outShows at once
TC10Arguments or output arrivedetails panelheightold height → new heightduration-2xl (420)ease-outResizes at once
TC11Output or error appearsnew sectionopacity, translate y0, distance-rise → 1, 0duration-lg (320)ease-settleShows at once
TC12Open or closechevronrotation0deg → 180degduration-lg (320)ease-outSwaps at once
TC13Fails while closeddetails panelopens once by itselfclosed → open (same as TC9)duration-2xl (420)ease-outResizes 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.