Subagent list
Parallel workers with their own progress, models, and completions.
Installation
npx shadcn@latest add "@assistant-ui/elements-subagent-list"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-subagent-list"Props-driven: no runtime or provider required.
A stack of cards, one per worker, each with a done or spinning icon, its model, and a progress bar, plus an optional trailing card for a synthesis step. With a runtime each worker is its own tool call and the list is assembled from them; standalone you pass in the roster and its progress directly.
Getting started
A dispatch to several sub-agents is naturally several tool calls in one assistant turn, one per worker. The list itself is not any single call's UI; it is assembled from all of them.
Render the dispatch calls invisibly
Register the dispatch tool with a render that returns nothing. Its schema and execution live on your server; here it only needs to stop the default tool card from also appearing inline, since the aggregate list below owns the visual.
"use client";
import { defineToolkit } from "@assistant-ui/react";
export const toolkit = defineToolkit({
dispatch_subagent: { type: "backend", render: () => null },
summarize_findings: { type: "backend", render: () => null },
});Aggregate them below the message
Read every part of the current message, keep the dispatch_subagent calls, and fold them into the props SubagentList expects. summarize_findings becomes the trailing summary card when it is present at all.
import { MessagePrimitive, useAuiState } from "@assistant-ui/react";
import { SubagentList } from "@/components/assistant-ui/elements/subagent-list";
function SubagentProgress() {
const parts = useAuiState((s) => s.message.parts);
const calls = parts.filter(
(p) => p.type === "tool-call" && p.toolName === "dispatch_subagent",
);
const summaryCall = parts.find(
(p) => p.type === "tool-call" && p.toolName === "summarize_findings",
);
if (calls.length === 0) return null;
return (
<SubagentList
agents={calls.map((c) => ({ name: c.args.name, model: c.args.model }))}
completedCount={calls.filter((c) => c.status.type === "complete").length}
progress={calls.map((c) => (c.status.type === "complete" ? 100 : 0))}
showSummary={summaryCall !== undefined}
summaryAgent={
summaryCall
? { name: summaryCall.args.name, model: summaryCall.args.model }
: { name: "", model: "" }
}
/>
);
}
function AssistantMessage() {
return (
<MessagePrimitive.Root>
<MessagePrimitive.Parts />
<SubagentProgress />
</MessagePrimitive.Root>
);
}Standalone, the element is a controlled display: you own the roster and report each worker's progress into it as it changes.
Hold the roster and its progress
"use client";
import { useState } from "react";
import {
SubagentList,
type SubagentItem,
} from "@/components/assistant-ui/elements/subagent-list";
const AGENTS: readonly SubagentItem[] = [
{ name: "Explore the runtime", model: "haiku" },
{ name: "Fix composer types", model: "sonnet" },
{ name: "Write regression tests", model: "sonnet" },
];
export function Agents() {
const [completedCount, setCompletedCount] = useState(0);
const [progress, setProgress] = useState([0, 0, 0]);
return (
<SubagentList
agents={AGENTS}
completedCount={completedCount}
progress={progress}
showSummary={false}
summaryAgent={{ name: "Summarize findings", model: "haiku" }}
/>
);
}Report progress and completion
function onWorkerProgress(index: number, percent: number) {
setProgress((prev) => prev.with(index, percent));
}
function onWorkerDone() {
setCompletedCount((count) => count + 1);
}completedCount marks agents done from the front of the array, so finish them in order for the checkmarks to land on the right cards.
Anatomy
<div data-slot="subagent-list">
<div>
<span>{/* check or spinner */}</span>
<span>{/* agent name */}</span>
<span>{/* model */}</span>
<span role="progressbar">{/* progress bar, named from its agent */}</span>
</div>
{/* one card per agent, then the summary card when shown */}
</div>An agent at index i is done when i < completedCount; its bar width and progressbar value come from progress[i], clamped into 0…100 with a missing or NaN entry read as 0. Each progressbar is named from its agent and exposes a 0…100 value to assistive technology. The summary card, when showSummary is true, always shows the spinner and a full-track shimmer treatment. Its progressbar carries no value because this version has no way to mark the summary itself done, only to show or hide it. The root carries a minimum height so the layout does not jump as agents complete and the summary card fades in.
Examples
Restyle the roster
Both lanes take className on the root. Each card uses the shared paper surface and the mono token for its model label, so retheming those in surfaces.tsx restyles every card here alongside every other element built on them.
<SubagentList className="max-w-sm gap-3" /* ... */ />Where progress comes from
A tool call's status only distinguishes running from complete, so the mapping above reads as two states: 0 while a worker's call is in flight, 100 once its result lands. A smoother live percentage needs your own progress channel, for example an app-level store keyed by the call's toolCallId that a websocket or polling update fills in, read into progress the same way.
async function runWorker(index: number) {
for (let pct = 0; pct <= 100; pct += 20) {
await tick();
setProgress((prev) => prev.with(index, pct));
}
setCompletedCount((count) => count + 1);
}API reference
Render props
| Source | Type | Description |
|---|---|---|
dispatch_subagent call args.name / args.model | string | One roster entry per call. |
dispatch_subagent call status.type | "running" | "requires-action" | "incomplete" | "complete" | "complete" counts toward completedCount and reads as 100 progress; anything else reads as 0. |
summarize_findings call | tool call or undefined | Presence alone drives showSummary; its args become summaryAgent. |
SubagentList
| Prop | Type | Default | Description |
|---|---|---|---|
agents | readonly SubagentItem[] | required | The roster, in display order. |
completedCount | number | required | How many agents, counted from the front of agents, are done. |
progress | readonly number[] | required | Bar width and accessible progress value per agent, index-aligned with agents. Values are clamped to 0…100; out-of-range or NaN entries read as 0. |
showSummary | boolean | required | Whether the trailing summary card renders. |
summaryAgent | SubagentItem | required | Name and model for the summary card. |
className | string | Merged onto the root. |
SubagentItem is { name: string; model: string }. All other div props are forwarded to the root.