Chapter 20 · Operator · AI reasoning flow

Explainability. The robot head.

A vertical timeline of what an AI agent is actually doing — every tool-call, every message, every citation, every decision. Two components: <TraceTimeline> shapes the spine; <TraceEventCard> draws each event. Together they’re the kit’s answer to “show me the thinking.”

20.1 The robot head

The canonical reasoning trace — a single agent turn unpacked. Ten typed events, automatic section breaks on tone change, smart-deduplicated timestamps, calm cards with iconic leading signals. Drop a <TraceTimeline events={…} /> beneath a <ChatMessage> or inside a <Modal>.

TraceTimeline

.trace-timeline

The full 10-event canonical journey — knowledge sources, fact capture, goal trigger, director match, tool call, form ops, model error, form post error, security intercept, and a meta message id. Time runs as deltas after the first absolute anchor.

  1. step 1Knowledge
    Knowledge sources used
    01:57:38
    Regional Policy v2.1
    Location Data
  2. step 2Capture
    Facts captured
    +24ms
    Full nameHoang Thu
    Emailthuht+110226002@magicblocks.ai
    LocationHo Chi Minh city
  3. step 3Goal
    Goal triggered
    +27ms
    GoalBook a meeting
    TimeApr 18, 9:00 AM
  4. step 4Match
    Director matched flow
    +27ms
    FlowMeeting booking
    ConfidenceHigh
    ActionAction 1
  5. step 5Tool
    Tool call · Slack
    live +1.2s
    Action Checked staging agent
    Result Undefined
  6. step 6Form
    Form completed
    +450ms
    FormMeeting booking
    Submitted atApr 18, 8:59 AM
  7. step 7Model
    Model error Retrying with fallback…
    +430ms
    StageResponse generation
    CodeMODEL_TIMEOUT
  8. step 8Form
    Form post error
    +290ms
    ReasonMissing required field
    Suggested fixAdd email address
  9. step 9Security
    Jailbreak attempt intercepted
    +610ms
    DetectionPrompt injection signature
    ActionContinued conversation safely
  10. step 10Meta
    Message ID
    +230ms
    m_AUacy1ZXTWTzUuVca_Tm7SIC1UiemZv
<!-- Each <li class="trace-timeline-item"> carries a <.trace-event> with -->
<!-- its leading icon, head (eyebrow + title + smart timestamp), and    -->
<!-- body items. Section breaks (.is-section-break) appear when the     -->
<!-- next event’s tone differs from its predecessor’s.                 -->
<div class="trace-timeline">
  <ol class="trace-timeline-list" role="list">
    <li class="trace-timeline-item">
      <div class="trace-event is-tone-success">
        <span class="trace-event-leading" aria-hidden="true">
          <!-- <BookIcon /> — 18×18 currentColor inline SVG -->
        </span>
        <div class="trace-event-card">
          <header class="trace-event-head">
            <div class="trace-event-title-block">
              <div class="trace-event-eyebrow">
                <span class="trace-event-step">step 1</span>
                <span class="trace-event-eyebrow-sep" aria-hidden="true">·</span>
                <span class="trace-event-eyebrow-text">Knowledge</span>
              </div>
              <span class="trace-event-title">Knowledge sources used</span>
            </div>
            <div class="trace-event-head-aside">
              <span class="trace-event-time">01:57:38</span>
            </div>
          </header>
          <!-- …body, items, footer… -->
        </div>
      </div>
    </li>
    <!-- …nine more <li class="trace-timeline-item">… -->
  </ol>
</div>
.trace-timeline {
  display: flex;
  flex-direction: column;
  gap: 0;
  font: 400 13.5px/1.5 var(--f-body);
  color: var(--fg);
}

.trace-timeline.is-compact { font-size: 12.5px; }

.trace-timeline-header { padding: 0 0 var(--s-3); }

.trace-timeline-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
}

.trace-timeline-item {
  position: relative;
  padding-bottom: var(--s-3);
}

.trace-timeline-item.is-section-break { padding-top: var(--s-3); }

.trace-timeline-item:last-child { padding-bottom: 0; }

.trace-timeline-empty {
  padding: var(--s-5);
  text-align: center;
  color: var(--fg-soft);
  font: 400 13px/1.5 var(--f-body);
}

.trace-timeline:not(.is-trackless):not(.is-leadingless) .trace-timeline-item::before {
  content: "";
  position: absolute;
  left: 13px; /* centre of 28px leading slot */
  top: 32px;
  bottom: 0;
  width: 2px;
  background: color-mix(in oklab, var(--ink) 10%, transparent);
  z-index: 0;
  border-radius: 999px;
  transition: background var(--dur-2) var(--ease);
}

.trace-timeline:not(.is-trackless):not(.is-leadingless) .trace-timeline-item:last-child::before {
  display: none;
}

.trace-timeline:not(.is-trackless):not(.is-leadingless) .trace-timeline-item:hover::before {
  background: color-mix(in oklab, var(--ink) 18%, transparent);
}

@media (prefers-reduced-motion: reduce) {
  .trace-timeline-item::before { transition: none; }
}

.trace-timeline.is-leadingless .trace-event { grid-template-columns: 1fr; }
import {
  TraceTimeline,
  BookIcon,
  PersonIcon,
  TargetIcon,
  RouteIcon,
  BoltIcon,
  ScrollIcon,
  ShieldIcon,
} from '@magicblocksai/ui';
import type { TraceEvent } from '@magicblocksai/ui';

const startMs = Date.parse('2026-04-18T01:57:38.000Z');

