{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"trace","title":"Trace","description":"Show the steps an AI agent took, such as its reasoning, tool calls and sources, as one-line rows that can expand to their details. Search terms: agent steps, thinking, chain of thought, citations.","dependencies":["lucide-react@1.18.0"],"registryDependencies":["@siteplane/base","@siteplane/button","@siteplane/collapsible","@siteplane/motion"],"files":[{"path":"packages/ui/src/components/trace/trace.tsx","content":"\"use client\";\n\nimport { ChevronRightIcon } from \"lucide-react\";\nimport * as React from \"react\";\n\nimport { cn } from \"../../system/utils/cn\";\nimport { slotClass } from \"../../system/utils/slot-class\";\nimport { Button } from \"../button\";\nimport {\n  Collapsible,\n  CollapsiblePanel,\n  CollapsibleTrigger,\n} from \"../collapsible\";\nimport { MotionShimmer } from \"../motion\";\n\nimport \"./trace.css\";\n\n/**\n * The state of the work a line reports. A line with no status reports work\n * that is done.\n */\nexport type TraceStatus = \"running\" | \"error\" | \"pending\";\n\n/*\n * The row of a step that does something: the trigger of a collapsible step\n * and a link. Both are the kit's muted inline Button (rulebook T2), which owns\n * the colours at rest, on hover, press and keyboard focus, the focus outline\n * and the 44 px touch target. The Button's inline profile draws no box, so the\n * row has the height of its line; it is laid out as a flex row that is only as\n * wide as its words, and a long title ends with an ellipsis (K5). A text\n * Button underlines on hover; a step is not text in running prose, so it does\n * not.\n */\nconst traceRowClassName =\n  \"flex w-fit max-w-full min-w-0 gap-1 text-start no-underline\";\n\nconst TraceStatusContext = React.createContext<TraceStatus | undefined>(\n  undefined,\n);\n\n/**\n * The text of children that are nothing but text: strings and numbers, such as\n * `Ran {command}`. With an element among the children it is undefined, so the\n * server and the browser always agree on it.\n */\nfunction plainText(node: React.ReactNode): string | undefined {\n  if (node === null || node === undefined || typeof node === \"boolean\")\n    return \"\";\n  if (\n    typeof node === \"string\" ||\n    typeof node === \"number\" ||\n    typeof node === \"bigint\"\n  )\n    return `${node}`;\n  if (!Array.isArray(node)) return undefined;\n  let text = \"\";\n  for (const child of node as React.ReactNode[]) {\n    const part = plainText(child);\n    if (part === undefined) return undefined;\n    text += part;\n  }\n  return text;\n}\n\n/** The steps of an agent, as one-line rows with an icon, a title and a status. */\nexport function Trace({\n  className,\n  ...props\n}: React.ComponentProps<\"div\">): React.ReactElement {\n  return (\n    <div\n      className={cn(\n        slotClass(\"trace\", props),\n        \"flex w-full min-w-0 flex-col gap-2\",\n        className,\n      )}\n      data-slot=\"trace\"\n      {...props}\n    />\n  );\n}\n\nexport interface TraceLineProps extends React.ComponentProps<\"span\"> {\n  /**\n   * `running` is work in progress (write the title in the present tense),\n   * `error` is work that failed (say so in the title) and `pending` is work\n   * that has not started. Without a status the line reports work that is done.\n   */\n  status?: TraceStatus | undefined;\n}\n\n/**\n * One step. It is a span, so it is valid inside the button or the link that\n * makes it do something, and it is neither a button nor a tab stop on its own.\n * Its status reaches its title through context.\n */\nexport function TraceLine({\n  className,\n  status,\n  ...props\n}: TraceLineProps): React.ReactElement {\n  return (\n    <TraceStatusContext.Provider value={status}>\n      <span\n        className={cn(\n          slotClass(\"trace-line\", props),\n          \"flex w-fit max-w-full min-w-0 items-center gap-2\",\n          className,\n        )}\n        data-slot=\"trace-line\"\n        data-status={status}\n        {...props}\n      />\n    </TraceStatusContext.Provider>\n  );\n}\n\n/**\n * A decorative icon slot, hidden from assistive technology. Before the title\n * it names the action; after it, it is an indicator. The glyph is 16 px\n * (rulebook I3) and keeps its full opacity inside a Button.\n */\nexport function TraceIcon({\n  className,\n  ...props\n}: React.ComponentProps<\"span\">): React.ReactElement {\n  return (\n    <span\n      aria-hidden=\"true\"\n      className={cn(\n        slotClass(\"trace-icon\", props),\n        \"flex size-4 shrink-0 items-center justify-center [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:m-0 [&_svg]:opacity-100\",\n        className,\n      )}\n      data-slot=\"trace-icon\"\n      {...props}\n    />\n  );\n}\n\n/**\n * The step in one sentence on one line. A long title ends with an ellipsis and\n * carries its full text in `title` (rulebook K5), taken from text children or\n * given as the prop. While its line runs, the words shimmer.\n */\nexport function TraceTitle({\n  children,\n  className,\n  title,\n  ...props\n}: React.ComponentProps<\"span\">): React.ReactElement {\n  const status = React.useContext(TraceStatusContext);\n  return (\n    <span\n      className={cn(\n        slotClass(\"trace-title\", props),\n        \"min-w-0 truncate\",\n        className,\n      )}\n      data-slot=\"trace-title\"\n      title={title ?? (plainText(children)?.trim() || undefined)}\n      {...props}\n    >\n      {status === \"running\" ? (\n        <MotionShimmer>{children}</MotionShimmer>\n      ) : (\n        children\n      )}\n    </span>\n  );\n}\n\n/**\n * Secondary text after the title, such as a duration, a count or a hostname.\n * It is one text step smaller than the title. A short value stays whole; a\n * long one, such as a long hostname, ends with an ellipsis like the title and\n * carries its full text in `title`, so the row never widens the thread (K5).\n */\nexport function TraceDescription({\n  children,\n  className,\n  title,\n  ...props\n}: React.ComponentProps<\"span\">): React.ReactElement {\n  return (\n    <span\n      className={cn(\n        slotClass(\"trace-description\", props),\n        \"min-w-0 truncate text-(length:--sp-rw-font-12) leading-(--sp-rw-line-18) tabular-nums\",\n        className,\n      )}\n      data-slot=\"trace-description\"\n      title={title ?? (plainText(children)?.trim() || undefined)}\n      {...props}\n    >\n      {children}\n    </span>\n  );\n}\n\n/**\n * A step that expands. It takes the props of Collapsible: `open` with\n * `onOpenChange`, or `defaultOpen`. Steps start closed. To open a step while\n * it runs and let the reader's click win, pass `open ?? running` with the\n * reader's choice kept in state.\n */\nexport function TraceCollapsible({\n  className,\n  ...props\n}: React.ComponentProps<typeof Collapsible>): React.ReactElement {\n  return (\n    <Collapsible\n      className={cn(\"flex w-full min-w-0 flex-col\", className)}\n      data-slot=\"trace-collapsible\"\n      {...props}\n    />\n  );\n}\n\n/**\n * The button that opens and closes a step. Put a TraceLine in it; the chevron\n * after the line is added here, and it turns with the shared chevron rule of\n * Collapsible. The trigger is the kit's CollapsibleTrigger, so it keeps that\n * part's slot, `collapsible-trigger`, which the chevron, the cursor and the\n * open state are keyed on. Its title is its accessible name and\n * `aria-expanded` says whether it is open.\n */\nexport function TraceCollapsibleTrigger({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<typeof CollapsibleTrigger>): React.ReactElement {\n  return (\n    <CollapsibleTrigger\n      className={cn(traceRowClassName, className)}\n      // CollapsibleTrigger stamps its own profile on what it renders; the row\n      // keeps the inline profile of its Button.\n      data-profile=\"inline\"\n      render={<Button profile=\"inline\" profileTone=\"muted\" variant=\"text\" />}\n      {...props}\n    >\n      {children}\n      <TraceIcon>\n        <ChevronRightIcon\n          className=\"sp-collapsible-chevron\"\n          data-slot=\"collapsible-chevron\"\n          data-turn=\"quarter\"\n        />\n      </TraceIcon>\n    </CollapsibleTrigger>\n  );\n}\n\n/**\n * The detail of a step. The panel is the kit's CollapsiblePanel, which\n * measures the content and moves the height over the kit's expand motion, so\n * the content enters and leaves with its own presence. `className` styles the\n * content inside the panel; every other prop, such as `keepMounted`, goes to\n * the panel. With `keepMounted` the detail stays mounted while closed, so\n * steps nested in it keep their open state.\n */\nexport function TraceCollapsibleContent({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<typeof CollapsiblePanel>): React.ReactElement {\n  return (\n    <CollapsiblePanel {...props}>\n      <div\n        className={cn(slotClass(\"trace-collapsible-content\"), className)}\n        data-slot=\"trace-collapsible-content\"\n      >\n        {children}\n      </div>\n    </CollapsiblePanel>\n  );\n}\n\n/** The words a TraceLink speaks to assistive technology. */\nexport type TraceLinkLabels = {\n  /** Read after the title of a link that opens in a new tab. */\n  newTab: string;\n};\n\nconst defaultTraceLinkLabels: TraceLinkLabels = {\n  newTab: \"(opens in a new tab)\",\n};\n\nexport interface TraceLinkProps extends React.ComponentProps<\"a\"> {\n  /** Replaces the English words a screen reader hears. */\n  labels?: Partial<TraceLinkLabels> | undefined;\n}\n\n/**\n * A step that is a link. Put a TraceLine in it. It is the same row as a\n * collapsible step, with the same colours, focus outline and touch target. A\n * link with `target=\"_blank\"` says so to screen readers after its title; show\n * it with an arrow icon after the line.\n */\nexport function TraceLink({\n  children,\n  className,\n  labels,\n  ...props\n}: TraceLinkProps): React.ReactElement {\n  const text = { ...defaultTraceLinkLabels, ...labels };\n  return (\n    <Button\n      className={cn(traceRowClassName, className)}\n      data-slot=\"trace-link\"\n      profile=\"inline\"\n      profileTone=\"muted\"\n      render={<a {...props} />}\n      variant=\"text\"\n    >\n      {children}\n      {props.target === \"_blank\" ? (\n        <span className=\"sr-only\"> {text.newTab}</span>\n      ) : null}\n    </Button>\n  );\n}\n","type":"registry:ui","target":"~/src/siteplane/components/trace/trace.tsx"},{"path":"packages/ui/src/components/trace/trace.css","content":"@layer theme, base, components, siteplane, utilities;\n\n@layer siteplane {\n  /* A Trace is the steps an agent took, as quiet rows of text: the tertiary\n   * text colour and the kit's 14 px body step. Zero specificity, so a\n   * className or a utility refines it. A row that does something, a\n   * CollapsibleTrigger or a TraceLink, is the kit's muted inline Button\n   * (rulebook T2): it rests in this colour and turns primary on hover, press\n   * and keyboard focus, and its line follows it because the line sets no\n   * colour of its own. */\n  :where(.sp-trace) {\n    color: var(--sp-rw-fg-tertiary);\n    font-size: var(--sp-rw-font-14);\n    line-height: var(--sp-rw-line-21);\n  }\n\n  /* Work that failed reads in the destructive text tone, also while the\n   * pointer is over its row. The title says so in words: colour alone is\n   * never the status. */\n  .sp-trace-line[data-status=\"error\"] {\n    color: var(--sp-rw-fg-destructive);\n  }\n\n  /* Work that has not started, such as a step of a plan. */\n  .sp-trace-line[data-status=\"pending\"] {\n    opacity: 0.6;\n  }\n\n  /* An icon after the title is an indicator, such as the arrow of a link that\n   * leaves the page. It sits half the line's gap closer to the words. */\n  .sp-trace-title ~ .sp-trace-icon {\n    margin-inline-start: calc(var(--sp-rw-space-1) * -1);\n  }\n\n  /* The detail of a row hangs from a rail under the centre of the row's 16 px\n   * icon, with its text starting where the title starts (24 px). The rail is\n   * the hairline of the surface tokens, so it paints as thick as every other\n   * hairline at every pixel density; half of its width is taken off the\n   * inset on each side of it, which keeps the centre and the text where they\n   * are. The space below is smaller than the space above: the Trace's gap\n   * follows it. Logical properties mirror it in a right-to-left page. */\n  .sp-trace-collapsible-content {\n    min-width: 0;\n    margin-inline-start: calc(\n      var(--sp-rw-space-2) - var(--sp-rw-border-default) / 2\n    );\n    border-inline-start: var(--sp-rw-border-default) solid\n      var(--sp-rw-border-translucent);\n    padding-block: var(--sp-rw-space-2) var(--sp-rw-space-1);\n    padding-inline: calc(var(--sp-rw-space-4) - var(--sp-rw-border-default) / 2)\n      var(--sp-rw-space-1);\n    color: var(--sp-rw-fg-tertiary);\n    overflow-wrap: anywhere;\n  }\n}\n","type":"registry:ui","target":"~/src/siteplane/components/trace/trace.css"},{"path":"packages/ui/src/components/trace/index.ts","content":"export * from \"./trace\";\n","type":"registry:ui","target":"~/src/siteplane/components/trace/index.ts"}],"type":"registry:ui","docs":"https://ui.siteplane.io/docs-markdown/components/trace.md","categories":["Chat & AI"],"meta":{"avoidWhen":["Avoid for a history of events by people, such as an audit log; Timeline shows events in time order.","Avoid for steps a person moves through, such as a checkout; Stepper shows where they are.","Avoid for a question that waits for the person, such as an approval of a tool call; ask it in an Alert after the Trace, and add the line once it is answered."],"description":"Show the steps an AI agent took, such as its reasoning, tool calls and sources, as one-line rows that can expand to their details.","knownLimitations":["Trace presents steps and holds no logic: the app maps its messages to lines, orders them and measures durations, which a description shows as text such as 6s.","The title is the accessible name of a line, and the status is not announced on its own: write it into the title, in the present tense while a step runs and as Failed to … when it failed.","A step that opens while it runs needs the app to keep the reader's choice in state and pass open ?? running; a step that is not controlled starts closed.","Titles and descriptions stay on one line; a long one ends with an ellipsis and carries its full text in a title attribute, taken from text children.","A link opens in a new tab only with target _blank, which the line announces to screen readers; the arrow icon after the title is the app's."],"providerNotes":["Depends on Collapsible, Button and the motion component, which the registry installs with Trace.","SiteplaneMotionProvider preset=\"reduced\" and the system setting keep the shimmer of a running title still and open and close the details at once."],"releaseStatus":"preview","searchTerms":["trace","agent steps","reasoning","thinking","tool call","chain of thought","sources","citations","ai"],"useWhen":["Use to show what an AI agent did while it answered: its reasoning, the tools it called and the sources it read, each as a line with a status.","Use for a step that expands to its details, such as the output of a tool call in a Code Block, or for a list of the links an answer used."]}}