# Agent Fleet

> Panel of parallel subagents: status, name and role, live activity, thin progress, elapsed time, tokens and cost, with a fleet summary and selection.

- **Slug:** `agent-fleet` · **Category:** agent · **Status:** experimental
- **Import:** `import { AgentFleet, FleetAgent } from "@kura-ui/react/agent-fleet";`
- **Docs:** https://kuro.abstergo.fyi/components/agent-fleet
- **Registry item:** https://kuro.abstergo.fyi/r/agent-fleet.json
- **Tags:** agent, subagents, fleet, parallel, orchestration, status, listbox, progress

## Install

```bash
# shadcn registry (copies source into your project)
npx shadcn@latest add https://kuro.abstergo.fyi/r/agent-fleet.json
# Kura CLI (same, plus dependency resolution and --json output)
npx @kura-ui/cli add agent-fleet
# npm package
npm install @kura-ui/react
```

## When to use

- An orchestrator delegates work to several subagents at once (explorer, test-writer, reviewer) and the human needs to see who is doing what.
- A master/detail layout where choosing a subagent shows its transcript or tool calls next to the list.
- Spotting at a glance which subagent is waiting for approval or has failed ('3 running · 1 waiting · 2 done').

## When not to use

- Sequential steps of one agent — use task-list or agent-timeline.
- Timing and nesting of spans inside a run — use trace-waterfall.
- The run as a whole (title, cost, stop) — use run-header, and put AgentFleet under it.

## Props

| Prop | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `agents` | `FleetAgent[]` | — | yes | { id, name, role?, status, activity?, progress? (0–1), startedAt?, endedAt?, tokens?, cost? }, in display order. activity is the current action while running, or the result once done/failed. |
| `title` | `React.ReactNode` | `"Subagents"` | no | Header title; the count follows it. |
| `selectedId` | `string \| null` | — | no | Selected agent id (controlled). |
| `defaultSelectedId` | `string \| null` | `null` | no | Initially selected agent id when uncontrolled. |
| `onSelect` | `(id: string) => void` | — | no | Called when a row is chosen by click, Enter or Space. When set (or selectedId is given) the list becomes a single-select listbox. |
| `density` | `"compact" \| "comfortable"` | `"comfortable"` | no | comfortable = two lines (name + role, then activity; tokens, cost and clock on the right, progress under them). compact = one 36px row (name, activity, progress, tokens, clock). |
| `empty` | `React.ReactNode` | — | no | Replaces the default empty state ('No subagents yet'). |

Native attributes of the root element, `className` and `ref` are passed through.

## Anatomy

- **AgentFleet** (`data-slot="agent-fleet"`) — Root panel with `data-density`. A size container: cost, tokens and summary drop out when narrow.
- **Header** (`data-slot="agent-fleet-header"`) — Title, count and agent-fleet-summary ('3 running · 1 waiting · 2 done'; waiting in amber, failed in red).
- **Agent** (`data-slot="agent-fleet-agent"`) — One row (<li>, role=option when selectable) with `data-status`, `data-selected`. Selected = active fill + 2px blue bar on the left.
- **Name** (`data-slot="agent-fleet-name"`) — Agent name.
- **Role** (`data-slot="agent-fleet-role"`) — Role (comfortable only).
- **Activity** (`data-slot="agent-fleet-activity"`) — One truncated line; shimmers while running, amber when waiting, red when failed.
- **Progress** (`data-slot="agent-fleet-progress"`) — 3px bar while live: blue running, amber waiting, grey paused.
- **Tokens** (`data-slot="agent-fleet-tokens"`) — Mono token total, e.g. 84.2k.
- **Cost** (`data-slot="agent-fleet-cost"`) — Mono cost (comfortable, wide containers).
- **Elapsed** (`data-slot="agent-fleet-elapsed"`) — Mono clock; ticks while live, frozen at endedAt − startedAt.
- **Empty** (`data-slot="agent-fleet-empty"`) — Empty state.

## Accessibility

