Elements

Agent status

One pill that always answers: what is it doing, and for how long.

workingRefactoring composer0s
fig. 01 · plays once, replay from the corner

Installation

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 init

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

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

app/status.tsx
"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

app/status.tsx
"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

app/toolkit.tsx
"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.

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

InputTypeDescription
className propstring | undefinedMerged onto the bound chip or tray trigger.
Taskss.thread.tasksDrives 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()TaskSummaryReturns total, running, waiting, failed, startedAt, and runningLabel for the current tasks.
summaryState(summary)AgentStateReturns 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)stringReturns the task summary label used by the chip and tray.