{"slug":"expandable-trace","category":"traces","job":"oversee","added":"2026-09-11T19:59Z","title":"Expandable Trace","description":"A collapsible \"Thought for Xs\" summary that expands into a step-by-step trace.","url":"https://ai-patterns.app/patterns/traces/expandable-trace","markdown":"https://ai-patterns.app/patterns/traces/expandable-trace.md","json":"https://ai-patterns.app/api/patterns/traces/expandable-trace","seenIn":[{"team":"Claude","source":"https://support.claude.com/en/articles/10574485-using-extended-thinking"}],"spec":"# Expandable Trace\n\n## Summary\nA collapsed-by-default summary line (\"Thought for N seconds\") that expands into a checklist of the discrete steps an agent took to produce its answer. It gives curious users a way to audit the reasoning without forcing everyone to read it by default.\n\n## When to use\n- Right after an agent finishes a multi-step task (tool calls, reasoning, search, edits) and you want to offer transparency without cluttering the default view.\n- When the steps have a natural, ordered sequence with a clear notion of \"done\" per step.\n\n## When not to use\n- For a single atomic action with no real sub-steps — there's nothing meaningful to expand into; just state the result.\n- As a substitute for real error handling. If a step failed, show that explicitly as a distinct state on that step (not by silently omitting it).\n- Auto-expanded by default in a dense feed. Default collapsed keeps the primary answer scannable; only auto-expand when the user asked to see reasoning, or in a dedicated debugging surface.\n\n## Anatomy\n- Header button: icon + \"Thought for N seconds\" + chevron. The entire header is the toggle target, not just the chevron.\n- Collapsible body: a vertical list of steps, each with a completion mark, a short label, and optional trailing metadata (e.g. \"6 sources\").\n- A connecting line between steps so they read as one continuous sequence, not disconnected items.\n\n## Behavior\n- Collapsed by default.\n- Expand/collapse animates height smoothly rather than snapping instantly.\n- The step list is a static historical record once rendered — it does not keep updating live. Use the Thinking Loader for the in-progress version of this information.\n- The duration shown in the header is fixed once the run is complete; it does not keep counting like the Thinking Loader's timer does.\n\n## Content guidelines\n- Step labels are short, neutral action phrases (\"Reading the uploaded document\"), not first-person narration (\"I read the uploaded document\").\n- Trailing metadata should be a single scannable fact, not another full sentence.\n\n## Accessibility\n- The header button needs `aria-expanded` reflecting current state.\n- The whole component must be operable by keyboard (Enter/Space on the header toggles it).\n- Never hide information the user needs to trust or act on the primary answer exclusively inside the collapsed trace — the answer must stand on its own without expanding this.\n\n## Related patterns\n- **Thinking Loader** — Use it instead while work is still in progress, then swap in this trace when it finishes.\n- **Stream of Thought** — Use it instead when the reasoning should stream in live before folding into this trace.\n- **Multi-Agent Trace** — Use it instead when the work was split across parallel sub-agents.\n","code":"\"use client\";\n\nimport * as React from \"react\";\nimport { AnimatePresence, motion } from \"motion/react\";\nimport { Check, ChevronDown, Sparkles } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\n\nexport interface TraceStep {\n  label: string;\n  meta?: string;\n}\n\nexport interface ExpandableTraceProps {\n  durationSeconds?: number;\n  steps: TraceStep[];\n  defaultOpen?: boolean;\n  className?: string;\n}\n\nexport function ExpandableTrace({\n  durationSeconds = 4,\n  steps,\n  defaultOpen = false,\n  className,\n}: ExpandableTraceProps) {\n  const [open, setOpen] = React.useState(defaultOpen);\n\n  return (\n    <div className={className}>\n      <button\n        type=\"button\"\n        onClick={() => setOpen((v) => !v)}\n        aria-expanded={open}\n        className=\"flex w-full items-center gap-2 px-4 py-3 text-left text-xs font-medium\"\n      >\n        <Sparkles className=\"size-4 text-muted-foreground\" />\n        <span>Thought for {durationSeconds} seconds</span>\n        <ChevronDown\n          className={cn(\n            \"ml-auto size-4 text-muted-foreground transition-transform duration-200\",\n            open && \"rotate-180\"\n          )}\n        />\n      </button>\n      <AnimatePresence initial={false}>\n        {open && (\n          <motion.div\n            initial={{ height: 0, opacity: 0 }}\n            animate={{ height: \"auto\", opacity: 1 }}\n            exit={{ height: 0, opacity: 0 }}\n            transition={{ duration: 0.2, ease: \"easeInOut\" }}\n            className=\"overflow-hidden\"\n          >\n            <ul className=\"px-4 pb-4\">\n              {steps.map((step, i) => (\n                <li key={i} className=\"flex gap-3\">\n                  <div className=\"flex flex-col items-center\">\n                    <span className=\"flex size-4 shrink-0 items-center justify-center rounded-full bg-muted\">\n                      <Check className=\"size-2.5 text-muted-foreground\" />\n                    </span>\n                    {i < steps.length - 1 && <span className=\"my-1 w-px flex-1 bg-border\" />}\n                  </div>\n                  <p className=\"pb-3 text-xs text-foreground/90\">\n                    {step.label}\n                    {step.meta && (\n                      <span className=\"ml-1.5 text-muted-foreground\">{step.meta}</span>\n                    )}\n                  </p>\n                </li>\n              ))}\n            </ul>\n          </motion.div>\n        )}\n      </AnimatePresence>\n    </div>\n  );\n}\n","dependencies":["lucide-react","motion"],"localImports":["@/lib/utils"]}