57 / Agent UI
Subagent Tree
Several agents working at once, nested under whoever handed the work down, each with its own state and elapsed time.
Agents3/7 done1 failed
Read the issue and split the work, done4.2s
Find every call site, done12s
14 files, 31 calls.Rewrite the call sites, running43s
src/upload, done8.1s
src/queue, running
src/workers, queued
Check the generated types, failed6.4s
Two entries still point at the old signature.
"use client"
import { SubagentTree } from "mischief-ui/subagent-tree"
const runs = [
{
id: "plan",
label: "Read the issue and split the work",
status: "done" as const,
duration: 4.2,
children: [
{
id: "search",
label: "Find every call site",
status: "done" as const,
duration: 11.6,
detail: "14 files, 31 calls.",
},
{
id: "migrate",
label: "Rewrite the call sites",
status: "running" as const,
duration: 42.8,
children: [
{
id: "a",
label: "src/upload",
status: "done" as const,
duration: 8.1,
},
{ id: "b", label: "src/queue", status: "running" as const },
{ id: "c", label: "src/workers", status: "queued" as const },
],
},
{
id: "types",
label: "Check the generated types",
status: "failed" as const,
duration: 6.4,
detail: "Two entries still point at the old signature.",
},
],
},
]
export function SubagentTreeDemo() {
return (
<div className="w-full max-w-lg">
<SubagentTree runs={runs} />
</div>
)
}Installation
Copy the source into your project, or keep it behind a package.
npx shadcn@latest add Tinkerers-Labs/mischief-ui/subagent-treeimport { SubagentTree } from "mischief-ui/subagent-tree"Or paste it in yourself. The source imports the shared cn helper from @/lib/utils, so point that at your own copy.
"use client" import * as React from "react" import { cn } from "@/lib/utils" export type AgentRunStatus = "queued" | "running" | "done" | "failed" export type AgentRun = { id: string label: string status?: AgentRunStatus /** What it is doing, or what it found. */ detail?: React.ReactNodeUsage
export function Fleet({ runs }) {
return <SubagentTree runs={runs} />
}It is nested lists, not a tree widget
The obvious reach here is role=tree with treeitem children and arrow-key navigation. It would be wrong. Nothing in this is expanded, selected or activated: it is work being watched, not a file browser.
A tree widget would announce "tree, level 2, 3 of 4" over that, and take the arrow keys hostage to move between things nobody can do anything to. Nested lists say the same shape, cost nothing, and leave the keyboard alone. Reach for the widget when the nodes become controls.
What a run can be
| Status | What it means |
|---|---|
queued | Handed out, not started |
running | Working now |
done | Finished |
failed | Stopped without finishing |
Status reaches a screen reader as a word after the label rather than as the colour of a dot, and the running marker stops pulsing when reduced motion is preferred. The count of what is running is announced once for the whole tree, not once per agent, because twelve agents finishing is one piece of news.
API
runsreadonly AgentRun[]The top level agents.labelstringNames the group. Defaults to Agents....rootPropsHTMLAttributes<HTMLDivElement>Native root attributes.AgentRun
idstringUnique within the tree.labelstringWhat this agent was asked to do.status"queued" | "running" | "done" | "failed"Defaults to done.detailReactNodeWhat it is doing, or what it found.durationnumberSeconds it has taken.childrenreadonly AgentRun[]Work it handed down.Accessibility
Nesting is expressed with nested lists, so a screen reader announces the depth and the number of items at each level without a tree widget's keyboard contract being invented for content that cannot be operated. Each run's status is spoken as a word, never carried by colour alone. One polite live region summarises the whole tree, so a fleet finishing is announced once rather than once per agent. The running marker respects reduced motion.