Skip to content
Star on GitHub
Agent statusThinking
Style
Accent
Sound

Built by DesignMe, free to use under the MIT licence.

Expert design and dev support for startups and enterprises.

Book a call

Thinking

The live trace, written as the agent works, then folded away. Free and MIT licensed, as HTML or a typed React component, in two styles with light and dark mode.

$npx designme add thinking
    01

    Install

    Add it to your project with the CLI.

    HTML, CSS and JavaScript

    $npx designme add thinking

    React

    $npx designme add thinking --react

    The CLI copies the source into your project, so you own it and can change anything. The HTML version lands in ./designme with the shared core and a demo.html. The React version lands in src/components/designme and needs React 19; it works in Next.js, Vite and Astro.

    02

    About

    What it is for and what it does.

    Thinking is the live trace an agent writes while it works: the steps it takes, one under another on a line that turns green as each finishes, each with what it found. A step that runs into a problem shows it in amber while it waits and tries again. When the work is done, the header says what it came to and the trace folds away, one click from opening again.

    • When an agent works for more than a few seconds and people want to see what it is doing rather than a spinner.
    • When each step has a result worth reading, like how many frames it opened or which cards failed a check.
    • When a step can hit a timeout or a rate limit and the agent waits it out instead of failing.
    • When the trace should step aside once the answer is ready but stay one click away.
    03

    Usage

    Import it and drive it from your own state.

    Import

    import { Thinking } from '@/components/designme/thinking/Thinking';

    Example

    The demo above as a React component, 45 lines you can copy.

    'use client';
    // The animated preview from designme.dev, rebuilt from the React component: an example of driving it from your own state.
    import { useRef, useState } from 'react';
    import { Thinking, type ThinkingStep } from './Thinking';
    import { useScript, useStory } from '../lib/useScript';
    
    /* ---------- Thinking: each step and what it found; measuring the cards times out once and tries again; then it folds away ---------- */
    export const PRICING_STEPS: { label: string; result: string; ms: number; retry?: string; tip?: string }[] = [
      { label: 'Open Pricing v4 in Figma', result: '6 frames, 3 plan cards', ms: 1300 },
      { label: 'Read the spacing tokens', result: 'tokens.css, 9 spacing values', ms: 1200 },
      { label: 'Measure the plan cards', result: '3 cards are off the 8px grid', ms: 1400, retry: "Figma didn't answer", tip: 'Pro has an 18px gap, Team 22px and Enterprise 13px' },
      { label: 'Check text contrast', result: 'All 14 text pairs pass AA', ms: 1100 },
      { label: 'Plan the fix', result: '2 changes, about 3 minutes', ms: 1200 }
    ];
    
    export default function ThinkingDemo() {
      const ref = useRef<HTMLDivElement>(null);
      const [steps, setSteps] = useState<ThinkingStep[]>([]);
      const [running, setRunning] = useState(true);
      const [open, setOpen] = useState(true);
      useScript(async ({ wait }) => {
        await wait(600);
        const out: ThinkingStep[] = [];
        const show = () => setSteps(out.slice());
        for (const s of PRICING_STEPS) {
          out.push({ label: s.label, state: 'running' }); show();
          const at = out.length - 1;
          let took = s.ms;
          await wait(s.ms);
          if (s.retry) {
            took += 2900;
            for (let k = 2; k > 0; k--) { out[at] = { ...out[at], state: 'retry', result: `${s.retry}. Trying again in ${k}s` }; show(); await wait(1000); }
            out[at] = { ...out[at], state: 'running', result: 'Trying again' }; show(); await wait(900);
          }
          out[at] = { ...out[at], state: 'done', result: s.result, resultTip: s.tip, time: (took / 1000).toFixed(1) + 's' }; show();
        }
        await wait(500); setRunning(false);
      });
      // the preview's story: once it has folded, the person opens it again to read what the cards measured
      useStory(ref, async ({ wait }) => {
        await wait(16000); setOpen(true);
      });
      return <div ref={ref} style={{ width: '100%' }}><Thinking steps={steps} running={running} title="Checking the pricing page" summary="3 cards to fix, 2 changes planned" open={open} onOpenChange={setOpen} /></div>;
    }
    
    04

    Props

    Everything Thinking takes.

    PropTypeWhat it does
    stepsrequiredThinkingStep[]
    runningrequiredbooleanSpins, counts seconds and keeps the trace open while true
    secondsnumberSeconds to show. Leave it out to let it count while running
    titlestringThe header while it runs, like "Checking the pricing page"
    summarystringThe header once it is done: what the work came to, like "3 cards to fix, 2 changes planned". Defaults to the number of steps
    collapseWhenDonebooleanFolds the trace away a moment after it finishes
    openbooleanWhether the trace is open. Leave it out to let the header toggle it
    defaultOpenboolean
    onOpenChange(open: boolean) => void
    classNamestring
    05

    Code

    The full source, as the CLI installs it.

    Thinking.tsxReact · 117 lines
    'use client';
    import { useEffect, useRef } from 'react';
    import { cx } from '../lib/cx';
    import { useControllable, useIsoLayoutEffect, useReducedMotion, useSeconds } from '../lib/hooks';
    import { Icon } from '../lib/icons';
    import { enter, Fold, Spin, Swap, Think, Tip } from '../lib/motion';
    import '../designme.css';
    import './thinking.css';
    
    export interface ThinkingStep {
      /** What the agent does, like "Measure the plan cards" */
      label: string;
      /** running spins and shimmers, retry waits out a problem in amber, done turns into a check */
      state: 'running' | 'retry' | 'done';
      /** What the step found, or what it waits on, under it: "3 cards are off the 8px grid". Opens and crossfades when it changes */
      result?: string;
      /** More detail shown when you hover the result, like the measured values */
      resultTip?: string;
      /** How long the step took, like "1.3s", shown at its right once it is done */
      time?: string;
    }
    
    export interface ThinkingProps {
      steps: ThinkingStep[];
      /** Spins, counts seconds and keeps the trace open while true */
      running: boolean;
      /** Seconds to show. Leave it out to let it count while running */
      seconds?: number;
      /** The header while it runs, like "Checking the pricing page" */
      title?: string;
      /** The header once it is done: what the work came to, like "3 cards to fix, 2 changes planned". Defaults to the number of steps */
      summary?: string;
      /** Folds the trace away a moment after it finishes */
      collapseWhenDone?: boolean;
      /** Whether the trace is open. Leave it out to let the header toggle it */
      open?: boolean;
      defaultOpen?: boolean;
      onOpenChange?: (open: boolean) => void;
      className?: string;
    }
    
    /** A line that settles in place when its words change, sharpening out of a soft blur. While it shimmers it just takes the new words */
    function Settle({ children, shimmer, className }: { children: string; shimmer: boolean; className?: string }) {
      const ref = useRef<HTMLSpanElement>(null);
      const prev = useRef({ text: children, shimmer });
      const reduce = useReducedMotion();
      useIsoLayoutEffect(() => {
        const el = ref.current, was = prev.current; prev.current = { text: children, shimmer };
        if (!el) return;
        // the shimmer draws the words in a clear colour; going straight to the solid colour stops it fading in from nothing
        if (was.shimmer && !shimmer){ el.style.transition = 'none'; void el.offsetWidth; requestAnimationFrame(() => { el.style.transition = ''; }); }
        if (shimmer || reduce || was.text === children) return;
        const bp = getComputedStyle(el).getPropertyValue('--dm-motion').trim() === 'bp';
        el.animate(bp ? [{ opacity: .6 }, { opacity: 1 }] : [{ opacity: .6, filter: 'blur(1.5px)' }, { opacity: 1, filter: 'blur(0)' }],
          { duration: bp ? 220 : 460, easing: bp ? 'steps(3, end)' : 'cubic-bezier(.22,1,.36,1)' });
      }, [children, shimmer]);
      return <span ref={ref} className={cx(shimmer && 'dm-shim', className)}>{children}</span>;
    }
    
    /** What a finished step found, written in a word at a time */
    function Words({ text }: { text: string }) {
      const w = text.split(' ');
      return <>{w.map((x, i) => <span key={i} className="dm-w" style={{ animationDelay: `${i * 45}ms` }}>{x}{i < w.length - 1 ? ' ' : ''}</span>)}</>;
    }
    
    function Mark({ state }: { state: ThinkingStep['state'] }) {
      if (state === 'running') return <Spin />;
      // keyed so each new mark plays its entrance once
      return <span key={state} className="dm-enter" style={{ display: 'grid' }}><Icon name={state === 'retry' ? 'clock' : 'check'} size="xs" /></span>;
    }
    
    /** The live trace: each step the agent takes on a line that turns green as they finish, and what each found, then a header
        that says what the work came to, folded away. */
    export function Thinking({ steps, running, seconds, title = 'Working on it', summary, collapseWhenDone = true, open, defaultOpen = true, onOpenChange, className }: ThinkingProps) {
      const counted = useSeconds(running && seconds === undefined);
      const secs = seconds ?? counted;
      const [isOpen, setOpen] = useControllable(open, defaultOpen, onOpenChange);
      // once it finishes, the trace waits a beat and folds itself away
      useEffect(() => {
        if (running || !collapseWhenDone || !steps.length) return;
        const t = setTimeout(() => setOpen(false), 2500);
        return () => clearTimeout(t);
      }, [running, collapseWhenDone, steps.length]); // eslint-disable-line react-hooks/exhaustive-deps
      const done = !running && steps.length > 0;
    
      return (
        <div data-dm="" className={cx('dm-tk', className)} data-open={isOpen ? '1' : '0'}>
          <button type="button" className={cx('dm-tk-h', done && 'dm-tk-fin')} aria-expanded={isOpen} onClick={() => setOpen(!isOpen)}>
            <span className="dm-tk-mark">{done ? <span key="ok" {...enter()} style={{ display: 'grid' }}><Icon name="ok" /></span> : <Think on={running} />}</span>
            <Settle className="dm-tk-title" shimmer={running}>{done ? summary ?? `${steps.length} ${steps.length === 1 ? 'step' : 'steps'}` : title}</Settle>
            <span className="dm-tk-time dm-num">{secs + 's'}</span>
            <Icon name="chev-d" size="xs" className="dm-tk-chev" />
          </button>
          <Fold open={isOpen} className="dm-tk-fold">
            <ol className="dm-tk-list" aria-live="polite">
              {steps.map((s, i) => (
                <li key={i} className={cx('dm-tk-st dm-enter', s.state === 'running' ? 'dm-run' : 'dm-' + s.state)}>
                  <span className="dm-mk"><Mark state={s.state} /></span>
                  <div className="dm-tx">
                    <span className="dm-tk-ln">
                      <Settle className="dm-lab" shimmer={s.state === 'running'}>{s.label}</Settle>
                      {s.state === 'done' && s.time && <span key="t" className="dm-tk-dur dm-num dm-enter">{s.time}</span>}
                    </span>
                    <span className={cx('dm-res', s.result && 'dm-on')}>
                      {s.state === 'done' && s.result
                        ? <span key={'w' + s.result}>{s.resultTip ? <Tip label={s.resultTip}><span tabIndex={0} data-tip=""><Words text={s.result} /></span></Tip> : <Words text={s.result} />}</span>
                        : <Swap>{s.result ?? ''}</Swap>}
                    </span>
                  </div>
                </li>
              ))}
            </ol>
          </Fold>
        </div>
      );
    }
    
    thinking.cssReact · 55 lines
    /* Generated from designme.dev by scripts/sync.mjs. Edit the site, then sync. */
    /* ---------- Thinking ---------- */
    /* the steps the agent takes, on a line that turns green as each one finishes; the step in hand turns in the accent with a
       soft halo, and what each step found writes itself in under it. The header ends as a summary of the work */
    /* neither the trace nor the box it sits in anchors the page's scroll: a centred demo that grows would otherwise pull
       the page along with it */
    .dm-stage-in:has(>.dm-tk){overflow-anchor:none}
    .dm-tk{font-size:14px;width:100%;max-width:420px;margin-inline:auto;overflow-anchor:none}
    /* on a card the trace keeps the room it will fill, so the header stays put as steps arrive */
    .dm-stage-in>.dm-tk{min-height:inherit}
    .dm-tk-h{display:flex;align-items:center;gap:10px;width:calc(100% + 16px);margin:0 -8px;padding:6px var(--dm-s-2);border-radius:10px;text-align:left;transition:background var(--dm-t-fast)}
    @media (hover:hover) and (pointer:fine){.dm-tk-h:hover{background:var(--dm-row)}}
    .dm-tk-mark{width:18px;height:18px;flex:none;display:grid;place-items:center;color:var(--dm-accent-mark,var(--dm-accent));transition:color .3s}
    .dm-tk-h.dm-tk-fin .dm-tk-mark{color:var(--dm-ok)}
    .dm-tk-h.dm-tk-fin .dm-tk-mark .dm-i{animation:dm-tkpop .5s var(--dm-ease) backwards}
    .dm-tk-title{flex:1;min-width:0;color:var(--dm-fg)}
    .dm-tk-time{color:var(--dm-muted);font-size:13px}
    .dm-tk-chev{color:var(--dm-muted);transition:transform .32s var(--dm-ease)}
    .dm-tk[data-open="0"] .dm-tk-chev{transform:rotate(-90deg)}
    .dm-tk-list{list-style:none;margin:0;padding:6px 0 0;display:flex;flex-direction:column}
    .dm-tk-st{position:relative;display:flex;gap:10px;padding:6px 0}
    /* the line from each step down to the next: grey, and green once the step above it is done */
    .dm-tk-st:not(:last-child)::before,.dm-tk-st.dm-done:not(:last-child)::after{content:"";position:absolute;left:8.25px;top:29px;bottom:-5px;width:1.5px;border-radius:1px;background:var(--dm-line)}
    .dm-tk-st.dm-done:not(:last-child)::after{background:color-mix(in srgb,var(--dm-ok) 55%,var(--dm-line));transform-origin:top;animation:dm-tkgrow .6s var(--dm-ease) backwards}
    @keyframes dm-tkgrow{from{transform:scaleY(0)}}
    .dm-tk-st .dm-mk{position:relative;width:18px;height:18px;flex:none;margin-top:1px;display:grid;place-items:center;border-radius:50%;color:var(--dm-muted);transition:background .32s,color .32s}
    .dm-tk-st .dm-mk .dm-spin{width:10px;height:10px}
    .dm-tk-st.dm-run .dm-mk{color:var(--dm-accent-mark,var(--dm-accent));background:var(--dm-accent-bg);animation:dm-tkhalo 1.8s var(--dm-ease-io) infinite}
    .dm-tk-st.dm-done .dm-mk{background:var(--dm-ok-bg);color:var(--dm-ok)}
    .dm-tk-st.dm-done .dm-mk .dm-i{animation:dm-tkpop .45s var(--dm-ease) backwards}
    .dm-tk-st.dm-retry .dm-mk{background:var(--dm-warn-bg);color:var(--dm-warn);animation:dm-tkhalow 1.8s var(--dm-ease-io) infinite}
    @keyframes dm-tkhalo{0%,100%{box-shadow:0 0 0 0 color-mix(in srgb,var(--dm-accent-mark,var(--dm-accent)) 0%,transparent)}50%{box-shadow:0 0 0 4px color-mix(in srgb,var(--dm-accent-mark,var(--dm-accent)) 14%,transparent)}}
    @keyframes dm-tkhalow{0%,100%{box-shadow:0 0 0 0 color-mix(in srgb,var(--dm-warn) 0%,transparent)}50%{box-shadow:0 0 0 4px color-mix(in srgb,var(--dm-warn) 16%,transparent)}}
    @keyframes dm-tkpop{0%{transform:scale(.4);opacity:0}60%{transform:scale(1.15);opacity:1}100%{transform:none}}
    .dm-tk-st .dm-tx{flex:1;min-width:0;display:flex;flex-direction:column}
    .dm-tk-st .dm-tk-ln{display:flex;align-items:baseline;gap:var(--dm-s-2)}
    .dm-tk-st .dm-lab{flex:1;min-width:0;color:var(--dm-fg);transition:color .32s}
    .dm-tk-st.dm-done .dm-lab{color:var(--dm-fg2)}
    /* how long the step took, once it is done */
    .dm-tk-st .dm-tk-dur{flex:none;font-size:12px;color:var(--dm-muted)}
    /* what a step found opens under it, a word at a time */
    .dm-tk-st .dm-res{display:grid;grid-template-rows:0fr;font-size:13px;color:var(--dm-muted);transition:grid-template-rows .32s var(--dm-ease),color .32s}
    .dm-tk-st .dm-res.dm-on{grid-template-rows:1fr}
    .dm-tk-st .dm-res>span{overflow:hidden;min-height:0}
    .dm-tk-st .dm-res .dm-w{display:inline-block;white-space:pre;animation:dm-tkword .42s var(--dm-ease) backwards}
    @keyframes dm-tkword{from{opacity:0;transform:translateY(3px);filter:blur(2px)}}
    .dm-tk-st.dm-retry .dm-res{color:var(--dm-warn)}
    .dm-tk-st .dm-res [data-tip]{text-decoration:underline dotted var(--dm-faint);text-underline-offset:3px;cursor:default}
    :root:not([data-style="soft"]) .dm-tk-st.dm-run .dm-mk:not(:where([data-style="soft"],[data-style="soft"] *)),:root:not([data-style="soft"]) .dm-tk-st.dm-retry .dm-mk:not(:where([data-style="soft"],[data-style="soft"] *)){animation:none}
    :root:not([data-style="soft"]) .dm-tk-st.dm-run .dm-mk:not(:where([data-style="soft"],[data-style="soft"] *)){box-shadow:inset 0 0 0 1px var(--dm-accent-mark)}
    :root:not([data-style="soft"]) .dm-tk-st.dm-done:not(:last-child):not(:where([data-style="soft"],[data-style="soft"] *))::after{background:var(--dm-ok);width:1px;left:8.5px}
    :root:not([data-style="soft"]) .dm-tk-st:not(:last-child):not(:where([data-style="soft"],[data-style="soft"] *))::before{width:1px;left:8.5px;background:repeating-linear-gradient(var(--dm-rail) 0 3px,transparent 3px 6px)}
    :root:not([data-style="soft"]) .dm-tk-st.dm-done .dm-mk .dm-i:not(:where([data-style="soft"],[data-style="soft"] *)),:root:not([data-style="soft"]) .dm-tk-h.dm-tk-fin .dm-tk-mark .dm-i:not(:where([data-style="soft"],[data-style="soft"] *)){animation:none}
    :root:not([data-style="soft"]) .dm-tk-st .dm-res .dm-w:not(:where([data-style="soft"],[data-style="soft"] *)){animation:dm-bpin .3s var(--dm-ease) backwards}
    
    thinking.htmlHTML · 13 lines
    <div class="card bare" data-c="thinking">
      <div class="stage-in">
        <div class="tk" data-open="1">
          <button class="tk-h" aria-expanded="true">
            <span class="tk-mark"><span class="think on"><i></i><i></i><i></i><i></i></span></span>
            <span class="tk-title shim">Checking the pricing page</span><span class="tk-time num">0s</span>
            <svg class="i xs tk-chev"><use href="#i-chev-d"/></svg>
          </button>
          <div class="fold tk-fold"><div><ol class="tk-list" aria-live="polite"></ol></div></div>
        </div>
      </div>
    </div>
    
    thinking.cssHTML · 54 lines
    /* ---------- Thinking ---------- */
    /* the steps the agent takes, on a line that turns green as each one finishes; the step in hand turns in the accent with a
       soft halo, and what each step found writes itself in under it. The header ends as a summary of the work */
    /* neither the trace nor the box it sits in anchors the page's scroll: a centred demo that grows would otherwise pull
       the page along with it */
    .stage-in:has(>.tk){overflow-anchor:none}
    .tk{font-size:14px;width:100%;max-width:420px;margin-inline:auto;overflow-anchor:none}
    /* on a card the trace keeps the room it will fill, so the header stays put as steps arrive */
    .stage-in>.tk{min-height:inherit}
    .tk-h{display:flex;align-items:center;gap:10px;width:calc(100% + 16px);margin:0 -8px;padding:6px var(--s-2);border-radius:10px;text-align:left;transition:background var(--t-fast)}
    @media (hover:hover) and (pointer:fine){.tk-h:hover{background:var(--row)}}
    .tk-mark{width:18px;height:18px;flex:none;display:grid;place-items:center;color:var(--accent-mark,var(--accent));transition:color .3s}
    .tk-h.tk-fin .tk-mark{color:var(--ok)}
    .tk-h.tk-fin .tk-mark .i{animation:tkpop .5s var(--ease) backwards}
    .tk-title{flex:1;min-width:0;color:var(--fg)}
    .tk-time{color:var(--muted);font-size:13px}
    .tk-chev{color:var(--muted);transition:transform .32s var(--ease)}
    .tk[data-open="0"] .tk-chev{transform:rotate(-90deg)}
    .tk-list{list-style:none;margin:0;padding:6px 0 0;display:flex;flex-direction:column}
    .tk-st{position:relative;display:flex;gap:10px;padding:6px 0}
    /* the line from each step down to the next: grey, and green once the step above it is done */
    .tk-st:not(:last-child)::before,.tk-st.done:not(:last-child)::after{content:"";position:absolute;left:8.25px;top:29px;bottom:-5px;width:1.5px;border-radius:1px;background:var(--line)}
    .tk-st.done:not(:last-child)::after{background:color-mix(in srgb,var(--ok) 55%,var(--line));transform-origin:top;animation:tkgrow .6s var(--ease) backwards}
    @keyframes tkgrow{from{transform:scaleY(0)}}
    .tk-st .mk{position:relative;width:18px;height:18px;flex:none;margin-top:1px;display:grid;place-items:center;border-radius:50%;color:var(--muted);transition:background .32s,color .32s}
    .tk-st .mk .spin{width:10px;height:10px}
    .tk-st.run .mk{color:var(--accent-mark,var(--accent));background:var(--accent-bg);animation:tkhalo 1.8s var(--ease-io) infinite}
    .tk-st.done .mk{background:var(--ok-bg);color:var(--ok)}
    .tk-st.done .mk .i{animation:tkpop .45s var(--ease) backwards}
    .tk-st.retry .mk{background:var(--warn-bg);color:var(--warn);animation:tkhalow 1.8s var(--ease-io) infinite}
    @keyframes tkhalo{0%,100%{box-shadow:0 0 0 0 color-mix(in srgb,var(--accent-mark,var(--accent)) 0%,transparent)}50%{box-shadow:0 0 0 4px color-mix(in srgb,var(--accent-mark,var(--accent)) 14%,transparent)}}
    @keyframes tkhalow{0%,100%{box-shadow:0 0 0 0 color-mix(in srgb,var(--warn) 0%,transparent)}50%{box-shadow:0 0 0 4px color-mix(in srgb,var(--warn) 16%,transparent)}}
    @keyframes tkpop{0%{transform:scale(.4);opacity:0}60%{transform:scale(1.15);opacity:1}100%{transform:none}}
    .tk-st .tx{flex:1;min-width:0;display:flex;flex-direction:column}
    .tk-st .tk-ln{display:flex;align-items:baseline;gap:var(--s-2)}
    .tk-st .lab{flex:1;min-width:0;color:var(--fg);transition:color .32s}
    .tk-st.done .lab{color:var(--fg2)}
    /* how long the step took, once it is done */
    .tk-st .tk-dur{flex:none;font-size:12px;color:var(--muted)}
    /* what a step found opens under it, a word at a time */
    .tk-st .res{display:grid;grid-template-rows:0fr;font-size:13px;color:var(--muted);transition:grid-template-rows .32s var(--ease),color .32s}
    .tk-st .res.on{grid-template-rows:1fr}
    .tk-st .res>span{overflow:hidden;min-height:0}
    .tk-st .res .w{display:inline-block;white-space:pre;animation:tkword .42s var(--ease) backwards}
    @keyframes tkword{from{opacity:0;transform:translateY(3px);filter:blur(2px)}}
    .tk-st.retry .res{color:var(--warn)}
    .tk-st .res [data-tip]{text-decoration:underline dotted var(--faint);text-underline-offset:3px;cursor:default}
    :root:not([data-style="soft"]) .tk-st.run .mk:not(:where([data-style="soft"],[data-style="soft"] *)),:root:not([data-style="soft"]) .tk-st.retry .mk:not(:where([data-style="soft"],[data-style="soft"] *)){animation:none}
    :root:not([data-style="soft"]) .tk-st.run .mk:not(:where([data-style="soft"],[data-style="soft"] *)){box-shadow:inset 0 0 0 1px var(--accent-mark)}
    :root:not([data-style="soft"]) .tk-st.done:not(:last-child):not(:where([data-style="soft"],[data-style="soft"] *))::after{background:var(--ok);width:1px;left:8.5px}
    :root:not([data-style="soft"]) .tk-st:not(:last-child):not(:where([data-style="soft"],[data-style="soft"] *))::before{width:1px;left:8.5px;background:repeating-linear-gradient(var(--rail) 0 3px,transparent 3px 6px)}
    :root:not([data-style="soft"]) .tk-st.done .mk .i:not(:where([data-style="soft"],[data-style="soft"] *)),:root:not([data-style="soft"]) .tk-h.tk-fin .tk-mark .i:not(:where([data-style="soft"],[data-style="soft"] *)){animation:none}
    :root:not([data-style="soft"]) .tk-st .res .w:not(:where([data-style="soft"],[data-style="soft"] *)){animation:bpin .3s var(--ease) backwards}
    
    thinking.jsHTML · 49 lines
    DM.def('thinking', (r, c) => {
      const tk = $('.tk', r), list = $('.tk-list', r), title = $('.tk-title', r), time = $('.tk-time', r), head = $('.tk-h', r), fold = $('.tk-fold', r), mark = $('.tk-mark', r);
      // each step: what the agent does and what it found. Measuring the cards times out once, counts down and tries again
      const S = [
        ['Open Pricing v4 in Figma', '6 frames, 3 plan cards', 1300],
        ['Read the spacing tokens', 'tokens.css, 9 spacing values', 1200],
        ['Measure the plan cards', '3 cards are off the 8px grid', 1400, { retry:"Figma didn't answer", tip:'Pro has an 18px gap, Team 22px and Enterprise 13px' }],
        ['Check text contrast', 'All 14 text pairs pass AA', 1100],
        ['Plan the fix', '2 changes, about 3 minutes', 1200]
      ];
      const setOpen = o => { tk.dataset.open = o ? '1' : '0'; fold.classList.toggle('shut', !o); head.setAttribute('aria-expanded', o); };
      head.onclick = () => setOpen(tk.dataset.open !== '1');
      // the line under a step opens and then crossfades; what the step found writes itself in a word at a time, and a tip on it
      // holds the detail
      const words = t => t.split(' ').map((w, i, a) => `<span class="w" style="animation-delay:${i * 45}ms">${w}${i < a.length - 1 ? ' ' : ''}</span>`).join('');
      const note = (li, text, tip, found) => { const res = $('.res', li), sp = res.firstElementChild; res.classList.add('on');
        if (found){ sp._sw = null; sp.classList.remove('out'); sp.innerHTML = tip ? `<span data-tip="${tip}">${words(text)}</span>` : words(text); }
        else swap(sp, text); };
      const glyph = (li, g) => { const mk = $('.mk', li); mk.innerHTML = g; enter(mk.firstElementChild); };
      let secs = 0, live = true; c.every(1000, () => { if (live){ secs++; time.textContent = secs + 's'; } });
      c.run(async () => {
        await c.wait(600);
        for (const [lab, res, ms, x] of S){
          const li = html(`<li class="tk-st run"><span class="mk"><span class="spin"></span></span><div class="tx"><span class="tk-ln"><span class="lab shim">${lab}</span><span class="tk-dur num"></span></span><span class="res"><span></span></span></div></li>`);
          list.appendChild(li); enter(li);
          const l = $('.lab', li); let took = ms;
          await c.wait(ms);
          if (x && x.retry){
            li.classList.replace('run', 'retry'); l.classList.remove('shim'); glyph(li, I('clock','xs'));
            for (let k = 2; k > 0; k--){ note(li, `${x.retry}. Trying again in ${k}s`); await c.wait(1000); }
            li.classList.replace('retry', 'run'); l.classList.add('shim'); glyph(li, '<span class="spin"></span>'); note(li, 'Trying again'); await c.wait(900); took += 2900;
          }
          li.classList.replace('run', 'done'); l.classList.remove('shim'); glyph(li, I('check','xs')); note(li, res, x && x.tip, true);
          const t = $('.tk-dur', li); t.textContent = (took / 1000).toFixed(1) + 's'; enter(t);
        }
        // the header becomes what the work came to, and folds the trace away
        await c.wait(500); live = false;
        head.classList.add('tk-fin'); mark.innerHTML = I('ok'); enter(mark.firstElementChild); title.classList.remove('shim'); swap(title, '3 cards to fix, 2 changes planned');
        await c.wait(2000); setOpen(false);
      });
      // the preview's story: once it has folded, the person opens it again and reads what the cards measured
      if (r === c.st) c.story(async s => {
        await s.until(() => fold.classList.contains('shut')); await s.beat(2);
        await s.tap(head); await s.beat(2);
        await s.point($('.res [data-tip]', list)); await s.beat(3);
        s.away(); await s.beat(1);
      });
    });
    
    06

    Questions

    What people ask about it.

    How do I show a step that is waiting out a problem?
    Set its state to 'retry' and put what it waits on in result, like "Figma didn't answer. Trying again in 2s". Update the result each second for a countdown, then set the state back to 'running' and finally 'done' with what it found.
    What does the header say when the work is done?
    Whatever you pass as summary, which should say what the work came to. Without it, the header says how many steps there were.
    Can I keep the trace open after it finishes?
    Yes. Set collapseWhenDone to false, or control it with open and onOpenChange.
    07

    More in Agent status

    Show what the agent is doing, how far along it is, and what it is working with.

    Reasoning trailThe agent's plan, notes, sources and edits, after it thinks.
    Agent lanesSeveral agents working in parallel, each with its own lane and hand-offs.
    Agent treeAn orchestrator and its sub-agents, with cost rolled up per branch.
    Agent inboxEvery run that needs you, is running or is done, in one list.
    Trace viewOne agent run as a waterfall of spans, with latency and tokens.
    Browser sessionThe agent browsing for you, step by step, with Pause and Take over.
    Run summaryWhat the agent did, what changed and what needs you.
    Agent run timelineEvery model and tool step of a run on one time axis.
    All 18 in Agent status →