About this snippet

A progress view for an AI agent working through a task. Each step shows its title, the tool it calls and what came back, with a spinner while it runs, a check when it finishes and its own time. The progress bar and the total timer sit at the top. The third step fails on its first try to show the error state, and Retry step carries on from there. Stop halts the current step, and Run again starts over.

The steps come from a plain array of objects, so the list and the progress count build themselves from your data. Replace runStep with a real tool call that resolves to true or false, and pass its AbortSignal to fetch so Stop can cancel the call. A rejected call shows as a failed step with its error message. The step icons are drawn in CSS from a data-state attribute (pending, running, done, failed or stopped), and the line between two steps turns green once the first one is done.

The running step gets aria-current set to step, and every step carries hidden state text, so screen readers hear Done or Failed before the title. The bar has the progressbar role with aria-valuenow and a readable aria-valuetext. A hidden status region announces each step as it starts, and the card follows a dark color scheme when the system uses one.

Size
14.9 KB
Uses
HTML, CSS, JavaScript
Loads
Nothing external
Views
26
<main class="page">
  <section class="agent" aria-labelledby="agent-title">
    <header class="agent-head">
      <div>
        <p class="agent-label">Agent task</p>
        <h1 class="agent-title" id="agent-title">Check the pricing page for broken links and write a summary</h1>
      </div>
      <span class="agent-state" id="agent-state" data-state="idle">Waiting</span>
    </header>
    <div class="agent-progress">
      <div class="bar" id="bar" role="progressbar" aria-label="Task progress" aria-valuemin="0" aria-valuemax="4" aria-valuenow="0" aria-valuetext="0 of 4 steps done">
        <span class="bar-fill" id="bar-fill"></span>
      </div>
      <p class="agent-meta">
        <span id="agent-count">0 of 4 steps done</span>
        <span id="agent-time">0.0 s</span>
      </p>
    </div>
    <ol class="steps" id="steps"></ol>
    <footer class="agent-foot">
      <button type="button" class="btn" id="stop" disabled>Stop</button>
      <button type="button" class="btn btn-primary" id="restart" disabled>Run again</button>
    </footer>
    <p class="sr-only" id="status" role="status"></p>
  </section>
</main>
*, *::before, *::after {
  box-sizing: border-box;
}

:root {
  --page: #eef2f7;
  --card: #ffffff;
  --text: #0f172a;
  --muted: #5b6678;
  --line: #e2e8f0;
  --ring: #b8c2d0;
  --accent: #2563eb;
  --done: #16a34a;
  --fail: #dc2626;
  --fail-text: #b91c1c;
  --code: #eef2f7;
  --focus: #2563eb;
}

body {
  font-family: system-ui, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  margin: 0;
  background: var(--page);
  color: var(--text);
}

.page {
  display: grid;
  grid-template-columns: minmax(0, 560px);
  justify-content: center;
  align-content: center;
  min-height: 100vh;
  padding: 24px 16px;
}

.agent {
  padding: 24px;
  border-radius: 18px;
  background: var(--card);
  box-shadow: 0 1px 2px rgba(15, 23, 42, 0.06), 0 12px 32px -12px rgba(15, 23, 42, 0.18);
}

.agent-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 16px;
}

.agent-label {
  margin: 0 0 4px;
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--muted);
}

.agent-title {
  margin: 0;
  font-size: 1.0625rem;
  line-height: 1.4;
}

.agent-state {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  flex-shrink: 0;
  padding: 4px 10px;
  border-radius: 999px;
  background: #f1f5f9;
  color: #475569;
  font-size: 0.8125rem;
  font-weight: 600;
}

.agent-state[data-state="running"] {
  background: #dbeafe;
  color: #1d4ed8;
}

.agent-state[data-state="running"]::before {
  content: "";
  width: 10px;
  height: 10px;
  border: 2px solid currentColor;
  border-right-color: transparent;
  border-radius: 50%;
  animation: spin 0.8s linear infinite;
}

.agent-state[data-state="done"] {
  background: #dcfce7;
  color: #166534;
}

.agent-state[data-state="failed"] {
  background: #fee2e2;
  color: #b91c1c;
}

.agent-progress {
  margin: 20px 0 8px;
}

.bar {
  height: 6px;
  overflow: hidden;
  border-radius: 3px;
  background: var(--line);
}

