AI Patterns

loaders

Tool Call Chip

An inline pill naming an in-flight tool call that resolves into a result summary.

Searching "tailwind v4 changelog"
"use client";

import * as React from "react";
import { AnimatePresence, motion } from "motion/react";
import { AlertCircle, Check, Code2, FileText, Globe, Loader2, Search } from "lucide-react";

import { cn } from "@/lib/utils";

const ICONS = {
  search: Search,
  file: FileText,
  code: Code2,
  network: Globe,
} as const;

export type ToolCallKind = keyof typeof ICONS;
export type ToolCallStatus = "running" | "success" | "error";

export interface ToolCallChipProps {
  kind?: ToolCallKind;
  verb: string;
  target: string;
  status: ToolCallStatus;
  /** Replaces the verb + target label once status is "success" or "error". */
  result?: string;
  className?: string;
}

export function ToolCallChip({
  kind = "search",
  verb,
  target,
  status,
  result,
  className,
}: ToolCallChipProps) {
  const Icon = ICONS[kind];

  return (
    <div
      className={cn(
        "inline-flex max-w-full items-center gap-2 rounded-full border bg-card px-3 py-1.5 text-xs",
        className
      )}
    >
      <Icon className="size-3.5 shrink-0 text-muted-foreground" aria-hidden />
      <span aria-live="polite" className="min-w-0 flex-1 truncate">
        {status === "running" ? (
          <>
            {verb} <span className="text-muted-foreground">&quot;{target}&quot;</span>
          </>
        ) : (
          (result ?? `${verb} "${target}"`)
        )}
      </span>
      <AnimatePresence mode="wait" initial={false}>
        <motion.span
          key={status}
          initial={{ opacity: 0, scale: 0.7 }}
          animate={{ opacity: 1, scale: 1 }}
          exit={{ opacity: 0, scale: 0.7 }}
          transition={{ duration: 0.15 }}
          className="shrink-0"
        >
          {status === "running" && (
            <Loader2 className="size-3.5 animate-spin text-muted-foreground" aria-hidden />
          )}
          {status === "success" && (
            <Check className="size-3.5 text-emerald-600 dark:text-emerald-400" aria-hidden />
          )}
          {status === "error" && <AlertCircle className="size-3.5 text-destructive" aria-hidden />}
        </motion.span>
      </AnimatePresence>
    </div>
  );
}

A UX spec for this pattern — written for agents implementing or reusing it, not the code.

# Tool Call Chip

## Summary
A compact inline pill shown while a specific, named tool call is in flight — a verb, its target, and a spinner — that resolves in place into a short result summary the instant the call finishes. Unlike the Thinking Loader's generic "the system is working" signal, this names the concrete action actually happening right now.

## When to use
- The agent is mid-call on a specific tool or function (a web search, a file read, a code execution, an API request) and the target is worth naming (the query, the filename, the endpoint).
- Inside an Expandable Trace or a message stream, as the live version of what becomes a completed step once resolved.

## When not to use
- For the overall "agent is working" state when no single tool call is active — use a Thinking Loader for that; don't invent a fake tool name to fill the gap.
- Stacked several deep for sub-steps of one logical call. One chip per call actually in flight — batch or fold finished ones into a trace instead of leaving a pile of chips on screen.
- When the target is sensitive and shouldn't be echoed verbatim (e.g. a raw credential) — summarize instead of printing it raw.

## Anatomy
- Icon: represents the tool's kind (search, file, code, network, ...).
- Label: a present-tense verb plus its target, e.g. `Searching "tailwind v4 changelog"`.
- State indicator: a small spinner while running; swaps to a check icon on success or an alert icon on failure.
- Resolved label: replaces the in-progress label with a brief result summary, e.g. `Searched — 4 results`.

## Behavior
- Appears the instant the call is dispatched — don't wait for a response to show that work has started.
- The target truncates with an ellipsis if it's long; never wraps to a second line.
- On completion, the spinner swaps to a check (or alert, on error) and the label updates to the result summary in the same chip — it does not get replaced by a new element.
- Once resolved, the chip stays visible as part of the record rather than vanishing; it commonly becomes one row of a subsequent Expandable Trace.
- Multiple sequential calls in one turn render as multiple chips in order, each independently transitioning from running to resolved.

## Content guidelines
- Verb + straight-quoted target, one line: `Reading "component.tsx"`, `Calling "get_weather"`.
- Keep the resolved summary equally short — a count or outcome, not a restatement of the whole result.
- Stay truthful: never show a tool name or target that doesn't match what's actually running.

## Accessibility
- Wrap the label and state indicator in an `aria-live="polite"` region so the running → resolved transition is announced.
- Don't rely on the spinner-to-check swap alone to signal completion — the label text change must carry the same meaning for non-visual users.
- Give the icon `aria-hidden` and let the text label carry the accessible name.

## Related patterns
- Thinking Loader is the generic counterpart for when no specific tool call is active.
- Expandable Trace is where a sequence of resolved chips typically ends up once a turn completes.
- Tool Approval is the gate that, when required, appears before a chip like this starts running.