Agent statusAgent phases
Built by DesignMe, free to use under the MIT licence.
Expert design and dev support for startups and enterprises, from Astro sites to React apps and AI products.
Book a callAgent phases
One line for which phase the agent is in, from thinking to done, that stops to ask you, says when it is slow, and folds to how long it took. Free and MIT licensed, as HTML, a typed React component or an Astro 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 agent-phasesReact
npx designme add agent-phases --reactAstro
npx designme add agent-phases --astroThe 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 islands. The Astro version lands in src/components/designme too, with the same markup, CSS and script as the HTML version. See Astro.
shadcn CLI, React
npx shadcn@latest add https://designme.dev/r/shadcn/agent-phases.json shadcn CLI, Astro
npx shadcn@latest add https://designme.dev/r/shadcn/astro/agent-phases.json In a project set up for shadcn/ui, its CLI adds the same files to your components folder, under designme/, with the shared core. To give the components your shadcn or Tailwind v4 theme, add the theme bridge.
About
What it is for and what it does.
One line that says which phase the agent is in: Thinking, Searching, Reading, Waiting for you, Writing and Done, each with its own glyph, over a track of six segments that fill phase by phase. When the agent needs a decision it stops the clock and asks, with Approve and Not now; when a phase runs long it says it is still working and offers Stop; when a step fails the line turns red and offers Retry. Once done it reads "Thought for 12s" and opens to the time each phase took.
- Above an agent's reply, when people only need to know what stage it is at rather than every step.
- When the agent can stop to ask for approval in the middle of its work.
- Where the detailed trace (Thinking or Reasoning trail) would be too much, as a lighter companion to them.
- Six phases, each with a glyph: turning dots, a radar, a page, a hand, a pen and a check. The phase's words shimmer while it lasts.
- A track of six short segments: done phases fill in the accent, the phase in hand breathes, and the whole track turns green once done.
- A timer that counts seconds by itself (or takes
seconds) and pauses while it waits for you. ask: in the waiting phase a tinted box asks the question, with Approve (primary) and Not now.slow: a quiet line with a clock, "Still working…", and Stop whenonStopis set.error: the glyph and the line turn red, with the detail and Retry under it.- Done: the line reads "Thought for 12s" and becomes a button that opens the list of phases with the time each took (
took).
- A role="status" region announces each change of phase (and an error, and Done with the time), never every second.
- Once done the line is a button with aria-expanded; before that it is marked aria-disabled.
- Approve, Not now, Stop and Retry are real buttons, reached with Tab.
- With reduced motion the turning dots, the radar and the breathing segment stand still.
Usage
Import it in React and drive it from your own state, or place it in an Astro page.
React
import { AgentPhases } from '@/components/designme/agent-phases/AgentPhases';React example
The demo above as a React component, 48 lines you can copy.
'use client';
// The animated preview from designme.dev, rebuilt from the React component: the search times out and is retried, reading is
// slow, the agent asks before offering a refund, and the finished line opens to the phases.
import { useEffect, useRef, useState } from 'react';
import { AgentPhases, type AgentPhase } from './AgentPhases';
import { useStory, useWait } from '../lib/useScript';
export default function AgentPhasesDemo() {
const ref = useRef<HTMLDivElement>(null);
const wait = useWait();
const [phase, setPhase] = useState<AgentPhase>('thinking');
const [label, setLabel] = useState<string | undefined>('Thinking about the question');
const [failed, setFailed] = useState(false);
const [slow, setSlow] = useState(false);
const [open, setOpen] = useState(false);
const [took, setTook] = useState<Partial<Record<AgentPhase, number>>>({});
const answer = useRef<((v: boolean) => void) | null>(null);
const until = (f: (done: (v: boolean) => void) => void) => new Promise<boolean>(res => f(res));
// the agent's work, which goes on whatever the visitor does; it waits for a person only to retry and to approve
useEffect(() => { void (async () => {
await wait(1600); setTook({ thinking: 2 });
setPhase('searching'); setLabel('Searching the help centre'); await wait(1300);
setFailed(true); await until(res => { answer.current = res; }); setFailed(false);
setLabel('Searching the help centre again'); await wait(1200); setTook(t => ({ ...t, searching: 4 }));
setPhase('reading'); setLabel('Reading 3 articles'); await wait(1800); setSlow(true); await wait(2200); setSlow(false); setTook(t => ({ ...t, reading: 4 }));
setPhase('waiting'); setLabel(undefined);
const ok = await until(res => { answer.current = res; });
setTook(t => ({ ...t, waiting: 2 }));
setPhase('writing'); setLabel(ok ? 'Writing the reply with the refund' : 'Writing the reply without a refund'); await wait(2000); setTook(t => ({ ...t, writing: 2 }));
setPhase('done'); setLabel(undefined);
})(); }, []); // eslint-disable-line react-hooks/exhaustive-deps
const reply = (v: boolean) => { const f = answer.current; answer.current = null; f?.(v); };
// the person in the preview: retry, approve, then open the finished line
useStory(ref, async ({ wait, press }) => {
await wait(4400); press('.dm-aph-n .dm-pill');
await wait(9000); press('.dm-aph-n .dm-primary');
await wait(3500); press('.dm-aph-h');
}, { hold: false });
return (
<div ref={ref} style={{ width: '100%' }}>
<AgentPhases phase={phase} label={label} took={took} open={open} onOpenChange={setOpen}
error={failed ? { text: 'The search timed out', detail: "The help centre didn't answer in 10s", onRetry: () => reply(true) } : undefined}
slow={slow ? { text: 'Still working, the refund policy is a long page' } : undefined}
ask={{ text: 'Offer Maya a refund of $48?', onApprove: () => reply(true), onDecline: () => reply(false) }} />
</div>
);
}
Astro
In a page in src/pages. It plays the demo above; change its agent-phases.js to show your own data. Attributes like class go on its root element.
---
import AgentPhases from '../components/designme/agent-phases/AgentPhases.astro';
---
<AgentPhases />
<AgentPhases data-style="soft" class="my-class" />Props
Everything AgentPhases takes.
| Prop | Type | What it does |
|---|---|---|
phaserequired | AgentPhase | |
label | string | What the line says, like "Reading 3 articles". Defaults to the phase's name; once done, "Thought for 12s" |
seconds | number | Seconds so far. Counts by itself while left out (and stops while waiting for you) |
error | { text: string; detail?: string; onRetry: () => void } | A step that failed: the line turns red and says so, with Retry |
slow | { text: string; onStop?: () => void } | Shown while the phase takes long: "Still working…", with Stop |
ask | { text: ReactNode; approveLabel?: string; onApprove: () => void; onDecline?: () => void } | What the agent waits for in the waiting phase, with Approve and Not now |
took | Partial<Record<AgentPhase, number>> | Seconds each phase took, by phase, for the list a finished line opens to |
open | boolean | |
defaultOpen | boolean | |
onOpenChange | (open: boolean) => void | |
className | string |
Code
The full source, as the CLI installs it.
AgentPhases.tsxReact · 70 lines
'use client';
import { useEffect, useRef, useState, type ReactNode } from 'react';
import { cx } from '../lib/cx';
import { useControllable, useSeconds } from '../lib/hooks';
import { Icon } from '../lib/icons';
import { Fold, Radar, Swap, Think } from '../lib/motion';
import '../designme.css';
import './agent-phases.css';
export type AgentPhase = 'thinking' | 'searching' | 'reading' | 'waiting' | 'writing' | 'done';
export interface AgentPhasesProps {
phase: AgentPhase;
/** What the line says, like "Reading 3 articles". Defaults to the phase's name; once done, "Thought for 12s" */
label?: string;
/** Seconds so far. Counts by itself while left out (and stops while waiting for you) */
seconds?: number;
/** A step that failed: the line turns red and says so, with Retry */
error?: { text: string; detail?: string; onRetry: () => void };
/** Shown while the phase takes long: "Still working…", with Stop */
slow?: { text: string; onStop?: () => void };
/** What the agent waits for in the waiting phase, with Approve and Not now */
ask?: { text: ReactNode; approveLabel?: string; onApprove: () => void; onDecline?: () => void };
/** Seconds each phase took, by phase, for the list a finished line opens to */
took?: Partial<Record<AgentPhase, number>>;
open?: boolean;
defaultOpen?: boolean;
onOpenChange?: (open: boolean) => void;
className?: string;
}
const ORDER: AgentPhase[] = ['thinking', 'searching', 'reading', 'waiting', 'writing', 'done'];
const NAME: Record<AgentPhase, string> = { thinking: 'Thinking', searching: 'Searching', reading: 'Reading', waiting: 'Waiting for you', writing: 'Writing', done: 'Done' };
const KEY: Record<AgentPhase, string> = { thinking: 'think', searching: 'search', reading: 'read', waiting: 'wait', writing: 'write', done: 'done' };
/** One line for which phase the agent is in, from thinking to done: it stops to ask you, says when it is slow or failed, and folds to how long it took. */
export function AgentPhases({ phase, label, seconds, error, slow, ask, took = {}, open, defaultOpen = false, onOpenChange, className }: AgentPhasesProps) {
const counted = useSeconds(seconds === undefined && phase !== 'done' && phase !== 'waiting');
const secs = seconds ?? counted;
const [isOpen, setOpen] = useControllable(open, defaultOpen, onOpenChange);
const at = ORDER.indexOf(phase), done = phase === 'done';
const p = error ? 'err' : KEY[phase];
const line = error ? error.text : label ?? (done ? `Thought for ${secs}s` : NAME[phase]);
const glyph = error ? <Icon name="alert" /> : { thinking: <Think />, searching: <Radar />, reading: <Icon name="file-text" />, waiting: <Icon name="hand" />, writing: <Icon name="edit" />, done: <Icon name="ok" /> }[phase];
// only a change of phase is announced
const [said, setSaid] = useState('');
const last = useRef('');
useEffect(() => { const t = error ? 'Error: ' + error.text : done ? `Done, thought for ${secs}s` : NAME[phase]; if (t !== last.current) { last.current = t; setSaid(t); } }, [phase, !!error]); // eslint-disable-line react-hooks/exhaustive-deps
return (
<div data-dm="" data-p={p} className={cx('dm-aph', className)}>
<button type="button" className="dm-aph-h" aria-expanded={done && isOpen} aria-disabled={!done || undefined} onClick={() => { if (done) setOpen(!isOpen); }}>
<span className="dm-aph-g" key={p}><span className="dm-enter" style={{ display: 'grid' }}>{glyph}</span></span>
<Swap className={cx('dm-aph-l', !done && !error && phase !== 'waiting' && 'dm-shim')}>{line}</Swap>
<span className="dm-aph-t dm-num">{done ? '' : secs + 's'}</span>
<Icon name="chev-d" size="xs" className="dm-aph-chev" />
</button>
<div className="dm-aph-tr" aria-hidden="true">{ORDER.map((ph, j) => <i key={ph} className={cx(j === at && !done && 'dm-now')}>{j <= at && <b />}</i>)}</div>
<div className="dm-aph-x">
{error ? <div className="dm-aph-n dm-err dm-enter"><Icon name="alert" size="xs" /><span>{error.detail ?? error.text}</span><button type="button" className="dm-pill dm-line dm-sm" onClick={error.onRetry}><Icon name="refresh" size="xs" />Retry</button></div>
: phase === 'waiting' && ask ? <div className="dm-aph-n dm-ask dm-enter"><span>{ask.text}</span><button type="button" className="dm-pill dm-primary dm-sm" onClick={ask.onApprove}>{ask.approveLabel ?? 'Approve'}</button>{ask.onDecline && <button type="button" className="dm-pill dm-ghost dm-sm" onClick={ask.onDecline}>Not now</button>}</div>
: slow && !done ? <div className="dm-aph-n dm-enter"><Icon name="clock" size="xs" /><span>{slow.text}</span>{slow.onStop && <button type="button" className="dm-pill dm-line dm-sm" onClick={slow.onStop}><Icon name="stop" size="xs" />Stop</button>}</div> : null}
</div>
<Fold open={done && isOpen}>
<ol className="dm-aph-list">{ORDER.slice(0, 5).filter(ph => took[ph] !== undefined).map(ph => <li key={ph}><Icon name="check" size="xs" /><span>{NAME[ph]}</span><span className="dm-num">{took[ph]}s</span></li>)}</ol>
</Fold>
<p className="dm-aph-sr" role="status">{said}</p>
</div>
);
}
agent-phases.cssReact · 42 lines
/* Generated from designme.dev by scripts/sync.mjs. Edit the site, then sync. */
/* ---------- Agent phases ---------- */
/* which phase the agent is in, in one line: its glyph, its name shimmering while it lasts, and the time; a track of six short
segments under it fills phase by phase. When it needs you, is slow or fails, a line under the track says so with what to do;
once done the line reads how long it took and opens to the phases */
.dm-aph{width:100%;max-width:420px;margin-inline:auto;display:flex;flex-direction:column;gap:var(--dm-s-2);font-size:14px}
.dm-aph-h{display:flex;align-items:center;gap:10px;width:calc(100% + 16px);margin:0 -8px;padding:6px var(--dm-s-2);border-radius:calc(10px * var(--dm-rk));text-align:left;transition:background var(--dm-t-fast)}
.dm-aph-h[aria-disabled="true"]{cursor:default}
@media (hover:hover) and (pointer:fine){.dm-aph-h:not([aria-disabled="true"]):hover{background:var(--dm-row)}}
.dm-aph-g{width:18px;height:18px;flex:none;display:grid;place-items:center;color:var(--dm-accent-fg,var(--dm-accent))}
.dm-aph[data-p="wait"] .dm-aph-g{color:var(--dm-warn-mark)}
.dm-aph[data-p="err"] .dm-aph-g{color:var(--dm-bad)}
.dm-aph[data-p="done"] .dm-aph-g{color:var(--dm-ok)}
.dm-aph-l{flex:1;min-width:0;color:var(--dm-fg)}
.dm-aph-t{font-size:13px;color:var(--dm-muted)}
.dm-aph-chev{color:var(--dm-muted);visibility:hidden;transition:transform .32s var(--dm-ease)}
.dm-aph[data-p="done"] .dm-aph-chev{visibility:visible}
.dm-aph-h[aria-expanded="false"] .dm-aph-chev{transform:rotate(-90deg)}
.dm-aph-tr{display:flex;gap:3px;height:3px}
.dm-aph-tr i{flex:1;border-radius:2px;background:var(--dm-line);position:relative;overflow:hidden}
.dm-aph-tr i b{position:absolute;inset:0;border-radius:inherit;background:var(--dm-accent-mark,var(--dm-accent));transform-origin:left;animation:dm-aphfill .5s var(--dm-ease) backwards}
.dm-aph-tr i.dm-now b{background:color-mix(in srgb,var(--dm-accent-mark,var(--dm-accent)) 45%,var(--dm-line));animation:dm-aphbreathe 1.6s var(--dm-ease-io) infinite}
.dm-aph[data-p="wait"] .dm-aph-tr i.dm-now b{background:var(--dm-warn-mark);animation:none}
.dm-aph[data-p="err"] .dm-aph-tr i.dm-now b{background:var(--dm-bad);animation:none}
.dm-aph[data-p="done"] .dm-aph-tr i b{background:var(--dm-ok)}
@keyframes dm-aphfill{from{transform:scaleX(0)}}
@keyframes dm-aphbreathe{50%{opacity:.45}}
.dm-aph-x{min-height:40px;display:flex;align-items:center}
.dm-aph-n{display:flex;align-items:center;gap:var(--dm-s-2);width:100%;font-size:13px;color:var(--dm-fg2)}
.dm-aph-n>span{flex:1;min-width:0}
.dm-aph-n.dm-ask{padding:6px 6px 6px var(--dm-s-3);border-radius:var(--dm-r-in);background:var(--dm-row);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--dm-line) 50%,transparent);color:var(--dm-fg)}
.dm-aph-n.dm-err{color:var(--dm-bad)}
.dm-aph-n .dm-pill{flex:none}
.dm-aph-list{list-style:none;margin:0;padding:0 0 var(--dm-s-1);display:flex;flex-direction:column;gap:2px}
.dm-aph-list li{display:flex;align-items:center;gap:10px;padding:4px 0;font-size:13px;color:var(--dm-fg2)}
.dm-aph-list li .dm-i{color:var(--dm-ok)}
.dm-aph-list li span:nth-child(2){flex:1}
.dm-aph-list li .dm-num{color:var(--dm-muted);font-size:12px}
.dm-aph-sr{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip-path:inset(50%);white-space:nowrap}
@media (prefers-reduced-motion:reduce){.dm-aph-tr i.dm-now b{animation:none}.dm-aph .dm-think.dm-on{animation:none}.dm-aph .dm-think.dm-on i{animation:none}}
:root:not([data-style="soft"]) .dm-aph-tr i:not(:where([data-style="soft"],[data-style="soft"] *)){height:3px}
AgentPhases.astroAstro · 30 lines
---
// Generated from designme.dev by scripts/sync.mjs. Edit the site, then sync.
// Agent phases: One line for which phase the agent is in, from thinking to done, that stops to ask you, says when it is slow, and folds to how long it took.
// The markup, CSS and script are designme.dev's own, run by the shared core in ../core, which mounts the component when it scrolls
// into view and plays its demo. To show your own data, change agent-phases.js. The markup is kept exactly as it is (is:raw).
import type { HTMLAttributes } from 'astro/types';
import '../core/designme.css';
import './agent-phases.css';
export interface Props extends HTMLAttributes<'div'> {
/** "soft" draws it in the Soft style. Leave it out for Blueprint, or for the style set on <html> (data-style="soft") */
'data-style'?: 'soft';
}
const { class: className, ...rest } = Astro.props;
---
<div class:list={['card', 'bare', className]} data-c="agent-phases" {...rest}>
<div class="stage-in"><Fragment is:raw>
<div class="aph">
<button class="aph-h" aria-expanded="false" aria-disabled="true"><span class="aph-g"></span><span class="aph-l"></span><span class="aph-t num">0s</span><svg class="i xs aph-chev"><use href="#i-chev-d"/></svg></button>
<div class="aph-tr" aria-hidden="true"><i></i><i></i><i></i><i></i><i></i><i></i></div>
<div class="aph-x"></div>
<div class="fold shut aph-fold"><div><ol class="aph-list"></ol></div></div>
<p class="aph-sr" role="status"></p>
</div>
</Fragment></div>
</div>
<script>
import './agent-phases.js';
</script>
agent-phases.htmlHTML · 12 lines
<div class="card bare" data-c="agent-phases">
<div class="stage-in">
<div class="aph">
<button class="aph-h" aria-expanded="false" aria-disabled="true"><span class="aph-g"></span><span class="aph-l"></span><span class="aph-t num">0s</span><svg class="i xs aph-chev"><use href="#i-chev-d"/></svg></button>
<div class="aph-tr" aria-hidden="true"><i></i><i></i><i></i><i></i><i></i><i></i></div>
<div class="aph-x"></div>
<div class="fold shut aph-fold"><div><ol class="aph-list"></ol></div></div>
<p class="aph-sr" role="status"></p>
</div>
</div>
</div>
agent-phases.cssHTML · 41 lines
/* ---------- Agent phases ---------- */
/* which phase the agent is in, in one line: its glyph, its name shimmering while it lasts, and the time; a track of six short
segments under it fills phase by phase. When it needs you, is slow or fails, a line under the track says so with what to do;
once done the line reads how long it took and opens to the phases */
.aph{width:100%;max-width:420px;margin-inline:auto;display:flex;flex-direction:column;gap:var(--dm-s-2);font-size:14px}
.aph-h{display:flex;align-items:center;gap:10px;width:calc(100% + 16px);margin:0 -8px;padding:6px var(--dm-s-2);border-radius:calc(10px * var(--dm-rk));text-align:left;transition:background var(--dm-t-fast)}
.aph-h[aria-disabled="true"]{cursor:default}
@media (hover:hover) and (pointer:fine){.aph-h:not([aria-disabled="true"]):hover{background:var(--dm-row)}}
.aph-g{width:18px;height:18px;flex:none;display:grid;place-items:center;color:var(--dm-accent-fg,var(--dm-accent))}
.aph[data-p="wait"] .aph-g{color:var(--dm-warn-mark)}
.aph[data-p="err"] .aph-g{color:var(--dm-bad)}
.aph[data-p="done"] .aph-g{color:var(--dm-ok)}
.aph-l{flex:1;min-width:0;color:var(--dm-fg)}
.aph-t{font-size:13px;color:var(--dm-muted)}
.aph-chev{color:var(--dm-muted);visibility:hidden;transition:transform .32s var(--dm-ease)}
.aph[data-p="done"] .aph-chev{visibility:visible}
.aph-h[aria-expanded="false"] .aph-chev{transform:rotate(-90deg)}
.aph-tr{display:flex;gap:3px;height:3px}
.aph-tr i{flex:1;border-radius:2px;background:var(--dm-line);position:relative;overflow:hidden}
.aph-tr i b{position:absolute;inset:0;border-radius:inherit;background:var(--dm-accent-mark,var(--dm-accent));transform-origin:left;animation:aphfill .5s var(--dm-ease) backwards}
.aph-tr i.now b{background:color-mix(in srgb,var(--dm-accent-mark,var(--dm-accent)) 45%,var(--dm-line));animation:aphbreathe 1.6s var(--dm-ease-io) infinite}
.aph[data-p="wait"] .aph-tr i.now b{background:var(--dm-warn-mark);animation:none}
.aph[data-p="err"] .aph-tr i.now b{background:var(--dm-bad);animation:none}
.aph[data-p="done"] .aph-tr i b{background:var(--dm-ok)}
@keyframes aphfill{from{transform:scaleX(0)}}
@keyframes aphbreathe{50%{opacity:.45}}
.aph-x{min-height:40px;display:flex;align-items:center}
.aph-n{display:flex;align-items:center;gap:var(--dm-s-2);width:100%;font-size:13px;color:var(--dm-fg2)}
.aph-n>span{flex:1;min-width:0}
.aph-n.ask{padding:6px 6px 6px var(--dm-s-3);border-radius:var(--dm-r-in);background:var(--dm-row);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--dm-line) 50%,transparent);color:var(--dm-fg)}
.aph-n.err{color:var(--dm-bad)}
.aph-n .pill{flex:none}
.aph-list{list-style:none;margin:0;padding:0 0 var(--dm-s-1);display:flex;flex-direction:column;gap:2px}
.aph-list li{display:flex;align-items:center;gap:10px;padding:4px 0;font-size:13px;color:var(--dm-fg2)}
.aph-list li .i{color:var(--dm-ok)}
.aph-list li span:nth-child(2){flex:1}
.aph-list li .num{color:var(--dm-muted);font-size:12px}
.aph-sr{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip-path:inset(50%);white-space:nowrap}
@media (prefers-reduced-motion:reduce){.aph-tr i.now b{animation:none}.aph .think.on{animation:none}.aph .think.on i{animation:none}}
:root:not([data-style="soft"]) .aph-tr i:not(:where([data-style="soft"],[data-style="soft"] *)){height:3px}
agent-phases.jsHTML · 62 lines
DM.def('agent-phases', (r, c) => {
const root = $('.aph', r), head = $('.aph-h', r), g = $('.aph-g', r), lab = $('.aph-l', r), tm = $('.aph-t', r), segs = $$('.aph-tr i', r), x = $('.aph-x', r), fold = $('.aph-fold', r), list = $('.aph-list', r), sr = $('.aph-sr', r);
// the phases in order, each with its glyph and what the line says while it lasts
const P = [
['think', 'Thinking', '<span class="think on"><i></i><i></i><i></i><i></i></span>', 'Thinking about the question'],
['search', 'Searching', `<span class="radar"></span>`, 'Searching the help centre'],
['read', 'Reading', I('file-text','s'), 'Reading 3 articles'],
['wait', 'Waiting for you', I('hand','s'), 'Waiting for you'],
['write', 'Writing', I('edit','s'), 'Writing the reply'],
['done', 'Done', I('ok','s'), '']
];
const took = {};
let at = -1, secs = 0, ticking = true, mode = '', resolve = null;
c.every(1000, () => { if (ticking && mode !== 'wait'){ secs++; tm.textContent = secs + 's'; } });
const glyph = h => { g.innerHTML = h; enter(g.firstElementChild); };
// a new phase: its glyph, its words shimmering, the next segment filling; only a change of phase is announced
function go(i, words){
at = i; const [k, name, gl, line] = P[i]; root.dataset.p = k; mode = k; glyph(gl);
lab.classList.toggle('shim', k !== 'done' && k !== 'wait'); swap(lab, words || line);
segs.forEach((s, j) => { s.classList.toggle('now', j === i && k !== 'done'); if (j <= i && !s.firstChild) s.appendChild(document.createElement('b')); });
sr.textContent = k === 'done' ? `Done, thought for ${secs}s` : name;
}
const note = h => { x.innerHTML = h; if (x.firstElementChild) enter(x.firstElementChild); };
// wait for a person: resolves with what they chose
const ask = h => new Promise(res => { resolve = res; note(h); $$('.aph-n button', x).forEach(b => b.onclick = () => { if (resolve){ const f = resolve; resolve = null; f(b.dataset.a); } }); });
c.run(async () => {
const t0 = () => secs;
let s = t0(); go(0); await c.wait(1600); took[0] = secs - s;
// searching fails once, then works when retried
s = t0(); go(1); await c.wait(1300);
mode = 'err'; root.dataset.p = 'err'; glyph(I('alert','s')); lab.classList.remove('shim'); swap(lab, 'The search timed out'); sr.textContent = 'Error: the search timed out';
await ask(`<div class="aph-n err">${I('alert','xs')}<span>The help centre didn't answer in 10s</span><button class="pill line sm" data-a="retry">${I('refresh','xs')}Retry</button></div>`);
note(''); go(1, 'Searching the help centre again'); await c.wait(1200); took[1] = secs - s;
// reading is slow: after a while it says so and offers Stop
s = t0(); go(2); await c.wait(1800);
let stopped = false;
note(`<div class="aph-n">${I('clock','xs')}<span>Still working, the refund policy is a long page</span><button class="pill line sm aph-stop" data-a="stop">${I('stop','xs')}Stop</button></div>`);
$('.aph-stop', x).onclick = () => { stopped = true; note(''); };
await c.wait(2200); note(''); took[2] = secs - s;
if (stopped){ go(5); lab.classList.remove('shim'); swap(lab, `Stopped after ${secs}s`); ticking = false; return; }
// waiting for you: it stops the clock until someone answers
s = t0(); go(3);
const a = await ask(`<div class="aph-n ask"><span>Offer Maya a refund of $48?</span><button class="pill primary sm" data-a="ok">Approve</button><button class="pill ghost sm" data-a="no">Not now</button></div>`);
note(''); took[3] = secs - s;
s = t0(); go(4, a === 'ok' ? 'Writing the reply with the refund' : 'Writing the reply without a refund'); await c.wait(2000); took[4] = secs - s;
// done: the line says how long it took and opens to the phases
ticking = false; go(5); swap(lab, `Thought for ${secs}s`); tm.textContent = '';
head.removeAttribute('aria-disabled');
list.innerHTML = P.slice(0, 5).map(([, name], i) => `<li>${I('check','xs')}<span>${name}</span><span class="num">${took[i] || 0}s</span></li>`).join('');
});
head.onclick = () => { if (head.hasAttribute('aria-disabled')) return; const o = head.getAttribute('aria-expanded') !== 'true'; head.setAttribute('aria-expanded', o); fold.classList.toggle('shut', !o); };
// the preview's story: retry the search, approve the refund, then open the finished line
if (r === c.st) c.story(async s => {
await s.until(() => mode === 'err' && $('.aph-n button', x)); await s.beat(3);
await s.tap($('.aph-n button', x)); s.away();
await s.until(() => mode === 'wait' && $('.aph-n button', x)); await s.beat(3);
await s.tap($('.aph-n [data-a="ok"]', x)); s.away();
await s.until(() => mode === 'done' && !head.hasAttribute('aria-disabled')); await s.beat(3);
await s.tap(head); s.away(); await s.beat(5);
});
});
Questions
What people ask about it.
- How is this different from Thinking?
- Thinking lists each step the agent takes and what it found. Agent phases says only which of six phases it is in, in one line, and handles the moments it needs you.
- Can I skip a phase?
- Yes. Move
phasestraight to the next one you need; the track fills up to it. Leave a phase out oftookand it is left out of the finished list. - When should I show the slow line?
- Whenever your agent decides a phase is taking long, for example after twice its usual time. Pass
slowthen, and clear it when the phase moves on.
More in Agent status
Show what the agent is doing, how far along it is, and what it is working with.