{"slug":"tool-call-chip","category":"loaders","job":"oversee","added":"2026-09-12T06:37Z","title":"Tool Call Chip","description":"An inline pill naming an in-flight tool call that resolves into a result summary.","url":"https://ai-patterns.app/patterns/loaders/tool-call-chip","markdown":"https://ai-patterns.app/patterns/loaders/tool-call-chip.md","json":"https://ai-patterns.app/api/patterns/loaders/tool-call-chip","seenIn":[{"team":"chatgpt","source":"https://help.openai.com/en/articles/9237897-chatgpt-search"},{"team":"Claude","source":"https://support.claude.com/en/articles/10684626-enable-and-use-web-search"}],"spec":"# Tool Call Chip\n\n## Summary\nA 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.\n\n## When to use\n- 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).\n- Inside an Expandable Trace or a message stream, as the live version of what becomes a completed step once resolved.\n\n## When not to use\n- 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.\n- 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.\n- When the target is sensitive and shouldn't be echoed verbatim (e.g. a raw credential) — summarize instead of printing it raw.\n\n## Anatomy\n- Icon: represents the tool's kind (search, file, code, network, ...).\n- Label: a present-tense verb plus its target, e.g. `Searching \"tailwind v4 changelog\"`.\n- State indicator: a small spinner while running; swaps to a check icon on success or an alert icon on failure.\n- Resolved label: replaces the in-progress label with a brief result summary, e.g. `Searched — 4 results`.\n\n## Behavior\n- Appears the instant the call is dispatched — don't wait for a response to show that work has started.\n- The target truncates with an ellipsis if it's long; never wraps to a second line.\n- 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.\n- Once resolved, the chip stays visible as part of the record rather than vanishing; it commonly becomes one row of a subsequent Expandable Trace.\n- Multiple sequential calls in one turn render as multiple chips in order, each independently transitioning from running to resolved.\n\n## Content guidelines\n- Verb + straight-quoted target, one line: `Reading \"component.tsx\"`, `Calling \"get_weather\"`.\n- Keep the resolved summary equally short — a count or outcome, not a restatement of the whole result.\n- Stay truthful: never show a tool name or target that doesn't match what's actually running.\n\n## Accessibility\n- Wrap the label and state indicator in an `aria-live=\"polite\"` region so the running → resolved transition is announced.\n- 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.\n- Give the icon `aria-hidden` and let the text label carry the accessible name.\n\n## Related patterns\n- **Thinking Loader** — Use it instead when the agent is working but no specific tool call is active.\n- **Expandable Trace** — Use it as well once the turn completes, where a sequence of resolved chips ends up.\n- **Tool Approval** — Use it before the chip when the call needs permission to run.\n- **Terminal Stream** — Use it instead when the call's full output matters and should be readable live.\n","code":"\"use client\";\n\nimport * as React from \"react\";\nimport { AnimatePresence, motion } from \"motion/react\";\nimport { AlertCircle, Check, Code2, FileText, Globe, Loader2, Search } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst ICONS = {\n  search: Search,\n  file: FileText,\n  code: Code2,\n  network: Globe,\n} as const;\n\nexport type ToolCallKind = keyof typeof ICONS;\nexport type ToolCallStatus = \"running\" | \"success\" | \"error\";\n\nexport interface ToolCallChipProps {\n  kind?: ToolCallKind;\n  verb: string;\n  target: string;\n  status: ToolCallStatus;\n  /** Replaces the verb + target label once status is \"success\" or \"error\". */\n  result?: string;\n  className?: string;\n}\n\nexport function ToolCallChip({\n  kind = \"search\",\n  verb,\n  target,\n  status,\n  result,\n  className,\n}: ToolCallChipProps) {\n  const Icon = ICONS[kind];\n\n  return (\n    <div\n      className={cn(\n        \"inline-flex max-w-full items-center gap-2 rounded-full border bg-card px-3 py-1.5 text-xs\",\n        className\n      )}\n    >\n      <Icon className=\"size-3.5 shrink-0 text-muted-foreground\" aria-hidden />\n      <span aria-live=\"polite\" className=\"min-w-0 flex-1 truncate\">\n        {status === \"running\" ? (\n          <>\n            {verb} <span className=\"text-muted-foreground\">&quot;{target}&quot;</span>\n          </>\n        ) : (\n          (result ?? `${verb} \"${target}\"`)\n        )}\n      </span>\n      <AnimatePresence mode=\"wait\" initial={false}>\n        <motion.span\n          key={status}\n          initial={{ opacity: 0, scale: 0.7 }}\n          animate={{ opacity: 1, scale: 1 }}\n          exit={{ opacity: 0, scale: 0.7 }}\n          transition={{ duration: 0.15 }}\n          className=\"shrink-0\"\n        >\n          {status === \"running\" && (\n            <Loader2 className=\"size-3.5 animate-spin text-muted-foreground\" aria-hidden />\n          )}\n          {status === \"success\" && (\n            <Check className=\"size-3.5 text-emerald-600 dark:text-emerald-400\" aria-hidden />\n          )}\n          {status === \"error\" && <AlertCircle className=\"size-3.5 text-destructive\" aria-hidden />}\n        </motion.span>\n      </AnimatePresence>\n    </div>\n  );\n}\n","dependencies":["lucide-react","motion"],"localImports":["@/lib/utils"]}