- Installation
- Usage
- Examples
- Trace Line
- Status
- Description
- Collapsible
- Open State
- Link
- Dialog
- Sheet
- Group
- Sources
- Chain of Thought
- Commit
- Whole Turn
- Reasoning
- Tool
- RTL
- Accessibility
- API Reference
-
Trace -
Trace:Collapsible -
Trace:Collapsible:Content -
Trace:Collapsible:Trigger -
Trace:Description -
Trace:Icon -
Trace:Line -
Trace:Title
Trace
The building block for reasoning, tool calls, sources and more.
<div class="">
<twig:Message align="end">
<twig:Message:Content>
<twig:Bubble align="end" variant="muted">
<twig:Bubble:Content>Why does pnpm install fail in CI?</twig:Bubble:Content>
</twig:Bubble>
</twig:Message:Content>
</twig:Message>
<twig:Message>
<twig:Message:Content class="">
<twig:Trace>
<twig:Trace:Collapsible>
<twig:Trace:Collapsible:Trigger>
<twig:Trace:Line>
<twig:Trace:Icon>
<twig:ux:icon name="lucide:circle-check" />
</twig:Trace:Icon>
<twig:Trace:Title>Thought for 5s</twig:Trace:Title>
</twig:Trace:Line>
</twig:Trace:Collapsible:Trigger>
<twig:Trace:Collapsible:Content class="">
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.
</twig:Trace:Collapsible:Content>
</twig:Trace:Collapsible>
<twig:Trace:Collapsible>
<twig:Trace:Collapsible:Trigger>
<twig:Trace:Line>
<twig:Trace:Icon>
<twig:ux:icon name="lucide:circle-check" />
</twig:Trace:Icon>
<twig:Trace:Title>Ran pnpm --version</twig:Trace:Title>
</twig:Trace:Line>
</twig:Trace:Collapsible:Trigger>
<twig:Trace:Collapsible:Content>
<twig:CodeBlock size="sm">
<twig:CodeBlock:Content wrap class="">10.18.0</twig:CodeBlock:Content>
</twig:CodeBlock>
</twig:Trace:Collapsible:Content>
</twig:Trace:Collapsible>
<twig:Trace:Collapsible>
<twig:Trace:Collapsible:Trigger>
<twig:Trace:Line>
<twig:Trace:Icon>
<twig:ux:icon name="lucide:circle-check" />
</twig:Trace:Icon>
<twig:Trace:Title>Ran git diff --stat HEAD~1 -- package.json pnpm-lock.yaml</twig:Trace:Title>
</twig:Trace:Line>
</twig:Trace:Collapsible:Trigger>
<twig:Trace:Collapsible:Content>
<twig:CodeBlock size="sm">
<twig:CodeBlock:Content wrap class=""> package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)</twig:CodeBlock:Content>
</twig:CodeBlock>
</twig:Trace:Collapsible:Content>
</twig:Trace:Collapsible>
<twig:Trace:Collapsible open>
<twig:Trace:Collapsible:Trigger>
<twig:Trace:Line status="error">
<twig:Trace:Icon>
<twig:ux:icon name="lucide:circle-alert" />
</twig:Trace:Icon>
<twig:Trace:Title>Failed to run pnpm install --frozen-lockfile</twig:Trace:Title>
</twig:Trace:Line>
</twig:Trace:Collapsible:Trigger>
<twig:Trace:Collapsible:Content>
<twig:CodeBlock size="sm" variant="destructive">
<twig:CodeBlock:Content wrap class="">ERR_PNPM_OUTDATED_LOCKFILE Cannot install with "frozen-lockfile" because pnpm-lock.yaml is not up to date with package.json</twig:CodeBlock:Content>
</twig:CodeBlock>
</twig:Trace:Collapsible:Content>
</twig:Trace:Collapsible>
<twig:Trace:Collapsible>
<twig:Trace:Collapsible:Trigger>
<twig:Trace:Line>
<twig:Trace:Icon>
<twig:ux:icon name="lucide:globe" />
</twig:Trace:Icon>
<twig:Trace:Title>Used 2 sources</twig:Trace:Title>
</twig:Trace:Line>
</twig:Trace:Collapsible:Trigger>
<twig:Trace:Collapsible:Content>
<twig:Trace>
{% for source in [
{title: 'pnpm install | pnpm.io', url: 'https://pnpm.io/cli/install#--frozen-lockfile'},
{title: 'Continuous Integration | pnpm.io', url: 'https://pnpm.io/continuous-integration'},
] %}
<a href="{{ source.url }}" target="_blank" rel="noreferrer">
<twig:Trace:Line>
<twig:Trace:Icon>
<twig:ux:icon name="lucide:globe" />
</twig:Trace:Icon>
<twig:Trace:Title>
{{ source.title }}
<span class=""> (opens in a new tab)</span>
</twig:Trace:Title>
<twig:Trace:Icon>
<twig:ux:icon name="lucide:arrow-up-right" />
</twig:Trace:Icon>
</twig:Trace:Line>
</a>
{% endfor %}
</twig:Trace>
</twig:Trace:Collapsible:Content>
</twig:Trace:Collapsible>
</twig: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>
</twig:Message:Content>
</twig:Message>
</div>
Installation
Available since UX Toolkit 3.7.
php bin/console ux:install trace --kit shadcn
Install the following Composer dependencies:
composer require twig/extra-bundle twig/html-extra:^3.24.0 symfony/ux-twig-component:^3.5 tales-from-a-dev/twig-tailwind-extra:^1.3.0 symfony/ux-icons
Install the following front-end dependencies:
npm install shadcn tw-animate-css
# ...or with Symfony AssetMapper
php bin/console importmap:require tw-animate-css/dist/tw-animate.css shadcn/dist/tailwind.css
Copy the following file(s) into your app:
<div
data-slot="trace"
{{ attributes.defaults({
class: 'flex w-full min-w-0 flex-col gap-2 *: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]]:rounded-md *:has-[>[data-slot=trace-line]]:text-start *:has-[>[data-slot=trace-line]]:outline-none *:has-[>[data-slot=trace-line]]:focus-visible:ring-3 *:has-[>[data-slot=trace-line]]:focus-visible:ring-ring/50 *:*: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]]:rounded-md *:*:has-[>[data-slot=trace-line]]:text-start *:*:has-[>[data-slot=trace-line]]:outline-none *:*:has-[>[data-slot=trace-line]]:focus-visible:ring-3 *:*:has-[>[data-slot=trace-line]]:focus-visible:ring-ring/50'|tailwind_classes,
}) }}
>
{##- The rows of the trace: `Trace:Line`, `Trace:Collapsible`, or a link or trigger wrapping a `Trace:Line`. -#}
{%- block content %}{% endblock -%}
</div>
{%- props
## boolean Whether the row is open on initial render.
open = false
-%}
{%- do provide('collapsible.open', open) -%}
<div
data-slot="trace-collapsible"
data-collapsible-open-value="{{ open ? 'true' : 'false' }}"
data-state="{{ open ? 'open' : 'closed' }}"
{{ attributes.defaults({
class: 'flex w-full min-w-0 flex-col'|tailwind_classes,
'data-controller': 'collapsible',
}) }}
>
{##- The row, typically includes `Trace:Collapsible:Trigger` and `Trace:Collapsible:Content`. -#}
{%- block content %}{% endblock -%}
</div>
{%- set _collapsible_open = inject('collapsible.open', false) -%}
<div
data-slot="trace-collapsible-content"
data-collapsible-target="content"
aria-hidden="{{ not _collapsible_open ? 'true' : 'false' }}"
data-state="{{ _collapsible_open ? 'open' : 'closed' }}"
class="min-w-0 overflow-hidden"
{{ not _collapsible_open ? 'hidden' }}
{{ attributes.without('class') }}
>
<div
data-slot="trace-collapsible-content-inner"
class="{{ ('mt-2 mb-1 ms-[calc(--spacing(2)-1px)] min-w-0 border-s pe-1 ps-4 leading-relaxed wrap-break-word text-muted-foreground ' ~ attributes.render('class'))|tailwind_merge }}"
>
{##- The detail revealed when the row is open. -#}
{%- block content %}{% endblock -%}
</div>
</div>
{%- set _collapsible_open = inject('collapsible.open', false) -%}
<button
type="button"
data-slot="trace-collapsible-trigger"
data-collapsible-target="trigger"
aria-expanded="{{ _collapsible_open ? 'true' : 'false' }}"
data-state="{{ _collapsible_open ? 'open' : 'closed' }}"
{{ attributes.defaults({
class: 'group/trace-collapsible-trigger flex w-fit max-w-full min-w-0 cursor-pointer items-center gap-1 rounded-md text-start text-muted-foreground transition-colors outline-none hover:text-foreground focus-visible:ring-3 focus-visible:ring-ring/50 has-[[data-status=error]]:text-destructive'|tailwind_classes,
'data-action': 'click->collapsible#toggle'|html_attr_type('sst'),
}) }}
>
{##- The `Trace:Line` that opens and closes the row. -#}
{%- block content %}{% endblock -%}
<twig:Trace:Icon class="transition-transform rtl:rotate-180 group-aria-expanded/trace-collapsible-trigger:rotate-90">
<twig:ux:icon name="lucide:chevron-right" />
</twig:Trace:Icon>
</button>
<span
data-slot="trace-description"
{{ attributes.defaults({
class: 'shrink-0 text-muted-foreground/70 tabular-nums'|tailwind_classes,
}) }}
>
{##- Secondary text, such as a count or a duration. -#}
{%- block content %}{% endblock -%}
</span>
<span
data-slot="trace-icon"
aria-hidden="true"
{{ attributes.defaults({
class: "flex size-4 shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [[data-slot=trace-title]~&]:-ms-1"|tailwind_classes,
}) }}
>
{##- The decorative icon. Before the title it names the action, after it is the indicator. -#}
{%- block content %}{% endblock -%}
</span>
{%- props
## 'running'|'error'|'pending'|null The state of the work the line reports.
status = null
-%}
<span
data-slot="trace-line"
{% if status %}data-status="{{ status }}"{% endif %}
{{ attributes.defaults({
class: 'flex w-fit max-w-full min-w-0 items-center gap-2 text-sm 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'|tailwind_classes,
}) }}
>
{##- The line content, typically includes `Trace:Icon`, `Trace:Title` and optionally `Trace:Description`. -#}
{%- block content %}{% endblock -%}
</span>
<span
data-slot="trace-title"
{{ attributes.defaults({
class: 'min-w-0 truncate motion-reduce:shimmer-none'|tailwind_classes,
}) }}
>
{##- One sentence that says what happened. -#}
{%- block content %}{% endblock -%}
</span>
import { Controller } from '@hotwired/stimulus';
export default class extends Controller {
static targets = ['trigger', 'content'];
static values = {
open: { type: Boolean, default: false },
};
connect() {
this.updateState();
}
toggle() {
this.openValue = !this.openValue;
}
openValueChanged() {
this.updateState();
}
updateState() {
const open = this.openValue;
const state = open ? 'open' : 'closed';
this.element.dataset.state = state;
for (const trigger of this.triggerTargets) {
trigger.setAttribute('aria-expanded', String(open));
trigger.dataset.state = state;
}
for (const content of this.contentTargets) {
content.dataset.state = state;
content.setAttribute('aria-hidden', String(!open));
if (open) {
content.removeAttribute('hidden');
} else {
content.setAttribute('hidden', '');
}
}
}
}
{%- props
## boolean Whether the collapsible is open by default.
open = false
-%}
{%- do provide('collapsible.open', open) -%}
<div
data-slot="collapsible"
data-collapsible-open-value="{{ open ? 'true' : 'false' }}"
data-state="{{ open ? 'open' : 'closed' }}"
{{ attributes.defaults({
'data-controller': 'collapsible',
}) }}
>
{##- The collapsible content, typically a `Collapsible:Trigger` and a `Collapsible:Content`. -#}
{%- block content %}{% endblock -%}
</div>
{%- set _collapsible_open = inject('collapsible.open', false) -%}
<div
data-slot="collapsible-content"
data-collapsible-target="content"
aria-hidden="{{ not _collapsible_open ? 'true' : 'false' }}"
data-state="{{ _collapsible_open ? 'open' : 'closed' }}"
{{ not _collapsible_open ? 'hidden' }}
{{ attributes }}
>
{##- The content revealed when the collapsible is open. -#}
{%- block content %}{% endblock -%}
</div>
{%- set _collapsible_open = inject('collapsible.open', false) -%}
{%- set collapsible_trigger_attrs = {
'data-slot': 'collapsible-trigger',
'data-collapsible-target': 'trigger',
'data-action': 'click->collapsible#toggle'|html_attr_type('sst'),
'aria-expanded': _collapsible_open ? 'true' : 'false',
'data-state': _collapsible_open ? 'open' : 'closed',
} -%}
{##- The clickable element that toggles the collapsible (e.g., a `Button`). -#}
{%- block content %}{% endblock -%}
Usage
Use a Trace:Line for a single step:
<twig:Trace>
<twig:Trace:Line>
<twig:Trace:Icon>
<twig:ux:icon name="lucide:search" />
</twig:Trace:Icon>
<twig:Trace:Title>Searched for minimumReleaseAge</twig:Trace:Title>
</twig:Trace:Line>
</twig:Trace>
Wrap the line in a Trace:Collapsible to show more detail when it expands:
<twig:Trace>
<twig:Trace:Collapsible>
<twig:Trace:Collapsible:Trigger>
<twig:Trace:Line>
<twig:Trace:Icon>
<twig:ux:icon name="lucide:terminal" />
</twig:Trace:Icon>
<twig:Trace:Title>Ran node --version</twig:Trace:Title>
</twig:Trace:Line>
</twig:Trace:Collapsible:Trigger>
<twig:Trace:Collapsible:Content>v24.18.0</twig:Trace:Collapsible:Content>
</twig:Trace:Collapsible>
</twig:Trace>
Examples
Trace Line
Use a Trace:Line on its own for a step with no detail to show. It reads as plain text.
<twig:Trace class="max-w-sm">
<twig:Trace:Line>
<twig:Trace:Icon>
<twig:ux:icon name="lucide:git-branch" />
</twig:Trace:Icon>
<twig:Trace:Title>Created branch fix/frozen-lockfile</twig:Trace:Title>
</twig:Trace:Line>
</twig:Trace>
Status
Set status on Trace:Line when a step is running, has failed or hasn't started yet.
<twig:Trace class="max-w-sm">
<twig:Trace:Line>
<twig:Trace:Icon>
<twig:ux:icon name="lucide:circle-check" />
</twig:Trace:Icon>
<twig:Trace:Title>Ran pnpm install</twig:Trace:Title>
</twig:Trace:Line>
<twig:Trace:Line status="error">
<twig:Trace:Icon>
<twig:ux:icon name="lucide:circle-alert" />
</twig:Trace:Icon>
<twig:Trace:Title>Failed to run pnpm lint</twig:Trace:Title>
</twig:Trace:Line>
<twig:Trace:Line status="running">
<twig:Trace:Icon>
<twig:ux:icon name="lucide:loader-circle" class="animate-spin" />
</twig:Trace:Icon>
<twig:Trace:Title>Running pnpm build</twig:Trace:Title>
</twig:Trace:Line>
<twig:Trace:Line status="pending">
<twig:Trace:Icon>
<twig:ux:icon name="lucide:circle" />
</twig:Trace:Icon>
<twig:Trace:Title>Run pnpm test</twig:Trace:Title>
</twig:Trace:Line>
</twig:Trace>
Description
Put secondary text such as a diff stat, a duration or a count in a Trace:Description after the Trace:Title. It does not truncate.
<twig:Trace class="max-w-sm">
<twig:Trace:Line>
<twig:Trace:Icon>
<twig:ux:icon name="lucide:file-pen" />
</twig:Trace:Icon>
<twig:Trace:Title>Edited components/settings/notification-preferences-form.tsx</twig:Trace:Title>
<twig:Trace:Description class="font-mono">+48 −12</twig:Trace:Description>
</twig:Trace:Line>
<twig:Trace:Line>
<twig:Trace:Icon>
<twig:ux:icon name="lucide:terminal" />
</twig:Trace:Icon>
<twig:Trace:Title>Ran pnpm test</twig:Trace:Title>
<twig:Trace:Description>14s</twig:Trace:Description>
</twig:Trace:Line>
<twig:Trace:Line>
<twig:Trace:Icon>
<twig:ux:icon name="lucide:search" />
</twig:Trace:Icon>
<twig:Trace:Title>Searched for useChat</twig:Trace:Title>
<twig:Trace:Description>8 results</twig:Trace:Description>
</twig:Trace:Line>
</twig:Trace>
Collapsible
Wrap the line in a Trace:Collapsible to show more detail when it expands. Put the Trace:Line inside Trace:Collapsible:Trigger and the detail in Trace:Collapsible:Content.
<div class="">
<twig:Trace:Collapsible>
<twig:Trace:Collapsible:Trigger>
<twig:Trace:Line>
<twig:Trace:Icon>
<twig:ux:icon name="lucide:terminal" />
</twig:Trace:Icon>
<twig:Trace:Title>Ran node --version</twig:Trace:Title>
</twig:Trace:Line>
</twig:Trace:Collapsible:Trigger>
<twig:Trace:Collapsible:Content>
<twig:CodeBlock size="sm">
<twig:CodeBlock:Content wrap class="">$ node --version
v24.18.0</twig:CodeBlock:Content>
</twig:CodeBlock>
</twig:Trace:Collapsible:Content>
</twig:Trace:Collapsible>
</div>
Open State
Rows start closed. Set open on Trace:Collapsible to start open. To toggle a row from another button inside it, wrap that button in Collapsible:Trigger and spread collapsible_trigger_attrs on it. That button then also reflects the open state, through its aria-expanded and data-state attributes.
<div class="">
<twig:Trace:Collapsible open>
<twig:Trace:Collapsible:Trigger>
<twig:Trace:Line>
<twig:Trace:Icon>
<twig:ux:icon name="lucide:terminal" />
</twig:Trace:Icon>
<twig:Trace:Title>Ran node --version</twig:Trace:Title>
</twig:Trace:Line>
</twig:Trace:Collapsible:Trigger>
<twig:Trace:Collapsible:Content>
<twig:CodeBlock size="sm">
<twig:CodeBlock:Content wrap class="">$ node --version
v24.18.0</twig:CodeBlock:Content>
</twig:CodeBlock>
</twig:Trace:Collapsible:Content>
</twig:Trace:Collapsible>
<twig:Trace:Collapsible>
<twig:Trace:Collapsible:Trigger>
<twig:Trace:Line>
<twig:Trace:Icon>
<twig:ux:icon name="lucide:terminal" />
</twig:Trace:Icon>
<twig:Trace:Title>Ran pnpm --version</twig:Trace:Title>
</twig:Trace:Line>
</twig:Trace:Collapsible:Trigger>
<twig:Trace:Collapsible:Content>
<twig:CodeBlock size="sm">
<twig:CodeBlock:Content wrap class="">$ pnpm --version
10.33.4</twig:CodeBlock:Content>
</twig:CodeBlock>
</twig:Trace:Collapsible:Content>
<twig:Collapsible:Trigger>
<twig:Button variant="outline" size="sm" class="" {{ ...collapsible_trigger_attrs }}>
<span class="">Show</span>
<span class="">Hide</span>
the pnpm output
</twig:Button>
</twig:Collapsible:Trigger>
</twig:Trace:Collapsible>
</div>
Link
Wrap the Trace:Line in an anchor. For a new tab, use an arrow-up-right icon and sr-only "(opens in a new tab)".
<twig:Trace class="max-w-sm">
<a href="#link">
<twig:Trace:Line>
<twig:Trace:Icon>
<twig:ux:icon name="lucide:file-text" />
</twig:Trace:Icon>
<twig:Trace:Title>Read apps/v4/lib/docs.ts</twig:Trace:Title>
<twig:Trace:Icon>
<twig:ux:icon name="lucide:chevron-right" class="rtl:rotate-180" />
</twig:Trace:Icon>
</twig:Trace:Line>
</a>
<a href="https://docs.astro.build/en/install-and-setup/" target="_blank" rel="noreferrer">
<twig:Trace:Line>
<twig:Trace:Icon>
<twig:ux:icon name="lucide:globe" />
</twig:Trace:Icon>
<twig:Trace:Title>
Install Astro | docs.astro.build
<span class="sr-only"> (opens in a new tab)</span>
</twig:Trace:Title>
<twig:Trace:Icon>
<twig:ux:icon name="lucide:arrow-up-right" />
</twig:Trace:Icon>
</twig:Trace:Line>
</a>
</twig:Trace>
Dialog
Wrap the Trace:Line in the dialog's trigger. The dialog owns its open state, Escape and focus return.
<div class="w-full max-w-sm" style="min-height: 320px">
<twig:Trace>
<twig:Dialog id="trace-dialog">
<twig:Dialog:Trigger>
<button type="button" {{ html_attr(dialog_trigger_attrs) }}>
<twig:Trace:Line>
<twig:Trace:Icon>
<twig:ux:icon name="lucide:globe" />
</twig:Trace:Icon>
<twig:Trace:Title>Searched the web for cache keys</twig:Trace:Title>
<twig:Trace:Icon>
<twig:ux:icon name="lucide:chevron-right" class="rtl:rotate-180" />
</twig:Trace:Icon>
</twig:Trace:Line>
</button>
</twig:Dialog:Trigger>
<twig:Dialog:Content>
<twig:Dialog:Header>
<twig:Dialog:Title>Searched the web for cache keys</twig:Dialog:Title>
<twig:Dialog:Description>Three results were used in the response.</twig:Dialog:Description>
</twig:Dialog:Header>
<twig:Trace>
{% for result in [
{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'},
] %}
<a href="{{ result.url }}" target="_blank" rel="noreferrer">
<twig:Trace:Line>
<twig:Trace:Icon>
<twig:ux:icon name="lucide:globe" />
</twig:Trace:Icon>
<twig:Trace:Title>
{{ result.title }}
<span class="sr-only"> (opens in a new tab)</span>
</twig:Trace:Title>
<twig:Trace:Icon>
<twig:ux:icon name="lucide:arrow-up-right" />
</twig:Trace:Icon>
</twig:Trace:Line>
</a>
{% endfor %}
</twig:Trace>
</twig:Dialog:Content>
</twig:Dialog>
</twig:Trace>
</div>
Sheet
The same shape as the dialog. Drawer and Popover work the same way.
<div class="w-full max-w-sm" style="min-height: 400px">
<twig:Trace>
<twig:Sheet id="trace-sheet">
<twig:Sheet:Trigger>
<button type="button" {{ html_attr(sheet_trigger_attrs) }}>
<twig:Trace:Line>
<twig:Trace:Icon>
<twig:ux:icon name="lucide:folder-search" />
</twig:Trace:Icon>
<twig:Trace:Title>Explored 4 files</twig:Trace:Title>
<twig:Trace:Icon>
<twig:ux:icon name="lucide:chevron-right" class="rtl:rotate-180" />
</twig:Trace:Icon>
</twig:Trace:Line>
</button>
</twig:Sheet:Trigger>
<twig:Sheet:Content>
<twig:Sheet:Header>
<twig:Sheet:Title>Explored 4 files</twig:Sheet:Title>
<twig:Sheet:Description>Files read while preparing the response.</twig:Sheet:Description>
</twig:Sheet:Header>
<ul class="flex flex-col gap-2 px-6 font-mono text-xs">
<li>apps/v4/lib/docs.ts</li>
<li>apps/v4/lib/components.ts</li>
<li>apps/v4/next.config.mjs</li>
<li>apps/v4/package.json</li>
</ul>
</twig:Sheet:Content>
</twig:Sheet>
</twig:Trace>
</div>
Group
Put rows that sit next to each other in a Trace. It also styles any trigger or link that wraps a Trace:Line.
<twig:Trace class="max-w-sm">
<twig:Trace:Line>
<twig:Trace:Icon>
<twig:ux:icon name="lucide:search" />
</twig:Trace:Icon>
<twig:Trace:Title>Searched for minimumReleaseAge</twig:Trace:Title>
</twig:Trace:Line>
<twig:Trace:Line>
<twig:Trace:Icon>
<twig:ux:icon name="lucide:file-text" />
</twig:Trace:Icon>
<twig:Trace:Title>Read pnpm-workspace.yaml</twig:Trace:Title>
</twig:Trace:Line>
<twig:Trace:Line>
<twig:Trace:Icon>
<twig:ux:icon name="lucide:terminal" />
</twig:Trace:Icon>
<twig:Trace:Title>Ran pnpm install</twig:Trace:Title>
</twig:Trace:Line>
</twig:Trace>
Sources
A collapsible row whose detail is a Trace of link rows.
<div class="w-full max-w-sm">
<twig:Trace:Collapsible open>
<twig:Trace:Collapsible:Trigger>
<twig:Trace:Line>
<twig:Trace:Icon>
<twig:ux:icon name="lucide:globe" />
</twig:Trace:Icon>
<twig:Trace:Title>Used 3 sources</twig:Trace:Title>
</twig:Trace:Line>
</twig:Trace:Collapsible:Trigger>
<twig:Trace:Collapsible:Content>
<twig:Trace>
{% for source in [
{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'},
] %}
<a href="{{ source.href }}" target="_blank" rel="noreferrer">
<twig:Trace:Line>
<twig:Trace:Icon>
<twig:ux:icon name="lucide:globe" />
</twig:Trace:Icon>
<twig:Trace:Title>
{{ source.title }}
<span class="sr-only"> (opens in a new tab)</span>
</twig:Trace:Title>
<twig:Trace:Icon>
<twig:ux:icon name="lucide:arrow-up-right" />
</twig:Trace:Icon>
</twig:Trace:Line>
</a>
{% endfor %}
</twig:Trace>
</twig:Trace:Collapsible:Content>
</twig:Trace:Collapsible>
</div>
Chain of Thought
A chain of thought is a collapsible row with step rows inside. Set the parent's status and title from its children.
<div class="">
<twig:Trace:Collapsible open>
<twig:Trace:Collapsible:Trigger>
<twig:Trace:Line status="running">
<twig:Trace:Title>Looking into the slow deploy</twig:Trace:Title>
</twig:Trace:Line>
</twig:Trace:Collapsible:Trigger>
<twig:Trace:Collapsible:Content>
<twig:Trace>
<twig:Trace:Collapsible>
<twig:Trace:Collapsible:Trigger>
<twig:Trace:Line>
<twig:Trace:Icon>
<twig:ux:icon name="lucide:search" />
</twig:Trace:Icon>
<twig:Trace:Title>Searched for the checkout build log</twig:Trace:Title>
</twig:Trace:Line>
</twig:Trace:Collapsible:Trigger>
<twig:Trace:Collapsible:Content class="">
<twig:Badge variant="secondary">vercel.com</twig:Badge>
<twig:Badge variant="secondary">github.com</twig:Badge>
</twig:Trace:Collapsible:Content>
</twig:Trace:Collapsible>
<a href="#steps">
<twig:Trace:Line>
<twig:Trace:Icon>
<twig:ux:icon name="lucide:file-text" />
</twig:Trace:Icon>
<twig:Trace:Title>Read the last twelve deploys</twig:Trace:Title>
<twig:Trace:Icon>
<twig:ux:icon name="lucide:chevron-right" class="" />
</twig:Trace:Icon>
</twig:Trace:Line>
</a>
<twig:Trace:Collapsible>
<twig:Trace:Collapsible:Trigger>
<twig:Trace:Line status="running">
<twig:Trace:Icon>
<twig:ux:icon name="lucide:search" />
</twig:Trace:Icon>
<twig:Trace:Title>Searching for the cache key</twig:Trace:Title>
</twig:Trace:Line>
</twig:Trace:Collapsible:Trigger>
<twig:Trace:Collapsible:Content>
<twig:CodeBlock size="sm">
<twig:CodeBlock:Content wrap class="">cache key image optimization</twig:CodeBlock:Content>
</twig:CodeBlock>
</twig:Trace:Collapsible:Content>
</twig:Trace:Collapsible>
</twig:Trace>
</twig:Trace:Collapsible:Content>
</twig:Trace:Collapsible>
</div>
Commit
A commit is a collapsible row with a line for each changed file. Say what changed in the title, Added, Edited or Deleted, and put the line counts in the description. Each file opens its diff in a Sheet.
<div class="" style="min-height: 400px">
<twig:Trace>
<twig:Trace:Collapsible open>
<twig:Trace:Collapsible:Trigger>
<twig:Trace:Line>
<twig:Trace:Icon>
<twig:ux:icon name="lucide:git-commit-horizontal" />
</twig:Trace:Icon>
<twig:Trace:Title>Committed a1b2c3: Fix the login redirect</twig:Trace:Title>
</twig:Trace:Line>
</twig:Trace:Collapsible:Trigger>
<twig:Trace:Collapsible:Content>
<twig:Trace>
{% for file in [
{
id: 'login-page',
path: 'app/login/page.tsx',
added: false,
additions: 2,
deletions: 2,
lines: [
{variant: 'hunk', text: '@@ -14,7 +14,7 @@ export default async function LoginPage({'},
{variant: 'default', text: ' searchParams,'},
{variant: 'default', text: '}) {'},
{variant: 'default', text: ' const { next } = await searchParams'},
{variant: 'removed', text: ' const redirectTo ='},
{variant: 'removed', text: ' next && next.startsWith("/") ? next : "/"'},
{variant: 'added', text: ' const redirectTo = safeRedirect(next)'},
{variant: 'added', text: ''},
{variant: 'default', text: ' return <LoginForm redirectTo={redirectTo} />'},
{variant: 'default', text: '}'},
],
},
{
id: 'auth-redirect',
path: 'lib/auth/redirect.ts',
added: true,
additions: 7,
deletions: 0,
lines: [
{variant: 'hunk', text: '@@ -0,0 +1,7 @@'},
{variant: 'added', text: 'export function safeRedirect(next: string | undefined) {'},
{variant: 'added', text: ' if (!next || !next.startsWith("/") || next.startsWith("//")) {'},
{variant: 'added', text: ' return "/"'},
{variant: 'added', text: ' }'},
{variant: 'added', text: ''},
{variant: 'added', text: ' return next'},
{variant: 'added', text: '}'},
],
},
] %}
<twig:Sheet id="trace-commit-{{ file.id }}">
<twig:Sheet:Trigger>
<button type="button" {{ html_attr(sheet_trigger_attrs) }}>
<twig:Trace:Line>
<twig:Trace:Icon>
<twig:ux:icon name="{{ file.added ? 'lucide:file-plus' : 'lucide:file-pen' }}" />
</twig:Trace:Icon>
<twig:Trace:Title>{{ file.added ? 'Added' : 'Edited' }} {{ file.path }}</twig:Trace:Title>
<twig:Trace:Description class="">+{{ file.additions }} −{{ file.deletions }}</twig:Trace:Description>
<twig:Trace:Icon>
<twig:ux:icon name="lucide:chevron-right" class="" />
</twig:Trace:Icon>
</twig:Trace:Line>
</button>
</twig:Sheet:Trigger>
<twig:Sheet:Content>
<twig:Sheet:Header>
<twig:Sheet:Title class="">{{ file.path }}</twig:Sheet:Title>
<twig:Sheet:Description>{{ file.additions }} additions, {{ file.deletions }} deletions</twig:Sheet:Description>
</twig:Sheet:Header>
<twig:CodeBlock class="">
<twig:CodeBlock:Content>
{%- for line in file.lines -%}
<twig:CodeBlock:Line variant="{{ line.variant == 'hunk' ? 'default' : line.variant }}" class="">{{ line.text }}</twig:CodeBlock:Line>
{%- endfor -%}
</twig:CodeBlock:Content>
</twig:CodeBlock>
</twig:Sheet:Content>
</twig:Sheet>
{% endfor %}
</twig:Trace>
</twig:Trace:Collapsible:Content>
</twig:Trace:Collapsible>
</twig:Trace>
</div>
Whole Turn
Three levels: the turn, a summary, and the rows. Text between steps sits between two Trace lists.
<div class="">
<twig:Trace:Collapsible open>
<twig:Trace:Collapsible:Trigger>
<twig:Trace:Line status="error">
<twig:Trace:Title>Worked for 2m 4s, 1 command failed</twig:Trace:Title>
</twig:Trace:Line>
</twig:Trace:Collapsible:Trigger>
<twig:Trace:Collapsible:Content class="">
<twig:Trace>
<twig:Trace:Collapsible>
<twig:Trace:Collapsible:Trigger>
<twig:Trace:Line>
<twig:Trace:Icon>
<twig:ux:icon name="lucide:brain" />
</twig:Trace:Icon>
<twig:Trace:Title>Thought</twig:Trace:Title>
<twig:Trace:Description>12s</twig:Trace:Description>
</twig:Trace:Line>
</twig:Trace:Collapsible:Trigger>
<twig:Trace:Collapsible:Content class="">
I should check which package managers are installed before I scaffold anything.
</twig:Trace:Collapsible:Content>
</twig:Trace:Collapsible>
<twig:Trace:Collapsible open>
<twig:Trace:Collapsible:Trigger>
<twig:Trace:Line status="error">
<twig:Trace:Icon>
<twig:ux:icon name="lucide:terminal" />
</twig:Trace:Icon>
<twig:Trace:Title>Ran 2 commands, 1 failed</twig:Trace:Title>
</twig:Trace:Line>
</twig:Trace:Collapsible:Trigger>
<twig:Trace:Collapsible:Content>
<twig:Trace>
<twig:Trace:Line>
<twig:Trace:Icon>
<twig:ux:icon name="lucide:terminal" />
</twig:Trace:Icon>
<twig:Trace:Title>Ran node --version</twig:Trace:Title>
</twig:Trace:Line>
<twig:Trace:Collapsible>
<twig:Trace:Collapsible:Trigger>
<twig:Trace:Line status="error">
<twig:Trace:Icon>
<twig:ux:icon name="lucide:terminal" />
</twig:Trace:Icon>
<twig:Trace:Title>Failed to run pnpm --version</twig:Trace:Title>
</twig:Trace:Line>
</twig:Trace:Collapsible:Trigger>
<twig:Trace:Collapsible:Content>
<twig:CodeBlock size="sm" variant="destructive">
<twig:CodeBlock:Content wrap class="">command not found: pnpm</twig:CodeBlock:Content>
</twig:CodeBlock>
</twig:Trace:Collapsible:Content>
</twig:Trace:Collapsible>
</twig:Trace>
</twig:Trace:Collapsible:Content>
</twig:Trace:Collapsible>
</twig:Trace>
<p>Node is installed and pnpm is not. Reading the docs config.</p>
<twig:Trace>
<a href="#turn">
<twig:Trace:Line>
<twig:Trace:Icon>
<twig:ux:icon name="lucide:file-text" />
</twig:Trace:Icon>
<twig:Trace:Title>Read apps/v4/lib/docs.ts</twig:Trace:Title>
<twig:Trace:Icon>
<twig:ux:icon name="lucide:chevron-right" class="" />
</twig:Trace:Icon>
</twig:Trace:Line>
</a>
</twig:Trace>
</twig:Trace:Collapsible:Content>
</twig:Trace:Collapsible>
</div>
Reasoning
Render the reasoning of an assistant turn. The row is a Trace:Line until the first text arrives: give it status="running" and the title "Thinking" while it streams, then wrap it in a Trace:Collapsible with the reasoning as its content.
<div class="flex w-full max-w-sm flex-col gap-4 py-6 text-sm">
<twig:Message align="end">
<twig:Message:Content>
<twig:Bubble align="end" variant="muted">
<twig:Bubble:Content>Why is the checkout deploy so slow?</twig:Bubble:Content>
</twig:Bubble>
</twig:Message:Content>
</twig:Message>
<twig:Message>
<twig:Message:Content class="gap-4">
<twig:Trace>
<twig:Trace:Collapsible>
<twig:Trace:Collapsible:Trigger>
<twig:Trace:Line>
<twig:Trace:Icon>
<twig:ux:icon name="lucide:brain" />
</twig:Trace:Icon>
<twig:Trace:Title>Thought</twig:Trace:Title>
</twig:Trace:Line>
</twig:Trace:Collapsible:Trigger>
<twig:Trace:Collapsible:Content class="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.
</twig:Trace:Collapsible:Content>
</twig:Trace:Collapsible>
</twig:Trace>
<p>Image optimization is the bottleneck. Key its cache on the lockfile and the image directory instead of the commit hash.</p>
</twig:Message:Content>
</twig:Message>
</div>
Tool
Render the tool calls of an assistant turn. A finished call is a collapsible row with its output, a failed one has status="error", and a running one is a Trace:Line with status="running".
<div class="">
<twig:Message align="end">
<twig:Message:Content>
<twig:Bubble align="end" variant="muted">
<twig:Bubble:Content>Which package managers are installed?</twig:Bubble:Content>
</twig:Bubble>
</twig:Message:Content>
</twig:Message>
<twig:Message>
<twig:Message:Content class="">
<twig:Trace>
{% for call in [
{command: 'node --version', output: 'v24.18.0', failed: false},
{command: 'npm --version', output: '11.6.2', failed: false},
{command: 'pnpm --version', output: 'command not found: pnpm', failed: true},
] %}
<twig:Trace:Collapsible>
<twig:Trace:Collapsible:Trigger>
<twig:Trace:Line :status="call.failed ? 'error' : null">
<twig:Trace:Icon>
<twig:ux:icon name="lucide:terminal" />
</twig:Trace:Icon>
<twig:Trace:Title>{{ call.failed ? 'Failed to run' : 'Ran' }} {{ call.command }}</twig:Trace:Title>
</twig:Trace:Line>
</twig:Trace:Collapsible:Trigger>
<twig:Trace:Collapsible:Content>
<twig:CodeBlock size="sm">
<twig:CodeBlock:Content wrap class="">{{ call.output }}</twig:CodeBlock:Content>
</twig:CodeBlock>
</twig:Trace:Collapsible:Content>
</twig:Trace:Collapsible>
{% endfor %}
</twig:Trace>
<p>Node 24 and npm 11 are installed. pnpm is not.</p>
</twig:Message:Content>
</twig:Message>
</div>
RTL
To enable RTL support, set the dir="rtl" attribute on the root element.
<div class="flex w-full flex-col items-center gap-4">
{# Arabic #}
<twig:Trace class="max-w-sm py-6 text-sm" dir="rtl">
<twig:Trace:Collapsible open>
<twig:Trace:Collapsible:Trigger>
<twig:Trace:Line>
<twig:Trace:Icon>
<twig:ux:icon name="lucide:terminal" />
</twig:Trace:Icon>
<twig:Trace:Title>تم تشغيل node --version</twig:Trace:Title>
</twig:Trace:Line>
</twig:Trace:Collapsible:Trigger>
<twig:Trace:Collapsible:Content>v24.18.0</twig:Trace:Collapsible:Content>
</twig:Trace:Collapsible>
<twig:Trace:Line status="error">
<twig:Trace:Icon>
<twig:ux:icon name="lucide:circle-alert" />
</twig:Trace:Icon>
<twig:Trace:Title>فشل تشغيل pnpm lint</twig:Trace:Title>
</twig:Trace:Line>
<a href="#rtl">
<twig:Trace:Line>
<twig:Trace:Icon>
<twig:ux:icon name="lucide:file-text" />
</twig:Trace:Icon>
<twig:Trace:Title>تمت قراءة apps/v4/lib/docs.ts</twig:Trace:Title>
<twig:Trace:Description>12 ثانية</twig:Trace:Description>
<twig:Trace:Icon>
<twig:ux:icon name="lucide:chevron-right" class="rtl:rotate-180" />
</twig:Trace:Icon>
</twig:Trace:Line>
</a>
</twig:Trace>
{# Hebrew #}
<twig:Trace class="max-w-sm py-6 text-sm" dir="rtl">
<twig:Trace:Collapsible open>
<twig:Trace:Collapsible:Trigger>
<twig:Trace:Line>
<twig:Trace:Icon>
<twig:ux:icon name="lucide:terminal" />
</twig:Trace:Icon>
<twig:Trace:Title>הורץ node --version</twig:Trace:Title>
</twig:Trace:Line>
</twig:Trace:Collapsible:Trigger>
<twig:Trace:Collapsible:Content>v24.18.0</twig:Trace:Collapsible:Content>
</twig:Trace:Collapsible>
<twig:Trace:Line status="error">
<twig:Trace:Icon>
<twig:ux:icon name="lucide:circle-alert" />
</twig:Trace:Icon>
<twig:Trace:Title>הרצת pnpm lint נכשלה</twig:Trace:Title>
</twig:Trace:Line>
<a href="#rtl">
<twig:Trace:Line>
<twig:Trace:Icon>
<twig:ux:icon name="lucide:file-text" />
</twig:Trace:Icon>
<twig:Trace:Title>נקרא apps/v4/lib/docs.ts</twig:Trace:Title>
<twig:Trace:Description>12 שניות</twig:Trace:Description>
<twig:Trace:Icon>
<twig:ux:icon name="lucide:chevron-right" class="rtl:rotate-180" />
</twig:Trace:Icon>
</twig:Trace:Line>
</a>
</twig:Trace>
</div>
Accessibility
Traceadds no roles of its own. Each line gets its semantics from the element that wraps it.- A
Trace:Lineon its own is aspan. It is read as text and is not a tab stop. - Wrap it in a
Trace:Collapsible:Triggerand it becomes a button witharia-expanded. Wrap it in a link, or in the trigger for a dialog or sheet, and it takes the keyboard and focus behavior of that element. Trace:Iconis hidden from assistive tech, soTrace:Titleis the accessible name. Write it as a sentence that says what happened, like "Edited trace.html.twig", not just "trace.html.twig".- When a link opens a new tab, add "(opens in a new tab)" to the title with
sr-only. - Status is shown with color and motion, so put it in the title too. Write "Running pnpm build" while it runs and "Failed to run pnpm build" when it errors.
- The shimmer on a running title stops when the reader prefers reduced motion. The title still says what is happening.
Trace:Collapsible:Contentmutes its text. If you put a block with a muted background inside it, give the blocktext-foregroundso it keeps its contrast.
API Reference
<twig:Trace>
| Block | Description |
|---|---|
content |
The rows of the trace: Trace:Line, Trace:Collapsible, or a link or trigger wrapping a Trace:Line. |
<twig:Trace:Collapsible>
| Prop | Type | Default |
|---|---|---|
open Whether the row is open on initial render.
|
boolean |
false |
| Block | Description |
|---|---|
content |
The row, typically includes Trace:Collapsible:Trigger and Trace:Collapsible:Content. |
<twig:Trace:Collapsible:Content>
| Block | Description |
|---|---|
content |
The detail revealed when the row is open. |
<twig:Trace:Collapsible:Trigger>
| Block | Description |
|---|---|
content |
The Trace:Line that opens and closes the row. |
<twig:Trace:Description>
| Block | Description |
|---|---|
content |
Secondary text, such as a count or a duration. |
<twig:Trace:Icon>
| Block | Description |
|---|---|
content |
The decorative icon. Before the title it names the action, after it is the indicator. |
<twig:Trace:Line>
| Prop | Type | Default |
|---|---|---|
status The state of the work the line reports.
|
'running'|'error'|'pending'|null |
null |
| Block | Description |
|---|---|
content |
The line content, typically includes Trace:Icon, Trace:Title and optionally Trace:Description. |
<twig:Trace:Title>
| Block | Description |
|---|---|
content |
One sentence that says what happened. |