Agent Trace Viewer
A collapsible execution timeline for LLM agents that streams in each reasoning step, tool call, observation and final answer with status icons, wall-clock durations and token counts, expandable JSON payloads, nested sub-steps, and an all/tools/errors filter, so you can inspect exactly what an agent did and where it spent time or failed.
MCP
Code
:root {
--bg: #0c0d10;
--surface: #15161b;
--surface-2: #1d1f27;
--ink: #e9eaf0;
--muted: #9a9cab;
--accent: #8b5cf6;
--accent-2: #22d3ee;
--ok: #34d399;
--warn: #fbbf24;
--danger: #f87171;
--line: rgba(255, 255, 255, 0.10);
--line-2: rgba(255, 255, 255, 0.18);
--r-sm: 8px;
--r-md: 12px;
--r-lg: 16px;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
}
body {
margin: 0;
min-height: 100vh;
background:
radial-gradient(1200px 600px at 15% -10%, rgba(139, 92, 246, 0.14), transparent 60%),
radial-gradient(1000px 500px at 100% 0%, rgba(34, 211, 238, 0.10), transparent 55%),
var(--bg);
color: var(--ink);
font-family: "Inter", system-ui, sans-serif;
line-height: 1.5;
padding: clamp(16px, 4vw, 44px);
display: flex;
justify-content: center;
}
.app {
width: 100%;
max-width: 760px;
}
.viewer {
background: linear-gradient(180deg, rgba(255, 255, 255, 0.02), transparent 40%), var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: 0 24px 60px -30px rgba(0, 0, 0, 0.8);
overflow: hidden;
}
/* Header */
.viewer__head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
padding: 20px 22px 18px;
border-bottom: 1px solid var(--line);
}
.viewer__title-row {
display: flex;
gap: 14px;
align-items: center;
min-width: 0;
}
.viewer__badge {
flex: none;
width: 40px;
height: 40px;
display: grid;
place-items: center;
border-radius: var(--r-md);
color: var(--accent);
background: rgba(139, 92, 246, 0.14);
border: 1px solid rgba(139, 92, 246, 0.3);
}
.viewer__title {
margin: 0;
font-size: 1.05rem;
font-weight: 700;
letter-spacing: -0.01em;
}
.viewer__sub {
margin: 2px 0 0;
font-size: 0.82rem;
color: var(--muted);
min-width: 0;
}
.viewer__task {
color: var(--ink);
font-weight: 500;
}
/* Buttons */
.btn {
display: inline-flex;
align-items: center;
gap: 7px;
font-family: inherit;
font-size: 0.85rem;
font-weight: 600;
color: var(--ink);
border: 1px solid var(--line-2);
background: var(--surface-2);
padding: 9px 15px;
border-radius: var(--r-sm);
cursor: pointer;
transition: transform 0.12s ease, background 0.18s ease, border-color 0.18s ease, opacity 0.18s ease;
}
.btn:hover {
border-color: var(--line-2);
background: #24262f;
}
.btn:active {
transform: translateY(1px);
}
.btn:focus-visible {
outline: 2px solid var(--accent-2);
outline-offset: 2px;
}
.btn:disabled {
opacity: 0.45;
cursor: not-allowed;
}
.btn--run {
background: linear-gradient(180deg, #9b6cff, var(--accent));
border-color: transparent;
color: #fff;
box-shadow: 0 8px 22px -10px rgba(139, 92, 246, 0.8);
}
.btn--run:hover {
background: linear-gradient(180deg, #a67bff, #7c4ff0);
}
.btn--run.is-running {
background: var(--surface-2);
color: var(--ink);
box-shadow: none;
}
.btn--ghost {
background: transparent;
border-color: var(--line);
color: var(--muted);
}
.btn--ghost:hover:not(:disabled) {
color: var(--ink);
background: rgba(255, 255, 255, 0.04);
}
.btn__icon {
transition: transform 0.2s ease;
}
/* Summary */
.summary {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 1px;
background: var(--line);
border-bottom: 1px solid var(--line);
}
.summary__cell {
background: var(--surface);
padding: 12px 18px;
display: flex;
flex-direction: column;
gap: 2px;
}
.summary__k {
font-size: 0.68rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--muted);
}
.summary__v {
font-family: "JetBrains Mono", monospace;
font-size: 1.05rem;
font-weight: 600;
font-variant-numeric: tabular-nums;
}
.summary__cell--err .summary__v {
color: var(--muted);
}
.summary__cell--err.has-errors .summary__v {
color: var(--danger);
}
/* Toolbar */
.toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 14px 18px;
border-bottom: 1px solid var(--line);
flex-wrap: wrap;
}
.segmented {
display: inline-flex;
background: var(--bg);
border: 1px solid var(--line);
border-radius: var(--r-sm);
padding: 3px;
gap: 2px;
}
.seg {
font-family: inherit;
font-size: 0.8rem;
font-weight: 600;
color: var(--muted);
background: transparent;
border: 0;
padding: 6px 14px;
border-radius: 6px;
cursor: pointer;
transition: color 0.15s ease, background 0.15s ease;
}
.seg:hover {
color: var(--ink);
}
.seg.is-active {
color: var(--ink);
background: var(--surface-2);
box-shadow: 0 1px 0 rgba(255, 255, 255, 0.06) inset;
}
.seg:focus-visible {
outline: 2px solid var(--accent-2);
outline-offset: 2px;
}
/* Trace */
.trace {
list-style: none;
margin: 0;
padding: 8px 18px 14px;
max-height: 460px;
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: var(--line-2) transparent;
}
.trace::-webkit-scrollbar {
width: 8px;
}
.trace::-webkit-scrollbar-thumb {
background: var(--line-2);
border-radius: 8px;
}
.step {
position: relative;
padding-left: 30px;
}
.step::before {
content: "";
position: absolute;
left: 11px;
top: 30px;
bottom: -6px;
width: 2px;
background: var(--line);
}
.step:last-child::before {
display: none;
}
.step.is-hidden {
display: none;
}
.step__dot {
position: absolute;
left: 4px;
top: 13px;
width: 16px;
height: 16px;
border-radius: 50%;
display: grid;
place-items: center;
background: var(--surface-2);
border: 2px solid var(--line-2);
color: var(--ink);
z-index: 1;
}
.step__dot svg {
width: 9px;
height: 9px;
}
.step[data-kind="thought"] .step__dot { color: var(--accent-2); border-color: rgba(34, 211, 238, 0.5); }
.step[data-kind="tool"] .step__dot { color: var(--accent); border-color: rgba(139, 92, 246, 0.55); }
.step[data-kind="observation"] .step__dot { color: var(--ok); border-color: rgba(52, 211, 153, 0.5); }
.step[data-kind="answer"] .step__dot { color: var(--warn); border-color: rgba(251, 191, 36, 0.55); }
.step.is-error .step__dot { color: var(--danger); border-color: rgba(248, 113, 113, 0.6); }
.step__head {
width: 100%;
text-align: left;
display: flex;
align-items: center;
gap: 10px;
background: var(--surface-2);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 9px 12px;
margin: 6px 0;
cursor: pointer;
font-family: inherit;
color: var(--ink);
transition: border-color 0.15s ease, background 0.15s ease;
}
.step__head:hover {
border-color: var(--line-2);
background: #23252e;
}
.step__head:focus-visible {
outline: 2px solid var(--accent-2);
outline-offset: 2px;
}
.step.is-error .step__head {
border-color: rgba(248, 113, 113, 0.35);
background: rgba(248, 113, 113, 0.06);
}
.step__chev {
flex: none;
color: var(--muted);
transition: transform 0.2s ease;
}
.step[aria-expanded="true"] .step__chev {
transform: rotate(90deg);
}
.step__kind {
flex: none;
font-size: 0.62rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
padding: 2px 7px;
border-radius: 100px;
border: 1px solid var(--line-2);
color: var(--muted);
}
.step[data-kind="thought"] .step__kind { color: var(--accent-2); border-color: rgba(34, 211, 238, 0.35); }
.step[data-kind="tool"] .step__kind { color: var(--accent); border-color: rgba(139, 92, 246, 0.4); }
.step[data-kind="observation"] .step__kind { color: var(--ok); border-color: rgba(52, 211, 153, 0.35); }
.step[data-kind="answer"] .step__kind { color: var(--warn); border-color: rgba(251, 191, 36, 0.4); }
.step.is-error .step__kind { color: var(--danger); border-color: rgba(248, 113, 113, 0.45); }
.step__label {
flex: 1;
min-width: 0;
font-size: 0.87rem;
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.step__label code {
font-family: "JetBrains Mono", monospace;
font-size: 0.82em;
color: var(--accent);
background: rgba(139, 92, 246, 0.12);
padding: 1px 5px;
border-radius: 5px;
}
.step__meta {
flex: none;
display: flex;
align-items: center;
gap: 8px;
font-family: "JetBrains Mono", monospace;
font-size: 0.72rem;
color: var(--muted);
font-variant-numeric: tabular-nums;
}
.step__meta .m {
display: inline-flex;
align-items: center;
gap: 3px;
white-space: nowrap;
}
.step__meta .m svg { width: 11px; height: 11px; opacity: 0.7; }
/* Body / payload */
.step__body {
overflow: hidden;
max-height: 0;
transition: max-height 0.28s ease;
}
.step__body-inner {
padding: 2px 0 8px;
}
.payload {
position: relative;
background: var(--bg);
border: 1px solid var(--line);
border-radius: var(--r-sm);
margin: 0;
overflow: auto;
}
.payload__bar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 10px;
border-bottom: 1px solid var(--line);
}
.payload__tag {
font-family: "JetBrains Mono", monospace;
font-size: 0.68rem;
color: var(--muted);
letter-spacing: 0.03em;
}
.copy-btn {
display: inline-flex;
align-items: center;
gap: 5px;
font-family: inherit;
font-size: 0.7rem;
font-weight: 600;
color: var(--muted);
background: transparent;
border: 1px solid var(--line);
border-radius: 6px;
padding: 3px 8px;
cursor: pointer;
transition: color 0.15s ease, border-color 0.15s ease;
}
.copy-btn:hover {
color: var(--ink);
border-color: var(--line-2);
}
.copy-btn:focus-visible {
outline: 2px solid var(--accent-2);
outline-offset: 2px;
}
.copy-btn svg { width: 12px; height: 12px; }
.payload pre {
margin: 0;
padding: 12px 14px;
font-family: "JetBrains Mono", monospace;
font-size: 0.76rem;
line-height: 1.65;
overflow-x: auto;
white-space: pre;
}
.tok-key { color: var(--accent-2); }
.tok-str { color: var(--ok); }
.tok-num { color: var(--warn); }
.tok-bool { color: var(--accent); }
.tok-null { color: var(--muted); }
/* Nested sub-steps */
.substeps {
list-style: none;
margin: 8px 0 2px;
padding: 0 0 0 6px;
border-left: 2px dashed var(--line);
}
.substep {
display: flex;
align-items: center;
gap: 9px;
padding: 6px 10px;
margin-left: 8px;
font-size: 0.8rem;
color: var(--muted);
}
.substep__dot {
flex: none;
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--accent);
}
.substep.is-error .substep__dot { background: var(--danger); }
.substep.is-ok .substep__dot { background: var(--ok); }
.substep__name {
color: var(--ink);
font-weight: 500;
}
.substep__dur {
margin-left: auto;
font-family: "JetBrains Mono", monospace;
font-size: 0.7rem;
font-variant-numeric: tabular-nums;
}
/* Streaming placeholder */
.step.is-streaming .step__label::after {
content: "";
display: inline-block;
width: 7px;
height: 1em;
margin-left: 3px;
background: var(--accent-2);
vertical-align: -2px;
animation: blink 0.9s steps(2) infinite;
}
@keyframes blink { 50% { opacity: 0; } }
.step {
animation: stepIn 0.34s cubic-bezier(0.22, 1, 0.36, 1) both;
}
@keyframes stepIn {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
@media (prefers-reduced-motion: reduce) {
.step,
.step__body,
.step__chev,
.btn__icon { animation: none !important; transition: none !important; }
}
/* Empty state */
.empty {
padding: 40px 24px;
text-align: center;
color: var(--muted);
}
.empty.is-hidden { display: none; }
.empty__spark {
font-size: 1.6rem;
color: var(--accent);
margin-bottom: 6px;
}
.empty__msg { margin: 0; font-size: 0.9rem; }
.empty__msg strong { color: var(--ink); }
/* Status bar */
.status-bar {
padding: 10px 18px;
border-top: 1px solid var(--line);
font-family: "JetBrains Mono", monospace;
font-size: 0.74rem;
color: var(--muted);
min-height: 39px;
display: flex;
align-items: center;
gap: 8px;
}
.status-bar__pulse {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--accent-2);
box-shadow: 0 0 0 0 rgba(34, 211, 238, 0.5);
animation: pulse 1.3s ease-out infinite;
}
@keyframes pulse {
to { box-shadow: 0 0 0 7px rgba(34, 211, 238, 0); }
}
.status-bar.is-done .status-bar__pulse {
background: var(--ok);
animation: none;
}
/* Toast */
.toast {
position: fixed;
left: 50%;
bottom: 26px;
transform: translateX(-50%) translateY(20px);
background: var(--surface-2);
border: 1px solid var(--line-2);
color: var(--ink);
font-size: 0.82rem;
font-weight: 500;
padding: 9px 16px;
border-radius: 100px;
box-shadow: 0 16px 40px -18px rgba(0, 0, 0, 0.9);
opacity: 0;
pointer-events: none;
transition: opacity 0.22s ease, transform 0.22s ease;
z-index: 40;
}
.toast.is-visible {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
@media (max-width: 520px) {
body { padding: 12px; }
.viewer__head { flex-direction: column; }
.viewer__actions { width: 100%; }
.btn--run { width: 100%; justify-content: center; }
.summary { grid-template-columns: repeat(2, 1fr); }
.summary__cell { padding: 10px 14px; }
.step__meta { display: none; }
.toolbar { padding: 12px; }
}(function () {
"use strict";
/* ---------- Trace data (a realistic agent run) ---------- */
const TRACE = [
{
kind: "thought",
label: "Break the task into subgoals",
tokens: 128,
duration: 640,
payload: {
reasoning:
"User wants the cheapest NYC → Tokyo flight next month. I should resolve the date window, query a flight search tool, then compare fares.",
plan: ["resolve_dates", "search_flights", "rank_by_price"],
},
},
{
kind: "tool",
label: "Call <code>resolve_dates</code>",
tokens: 74,
duration: 210,
payload: {
name: "resolve_dates",
arguments: { expression: "next month", from: "2026-07-01" },
},
substeps: [
{ name: "parse relative range", ok: true, duration: 40 },
{ name: "clamp to calendar", ok: true, duration: 22 },
],
},
{
kind: "observation",
label: "Date window resolved",
tokens: 46,
duration: 12,
payload: { start: "2026-08-01", end: "2026-08-31", days: 31 },
},
{
kind: "tool",
label: "Call <code>search_flights</code>",
tokens: 312,
duration: 1840,
payload: {
name: "search_flights",
arguments: {
origin: "JFK",
destination: "HND",
depart_between: ["2026-08-01", "2026-08-31"],
cabin: "economy",
sort: "price_asc",
max_results: 5,
},
},
substeps: [
{ name: "query carrier: ANA", ok: true, duration: 610 },
{ name: "query carrier: JAL", ok: true, duration: 580 },
{ name: "query carrier: United", ok: true, duration: 640 },
],
},
{
kind: "observation",
label: "Received 5 fare candidates",
tokens: 268,
duration: 18,
payload: {
results: [
{ carrier: "ANA", price_usd: 812, stops: 1, depart: "2026-08-19" },
{ carrier: "United", price_usd: 848, stops: 1, depart: "2026-08-12" },
{ carrier: "JAL", price_usd: 903, stops: 0, depart: "2026-08-22" },
],
currency: "USD",
},
},
{
kind: "tool",
label: "Call <code>currency_convert</code>",
tokens: 58,
duration: 900,
error: true,
payload: {
name: "currency_convert",
arguments: { amount: 812, from: "USD", to: "JPY" },
error: {
code: "UPSTREAM_TIMEOUT",
message: "FX rate provider did not respond within 900ms",
retryable: true,
},
},
substeps: [{ name: "http GET /rates", ok: false, duration: 900 }],
},
{
kind: "thought",
label: "Recover from failed conversion",
tokens: 90,
duration: 380,
payload: {
reasoning:
"Currency conversion timed out but it is non-critical — the user did not request JPY. I will proceed and report the USD price directly.",
decision: "skip_conversion",
},
},
{
kind: "answer",
label: "Cheapest flight found",
tokens: 156,
duration: 520,
payload: {
answer:
"The cheapest NYC → Tokyo flight next month is ANA at $812 (1 stop), departing Aug 19, 2026.",
confidence: 0.94,
sources: ["search_flights"],
},
},
];
/* ---------- Icons ---------- */
const ICONS = {
thought:
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3a6 6 0 0 0-4 10.5V17h8v-3.5A6 6 0 0 0 12 3Z"/><path d="M9 21h6"/></svg>',
tool:
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 6a3.5 3.5 0 0 1 4.7 4.4l-9 9-3.7-3.7 9-9Z"/></svg>',
observation:
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12s3.5-6 10-6 10 6 10 6-3.5 6-10 6-10-6-10-6Z"/><circle cx="12" cy="12" r="2.4"/></svg>',
answer:
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12l4.5 4.5L19 6.5"/></svg>',
error:
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M12 8v5M12 16.5v.5"/></svg>',
};
const CHEV =
'<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M9 6l6 6-6 6"/></svg>';
const CLOCK =
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></svg>';
const COIN =
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M12 7v10M9.5 9.5h4a1.5 1.5 0 0 1 0 3h-3a1.5 1.5 0 0 0 0 3h4"/></svg>';
const COPY =
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="11" height="11" rx="2"/><path d="M5 15V5a2 2 0 0 1 2-2h10"/></svg>';
const KIND_LABEL = {
thought: "Thought",
tool: "Tool call",
observation: "Observation",
answer: "Answer",
};
/* ---------- DOM refs ---------- */
const traceEl = document.getElementById("trace");
const emptyEl = document.getElementById("empty");
const runBtn = document.getElementById("run-btn");
const expandBtn = document.getElementById("expand-btn");
const statusBar = document.getElementById("status-bar");
const toast = document.getElementById("toast");
const segBtns = Array.prototype.slice.call(document.querySelectorAll(".seg"));
const stat = {
steps: document.getElementById("stat-steps"),
duration: document.getElementById("stat-duration"),
tokens: document.getElementById("stat-tokens"),
errors: document.getElementById("stat-errors"),
};
const errCell = stat.errors.closest(".summary__cell");
let running = false;
let filter = "all";
let allExpanded = false;
/* ---------- Helpers ---------- */
function escapeHtml(s) {
return String(s).replace(/[&<>]/g, function (c) {
return { "&": "&", "<": "<", ">": ">" }[c];
});
}
function highlightJson(obj) {
const json = JSON.stringify(obj, null, 2);
return json.replace(
/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false)\b|\bnull\b|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)/g,
function (match) {
let cls = "tok-num";
if (/^"/.test(match)) {
cls = /:$/.test(match) ? "tok-key" : "tok-str";
} else if (/true|false/.test(match)) {
cls = "tok-bool";
} else if (/null/.test(match)) {
cls = "tok-null";
}
return '<span class="' + cls + '">' + escapeHtml(match) + "</span>";
}
);
}
function fmtSecs(ms) {
return (ms / 1000).toFixed(2) + "s";
}
function showToast(msg) {
toast.textContent = msg;
toast.classList.add("is-visible");
clearTimeout(showToast._t);
showToast._t = setTimeout(function () {
toast.classList.remove("is-visible");
}, 1700);
}
async function copyText(text) {
try {
if (navigator.clipboard && navigator.clipboard.writeText) {
await navigator.clipboard.writeText(text);
return true;
}
} catch (e) {
/* fall through to legacy path */
}
try {
const ta = document.createElement("textarea");
ta.value = text;
ta.setAttribute("readonly", "");
ta.style.position = "fixed";
ta.style.opacity = "0";
document.body.appendChild(ta);
ta.select();
const ok = document.execCommand("copy");
document.body.removeChild(ta);
return ok;
} catch (e) {
return false;
}
}
/* ---------- Build one step node ---------- */
function buildStep(step, index) {
const li = document.createElement("li");
li.className = "step" + (step.error ? " is-error" : "");
li.dataset.kind = step.kind;
li.dataset.index = String(index);
li.setAttribute("aria-expanded", "false");
const iconKey = step.error ? "error" : step.kind;
const bodyId = "step-body-" + index;
let metaHtml =
'<span class="m" title="wall-clock duration">' +
CLOCK +
fmtSecs(step.duration) +
"</span>" +
'<span class="m" title="tokens used">' +
COIN +
step.tokens +
"</span>";
const head = document.createElement("button");
head.type = "button";
head.className = "step__head";
head.setAttribute("aria-expanded", "false");
head.setAttribute("aria-controls", bodyId);
head.innerHTML =
'<span class="step__chev" aria-hidden="true">' +
CHEV +
"</span>" +
'<span class="step__kind">' +
(step.error ? "Error" : KIND_LABEL[step.kind]) +
"</span>" +
'<span class="step__label">' +
step.label +
"</span>" +
'<span class="step__meta">' +
metaHtml +
"</span>";
const dot = document.createElement("span");
dot.className = "step__dot";
dot.setAttribute("aria-hidden", "true");
dot.innerHTML = ICONS[iconKey];
const body = document.createElement("div");
body.className = "step__body";
body.id = bodyId;
body.setAttribute("role", "region");
const inner = document.createElement("div");
inner.className = "step__body-inner";
// substeps
if (step.substeps && step.substeps.length) {
const sub = document.createElement("ul");
sub.className = "substeps";
step.substeps.forEach(function (s) {
const sli = document.createElement("li");
sli.className = "substep " + (s.ok ? "is-ok" : "is-error");
sli.innerHTML =
'<span class="substep__dot" aria-hidden="true"></span>' +
'<span class="substep__name">' +
escapeHtml(s.name) +
"</span>" +
'<span class="substep__dur">' +
fmtSecs(s.duration) +
"</span>";
sub.appendChild(sli);
});
inner.appendChild(sub);
}
// payload
const pay = document.createElement("figure");
pay.className = "payload";
pay.innerHTML =
'<div class="payload__bar">' +
'<span class="payload__tag">' +
(step.kind === "tool" ? "tool_call.json" : step.error ? "error.json" : "payload.json") +
"</span>" +
'<button class="copy-btn" type="button">' +
COPY +
"<span>Copy</span></button>" +
"</div>" +
"<pre><code>" +
highlightJson(step.payload) +
"</code></pre>";
const copyBtn = pay.querySelector(".copy-btn");
copyBtn.addEventListener("click", function (e) {
e.stopPropagation();
copyText(JSON.stringify(step.payload, null, 2)).then(function (ok) {
showToast(ok ? "Payload copied to clipboard" : "Copy failed");
});
});
inner.appendChild(pay);
body.appendChild(inner);
head.addEventListener("click", function () {
toggleStep(li);
});
li.appendChild(dot);
li.appendChild(head);
li.appendChild(body);
return li;
}
function toggleStep(li, force) {
const expanded = force != null ? force : li.getAttribute("aria-expanded") !== "true";
const body = li.querySelector(".step__body");
const head = li.querySelector(".step__head");
li.setAttribute("aria-expanded", String(expanded));
head.setAttribute("aria-expanded", String(expanded));
body.style.maxHeight = expanded ? body.scrollHeight + 40 + "px" : "0px";
}
/* ---------- Filtering ---------- */
function applyFilter() {
const steps = traceEl.querySelectorAll(".step");
let visible = 0;
steps.forEach(function (li) {
const kind = li.dataset.kind;
const isErr = li.classList.contains("is-error");
let show = true;
if (filter === "tools") show = kind === "tool";
else if (filter === "errors") show = isErr;
li.classList.toggle("is-hidden", !show);
if (show) visible++;
});
if (steps.length) {
emptyEl.classList.toggle("is-hidden", visible > 0);
if (visible === 0) {
emptyEl.querySelector(".empty__msg").innerHTML =
"No steps match the <strong>" + filter + "</strong> filter.";
}
}
}
/* ---------- Stats ---------- */
function updateStats(upto) {
let steps = 0;
let dur = 0;
let tokens = 0;
let errors = 0;
for (let i = 0; i < upto; i++) {
const s = TRACE[i];
steps++;
dur += s.duration;
tokens += s.tokens;
if (s.error) errors++;
}
stat.steps.textContent = steps;
stat.duration.textContent = fmtSecs(dur);
stat.tokens.textContent = tokens.toLocaleString();
stat.errors.textContent = errors;
errCell.classList.toggle("has-errors", errors > 0);
}
function setStatus(text, done) {
statusBar.classList.toggle("is-done", !!done);
if (!text) {
statusBar.innerHTML = "";
return;
}
const pulse = done ? "" : '<span class="status-bar__pulse" aria-hidden="true"></span>';
statusBar.innerHTML = pulse + "<span>" + escapeHtml(text) + "</span>";
}
/* ---------- Streaming run ---------- */
function delay(ms) {
return new Promise(function (res) {
setTimeout(res, ms);
});
}
async function run() {
if (running) return;
running = true;
allExpanded = false;
expandBtn.textContent = "Expand all";
expandBtn.disabled = true;
traceEl.innerHTML = "";
emptyEl.classList.add("is-hidden");
runBtn.classList.add("is-running");
runBtn.querySelector(".btn__label").textContent = "Running…";
updateStats(0);
for (let i = 0; i < TRACE.length; i++) {
const step = TRACE[i];
setStatus("Executing step " + (i + 1) + " / " + TRACE.length + " — " + KIND_LABEL[step.kind] + "…");
const node = buildStep(step, i);
node.classList.add("is-streaming");
traceEl.appendChild(node);
traceEl.scrollTop = traceEl.scrollHeight;
// simulate the step "thinking/executing" using its own duration (scaled)
await delay(Math.min(520, 180 + step.duration * 0.18));
node.classList.remove("is-streaming");
updateStats(i + 1);
applyFilter();
}
setStatus("Run complete — " + TRACE.length + " steps, " + stat.tokens.textContent + " tokens", true);
runBtn.classList.remove("is-running");
runBtn.querySelector(".btn__label").textContent = "Run again";
expandBtn.disabled = false;
running = false;
}
/* ---------- Events ---------- */
runBtn.addEventListener("click", run);
segBtns.forEach(function (btn) {
btn.addEventListener("click", function () {
segBtns.forEach(function (b) {
b.classList.remove("is-active");
b.setAttribute("aria-selected", "false");
});
btn.classList.add("is-active");
btn.setAttribute("aria-selected", "true");
filter = btn.dataset.filter;
applyFilter();
});
});
expandBtn.addEventListener("click", function () {
allExpanded = !allExpanded;
expandBtn.textContent = allExpanded ? "Collapse all" : "Expand all";
traceEl.querySelectorAll(".step").forEach(function (li) {
if (!li.classList.contains("is-hidden")) toggleStep(li, allExpanded);
});
});
setStatus("Idle — no trace loaded yet.", true);
})();<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Agent Trace Viewer</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="app" role="main">
<div class="viewer" aria-labelledby="viewer-title">
<header class="viewer__head">
<div class="viewer__title-row">
<div class="viewer__badge" aria-hidden="true">
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2a3 3 0 0 1 3 3v1a3 3 0 0 1 3 3v.5a2.5 2.5 0 0 1 0 5V16a3 3 0 0 1-3 3 3 3 0 0 1-6 0 3 3 0 0 1-3-3v-.5a2.5 2.5 0 0 1 0-5V9a3 3 0 0 1 3-3V5a3 3 0 0 1 3-3Z"/></svg>
</div>
<div>
<h1 id="viewer-title" class="viewer__title">Agent Trace Viewer</h1>
<p class="viewer__sub">Task: <span class="viewer__task">Find the cheapest flight NYC → Tokyo next month</span></p>
</div>
</div>
<div class="viewer__actions">
<button id="run-btn" class="btn btn--run" type="button">
<svg class="btn__icon" viewBox="0 0 24 24" width="15" height="15" fill="currentColor" aria-hidden="true"><path d="M8 5v14l11-7z"/></svg>
<span class="btn__label">Run</span>
</button>
</div>
</header>
<div class="summary" role="group" aria-label="Run summary">
<div class="summary__cell">
<span class="summary__k">Steps</span>
<span class="summary__v" id="stat-steps">0</span>
</div>
<div class="summary__cell">
<span class="summary__k">Duration</span>
<span class="summary__v" id="stat-duration">0.00s</span>
</div>
<div class="summary__cell">
<span class="summary__k">Tokens</span>
<span class="summary__v" id="stat-tokens">0</span>
</div>
<div class="summary__cell summary__cell--err">
<span class="summary__k">Errors</span>
<span class="summary__v" id="stat-errors">0</span>
</div>
</div>
<div class="toolbar" role="toolbar" aria-label="Filter trace">
<div class="segmented" role="tablist" aria-label="Filter steps by kind">
<button class="seg is-active" role="tab" aria-selected="true" data-filter="all" type="button">All</button>
<button class="seg" role="tab" aria-selected="false" data-filter="tools" type="button">Tools</button>
<button class="seg" role="tab" aria-selected="false" data-filter="errors" type="button">Errors</button>
</div>
<button id="expand-btn" class="btn btn--ghost" type="button" disabled>Expand all</button>
</div>
<ol class="trace" id="trace" aria-live="polite" aria-label="Execution trace"></ol>
<div class="empty" id="empty">
<div class="empty__spark" aria-hidden="true">▸</div>
<p class="empty__msg">Press <strong>Run</strong> to stream the agent's execution trace.</p>
</div>
<div class="status-bar" id="status-bar" role="status" aria-live="polite"></div>
</div>
</main>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>The Agent Trace Viewer renders a full agent run as a vertical timeline: each row is a step typed as a thought, tool call, observation, or answer, complete with a status icon, a wall-clock duration, and a token count. Rows expand to reveal a syntax-highlighted JSON payload — the raw arguments a tool was called with or the data it returned — and tool calls can nest their own sub-steps beneath them, mirroring how a real agent decomposes work.
Press Run to watch the trace stream in step by step, the way it would arrive from a live agent over a stream. A segmented filter narrows the view to all steps, only tool calls, or only errors, and a summary bar keeps a running tally of total duration, tokens, and error count. Every row is keyboard-operable with visible focus, and payloads copy to the clipboard in one click.
The demo showcases the browser Clipboard API (navigator.clipboard.writeText) for copying payloads, with a graceful execCommand fallback, alongside requestAnimationFrame-driven streaming and the <details>-style disclosure pattern rebuilt with ARIA for full control over animation and nesting.