- Role: `listbox (selectable) or list`
- The list is labelled by the title. Selectable rows are options with aria-selected and a roving tabindex (one Tab stop).
- Each row's status glyph is a labelled image ('Running', 'Waiting for you'), so status is not colour-only; progress is read as a percentage.
- A polite status region announces only transitions out of running ('client-patcher done', 'test-writer failed'), not ticking clocks or progress.
- Reduced motion: New rows fade in without rising; running glyphs, activity shimmer and progress width transitions stop.

| Keys | Action |
| --- | --- |
| ArrowDown / ArrowUp (j / k) | Move focus between agents |
| Home / End | First / last agent |
| Enter / Space | Select the focused agent |

## Notes for agents

- Give every agent a stable id; rows key on it and the announcer diffs statuses by id.
- Write activity in present tense while running ('Rewriting mocks in login.test.ts') and as a result when done ('Found 23 calls in 9 files'), or the failure when failed.
- Pass startedAt from the harness (not Date.now() at render) and endedAt when the agent stops so clocks stay correct across reloads.
- progress is optional; omit it for open-ended work rather than inventing a number.

## Examples

### Three subagents (minimal)

Done, running and queued; not selectable.

```tsx
"use client";

import * as React from "react";
import { AgentFleet } from "@kura-ui/react/agent-fleet";

export function AgentFleetMinimal() {
  const [t] = React.useState(() => Date.now());
  return (
    <AgentFleet
      className="w-full max-w-xl"
      agents={[
        {
          id: "explorer",
          name: "explorer",
          role: "Maps call sites",
          status: "done",
          activity: "Found 23 calls to fetchUser() in 9 files",
          startedAt: t - 540_000,
          endedAt: t - 446_000,
          tokens: 84_200,
        },
        {
          id: "test-writer",
          name: "test-writer",
          role: "Updates tests",
          status: "running",
          activity: "Rewriting session mocks in login.test.ts",
          progress: 0.4,
          startedAt: t - 128_000,
          tokens: 31_900,
        },
        {
          id: "reviewer",
          name: "reviewer",
          role: "Reviews the diff",
          status: "queued",
          activity: "Starts when the patches land",
        },
      ]}
    />
  );
}
```

### Live fleet (full)

Six subagents advance: an approval clears, patches land, tests fail, the reviewer starts. Controlled selection.