const events: TraceEvent[] = [
  {
    id: 'e1',
    leadingIcon: <BookIcon />,
    eyebrow: 'Knowledge',
    title: 'Knowledge sources used',
    timeMs: startMs,
    tone: 'success',
    items: [
      { kind: 'card', label: 'Regional Policy v2.1' },
      { kind: 'card', label: 'Location Data' },
    ],
  },
  {
    id: 'e2',
    leadingIcon: <PersonIcon />,
    eyebrow: 'Capture',
    title: 'Facts captured',
    timeMs: startMs + 24,
    tone: 'success',
    items: [
      { label: 'Full name', value: 'Hoang Thu' },
      { label: 'Email', value: 'thuht+110226002@magicblocks.ai' },
      { label: 'Location', value: 'Ho Chi Minh city' },
    ],
  },
  {
    id: 'e3',
    leadingIcon: <TargetIcon />,
    eyebrow: 'Goal',
    title: 'Goal triggered',
    timeMs: startMs + 51,
    tone: 'success',
    items: [
      { label: 'Goal', value: 'Book a meeting' },
      { label: 'Time', value: 'Apr 18, 9:00 AM' },
    ],
  },
  {
    id: 'e4',
    leadingIcon: <RouteIcon />,
    eyebrow: 'Match',
    title: 'Director matched flow',
    timeMs: startMs + 78,
    tone: 'success',
    items: [
      { label: 'Flow', value: 'Meeting booking' },
      { label: 'Confidence', value: 'High' },
      { label: 'Action', value: 'Action 1' },
    ],
  },
  {
    id: 'e5',
    leadingIcon: <BoltIcon />,
    eyebrow: 'Tool',
    title: 'Tool call · Slack',
    timeMs: startMs + 1240,
    tone: 'warn',
    streaming: true,
    items: [
      { kind: 'card', label: 'Action', value: 'Checked staging agent' },
      { kind: 'card', label: 'Result', value: 'Undefined' },
    ],
  },
  {
    id: 'e6',
    leadingIcon: <ScrollIcon />,
    eyebrow: 'Form',
    title: 'Form completed',
    timeMs: startMs + 1690,
    tone: 'warn',
    items: [
      { label: 'Form', value: 'Meeting booking' },
      { label: 'Submitted at', value: 'Apr 18, 8:59 AM' },
    ],
  },
  {
    id: 'e7',
    leadingIcon: <BoltIcon />,
    eyebrow: 'Model',
    title: 'Model error',
    caption: 'Retrying with fallback…',
    timeMs: startMs + 2120,
    tone: 'error',
    items: [
      { label: 'Stage', value: 'Response generation' },
      { label: 'Code', value: 'MODEL_TIMEOUT' },
    ],
  },
  {
    id: 'e8',
    leadingIcon: <ScrollIcon />,
    eyebrow: 'Form',
    title: 'Form post error',
    timeMs: startMs + 2410,
    tone: 'error',
    items: [
      { label: 'Reason', value: 'Missing required field' },
      { label: 'Suggested fix', value: 'Add email address' },
    ],
  },
  {
    id: 'e9',
    leadingIcon: <ShieldIcon />,
    eyebrow: 'Security',
    title: 'Jailbreak attempt intercepted',
    timeMs: startMs + 3020,
    tone: 'security',
    items: [
      { label: 'Detection', value: 'Prompt injection signature' },
      { label: 'Action', value: 'Continued conversation safely' },
    ],
  },
  {
    id: 'e10',
    eyebrow: 'Meta',
    title: 'Message ID',
    timeMs: startMs + 3250,
    tone: 'info',
    items: [{ kind: 'code', value: 'm_AUacy1ZXTWTzUuVca_Tm7SIC1UiemZv' }],
  },
];

<TraceTimeline events={events} timeStrategy="first-then-deltas" />

20.2 Event card anatomy

A single <TraceEventCard> is the atom of the timeline. Six tonal variants — success, warn, error, security, info, neutral — each carrying the same internal shape: leading icon, eyebrow, title, timestamp, body items. Use the card outside a timeline whenever you need a one-off classified event row.

TraceEventCard

.trace-event

Side-by-side render of every tone. The tone drives the leading-icon ring, the eyebrow chip colour, the bullet-dot fill, and (for error + security only) the coloured left rail on the card.

success
Knowledge
Knowledge sources used
01:57:38
SourceRegional Policy v2.1
warn
Tool
Tool call · Slack
+1.2s
ActionChecked staging agent
error
Model
Model error Retrying with fallback…
+430ms
CodeMODEL_TIMEOUT
security
Security
Jailbreak attempt intercepted
+610ms
DetectionPrompt injection signature
info
Meta
Message ID
+230ms
m_AUacy1ZXTWTzUuVca
neutral
Event
Generic event
01:58:02
NoteDefault tone
<!-- The .trace-event root carries the is-tone-* modifier; everything -->
<!-- inside the card reads from the CSS custom properties scoped by it. -->
<div class="trace-event is-tone-success">
  <span class="trace-event-leading" aria-hidden="true">
    <!-- inline 18×18 icon SVG -->
  </span>
  <div class="trace-event-card">
    <header class="trace-event-head">
      <div class="trace-event-title-block">
        <div class="trace-event-eyebrow">
          <span class="trace-event-eyebrow-text">Knowledge</span>
        </div>
        <span class="trace-event-title">Knowledge sources used</span>
      </div>
      <div class="trace-event-head-aside">
        <span class="trace-event-time">01:57:38</span>
      </div>
    </header>
    <div class="trace-event-body"><!-- …items… --></div>
  </div>
