# Trace

Rows for the steps an agent takes: reasoning, tool calls, sources, plans and file edits.

Trace is an open-source component from suiss/ui for React, React Native, SwiftUI and Jetpack Compose. The @esuiss/ui CLI copies its source into your project, so you own the code.

Source: https://suiss.app/ui/docs/components/trace

**React**

**Example**

```tsx
"use client"

import {
  ArrowUpRightIcon,
  CheckCircleIcon,
  GlobeIcon,
  WarningCircleIcon,
} from "@phosphor-icons/react"
import { Bubble, BubbleContent } from "@/components/ui/bubble"
import { CodeBlock, CodeBlockContent } from "@/components/ui/code-block"
import { Message, MessageContent } from "@/components/ui/message"
import {
  Trace,
  TraceCollapsible,
  TraceCollapsibleContent,
  TraceCollapsibleTrigger,
  TraceIcon,
  TraceLine,
  TraceTitle,
} from "@/components/ui/trace"

const commands = [
  { command: "pnpm --version", output: "10.18.0" },
  {
    command: "git diff --stat HEAD~1 -- package.json pnpm-lock.yaml",
    output:
      " package.json | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)",
  },
  {
    command: "pnpm install --frozen-lockfile",
    error:
      'ERR_PNPM_OUTDATED_LOCKFILE  Cannot install with "frozen-lockfile" because pnpm-lock.yaml is not up to date with package.json',
  },
]

const sources = [
  {
    url: "https://pnpm.io/cli/install#--frozen-lockfile",
    title: "pnpm install | pnpm.io",
  },
  {
    url: "https://pnpm.io/continuous-integration",
    title: "Continuous Integration | pnpm.io",
  },
]

export default function TraceDemo() {
  return (
    <div className="flex w-full max-w-sm flex-col gap-4 py-6 text-sm">
      <Message align="end">
        <MessageContent>
          <Bubble align="end" variant="muted">
            <BubbleContent>Why does pnpm install fail in CI?</BubbleContent>
          </Bubble>
        </MessageContent>
      </Message>
      <Message>
        <MessageContent className="gap-4">
          <Trace>
            <TraceCollapsible>
              <TraceCollapsibleTrigger>
                <TraceLine>
                  <TraceIcon>
                    <CheckCircleIcon />
                  </TraceIcon>
                  <TraceTitle>Thought for 5s</TraceTitle>
                </TraceLine>
              </TraceCollapsibleTrigger>
              <TraceCollapsibleContent>
                CI installs with a frozen lockfile, so the install stops when
                package.json and pnpm-lock.yaml disagree. I should check what
                the last commit changed, then reproduce the install.
              </TraceCollapsibleContent>
            </TraceCollapsible>
            {commands.map((item) => (
              <TraceCollapsible
                key={item.command}
                defaultOpen={Boolean(item.error)}
              >
                <TraceCollapsibleTrigger>
                  <TraceLine status={item.error ? "error" : undefined}>
                    <TraceIcon>
                      {item.error ? (
                        <WarningCircleIcon />
                      ) : (
                        <CheckCircleIcon />
                      )}
                    </TraceIcon>
                    <TraceTitle>
                      {item.error ? "Failed to run" : "Ran"} {item.command}
                    </TraceTitle>
                  </TraceLine>
                </TraceCollapsibleTrigger>
                <TraceCollapsibleContent>
                  <CodeBlock
                    size="sm"
                    variant={item.error ? "destructive" : "default"}
                  >
                    <CodeBlockContent wrap className="max-h-64">
                      {item.error ?? item.output}
                    </CodeBlockContent>
                  </CodeBlock>
                </TraceCollapsibleContent>
              </TraceCollapsible>
            ))}
            <TraceCollapsible>
              <TraceCollapsibleTrigger>
                <TraceLine>
                  <TraceIcon>
                    <GlobeIcon />
                  </TraceIcon>
                  <TraceTitle>Used {sources.length} sources</TraceTitle>
                </TraceLine>
              </TraceCollapsibleTrigger>
              <TraceCollapsibleContent>
                <Trace>
                  {sources.map((source) => (
                    <a
                      key={source.url}
                      href={source.url}
                      target="_blank"
                      rel="noreferrer"
                    >
                      <TraceLine>
                        <TraceIcon>
                          <GlobeIcon />
                        </TraceIcon>
                        <TraceTitle>
                          {source.title}
                          <span className="sr-only"> (opens in a new tab)</span>
                        </TraceTitle>
                        <TraceIcon>
                          <ArrowUpRightIcon />
                        </TraceIcon>
                      </TraceLine>
                    </a>
                  ))}
                </Trace>
              </TraceCollapsibleContent>
            </TraceCollapsible>
          </Trace>
          <p>
            The last commit bumped a version in package.json without updating
            pnpm-lock.yaml, so the frozen install fails. Run pnpm install
            locally and commit the lockfile.
          </p>
        </MessageContent>
      </Message>
    </div>
  )
}
```

A `Trace` is a list of `TraceLine` rows. Each row has a `TraceIcon`, a `TraceTitle`, and an optional `TraceDescription` and status. A row can expand to show more detail, link to a page, or open a dialog or sheet.

