Agent status
FreeOne line inside a reply that says what the agent is doing right now, thinking, searching, reading or writing, with what it is working on and a running clock, then settles into how long it took.
pnpm dlx shadcn@latest add https://caliperui.site/r/agent-status.jsonWhat would 20 seats cost on the annual plan?
When to use #
Use it for
- Inside an assistant reply while the answer is being made.
- Any long agent step where people should see progress without opening a log.
Skip it when
- A full record of every step; use agent activity, which folds the steps away.
- Waits with no phases to report; use agent loading.
Decisions #
Why it is built this way, so you know what to keep when you change your copy.
Behaviour
- Phases come from your agent, not a timer
- The line changes only when you pass a new phase, so it never claims to be reading while the model is still thinking. Rotating words that do not match the work erode trust fast.
- Announce the phase, not the clock
- Screen readers hear each new phase once. The seconds tick visually but stay out of the live region, so the line is not read out every second.
Styling
- Shimmer while working, plain when settled
- The phase word shimmers only while something is happening; finished and stopped turns read in muted text with the time it took.
Usage
- Pass what the phase is about
- A query or the page being read in
detailis what makes the line worth reading; on its own a phase word says little.
Installation #
pnpm dlx shadcn@latest add https://caliperui.site/r/agent-status.json
Adds its 3 files to components/ui/, each with its own stylesheet, and installs their packages. No account needed. The first part also adds Caliper UI’s tokens; import them once as in Installation.
Usage #
This is the code behind the preview above. It is built from AgentStatus and elapsedLabel.
import { useEffect, useState } from "react" import { AgentStatus, type AgentPhase } from "@/components/ui/agent-status" const turn: { phase: AgentPhase; detail?: string; ms: number }[] = [ { phase: "thinking", ms: 1300 }, { phase: "searching", detail: "annual plan discounts for teams of 20", ms: 1800 }, { phase: "reading", detail: "pricing.halden.example", ms: 1500 }, { phase: "writing", ms: 2200 }, { phase: "done", ms: 0 }, ]; const reply = "The Team plan is €24 a seat a month, or €230 a seat a year paid annually, which saves two months. For 20 people that is €4,600 a year instead of €5,760."; export function AgentStatusDemo() { const [step, setStep] = useState(0); const [started] = useState(() => Date.now()); const [written, setWritten] = useState(0); const current = turn[step]!; useEffect(() => { if (!current.ms) return; const timer = window.setTimeout(() => setStep((value) => value + 1), current.ms); return () => window.clearTimeout(timer); }, [current.ms, step]); // While writing, the reply appears a few characters at a time. useEffect(() => { if (current.phase !== "writing" && current.phase !== "done") return; const timer = window.setInterval(() => setWritten((value) => Math.min(reply.length, value + 4)), 30); return () => window.clearInterval(timer); }, [current.phase]); return ( <div className="grid w-full max-w-lg gap-4"> <p className="ml-auto max-w-[80%] rounded-[14px] bg-muted px-3.5 py-2 text-sm">What would 20 seats cost on the annual plan?</p> <div className="grid gap-2"> <AgentStatus phase={current.phase} detail={current.detail} startedAt={started} /> {written > 0 && <p className="text-sm leading-6">{reply.slice(0, written)}</p>} </div> </div> ); }
API reference #
AgentStatus
component
elapsedLabel
component
No props of its own; it accepts the native props of its root element.
Accessibility #
It exposes status semantics, with aria-hidden kept in step with state. Changes that matter are announced through a live region.
No custom key handling; Tab and the native activation keys do the work.
Motion #
Transitions use the shared duration and easing tokens. Under prefers-reduced-motion those tokens collapse to instant and travel distances go to zero, so state changes still happen without movement.