Agent status
One pill that always answers: what is it doing, and for how long.
Installation
npx assistant-ui@latest add elements-agent-statusThe CLI reads react-native from your package.json and installs from the native registry tree. The element takes the same props as the React one; the React Native elements guide covers setup and what changes on a phone.
npx shadcn@latest add "@assistant-ui/agent-status"First time? Set up a runtime
Runtime components read their state from an assistant-ui runtime. Add one to an existing project:
npx assistant-ui@latest initThen wrap your app in a runtime provider:
import { AssistantRuntimeProvider } from "@assistant-ui/react";
import { useChatRuntime, AssistantChatTransport } from "@assistant-ui/ai-sdk";
export default function App() {
const runtime = useChatRuntime({
transport: new AssistantChatTransport({ api: "/api/chat" }),
});
return (
<AssistantRuntimeProvider runtime={runtime}>
{/* your components */}
</AssistantRuntimeProvider>
);
}The installation guide covers new projects, templates, and API routes.
npx shadcn@latest add "@assistant-ui/elements-agent-status"Props-driven: no runtime or provider required.
A rounded pill with a state dot, a label that crossfades when it changes, an elapsed time while the agent is still going, and a trailing icon. With a runtime the state and label summarize nested tasks; standalone you hold all three and update them yourself.
Getting started
Render the bound chip
"use client";
import { AgentStatus } from "@/components/assistant-ui/elements/agent-status.aui";
export function HeaderStatus() {
return <AgentStatus />;
}Both bound components read the thread through the nearest AssistantRuntimeProvider, so mount them inside it, in a header or toolbar of the same tree. AgentStatus reads s.thread.tasks and is a live chip: it renders only while a task runs or waits for input, and nothing once everything has settled. Its label is the only running task's label, N of M tasks running when more than one is running, or N tasks waiting for input when work is blocked. Elapsed time starts from the earliest running task's timing.
Open the tray
"use client";
import { TaskTray } from "@/components/assistant-ui/elements/agent-status.aui";
export function HeaderTasks() {
return <TaskTray />;
}TaskTray uses the same pill as a button that opens a popover, with a chevron as the trailing icon so the control reads as a disclosure. The pill stays after the work settles and carries the tally, M tasks done or M tasks done, N failed (a cancelled task counts as finished, not failed), with the failed state when a failure remains. Its list contains every task in document order, indents nested tasks, shows each row's state, label, meta tag, and elapsed time, and pages four rows at a time.
Bind a single tool call instead
"use client";
import { defineToolkit } from "@assistant-ui/react";
import { AgentStatus } from "@/components/assistant-ui/elements/agent-status";
export const toolkit = defineToolkit({
report_status: {
type: "backend",
render: ({ args, status }) => (
<AgentStatus
state={
status.type === "requires-action"
? "waiting"
: status.type === "running"
? "working"
: "done"
}
label={args.label}
elapsed={args.elapsed}
/>
),
},
});Use the props only element when a single tool call owns the status. requires-action maps to waiting, running maps to working, and both complete and incomplete calls map to done.
Standalone, the element is a controlled display: you own state, label, and elapsed, and update them as the agent's work moves along.
Hold the status state
"use client";
import { useState } from "react";
import {
AgentStatus,
type AgentState,
} from "@/components/assistant-ui/elements/agent-status";
export function Status() {
const [state, setState] = useState<AgentState>("working");
const [label, setLabel] = useState("Refactoring composer");
const [elapsed, setElapsed] = useState("0:04");
return <AgentStatus state={state} label={label} elapsed={elapsed} />;
}Move through the run
setState("waiting");
setLabel("Waiting for approval");
// later
setState("done");
setLabel("Finished, 2 files changed");Changing label alone still animates: the text fades and blurs between values because the element keys the label on its own content.
Anatomy
<span data-slot="agent-status">
<span>{/* check, cross, a pulsing dot, or an outlined one */}</span>
<span>{/* visually hidden state */}</span>
<span>{/* label */}</span>
<span>{/* elapsed, only while state is "working" or "waiting" */}</span>
<span aria-hidden>{/* decorative trailing icon, or the trailing node you pass */}</span>
</span>The leading indicator is a check once state is "done" and a cross once it is "failed", otherwise a dot: filled blue and pulsing while "working", a static muted outline while "waiting", so the states differ in shape and motion and not only in color. The root is a span, so the pill can sit inside a button or a link. The state is also rendered as visually hidden text, so it is exposed to assistive technology rather than carried by the dot's color alone. elapsed only renders when it is supplied and state is "working" or "waiting". The trailing icon swaps between the pause and replay treatments based on state unless trailing supplies a node, and is decorative: it carries no hover or press treatment and is hidden from assistive technology. Props spread onto the root pill.
Examples
Restyle the pill
Both lanes take className on the root. The elapsed text uses the mono token from surfaces.tsx.
<AgentStatus className="gap-3" state={state} label={label} elapsed={elapsed} />A pill with no elapsed time
Omit elapsed for work that is not worth timing, like a wait state with no useful duration.
<AgentStatus state="waiting" label="Waiting for approval" />API reference
Bound AgentStatus and TaskTray
| Input | Type | Description |
|---|---|---|
className prop | string | undefined | Merged onto the bound chip or tray trigger. |
| Tasks | s.thread.tasks | Drives the aggregate state, label, and elapsed time. AgentStatus returns null unless a task runs or waits; TaskTray returns null only when the thread has no tasks. |
useTaskSummary() | TaskSummary | Returns total, running, waiting, failed, startedAt, and runningLabel for the current tasks. |
summaryState(summary) | AgentState | Returns working while any task runs, waiting when none run but work waits, failed when the settled work includes a failure, and done otherwise. |
summaryLabel(summary) | string | Returns the task summary label used by the chip and tray. |
AgentStatus
| Prop | Type | Default | Description |
|---|---|---|---|
state | "working" | "waiting" | "done" | "failed" | required | Drives the leading indicator, visually hidden state text, and trailing icon. |
label | string | required | Crossfades in whenever its value changes. |
elapsed | string | Shown only while state is "working" or "waiting". | |
trailing | ReactNode | Replaces the decorative trailing icon, for example with a chevron when the pill opens something. | |
className | string | Merged onto the root. |
All other span props are forwarded to the root.