Every row has the same shape, so reasoning, tool calls, sources and plans built from Trace look and behave the same. Use Trace for the steps an agent takes, such as calling a tool, running a subagent or editing a file. Use [Message](https://suiss.app/ui/docs/components/message) for its reply and [Marker](https://suiss.app/ui/docs/components/marker) for notes and dividers between turns.

## Installation

### CLI

```bash title="Terminal"
npx shadcn@latest add @esuiss/trace
```

Works in any project with a `components.json`. See [Installation](https://suiss.app/ui/docs/installation#add-components) for the one-line registry setup. Or use the [suiss/ui CLI](https://suiss.app/ui/docs/cli):

```bash title="Terminal"
npx @esuiss/ui@latest add trace
```

### Manual

1. Add [Collapsible](https://suiss.app/ui/docs/components/collapsible) and the [`cn` helper](https://suiss.app/ui/docs/installation#the-cn-helper).

2. Copy the source into `components/ui/trace.tsx`:

```tsx title="components/ui/trace.tsx"
import { CaretRightIcon } from "@phosphor-icons/react"
import type * as React from "react"

import {
  Collapsible,
  CollapsibleContent,
  CollapsibleTrigger,
} from "@/components/ui/collapsible"
import { cn } from "@/lib/utils"

function Trace({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="trace"
      className={cn(
        "gap-2 flex w-full min-w-0 flex-col *:has-[>[data-slot=trace-line]]:flex *:has-[>[data-slot=trace-line]]:w-fit *:has-[>[data-slot=trace-line]]:max-w-full *:has-[>[data-slot=trace-line]]:min-w-0 *:has-[>[data-slot=trace-line]]:outline-none *:has-[>[data-slot=trace-line]]:rounded-lg *:has-[>[data-slot=trace-line]]:focus-visible:ring-3 *:has-[>[data-slot=trace-line]]:focus-visible:ring-ring/50 *:has-[>[data-slot=trace-line]]:text-start",
        className
      )}
      {...props}
    />
  )
}

function TraceLine({
  className,
  status,
  ...props
}: React.ComponentProps<"span"> & {
  status?: "running" | "error" | "pending"
}) {
  return (
    <span
      data-slot="trace-line"
      data-status={status}
      className={cn(
        "gap-2 text-sm flex w-fit max-w-full min-w-0 items-center text-muted-foreground transition-colors in-[a,button]:cursor-pointer in-[a:hover,button:hover]:text-foreground data-[status=error]:text-destructive data-[status=error]:in-[a:hover,button:hover]:text-destructive data-[status=pending]:opacity-60 data-[status=running]:*:data-[slot=trace-title]:shimmer",
        className
      )}
      {...props}
    />
  )
}

function TraceIcon({ className, ...props }: React.ComponentProps<"span">) {
  return (
    <span
      data-slot="trace-icon"
      aria-hidden="true"
      className={cn(
        "size-4 [[data-slot=trace-title]~&]:-ms-1 flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
        className
      )}
      {...props}
    />
  )
}

function TraceTitle({ className, ...props }: React.ComponentProps<"span">) {
  return (
    <span
      data-slot="trace-title"
      className={cn(
        "min-w-0 truncate motion-reduce:shimmer-none",
        className
      )}
      {...props}
    />
  )
}

function TraceDescription({
  className,
  ...props
}: React.ComponentProps<"span">) {
  return (
    <span
      data-slot="trace-description"
      className={cn(
        "shrink-0 text-muted-foreground/70 tabular-nums",
        className
      )}
      {...props}
    />
  )
}

function TraceCollapsible({
  className,
  ...props
}: Omit<React.ComponentProps<typeof Collapsible>, "open"> &
  (
    | { open?: undefined }
    | {
        open: boolean
        onOpenChange: NonNullable<
          React.ComponentProps<typeof Collapsible>["onOpenChange"]
        >
      }
  )) {
  return (
    <Collapsible
      data-slot="trace-collapsible"
      className={cn(
        "flex w-full min-w-0 flex-col",
        className
      )}
      {...props}
    />
  )
}

function TraceCollapsibleTrigger({
  className,
  children,
  ...props
}: React.ComponentProps<typeof CollapsibleTrigger>) {
  return (
    <CollapsibleTrigger
      data-slot="trace-collapsible-trigger"
      className={cn(
        "gap-1 rounded-lg focus-visible:ring-3 focus-visible:ring-ring/50 text-start group/trace-collapsible-trigger flex w-fit max-w-full min-w-0 cursor-pointer items-center text-muted-foreground transition-colors outline-none hover:text-foreground has-[[data-status=error]]:text-destructive",
        className
      )}
      {...props}
    >
      {children}
      <TraceIcon className="transition-transform group-aria-expanded/trace-collapsible-trigger:rotate-90">
        <CaretRightIcon />
      </TraceIcon>
    </CollapsibleTrigger>
  )
}

function TraceCollapsibleContent({
  className,
  children,
  ...props
}: React.ComponentProps<typeof CollapsibleContent>) {
  return (
    <CollapsibleContent
      data-slot="trace-collapsible-content"
      className="duration-200 h-(--collapsible-panel-height) min-w-0 overflow-hidden transition-[height] data-ending-style:h-0 data-starting-style:h-0"
      {...props}
    >
      <div
        data-slot="trace-collapsible-content-inner"
        className={cn(
          "mt-2 mb-1 ms-[calc(--spacing(2)-1px)] border-s pe-1 ps-4 text-sm leading-relaxed min-w-0 wrap-break-word text-muted-foreground",
          className
        )}
      >
        {children}
      </div>
    </CollapsibleContent>
  )
}

export {
  Trace,
  TraceCollapsible,
  TraceCollapsibleContent,
  TraceCollapsibleTrigger,
  TraceDescription,
  TraceIcon,
  TraceLine,
  TraceTitle,
}
```

3. Update the import paths to match your project.

## Usage

```tsx
import { Trace, TraceIcon, TraceLine, TraceTitle } from "@/components/ui/trace"
```

```tsx
<Trace>
  <TraceLine>
    <TraceIcon>
      <SearchIcon />
    </TraceIcon>
    <TraceTitle>Searched for minimumReleaseAge</TraceTitle>
  </TraceLine>
</Trace>
```

Wrap the row in a `TraceCollapsible` to show more detail when it expands. Put the `TraceLine` inside `TraceCollapsibleTrigger` and the detail in `TraceCollapsibleContent`:

```tsx
<Trace>
  <TraceCollapsible>
    <TraceCollapsibleTrigger>
      <TraceLine>
        <TraceIcon>
          <TerminalIcon />
        </TraceIcon>
        <TraceTitle>Ran node --version</TraceTitle>
      </TraceLine>
    </TraceCollapsibleTrigger>
    <TraceCollapsibleContent>v24.18.0</TraceCollapsibleContent>
  </TraceCollapsible>
</Trace>
```

`TraceLine` is always the row. To make a row do something, wrap it in that thing's trigger: a link, a `DialogTrigger`, a `SheetTrigger`, a drawer or a popover.

Rows start closed. Set `defaultOpen` on `TraceCollapsible` to start open, or pass `open` with `onOpenChange` to control it. Set `keepMounted` on `TraceCollapsibleContent` so nested rows keep their open state while the parent is closed.

## Examples

### Static

A `TraceLine` on its own, for a step with no detail to show. It reads as plain text.

**Example**

```tsx
import { GitBranchIcon } from "@phosphor-icons/react"
import { Trace, TraceIcon, TraceLine, TraceTitle } from "@/components/ui/trace"

export default function TraceStatic() {
  return (
    <Trace className="max-w-sm">
      <TraceLine>
        <TraceIcon>
          <GitBranchIcon />
        </TraceIcon>
        <TraceTitle>Created branch fix/frozen-lockfile</TraceTitle>
      </TraceLine>
    </Trace>
  )
}
```

### Status

Set `status` on `TraceLine` to `running`, `error` or `pending`. Leave it unset for finished work, and say the state in the title too: "Running pnpm build", "Failed to run pnpm lint", "Run pnpm test".

**Example**

```tsx
import {
  CheckCircleIcon,
  CircleIcon,
  CircleNotchIcon,
  WarningCircleIcon,
} from "@phosphor-icons/react"
import { Trace, TraceIcon, TraceLine, TraceTitle } from "@/components/ui/trace"

export default function TraceStatus() {
  return (
    <Trace className="max-w-sm">
      <TraceLine>
        <TraceIcon>
          <CheckCircleIcon />
        </TraceIcon>
        <TraceTitle>Ran pnpm install</TraceTitle>
      </TraceLine>
      <TraceLine status="error">
        <TraceIcon>
          <WarningCircleIcon />
        </TraceIcon>
        <TraceTitle>Failed to run pnpm lint</TraceTitle>
      </TraceLine>
      <TraceLine status="running">
        <TraceIcon>
          <CircleNotchIcon className="animate-spin" />
        </TraceIcon>
        <TraceTitle>Running pnpm build</TraceTitle>
      </TraceLine>
      <TraceLine status="pending">
        <TraceIcon>
          <CircleIcon />
        </TraceIcon>
        <TraceTitle>Run pnpm test</TraceTitle>
      </TraceLine>
    </Trace>
  )
}
```

### Description

Secondary text such as a diff stat, a duration or a count, after the title. It does not truncate.

**Example**

```tsx
import {
  MagnifyingGlassIcon,
  NotePencilIcon,
  TerminalIcon,
} from "@phosphor-icons/react"
import {
  Trace,
  TraceDescription,
  TraceIcon,
  TraceLine,
  TraceTitle,
} from "@/components/ui/trace"

export default function TraceDescriptionDemo() {
  return (
    <Trace className="max-w-sm">
      <TraceLine>
        <TraceIcon>
          <NotePencilIcon />
        </TraceIcon>
        <TraceTitle>
          Edited components/settings/notification-preferences-form.tsx
        </TraceTitle>
        <TraceDescription className="font-mono">+48 −12</TraceDescription>
      </TraceLine>
      <TraceLine>
        <TraceIcon>
          <TerminalIcon />
        </TraceIcon>
        <TraceTitle>Ran pnpm test</TraceTitle>
        <TraceDescription>14s</TraceDescription>
      </TraceLine>
      <TraceLine>
        <TraceIcon>
          <MagnifyingGlassIcon />
        </TraceIcon>
        <TraceTitle>Searched for useChat</TraceTitle>
        <TraceDescription>8 results</TraceDescription>
      </TraceLine>
    </Trace>
  )
}
```

### Collapsible

A row that expands to show the command output.

**Example**

```tsx
import { TerminalIcon } from "@phosphor-icons/react"
import { CodeBlock, CodeBlockContent } from "@/components/ui/code-block"
import {
  TraceCollapsible,
  TraceCollapsibleContent,
  TraceCollapsibleTrigger,
  TraceIcon,
  TraceLine,
  TraceTitle,
} from "@/components/ui/trace"

export default function TraceCollapsibleDemo() {
  return (
    <div className="w-full max-w-sm">
      <TraceCollapsible>
        <TraceCollapsibleTrigger>
          <TraceLine>
            <TraceIcon>
              <TerminalIcon />
            </TraceIcon>
            <TraceTitle>Ran node --version</TraceTitle>
          </TraceLine>
        </TraceCollapsibleTrigger>
        <TraceCollapsibleContent>
          <CodeBlock size="sm">
            <CodeBlockContent wrap className="max-h-64">
              {"$ node --version\nv24.18.0"}
            </CodeBlockContent>
          </CodeBlock>
        </TraceCollapsibleContent>
      </TraceCollapsible>
    </div>
  )
}
```

### Controlled

Opens and closes a row from outside. To open a row while it streams and still let the reader close it, pass the reader's choice and fall back to the running state.

**Example**

```tsx
"use client"

import { TerminalIcon } from "@phosphor-icons/react"
import * as React from "react"

import { Button } from "@/components/ui/button"
import { CodeBlock, CodeBlockContent } from "@/components/ui/code-block"
import {
  TraceCollapsible,
  TraceCollapsibleContent,
  TraceCollapsibleTrigger,
  TraceIcon,
  TraceLine,
  TraceTitle,
} from "@/components/ui/trace"

export default function TraceOpen() {
  const [open, setOpen] = React.useState(false)

  return (
    <div className="flex w-full max-w-sm flex-col items-start gap-4">
      <TraceCollapsible defaultOpen>
        <TraceCollapsibleTrigger>
          <TraceLine>
            <TraceIcon>
              <TerminalIcon />
            </TraceIcon>
            <TraceTitle>Ran node --version</TraceTitle>
          </TraceLine>
        </TraceCollapsibleTrigger>
        <TraceCollapsibleContent>
          <CodeBlock size="sm">
            <CodeBlockContent wrap className="max-h-64">
              {"$ node --version\nv24.18.0"}
            </CodeBlockContent>
          </CodeBlock>
        </TraceCollapsibleContent>
      </TraceCollapsible>
      <TraceCollapsible open={open} onOpenChange={setOpen}>
        <TraceCollapsibleTrigger>
          <TraceLine>
            <TraceIcon>
              <TerminalIcon />
            </TraceIcon>
            <TraceTitle>Ran pnpm --version</TraceTitle>
          </TraceLine>
        </TraceCollapsibleTrigger>
        <TraceCollapsibleContent>
          <CodeBlock size="sm">
            <CodeBlockContent wrap className="max-h-64">
              {"$ pnpm --version\n10.33.4"}
            </CodeBlockContent>
          </CodeBlock>
        </TraceCollapsibleContent>
      </TraceCollapsible>
      <Button variant="outline" size="sm" onClick={() => setOpen(!open)}>
        {open ? "Hide" : "Show"} the pnpm output
      </Button>
    </div>
  )
}
```

### Link

Wraps the `TraceLine` in an anchor. For a new tab, use an arrow icon and add "(opens in a new tab)" to the title with `sr-only`.

**Example**

```tsx
import {
  ArrowUpRightIcon,
  CaretRightIcon,
  FileTextIcon,
  GlobeIcon,
} from "@phosphor-icons/react"
import { Trace, TraceIcon, TraceLine, TraceTitle } from "@/components/ui/trace"

export default function TraceLink() {
  return (
    <Trace className="max-w-sm">
      <a href="#link">
        <TraceLine>
          <TraceIcon>
            <FileTextIcon />
          </TraceIcon>
          <TraceTitle>Read apps/web/lib/docs.ts</TraceTitle>
          <TraceIcon>
            <CaretRightIcon />
          </TraceIcon>
        </TraceLine>
      </a>
      <a
        href="https://docs.astro.build/en/install-and-setup/"
        target="_blank"
        rel="noreferrer"
      >
        <TraceLine>
          <TraceIcon>
            <GlobeIcon />
          </TraceIcon>
          <TraceTitle>
            Install Astro | docs.astro.build
            <span className="sr-only"> (opens in a new tab)</span>
          </TraceTitle>
          <TraceIcon>
            <ArrowUpRightIcon />
          </TraceIcon>
        </TraceLine>
      </a>
    </Trace>
  )
}
```

### Dialog

Wraps the `TraceLine` in a `DialogTrigger`. The dialog owns its open state, Escape and focus return.

**Example**

```tsx
import {
  ArrowUpRightIcon,
  CaretRightIcon,
  GlobeIcon,
} from "@phosphor-icons/react"
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "@/components/ui/dialog"
import { Trace, TraceIcon, TraceLine, TraceTitle } from "@/components/ui/trace"

const results = [
  {
    title: "Caching build outputs",
    url: "https://nextjs.org/docs/app/guides/caching",
  },
  {
    title: "Image optimization reference",
    url: "https://nextjs.org/docs/app/api-reference/components/image",
  },
  {
    title: "Deploy hooks and cache keys",
    url: "https://vercel.com/docs/deploy-hooks",
  },
]

export default function TraceDialog() {
  return (
    <Trace className="max-w-sm">
      <Dialog>
        <DialogTrigger>
          <TraceLine>
            <TraceIcon>
              <GlobeIcon />
            </TraceIcon>
            <TraceTitle>Searched the web for cache keys</TraceTitle>
            <TraceIcon>
              <CaretRightIcon />
            </TraceIcon>
          </TraceLine>
        </DialogTrigger>
        <DialogContent>
          <DialogHeader>
            <DialogTitle>Searched the web for cache keys</DialogTitle>
            <DialogDescription>
              Three results were used in the response.
            </DialogDescription>
          </DialogHeader>
          <Trace>
            {results.map((result) => (
              <a
                key={result.url}
                href={result.url}
                target="_blank"
                rel="noreferrer"
              >
                <TraceLine>
                  <TraceIcon>
                    <GlobeIcon />
                  </TraceIcon>
                  <TraceTitle>
                    {result.title}
                    <span className="sr-only"> (opens in a new tab)</span>
                  </TraceTitle>
                  <TraceIcon>
                    <ArrowUpRightIcon />
                  </TraceIcon>
                </TraceLine>
              </a>
            ))}
          </Trace>
        </DialogContent>
      </Dialog>
    </Trace>
  )
}
```

### Sheet

The same shape as the dialog, in a sheet.

**Example**

```tsx
import { CaretRightIcon, FolderOpenIcon } from "@phosphor-icons/react"
import {
  Sheet,
  SheetContent,
  SheetDescription,
  SheetHeader,
  SheetTitle,
  SheetTrigger,
} from "@/components/ui/sheet"
import { Trace, TraceIcon, TraceLine, TraceTitle } from "@/components/ui/trace"

export default function TraceSheet() {
  return (
    <Trace className="max-w-sm">
      <Sheet>
        <SheetTrigger>
          <TraceLine>
            <TraceIcon>
              <FolderOpenIcon />
            </TraceIcon>
            <TraceTitle>Explored 4 files</TraceTitle>
            <TraceIcon>
              <CaretRightIcon />
            </TraceIcon>
          </TraceLine>
        </SheetTrigger>
        <SheetContent>
          <SheetHeader>
            <SheetTitle>Explored 4 files</SheetTitle>
            <SheetDescription>
              Files read while preparing the response.
            </SheetDescription>
          </SheetHeader>
          <ul className="flex flex-col gap-2 px-6 font-mono text-xs">
            <li>apps/web/lib/docs.ts</li>
            <li>apps/web/lib/components.ts</li>
            <li>apps/web/next.config.mjs</li>
            <li>apps/web/package.json</li>
          </ul>
        </SheetContent>
      </Sheet>
    </Trace>
  )
}
```

### Group

Rows next to each other in one `Trace`, which also styles any trigger or link that wraps a `TraceLine`.

**Example**

```tsx
import {
  FileTextIcon,
  MagnifyingGlassIcon,
  TerminalIcon,
} from "@phosphor-icons/react"
import { Trace, TraceIcon, TraceLine, TraceTitle } from "@/components/ui/trace"

export default function TraceGroupDemo() {
  return (
    <Trace className="max-w-sm">
      <TraceLine>
        <TraceIcon>
          <MagnifyingGlassIcon />
        </TraceIcon>
        <TraceTitle>Searched for minimumReleaseAge</TraceTitle>
      </TraceLine>
      <TraceLine>
        <TraceIcon>
          <FileTextIcon />
        </TraceIcon>
        <TraceTitle>Read pnpm-workspace.yaml</TraceTitle>
      </TraceLine>
      <TraceLine>
        <TraceIcon>
          <TerminalIcon />
        </TraceIcon>
        <TraceTitle>Ran pnpm install</TraceTitle>
      </TraceLine>
    </Trace>
  )
}
```

### Sources

A collapsible row whose detail is a `Trace` of link rows.

**Example**

```tsx
import { ArrowUpRightIcon, GlobeIcon } from "@phosphor-icons/react"
import {
  Trace,
  TraceCollapsible,
  TraceCollapsibleContent,
  TraceCollapsibleTrigger,
  TraceIcon,
  TraceLine,
  TraceTitle,
} from "@/components/ui/trace"

const sources = [
  {
    title: "Install Astro | docs.astro.build",
    href: "https://docs.astro.build/en/install-and-setup/",
  },
  {
    title: "create-astro | npmjs.com",
    href: "https://www.npmjs.com/package/create-astro",
  },
  {
    title: "Settings: minimumReleaseAge | pnpm.io",
    href: "https://pnpm.io/settings#minimumreleaseage",
  },
]

export default function TraceSources() {
  return (
    <div className="w-full max-w-sm">
      <TraceCollapsible defaultOpen>
        <TraceCollapsibleTrigger>
          <TraceLine>
            <TraceIcon>
              <GlobeIcon />
            </TraceIcon>
            <TraceTitle>Used {sources.length} sources</TraceTitle>
          </TraceLine>
        </TraceCollapsibleTrigger>
        <TraceCollapsibleContent>
          <Trace>
            {sources.map((source) => (
              <a
                key={source.href}
                href={source.href}
                target="_blank"
                rel="noreferrer"
              >
                <TraceLine>
                  <TraceIcon>
                    <GlobeIcon />
                  </TraceIcon>
                  <TraceTitle>
                    {source.title}
                    <span className="sr-only"> (opens in a new tab)</span>
                  </TraceTitle>
                  <TraceIcon>
                    <ArrowUpRightIcon />
                  </TraceIcon>
                </TraceLine>
              </a>
            ))}
          </Trace>
        </TraceCollapsibleContent>
      </TraceCollapsible>
    </div>
  )
}
```

### Steps

A summary row with step rows inside. The parent's status and title come from its children.

**Example**

```tsx
import {
  CaretRightIcon,
  FileTextIcon,
  MagnifyingGlassIcon,
} from "@phosphor-icons/react"
import { Badge } from "@/components/ui/badge"
import { CodeBlock, CodeBlockContent } from "@/components/ui/code-block"
import {
  Trace,
  TraceCollapsible,
  TraceCollapsibleContent,
  TraceCollapsibleTrigger,
  TraceIcon,
  TraceLine,
  TraceTitle,
} from "@/components/ui/trace"

export default function TraceSteps() {
  return (
    <div className="w-full max-w-sm">
      <TraceCollapsible defaultOpen>
        <TraceCollapsibleTrigger>
          <TraceLine status="running">
            <TraceTitle>Looking into the slow deploy</TraceTitle>
          </TraceLine>
        </TraceCollapsibleTrigger>
        <TraceCollapsibleContent keepMounted>
          <Trace>
            <TraceCollapsible>
              <TraceCollapsibleTrigger>
                <TraceLine>
                  <TraceIcon>
                    <MagnifyingGlassIcon />
                  </TraceIcon>
                  <TraceTitle>Searched for the checkout build log</TraceTitle>
                </TraceLine>
              </TraceCollapsibleTrigger>
              <TraceCollapsibleContent className="flex flex-wrap gap-1.5">
                <Badge variant="secondary">vercel.com</Badge>
                <Badge variant="secondary">github.com</Badge>
              </TraceCollapsibleContent>
            </TraceCollapsible>
            <a href="#steps">
              <TraceLine>
                <TraceIcon>
                  <FileTextIcon />
                </TraceIcon>
                <TraceTitle>Read the last twelve deploys</TraceTitle>
                <TraceIcon>
                  <CaretRightIcon />
                </TraceIcon>
              </TraceLine>
            </a>
            <TraceCollapsible>
              <TraceCollapsibleTrigger>
                <TraceLine status="running">
                  <TraceIcon>
                    <MagnifyingGlassIcon />
                  </TraceIcon>
                  <TraceTitle>Searching for the cache key</TraceTitle>
                </TraceLine>
              </TraceCollapsibleTrigger>
              <TraceCollapsibleContent>
                <CodeBlock size="sm">
                  <CodeBlockContent wrap className="max-h-64">
                    cache key image optimization
                  </CodeBlockContent>
                </CodeBlock>
              </TraceCollapsibleContent>
            </TraceCollapsible>
          </Trace>
        </TraceCollapsibleContent>
      </TraceCollapsible>
    </div>
  )
}
```

### Commit

A row for each changed file, with the line counts in the description. Each file opens its diff in a sheet.

**Example**

```tsx
import {
  CaretRightIcon,
  FilePlusIcon,
  GitCommitIcon,
  NotePencilIcon,
} from "@phosphor-icons/react"
import {
  CodeBlock,
  CodeBlockContent,
  CodeBlockLine,
} from "@/components/ui/code-block"
import {
  Sheet,
  SheetContent,
  SheetDescription,
  SheetHeader,
  SheetTitle,
  SheetTrigger,
} from "@/components/ui/sheet"
import {
  Trace,
  TraceCollapsible,
  TraceCollapsibleContent,
  TraceCollapsibleTrigger,
  TraceDescription,
  TraceIcon,
  TraceLine,
  TraceTitle,
} from "@/components/ui/trace"

const files = [
  {
    path: "app/login/page.tsx",
    added: false,
    additions: 2,
    deletions: 2,
    diff: `@@ -14,7 +14,7 @@ export default async function LoginPage({
   searchParams,
 }) {
   const { next } = await searchParams
-  const redirectTo =
-    next && next.startsWith("/") ? next : "/"
+  const redirectTo = safeRedirect(next)
+
   return <LoginForm redirectTo={redirectTo} />
 }`,
  },
  {
    path: "lib/auth/redirect.ts",
    added: true,
    additions: 7,
    deletions: 0,
    diff: `@@ -0,0 +1,7 @@
+export function safeRedirect(next: string | undefined) {
+  if (!next || !next.startsWith("/") || next.startsWith("//")) {
+    return "/"
+  }
+
+  return next
+}`,
  },
]

function lineVariant(line: string) {
  if (line.startsWith("+")) return "added"
  if (line.startsWith("-")) return "removed"
  return "default"
}

function DiffLines({ diff }: { diff: string }) {
  return diff.split("\n").map((line, index) => {
    if (line.startsWith("@@")) {
      return (
        <CodeBlockLine key={index} className="text-muted-foreground">
          {line}
        </CodeBlockLine>
      )
    }

    return (
      <CodeBlockLine key={index} variant={lineVariant(line)}>
        {line.slice(1)}
      </CodeBlockLine>
    )
  })
}

export default function TraceCommit() {
  return (
    <Trace className="max-w-sm">
      <TraceCollapsible defaultOpen>
        <TraceCollapsibleTrigger>
          <TraceLine>
            <TraceIcon>
              <GitCommitIcon />
            </TraceIcon>
            <TraceTitle>Committed a1b2c3: Fix the login redirect</TraceTitle>
          </TraceLine>
        </TraceCollapsibleTrigger>
        <TraceCollapsibleContent>
          <Trace>
            {files.map((file) => (
              <Sheet key={file.path}>
                <SheetTrigger>
                  <TraceLine>
                    <TraceIcon>
                      {file.added ? (
                        <FilePlusIcon />
                      ) : (
                        <NotePencilIcon />
                      )}
                    </TraceIcon>
                    <TraceTitle>
                      {file.added ? "Added" : "Edited"} {file.path}
                    </TraceTitle>
                    <TraceDescription className="font-mono">
                      +{file.additions} −{file.deletions}
                    </TraceDescription>
                    <TraceIcon>
                      <CaretRightIcon />
                    </TraceIcon>
                  </TraceLine>
                </SheetTrigger>
                <SheetContent>
                  <SheetHeader>
                    <SheetTitle className="font-mono">{file.path}</SheetTitle>
                    <SheetDescription>
                      {file.additions} additions, {file.deletions} deletions
                    </SheetDescription>
                  </SheetHeader>
                  <CodeBlock className="mx-6 mb-6 w-auto">
                    <CodeBlockContent>
                      <DiffLines diff={file.diff} />
                    </CodeBlockContent>
                  </CodeBlock>
                </SheetContent>
              </Sheet>
            ))}
          </Trace>
        </TraceCollapsibleContent>
      </TraceCollapsible>
    </Trace>
  )
}
```

### Whole turn

Three levels: the turn, a summary and the rows. Text between steps sits between two `Trace` lists.

**Example**

```tsx
import {
  BrainIcon,
  CaretRightIcon,
  FileTextIcon,
  TerminalIcon,
} from "@phosphor-icons/react"
import { CodeBlock, CodeBlockContent } from "@/components/ui/code-block"
import {
  Trace,
  TraceCollapsible,
  TraceCollapsibleContent,
  TraceCollapsibleTrigger,
  TraceDescription,
  TraceIcon,
  TraceLine,
  TraceTitle,
} from "@/components/ui/trace"

export default function TraceTurn() {
  return (
    <div className="w-full max-w-sm">
      <TraceCollapsible defaultOpen>
        <TraceCollapsibleTrigger>
          <TraceLine status="error">
            <TraceTitle>Worked for 2m 4s, 1 command failed</TraceTitle>
          </TraceLine>
        </TraceCollapsibleTrigger>
        <TraceCollapsibleContent keepMounted className="flex flex-col gap-2">
          <Trace>
            <TraceCollapsible>
              <TraceCollapsibleTrigger>
                <TraceLine>
                  <TraceIcon>
                    <BrainIcon />
                  </TraceIcon>
                  <TraceTitle>Thought</TraceTitle>
                  <TraceDescription>12s</TraceDescription>
                </TraceLine>
              </TraceCollapsibleTrigger>
              <TraceCollapsibleContent className="whitespace-pre-wrap">
                I should check which package managers are installed before I
                scaffold anything.
              </TraceCollapsibleContent>
            </TraceCollapsible>
            <TraceCollapsible defaultOpen>
              <TraceCollapsibleTrigger>
                <TraceLine status="error">
                  <TraceIcon>
                    <TerminalIcon />
                  </TraceIcon>
                  <TraceTitle>Ran 2 commands, 1 failed</TraceTitle>
                </TraceLine>
              </TraceCollapsibleTrigger>
              <TraceCollapsibleContent keepMounted>
                <Trace>
                  <TraceLine>
                    <TraceIcon>
                      <TerminalIcon />
                    </TraceIcon>
                    <TraceTitle>Ran node --version</TraceTitle>
                  </TraceLine>
                  <TraceCollapsible>
                    <TraceCollapsibleTrigger>
                      <TraceLine status="error">
                        <TraceIcon>
                          <TerminalIcon />
                        </TraceIcon>
                        <TraceTitle>Failed to run pnpm --version</TraceTitle>
                      </TraceLine>
                    </TraceCollapsibleTrigger>
                    <TraceCollapsibleContent>
                      <CodeBlock size="sm" variant="destructive">
                        <CodeBlockContent wrap className="max-h-64">
                          command not found: pnpm
                        </CodeBlockContent>
                      </CodeBlock>
                    </TraceCollapsibleContent>
                  </TraceCollapsible>
                </Trace>
              </TraceCollapsibleContent>
            </TraceCollapsible>
          </Trace>
          <p>Node is installed and pnpm is not. Reading the docs config.</p>
          <Trace>
            <a href="#turn">
              <TraceLine>
                <TraceIcon>
                  <FileTextIcon />
                </TraceIcon>
                <TraceTitle>Read apps/web/lib/docs.ts</TraceTitle>
                <TraceIcon>
                  <CaretRightIcon />
                </TraceIcon>
              </TraceLine>
            </a>
          </Trace>
        </TraceCollapsibleContent>
      </TraceCollapsible>
    </div>
  )
}
```

### Reasoning

A thinking row that expands into the reasoning text.

**Example**

```tsx
import { BrainIcon } from "@phosphor-icons/react"
import { Bubble, BubbleContent } from "@/components/ui/bubble"
import { Message, MessageContent } from "@/components/ui/message"
import {
  Trace,
  TraceCollapsible,
  TraceCollapsibleContent,
  TraceCollapsibleTrigger,
  TraceIcon,
  TraceLine,
  TraceTitle,
} from "@/components/ui/trace"

export default function TraceThinking() {
  return (
    <div className="flex w-full max-w-sm flex-col gap-4 py-6 text-sm">
      <Message align="end">
        <MessageContent>
          <Bubble align="end" variant="muted">
            <BubbleContent>Why is the checkout deploy so slow?</BubbleContent>
          </Bubble>
        </MessageContent>
      </Message>
      <Message>
        <MessageContent className="gap-4">
          <Trace>
            <TraceCollapsible defaultOpen>
              <TraceCollapsibleTrigger>
                <TraceLine>
                  <TraceIcon>
                    <BrainIcon />
                  </TraceIcon>
                  <TraceTitle>Thought</TraceTitle>
                </TraceLine>
              </TraceCollapsibleTrigger>
              <TraceCollapsibleContent className="whitespace-pre-wrap">
                The build log shows install and compile finish in under a
                minute. Image optimization takes almost four, and it reruns on
                every deploy because the cache key includes the commit hash.
              </TraceCollapsibleContent>
            </TraceCollapsible>
          </Trace>
          <p>
            Image optimization is the bottleneck. Key its cache on the lockfile
            and the image directory instead of the commit hash.
          </p>
        </MessageContent>
      </Message>
    </div>
  )
}
```

### Tool calls

Tool calls with their input and output, including a failed call.

**Example**

```tsx
import { TerminalIcon } from "@phosphor-icons/react"
import { Bubble, BubbleContent } from "@/components/ui/bubble"
import { CodeBlock, CodeBlockContent } from "@/components/ui/code-block"
import { Message, MessageContent } from "@/components/ui/message"
import {
  Trace,
  TraceCollapsible,
  TraceCollapsibleContent,
  TraceCollapsibleTrigger,
  TraceIcon,
  TraceLine,
  TraceTitle,
} from "@/components/ui/trace"

const calls = [
  { command: "node --version", output: "v24.18.0" },
  { command: "npm --version", output: "11.6.2" },
  { command: "pnpm --version", error: "command not found: pnpm" },
]

export default function TraceToolCalls() {
  return (
    <div className="flex w-full max-w-sm flex-col gap-4 py-6 text-sm">
      <Message align="end">
        <MessageContent>
          <Bubble align="end" variant="muted">
            <BubbleContent>Which package managers are installed?</BubbleContent>
          </Bubble>
        </MessageContent>
      </Message>
      <Message>
        <MessageContent className="gap-4">
          <Trace>
            {calls.map((call) => (
              <TraceCollapsible key={call.command}>
                <TraceCollapsibleTrigger>
                  <TraceLine status={call.error ? "error" : undefined}>
                    <TraceIcon>
                      <TerminalIcon />
                    </TraceIcon>
                    <TraceTitle>
                      {call.error ? "Failed to run" : "Ran"} {call.command}
                    </TraceTitle>
                  </TraceLine>
                </TraceCollapsibleTrigger>
                <TraceCollapsibleContent>
                  <CodeBlock size="sm">
                    <CodeBlockContent wrap className="max-h-64">
                      {call.error ?? call.output}
                    </CodeBlockContent>
                  </CodeBlock>
                </TraceCollapsibleContent>
              </TraceCollapsible>
            ))}
          </Trace>
          <p>Node 24 and npm 11 are installed. pnpm is not.</p>
        </MessageContent>
      </Message>
    </div>
  )
}
```

## API Reference

- `Trace`
- `TraceLine`
  - `TraceIcon`
  - `TraceTitle`
  - `TraceDescription`
- `TraceCollapsible`
  - `TraceCollapsibleTrigger`
    - `TraceLine`
  - `TraceCollapsibleContent`

### Trace

A list of rows. It also styles any trigger or link that wraps a `TraceLine`.

Also accepts the props of `React.ComponentProps<"div">`.

### TraceLine

One row: an icon and a title. It is a `span`, so it is not a button or a tab stop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `status` | `"running" \| "error" \| "pending"` | — | The state of the work the row reports. `running` shimmers the title, `error` turns it destructive and `pending` mutes it. |

Also accepts the props of `React.ComponentProps<"span">`.

### TraceIcon

A decorative icon slot, hidden from assistive tech. Before the title it names the action; after it, it is the indicator.

Also accepts the props of `React.ComponentProps<"span">`.

### TraceTitle

One sentence on one line. It truncates, and shimmers while the row is running.

Also accepts the props of `React.ComponentProps<"span">`.

### TraceDescription

Secondary text after the title, such as a count or a duration. It does not truncate.

Also accepts the props of `React.ComponentProps<"span">`.

### TraceCollapsible

A row that expands.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `open` | `boolean` | — | The controlled open state. Pass it with `onOpenChange`. |
| `defaultOpen` | `boolean` | `false` | Whether the row starts open. |
| `onOpenChange` | `(open: boolean) => void` | — | Called when the open state changes. |

Also accepts the props of [Collapsible](https://suiss.app/ui/docs/components/collapsible).

### TraceCollapsibleTrigger

The button that opens and closes the row. Put a `TraceLine` inside it; it adds a chevron after the line.

Also accepts the props of `CollapsibleTrigger`.

### TraceCollapsibleContent

The detail of a row that expands, indented under the title.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `keepMounted` | `boolean` | `false` | Keeps the detail mounted while closed, so nested rows keep their open state. |

Also accepts the props of `CollapsibleContent`.

## Accessibility

- Trace adds no roles of its own. Each row gets its semantics from the element that wraps it.
- A `TraceLine` on its own is read as text and is not a tab stop.
- Inside `TraceCollapsibleTrigger` the row is a button with `aria-expanded`. Inside a link or a dialog or sheet trigger, it takes that element's keyboard and focus behavior.
- `TraceIcon` is hidden from assistive tech, so `TraceTitle` is the accessible name. Write it as a sentence that says what happened, such as "Edited trace.tsx", not just "trace.tsx".
- Status is shown with color and motion, so put it in the title too.
- The running shimmer stops when the reader prefers reduced motion.

## Notes

- Styling hooks: `data-slot="trace"`, `"trace-line"`, `"trace-icon"`, `"trace-title"`, `"trace-description"`, `"trace-collapsible"`, `"trace-collapsible-trigger"` and `"trace-collapsible-content"`, plus `data-status` on the row.
- `TraceCollapsibleContent` mutes its text. Give a block with a muted background inside it `text-foreground` to keep its contrast; [Code Block](https://suiss.app/ui/docs/components/code-block) sets its own text color.

**React Native, SwiftUI, Jetpack Compose**

```tsx
import { CheckCircleIcon, WarningCircleIcon } from "phosphor-react-native"
import { View } from "react-native"
import { Bubble, BubbleContent } from "@/components/ui/bubble"
import { CodeBlock, CodeBlockContent } from "@/components/ui/code-block"
import { Icon } from "@/components/ui/icon"
import { Message, MessageContent } from "@/components/ui/message"
import { Text } from "@/components/ui/text"
import { Trace, TraceCollapsible, TraceCollapsibleContent, TraceCollapsibleTrigger, TraceIcon, TraceLine, TraceTitle } from "@/components/ui/trace"

function Output({ children, error }: { children: string; error?: boolean }) {
  return (
    <CodeBlock size="sm" variant={error ? "destructive" : "default"}>
      <CodeBlockContent wrap>{children}</CodeBlockContent>
    </CodeBlock>
  )
}

export function TraceExample() {
  return (
    <View className="w-full gap-4">
      <Message align="end">
        <MessageContent>
          <Bubble variant="muted">
            <BubbleContent>Why does pnpm install fail in CI?</BubbleContent>
          </Bubble>
        </MessageContent>
      </Message>
      <Trace>
        <TraceCollapsible>
          <TraceCollapsibleTrigger>
            <TraceLine>
              <TraceIcon>
                <Icon as={CheckCircleIcon} />
              </TraceIcon>
              <TraceTitle>Thought for 5s</TraceTitle>
            </TraceLine>
          </TraceCollapsibleTrigger>
          <TraceCollapsibleContent>
            <Text>
              CI installs with a frozen lockfile, so the install stops when
              package.json and pnpm-lock.yaml disagree.
            </Text>
          </TraceCollapsibleContent>
        </TraceCollapsible>
        <TraceLine>
          <TraceIcon>
            <Icon as={CheckCircleIcon} />
          </TraceIcon>
          <TraceTitle>Ran pnpm --version</TraceTitle>
        </TraceLine>
        <TraceCollapsible defaultOpen>
          <TraceCollapsibleTrigger>
            <TraceLine status="error">
              <TraceIcon>
                <Icon as={WarningCircleIcon} />
              </TraceIcon>
              <TraceTitle>Failed to run pnpm install</TraceTitle>
            </TraceLine>
          </TraceCollapsibleTrigger>
          <TraceCollapsibleContent>
            <Output error>
              ERR_PNPM_OUTDATED_LOCKFILE Cannot install with
              &quot;frozen-lockfile&quot; because pnpm-lock.yaml is not up to
              date with package.json
            </Output>
          </TraceCollapsibleContent>
        </TraceCollapsible>
      </Trace>
      <Text>
        The last commit bumped a version in package.json without updating
        pnpm-lock.yaml. Run pnpm install locally and commit the lockfile.
      </Text>
    </View>
  )
}
```

```swift
import EsuissUI
import PhosphorSwift
import SwiftUI

struct TraceExample: View {
  var body: some View {
    VStack(alignment: .leading, spacing: 16) {
      Message(align: .end) {
        MessageContent {
          Bubble(variant: .muted, align: .end) { BubbleContent("Why does pnpm install fail in CI?") }
        }
      }
      Trace {
        TraceCollapsible {
          TraceCollapsibleTrigger {
            TraceLine {
              TraceIcon { Ph.checkCircle.regular }
              TraceTitle("Thought for 5s")
            }
          }
          TraceCollapsibleContent {
            Text("CI installs with a frozen lockfile, so the install stops when package.json and pnpm-lock.yaml disagree.")
          }
        }
        TraceLine {
          TraceIcon { Ph.checkCircle.regular }
          TraceTitle("Ran pnpm --version")
        }
        TraceCollapsible(initiallyExpanded: true) {
          TraceCollapsibleTrigger {
            TraceLine(status: .error) {
              TraceIcon { Ph.warningCircle.regular }
              TraceTitle("Failed to run pnpm install")
            }
          }
          TraceCollapsibleContent {
            TraceOutput(text: "ERR_PNPM_OUTDATED_LOCKFILE Cannot install with \"frozen-lockfile\" because pnpm-lock.yaml is not up to date with package.json", error: true)
          }
        }
      }
      Text("The last commit bumped a version in package.json without updating pnpm-lock.yaml. Run pnpm install locally and commit the lockfile.")
        .textStyle(.base)
    }
  }
}

struct TraceOutput: View {
  let text: String
  var error = false

  var body: some View {
    CodeBlock(error ? .destructive : .default, size: .sm) {
      CodeBlockContent(text, wrap: true)
    }
  }
}
```

```kotlin
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.regular.CheckCircle
import com.adamglin.phosphoricons.regular.WarningCircle
import com.esuiss.ui.components.Bubble
import com.esuiss.ui.components.BubbleContent
import com.esuiss.ui.components.BubbleVariant
import com.esuiss.ui.components.CodeBlock
import com.esuiss.ui.components.CodeBlockContent
import com.esuiss.ui.components.CodeBlockSize
import com.esuiss.ui.components.CodeBlockVariant
import com.esuiss.ui.components.Icon
import com.esuiss.ui.components.Message
import com.esuiss.ui.components.MessageAlign
import com.esuiss.ui.components.MessageContent
import com.esuiss.ui.components.Text
import com.esuiss.ui.components.Trace
import com.esuiss.ui.components.TraceCollapsible
import com.esuiss.ui.components.TraceCollapsibleContent
import com.esuiss.ui.components.TraceCollapsibleTrigger
import com.esuiss.ui.components.TraceDescription
import com.esuiss.ui.components.TraceIcon
import com.esuiss.ui.components.TraceLine
import com.esuiss.ui.components.TraceStatus
import com.esuiss.ui.components.TraceTitle

@Composable
fun TraceOutput(text: String, error: Boolean = false) {
    CodeBlock(
        variant = if (error) CodeBlockVariant.Destructive else CodeBlockVariant.Default,
        size = CodeBlockSize.Sm,
    ) {
        CodeBlockContent(text, wrap = true)
    }
}

@Composable
fun TraceRow(icon: ImageVector, title: String, status: TraceStatus? = null, description: String? = null) {
    TraceLine(status = status) {
        TraceIcon { Icon(icon, contentDescription = null) }
        TraceTitle(title)
        if (description != null) TraceDescription(description)
    }
}

@Composable
fun TraceDefault() {
    Column(Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(16.dp)) {
        Message(Modifier.fillMaxWidth(), align = MessageAlign.End) {
            MessageContent {
                Bubble(variant = BubbleVariant.Muted) {
                    BubbleContent("Why does pnpm install fail in CI?")
                }
            }
        }
        Trace {
            TraceCollapsible {
                TraceCollapsibleTrigger {
                    TraceLine {
                        TraceIcon { Icon(PhosphorIcons.Regular.CheckCircle, contentDescription = null) }
                        TraceTitle("Thought for 5s")
                    }
                }
                TraceCollapsibleContent {
                    Text("CI installs with a frozen lockfile, so the install stops when package.json and pnpm-lock.yaml disagree.")
                }
            }
            TraceRow(PhosphorIcons.Regular.CheckCircle, "Ran pnpm --version")
            TraceCollapsible(initiallyExpanded = true) {
                TraceCollapsibleTrigger {
                    TraceLine(status = TraceStatus.Error) {
                        TraceIcon { Icon(PhosphorIcons.Regular.WarningCircle, contentDescription = null) }
                        TraceTitle("Failed to run pnpm install")
                    }
                }
                TraceCollapsibleContent {
                    TraceOutput(
                        "ERR_PNPM_OUTDATED_LOCKFILE Cannot install with \"frozen-lockfile\" because pnpm-lock.yaml is not up to date with package.json",
                        error = true,
                    )
                }
            }
        }
        Text("The last commit bumped a version in package.json without updating pnpm-lock.yaml. Run pnpm install locally and commit the lockfile.")
    }
}
```

A `Trace` is a column of `TraceLine` rows. Each row has a `TraceIcon`, a `TraceTitle` and an optional `TraceDescription` and status. Wrap a row in `TraceCollapsible` to show more detail when it expands, or in a `Pressable` to open a link or a sheet.

Use Trace for the steps an agent takes, such as calling a tool, running a command or editing a file. Use [Message](https://suiss.app/ui/docs/components/message) for its reply and [Marker](https://suiss.app/ui/docs/components/marker) for notes between turns.

## Installation

**React Native**

```bash title="Terminal"
npx shadcn@latest add @esuiss-rn/trace
```

See [Installation](https://suiss.app/ui/docs/installation) for the `components.json` setup. Or use the [suiss/ui CLI](https://suiss.app/ui/docs/cli):

```bash title="Terminal"
npx @esuiss/ui@latest add trace
```

**SwiftUI**

```bash title="Terminal"
npx @esuiss/ui@latest add trace --platform swift
```

**Jetpack Compose**

```bash title="Terminal"
npx @esuiss/ui@latest add trace --platform kotlin
```

The CLI also adds what it builds on: [Icon](https://suiss.app/ui/docs/components/icon), [Text](https://suiss.app/ui/docs/components/text).

### Manual

**React Native**

**Uniwind**

1. Install the dependencies:

```bash title="Terminal"
npx expo install cn phosphor-react-native react-native-reanimated react-native-svg react-native-worklets
```

2. Add the pieces it builds on: [Icon](https://suiss.app/ui/docs/components/icon), [Text](https://suiss.app/ui/docs/components/text), and the primitives `components/primitives/collapsible.tsx`, `components/primitives/use-render.tsx`.

3. Copy the source into `components/ui/trace.tsx`:

```tsx title="uniwind/components/ui/trace.tsx"
import { cn } from "cn"
import { CaretRightIcon } from "phosphor-react-native"
import * as React from "react"
import { View, type ViewProps } from "react-native"
import Animated, {
  useAnimatedStyle,
  useDerivedValue,
  useReducedMotion,
  useSharedValue,
  withRepeat,
  withTiming,
} from "react-native-reanimated"
import * as CollapsiblePrimitive from "@/components/primitives/collapsible"
import { resolveClassName } from "@/components/primitives/use-render"
import { Icon, IconClassContext } from "@/components/ui/icon"
import { Text, TextClassContext } from "@/components/ui/text"

type TraceStatus = "running" | "error" | "pending"

const TraceStatusContext = React.createContext<TraceStatus | undefined>(
  undefined
)

function Trace({ className, ...props }: ViewProps) {
  return (
    <View
      className={cn("w-full min-w-0 flex-col gap-2", className)}
      {...props}
    />
  )
}

function TraceLine({
  className,
  status,
  ...props
}: ViewProps & { status?: TraceStatus }) {
  const tone = status === "error" ? "text-destructive" : "text-muted-foreground"
  return (
    <TraceStatusContext.Provider value={status}>
      <TextClassContext.Provider value={cn("text-sm", tone)}>
        <IconClassContext.Provider value={cn("size-4", tone)}>
          <View
            className={cn(
              "max-w-full min-w-0 shrink flex-row items-center gap-2",
              status === "pending" && "opacity-60",
              className
            )}
            {...props}
          />
        </IconClassContext.Provider>
      </TextClassContext.Provider>
    </TraceStatusContext.Provider>
  )
}

function TraceIcon({ className, ...props }: ViewProps) {
  return (
    <View
      accessible={false}
      importantForAccessibility="no-hide-descendants"
      className={cn("size-4 shrink-0 items-center justify-center", className)}
      {...props}
    />
  )
}

function TraceTitle({
  className,
  ...props
}: React.ComponentProps<typeof Text>) {
  const status = React.useContext(TraceStatusContext)
  const reduceMotion = useReducedMotion()
  const running = status === "running" && !reduceMotion
  const opacity = useSharedValue(1)

  React.useEffect(() => {
    opacity.value = running
      ? withRepeat(withTiming(0.45, { duration: 900 }), -1, true)
      : withTiming(1, { duration: 150 })
  }, [opacity, running])

  const animatedStyle = useAnimatedStyle(() => ({ opacity: opacity.value }))

  return (
    <Animated.View className="min-w-0 shrink" style={animatedStyle}>
      <Text numberOfLines={1} className={cn("min-w-0", className)} {...props} />
    </Animated.View>
  )
}

function TraceDescription({
  className,
  style,
  ...props
}: React.ComponentProps<typeof Text>) {
  return (
    <Text
      className={cn("shrink-0 text-muted-foreground/70", className)}
      style={[{ fontVariant: ["tabular-nums"] }, style]}
      {...props}
    />
  )
}

function TraceCollapsible({
  className,
  ...props
}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
  return (
    <CollapsiblePrimitive.Root
      className={(state) =>
        cn("w-full min-w-0 flex-col", resolveClassName(className, state))
      }
      {...props}
    />
  )
}

function TraceCollapsibleTrigger({
  className,
  children,
  ...props
}: React.ComponentProps<typeof CollapsiblePrimitive.Trigger>) {
  const { open } = CollapsiblePrimitive.useRootContext()
  const progress = useDerivedValue(() =>
    withTiming(open ? 1 : 0, { duration: 200 })
  )
  const caretStyle = useAnimatedStyle(() => ({
    transform: [{ rotate: `${progress.value * 90}deg` }],
  }))

  return (
    <CollapsiblePrimitive.Trigger
      className={(state) =>
        cn(
          "max-w-full min-w-0 self-start flex-row items-center gap-1 rounded-lg active:opacity-70",
          resolveClassName(className, state)
        )
      }
      {...props}
    >
      {children}
      <Animated.View
        style={caretStyle}
        accessible={false}
        importantForAccessibility="no-hide-descendants"
      >
        <Icon as={CaretRightIcon} className="size-4 text-muted-foreground" />
      </Animated.View>
    </CollapsiblePrimitive.Trigger>
  )
}

function TraceCollapsibleContent({
  className,
  children,
  ...props
}: React.ComponentProps<typeof CollapsiblePrimitive.Panel>) {
  return (
    <CollapsiblePrimitive.Panel
      className={(state) =>
        cn(
          "mt-2 mb-1 ms-[7px] min-w-0 border-s border-border ps-4 pe-1",
          resolveClassName(className, state)
        )
      }
      {...props}
    >
      <TextClassContext.Provider value="text-sm leading-relaxed text-muted-foreground">
        {children}
      </TextClassContext.Provider>
    </CollapsiblePrimitive.Panel>
  )
}

export {
  Trace,
  TraceCollapsible,
  TraceCollapsibleContent,
  TraceCollapsibleTrigger,
  TraceDescription,
  TraceIcon,
  TraceLine,
  TraceTitle,
}
```

4. Update the import paths to match your project.

**NativeWind**

1. Install the dependencies:

```bash title="Terminal"
npx expo install cn phosphor-react-native react-native-reanimated react-native-svg react-native-worklets
```

2. Add the pieces it builds on: [Icon](https://suiss.app/ui/docs/components/icon), [Text](https://suiss.app/ui/docs/components/text), and the primitives `components/primitives/collapsible.tsx`, `components/primitives/use-render.tsx`.

3. Copy the source into `components/ui/trace.tsx`:

```tsx title="nativewind/components/ui/trace.tsx"
import { cn } from "cn"
import { CaretRightIcon } from "phosphor-react-native"
import * as React from "react"
import { View, type ViewProps } from "react-native"
import Animated, {
  useAnimatedStyle,
  useDerivedValue,
  useReducedMotion,
  useSharedValue,
  withRepeat,
  withTiming,
} from "react-native-reanimated"
import * as CollapsiblePrimitive from "@/components/primitives/collapsible"
import { resolveClassName } from "@/components/primitives/use-render"
import { Icon, IconClassContext } from "@/components/ui/icon"
import { Text, TextClassContext } from "@/components/ui/text"

type TraceStatus = "running" | "error" | "pending"

const TraceStatusContext = React.createContext<TraceStatus | undefined>(
  undefined
)

function Trace({ className, ...props }: ViewProps) {
  return (
    <View
      className={cn("w-full min-w-0 flex-col gap-2", className)}
      {...props}
    />
  )
}

function TraceLine({
  className,
  status,
  ...props
}: ViewProps & { status?: TraceStatus }) {
  const tone = status === "error" ? "text-destructive" : "text-muted-foreground"
  return (
    <TraceStatusContext.Provider value={status}>
      <TextClassContext.Provider value={cn("text-sm", tone)}>
        <IconClassContext.Provider value={cn("size-4", tone)}>
          <View
            className={cn(
              "max-w-full min-w-0 shrink flex-row items-center gap-2",
              status === "pending" && "opacity-60",
              className
            )}
            {...props}
          />
        </IconClassContext.Provider>
      </TextClassContext.Provider>
    </TraceStatusContext.Provider>
  )
}

function TraceIcon({ className, ...props }: ViewProps) {
  return (
    <View
      accessible={false}
      importantForAccessibility="no-hide-descendants"
      className={cn("size-4 shrink-0 items-center justify-center", className)}
      {...props}
    />
  )
}

function TraceTitle({
  className,
  ...props
}: React.ComponentProps<typeof Text>) {
  const status = React.useContext(TraceStatusContext)
  const reduceMotion = useReducedMotion()
  const running = status === "running" && !reduceMotion
  const opacity = useSharedValue(1)

  React.useEffect(() => {
    opacity.value = running
      ? withRepeat(withTiming(0.45, { duration: 900 }), -1, true)
      : withTiming(1, { duration: 150 })
  }, [opacity, running])

  const animatedStyle = useAnimatedStyle(() => ({ opacity: opacity.value }))

  return (
    <Animated.View className="min-w-0 shrink" style={animatedStyle}>
      <Text numberOfLines={1} className={cn("min-w-0", className)} {...props} />
    </Animated.View>
  )
}

function TraceDescription({
  className,
  style,
  ...props
}: React.ComponentProps<typeof Text>) {
  return (
    <Text
      className={cn("shrink-0 text-muted-foreground-70", className)}
      style={[{ fontVariant: ["tabular-nums"] }, style]}
      {...props}
    />
  )
}

function TraceCollapsible({
  className,
  ...props
}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
  return (
    <CollapsiblePrimitive.Root
      className={(state) =>
        cn("w-full min-w-0 flex-col", resolveClassName(className, state))
      }
      {...props}
    />
  )
}

function TraceCollapsibleTrigger({
  className,
  children,
  ...props
}: React.ComponentProps<typeof CollapsiblePrimitive.Trigger>) {
  const { open } = CollapsiblePrimitive.useRootContext()
  const progress = useDerivedValue(() =>
    withTiming(open ? 1 : 0, { duration: 200 })
  )
  const caretStyle = useAnimatedStyle(() => ({
    transform: [{ rotate: `${progress.value * 90}deg` }],
  }))

  return (
    <CollapsiblePrimitive.Trigger
      className={(state) =>
        cn(
          "max-w-full min-w-0 self-start flex-row items-center gap-1 rounded-lg active:opacity-70",
          resolveClassName(className, state)
        )
      }
      {...props}
    >
      {children}
      <Animated.View
        style={caretStyle}
        accessible={false}
        importantForAccessibility="no-hide-descendants"
      >
        <Icon as={CaretRightIcon} className="size-4 text-muted-foreground" />
      </Animated.View>
    </CollapsiblePrimitive.Trigger>
  )
}

function TraceCollapsibleContent({
  className,
  children,
  ...props
}: React.ComponentProps<typeof CollapsiblePrimitive.Panel>) {
  return (
    <CollapsiblePrimitive.Panel
      className={(state) =>
        cn(
          "mt-2 mb-1 ms-[7px] min-w-0 border-s border-border ps-4 pe-1",
          resolveClassName(className, state)
        )
      }
      {...props}
    >
      <TextClassContext.Provider value="text-sm leading-relaxed text-muted-foreground">
        {children}
      </TextClassContext.Provider>
    </CollapsiblePrimitive.Panel>
  )
}

export {
  Trace,
  TraceCollapsible,
  TraceCollapsibleContent,
  TraceCollapsibleTrigger,
  TraceDescription,
  TraceIcon,
  TraceLine,
  TraceTitle,
}
```

4. Update the import paths to match your project.

**StyleSheet**

1. Install the dependencies:

```bash title="Terminal"
npx expo install phosphor-react-native react-native-reanimated react-native-svg react-native-worklets
```

2. Add the pieces it builds on: [Icon](https://suiss.app/ui/docs/components/icon), [Text](https://suiss.app/ui/docs/components/text), and the primitives `components/primitives/collapsible.tsx`, `components/primitives/use-render.tsx`.

3. Copy the source into `components/ui/trace.tsx`:

```tsx title="stylesheet/components/ui/trace.tsx"
import { CaretRightIcon } from "phosphor-react-native"
import * as React from "react"
import {
  type GestureResponderEvent,
  StyleSheet,
  View,
  type ViewProps,
} from "react-native"
import Animated, {
  useAnimatedStyle,
  useDerivedValue,
  useReducedMotion,
  useSharedValue,
  withRepeat,
  withTiming,
} from "react-native-reanimated"
import * as CollapsiblePrimitive from "@/components/primitives/collapsible"
import { Icon, IconSizeContext } from "@/components/ui/icon"
import { Text, TextStyleContext } from "@/components/ui/text"
import { fontSize, useTheme } from "@/components/ui/theme"

type TraceStatus = "running" | "error" | "pending"

const TraceStatusContext = React.createContext<TraceStatus | undefined>(
  undefined
)

const styles = StyleSheet.create({
  root: { width: "100%", minWidth: 0, flexDirection: "column", gap: 8 },
  line: {
    maxWidth: "100%",
    minWidth: 0,
    flexShrink: 1,
    flexDirection: "row",
    alignItems: "center",
    gap: 8,
  },
  pending: { opacity: 0.6 },
  icon: {
    width: 16,
    height: 16,
    flexShrink: 0,
    alignItems: "center",
    justifyContent: "center",
  },
  titleBox: { minWidth: 0, flexShrink: 1 },
  title: { minWidth: 0 },
  description: { flexShrink: 0, fontVariant: ["tabular-nums"] },
  collapsible: { width: "100%", minWidth: 0, flexDirection: "column" },
  trigger: {
    maxWidth: "100%",
    minWidth: 0,
    alignSelf: "flex-start",
    flexDirection: "row",
    alignItems: "center",
    gap: 4,
    borderRadius: 8,
  },
  pressed: { opacity: 0.7 },
  content: {
    marginTop: 8,
    marginBottom: 4,
    marginStart: 7,
    minWidth: 0,
    borderStartWidth: 1,
    paddingStart: 16,
    paddingEnd: 4,
  },
})

function Trace({ style, ...props }: ViewProps) {
  return <View style={[styles.root, style]} {...props} />
}

function TraceLine({
  style,
  status,
  ...props
}: ViewProps & { status?: TraceStatus }) {
  const { colors } = useTheme()
  const color = status === "error" ? colors.destructive : colors.mutedForeground
  return (
    <TraceStatusContext.Provider value={status}>
      <TextStyleContext.Provider value={{ ...fontSize.sm, color }}>
        <IconSizeContext.Provider value={16}>
          <View
            style={[styles.line, status === "pending" && styles.pending, style]}
            {...props}
          />
        </IconSizeContext.Provider>
      </TextStyleContext.Provider>
    </TraceStatusContext.Provider>
  )
}

function TraceIcon({ style, ...props }: ViewProps) {
  return (
    <View
      accessible={false}
      importantForAccessibility="no-hide-descendants"
      style={[styles.icon, style]}
      {...props}
    />
  )
}

function TraceTitle({ style, ...props }: React.ComponentProps<typeof Text>) {
  const status = React.useContext(TraceStatusContext)
  const reduceMotion = useReducedMotion()
  const running = status === "running" && !reduceMotion
  const opacity = useSharedValue(1)

  React.useEffect(() => {
    opacity.value = running
      ? withRepeat(withTiming(0.45, { duration: 900 }), -1, true)
      : withTiming(1, { duration: 150 })
  }, [opacity, running])

  const animatedStyle = useAnimatedStyle(() => ({ opacity: opacity.value }))

  return (
    <Animated.View style={[styles.titleBox, animatedStyle]}>
      <Text numberOfLines={1} style={[styles.title, style]} {...props} />
    </Animated.View>
  )
}

function TraceDescription({
  style,
  ...props
}: React.ComponentProps<typeof Text>) {
  const { colors } = useTheme()
  return (
    <Text
      style={[
        styles.description,
        { color: colors.mutedForeground, opacity: 0.7 },
        style,
      ]}
      {...props}
    />
  )
}

function TraceCollapsible({
  style,
  ...props
}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
  return (
    <CollapsiblePrimitive.Root
      style={(state) => [
        styles.collapsible,
        typeof style === "function" ? style(state) : style,
      ]}
      {...props}
    />
  )
}

function TraceCollapsibleTrigger({
  style,
  children,
  onPressIn,
  onPressOut,
  ...props
}: React.ComponentProps<typeof CollapsiblePrimitive.Trigger>) {
  const { colors } = useTheme()
  const [pressed, setPressed] = React.useState(false)
  const { open } = CollapsiblePrimitive.useRootContext()
  const progress = useDerivedValue(() =>
    withTiming(open ? 1 : 0, { duration: 200 })
  )
  const caretStyle = useAnimatedStyle(() => ({
    transform: [{ rotate: `${progress.value * 90}deg` }],
  }))

  return (
    <CollapsiblePrimitive.Trigger
      style={(state) => [
        styles.trigger,
        pressed && styles.pressed,
        typeof style === "function" ? style(state) : style,
      ]}
      onPressIn={(event: GestureResponderEvent) => {
        setPressed(true)
        onPressIn?.(event)
      }}
      onPressOut={(event: GestureResponderEvent) => {
        setPressed(false)
        onPressOut?.(event)
      }}
      {...props}
    >
      {children}
      <Animated.View
        style={caretStyle}
        accessible={false}
        importantForAccessibility="no-hide-descendants"
      >
        <Icon as={CaretRightIcon} size={16} color={colors.mutedForeground} />
      </Animated.View>
    </CollapsiblePrimitive.Trigger>
  )
}

function TraceCollapsibleContent({
  style,
  children,
  ...props
}: React.ComponentProps<typeof CollapsiblePrimitive.Panel>) {
  const { colors } = useTheme()
  return (
    <CollapsiblePrimitive.Panel
      style={(state) => [
        styles.content,
        { borderColor: colors.border },
        typeof style === "function" ? style(state) : style,
      ]}
      {...props}
    >
      <TextStyleContext.Provider
        value={{
          ...fontSize.sm,
          lineHeight: 22,
          color: colors.mutedForeground,
        }}
      >
        {children}
      </TextStyleContext.Provider>
    </CollapsiblePrimitive.Panel>
  )
}

export {
  Trace,
  TraceCollapsible,
  TraceCollapsibleContent,
  TraceCollapsibleTrigger,
  TraceDescription,
  TraceIcon,
  TraceLine,
  TraceTitle,
}
```

4. Update the import paths to match your project.

**SwiftUI**

1. Target iOS 17 or later.

2. Copy the theme files every view uses:

- `swift/Theme/Theme.swift`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/swiftui/Sources/EsuissUI/Theme/Theme.swift)

- `swift/Theme/Tokens.swift`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/swiftui/Sources/EsuissUI/Theme/Tokens.swift)

3. Copy the source and every file it uses:

```swift title="swift/Components/Trace.swift"
import PhosphorSwift
import SwiftUI

public enum TraceStatus: Sendable {
  case running, error, pending
}

extension EnvironmentValues {
  @Entry var traceStatus: TraceStatus?
}

public struct Trace<Content: View>: View {
  let content: Content

  public init(@ViewBuilder content: () -> Content) {
    self.content = content()
  }

  public var body: some View {
    VStack(alignment: .leading, spacing: 8) {
      content
    }
    .frame(maxWidth: .infinity, alignment: .leading)
  }
}

public struct TraceLine<Content: View>: View {
  let status: TraceStatus?
  let content: Content

  public init(status: TraceStatus? = nil, @ViewBuilder content: () -> Content) {
    self.status = status
    self.content = content()
  }

  public var body: some View {
    HStack(spacing: 8) {
      content
    }
    .scaledTextStyle(.sm)
    .foregroundStyle(status == .error ? EsuissColor.destructive : EsuissColor.mutedForeground)
    .opacity(status == .pending ? 0.6 : 1)
    .lineLimit(1)
    .environment(\.traceStatus, status)
    .accessibilityElement(children: .combine)
  }
}

public struct TraceIcon<Content: View>: View {
  let content: Content

  public init(@ViewBuilder content: () -> Content) {
    self.content = content()
  }

  public var body: some View {
    content
      .frame(width: 16, height: 16)
      .accessibilityHidden(true)
  }
}

public struct TraceTitle: View {
  let text: Text

  @Environment(\.traceStatus) private var status
  @Environment(\.accessibilityReduceMotion) private var reduceMotion
  @State private var dimmed = false

  public init(_ title: LocalizedStringKey) {
    self.text = Text(title)
  }

  public init(verbatim title: String) {
    self.text = Text(verbatim: title)
  }

  public var body: some View {
    let running = status == .running && !reduceMotion
    text
      .truncationMode(.tail)
      .opacity(running && dimmed ? 0.45 : 1)
      .animation(running ? .easeInOut(duration: 0.9).repeatForever(autoreverses: true) : .default, value: dimmed)
      .onAppear { dimmed = running }
      .onChange(of: running) { _, value in dimmed = value }
  }
}

public struct TraceDescription: View {
  let text: Text

  public init(_ description: LocalizedStringKey) {
    self.text = Text(description)
  }

  public init(verbatim description: String) {
    self.text = Text(verbatim: description)
  }

  public var body: some View {
    text
      .monospacedDigit()
      .opacity(0.7)
      .fixedSize()
  }
}

public struct TraceCollapsible<Content: View>: View {
  let external: Binding<Bool>?
  let content: Content

  @State private var local: Bool
  @Environment(\.accessibilityReduceMotion) private var reduceMotion

  public init(isExpanded: Binding<Bool>, @ViewBuilder content: () -> Content) {
    self.external = isExpanded
    self._local = State(initialValue: false)
    self.content = content()
  }

  public init(initiallyExpanded: Bool = false, @ViewBuilder content: () -> Content) {
    self.external = nil
    self._local = State(initialValue: initiallyExpanded)
    self.content = content()
  }

  public var body: some View {
    let binding = external ?? $local
    let animation: Animation = reduceMotion ? .linear(duration: 0.15) : .easeInOut(duration: 0.2)
    VStack(alignment: .leading, spacing: 0) {
      content
    }
    .frame(maxWidth: .infinity, alignment: .leading)
    .environment(
      \.collapsible,
      CollapsibleContext(
        isExpanded: binding.wrappedValue,
        toggle: { withAnimation(animation) { binding.wrappedValue.toggle() } }
      )
    )
  }
}

public struct TraceCollapsibleTrigger<Label: View>: View {
  let label: Label

  @Environment(\.collapsible) private var collapsible

  public init(@ViewBuilder label: () -> Label) {
    self.label = label()
  }

  public var body: some View {
    Button {
      collapsible.toggle()
    } label: {
      HStack(spacing: 4) {
        label
        Ph.caretRight.regular
          .frame(width: 16, height: 16)
          .foregroundStyle(EsuissColor.mutedForeground)
          .rotationEffect(.degrees(collapsible.isExpanded ? 90 : 0))
          .environment(\.layoutDirection, .leftToRight)
          .accessibilityHidden(true)
      }
      .contentShape(Rectangle())
    }
    .buttonStyle(.plain)
    .accessibilityValue(Text(collapsible.isExpanded ? "Expanded" : "Collapsed"))
  }
}

public struct TraceCollapsibleContent<Content: View>: View {
  let content: Content

  @Environment(\.collapsible) private var collapsible
  @Environment(\.accessibilityReduceMotion) private var reduceMotion

  public init(@ViewBuilder content: () -> Content) {
    self.content = content()
  }

  public var body: some View {
    if collapsible.isExpanded {
      VStack(alignment: .leading, spacing: 8) {
        content
      }
      .scaledTextStyle(.sm)
      .foregroundStyle(EsuissColor.mutedForeground)
      .lineSpacing(4)
      .frame(maxWidth: .infinity, alignment: .leading)
      .padding(.leading, 16)
      .padding(.trailing, 4)
      .overlay(alignment: .leading) {
        Rectangle().fill(EsuissColor.border).frame(width: 1)
      }
      .padding(.leading, 7)
      .padding(.top, 8)
      .padding(.bottom, 4)
      .transition(reduceMotion ? .opacity : .opacity.combined(with: .offset(y: -8)))
    }
  }
}
```

- `swift/Components/Collapsible.swift`: see [Collapsible](https://suiss.app/ui/docs/components/collapsible.md)

4. Add the files to your app target.

**Jetpack Compose**

1. Set `minSdk` to 24 or higher and add the dependencies:

```kotlin title="build.gradle.kts"
implementation(platform("androidx.compose:compose-bom:2026.09.00"))
implementation("androidx.compose.foundation:foundation")
implementation("androidx.compose.ui:ui")
```

2. Copy the theme files every view uses:

- `kotlin/theme/Theme.kt`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/kotlin/ui/src/main/kotlin/com/esuiss/ui/theme/Theme.kt)

- `kotlin/theme/Tokens.kt`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/kotlin/ui/src/main/kotlin/com/esuiss/ui/theme/Tokens.kt)

3. Copy the source and every file it uses:

```kt title="kotlin/components/Trace.kt"
package com.esuiss.ui.components

import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.semantics.clearAndSetSemantics
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.regular.CaretRight
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Typography

enum class TraceStatus { Running, Error, Pending }

private val LocalTraceStatus = staticCompositionLocalOf<TraceStatus?> { null }

private val LocalTraceExpanded = staticCompositionLocalOf { false }

@Composable
fun Trace(
    modifier: Modifier = Modifier,
    content: @Composable ColumnScope.() -> Unit,
) {
    Column(
        modifier = modifier.fillMaxWidth(),
        verticalArrangement = Arrangement.spacedBy(8.dp),
        content = content,
    )
}

@Composable
fun TraceLine(
    modifier: Modifier = Modifier,
    status: TraceStatus? = null,
    content: @Composable RowScope.() -> Unit,
) {
    val colors = EsuissTheme.colors
    val color = if (status == TraceStatus.Error) colors.destructive else colors.mutedForeground
    Row(
        modifier = modifier
            .alpha(if (status == TraceStatus.Pending) 0.6f else 1f)
            .semantics(mergeDescendants = true) {},
        horizontalArrangement = Arrangement.spacedBy(8.dp),
        verticalAlignment = Alignment.CenterVertically,
    ) {
        CompositionLocalProvider(
            LocalTraceStatus provides status,
            LocalContentStyle provides Typography.sm.copy(color = color),
            LocalContentColor provides color,
            LocalIconSize provides 16.dp,
        ) {
            content()
        }
    }
}

@Composable
fun TraceIcon(
    modifier: Modifier = Modifier,
    content: @Composable BoxScope.() -> Unit,
) {
    Box(
        modifier = modifier.size(16.dp).clearAndSetSemantics {},
        contentAlignment = Alignment.Center,
    ) {
        content()
    }
}

@Composable
fun RowScope.TraceTitle(text: String, modifier: Modifier = Modifier) {
    val running = LocalTraceStatus.current == TraceStatus.Running
    val pulse = if (running) {
        val transition = rememberInfiniteTransition(label = "trace-title")
        transition.animateFloat(
            initialValue = 1f,
            targetValue = 0.45f,
            animationSpec = infiniteRepeatable(tween(900), RepeatMode.Reverse),
            label = "trace-title-alpha",
        ).value
    } else {
        1f
    }
    BasicText(
        text,
        modifier = modifier.weight(1f, fill = false).alpha(pulse),
        style = LocalContentStyle.current,
        maxLines = 1,
        overflow = TextOverflow.Ellipsis,
    )
}

@Composable
fun TraceDescription(text: String, modifier: Modifier = Modifier) {
    BasicText(
        text,
        modifier = modifier.alpha(0.7f),
        style = LocalContentStyle.current.copy(fontFeatureSettings = "tnum"),
        maxLines = 1,
    )
}

@Composable
fun TraceCollapsible(
    expanded: Boolean,
    onExpandedChange: (Boolean) -> Unit,
    modifier: Modifier = Modifier,
    content: @Composable ColumnScope.() -> Unit,
) {
    CompositionLocalProvider(LocalTraceExpanded provides expanded) {
        Collapsible(
            expanded = expanded,
            onExpandedChange = onExpandedChange,
            modifier = modifier.fillMaxWidth(),
            content = content,
        )
    }
}

@Composable
fun TraceCollapsible(
    modifier: Modifier = Modifier,
    initiallyExpanded: Boolean = false,
    content: @Composable ColumnScope.() -> Unit,
) {
    var expanded by rememberSaveable { mutableStateOf(initiallyExpanded) }
    TraceCollapsible(expanded, { expanded = it }, modifier, content)
}

@Composable
fun TraceCollapsibleTrigger(
    modifier: Modifier = Modifier,
    content: @Composable RowScope.() -> Unit,
) {
    val expanded = LocalTraceExpanded.current
    val rotation by animateFloatAsState(if (expanded) 90f else 0f, tween(200), label = "trace-caret")
    CollapsibleTrigger(modifier = modifier) {
        Row(
            horizontalArrangement = Arrangement.spacedBy(4.dp),
            verticalAlignment = Alignment.CenterVertically,
        ) {
            content()
            Icon(
                PhosphorIcons.Regular.CaretRight,
                contentDescription = null,
                modifier = Modifier.rotate(rotation),
                size = 16.dp,
                tint = EsuissTheme.colors.mutedForeground,
            )
        }
    }
}

@Composable
fun TraceCollapsibleContent(
    modifier: Modifier = Modifier,
    content: @Composable ColumnScope.() -> Unit,
) {
    val colors = EsuissTheme.colors
    CollapsibleContent(modifier = modifier) {
        Column(
            modifier = Modifier
                .padding(start = 7.dp, top = 8.dp, bottom = 4.dp)
                .drawBehind {
                    val stroke = 1.dp.toPx()
                    val x = if (layoutDirection == LayoutDirection.Rtl) size.width - stroke else 0f
                    drawRect(colors.border, Offset(x, 0f), Size(stroke, size.height))
                }
                .padding(start = 16.dp, end = 4.dp)
                .fillMaxWidth(),
            verticalArrangement = Arrangement.spacedBy(8.dp),
        ) {
            CompositionLocalProvider(
                LocalContentStyle provides Typography.sm.copy(color = colors.mutedForeground, lineHeight = 22.sp),
                LocalContentColor provides colors.mutedForeground,
            ) {
                content()
            }
        }
    }
}
```

- `kotlin/components/Button.kt`: see [Button](https://suiss.app/ui/docs/components/button.md)

- `kotlin/components/ButtonGroup.kt`: see [Button Group](https://suiss.app/ui/docs/components/button-group.md)

- `kotlin/components/Collapsible.kt`: see [Collapsible](https://suiss.app/ui/docs/components/collapsible.md)

- `kotlin/components/Content.kt`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/kotlin/ui/src/main/kotlin/com/esuiss/ui/components/Content.kt)

- `kotlin/components/Icon.kt`: see [Icon](https://suiss.app/ui/docs/components/icon.md)

4. Update the `com.esuiss.ui` package names to match your project.

## Usage

```tsx
import { Trace, TraceCollapsible, TraceCollapsibleContent, TraceCollapsibleTrigger, TraceDescription, TraceIcon, TraceLine, TraceTitle } from "@/components/ui/trace"
```

```tsx
<Trace>
  <TraceCollapsible>
    <TraceCollapsibleTrigger>
      <TraceLine>
        <TraceIcon>
          <Icon as={TerminalIcon} />
        </TraceIcon>
        <TraceTitle>Ran node --version</TraceTitle>
      </TraceLine>
    </TraceCollapsibleTrigger>
    <TraceCollapsibleContent>
      <Text>v24.18.0</Text>
    </TraceCollapsibleContent>
  </TraceCollapsible>
</Trace>
```

Rows start closed. Set `defaultOpen` on `TraceCollapsible` to start open, or pass `open` with `onOpenChange` to control it. In SwiftUI use `TraceCollapsible(initiallyExpanded:)` or `TraceCollapsible(isExpanded:)`; in Compose pass `initiallyExpanded` or `expanded` with `onExpandedChange`.

## Examples

### Static

A `TraceLine` on its own, for a step with no detail to show.

```tsx
import { GitBranchIcon } from "phosphor-react-native"
import { Icon } from "@/components/ui/icon"
import { Trace, TraceIcon, TraceLine, TraceTitle } from "@/components/ui/trace"

export function TraceStaticExample() {
  return (
    <Trace>
      <TraceLine>
        <TraceIcon>
          <Icon as={GitBranchIcon} />
        </TraceIcon>
        <TraceTitle>Created branch fix/frozen-lockfile</TraceTitle>
      </TraceLine>
    </Trace>
  )
}
```

```swift
import EsuissUI
import PhosphorSwift
import SwiftUI

struct TraceStaticExample: View {
  var body: some View {
    Trace {
      TraceLine {
        TraceIcon { Ph.gitBranch.regular }
        TraceTitle("Created branch fix/frozen-lockfile")
      }
    }
  }
}
```

```kotlin
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.vector.ImageVector
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.regular.GitBranch
import com.esuiss.ui.components.Icon
import com.esuiss.ui.components.Trace
import com.esuiss.ui.components.TraceDescription
import com.esuiss.ui.components.TraceIcon
import com.esuiss.ui.components.TraceLine
import com.esuiss.ui.components.TraceStatus
import com.esuiss.ui.components.TraceTitle

@Composable
fun TraceRow(icon: ImageVector, title: String, status: TraceStatus? = null, description: String? = null) {
    TraceLine(status = status) {
        TraceIcon { Icon(icon, contentDescription = null) }
        TraceTitle(title)
        if (description != null) TraceDescription(description)
    }
}

@Composable
fun TraceStatic() {
    Trace { TraceRow(PhosphorIcons.Regular.GitBranch, "Created branch fix/frozen-lockfile") }
}
```

### Status

`running` pulses the title, `error` turns the row destructive and `pending` dims it. Say the state in the title too.

```tsx
import { CheckCircleIcon, CircleIcon, CircleNotchIcon, WarningCircleIcon } from "phosphor-react-native"
import { Icon } from "@/components/ui/icon"
import { Trace, TraceIcon, TraceLine, TraceTitle } from "@/components/ui/trace"

export function TraceStatusExample() {
  return (
    <Trace>
      <TraceLine>
        <TraceIcon>
          <Icon as={CheckCircleIcon} />
        </TraceIcon>
        <TraceTitle>Ran pnpm install</TraceTitle>
      </TraceLine>
      <TraceLine status="error">
        <TraceIcon>
          <Icon as={WarningCircleIcon} />
        </TraceIcon>
        <TraceTitle>Failed to run pnpm lint</TraceTitle>
      </TraceLine>
      <TraceLine status="running">
        <TraceIcon>
          <Icon as={CircleNotchIcon} />
        </TraceIcon>
        <TraceTitle>Running pnpm build</TraceTitle>
      </TraceLine>
      <TraceLine status="pending">
        <TraceIcon>
          <Icon as={CircleIcon} />
        </TraceIcon>
        <TraceTitle>Run pnpm test</TraceTitle>
      </TraceLine>
    </Trace>
  )
}
```

```swift
import EsuissUI
import PhosphorSwift
import SwiftUI

struct TraceStatusExample: View {
  var body: some View {
    Trace {
      TraceLine {
        TraceIcon { Ph.checkCircle.regular }
        TraceTitle("Ran pnpm install")
      }
      TraceLine(status: .error) {
        TraceIcon { Ph.warningCircle.regular }
        TraceTitle("Failed to run pnpm lint")
      }
      TraceLine(status: .running) {
        TraceIcon { Ph.circleNotch.regular }
        TraceTitle("Running pnpm build")
      }
      TraceLine(status: .pending) {
        TraceIcon { Ph.circle.regular }
        TraceTitle("Run pnpm test")
      }
    }
  }
}
```

```kotlin
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.vector.ImageVector
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.regular.CheckCircle
import com.adamglin.phosphoricons.regular.Circle
import com.adamglin.phosphoricons.regular.CircleNotch
import com.adamglin.phosphoricons.regular.WarningCircle
import com.esuiss.ui.components.Icon
import com.esuiss.ui.components.Trace
import com.esuiss.ui.components.TraceDescription
import com.esuiss.ui.components.TraceIcon
import com.esuiss.ui.components.TraceLine
import com.esuiss.ui.components.TraceStatus
import com.esuiss.ui.components.TraceTitle

@Composable
fun TraceRow(icon: ImageVector, title: String, status: TraceStatus? = null, description: String? = null) {
    TraceLine(status = status) {
        TraceIcon { Icon(icon, contentDescription = null) }
        TraceTitle(title)
        if (description != null) TraceDescription(description)
    }
}

@Composable
fun TraceStatusStory() {
    Trace {
        TraceRow(PhosphorIcons.Regular.CheckCircle, "Ran pnpm install")
        TraceRow(PhosphorIcons.Regular.WarningCircle, "Failed to run pnpm lint", TraceStatus.Error)
        TraceRow(PhosphorIcons.Regular.CircleNotch, "Running pnpm build", TraceStatus.Running)
        TraceRow(PhosphorIcons.Regular.Circle, "Run pnpm test", TraceStatus.Pending)
    }
}
```

### Description

Secondary text such as a diff stat, a duration or a count after the title.

```tsx
import { MagnifyingGlassIcon, NotePencilIcon, TerminalIcon } from "phosphor-react-native"
import { Icon } from "@/components/ui/icon"
import { Trace, TraceDescription, TraceIcon, TraceLine, TraceTitle } from "@/components/ui/trace"

export function TraceDescriptionExample() {
  return (
    <Trace>
      <TraceLine>
        <TraceIcon>
          <Icon as={NotePencilIcon} />
        </TraceIcon>
        <TraceTitle>Edited settings/notifications.tsx</TraceTitle>
        <TraceDescription>+48 −12</TraceDescription>
      </TraceLine>
      <TraceLine>
        <TraceIcon>
          <Icon as={TerminalIcon} />
        </TraceIcon>
        <TraceTitle>Ran pnpm test</TraceTitle>
        <TraceDescription>14s</TraceDescription>
      </TraceLine>
      <TraceLine>
        <TraceIcon>
          <Icon as={MagnifyingGlassIcon} />
        </TraceIcon>
        <TraceTitle>Searched for useChat</TraceTitle>
        <TraceDescription>8 results</TraceDescription>
      </TraceLine>
    </Trace>
  )
}
```

```swift
import EsuissUI
import PhosphorSwift
import SwiftUI

struct TraceDescriptionExample: View {
  var body: some View {
    Trace {
      TraceLine {
        TraceIcon { Ph.notePencil.regular }
        TraceTitle("Edited settings/notifications.tsx")
        TraceDescription(verbatim: "+48 −12")
      }
      TraceLine {
        TraceIcon { Ph.terminal.regular }
        TraceTitle("Ran pnpm test")
        TraceDescription(verbatim: "14s")
      }
      TraceLine {
        TraceIcon { Ph.magnifyingGlass.regular }
        TraceTitle("Searched for useChat")
        TraceDescription("8 results")
      }
    }
  }
}
```

```kotlin
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.vector.ImageVector
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.regular.MagnifyingGlass
import com.adamglin.phosphoricons.regular.NotePencil
import com.adamglin.phosphoricons.regular.Terminal
import com.esuiss.ui.components.Icon
import com.esuiss.ui.components.Trace
import com.esuiss.ui.components.TraceDescription
import com.esuiss.ui.components.TraceIcon
import com.esuiss.ui.components.TraceLine
import com.esuiss.ui.components.TraceStatus
import com.esuiss.ui.components.TraceTitle

@Composable
fun TraceRow(icon: ImageVector, title: String, status: TraceStatus? = null, description: String? = null) {
    TraceLine(status = status) {
        TraceIcon { Icon(icon, contentDescription = null) }
        TraceTitle(title)
        if (description != null) TraceDescription(description)
    }
}

@Composable
fun TraceDescriptionStory() {
    Trace {
        TraceRow(PhosphorIcons.Regular.NotePencil, "Edited settings/notifications.tsx", description = "+48 −12")
        TraceRow(PhosphorIcons.Regular.Terminal, "Ran pnpm test", description = "14s")
        TraceRow(PhosphorIcons.Regular.MagnifyingGlass, "Searched for useChat", description = "8 results")
    }
}
```

### Collapsible

A row that expands to show the command output.

```tsx
import { TerminalIcon } from "phosphor-react-native"
import { CodeBlock, CodeBlockContent } from "@/components/ui/code-block"
import { Icon } from "@/components/ui/icon"
import { TraceCollapsible, TraceCollapsibleContent, TraceCollapsibleTrigger, TraceIcon, TraceLine, TraceTitle } from "@/components/ui/trace"

function Output({ children, error }: { children: string; error?: boolean }) {
  return (
    <CodeBlock size="sm" variant={error ? "destructive" : "default"}>
      <CodeBlockContent wrap>{children}</CodeBlockContent>
    </CodeBlock>
  )
}

export function TraceCollapsibleExample() {
  return (
    <TraceCollapsible>
      <TraceCollapsibleTrigger>
        <TraceLine>
          <TraceIcon>
            <Icon as={TerminalIcon} />
          </TraceIcon>
          <TraceTitle>Ran node --version</TraceTitle>
        </TraceLine>
      </TraceCollapsibleTrigger>
      <TraceCollapsibleContent>
        <Output>{"$ node --version\nv24.18.0"}</Output>
      </TraceCollapsibleContent>
    </TraceCollapsible>
  )
}
```

```swift
import EsuissUI
import PhosphorSwift
import SwiftUI

struct TraceCollapsibleExample: View {
  var body: some View {
    TraceCollapsible {
      TraceCollapsibleTrigger {
        TraceLine {
          TraceIcon { Ph.terminal.regular }
          TraceTitle("Ran node --version")
        }
      }
      TraceCollapsibleContent { TraceOutput(text: "$ node --version\nv24.18.0") }
    }
  }
}

struct TraceOutput: View {
  let text: String
  var error = false

  var body: some View {
    CodeBlock(error ? .destructive : .default, size: .sm) {
      CodeBlockContent(text, wrap: true)
    }
  }
}
```

```kotlin
import androidx.compose.runtime.Composable
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.regular.Terminal
import com.esuiss.ui.components.CodeBlock
import com.esuiss.ui.components.CodeBlockContent
import com.esuiss.ui.components.CodeBlockSize
import com.esuiss.ui.components.CodeBlockVariant
import com.esuiss.ui.components.Icon
import com.esuiss.ui.components.TraceCollapsible
import com.esuiss.ui.components.TraceCollapsibleContent
import com.esuiss.ui.components.TraceCollapsibleTrigger
import com.esuiss.ui.components.TraceIcon
import com.esuiss.ui.components.TraceLine
import com.esuiss.ui.components.TraceTitle

@Composable
fun TraceOutput(text: String, error: Boolean = false) {
    CodeBlock(
        variant = if (error) CodeBlockVariant.Destructive else CodeBlockVariant.Default,
        size = CodeBlockSize.Sm,
    ) {
        CodeBlockContent(text, wrap = true)
    }
}

@Composable
fun TraceCollapsibleStory() {
    TraceCollapsible {
        TraceCollapsibleTrigger {
            TraceLine {
                TraceIcon { Icon(PhosphorIcons.Regular.Terminal, contentDescription = null) }
                TraceTitle("Ran node --version")
            }
        }
        TraceCollapsibleContent { TraceOutput("$ node --version\nv24.18.0") }
    }
}
```

### Controlled

Opens and closes a row from outside with `open` and `onOpenChange`.

```tsx
import { TerminalIcon } from "phosphor-react-native"
import * as React from "react"
import { View } from "react-native"
import { Button } from "@/components/ui/button"
import { CodeBlock, CodeBlockContent } from "@/components/ui/code-block"
import { Icon } from "@/components/ui/icon"
import { TraceCollapsible, TraceCollapsibleContent, TraceCollapsibleTrigger, TraceIcon, TraceLine, TraceTitle } from "@/components/ui/trace"

function Output({ children, error }: { children: string; error?: boolean }) {
  return (
    <CodeBlock size="sm" variant={error ? "destructive" : "default"}>
      <CodeBlockContent wrap>{children}</CodeBlockContent>
    </CodeBlock>
  )
}

function ControlledExample() {
  const [open, setOpen] = React.useState(false)
  return (
    <View className="items-start gap-4">
      <TraceCollapsible open={open} onOpenChange={setOpen}>
        <TraceCollapsibleTrigger>
          <TraceLine>
            <TraceIcon>
              <Icon as={TerminalIcon} />
            </TraceIcon>
            <TraceTitle>Ran pnpm --version</TraceTitle>
          </TraceLine>
        </TraceCollapsibleTrigger>
        <TraceCollapsibleContent>
          <Output>{"$ pnpm --version\n10.33.4"}</Output>
        </TraceCollapsibleContent>
      </TraceCollapsible>
      <Button variant="outline" size="sm" onPress={() => setOpen(!open)}>
        {open ? "Hide the pnpm output" : "Show the pnpm output"}
      </Button>
    </View>
  )
}

export function TraceControlledExample() {
  return (
    <ControlledExample />
  )
}
```

```swift
import EsuissUI
import PhosphorSwift
import SwiftUI

struct TraceOutput: View {
  let text: String
  var error = false

  var body: some View {
    CodeBlock(error ? .destructive : .default, size: .sm) {
      CodeBlockContent(text, wrap: true)
    }
  }
}

struct TraceControlledExample: View {
  @State private var open = false

  var body: some View {
    VStack(alignment: .leading, spacing: 16) {
      TraceCollapsible(isExpanded: $open) {
        TraceCollapsibleTrigger {
          TraceLine {
            TraceIcon { Ph.terminal.regular }
            TraceTitle("Ran pnpm --version")
          }
        }
        TraceCollapsibleContent { TraceOutput(text: "$ pnpm --version\n10.33.4") }
      }
      Button(open ? "Hide the pnpm output" : "Show the pnpm output") { open.toggle() }
        .buttonStyle(.variant(.outline, size: .sm))
    }
  }
}
```

```kotlin
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.unit.dp
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.regular.Terminal
import com.esuiss.ui.components.Button
import com.esuiss.ui.components.ButtonSize
import com.esuiss.ui.components.ButtonVariant
import com.esuiss.ui.components.CodeBlock
import com.esuiss.ui.components.CodeBlockContent
import com.esuiss.ui.components.CodeBlockSize
import com.esuiss.ui.components.CodeBlockVariant
import com.esuiss.ui.components.Icon
import com.esuiss.ui.components.TraceCollapsible
import com.esuiss.ui.components.TraceCollapsibleContent
import com.esuiss.ui.components.TraceCollapsibleTrigger
import com.esuiss.ui.components.TraceIcon
import com.esuiss.ui.components.TraceLine
import com.esuiss.ui.components.TraceTitle

@Composable
fun TraceOutput(text: String, error: Boolean = false) {
    CodeBlock(
        variant = if (error) CodeBlockVariant.Destructive else CodeBlockVariant.Default,
        size = CodeBlockSize.Sm,
    ) {
        CodeBlockContent(text, wrap = true)
    }
}

@Composable
fun TraceControlled() {
    var open by rememberSaveable { mutableStateOf(false) }
    Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
        TraceCollapsible(expanded = open, onExpandedChange = { open = it }) {
            TraceCollapsibleTrigger {
                TraceLine {
                    TraceIcon { Icon(PhosphorIcons.Regular.Terminal, contentDescription = null) }
                    TraceTitle("Ran pnpm --version")
                }
            }
            TraceCollapsibleContent { TraceOutput("$ pnpm --version\n10.33.4") }
        }
        Button(
            if (open) "Hide the pnpm output" else "Show the pnpm output",
            onClick = { open = !open },
            variant = ButtonVariant.Outline,
            size = ButtonSize.Sm,
        )
    }
}
```

### Link

Wraps the `TraceLine` in a `Pressable` that opens a URL.

```tsx
import { ArrowUpRightIcon, GlobeIcon } from "phosphor-react-native"
import { Linking, Pressable } from "react-native"
import { Icon } from "@/components/ui/icon"
import { Trace, TraceIcon, TraceLine, TraceTitle } from "@/components/ui/trace"

function SourceRow({ title, url }: { title: string; url: string }) {
  return (
    <Pressable
      accessibilityRole="link"
      accessibilityHint="Opens in the browser"
      onPress={() => Linking.openURL(url)}
      className="self-start active:opacity-70"
    >
      <TraceLine>
        <TraceIcon>
          <Icon as={GlobeIcon} />
        </TraceIcon>
        <TraceTitle>{title}</TraceTitle>
        <TraceIcon>
          <Icon as={ArrowUpRightIcon} />
        </TraceIcon>
      </TraceLine>
    </Pressable>
  )
}

export function TraceLinkExample() {
  return (
    <Trace>
      <SourceRow
        title="Install Astro | docs.astro.build"
        url="https://docs.astro.build/en/install-and-setup/"
      />
    </Trace>
  )
}
```

```swift
import EsuissUI
import PhosphorSwift
import SwiftUI

struct TraceLinkExample: View {
  var body: some View {
    Trace {
      TraceSourceRow(title: "Install Astro | docs.astro.build", url: "https://docs.astro.build/en/install-and-setup/")
    }
  }
}

struct TraceSourceRow: View {
  let title: String
  let url: String

  @Environment(\.openURL) private var openURL

  var body: some View {
    Button {
      if let link = URL(string: url) { openURL(link) }
    } label: {
      TraceLine {
        TraceIcon { Ph.globe.regular }
        TraceTitle(verbatim: title)
        TraceIcon { Ph.arrowUpRight.regular }
      }
    }
    .buttonStyle(.plain)
    .accessibilityAddTraits(.isLink)
  }
}
```

```kotlin
import androidx.compose.foundation.clickable
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.semantics.Role
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.regular.ArrowUpRight
import com.adamglin.phosphoricons.regular.Globe
import com.esuiss.ui.components.Button
import com.esuiss.ui.components.Icon
import com.esuiss.ui.components.Trace
import com.esuiss.ui.components.TraceIcon
import com.esuiss.ui.components.TraceLine
import com.esuiss.ui.components.TraceTitle

@Composable
fun SourceRow(title: String, url: String) {
    val uriHandler = LocalUriHandler.current
    TraceLine(Modifier.clickable(role = Role.Button) { uriHandler.openUri(url) }) {
        TraceIcon { Icon(PhosphorIcons.Regular.Globe, contentDescription = null) }
        TraceTitle(title)
        TraceIcon { Icon(PhosphorIcons.Regular.ArrowUpRight, contentDescription = null) }
    }
}

@Composable
fun TraceLink() {
    Trace { SourceRow("Install Astro | docs.astro.build", "https://docs.astro.build/en/install-and-setup/") }
}
```

### Group

Rows next to each other in one `Trace`.

```tsx
import { FileTextIcon, MagnifyingGlassIcon, TerminalIcon } from "phosphor-react-native"
import { Icon } from "@/components/ui/icon"
import { Trace, TraceIcon, TraceLine, TraceTitle } from "@/components/ui/trace"

export function TraceGroupExample() {
  return (
    <Trace>
      <TraceLine>
        <TraceIcon>
          <Icon as={MagnifyingGlassIcon} />
        </TraceIcon>
        <TraceTitle>Searched for minimumReleaseAge</TraceTitle>
      </TraceLine>
      <TraceLine>
        <TraceIcon>
          <Icon as={FileTextIcon} />
        </TraceIcon>
        <TraceTitle>Read pnpm-workspace.yaml</TraceTitle>
      </TraceLine>
      <TraceLine>
        <TraceIcon>
          <Icon as={TerminalIcon} />
        </TraceIcon>
        <TraceTitle>Ran pnpm install</TraceTitle>
      </TraceLine>
    </Trace>
  )
}
```

```swift
import EsuissUI
import PhosphorSwift
import SwiftUI

struct TraceGroupExample: View {
  var body: some View {
    Trace {
      TraceLine {
        TraceIcon { Ph.magnifyingGlass.regular }
        TraceTitle("Searched for minimumReleaseAge")
      }
      TraceLine {
        TraceIcon { Ph.fileText.regular }
        TraceTitle("Read pnpm-workspace.yaml")
      }
      TraceLine {
        TraceIcon { Ph.terminal.regular }
        TraceTitle("Ran pnpm install")
      }
    }
  }
}
```

```kotlin
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.vector.ImageVector
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.regular.FileText
import com.adamglin.phosphoricons.regular.MagnifyingGlass
import com.adamglin.phosphoricons.regular.Terminal
import com.esuiss.ui.components.Icon
import com.esuiss.ui.components.Trace
import com.esuiss.ui.components.TraceDescription
import com.esuiss.ui.components.TraceIcon
import com.esuiss.ui.components.TraceLine
import com.esuiss.ui.components.TraceStatus
import com.esuiss.ui.components.TraceTitle

@Composable
fun TraceRow(icon: ImageVector, title: String, status: TraceStatus? = null, description: String? = null) {
    TraceLine(status = status) {
        TraceIcon { Icon(icon, contentDescription = null) }
        TraceTitle(title)
        if (description != null) TraceDescription(description)
    }
}

@Composable
fun TraceGroup() {
    Trace {
        TraceRow(PhosphorIcons.Regular.MagnifyingGlass, "Searched for minimumReleaseAge")
        TraceRow(PhosphorIcons.Regular.FileText, "Read pnpm-workspace.yaml")
        TraceRow(PhosphorIcons.Regular.Terminal, "Ran pnpm install")
    }
}
```

### Sources

A collapsible row whose detail is a `Trace` of link rows.

```tsx
import { ArrowUpRightIcon, GlobeIcon } from "phosphor-react-native"
import { Linking, Pressable } from "react-native"
import { Icon } from "@/components/ui/icon"
import { Trace, TraceCollapsible, TraceCollapsibleContent, TraceCollapsibleTrigger, TraceIcon, TraceLine, TraceTitle } from "@/components/ui/trace"

const sources = [
  { title: "pnpm install | pnpm.io", url: "https://pnpm.io/cli/install" },
  {
    title: "Continuous Integration | pnpm.io",
    url: "https://pnpm.io/continuous-integration",
  },
]

function SourceRow({ title, url }: { title: string; url: string }) {
  return (
    <Pressable
      accessibilityRole="link"
      accessibilityHint="Opens in the browser"
      onPress={() => Linking.openURL(url)}
      className="self-start active:opacity-70"
    >
      <TraceLine>
        <TraceIcon>
          <Icon as={GlobeIcon} />
        </TraceIcon>
        <TraceTitle>{title}</TraceTitle>
        <TraceIcon>
          <Icon as={ArrowUpRightIcon} />
        </TraceIcon>
      </TraceLine>
    </Pressable>
  )
}

export function TraceSourcesExample() {
  return (
    <TraceCollapsible defaultOpen>
      <TraceCollapsibleTrigger>
        <TraceLine>
          <TraceIcon>
            <Icon as={GlobeIcon} />
          </TraceIcon>
          <TraceTitle>Used {sources.length} sources</TraceTitle>
        </TraceLine>
      </TraceCollapsibleTrigger>
      <TraceCollapsibleContent>
        <Trace>
          {sources.map((source) => (
            <SourceRow key={source.url} {...source} />
          ))}
        </Trace>
      </TraceCollapsibleContent>
    </TraceCollapsible>
  )
}
```

```swift
import EsuissUI
import PhosphorSwift
import SwiftUI

struct TraceSourcesExample: View {
  var body: some View {
    TraceCollapsible(initiallyExpanded: true) {
      TraceCollapsibleTrigger {
        TraceLine {
          TraceIcon { Ph.globe.regular }
          TraceTitle("Used 2 sources")
        }
      }
      TraceCollapsibleContent {
        Trace {
          TraceSourceRow(title: "pnpm install | pnpm.io", url: "https://pnpm.io/cli/install")
          TraceSourceRow(title: "Continuous Integration | pnpm.io", url: "https://pnpm.io/continuous-integration")
        }
      }
    }
  }
}

struct TraceSourceRow: View {
  let title: String
  let url: String

  @Environment(\.openURL) private var openURL

  var body: some View {
    Button {
      if let link = URL(string: url) { openURL(link) }
    } label: {
      TraceLine {
        TraceIcon { Ph.globe.regular }
        TraceTitle(verbatim: title)
        TraceIcon { Ph.arrowUpRight.regular }
      }
    }
    .buttonStyle(.plain)
    .accessibilityAddTraits(.isLink)
  }
}
```

```kotlin
import androidx.compose.foundation.clickable
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.semantics.Role
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.regular.ArrowUpRight
import com.adamglin.phosphoricons.regular.Globe
import com.esuiss.ui.components.Button
import com.esuiss.ui.components.Icon
import com.esuiss.ui.components.Trace
import com.esuiss.ui.components.TraceCollapsible
import com.esuiss.ui.components.TraceCollapsibleContent
import com.esuiss.ui.components.TraceCollapsibleTrigger
import com.esuiss.ui.components.TraceIcon
import com.esuiss.ui.components.TraceLine
import com.esuiss.ui.components.TraceTitle

@Composable
fun SourceRow(title: String, url: String) {
    val uriHandler = LocalUriHandler.current
    TraceLine(Modifier.clickable(role = Role.Button) { uriHandler.openUri(url) }) {
        TraceIcon { Icon(PhosphorIcons.Regular.Globe, contentDescription = null) }
        TraceTitle(title)
        TraceIcon { Icon(PhosphorIcons.Regular.ArrowUpRight, contentDescription = null) }
    }
}

@Composable
fun TraceSources() {
    TraceCollapsible(initiallyExpanded = true) {
        TraceCollapsibleTrigger {
            TraceLine {
                TraceIcon { Icon(PhosphorIcons.Regular.Globe, contentDescription = null) }
                TraceTitle("Used 2 sources")
            }
        }
        TraceCollapsibleContent {
            Trace {
                SourceRow("pnpm install | pnpm.io", "https://pnpm.io/cli/install")
                SourceRow("Continuous Integration | pnpm.io", "https://pnpm.io/continuous-integration")
            }
        }
    }
}
```

### Steps

A summary row with step rows inside.

```tsx
import { FileTextIcon, MagnifyingGlassIcon } from "phosphor-react-native"
import { Icon } from "@/components/ui/icon"
import { Trace, TraceCollapsible, TraceCollapsibleContent, TraceCollapsibleTrigger, TraceIcon, TraceLine, TraceTitle } from "@/components/ui/trace"

export function TraceStepsExample() {
  return (
    <TraceCollapsible defaultOpen>
      <TraceCollapsibleTrigger>
        <TraceLine status="running">
          <TraceTitle>Looking into the slow deploy</TraceTitle>
        </TraceLine>
      </TraceCollapsibleTrigger>
      <TraceCollapsibleContent keepMounted>
        <Trace>
          <TraceLine>
            <TraceIcon>
              <Icon as={MagnifyingGlassIcon} />
            </TraceIcon>
            <TraceTitle>Searched for the checkout build log</TraceTitle>
          </TraceLine>
          <TraceLine>
            <TraceIcon>
              <Icon as={FileTextIcon} />
            </TraceIcon>
            <TraceTitle>Read the last twelve deploys</TraceTitle>
          </TraceLine>
          <TraceLine status="running">
            <TraceIcon>
              <Icon as={MagnifyingGlassIcon} />
            </TraceIcon>
            <TraceTitle>Searching for the cache key</TraceTitle>
          </TraceLine>
        </Trace>
      </TraceCollapsibleContent>
    </TraceCollapsible>
  )
}
```

```swift
import EsuissUI
import PhosphorSwift
import SwiftUI

struct TraceStepsExample: View {
  var body: some View {
    TraceCollapsible(initiallyExpanded: true) {
      TraceCollapsibleTrigger {
        TraceLine(status: .running) { TraceTitle("Looking into the slow deploy") }
      }
      TraceCollapsibleContent {
        Trace {
          TraceLine {
            TraceIcon { Ph.magnifyingGlass.regular }
            TraceTitle("Searched for the checkout build log")
          }
          TraceLine {
            TraceIcon { Ph.fileText.regular }
            TraceTitle("Read the last twelve deploys")
          }
          TraceLine(status: .running) {
            TraceIcon { Ph.magnifyingGlass.regular }
            TraceTitle("Searching for the cache key")
          }
        }
      }
    }
  }
}
```

```kotlin
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.vector.ImageVector
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.regular.FileText
import com.adamglin.phosphoricons.regular.MagnifyingGlass
import com.esuiss.ui.components.Icon
import com.esuiss.ui.components.Trace
import com.esuiss.ui.components.TraceCollapsible
import com.esuiss.ui.components.TraceCollapsibleContent
import com.esuiss.ui.components.TraceCollapsibleTrigger
import com.esuiss.ui.components.TraceDescription
import com.esuiss.ui.components.TraceIcon
import com.esuiss.ui.components.TraceLine
import com.esuiss.ui.components.TraceStatus
import com.esuiss.ui.components.TraceTitle

@Composable
fun TraceRow(icon: ImageVector, title: String, status: TraceStatus? = null, description: String? = null) {
    TraceLine(status = status) {
        TraceIcon { Icon(icon, contentDescription = null) }
        TraceTitle(title)
        if (description != null) TraceDescription(description)
    }
}

@Composable
fun TraceSteps() {
    TraceCollapsible(initiallyExpanded = true) {
        TraceCollapsibleTrigger {
            TraceLine(status = TraceStatus.Running) { TraceTitle("Looking into the slow deploy") }
        }
        TraceCollapsibleContent {
            Trace {
                TraceRow(PhosphorIcons.Regular.MagnifyingGlass, "Searched for the checkout build log")
                TraceRow(PhosphorIcons.Regular.FileText, "Read the last twelve deploys")
                TraceRow(PhosphorIcons.Regular.MagnifyingGlass, "Searching for the cache key", TraceStatus.Running)
            }
        }
    }
}
```

### Commit

A row for each changed file, with the line counts in the description.

```tsx
import { FilePlusIcon, GitCommitIcon, NotePencilIcon } from "phosphor-react-native"
import { Icon } from "@/components/ui/icon"
import { Trace, TraceCollapsible, TraceCollapsibleContent, TraceCollapsibleTrigger, TraceDescription, TraceIcon, TraceLine, TraceTitle } from "@/components/ui/trace"

const files = [
  { icon: NotePencilIcon, path: "app/login/page.tsx", stat: "+12 −4" },
  { icon: NotePencilIcon, path: "lib/auth/redirect.ts", stat: "+31 −9" },
  { icon: FilePlusIcon, path: "lib/auth/redirect.test.ts", stat: "+58" },
]

export function TraceCommitExample() {
  return (
    <TraceCollapsible defaultOpen>
      <TraceCollapsibleTrigger>
        <TraceLine>
          <TraceIcon>
            <Icon as={GitCommitIcon} />
          </TraceIcon>
          <TraceTitle>Committed a1b2c3: Fix the login redirect</TraceTitle>
        </TraceLine>
      </TraceCollapsibleTrigger>
      <TraceCollapsibleContent>
        <Trace>
          {files.map((file) => (
            <TraceLine key={file.path}>
              <TraceIcon>
                <Icon as={file.icon} />
              </TraceIcon>
              <TraceTitle>{file.path}</TraceTitle>
              <TraceDescription>{file.stat}</TraceDescription>
            </TraceLine>
          ))}
        </Trace>
      </TraceCollapsibleContent>
    </TraceCollapsible>
  )
}
```

```swift
import EsuissUI
import PhosphorSwift
import SwiftUI

struct TraceCommitExample: View {
  var body: some View {
    TraceCollapsible(initiallyExpanded: true) {
      TraceCollapsibleTrigger {
        TraceLine {
          TraceIcon { Ph.gitCommit.regular }
          TraceTitle("Committed a1b2c3: Fix the login redirect")
        }
      }
      TraceCollapsibleContent {
        Trace {
          TraceLine {
            TraceIcon { Ph.notePencil.regular }
            TraceTitle(verbatim: "app/login/page.tsx")
            TraceDescription(verbatim: "+12 −4")
          }
          TraceLine {
            TraceIcon { Ph.notePencil.regular }
            TraceTitle(verbatim: "lib/auth/redirect.ts")
            TraceDescription(verbatim: "+31 −9")
          }
          TraceLine {
            TraceIcon { Ph.filePlus.regular }
            TraceTitle(verbatim: "lib/auth/redirect.test.ts")
            TraceDescription(verbatim: "+58")
          }
        }
      }
    }
  }
}
```

```kotlin
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.vector.ImageVector
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.regular.FilePlus
import com.adamglin.phosphoricons.regular.GitCommit
import com.adamglin.phosphoricons.regular.NotePencil
import com.esuiss.ui.components.Icon
import com.esuiss.ui.components.Trace
import com.esuiss.ui.components.TraceCollapsible
import com.esuiss.ui.components.TraceCollapsibleContent
import com.esuiss.ui.components.TraceCollapsibleTrigger
import com.esuiss.ui.components.TraceDescription
import com.esuiss.ui.components.TraceIcon
import com.esuiss.ui.components.TraceLine
import com.esuiss.ui.components.TraceStatus
import com.esuiss.ui.components.TraceTitle

@Composable
fun TraceRow(icon: ImageVector, title: String, status: TraceStatus? = null, description: String? = null) {
    TraceLine(status = status) {
        TraceIcon { Icon(icon, contentDescription = null) }
        TraceTitle(title)
        if (description != null) TraceDescription(description)
    }
}

@Composable
fun TraceCommit() {
    TraceCollapsible(initiallyExpanded = true) {
        TraceCollapsibleTrigger {
            TraceLine {
                TraceIcon { Icon(PhosphorIcons.Regular.GitCommit, contentDescription = null) }
                TraceTitle("Committed a1b2c3: Fix the login redirect")
            }
        }
        TraceCollapsibleContent {
            Trace {
                TraceRow(PhosphorIcons.Regular.NotePencil, "app/login/page.tsx", description = "+12 −4")
                TraceRow(PhosphorIcons.Regular.NotePencil, "lib/auth/redirect.ts", description = "+31 −9")
                TraceRow(PhosphorIcons.Regular.FilePlus, "lib/auth/redirect.test.ts", description = "+58")
            }
        }
    }
}
```

### Whole turn

Three levels: the turn, a summary and the rows, with text between the steps.

```tsx
import { BrainIcon, FolderOpenIcon, TerminalIcon } from "phosphor-react-native"
import { Icon } from "@/components/ui/icon"
import { Text } from "@/components/ui/text"
import { Trace, TraceCollapsible, TraceCollapsibleContent, TraceCollapsibleTrigger, TraceDescription, TraceIcon, TraceLine, TraceTitle } from "@/components/ui/trace"

export function TraceTurnExample() {
  return (
    <TraceCollapsible defaultOpen>
      <TraceCollapsibleTrigger>
        <TraceLine status="error">
          <TraceTitle>Worked for 2m 4s, 1 command failed</TraceTitle>
        </TraceLine>
      </TraceCollapsibleTrigger>
      <TraceCollapsibleContent keepMounted>
        <Trace>
          <TraceLine>
            <TraceIcon>
              <Icon as={BrainIcon} />
            </TraceIcon>
            <TraceTitle>Thought</TraceTitle>
            <TraceDescription>12s</TraceDescription>
          </TraceLine>
          <TraceLine>
            <TraceIcon>
              <Icon as={TerminalIcon} />
            </TraceIcon>
            <TraceTitle>Ran node --version</TraceTitle>
          </TraceLine>
          <TraceLine status="error">
            <TraceIcon>
              <Icon as={TerminalIcon} />
            </TraceIcon>
            <TraceTitle>Failed to run pnpm --version</TraceTitle>
          </TraceLine>
        </Trace>
        <Text className="my-2 text-foreground">
          Node is installed and pnpm is not. Reading the docs config.
        </Text>
        <Trace>
          <TraceLine>
            <TraceIcon>
              <Icon as={FolderOpenIcon} />
            </TraceIcon>
            <TraceTitle>Read apps/web/lib/docs.ts</TraceTitle>
          </TraceLine>
        </Trace>
      </TraceCollapsibleContent>
    </TraceCollapsible>
  )
}
```

```swift
import EsuissUI
import PhosphorSwift
import SwiftUI

struct TraceTurnExample: View {
  var body: some View {
    TraceCollapsible(initiallyExpanded: true) {
      TraceCollapsibleTrigger {
        TraceLine(status: .error) { TraceTitle("Worked for 2m 4s, 1 command failed") }
      }
      TraceCollapsibleContent {
        Trace {
          TraceLine {
            TraceIcon { Ph.brain.regular }
            TraceTitle("Thought")
            TraceDescription(verbatim: "12s")
          }
          TraceLine {
            TraceIcon { Ph.terminal.regular }
            TraceTitle("Ran node --version")
          }
          TraceLine(status: .error) {
            TraceIcon { Ph.terminal.regular }
            TraceTitle("Failed to run pnpm --version")
          }
        }
        Text("Node is installed and pnpm is not. Reading the docs config.")
          .foregroundStyle(EsuissColor.foreground)
        Trace {
          TraceLine {
            TraceIcon { Ph.folderOpen.regular }
            TraceTitle(verbatim: "Read apps/web/lib/docs.ts")
          }
        }
      }
    }
  }
}
```

```kotlin
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.vector.ImageVector
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.regular.Brain
import com.adamglin.phosphoricons.regular.FolderOpen
import com.adamglin.phosphoricons.regular.Terminal
import com.esuiss.ui.components.Icon
import com.esuiss.ui.components.Trace
import com.esuiss.ui.components.TraceCollapsible
import com.esuiss.ui.components.TraceCollapsibleContent
import com.esuiss.ui.components.TraceCollapsibleTrigger
import com.esuiss.ui.components.TraceDescription
import com.esuiss.ui.components.TraceIcon
import com.esuiss.ui.components.TraceLine
import com.esuiss.ui.components.TraceStatus
import com.esuiss.ui.components.TraceTitle
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Typography

@Composable
fun TraceRow(icon: ImageVector, title: String, status: TraceStatus? = null, description: String? = null) {
    TraceLine(status = status) {
        TraceIcon { Icon(icon, contentDescription = null) }
        TraceTitle(title)
        if (description != null) TraceDescription(description)
    }
}

@Composable
fun TraceTurn() {
    TraceCollapsible(initiallyExpanded = true) {
        TraceCollapsibleTrigger {
            TraceLine(status = TraceStatus.Error) { TraceTitle("Worked for 2m 4s, 1 command failed") }
        }
        TraceCollapsibleContent {
            Trace {
                TraceRow(PhosphorIcons.Regular.Brain, "Thought", description = "12s")
                TraceRow(PhosphorIcons.Regular.Terminal, "Ran node --version")
                TraceRow(PhosphorIcons.Regular.Terminal, "Failed to run pnpm --version", TraceStatus.Error)
            }
            BasicText(
                "Node is installed and pnpm is not. Reading the docs config.",
                style = Typography.sm.copy(color = EsuissTheme.colors.foreground),
            )
            Trace { TraceRow(PhosphorIcons.Regular.FolderOpen, "Read apps/web/lib/docs.ts") }
        }
    }
}
```

### Reasoning

A thinking row that expands into the reasoning text.

```tsx
import { BrainIcon } from "phosphor-react-native"
import { Icon } from "@/components/ui/icon"
import { Text } from "@/components/ui/text"
import { TraceCollapsible, TraceCollapsibleContent, TraceCollapsibleTrigger, TraceIcon, TraceLine, TraceTitle } from "@/components/ui/trace"

export function TraceReasoningExample() {
  return (
    <TraceCollapsible defaultOpen>
      <TraceCollapsibleTrigger>
        <TraceLine>
          <TraceIcon>
            <Icon as={BrainIcon} />
          </TraceIcon>
          <TraceTitle>Thought</TraceTitle>
        </TraceLine>
      </TraceCollapsibleTrigger>
      <TraceCollapsibleContent>
        <Text>
          The build log shows install and compile finish in under a minute.
          Image optimization takes almost four, and it reruns on every deploy
          because the cache key includes the commit hash.
        </Text>
      </TraceCollapsibleContent>
    </TraceCollapsible>
  )
}
```

```swift
import EsuissUI
import PhosphorSwift
import SwiftUI

struct TraceReasoningExample: View {
  var body: some View {
    TraceCollapsible(initiallyExpanded: true) {
      TraceCollapsibleTrigger {
        TraceLine {
          TraceIcon { Ph.brain.regular }
          TraceTitle("Thought")
        }
      }
      TraceCollapsibleContent {
        Text("The build log shows install and compile finish in under a minute. Image optimization takes almost four, and it reruns on every deploy because the cache key includes the commit hash.")
      }
    }
  }
}
```

```kotlin
import androidx.compose.runtime.Composable
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.regular.Brain
import com.esuiss.ui.components.Icon
import com.esuiss.ui.components.Text
import com.esuiss.ui.components.TraceCollapsible
import com.esuiss.ui.components.TraceCollapsibleContent
import com.esuiss.ui.components.TraceCollapsibleTrigger
import com.esuiss.ui.components.TraceIcon
import com.esuiss.ui.components.TraceLine
import com.esuiss.ui.components.TraceTitle

@Composable
fun TraceReasoning() {
    TraceCollapsible(initiallyExpanded = true) {
        TraceCollapsibleTrigger {
            TraceLine {
                TraceIcon { Icon(PhosphorIcons.Regular.Brain, contentDescription = null) }
                TraceTitle("Thought")
            }
        }
        TraceCollapsibleContent {
            Text("The build log shows install and compile finish in under a minute. Image optimization takes almost four, and it reruns on every deploy because the cache key includes the commit hash.")
        }
    }
}
```

### Tool calls

Tool calls with their output, including a failed call.

```tsx
import { FileTextIcon, TerminalIcon, WarningCircleIcon } from "phosphor-react-native"
import { CodeBlock, CodeBlockContent } from "@/components/ui/code-block"
import { Icon } from "@/components/ui/icon"
import { Trace, TraceCollapsible, TraceCollapsibleContent, TraceCollapsibleTrigger, TraceIcon, TraceLine, TraceTitle } from "@/components/ui/trace"

function Output({ children, error }: { children: string; error?: boolean }) {
  return (
    <CodeBlock size="sm" variant={error ? "destructive" : "default"}>
      <CodeBlockContent wrap>{children}</CodeBlockContent>
    </CodeBlock>
  )
}

const calls = [
  {
    title: "Read package.json",
    icon: FileTextIcon,
    output: '{ "name": "acme", "packageManager": "pnpm@10.18.0" }',
  },
  {
    title: "Failed to run pnpm install",
    icon: TerminalIcon,
    error: "ERR_PNPM_OUTDATED_LOCKFILE pnpm-lock.yaml is out of date",
  },
]

export function TraceToolCallsExample() {
  return (
    <Trace>
      {calls.map((call) => (
        <TraceCollapsible key={call.title}>
          <TraceCollapsibleTrigger>
            <TraceLine status={call.error ? "error" : undefined}>
              <TraceIcon>
                <Icon as={call.error ? WarningCircleIcon : call.icon} />
              </TraceIcon>
              <TraceTitle>{call.title}</TraceTitle>
            </TraceLine>
          </TraceCollapsibleTrigger>
          <TraceCollapsibleContent>
            <Output error={Boolean(call.error)}>
              {call.error ?? call.output ?? ""}
            </Output>
          </TraceCollapsibleContent>
        </TraceCollapsible>
      ))}
    </Trace>
  )
}
```

```swift
import EsuissUI
import PhosphorSwift
import SwiftUI

struct TraceToolCallsExample: View {
  var body: some View {
    Trace {
      TraceCollapsible {
        TraceCollapsibleTrigger {
          TraceLine {
            TraceIcon { Ph.fileText.regular }
            TraceTitle(verbatim: "Read package.json")
          }
        }
        TraceCollapsibleContent {
          TraceOutput(text: "{ \"name\": \"acme\", \"packageManager\": \"pnpm@10.18.0\" }")
        }
      }
      TraceCollapsible {
        TraceCollapsibleTrigger {
          TraceLine(status: .error) {
            TraceIcon { Ph.warningCircle.regular }
            TraceTitle("Failed to run pnpm install")
          }
        }
        TraceCollapsibleContent {
          TraceOutput(text: "ERR_PNPM_OUTDATED_LOCKFILE pnpm-lock.yaml is out of date", error: true)
        }
      }
    }
  }
}

struct TraceOutput: View {
  let text: String
  var error = false

  var body: some View {
    CodeBlock(error ? .destructive : .default, size: .sm) {
      CodeBlockContent(text, wrap: true)
    }
  }
}
```

```kotlin
import androidx.compose.runtime.Composable
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.regular.FileText
import com.adamglin.phosphoricons.regular.WarningCircle
import com.esuiss.ui.components.CodeBlock
import com.esuiss.ui.components.CodeBlockContent
import com.esuiss.ui.components.CodeBlockSize
import com.esuiss.ui.components.CodeBlockVariant
import com.esuiss.ui.components.Icon
import com.esuiss.ui.components.Trace
import com.esuiss.ui.components.TraceCollapsible
import com.esuiss.ui.components.TraceCollapsibleContent
import com.esuiss.ui.components.TraceCollapsibleTrigger
import com.esuiss.ui.components.TraceIcon
import com.esuiss.ui.components.TraceLine
import com.esuiss.ui.components.TraceStatus
import com.esuiss.ui.components.TraceTitle

@Composable
fun TraceOutput(text: String, error: Boolean = false) {
    CodeBlock(
        variant = if (error) CodeBlockVariant.Destructive else CodeBlockVariant.Default,
        size = CodeBlockSize.Sm,
    ) {
        CodeBlockContent(text, wrap = true)
    }
}

@Composable
fun TraceToolCalls() {
    Trace {
        TraceCollapsible {
            TraceCollapsibleTrigger {
                TraceLine {
                    TraceIcon { Icon(PhosphorIcons.Regular.FileText, contentDescription = null) }
                    TraceTitle("Read package.json")
                }
            }
            TraceCollapsibleContent {
                TraceOutput("{ \"name\": \"acme\", \"packageManager\": \"pnpm@10.18.0\" }")
            }
        }
        TraceCollapsible {
            TraceCollapsibleTrigger {
                TraceLine(status = TraceStatus.Error) {
                    TraceIcon { Icon(PhosphorIcons.Regular.WarningCircle, contentDescription = null) }
                    TraceTitle("Failed to run pnpm install")
                }
            }
            TraceCollapsibleContent {
                TraceOutput("ERR_PNPM_OUTDATED_LOCKFILE pnpm-lock.yaml is out of date", error = true)
            }
        }
    }
}
```

## API Reference

### Trace

A column of rows.

Also accepts the props of `ViewProps`.

### TraceLine

One row: an icon, a title and an optional description.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `status` | `"running" \| "error" \| "pending"` | — | The state of the work the row reports. |

Also accepts the props of `ViewProps`.

### TraceIcon

A 16 pt icon slot, hidden from assistive technology.

Also accepts the props of `ViewProps`.

### TraceTitle

One line of text that truncates. It pulses while the row is running unless the reader prefers reduced motion.

Also accepts the props of `Text`.

### TraceDescription

Secondary text after the title, with tabular numbers.

Also accepts the props of `Text`.

### TraceCollapsible

A row that expands.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `open` | `boolean` | — | The controlled open state. Pass it with `onOpenChange`. |
| `defaultOpen` | `boolean` | `false` | Whether the row starts open. |
| `onOpenChange` | `(open: boolean) => void` | — | Called when the open state changes. |

### TraceCollapsibleTrigger

The pressable row that opens and closes the detail. It adds a chevron after the line.

### TraceCollapsibleContent

The detail of a row, indented under the title with a leading rule.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `keepMounted` | `boolean` | `false` | Keeps the detail mounted while closed, so nested rows keep their state. |

## Accessibility

- The trigger is a button whose state reads as expanded or collapsed.
- `TraceIcon` is hidden from assistive technology, so write the title as a sentence that says what happened.
- Status is shown with color and motion, so put it in the title too.