.bar-fill {
  display: block;
  width: 0;
  height: 100%;
  border-radius: inherit;
  background: var(--accent);
  transition: width 0.4s ease;
}

.agent-meta {
  display: flex;
  justify-content: space-between;
  margin: 8px 0 0;
  font-size: 0.8125rem;
  font-variant-numeric: tabular-nums;
  color: var(--muted);
}

.steps {
  display: grid;
  gap: 14px;
  margin: 20px 0;
  padding: 0;
  list-style: none;
}

.step {
  position: relative;
  display: grid;
  grid-template-columns: 22px minmax(0, 1fr) auto;
  gap: 14px;
}

.step:not(:last-child)::after {
  content: "";
  position: absolute;
  top: 28px;
  bottom: -10px;
  left: 10px;
  width: 2px;
  border-radius: 1px;
  background: var(--line);
}

.step[data-state="done"]:not(:last-child)::after {
  background: var(--done);
}

.step-icon {
  position: relative;
  display: grid;
  place-items: center;
  width: 22px;
  height: 22px;
  margin-top: 1px;
  border: 2px solid var(--ring);
  border-radius: 50%;
}

.step[data-state="running"] .step-icon {
  border-color: var(--line);
  border-top-color: var(--accent);
  animation: spin 0.8s linear infinite;
}

.step[data-state="done"] .step-icon {
  border-color: var(--done);
  background: var(--done);
}

.step[data-state="done"] .step-icon::after {
  content: "";
  width: 5px;
  height: 9px;
  margin-top: -2px;
  border: solid #ffffff;
  border-width: 0 2px 2px 0;
  transform: rotate(45deg);
}

.step[data-state="failed"] .step-icon {
  border-color: var(--fail);
  background: var(--fail);
}

.step[data-state="failed"] .step-icon::before,
.step[data-state="failed"] .step-icon::after {
  content: "";
  position: absolute;
  width: 10px;
  height: 2px;
  border-radius: 1px;
  background: #ffffff;
  transform: rotate(45deg);
}

.step[data-state="failed"] .step-icon::after {
  transform: rotate(-45deg);
}

.step[data-state="stopped"] .step-icon::after {
  content: "";
  width: 8px;
  height: 8px;
  border-radius: 2px;
  background: var(--ring);
}

.step-title {
  margin: 0;
  font-weight: 600;
  line-height: 1.4;
}

.step[data-state="pending"] .step-title {
  color: var(--muted);
  font-weight: 500;
}

.step-body {
  overflow-wrap: anywhere;
}

.step-detail {
  margin: 4px 0 0;
  font-size: 0.875rem;
  color: var(--muted);
}

.step-detail code {
  padding: 1px 6px;
  border-radius: 5px;
  background: var(--code);
  color: var(--text);
  font-family: ui-monospace, Consolas, monospace;
  font-size: 0.8125rem;
}

.step-result {
  margin: 6px 0 0;
  font-size: 0.875rem;
}

.step-result:empty {
  display: none;
}

.step[data-state="failed"] .step-result {
  color: var(--fail-text);
}

.step-time {
  font-size: 0.8125rem;
  font-variant-numeric: tabular-nums;
  color: var(--muted);
  line-height: 1.6;
}

.step-retry {
  margin-top: 10px;
}

.agent-foot {
  display: flex;
  justify-content: flex-end;
  gap: 10px;
  padding-top: 16px;
  border-top: 1px solid var(--line);
}

.btn {
  min-height: 38px;
  padding: 7px 16px;
  border: 1px solid var(--ring);
  border-radius: 10px;
  background: transparent;
  color: var(--text);
  font: inherit;
  font-size: 0.875rem;
  font-weight: 600;
  cursor: pointer;
}

.btn:hover:not(:disabled) {
  background: var(--code);
}

.btn-primary {
  border-color: var(--text);
  background: var(--text);
  color: var(--card);
}

.btn-primary:hover:not(:disabled) {
  background: var(--muted);
  border-color: var(--muted);
}

.btn:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

.btn:disabled {
  opacity: 0.45;
  cursor: default;
}

.btn[hidden] {
  display: none;
}

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

@keyframes spin {
  to {
    transform: rotate(360deg);
  }
}

@media (max-width: 420px) {
  .agent {
    padding: 20px 16px;
  }

  .agent-head {
    flex-direction: column;
    gap: 10px;
  }
}

@media (pointer: coarse) {
  .btn {
    min-height: 44px;
  }
}

