{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "agent-steps",
  "type": "registry:block",
  "title": "Agent Steps",
  "description": "A live task list for an AI agent run: glyphs morph from pending to a spinning arc to a drawn check or cross, timers tick while a step runs, and each step springs open to a streaming log. Failed steps retry with a badge, and a small API wires it to a real agent.",
  "docs": "Agent Steps was added to public/vividsites/agent-steps.html. Paste its markup, <style> and <script> where you want it on the page (or open the file to see it alone). Customise it with the CSS variables at the top. Docs and live demo: https://vividsites.app/c/agent-steps",
  "categories": [
    "ui",
    "ai-interfaces"
  ],
  "files": [
    {
      "path": "components/vividsites/agent-steps.html",
      "type": "registry:file",
      "target": "public/vividsites/agent-steps.html",
      "content": "<!--\nvs-agent-steps: a live task list for an AI agent run. Each step's glyph morphs pending\n  (hollow) -> running (spinning arc) -> done (the check draws) / failed (the cross draws) /\n  skipped; a rail between glyphs fills as steps finish; per-step timers tick (mm:ss.s) only\n  while running. A row expands on a spring into a mono log pane that streams, auto-scrolls\n  (and stops following if you scroll up) and has a copy button. Failed steps can retry\n  (a \"retry n\" badge). The header shows status, total elapsed time and a segmented bar.\n\n  Markup: a <section class=\"vs-agent-steps\" data-title=\"…\"> holding an <ol> of step titles.\n    <li> may carry data-detail (shown under the title while pending).\n  Wire it to your agent:\n    const run = vsAgentSteps.mount(el)            // or mount(el, [{ title, detail }], { follow, title })\n    run.start(i)          step i -> running (its log opens when follow is on)\n    run.log(i, line)      append a line (\"> cmd\", \"✓ ok\", \"✗ error\", \"M path\" are coloured)\n    run.done(i, note?)    -> done, optional one-line result under the title\n    run.fail(i, message)  -> failed, message shown in red\n    run.retry(i)          badge + separator in the log, back to running\n    run.skip(i, note?)    run.expand(i, open?)    run.detail(i, text)    run.reset()\n    run.steps             [{ title, state, retries, elapsed, lines }]\n  Events (bubble): \"vs-agent-steps:change\" { index, state, prev, step, retries },\n    \"vs-agent-steps:complete\" { ok, elapsed }.\n  Attributes on the section: data-theme=\"dark\", data-density=\"compact\", data-follow=\"off\"\n    (don't auto-open the running step's log), data-manual (don't auto-mount).\n  DEMO SWITCH: data-demo (or data-demo=\"loop\") plays the <li>s' own fake data (data-log,\n    data-fail, data-retry-log, data-done; lines separated by newlines) at data-speed.\n    Remove data-demo when you connect a real agent.\n  Keyboard: rows are buttons (Enter/Space expand), ↑ ↓ Home End move between steps.\n  Screen readers: step changes are announced politely, not every log line.\n  The frame loop sleeps when nothing runs, off-screen, or in a hidden tab.\n-->\n<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\"><link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n<link href=\"https://fonts.googleapis.com/css2?family=Onest:wght@400;500&family=JetBrains+Mono:wght@400&display=swap\" rel=\"stylesheet\">\n<section class=\"vs-agent-steps\" data-title=\"Migrate billing service to v2\" data-demo=\"loop\" aria-label=\"Agent run\">\n  <ol>\n    <li data-done=\"214 files · 23 call sites of invoices.v1\" data-log=\"\n      > git clone ledger/billing-service\n      indexed 214 files in 9 packages\n      found 23 call sites of invoices.v1\n      found 4 webhook handlers\">Read the repository</li>\n    <li data-done=\"7 changes · proration goes last\" data-log=\"\n      reading docs/billing-v2.md\n      map Invoice → InvoiceV2, 7 fields renamed\n      map invoice.paid → invoice.settled\n      plan: 7 changes, proration last\">Plan the migration</li>\n    <li data-done=\"14 files · +412 −268\" data-log=\"\n      M src/invoices/create.ts\n      M src/invoices/finalize.ts\n      M src/webhooks/handlers.ts\n      M src/webhooks/verify.ts\n      M src/proration/calc.ts\n      … 9 more files\">Edit billing files</li>\n    <li data-done=\"214 passed on retry\" data-fail=\"2 failing in proration.spec.ts\" data-log=\"\n      > pnpm test --filter billing\n      ✓ invoices   48 passed\n      ✓ webhooks   31 passed\n      ✗ proration  2 failed\n      expected 1205 to equal 1204\" data-retry-log=\"\n      patch src/proration/calc.ts: round half-cents up\n      > pnpm test --filter billing\n      ✓ proration  19 passed\n      ✓ 214 passed, 0 failed\">Run the test suite</li>\n    <li data-done=\"No errors, no warnings\" data-log=\"\n      > tsc --noEmit\n      > eslint src --max-warnings 0\n      ✓ 0 errors, 0 warnings\">Type-check and lint</li>\n    <li data-done=\"#482 opened · 3 reviewers\" data-log=\"\n      > git push origin agent/billing-v2\n      opened #482 Migrate billing service to v2\n      requested review from 3 owners\">Open a pull request</li>\n  </ol>\n</section>\n\n<style>\n.vs-agent-steps {\n  --vs-agent-steps-accent: #2f6bff;               /* running: arc, bar, status dot */\n  --vs-agent-steps-ok: #16a05a;                   /* done */\n  --vs-agent-steps-bad: #e5484d;                  /* failed, retry badge */\n  --vs-agent-steps-bg: #fdfdfc;                   /* panel */\n  --vs-agent-steps-ink: #1b1c1f;\n  --vs-agent-steps-dim: rgba(27,28,31,.5);\n  --vs-agent-steps-line: rgba(27,28,31,.1);       /* hairlines, pending rings, rail */\n  --vs-agent-steps-well: rgba(27,28,31,.035);     /* hover rows, log pane */\n  --vs-agent-steps-radius: 20px;\n  --vs-agent-steps-width: 42.5rem;\n  --vs-agent-steps-row: 12px;                     /* row padding (density) */\n  --vs-agent-steps-log-h: 9.5rem;                 /* log pane max height */\n  --vs-agent-steps-font: \"Onest\", ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", sans-serif;\n  --vs-agent-steps-mono: \"JetBrains Mono\", ui-monospace, \"SF Mono\", Menlo, monospace;\n  --vs-agent-steps-shadow: 0 0 0 .5px rgba(27,28,31,.1), inset 0 1px 0 #fff, 0 1px 2px rgba(27,28,31,.04), 0 16px 36px -16px rgba(27,28,31,.18), 0 48px 96px -48px rgba(27,28,31,.28);\n  --vs-agent-steps-spring: cubic-bezier(.16,1,.3,1);   /* replaced by a real spring (linear()) where supported */\n  --vs-agent-steps-ease: cubic-bezier(.16,1,.3,1);\n\n  box-sizing: border-box; width: min(100%, var(--vs-agent-steps-width)); min-width: 0; border-radius: var(--vs-agent-steps-radius);\n  background: var(--vs-agent-steps-bg); color: var(--vs-agent-steps-ink); box-shadow: var(--vs-agent-steps-shadow);\n  font-family: var(--vs-agent-steps-font); -webkit-font-smoothing: antialiased; text-align: left; overflow: hidden;\n}\n.vs-agent-steps[data-theme=\"dark\"] {\n  --vs-agent-steps-accent: #5b8cff; --vs-agent-steps-ok: #3cc47c; --vs-agent-steps-bad: #ff6369;\n  --vs-agent-steps-bg: #17181b; --vs-agent-steps-ink: #eeeef0; --vs-agent-steps-dim: rgba(238,238,240,.5);\n  --vs-agent-steps-line: rgba(238,238,240,.1); --vs-agent-steps-well: rgba(238,238,240,.045);\n  --vs-agent-steps-shadow: 0 0 0 .5px rgba(255,255,255,.09), inset 0 1px 0 rgba(255,255,255,.05), 0 16px 36px -16px rgba(0,0,0,.6), 0 48px 96px -48px rgba(0,0,0,.8);\n}\n.vs-agent-steps[data-density=\"compact\"] { --vs-agent-steps-row: 7px; }\n.vs-agent-steps *, .vs-agent-steps *::before, .vs-agent-steps *::after { box-sizing: border-box; }\n.vs-agent-steps > ol:not(.vs-agent-steps-list) { display: none; }\n\n/* header */\n.vs-agent-steps-head { padding: 18px 22px 16px; border-bottom: .5px solid var(--vs-agent-steps-line); }\n.vs-agent-steps-top { display: flex; align-items: center; gap: 10px; min-height: 22px; }\n.vs-agent-steps-status {\n  display: inline-flex; align-items: center; gap: 7px; height: 22px; padding: 0 9px 0 8px; border-radius: 999px;\n  font-size: .75rem; font-weight: 500; color: var(--st, var(--vs-agent-steps-dim));\n  background: color-mix(in srgb, var(--st, var(--vs-agent-steps-ink)) 9%, transparent);\n}\n.vs-agent-steps-status::before { content: \"\"; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }\n.vs-agent-steps[data-state=\"running\"] .vs-agent-steps-status { --st: var(--vs-agent-steps-accent); }\n.vs-agent-steps[data-state=\"running\"] .vs-agent-steps-status::before { animation: vs-agent-steps-pulse 1.4s ease-in-out infinite; }\n.vs-agent-steps[data-state=\"done\"] .vs-agent-steps-status { --st: var(--vs-agent-steps-ok); }\n.vs-agent-steps[data-state=\"failed\"] .vs-agent-steps-status { --st: var(--vs-agent-steps-bad); }\n@keyframes vs-agent-steps-pulse { 0%, 100% { box-shadow: 0 0 0 0 color-mix(in srgb, currentColor 45%, transparent); } 60% { box-shadow: 0 0 0 5px transparent; } }\n.vs-agent-steps-clock { margin-left: auto; font: 400 .8rem/1 var(--vs-agent-steps-mono); font-variant-numeric: tabular-nums; color: var(--vs-agent-steps-dim); letter-spacing: -.01em; }\n.vs-agent-steps-clock b { font-weight: 400; color: var(--vs-agent-steps-ink); }\n.vs-agent-steps-title { margin: 10px 0 14px; font-size: 1.15rem; line-height: 1.3; font-weight: 500; letter-spacing: -.015em; }\n.vs-agent-steps-prog { display: flex; align-items: center; gap: 12px; }\n.vs-agent-steps-bar { flex: 1; display: flex; gap: 3px; height: 4px; }\n.vs-agent-steps-bar i {\n  position: relative; flex: 1; border-radius: 2px; overflow: hidden; background: color-mix(in srgb, var(--vs-agent-steps-ink) 8%, transparent);\n}\n.vs-agent-steps-bar i::after {\n  content: \"\"; position: absolute; inset: 0; border-radius: inherit; background: var(--c, transparent); transform-origin: 0 50%;\n  transform: scaleX(var(--f, 0)); transition: transform .7s var(--vs-agent-steps-spring);\n}\n.vs-agent-steps-bar i[data-s=\"running\"] { --c: var(--vs-agent-steps-accent); --f: .5; }\n.vs-agent-steps-bar i[data-s=\"running\"]::before {\n  content: \"\"; position: absolute; inset: 0; z-index: 1; background: linear-gradient(90deg, transparent, rgba(255,255,255,.75), transparent);\n  transform: translateX(-100%); animation: vs-agent-steps-sweep 1.3s cubic-bezier(.4,0,.2,1) infinite;\n}\n.vs-agent-steps-bar i[data-s=\"done\"] { --c: var(--vs-agent-steps-ok); --f: 1; }\n.vs-agent-steps-bar i[data-s=\"failed\"] { --c: var(--vs-agent-steps-bad); --f: 1; }\n.vs-agent-steps-bar i[data-s=\"skipped\"] { --c: var(--vs-agent-steps-dim); --f: 1; opacity: .5; }\n@keyframes vs-agent-steps-sweep { to { transform: translateX(100%); } }\n.vs-agent-steps-count { font: 400 .75rem/1 var(--vs-agent-steps-mono); font-variant-numeric: tabular-nums; color: var(--vs-agent-steps-dim); }\n.vs-agent-steps-count b { font-weight: 400; color: var(--vs-agent-steps-ink); }\n\n/* steps */\n.vs-agent-steps-list { list-style: none; margin: 0; padding: 8px 10px 10px; }\n.vs-agent-steps-step { position: relative; }\n.vs-agent-steps-row {\n  all: unset; box-sizing: border-box; position: relative; display: grid; grid-template-columns: 22px minmax(0, 1fr) auto; align-items: center; column-gap: 14px;\n  width: 100%; padding: var(--vs-agent-steps-row) 12px; border-radius: 12px; cursor: pointer; -webkit-tap-highlight-color: transparent;\n  transition: background-color .2s ease, transform .35s var(--vs-agent-steps-spring);\n}\n.vs-agent-steps-row:hover { background: var(--vs-agent-steps-well); }\n.vs-agent-steps-row:active { transform: scale(.992); }\n.vs-agent-steps-row:focus-visible { box-shadow: inset 0 0 0 2px var(--vs-agent-steps-accent); }\n.vs-agent-steps-row[aria-disabled=\"true\"] { cursor: default; }\n.vs-agent-steps-row[aria-disabled=\"true\"]:hover { background: none; }\n.vs-agent-steps-row[aria-disabled=\"true\"]:active { transform: none; }\n.vs-agent-steps-txt { min-width: 0; display: grid; gap: 2px; }\n.vs-agent-steps-name {\n  font-size: .94rem; line-height: 1.35; font-weight: 500; letter-spacing: -.006em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;\n}\n.vs-agent-steps-step[data-s=\"pending\"] .vs-agent-steps-name { color: var(--vs-agent-steps-dim); }\n.vs-agent-steps-step[data-s=\"skipped\"] .vs-agent-steps-name { color: var(--vs-agent-steps-dim); text-decoration: line-through; text-decoration-thickness: 1px; text-decoration-color: var(--vs-agent-steps-line); }\n.vs-agent-steps-step[data-s=\"running\"] .vs-agent-steps-name {\n  background: linear-gradient(100deg, var(--vs-agent-steps-ink) 40%, color-mix(in srgb, var(--vs-agent-steps-ink) 35%, transparent) 50%, var(--vs-agent-steps-ink) 60%) 0 0 / 250% 100%;\n  -webkit-background-clip: text; background-clip: text; color: transparent; animation: vs-agent-steps-shine 2.2s linear infinite;\n}\n@keyframes vs-agent-steps-shine { from { background-position: 100% 0; } to { background-position: -150% 0; } }\n.vs-agent-steps-detail { font-size: .8rem; line-height: 1.35; color: var(--vs-agent-steps-dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n.vs-agent-steps-detail:empty { display: none; }\n.vs-agent-steps-step[data-s=\"failed\"] .vs-agent-steps-detail { color: var(--vs-agent-steps-bad); }\n.vs-agent-steps-side { display: flex; align-items: center; gap: 10px; align-self: start; height: 20px; }\n.vs-agent-steps-retry {\n  display: none; align-items: center; height: 20px; padding: 0 7px; border-radius: 6px; font: 400 .68rem/1 var(--vs-agent-steps-mono); letter-spacing: .01em; white-space: nowrap;\n  color: color-mix(in srgb, var(--vs-agent-steps-bad) 85%, var(--vs-agent-steps-ink)); background: color-mix(in srgb, var(--vs-agent-steps-bad) 10%, transparent);\n  box-shadow: inset 0 0 0 .5px color-mix(in srgb, var(--vs-agent-steps-bad) 25%, transparent);\n}\n.vs-agent-steps-step[data-retries] .vs-agent-steps-retry { display: inline-flex; }\n.vs-agent-steps-time { min-width: 4.4em; text-align: right; font: 400 .78rem/1 var(--vs-agent-steps-mono); font-variant-numeric: tabular-nums; color: var(--vs-agent-steps-dim); transition: color .3s; }\n.vs-agent-steps-step[data-s=\"running\"] .vs-agent-steps-time { color: var(--vs-agent-steps-ink); }\n.vs-agent-steps-chev { width: 14px; height: 14px; color: var(--vs-agent-steps-dim); transition: transform .45s var(--vs-agent-steps-spring), opacity .2s; }\n.vs-agent-steps-row[aria-expanded=\"true\"] .vs-agent-steps-chev { transform: rotate(90deg); }\n.vs-agent-steps-row[aria-disabled=\"true\"] .vs-agent-steps-chev, .vs-agent-steps-step[data-s=\"pending\"] .vs-agent-steps-chev { opacity: .25; }\n\n/* the glyph: one svg, five states */\n.vs-agent-steps-glyph { position: relative; z-index: 1; width: 22px; height: 22px; display: block; overflow: visible; align-self: start; margin-top: -1px; }\n.vs-agent-steps-glyph * { transform-box: fill-box; transform-origin: center; }\n.vs-agent-steps-glyph .r { fill: var(--vs-agent-steps-bg); stroke: color-mix(in srgb, var(--vs-agent-steps-ink) 22%, transparent); stroke-width: 1.4; transition: stroke .3s ease, opacity .3s ease; }\n.vs-agent-steps-glyph .a { fill: none; stroke: var(--vs-agent-steps-accent); stroke-width: 1.8; stroke-linecap: round; stroke-dasharray: 14 60; opacity: 0; transition: opacity .25s ease; }\n.vs-agent-steps-glyph .spin { transform-origin: 11px 11px; transform-box: view-box; }\n.vs-agent-steps-glyph .d { fill: var(--vs-agent-steps-accent); transform: scale(0); transition: transform .4s var(--vs-agent-steps-spring); }\n.vs-agent-steps-glyph .f { transform: scale(0); transition: transform .5s var(--vs-agent-steps-spring); fill: var(--vs-agent-steps-ok); }\n.vs-agent-steps-glyph .k, .vs-agent-steps-glyph .x { fill: none; stroke: #fff; stroke-width: 1.9; stroke-linecap: round; stroke-linejoin: round; stroke-dasharray: 1; stroke-dashoffset: 1; transition: stroke-dashoffset .38s var(--vs-agent-steps-ease); }\n.vs-agent-steps-glyph .sk { fill: none; stroke: var(--vs-agent-steps-dim); stroke-width: 1.6; stroke-linecap: round; opacity: 0; transition: opacity .25s; }\n.vs-agent-steps-step[data-s=\"running\"] .vs-agent-steps-glyph .r { stroke: color-mix(in srgb, var(--vs-agent-steps-accent) 22%, transparent); }\n.vs-agent-steps-step[data-s=\"running\"] .vs-agent-steps-glyph .a { opacity: 1; }\n.vs-agent-steps-step[data-s=\"running\"] .vs-agent-steps-glyph .spin { animation: vs-agent-steps-spin .9s linear infinite; }\n.vs-agent-steps-step[data-s=\"running\"] .vs-agent-steps-glyph .d { transform: scale(1); }\n.vs-agent-steps-glyph .fb { fill: var(--vs-agent-steps-bad); }\n.vs-agent-steps-step[data-s=\"done\"] .vs-agent-steps-glyph .f:not(.fb),\n.vs-agent-steps-step[data-s=\"failed\"] .vs-agent-steps-glyph .fb { transform: scale(1); }\n.vs-agent-steps-step[data-s=\"done\"] .vs-agent-steps-glyph .k { stroke-dashoffset: 0; transition-delay: .12s; }\n.vs-agent-steps-step[data-s=\"failed\"] .vs-agent-steps-glyph .x { stroke-dashoffset: 0; transition-delay: .12s; }\n.vs-agent-steps-step[data-s=\"failed\"] .vs-agent-steps-glyph .x2 { transition-delay: .24s; }\n.vs-agent-steps-step[data-s=\"skipped\"] .vs-agent-steps-glyph .r { stroke-dasharray: 2.4 2.4; }\n.vs-agent-steps-step[data-s=\"skipped\"] .vs-agent-steps-glyph .sk { opacity: 1; }\n@keyframes vs-agent-steps-spin { to { transform: rotate(360deg); } }\n\n/* the rail between glyphs: fills as steps finish */\n.vs-agent-steps-rail {\n  position: absolute; left: 23px; width: 1.5px; margin-left: -.75px; top: calc(var(--vs-agent-steps-row) + 26px); bottom: calc(-1 * var(--vs-agent-steps-row) + 4px);\n  border-radius: 1px; background: color-mix(in srgb, var(--vs-agent-steps-ink) 9%, transparent); overflow: hidden; pointer-events: none;\n}\n.vs-agent-steps-step:last-child .vs-agent-steps-rail { display: none; }\n.vs-agent-steps-rail::after {\n  content: \"\"; position: absolute; inset: 0; background: var(--c, var(--vs-agent-steps-ok)); transform-origin: 50% 0; transform: scaleY(0);\n  transition: transform .8s var(--vs-agent-steps-ease);\n}\n.vs-agent-steps-step[data-s=\"done\"] .vs-agent-steps-rail::after,\n.vs-agent-steps-step[data-s=\"skipped\"] .vs-agent-steps-rail::after { transform: scaleY(1); }\n.vs-agent-steps-step[data-s=\"skipped\"] .vs-agent-steps-rail { --c: color-mix(in srgb, var(--vs-agent-steps-ink) 22%, transparent); }\n.vs-agent-steps-step[data-s=\"running\"] .vs-agent-steps-rail::after {\n  background: linear-gradient(transparent, color-mix(in srgb, var(--vs-agent-steps-accent) 70%, transparent), transparent);\n  transform: translateY(-100%); animation: vs-agent-steps-flow 1.6s cubic-bezier(.4,0,.2,1) infinite;\n}\n@keyframes vs-agent-steps-flow { to { transform: translateY(100%); } }\n\n/* log pane */\n.vs-agent-steps-drawer { height: 0; overflow: hidden; }\n.vs-agent-steps-drawer[hidden] { display: none; }\n.vs-agent-steps-log { position: relative; margin: 2px 12px 10px 48px; border-radius: 11px; background: color-mix(in srgb, var(--vs-agent-steps-ink) 3.5%, var(--vs-agent-steps-bg)); box-shadow: inset 0 0 0 .5px var(--vs-agent-steps-line); }\n.vs-agent-steps-loghead { display: flex; align-items: center; gap: 8px; height: 32px; padding: 0 5px 0 12px; border-bottom: .5px solid var(--vs-agent-steps-line); font-size: .72rem; font-weight: 500; color: var(--vs-agent-steps-dim); }\n.vs-agent-steps-loghead span { margin-right: auto; }\n.vs-agent-steps-loghead span b { font: 400 .7rem/1 var(--vs-agent-steps-mono); font-variant-numeric: tabular-nums; margin-left: 6px; }\n.vs-agent-steps-copy {\n  all: unset; box-sizing: border-box; position: relative; display: inline-grid; place-items: center; width: 26px; height: 24px; border-radius: 7px; cursor: pointer; color: var(--vs-agent-steps-dim);\n  transition: background-color .2s, color .2s, transform .3s var(--vs-agent-steps-spring);\n}\n.vs-agent-steps-copy:hover { background: color-mix(in srgb, var(--vs-agent-steps-ink) 6%, transparent); color: var(--vs-agent-steps-ink); }\n.vs-agent-steps-copy:active { transform: scale(.9); }\n.vs-agent-steps-copy:focus-visible { box-shadow: inset 0 0 0 2px var(--vs-agent-steps-accent); }\n.vs-agent-steps-copy svg { grid-area: 1 / 1; width: 14px; height: 14px; transition: transform .45s var(--vs-agent-steps-spring), opacity .2s; }\n.vs-agent-steps-copy .ok { opacity: 0; transform: scale(.4); color: var(--vs-agent-steps-ok); }\n.vs-agent-steps-copy.is-done .cp { opacity: 0; transform: scale(.4); }\n.vs-agent-steps-copy.is-done .ok { opacity: 1; transform: scale(1); }\n.vs-agent-steps-lines {\n  max-height: var(--vs-agent-steps-log-h); overflow-y: auto; overscroll-behavior: contain; padding: 8px 12px 10px;\n  font: 400 .74rem/1.72 var(--vs-agent-steps-mono); color: color-mix(in srgb, var(--vs-agent-steps-ink) 78%, transparent);\n  scrollbar-width: thin; scrollbar-color: var(--vs-agent-steps-line) transparent;\n}\n.vs-agent-steps-lines div { white-space: pre-wrap; overflow-wrap: anywhere; }\n.vs-agent-steps-lines .cmd { color: var(--vs-agent-steps-ink); }\n.vs-agent-steps-lines .cmd::first-letter { color: var(--vs-agent-steps-accent); }\n.vs-agent-steps-lines .ok::first-letter { color: var(--vs-agent-steps-ok); }\n.vs-agent-steps-lines .bad { color: var(--vs-agent-steps-bad); }\n.vs-agent-steps-lines .mod { color: color-mix(in srgb, var(--vs-agent-steps-ink) 62%, transparent); }\n.vs-agent-steps-lines .sep { color: var(--vs-agent-steps-dim); display: flex; align-items: center; gap: 8px; }\n.vs-agent-steps-lines .sep::before, .vs-agent-steps-lines .sep::after { content: \"\"; flex: 1; height: .5px; background: var(--vs-agent-steps-line); }\n.vs-agent-steps-lines .caret::after { content: \"\"; display: inline-block; width: .5em; height: 1.05em; margin-left: 2px; vertical-align: -.18em; background: var(--vs-agent-steps-accent); animation: vs-agent-steps-blink 1s steps(1) infinite; }\n@keyframes vs-agent-steps-blink { 50% { opacity: 0; } }\n.vs-agent-steps-sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }\n\n@media (max-width: 600px) {\n  .vs-agent-steps { border-radius: 18px; }\n  .vs-agent-steps-head { padding: 16px 16px 14px; }\n  .vs-agent-steps-list { padding: 6px 4px 8px; }\n  .vs-agent-steps-row { column-gap: 11px; padding-left: 12px; padding-right: 10px; }\n  .vs-agent-steps-log { margin-left: 12px; margin-right: 8px; }\n  .vs-agent-steps-side { gap: 7px; }\n  .vs-agent-steps-chev { display: none; }\n}\n@media (prefers-reduced-motion: reduce) {\n  .vs-agent-steps *, .vs-agent-steps *::before, .vs-agent-steps *::after { animation: none !important; transition-duration: .01ms !important; transition-delay: 0s !important; }\n  .vs-agent-steps-step[data-s=\"running\"] .vs-agent-steps-name { color: var(--vs-agent-steps-ink); background: none; }\n}\n</style>\n\n<script>\n(() => {\n  if (window.vsAgentSteps) return;\n  const reduce = matchMedia('(prefers-reduced-motion: reduce)');\n  const esc = s => String(s == null ? '' : s).replace(/[&<>\"]/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '\"': '&quot;' }[c]));\n  const fmt = ms => { const s = Math.max(0, ms) / 1000, m = Math.floor(s / 60), r = s - m * 60; return String(m).padStart(2, '0') + ':' + r.toFixed(1).padStart(4, '0'); };\n  const LABEL = { idle: 'Queued', running: 'Running', done: 'Completed', failed: 'Failed', paused: 'Paused' };\n  const WORD = { pending: 'pending', running: 'running', done: 'done', failed: 'failed', skipped: 'skipped' };\n  // a real spring, sampled into a CSS linear() easing so CSS transitions overshoot and settle like one\n  const SPRING = (() => {\n    try { if (!CSS.supports('transition-timing-function', 'linear(0, 1)')) return null; } catch (e) { return null; }\n    let x = 0, v = 0; const k = 260, c = 22, h = 1 / 240, pts = [];\n    for (let i = 0; i <= 240; i++) { if (i % 6 === 0) pts.push(+x.toFixed(4)); v += (k * (1 - x) - c * v) * h; x += v * h; }\n    pts.push(1); return `linear(${pts.join(',')})`;\n  })();\n  const GLYPH = '<svg class=\"vs-agent-steps-glyph\" viewBox=\"0 0 22 22\" aria-hidden=\"true\"><circle class=\"r\" cx=\"11\" cy=\"11\" r=\"8.5\"/><circle class=\"d\" cx=\"11\" cy=\"11\" r=\"2.5\"/><g class=\"spin\"><circle class=\"a\" cx=\"11\" cy=\"11\" r=\"8.5\"/></g><circle class=\"f\" cx=\"11\" cy=\"11\" r=\"10\"/><circle class=\"f fb\" cx=\"11\" cy=\"11\" r=\"10\"/><path class=\"k\" pathLength=\"1\" d=\"M7 11.4 9.7 14 15.1 8.5\"/><path class=\"x\" pathLength=\"1\" d=\"M8 8l6 6\"/><path class=\"x x2\" pathLength=\"1\" d=\"M14 8l-6 6\"/><path class=\"sk\" d=\"M7.8 11h6.4\"/></svg>';\n  const CHEV = '<svg class=\"vs-agent-steps-chev\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"m6 3.5 4.5 4.5L6 12.5\"/></svg>';\n  const COPY = '<svg class=\"cp\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linejoin=\"round\" aria-hidden=\"true\"><rect x=\"5.5\" y=\"5.5\" width=\"8\" height=\"8\" rx=\"2\"/><path d=\"M10.5 3.2A1.7 1.7 0 0 0 9 2.5H4.2A1.7 1.7 0 0 0 2.5 4.2V9a1.7 1.7 0 0 0 .7 1.4\"/></svg><svg class=\"ok\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"m3.5 8.5 3 3 6-6.5\"/></svg>';\n  const kind = t => /^[>$] /.test(t) ? 'cmd' : /^[✓+]/.test(t) ? 'ok' : /^[✗!×]/.test(t) ? 'bad' : /^[MAD] /.test(t) ? 'mod' : '';\n  const lines = s => (s || '').split('\\n').map(l => l.trim()).filter(Boolean);\n  let uid = 0;\n\n  function mount(el, steps, opts = {}) {\n    if (el.__vsAs) return el.__vsAs;\n    const id = 'vs-as-' + (++uid);\n    const src = el.querySelector(':scope > ol');\n    if (!steps && src) steps = [...src.children].map(li => ({ title: li.textContent.trim(), detail: li.dataset.detail || '', log: lines(li.dataset.log),\n      fail: li.dataset.fail || '', retryLog: lines(li.dataset.retryLog), done: li.dataset.done || '' }));\n    steps = (steps || []).map(s => typeof s === 'string' ? { title: s } : { ...s });\n    if (src) src.remove();\n    if (SPRING) el.style.setProperty('--vs-agent-steps-spring', SPRING);\n    const follow = opts.follow != null ? opts.follow : el.dataset.follow !== 'off';\n    const title = opts.title || el.dataset.title || '';\n\n    el.innerHTML = `<div class=\"vs-agent-steps-head\"><div class=\"vs-agent-steps-top\"><span class=\"vs-agent-steps-status\">Queued</span><span class=\"vs-agent-steps-clock\" aria-label=\"Elapsed\"><b>00:00.0</b></span></div>\n      ${title ? `<h3 class=\"vs-agent-steps-title\">${esc(title)}</h3>` : ''}\n      <div class=\"vs-agent-steps-prog\"><div class=\"vs-agent-steps-bar\" aria-hidden=\"true\">${steps.map(() => '<i></i>').join('')}</div><span class=\"vs-agent-steps-count\"><b>0</b> / ${steps.length}</span></div></div>\n      <ol class=\"vs-agent-steps-list\">${steps.map((s, i) => `<li class=\"vs-agent-steps-step\" data-s=\"pending\"><span class=\"vs-agent-steps-rail\"></span>\n        <button class=\"vs-agent-steps-row\" type=\"button\" aria-expanded=\"false\" aria-controls=\"${id}-log${i}\" aria-disabled=\"true\">${GLYPH}\n          <span class=\"vs-agent-steps-txt\"><span class=\"vs-agent-steps-name\">${esc(s.title)}</span><span class=\"vs-agent-steps-detail\">${esc(s.detail || '')}</span></span>\n          <span class=\"vs-agent-steps-side\"><span class=\"vs-agent-steps-retry\"></span><span class=\"vs-agent-steps-time\"></span>${CHEV}</span></button>\n        <div class=\"vs-agent-steps-drawer\" id=\"${id}-log${i}\" hidden><div class=\"vs-agent-steps-log\"><div class=\"vs-agent-steps-loghead\"><span>Output<b>0</b></span>\n          <button class=\"vs-agent-steps-copy\" type=\"button\" aria-label=\"Copy output\">${COPY}</button></div>\n          <div class=\"vs-agent-steps-lines\" role=\"log\" aria-live=\"off\" tabindex=\"0\" aria-label=\"Output of ${esc(s.title)}\"></div></div></div></li>`).join('')}</ol>\n      <p class=\"vs-agent-steps-sr\" aria-live=\"polite\"></p>`;\n    const $ = s => el.querySelector(s);\n    const status = $('.vs-agent-steps-status'), clock = $('.vs-agent-steps-clock b'), count = $('.vs-agent-steps-count b'), live = $('.vs-agent-steps-sr');\n    const segs = [...el.querySelectorAll('.vs-agent-steps-bar i')];\n    const S = [...el.querySelectorAll('.vs-agent-steps-step')].map((li, i) => ({\n      li, i, data: steps[i], s: 'pending', acc: 0, t0: 0, retries: 0, open: false, auto: false, n: 0, lastTxt: '',\n      row: li.querySelector('.vs-agent-steps-row'), detail: li.querySelector('.vs-agent-steps-detail'), time: li.querySelector('.vs-agent-steps-time'),\n      badge: li.querySelector('.vs-agent-steps-retry'), drawer: li.querySelector('.vs-agent-steps-drawer'), lines: li.querySelector('.vs-agent-steps-lines'),\n      lc: li.querySelector('.vs-agent-steps-loghead b'), copy: li.querySelector('.vs-agent-steps-copy'), h: { x: 0, v: 0, g: 0 },\n    }));\n    let pauseT = 0, idleSince = 0, runT0 = 0, runEnd = 0, gen = 0, raf = 0, last = 0, onScreen = true, lastClock = '';\n\n    // ---- frame loop: drawer springs + live timers; sleeps when idle, off-screen or hidden\n    const K = 340, C = 34;\n    const tick = now => {\n      const dt = Math.min(.064, (now - last) / 1000 || .016); last = now; let busy = false;\n      for (const st of S) { const p = st.h;\n        if (reduce.matches) { p.x = p.g; p.v = 0; }\n        else if (Math.abs(p.v) > .5 || Math.abs(p.x - p.g) > .3) { busy = true; const n = Math.max(1, Math.round(dt * 120)), h = dt / n;\n          for (let j = 0; j < n; j++) { p.v += (-K * (p.x - p.g) - C * p.v) * h; p.x += p.v * h; } }\n        else { p.x = p.g; p.v = 0; }\n        const hp = Math.max(0, p.x).toFixed(1) + 'px'; if (st.drawer.style.height !== hp) st.drawer.style.height = hp;\n        if (p.x === p.g && p.g === 0 && !st.open && !st.drawer.hidden) st.drawer.hidden = true;\n        if (st.s === 'running') { busy = true; const t = fmt(st.acc + now - st.t0); if (t !== st.lastTxt) { st.time.textContent = st.lastTxt = t; } }\n      }\n      if (runT0) { const t = fmt((runEnd || now) - runT0); if (t !== lastClock) clock.textContent = lastClock = t; if (!runEnd) busy = true; }\n      raf = busy && onScreen && !document.hidden ? requestAnimationFrame(tick) : 0;\n    };\n    const wake = () => { if (!raf && onScreen && !document.hidden) { last = performance.now(); raf = requestAnimationFrame(tick); } };\n    const io = 'IntersectionObserver' in window ? new IntersectionObserver(es => { onScreen = es[0].isIntersecting; if (onScreen) wake(); }) : null;\n    io && io.observe(el);\n    const onVis = () => { if (!document.hidden) wake(); }; document.addEventListener('visibilitychange', onVis);\n\n    function measure(st) { if (st.open) { st.h.g = st.drawer.firstElementChild.offsetHeight + 12; wake(); } }\n    function expand(i, open, auto) {\n      const st = S[i]; if (!st) return; open = open == null ? !st.open : !!open;\n      if (open && !st.n) return; st.open = open; st.auto = open && !!auto;\n      st.row.setAttribute('aria-expanded', String(open));\n      if (open) { st.drawer.hidden = false; measure(st); st.lines.scrollTop = st.lines.scrollHeight; } else { st.h.g = 0; }\n      wake();\n    }\n    function update() {\n      const busy = S.some(s => s.s === 'running'), fin = S.every(s => s.s === 'done' || s.s === 'skipped'), bad = S.some(s => s.s === 'failed');\n      let state = busy ? 'running' : fin && S.length ? 'done' : bad ? 'failed' : S.some(s => s.s !== 'pending') ? 'paused' : 'idle';\n      const prev = el.dataset.state, t = performance.now(); clearTimeout(pauseT);\n      // between two steps the run is not \"paused\": only say so if nothing starts for a while\n      if (busy) idleSince = 0; else if (!idleSince && prev === 'running') idleSince = t;\n      if (state === 'paused' && idleSince && t - idleSince < 1500) { state = 'running'; pauseT = setTimeout(update, 1500 - (t - idleSince) + 20); }\n      el.dataset.state = state; status.textContent = LABEL[state];\n      count.textContent = S.filter(s => s.s === 'done' || s.s === 'skipped').length;\n      segs.forEach((g, i) => g.dataset.s = S[i].s);\n      const now = performance.now();\n      if (state === 'running') runEnd = 0; else if (runT0 && !runEnd && (state === 'done' || state === 'failed')) runEnd = now;\n      if (state === 'done' && prev !== 'done' && follow) { const g = gen; setTimeout(() => { if (gen === g && el.dataset.state === 'done') S.forEach(o => { if (o.auto && o.open) expand(o.i, false); }); }, 900); }\n      if (state === 'done' && prev !== 'done') { say(`Run complete in ${fmt(runEnd - runT0)}.`); el.dispatchEvent(new CustomEvent('vs-agent-steps:complete', { bubbles: true, detail: { ok: true, elapsed: runEnd - runT0 } })); }\n      wake();\n    }\n    let sayT = 0; const say = t => { clearTimeout(sayT); live.textContent = ''; sayT = setTimeout(() => live.textContent = t, 60); };\n    function set(i, s, note) {\n      const st = S[i]; if (!st) return; const prev = st.s; const now = performance.now();\n      if (prev === 'running') { st.acc += now - st.t0; st.time.textContent = st.lastTxt = fmt(st.acc); st.lines.lastElementChild && st.lines.lastElementChild.classList.remove('caret'); }\n      st.s = s; st.li.dataset.s = s;\n      if (s === 'running') { st.t0 = now; if (!runT0) runT0 = now; }\n      if (note != null) st.detail.textContent = note;\n      st.row.setAttribute('aria-label', `${st.data.title}, ${WORD[s]}${st.retries ? `, retry ${st.retries}` : ''}${st.acc ? ', ' + fmt(st.acc) : ''}`);\n      update();\n      el.dispatchEvent(new CustomEvent('vs-agent-steps:change', { bubbles: true, detail: { index: i, state: s, prev, step: st.data, retries: st.retries } }));\n    }\n    function start(i) {\n      const st = S[i]; if (!st) return; if (st.stale) clearVis(st, false);\n      if (follow) S.forEach(o => { if (o !== st && o.auto && o.open) expand(o.i, false); });\n      set(i, 'running'); say(`Step ${i + 1}, ${st.data.title}: running.`);\n      if (follow && st.n) expand(i, true, true);\n    }\n    function log(i, text) {\n      const st = S[i]; if (!st) return;\n      String(text).split('\\n').forEach(t => {\n        const d = document.createElement('div'); const k = kind(t); if (k) d.className = k; d.textContent = t;\n        st.lines.lastElementChild && st.lines.lastElementChild.classList.remove('caret');\n        if (st.s === 'running') d.classList.add('caret');\n        const stick = st.lines.scrollTop + st.lines.clientHeight >= st.lines.scrollHeight - 14;\n        st.lines.appendChild(d); st.n++; st.lc.textContent = st.n;\n        if (!reduce.matches && st.open) d.animate([{ opacity: 0, transform: 'translateY(3px)' }, { opacity: 1, transform: 'none' }], { duration: 240, easing: 'cubic-bezier(.16,1,.3,1)' });\n        if (stick) st.lines.scrollTop = st.lines.scrollHeight;\n      });\n      st.row.setAttribute('aria-disabled', 'false');\n      if (st.s === 'running') { const t = st.lines.lastElementChild.textContent; st.detail.textContent = t.replace(/^([>$]|[✓✗×!]) */, ''); }   // the collapsed row peeks at the latest line\n      if (follow && st.s === 'running' && !st.open && st.n && !st.userClosed) expand(i, true, true);\n      measure(st);\n    }\n    function sep(i, t) { const st = S[i]; const d = document.createElement('div'); d.className = 'sep'; d.textContent = t; st.lines.appendChild(d); st.n++; measure(st); }\n    function done(i, note) { const st = S[i]; if (!st) return; set(i, 'done', note); say(`Step ${i + 1} done${note ? ': ' + note : ''}.`); }\n    function fail(i, msg) { const st = S[i]; if (!st) return; set(i, 'failed', msg || 'Failed'); say(`Step ${i + 1} failed${msg ? ': ' + msg : ''}.`); }\n    function skip(i, note) { set(i, 'skipped', note); }\n    function retry(i) {\n      const st = S[i]; if (!st) return; st.retries++; st.li.dataset.retries = st.retries; st.badge.textContent = 'retry ' + st.retries;\n      sep(i, 'retry ' + st.retries); st.userClosed = false; start(i);\n    }\n    function reset() {\n      // the state resets at once (so no events fire from stale steps); the visuals cascade back from the last step\n      gen++; runT0 = runEnd = idleSince = 0; clearTimeout(pauseT); lastClock = ''; clock.textContent = '00:00.0';\n      const n = S.length, g = gen;\n      S.forEach(st => { if (st.open) expand(st.i, false); st.s = 'pending'; st.acc = 0; st.retries = 0; st.userClosed = false; st.lastTxt = ''; });\n      el.dataset.state = 'idle'; status.textContent = LABEL.idle; count.textContent = 0;\n      S.forEach((st, i) => { st.stale = true; setTimeout(() => { if (gen === g && st.stale) clearVis(st, true); }, reduce.matches ? 0 : (n - 1 - i) * 45); });\n      wake();\n    }\n    function clearVis(st, soft) {\n      st.stale = false; st.li.dataset.s = st.s; delete st.li.dataset.retries; st.badge.textContent = ''; segs[st.i].dataset.s = st.s;\n      st.time.textContent = ''; st.detail.textContent = st.data.detail || ''; st.row.setAttribute('aria-label', `${st.data.title}, ${WORD[st.s]}`);\n      const wipe = () => { if (st.open) return; st.lines.textContent = ''; st.n = 0; st.lc.textContent = 0; st.row.setAttribute('aria-disabled', 'true'); };\n      if (soft) setTimeout(() => { if (st.s === 'pending' && !st.stale) wipe(); }, 450); else wipe();\n    }\n\n    // ---- input\n    const onClick = e => {\n      const row = e.target.closest('.vs-agent-steps-row'); if (row) { const st = S.find(s => s.row === row); if (st && st.n) { expand(st.i); st.userClosed = !st.open; } return; }\n      const cp = e.target.closest('.vs-agent-steps-copy'); if (cp) { const st = S.find(s => s.copy === cp); copy(st); }\n    };\n    function copy(st) {\n      const text = [...st.lines.children].map(d => d.textContent).join('\\n');\n      const ok = () => { st.copy.classList.add('is-done'); st.copy.setAttribute('aria-label', 'Copied'); clearTimeout(st.cpT); st.cpT = setTimeout(() => { st.copy.classList.remove('is-done'); st.copy.setAttribute('aria-label', 'Copy output'); }, 1500); };\n      const legacy = () => { const t = document.createElement('textarea'); t.value = text; t.style.cssText = 'position:fixed;opacity:0'; document.body.appendChild(t); t.select(); try { document.execCommand('copy'); } catch (e) {} t.remove(); ok(); };\n      if (navigator.clipboard && window.isSecureContext) navigator.clipboard.writeText(text).then(ok, legacy); else legacy();\n    }\n    const onKey = e => {\n      const row = e.target.closest && e.target.closest('.vs-agent-steps-row'); if (!row) return;\n      const i = S.findIndex(s => s.row === row), to = { ArrowDown: i + 1, ArrowUp: i - 1, Home: 0, End: S.length - 1 }[e.key];\n      if (to != null && S[to]) { e.preventDefault(); S[to].row.focus(); }\n    };\n    el.addEventListener('click', onClick); el.addEventListener('keydown', onKey);\n    update();\n\n    const api = {\n      start, log, done, fail, retry, skip, reset, expand: (i, open) => { expand(i, open); if (S[i]) S[i].userClosed = !S[i].open; },\n      detail: (i, t) => { if (S[i]) S[i].detail.textContent = t; },\n      get steps() { return S.map(s => ({ title: s.data.title, state: s.s, retries: s.retries, elapsed: s.acc + (s.s === 'running' ? performance.now() - s.t0 : 0), lines: s.n })); },\n      get gen() { return gen; }, get data() { return steps; },\n      destroy() { gen++; cancelAnimationFrame(raf); io && io.disconnect(); document.removeEventListener('visibilitychange', onVis); el.removeEventListener('click', onClick); el.removeEventListener('keydown', onKey); delete el.__vsAs; },\n    };\n    el.__vsAs = api;\n    if (el.hasAttribute('data-demo')) simulate(api, { loop: el.dataset.demo === 'loop', speed: () => +el.dataset.speed || 1 });\n    return api;\n  }\n\n  // ---- demo runner: plays the steps' own data (log / fail / retryLog / done). Not needed with a real agent.\n  function simulate(run, o = {}) {\n    const g = run.gen, sp = () => (typeof o.speed === 'function' ? o.speed() : o.speed) || 1;\n    let seed = 7; const rnd = () => (seed = (seed * 16807) % 2147483647) / 2147483647;\n    const wait = ms => new Promise(r => setTimeout(r, ms / sp()));\n    const alive = () => run.gen === g;\n    (async () => {\n      await wait(o.delay != null ? o.delay : 500);\n      const data = run.data;\n      for (let i = 0; i < data.length && alive(); i++) {\n        const d = data[i]; run.start(i); await wait(380);\n        for (const l of d.log || []) { if (!alive()) return; run.log(i, l); await wait(170 + rnd() * 230); }\n        if (d.fail && alive()) {\n          await wait(260); run.fail(i, d.fail); await wait(1150); if (!alive()) return;\n          run.retry(i); await wait(300);\n          for (const l of d.retryLog || []) { if (!alive()) return; run.log(i, l); await wait(170 + rnd() * 230); }\n        }\n        if (!alive()) return; await wait(220); run.done(i, d.done || undefined); await wait(300);\n      }\n      if (o.loop && alive()) { await new Promise(r => setTimeout(r, o.hold || 2600)); if (!alive()) return; run.reset(); await new Promise(r => setTimeout(r, 700)); simulate(run, o); }\n    })();\n    return { stop: () => run.reset() };\n  }\n\n  window.vsAgentSteps = { mount, simulate };\n  const auto = () => document.querySelectorAll('.vs-agent-steps:not([data-manual])').forEach(el => mount(el));\n  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', auto); else auto();\n})();\n</script>\n"
    }
  ],
  "meta": {
    "category": "ui",
    "group": "AI interfaces",
    "tags": [
      "ai",
      "agent",
      "steps",
      "timeline",
      "task list",
      "progress",
      "logs",
      "status"
    ],
    "tech": [
      "CSS",
      "JS"
    ],
    "deps": [],
    "source": null,
    "free": true,
    "accent": "#2f6bff",
    "added": "2026-10-01",
    "demo": "https://vividsites.app/api/component/agent-steps/index.html",
    "thumbnail": "https://vividsites.app/api/component/agent-steps/thumb.jpg",
    "video": "https://vividsites.app/api/component/agent-steps/thumb.mp4",
    "page": "https://vividsites.app/c/agent-steps"
  }
}