```tsx
"use client";

import * as React from "react";
import { AgentFleet, type FleetAgent } from "@kura-ui/react/agent-fleet";

const BEATS: Beat[] = [
  {},
  {
    client: { progress: 0.71, tokens: 61_400, cost: 0.17 },
    tests: { progress: 0.34, tokens: 22_800 },
  },
  {
    middleware: {
      status: "running",
      activity: "Moving session refresh into the edge middleware",
      progress: 0.42,
    },
    tests: { progress: 0.48, tokens: 29_300 },
  },
  {
    client: {
      status: "done",
      activity: "Patched 9 call sites · +64 −41",
      progress: 1,
      tokens: 72_900,
      cost: 0.2,
    },
    middleware: { progress: 0.7, tokens: 38_100 },
  },
  {
    tests: {
      activity: "Running pnpm test --filter web",
      progress: 0.81,
      tokens: 40_600,
    },
    middleware: {
      status: "done",
      activity: "Patched src/middleware.ts · +38 −12",
      progress: 1,
      tokens: 44_500,
      cost: 0.12,
    },
  },
  {
    tests: {
      status: "failed",
      activity: "1 failed · login.test.ts › refreshes an expired session",
      tokens: 42_300,
      cost: 0.11,
    },
  },
  {
    reviewer: {
      status: "running",
      activity: "Reading the diff: 7 files, +142 −38",
      progress: 0.36,
      tokens: 18_200,
      cost: 0.05,
    },
  },
];

function initialFleet(): FleetAgent[] {
  return [
    {
      id: "explorer",
      name: "explorer",
      role: "Maps call sites",
      status: "done",
      activity: "Found 23 calls to fetchUser() in 9 files",
      startedAt: ago(612),
      endedAt: ago(518),
      tokens: 84_200,
      cost: 0.21,
    },
    {
      id: "docs",
      name: "docs-reader",
      role: "Reads @acme/auth docs",
      status: "done",
      activity: "Summarised the session and refresh APIs",
      startedAt: ago(605),
      endedAt: ago(561),
      tokens: 26_700,
      cost: 0.07,
    },
    {
      id: "client",
      name: "client-patcher",
      role: "Patches src/auth/client.ts",
      status: "running",
      activity: "Replacing fetchUser() with auth.getSession()",
      progress: 0.55,
      startedAt: ago(214),
      tokens: 53_100,
      cost: 0.15,
    },
    {
      id: "middleware",
      name: "middleware-patcher",
      role: "Patches src/middleware.ts",
      status: "waiting",
      activity: "Needs approval to edit src/middleware.ts",
      progress: 0.3,
      startedAt: ago(201),
      tokens: 31_400,
      cost: 0.09,
    },
    {
      id: "tests",
      name: "test-writer",
      role: "Updates tests",
      status: "running",
      activity: "Rewriting session mocks in login.test.ts",
      progress: 0.22,
      startedAt: ago(96),
      tokens: 17_600,
      cost: 0.05,
    },
    {
      id: "reviewer",
      name: "reviewer",
      role: "Reviews the diff",
      status: "queued",
      activity: "Starts when the patches land",
    },
  ];
}

const ago = (s: number) => Date.now() - s * 1000;

export function AgentFleetFull() {
  const [agents, setAgents] = React.useState<FleetAgent[]>(initialFleet);
  const [beat, setBeat] = React.useState(0);
  const [selectedId, setSelectedId] = React.useState<string | null>("client");

  React.useEffect(() => {
    if (beat >= BEATS.length - 1) return;
    const t = setTimeout(() => {
      const next = beat + 1;
      const patch = BEATS[next] ?? {};
      const now = Date.now();
      setAgents((list) =>
        list.map((a) => {
          const p = patch[a.id];
          if (!p) return a;
          const merged = { ...a, ...p };
          if (p.status === "running" && a.startedAt == null) merged.startedAt = now;
          if ((p.status === "done" || p.status === "failed") && a.endedAt == null)
            merged.endedAt = now;
          return merged;
        }),
      );
      setBeat(next);
    }, 1600);
    return () => clearTimeout(t);
  }, [beat]);

  return (
    <AgentFleet
      className="w-full max-w-2xl"
      agents={agents}
      selectedId={selectedId}
      onSelect={setSelectedId}
    />
  );
}
```

### Compact (variant)

One 36px row per agent.

```tsx
"use client";

import * as React from "react";
import { AgentFleet, type FleetAgent } from "@kura-ui/react/agent-fleet";

function initialFleet(): FleetAgent[] {
  return [
    {
      id: "explorer",
      name: "explorer",
      role: "Maps call sites",
      status: "done",
      activity: "Found 23 calls to fetchUser() in 9 files",
      startedAt: ago(612),
      endedAt: ago(518),
      tokens: 84_200,
      cost: 0.21,
    },
    {
      id: "docs",
      name: "docs-reader",
      role: "Reads @acme/auth docs",
      status: "done",
      activity: "Summarised the session and refresh APIs",
      startedAt: ago(605),
      endedAt: ago(561),
      tokens: 26_700,
      cost: 0.07,
    },
    {
      id: "client",
      name: "client-patcher",
      role: "Patches src/auth/client.ts",
      status: "running",
      activity: "Replacing fetchUser() with auth.getSession()",
      progress: 0.55,
      startedAt: ago(214),
      tokens: 53_100,
      cost: 0.15,
    },
    {
      id: "middleware",
      name: "middleware-patcher",
      role: "Patches src/middleware.ts",
      status: "waiting",
      activity: "Needs approval to edit src/middleware.ts",
      progress: 0.3,
      startedAt: ago(201),
      tokens: 31_400,
      cost: 0.09,
    },
    {
      id: "tests",
      name: "test-writer",
      role: "Updates tests",
      status: "running",
      activity: "Rewriting session mocks in login.test.ts",
      progress: 0.22,
      startedAt: ago(96),
      tokens: 17_600,
      cost: 0.05,
    },
    {
      id: "reviewer",
      name: "reviewer",
      role: "Reviews the diff",
      status: "queued",
      activity: "Starts when the patches land",
    },
  ];
}

const ago = (s: number) => Date.now() - s * 1000;

export function AgentFleetCompact() {
  const [agents] = React.useState(initialFleet);
  return (
    <AgentFleet
      className="w-full max-w-2xl"
      density="compact"
      agents={agents}
      defaultSelectedId="middleware"
      onSelect={() => {}}
    />
  );
}
```