@media (prefers-color-scheme: dark) {
  :root {
    --page: #0b1120;
    --card: #151e30;
    --text: #e2e8f0;
    --muted: #a3afc2;
    --line: #2a3548;
    --ring: #56627a;
    --accent: #60a5fa;
    --done: #22c55e;
    --fail: #ef4444;
    --fail-text: #fca5a5;
    --code: #1e293b;
    --focus: #60a5fa;
  }

  .agent-state {
    background: #1e293b;
    color: #cbd5e1;
  }

  .agent-state[data-state="running"] {
    background: #172554;
    color: #93c5fd;
  }

  .agent-state[data-state="done"] {
    background: #052e16;
    color: #86efac;
  }

  .agent-state[data-state="failed"] {
    background: #450a0a;
    color: #fca5a5;
  }

  .step[data-state="done"] .step-icon::after {
    border-color: #0b1120;
  }

  .step[data-state="failed"] .step-icon::before,
  .step[data-state="failed"] .step-icon::after {
    background: #0b1120;
  }
}

@media (forced-colors: active) {
  .step-icon {
    border-color: CanvasText;
  }

  .step[data-state="running"] .step-icon {
    border-color: GrayText;
    border-top-color: Highlight;
  }

  .step[data-state="done"] .step-icon,
  .step[data-state="failed"] .step-icon {
    forced-color-adjust: none;
    border-color: CanvasText;
    background: CanvasText;
  }

  .step[data-state="done"] .step-icon::after {
    forced-color-adjust: none;
    border-color: Canvas;
  }

  .step[data-state="failed"] .step-icon::before,
  .step[data-state="failed"] .step-icon::after {
    forced-color-adjust: none;
    background: Canvas;
  }

  .step[data-state="stopped"] .step-icon::after {
    forced-color-adjust: none;
    background: CanvasText;
  }

  .bar {
    border: 1px solid CanvasText;
  }

  .bar-fill {
    forced-color-adjust: none;
    background: Highlight;
  }
}

@media (prefers-reduced-motion: reduce) {
  .step[data-state="running"] .step-icon,
  .agent-state[data-state="running"]::before {
    animation-duration: 2.4s;
  }

  .bar-fill {
    transition: none;
  }
}
const steps = [
  { title: "Open the pricing page", tool: "fetch_page", input: "/pricing", result: "Page loaded", ms: 1100 },
  { title: "Collect the links", tool: "extract_links", input: "main content", result: "Found 24 links", ms: 1300 },
  { title: "Check each link", tool: "check_links", input: "24 URLs", result: "22 work, 2 return 404", ms: 1900, failOnce: "3 requests timed out" },
  { title: "Write the summary", tool: "write_summary", input: "link results", result: "Summary ready", ms: 1400 }
];

const stateLabels = { pending: "Pending", running: "Running", done: "Done", failed: "Failed", stopped: "Stopped" };

const list = document.getElementById("steps");
const agentState = document.getElementById("agent-state");
const bar = document.getElementById("bar");
const barFill = document.getElementById("bar-fill");
const countEl = document.getElementById("agent-count");
const timeEl = document.getElementById("agent-time");
const stopBtn = document.getElementById("stop");
const restartBtn = document.getElementById("restart");
const status = document.getElementById("status");

let run = 0;
let current = -1;
let running = false;
let attempts = [];
let spent = 0;
let segmentStart = 0;
let stepStart = 0;
let clockTimer = null;
let controller = null;

function el(tag, className, text) {
  const node = document.createElement(tag);
  if (className) node.className = className;
  if (text) node.textContent = text;
  return node;
}

// Build one list item per step from the data above
const items = steps.map(function(step, i) {
  const li = el("li", "step");
  const icon = el("span", "step-icon");
  const body = el("div", "step-body");
  const state = el("span", "sr-only");
  const title = el("p", "step-title", step.title);
  const detail = el("p", "step-detail");
  const result = el("p", "step-result");
  const retry = el("button", "btn step-retry", "Retry step");
  const time = el("span", "step-time");
  icon.setAttribute("aria-hidden", "true");
  detail.append(el("code", "", step.tool), " " + step.input);
  retry.type = "button";
  retry.addEventListener("click", function() {
    runFrom(i);
    stopBtn.focus();
  });
  title.prepend(state);
  body.append(title, detail, result, retry);
  li.append(icon, body, time);
  list.append(li);
  return { li: li, state: state, result: result, retry: retry, time: time };
});

