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-timelineThe 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.
-
step 1KnowledgeKnowledge sources used01:57:38Regional Policy v2.1Location Data
-
step 2CaptureFacts captured+24msFull nameHoang ThuEmailthuht+110226002@magicblocks.aiLocationHo Chi Minh city
-
step 3GoalGoal triggered+27msGoalBook a meetingTimeApr 18, 9:00 AM
-
step 4MatchDirector matched flow+27msFlowMeeting bookingConfidenceHighActionAction 1
-
step 5ToolTool call · Slacklive +1.2sAction Checked staging agentResult Undefined
-
step 6FormForm completed+450msFormMeeting bookingSubmitted atApr 18, 8:59 AM
-
step 7ModelModel error Retrying with fallback…+430msStageResponse generationCodeMODEL_TIMEOUT
-
step 8FormForm post error+290msReasonMissing required fieldSuggested fixAdd email address
-
step 9SecurityJailbreak attempt intercepted+610msDetectionPrompt injection signatureActionContinued conversation safely
-
step 10MetaMessage 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-eventSide-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.
m_AUacy1ZXTWTzUuVca
<!-- 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-breakSix 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.
-
step 1DiscoveryKnowledge sources used02:14:01SourceRegional Policy v2.1
-
step 2DiscoveryFacts captured+18msFull nameHoang Thu
-
step 3DiscoveryGoal triggered+33msGoalBook a meeting
-
step 4DecisionTool call · Slack+1.4sActionPosted reply
-
step 5DecisionForm completed+520msFormMeeting booking
-
step 6DecisionDirector matched flow+44msFlowMeeting 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-streamingFour 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.
-
step 1KnowledgeKnowledge sources used01:57:38
-
step 2CaptureFacts captured+24ms
-
step 3ToolTool call · Slacklive +1.2s
-
step 4FormForm 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-compactSame 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.
-
step 1KnowledgeKnowledge sources used02:30:00
-
step 2CaptureFacts captured+20ms
-
step 3ToolTool call · Slack+1.2s
-
step 4ModelModel error+430ms
-
step 1KnowledgeKnowledge sources used02:30:00
-
step 2CaptureFacts captured+20ms
-
step 3ToolTool call · Slack+1.2s
-
step 4ModelModel 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-graphFive 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.
<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-traceClosed, 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.
<!-- 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-passageA 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.
Rate-limit password attempts to 5 per 15 minutes per account.
We will not sell or rent your personal information.
<!-- 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-cardClick 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.
Reset links expire 24 hours after they are sent.
<!-- 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>} />