### Empty (state)

Before any delegation.

```tsx
import { AgentFleet } from "@kura-ui/react/agent-fleet";

export function AgentFleetEmpty() {
  return <AgentFleet className="w-full max-w-xl" agents={[]} />;
}
```

### Master / detail with tool-call (composition)

Selecting a subagent shows its recent tool calls.

```tsx
"use client";

import { FilePen, FileSearch, SquareTerminal } from "lucide-react";
import * as React from "react";
import { ToolCall } from "@kura-ui/react/tool-call";
import { AgentFleet, type FleetAgent } from "@kura-ui/react/agent-fleet";

const CALLS: Record<string, React.ReactNode> = {
  explorer: (
    <>
      <ToolCall
        name="grep"
        icon={<FileSearch />}
        args={{ pattern: "fetchUser\\(", path: "src" }}
        status="success"
        duration={420}
        summary="fetchUser\( in src/ · 23 matches"
        variant="inline"
      />
      <ToolCall
        name="read_file"
        icon={<FileSearch />}
        args={{ path: "src/routes/api/session.ts" }}
        status="success"
        duration={180}
        variant="inline"
      />
    </>
  ),
  "test-writer": (
    <>
      <ToolCall
        name="edit_file"
        icon={<FilePen />}
        args={{ path: "src/auth/__tests__/login.test.ts" }}
        status="success"
        duration={2_400}
        summary="login.test.ts · +18 −9"
        variant="inline"
      />
      <ToolCall
        name="bash"
        icon={<SquareTerminal />}
        args={{ command: "pnpm test --filter web" }}
        status="running"
        variant="inline"
      />
    </>
  ),
  reviewer: <p className="px-1.5 text-[13px] text-kura-fg-subtle">No tool calls yet.</p>,
};

export function AgentFleetComposition() {
  const [t] = React.useState(() => Date.now());
  const [selectedId, setSelectedId] = React.useState("test-writer");
  const agents: FleetAgent[] = [
    {
      id: "explorer",
      name: "explorer",
      role: "Maps call sites",
      status: "done",
      activity: "Found 23 calls to fetchUser() in 9 files",
      startedAt: t - 540_000,
      endedAt: t - 446_000,
      tokens: 84_200,
      cost: 0.21,
    },
    {
      id: "test-writer",
      name: "test-writer",
      role: "Updates tests",
      status: "running",
      activity: "Running pnpm test --filter web",
      progress: 0.78,
      startedAt: t - 151_000,
      tokens: 40_600,
      cost: 0.11,
    },
    {
      id: "reviewer",
      name: "reviewer",
      role: "Reviews the diff",
      status: "queued",
      activity: "Starts when tests pass",
    },
  ];
  const agent = agents.find((a) => a.id === selectedId);
  return (
    <div className="grid w-full max-w-3xl gap-3 md:grid-cols-[minmax(0,1fr)_minmax(0,1fr)]">
      <AgentFleet
        agents={agents}
        density="compact"
        selectedId={selectedId}
        onSelect={setSelectedId}
      />
      <section
        aria-labelledby="fleet-detail-title"
        className="flex min-w-0 flex-col overflow-hidden rounded-kura-xl bg-kura-surface shadow-kura-sm"
      >
        <div className="flex h-10 items-center gap-2 border-kura-border-subtle border-b px-3.5">
          <h4 id="fleet-detail-title" className="font-medium text-[13px]">
            {agent?.name}
          </h4>
          <span className="truncate text-[12px] text-kura-fg-subtle">{agent?.role}</span>
        </div>
        <div className="flex flex-col gap-0.5 p-2">{CALLS[selectedId]}</div>
      </section>
    </div>
  );
}
```

## Dependencies

- npm: none
- Kura registry items: `kura-theme`, `kura-format`, `shimmer-text`, `status-glyph`, `use-controllable-state`, `use-elapsed`, `utils`
- Related: `run-header`, `task-list`, `trace-waterfall`, `tool-call`, `agent-timeline`