function seconds(ms) {
  return (ms / 1000).toFixed(1) + " s";
}

function wait(ms) {
  return new Promise(function(resolve) {
    setTimeout(resolve, ms);
  });
}

function setStep(i, state, text) {
  const item = items[i];
  item.li.dataset.state = state;
  item.state.textContent = stateLabels[state] + ": ";
  item.result.textContent = text || "";
  item.retry.hidden = state !== "failed";
  if (state === "running") {
    item.li.setAttribute("aria-current", "step");
  } else {
    item.li.removeAttribute("aria-current");
  }
}

function setAgent(state, label) {
  const focused = document.activeElement;
  agentState.dataset.state = state;
  agentState.textContent = label;
  stopBtn.disabled = state !== "running";
  restartBtn.disabled = state === "running";
  // Keep focus on a button that still works
  if (focused === stopBtn && stopBtn.disabled) {
    const retry = list.querySelector(".step[data-state='failed'] .step-retry");
    (retry || restartBtn).focus();
  } else if (focused === restartBtn && restartBtn.disabled) {
    stopBtn.focus();
  }
}

function updateProgress() {
  const done = items.filter(function(item) {
    return item.li.dataset.state === "done";
  }).length;
  const text = done + " of " + steps.length + " steps done";
  bar.setAttribute("aria-valuemax", String(steps.length));
  bar.setAttribute("aria-valuenow", String(done));
  bar.setAttribute("aria-valuetext", text);
  barFill.style.width = (done / steps.length) * 100 + "%";
  countEl.textContent = text;
}

function tick() {
  const now = performance.now();
  timeEl.textContent = seconds(spent + now - segmentStart);
  if (current >= 0) items[current].time.textContent = seconds(now - stepStart);
}

function startClock() {
  segmentStart = performance.now();
  clockTimer = setInterval(tick, 100);
}

function stopClock() {
  clearInterval(clockTimer);
  tick();
  spent += performance.now() - segmentStart;
}

// Replace with a real tool call, passing signal to fetch
function runStep(step, i, signal) {
  attempts[i] = (attempts[i] || 0) + 1;
  return wait(step.ms).then(function() {
    return !(step.failOnce && attempts[i] === 1);
  });
}

async function runFrom(start) {
  const id = ++run;
  controller = new AbortController();
  running = true;
  setAgent("running", "Running");
  startClock();
  for (let i = start; i < steps.length; i++) {
    current = i;
    stepStart = performance.now();
    setStep(i, "running");
    status.textContent = "Step " + (i + 1) + " of " + steps.length + ": " + steps[i].title;
    let ok = false;
    let reason = steps[i].failOnce || "The step did not finish";
    try {
      ok = await runStep(steps[i], i, controller.signal);
    } catch (err) {
      if (err && err.message) reason = err.message;
      else if (typeof err === "string" && err) reason = err;
    }
    if (id !== run) return;
    if (!ok) {
      stopClock();
      running = false;
      setStep(i, "failed", reason);
      setAgent("failed", "Failed");
      status.textContent = "Step " + (i + 1) + " failed: " + reason;
      return;
    }
    tick();
    setStep(i, "done", steps[i].result);
    updateProgress();
  }
  current = -1;
  stopClock();
  running = false;
  setAgent("done", "Done");
  status.textContent = "Task done in " + timeEl.textContent;
}

function stop() {
  if (!running) return;
  run++;
  controller.abort();
  stopClock();
  running = false;
  setStep(current, "stopped", "Stopped before it finished");
  current = -1;
  setAgent("stopped", "Stopped");
  status.textContent = "Task stopped";
}

function restart() {
  run++;
  clearInterval(clockTimer);
  attempts = [];
  spent = 0;
  current = -1;
  items.forEach(function(item, i) {
    setStep(i, "pending");
    item.time.textContent = "";
  });
  updateProgress();
  timeEl.textContent = seconds(0);
  runFrom(0);
}

stopBtn.addEventListener("click", stop);
restartBtn.addEventListener("click", restart);

items.forEach(function(item, i) {
  setStep(i, "pending");
});
updateProgress();
setTimeout(restart, 400);
Esc
↑↓ moveEnter select

Start from a template

Open an .html file, or HTML, CSS, JS, Markdown, Less or TypeScript files. You can also drop them here. Styles and scripts go into their panes.

Share

Source

Keyboard shortcuts

Save