Agent statusThinking
Built by DesignMe, free to use under the MIT licence.
Expert design and dev support for startups and enterprises.
Book a callThinking
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.
Install
Add it to your project with the CLI.
HTML, CSS and JavaScript
npx designme add thinkingReact
npx designme add thinking --reactThe 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.
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.
- Each step has a
label, astate, an optionalresultand an optionaltime. A running step turns in the accent with a soft halo and its label shimmers,retryshows a clock in amber while the step waits on a problem, anddoneturns into a check. - The line from each step down to the next turns green, top to bottom, once the step above it is done, so the trace fills in as the work gets through it.
- While a step waits or retries its
resultcrossfades in place, so a countdown like "Trying again in 2s" updates where it is. Once the step is done, what it found writes itself in a word at a time, and itstime, like "1.3s", shows at its right.resultTipadds more detail on hover. - The header shows
titlewhile it runs, like "Checking the pricing page", beside four dots turning in the accent, and a clock that counts by itself unless you passseconds. - When
runningturns false, the header becomessummary, like "3 cards to fix, 2 changes planned", with a check. WithcollapseWhenDoneon, the default, the trace folds away a moment later. - The header toggles the trace open and shut, and
open,defaultOpenandonOpenChangelet you control it.
- The header is a button with
aria-expanded, so screen readers hear whether the trace is open. - The list of steps is an
aria-live="polite"region, so new steps and their results are announced as they arrive. - The line between the steps is decoration: each step's state is in its mark and its words.
- With
prefers-reduced-motion: reduce, the spinner, halo, shimmer, the line's growth and the words' entrance stop and changes appear at once.
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>;
}
Props
Everything Thinking takes.
| Prop | Type | What it does |
|---|---|---|
stepsrequired | ThinkingStep[] | |
runningrequired | boolean | Spins, counts seconds and keeps the trace open while true |
seconds | number | Seconds to show. Leave it out to let it count while running |
title | string | The header while it runs, like "Checking the pricing page" |
summary | 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 |
collapseWhenDone | boolean | Folds the trace away a moment after it finishes |
open | boolean | Whether the trace is open. Leave it out to let the header toggle it |
defaultOpen | boolean | |
onOpenChange | (open: boolean) => void | |
className | string |
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);
});
});
Questions
What people ask about it.
- How do I show a step that is waiting out a problem?
- Set its
stateto'retry'and put what it waits on inresult, 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
collapseWhenDoneto false, or control it withopenandonOpenChange.
More in Agent status
Show what the agent is doing, how far along it is, and what it is working with.