caliper uidocs

Agent status

Free

One 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.

Installs components/ui/agent-status.tsxDepends on lucide-reactMotion shared motion tokens
pnpm dlx shadcn@latest add https://caliperui.site/r/agent-status.json

What would 20 seats cost on the annual plan?

ThinkingThinking

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 detail is what makes the line worth reading; on its own a phase word says little.

Installation #

terminal
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.

example.tsx
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
PropTypeDefaultDescription
phase *AgentPhase—
detailReactNode—What the phase is about: the query, the page being read.
startedAtnumber—When the turn began (ms since epoch); shows a running clock.
endedAtnumber—When it finished; freezes the clock. Defaults to the moment the phase stopped working.
labelstring—Overrides the phase word, e.g. "Comparing three plans".
actionReactNode—Something to do beside it, such as a button that opens the full activity.
classNamestring—

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.