</div>
.trace-event {
  --trace-tone: var(--fg-soft);
  --trace-tone-soft: color-mix(in oklab, var(--fg-soft) 8%, var(--bg-paper));
  --trace-tone-text: var(--fg);
  --trace-tone-border: var(--hair);
  position: relative;
  display: grid;
  grid-template-columns: 28px 1fr;
  gap: var(--s-3);
  align-items: flex-start;
}

.trace-event.is-compact { gap: var(--s-2); grid-template-columns: 24px 1fr; }

.trace-timeline.is-leadingless .trace-event { grid-template-columns: 1fr; }

.trace-event.is-tone-success {
  --trace-tone: #1A8754;
  --trace-tone-soft: color-mix(in oklab, #1A8754 10%, var(--bg-paper));
  --trace-tone-text: #14633d;
  --trace-tone-border: color-mix(in oklab, #1A8754 24%, var(--hair));
}

.trace-event.is-tone-warn {
  --trace-tone: #B7791F;
  --trace-tone-soft: color-mix(in oklab, #F9AD03 12%, var(--bg-paper));
  --trace-tone-text: #8A5A0F;
  --trace-tone-border: color-mix(in oklab, #F9AD03 28%, var(--hair));
}

.trace-event.is-tone-error {
  --trace-tone: #C0392B;
  --trace-tone-soft: color-mix(in oklab, #C0392B 10%, var(--bg-paper));
  --trace-tone-text: #8B2417;
  --trace-tone-border: color-mix(in oklab, #C0392B 26%, var(--hair));
}

.trace-event.is-tone-security {
  --trace-tone: #6E45D2;
  --trace-tone-soft: color-mix(in oklab, #6E45D2 10%, var(--bg-paper));
  --trace-tone-text: #4A2C9B;
  --trace-tone-border: color-mix(in oklab, #6E45D2 24%, var(--hair));
}

.trace-event.is-tone-info {
  --trace-tone: #2563EB;
  --trace-tone-soft: color-mix(in oklab, #2563EB 8%, var(--bg-paper));
  --trace-tone-text: #1D4FBE;
  --trace-tone-border: color-mix(in oklab, #2563EB 22%, var(--hair));
}

body[data-theme="dark"] .trace-event.is-tone-success {
  --trace-tone-text: #3FBE7A;
  --trace-tone-soft: color-mix(in oklab, #1A8754 18%, var(--bg-paper));
}

body[data-theme="dark"] .trace-event.is-tone-warn {
  --trace-tone-text: #E8B547;
  --trace-tone-soft: color-mix(in oklab, #F9AD03 20%, var(--bg-paper));
}

body[data-theme="dark"] .trace-event.is-tone-error {
  --trace-tone-text: #E37165;
  --trace-tone-soft: color-mix(in oklab, #C0392B 20%, var(--bg-paper));
}

body[data-theme="dark"] .trace-event.is-tone-security {
  --trace-tone-text: #A684EE;
  --trace-tone-soft: color-mix(in oklab, #6E45D2 20%, var(--bg-paper));
}

body[data-theme="dark"] .trace-event.is-tone-info {
  --trace-tone-text: #6D9CFA;
  --trace-tone-soft: color-mix(in oklab, #2563EB 20%, var(--bg-paper));
}

/* …additional rules trimmed for brevity — see _shared.css */
import { TraceEventCard, BookIcon, BoltIcon, ShieldIcon } from '@magicblocksai/ui';
import type { TraceEvent } from '@magicblocksai/ui';

const success: TraceEvent = {
  id: 's',
  leadingIcon: <BookIcon />,
  eyebrow: 'Knowledge',
  title: 'Knowledge sources used',
  tone: 'success',
  timestamp: '01:57:38',
  items: [{ label: 'Source', value: 'Regional Policy v2.1' }],
};

<TraceEventCard event={success} hideStepNumber />
<TraceEventCard event={{ ...warn,     leadingIcon: <BoltIcon />,   tone: 'warn'     }} hideStepNumber />
<TraceEventCard event={{ ...error,    leadingIcon: <BoltIcon />,   tone: 'error'    }} hideStepNumber />
<TraceEventCard event={{ ...security, leadingIcon: <ShieldIcon />, tone: 'security' }} hideStepNumber />
<TraceEventCard event={{ ...info,                                  tone: 'info'     }} hideStepNumber />
<TraceEventCard event={{ ...neutral,                               tone: 'neutral'  }} hideStepNumber />

20.3 Section auto-grouping

When adjacent events change tone, the timeline inserts a section break automatically — extra top padding chunks the events into phases. No consumer wiring needed; the timeline detects the transition and applies .is-section-break on the next row.

TraceTimeline · phased

.is-section-break

Six events split 3-and-3 across two phases — Discovery (success) and Decision (warn). The fourth event picks up the section break automatically; the eye chunks the two phases without needing headings.

  1. step 1Discovery
    Knowledge sources used
    02:14:01
    SourceRegional Policy v2.1
  2. step 2Discovery
    Facts captured
    +18ms
    Full nameHoang Thu
  3. step 3Discovery
    Goal triggered
    +33ms
    GoalBook a meeting
  4. step 4Decision
    Tool call · Slack
    +1.4s
    ActionPosted reply
  5. step 5Decision
    Form completed
    +520ms
    FormMeeting booking
  6. step 6Decision
    Director matched flow
    +44ms
    FlowMeeting booking
<!-- Section 20.3 — the fourth <li> carries .is-section-break -->
<!-- differs from the previous event’s tone. The CSS rule adds   -->
<!-- extra top padding so the eye reads two clusters.            -->
<ol class="trace-timeline-list" role="list">
  <li class="trace-timeline-item">
    <div class="trace-event is-tone-success">…</div>
  </li>
  <li class="trace-timeline-item">
    <div class="trace-event is-tone-success">…</div>
  </li>
  <li class="trace-timeline-item">
    <div class="trace-event is-tone-success">…</div>
  </li>
  <li class="trace-timeline-item is-section-break">
    <div class="trace-event is-tone-warn is-section-break">…</div>
  </li>
  <li class="trace-timeline-item">
    <div class="trace-event is-tone-warn">…</div>
  </li>
  <li class="trace-timeline-item">
    <div class="trace-event is-tone-warn">…</div>
  </li>
</ol>
.trace-timeline-item.is-section-break { padding-top: var(--s-3); }
import {
  TraceTimeline,
  BookIcon,
  PersonIcon,
  TargetIcon,
  RouteIcon,
  BoltIcon,
  ScrollIcon,
} from '@magicblocksai/ui';
import type { TraceEvent } from '@magicblocksai/ui';

const startMs = Date.parse('2026-04-18T02:14:01.000Z');

const events: TraceEvent[] = [
  { id: 'd1', leadingIcon: <BookIcon />,   eyebrow: 'Discovery', title: 'Knowledge sources used', tone: 'success', timeMs: startMs },
  { id: 'd2', leadingIcon: <PersonIcon />, eyebrow: 'Discovery', title: 'Facts captured',        tone: 'success', timeMs: startMs + 18 },
  { id: 'd3', leadingIcon: <TargetIcon />, eyebrow: 'Discovery', title: 'Goal triggered',        tone: 'success', timeMs: startMs + 51 },
  // ↓ section break inserted automatically — tone flips from success → warn
  { id: 'x4', leadingIcon: <BoltIcon />,   eyebrow: 'Decision',  title: 'Tool call · Slack',     tone: 'warn',    timeMs: startMs + 1451 },
  { id: 'x5', leadingIcon: <ScrollIcon />, eyebrow: 'Decision',  title: 'Form completed',        tone: 'warn',    timeMs: startMs + 1971 },
  { id: 'x6', leadingIcon: <RouteIcon />,  eyebrow: 'Decision',  title: 'Director matched flow', tone: 'warn',    timeMs: startMs + 2015 },
];

<TraceTimeline events={events} />

20.4 Streaming & dedup timestamps

Live traces flag the in-flight event with streaming: true — the leading icon pulses, a small “live” indicator sits beside the delta, and the card picks up a faint tonal ring. Timestamps anchor on the first event and run as deltas thereafter; eleven repeating 01:57:38 lines collapse to one anchor plus ten readable steps.

TraceTimeline · streaming

.is-streaming

Four events in a single phase — the third is mid-flight. The first row shows the absolute anchor; rows two through four show +24ms, +1.2s, +450ms as deltas.

  1. step 1Knowledge
    Knowledge sources used
    01:57:38
  2. step 2Capture
    Facts captured
    +24ms
  3. step 3Tool
    Tool call · Slack
    live +1.2s
  4. step 4Form
    Form completed
    +450ms
<!-- Streaming flag adds .is-streaming to .trace-event, a pulse  -->
<!-- ring on the leading icon, and the “live” indicator next to  -->
<!-- the delta. Static frame under prefers-reduced-motion.       -->
<li class="trace-timeline-item is-section-break">
  <div class="trace-event is-tone-warn is-streaming is-section-break">
    <span class="trace-event-leading" aria-hidden="true">
      <!-- icon SVG -->
      <span class="trace-event-pulse" aria-hidden="true"></span>
    </span>
    <div class="trace-event-card">
      <header class="trace-event-head">
        <!-- …title block… -->
        <div class="trace-event-head-aside">
          <span class="trace-event-live" role="status">
            <span class="trace-event-live-dot" aria-hidden="true"></span>live
          </span>
          <span class="trace-event-time is-delta">+1.2s</span>
        </div>
      </header>
    </div>
  </div>
</li>
.trace-event.is-streaming .trace-event-card {
  box-shadow: 0 0 0 2px color-mix(in oklab, var(--trace-tone, var(--accent)) 18%, transparent);
}
import { TraceTimeline, BookIcon, PersonIcon, BoltIcon, ScrollIcon } from '@magicblocksai/ui';
import type { TraceEvent } from '@magicblocksai/ui';

const startMs = Date.parse('2026-04-18T01:57:38.000Z');

const events: TraceEvent[] = [
  { id: 'a', leadingIcon: <BookIcon />,   eyebrow: 'Knowledge', title: 'Knowledge sources used', tone: 'success', timeMs: startMs },
  { id: 'b', leadingIcon: <PersonIcon />, eyebrow: 'Capture',   title: 'Facts captured',         tone: 'success', timeMs: startMs + 24 },
  { id: 'c', leadingIcon: <BoltIcon />,   eyebrow: 'Tool',      title: 'Tool call · Slack',      tone: 'warn',    timeMs: startMs + 1224, streaming: true },
  { id: 'd', leadingIcon: <ScrollIcon />, eyebrow: 'Form',      title: 'Form completed',         tone: 'warn',    timeMs: startMs + 1674 },
];

<TraceTimeline events={events} timeStrategy="first-then-deltas" />

20.5 Calm, at two densities

Calm is the kit’s default: tone colour reads through the leading icon, eyebrow chip, and bullet dots; only error + security earn a coloured left rail. The two densities trade card padding and type size — comfortable for inline-trace surfaces, compact for narrow side rails. Same payload, same calm treatment, two different rhythms.

TraceTimeline · density

.is-compact

Same four-event payload rendered at both densities. The comfortable side reads with confident generosity; the compact side fits the same content into a 24px-wide gutter and 12.5px type.

comfortable (default)
  1. step 1Knowledge
    Knowledge sources used
    02:30:00
  2. step 2Capture
    Facts captured
    +20ms
  3. step 3Tool
    Tool call · Slack
    +1.2s
  4. step 4Model
    Model error
    +430ms
compact
  1. step 1Knowledge
    Knowledge sources used
    02:30:00
  2. step 2Capture
    Facts captured
    +20ms
  3. step 3Tool
    Tool call · Slack
    +1.2s
  4. step 4Model
    Model error
    +430ms
<!-- Comfortable — default. 28px leading-icon slot, generous padding. -->
<div class="trace-timeline">
  <ol class="trace-timeline-list" role="list">
    <li class="trace-timeline-item">
      <div class="trace-event is-tone-success">…</div>
    </li>
    <!-- … -->
  </ol>
</div>

<!-- Compact — 24px leading-icon slot, tighter padding, 12.5px type. -->
<div class="trace-timeline is-compact">
  <ol class="trace-timeline-list" role="list">
    <li class="trace-timeline-item">
      <div class="trace-event is-tone-success is-compact">…</div>
    </li>
    <!-- … -->
  </ol>
</div>
.industry-bar.is-compact .industry-bar-track { height: 32px; }

@media (max-width: 768px) {
  .industry-bar-track {
    height: 36px;
    padding: 0 var(--s-4, 16px);
    -webkit-mask-image: linear-gradient(to right, black calc(100% - 28px), transparent);
    mask-image: linear-gradient(to right, black calc(100% - 28px), transparent);
  }
  .industry-bar-link { font-size: 11px; }
  .industry-bar.is-compact .industry-bar-track { height: 30px; }
}

.saved-views-rail.is-compact .saved-views-rail-button { padding: 4px 8px; font-size: 12.5px; }

.version-switcher.is-compact .version-switcher-trigger { padding: 4px 8px; font-size: 12.5px; }

.chat-msg.is-compact {
  grid-template-columns: 24px 1fr;
  gap: var(--s-2);
}

.chat-msg.is-compact + .chat-msg.is-compact { margin-top: var(--s-2); }

.chat-msg.is-from-user.is-compact {
  grid-template-columns: 1fr 24px;
}

.chat-msg.is-compact .chat-msg-avatar {
  width: 24px;
  height: 24px;
  font-size: 10.5px;
}

.chat-msg.is-compact .chat-msg-bubble-wrap {
  max-width: min(480px, 86%);
}

.chat-msg.is-compact .chat-msg-bubble {
  padding: 8px 12px;
  font-size: 13px;
  line-height: 1.45;
  border-radius: 12px 12px 12px 3px;
}

.chat-msg.is-compact.is-from-user .chat-msg-bubble {
  border-radius: 12px 12px 3px 12px;
}

.chat-transcript.is-compact { font-size: 12.5px; }

.trace-timeline.is-compact { font-size: 12.5px; }

.trace-event.is-compact { gap: var(--s-2); grid-template-columns: 24px 1fr; }

.trace-event.is-compact .trace-event-leading {
  width: 24px;
  height: 24px;
  font-size: 11px;
}

.trace-event.is-compact .trace-event-leading svg { width: 12px; height: 12px; }

.trace-event.is-compact .trace-event-card { padding: var(--s-2) var(--s-3); gap: 4px; }

.trace-event.is-compact .trace-event-title { font-size: 13px; }

.trace-event.is-compact .trace-item-bullet { font-size: 12.5px; }

.trace-event.is-compact .trace-item-card-trigger { padding: 6px 10px; font-size: 12.5px; }
import { TraceTimeline, BookIcon, PersonIcon, BoltIcon } from '@magicblocksai/ui';
import type { TraceEvent } from '@magicblocksai/ui';

const startMs = Date.parse('2026-04-18T02:30:00.000Z');

const events: TraceEvent[] = [
  { id: 'k1', leadingIcon: <BookIcon />,   eyebrow: 'Knowledge', title: 'Knowledge sources used', tone: 'success', timeMs: startMs },
  { id: 'k2', leadingIcon: <PersonIcon />, eyebrow: 'Capture',   title: 'Facts captured',         tone: 'success', timeMs: startMs + 20 },
  { id: 'k3', leadingIcon: <BoltIcon />,   eyebrow: 'Tool',      title: 'Tool call · Slack',      tone: 'warn',    timeMs: startMs + 1220 },
  { id: 'k4', leadingIcon: <BoltIcon />,   eyebrow: 'Model',     title: 'Model error',            tone: 'error',   timeMs: startMs + 1650 },
];

// comfortable — default
<TraceTimeline events={events} />

// compact — narrow side-rail surfaces
<TraceTimeline events={events} density="compact" />

20.6 Journey graph

Agent reasoning visualisation — a node-and-edge diagram where each node is a step in an agent's reasoning trace (decision, tool call, retrieval, response) and each edge carries an outcome label. Nodes are click-selectable; the selected node carries an accent ring and exposes its detail (latency / cost / output preview) in a side panel that the consumer slots via the detail render prop. Hand-rolled SVG layout: consumers supply explicit x / y per node. An edge can also carry a title — the transition's rule in full — surfaced as a native hover tooltip (and exposed to keyboard / screen-reader users via a focusable, labelled hit-area); hover the highlighted “KB likely” arrow.

Five-step reasoning trace

.journey-graph

Five nodes connected by edges showing one query's reasoning path: decide → retrieve + tool-call (parallel) → consolidate → respond. The CRM.getDeal node is selected so its metadata renders in the side panel.

Taken when the question matches a knowledge-base topicKB likelyAccount datafoundfoundShould I look this up?KB search: "renewal"CRM.getDealBoth have answers?Compose reply
<div class="journey-graph" data-detail="true">
  <svg class="journey-graph-svg" viewBox="0 0 600 320" role="img" aria-label="Agent reasoning graph">
    <!-- Edges (lines + optional midpoint labels). An edge with a `title`
         becomes focusable + gets a native hover tooltip + a wide hit-area. -->
    <g class="journey-graph-edge-group" role="img" tabindex="0"
       aria-label="Taken when the question matches a knowledge-base topic">
      <title>Taken when the question matches a knowledge-base topic</title>
      <line class="journey-graph-edge-hit" x1="80" y1="120" x2="240" y2="80"/>
      <line class="journey-graph-edge" x1="80"  y1="120" x2="240" y2="80"/>
      <text class="journey-graph-edge-label" x="160" y="94">KB likely</text>
    </g>
    <!-- … -->
    <!-- Nodes (kind-specific shape + label) -->
    <g class="journey-node" data-kind="tool" tabindex="0" role="button"
       aria-label="CRM.getDeal" aria-selected="true">
      <rect class="journey-node-shape" x="200" y="180" width="80" height="40"/>
      <text class="journey-node-label" x="240" y="204">CRM.getDeal</text>
    </g>
    <!-- … -->
  </svg>
  <aside class="journey-graph-detail">
    <h4 class="journey-graph-detail-title">CRM.getDeal</h4>
    <div class="journey-graph-detail-badges">
      <span class="journey-graph-detail-badge">142 ms</span>
      <span class="journey-graph-detail-badge">$0.002</span>
    </div>
    <pre class="journey-graph-detail-output">deal{id, value, stage, owner}</pre>
  </aside>
</div>
.journey-graph {
  display: grid;
  grid-template-columns: 1fr 240px;
  gap: var(--s-4);
  background: var(--bg-paper);
  border: 1px solid var(--hair);
  border-radius: var(--r-md);
  padding: var(--s-4);
}

.journey-graph[data-detail="false"] { grid-template-columns: 1fr; }

.journey-graph-svg { width: 100%; height: auto; max-height: 360px; }

.journey-graph-edge { stroke: var(--hair); stroke-width: 1.2; fill: none; }

.journey-graph-edge-label {
  font: 500 11px/1 var(--f-mono);
  fill: var(--fg-dim);
  text-anchor: middle;
}

.journey-graph-detail {
  background: var(--bg-sunk);
  border-radius: var(--r-sm);
  padding: var(--s-3);
  font: 400 13px/1.4 var(--f-body);
}

.journey-graph-detail-title {
  font: 600 14px/1.3 var(--f-body); color: var(--fg);
  margin: 0 0 var(--s-2);
}

.journey-graph-detail-badges {
  display: flex; gap: var(--s-2); flex-wrap: wrap;
  margin-bottom: var(--s-3);
}

.journey-graph-detail-badge {
  display: inline-flex; align-items: center;
  padding: 4px 8px; min-height: 24px;
  background: var(--bg-paper); border: 1px solid var(--hair);
  border-radius: 999px;
  font: 500 11px/1 var(--f-mono); color: var(--fg-soft);
}

.journey-graph-detail-output {
  margin: 0; padding: var(--s-2);
  background: var(--bg-paper); border-radius: var(--r-sm);
  font: 400 12px/1.4 var(--f-mono); color: var(--fg);
  white-space: pre-wrap;
}

@media (max-width: 480px) {
  .journey-graph { grid-template-columns: 1fr; padding: var(--s-3); }
}
import { JourneyGraph } from '@magicblocksai/ui';
import type { JourneyGraphNode, JourneyGraphEdge } from '@magicblocksai/ui';

const nodes: JourneyGraphNode[] = [
  { id: 'n1', kind: 'decision',  label: 'Should I look this up?', x: 80,  y: 120 },
  { id: 'n2', kind: 'retrieval', label: 'KB search: "renewal"',   x: 240, y: 80 },
  { id: 'n3', kind: 'tool',      label: 'CRM.getDeal',            x: 240, y: 200,
    meta: { latencyMs: 142, costUsd: 0.002, outputPreview: 'deal{id, value, stage, owner}' } },
  { id: 'n4', kind: 'decision',  label: 'Both have answers?',     x: 400, y: 140 },
  { id: 'n5', kind: 'response',  label: 'Compose reply',          x: 540, y: 140 },
];

const edges: JourneyGraphEdge[] = [
  { from: 'n1', to: 'n2', label: 'KB likely',
    title: 'Taken when the question matches a knowledge-base topic' },
  { from: 'n1', to: 'n3', label: 'Account data' },
  { from: 'n2', to: 'n4', label: 'found' },
  { from: 'n3', to: 'n4', label: 'found' },
  { from: 'n4', to: 'n5' },
];

<JourneyGraph
  nodes={nodes}
  edges={edges}
  defaultSelectedNodeId="n3"
  detail={(node) => node ? (
    <aside className="journey-graph-detail">
      <h4 className="journey-graph-detail-title">{node.label}</h4>
      <div className="journey-graph-detail-badges">
        {node.meta?.latencyMs != null && <span className="journey-graph-detail-badge">{node.meta.latencyMs} ms</span>}
        {node.meta?.costUsd != null && <span className="journey-graph-detail-badge">${node.meta.costUsd.toFixed(3)}</span>}
      </div>
      {node.meta?.outputPreview && <pre className="journey-graph-detail-output">{node.meta.outputPreview}</pre>}
    </aside>
  ) : null}
/>

20.7 Compact trace

The condensed sibling of the timeline above (§20.1), for chat-sized surfaces — a chat-testing window, an embed, a side panel. Same trace data (a TraceEvent[]); the optional phase field groups consecutive events into named, collapsible sections. Three independent disclosure levels — top → phases → steps → detail — and the top closes to a single resting pill under the reply. A step opens to its items in full (every knowledge / source card visible — no popup, no truncation). Timing rides along at every level: total · per-phase duration · per-step delta · exact in the detail. No phase on any event → a flat two-level list, so short traces stay simple.

Nested disclosure — try the chevrons

.compact-trace

Closed, it’s a discrete resting pill — it hugs its own content so it sits quietly under a reply without overwhelming a chat or session window. Open it and it fills its container: the top, each phase, and each step open independently, and “Retrieved knowledge” reveals its sources inline. In a wide operator pane the cards flow into a multi-column grid; in a narrow column they stack. One component — discrete until you want it, full-width when you do.

Onboarding guide.pdfReset links expire after 24 hoursUSED
Billing FAQ · §3Account changes need a verified emailREF
Security policy v2Rate-limit attempts to 5 per 15 minREF
<!-- Class-driven disclosure: toggle .is-open on the trigger + show/hide the
     sibling region. The React component does this for you. -->
<div class="compact-trace">
  <button class="compact-trace-top is-open" aria-expanded="true">
    <span class="compact-trace-chevron is-open"></span>
    <span class="compact-trace-dot is-tone-success"></span>
    <span class="compact-trace-title">Agent reasoning</span>
    <span class="compact-trace-meta">
      <span class="compact-trace-total">1.8s</span>
      <span class="compact-trace-status is-ok">✓</span>
    </span>
  </button>
  <div class="compact-trace-body">
    <div class="compact-trace-phase-wrap">
      <button class="compact-trace-phase is-open" aria-expanded="true">
        <span class="compact-trace-chevron is-open"></span>
        <span class="compact-trace-dot is-tone-success"></span>
        <span class="compact-trace-phase-label">Understanding</span>
        <span class="compact-trace-phase-meta">2 · 240ms</span>
      </button>
      <div class="compact-trace-phase-body">
        <div class="compact-trace-step is-open">
          <button class="compact-trace-step-trigger" aria-expanded="true">
            <span class="compact-trace-dot is-tone-success"></span>
            <span class="compact-trace-step-title">Retrieved knowledge</span>
            <span class="compact-trace-step-time">01:57:38</span>
            <span class="compact-trace-chevron is-open"></span>
          </button>
          <div class="compact-trace-step-detail">
            <!-- all knowledge cards, in full (composes <TraceItemCard>) -->
          </div>
        </div>
      </div>
    </div>
  </div>
</div>
/* Ships in _shared.css (operator surface). The wordmark of the trace system —
 * top / phase / step / detail share the .compact-trace* family; tone dots reuse
 * the timeline's --trace-tone hexes; compact density by default, .is-comfortable
 * loosens it. Dark-mode + reduced-motion are inherited from semantic tokens. */
.compact-trace { /* … */ }
.compact-trace.is-comfortable { /* looser gutters + type */ }
import { Trace } from "@magicblocksai/ui";

// Same TraceEvent[] you'd pass <TraceTimeline>, plus an optional `phase`.
const events = [
  { id: "kb", phase: "Understanding", tone: "success", title: "Retrieved knowledge", timeMs: 0,
    items: [
      { kind: "card", label: "Onboarding guide.pdf", value: "Reset links expire after 24 hours", meta: "USED" },
      { kind: "card", label: "Billing FAQ · §3", value: "Account changes need a verified email", meta: "REF" },
    ] },
  { id: "goal", phase: "Understanding", tone: "success", title: "Captured the goal", timeMs: 24 },
  { id: "slack", phase: "Taking action", tone: "warn", title: "Called Slack", timeMs: 1224 },
  { id: "err", phase: "Taking action", tone: "error", title: "Model error — retried", timeMs: 1654 },
  { id: "safe", phase: "Safety", tone: "security", title: "Blocked unsafe request", timeMs: 1866 },
];

<Trace events={events} defaultOpen defaultOpenPhase="Understanding" />

// Same events, the full robot-head timeline — one component, both surfaces.
<Trace events={events} layout="timeline" />

// No `phase` on any event → a flat top → steps list. Pass `density="comfortable"`
// to loosen, or control the top with `open` / `onOpenChange`.

// v3.4.0 — show knowledge the trace didn't carry inline. A `source` item holds
// a lightweight ref; `resolveSource` loads it on first open (a <SourceCard>).
// Width follows open: a discrete resting pill that fills its container when open.
const kb = { id: "kb", phase: "Understanding", tone: "success", title: "Retrieved knowledge", timeMs: 0,
  items: [{ kind: "source", label: "Security policy v2 · §4.1", meta: "0.88 sim", source: "sec-4-1" }] };

<Trace
  events={[kb]}
  defaultAllPhasesOpen
  resolveSource={async (ref) => renderPassage(await fetchSource(ref))}
/>

20.8 Source passage

The quoted-source primitive of the trace family — a verbatim passage with a mono cite line underneath: source · relevance · freshness · an optional link. Wrap the matched span in <mark> to highlight exactly what the retrieval hit. It’s the body a knowledge card reveals when you open it, and what a <Trace resolveSource> resolver typically returns — but it reads perfectly well standalone in any audit, RAG, or compliance surface. Six tones tint the left rule. Supersedes <EvidenceQuote>.

SourcePassage

.source-passage

A cited quote. Everything on the cite line is optional — pass only what you have. The highlight, the similarity score, and the tone all read at a glance; the link opens the full document in a new tab.

Reset links expire 24 hours after they are sent.
Onboarding guide.pdf · p.4 0.94 sim chunk 3 / 16 Open ↗
Rate-limit password attempts to 5 per 15 minutes per account.
Security policy v2 · §4.1 0.88 sim updated 2026-05
We will not sell or rent your personal information.
From your privacy policy
<!-- A figure + blockquote + cite line. Every cite slot is optional;
     <mark> highlights the matched span. The React component builds this. -->
<figure class="source-passage">
  <blockquote class="source-passage-body">
    Reset links expire <mark>24 hours</mark> after they are sent.
  </blockquote>
  <figcaption class="source-passage-cite">
    <span class="source-passage-source">Onboarding guide.pdf · p.4</span>
    <span class="source-passage-relevance">0.94 sim</span>
    <span class="source-passage-meta">chunk 3 / 16</span>
    <a class="source-passage-link" href="…" target="_blank" rel="noopener noreferrer">Open ↗</a>
  </figcaption>
</figure>

<!-- Tone tints the left rule: add .is-tone-security (or success / warn /
     error / info / neutral). -->
<figure class="source-passage is-tone-security">…</figure>
/* Ships in _shared.css (core surface — sits with .evidence-quote, the
 * primitive it supersedes). The left rule reads --trace-tone, shared with
 * the rest of the trace family; the <mark> highlight uses --accent-soft.
 * Dark-mode + reduced-motion inherited from semantic tokens. */
.source-passage { border-left: 3px solid var(--trace-tone, var(--accent)); }
.source-passage-body mark { background: var(--accent-soft); }
.source-passage.is-tone-security { --trace-tone: #6E45D2; }
import { SourcePassage } from "@magicblocksai/ui";

// Every cite slot is optional — pass only what you have. Wrap the matched
// span in <mark> to highlight what the retrieval actually hit.
<SourcePassage
  cite="Security policy v2 · §4.1"
  relevance={0.88}
  meta="updated 2026-05"
  tone="security"
>
  Rate-limit password attempts to <mark>5 per 15 minutes</mark> per account.
</SourcePassage>

// Add `url` for an "Open ↗" link. Standalone, or as the loaded body
// of a <SourceCard> / what a <Trace resolveSource> resolver returns.

20.9 Source card

The knowledge card of the trace family — a label · value · meta head that expands to a source body. The body can be passed eagerly (details), or loaded the first time the card opens (loadDetails) and cached — which is how a trace renders knowledge that isn’t carried inline. The card owns the whole lifecycle: collapsed → loading → loaded → error + retry. The loaded body is typically a <SourcePassage> (§20.8). Supersedes <TraceItemCard>.

SourceCard

.source-card

Click the first card to reveal its source. The others show the states the card manages while loadDetails runs — a loading shimmer, and a failure with a retry. Tone tints the left rule, the open border, and the body wash.

Couldn’t load this source.
<!-- Head + on-open body. The React component renders the body only when
     open, swapping loading → loaded → error. This static markup shows the
     loaded shape; toggle .is-open on the card + show/hide the body. -->
<div class="source-card is-tone-security is-open">
  <button class="source-card-trigger" aria-expanded="true" aria-controls="sc-1">
    <span class="source-card-body">
      <span class="source-card-label">Security policy v2 · §4.1</span>
      <span class="source-card-value">Rate-limit password attempts</span>
    </span>
    <span class="source-card-meta">0.88 sim</span>
    <span class="source-card-chevron is-open"></span>
  </button>
  <div class="source-card-details" id="sc-1" aria-live="polite">
    <!-- loaded body — typically a .source-passage (§20.8) -->
  </div>
</div>

<!-- loading: .source-card-details aria-busy with .source-card-loading bars.
     error:   .source-card-error + .source-card-retry. -->
/* Ships in _shared.css (operator surface — sits with .trace-item-card, the
 * primitive it supersedes). Same --trace-tone family, set per-tone on the
 * card so it reads standalone. The shimmer + retry honour reduced-motion. */
.source-card { border-left: 3px solid var(--trace-tone, var(--hair)); }
.source-card-details { background: var(--trace-tone-soft, var(--bg-warm)); }
.source-card.is-tone-security { --trace-tone: #6E45D2; }
import { SourceCard, SourcePassage } from "@magicblocksai/ui";

// The card loads its body the first time it opens — collapsed → loading →
// loaded → error+retry, cached after the first fetch. Memoise loadDetails.
<SourceCard
  tone="security"
  label="Security policy v2 · §4.1"
  value="Rate-limit password attempts"
  meta="0.88 sim"
  loadDetails={async () => {
    const p = await fetchPassage("sec-4-1");
    return <SourcePassage tone="security" cite={p.cite}>{p.text}</SourcePassage>;
  }}
/>

// Eager body (no loading state), or omit both for a static read-only card.
<SourceCard label="Match" value="Ho Chi Minh City → APAC" details={<p></p>} />