{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "agent-fleet",
  "type": "registry:ui",
  "title": "Agent Fleet",
  "description": "Panel of parallel subagents: status, name and role, live activity, thin progress, elapsed time, tokens and cost, with a fleet summary and selection.",
  "dependencies": [],
  "registryDependencies": [
    "https://kuro.abstergo.fyi/r/kura-theme.json",
    "https://kuro.abstergo.fyi/r/kura-format.json",
    "https://kuro.abstergo.fyi/r/shimmer-text.json",
    "https://kuro.abstergo.fyi/r/status-glyph.json",
    "https://kuro.abstergo.fyi/r/use-controllable-state.json",
    "https://kuro.abstergo.fyi/r/use-elapsed.json",
    "utils"
  ],
  "files": [
    {
      "path": "registry/kura/agent-fleet/agent-fleet.tsx",
      "type": "registry:ui",
      "target": "components/kura/agent-fleet/agent-fleet.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { useControllableState } from \"@/hooks/kura/use-controllable-state\";\nimport { useElapsed } from \"@/hooks/kura/use-elapsed\";\nimport { cn } from \"@/lib/utils\";\nimport { formatClock, formatCost, formatTokens } from \"@/lib/kura/format\";\nimport { ShimmerText } from \"@/components/kura/shimmer-text\";\nimport { type AgentStatus, StatusGlyph } from \"@/components/kura/status-glyph\";\n\nexport interface FleetAgent {\n  /** Stable id, used for selection and React keys. */\n  id: string;\n  /** Short name, e.g. \"explorer\". */\n  name: string;\n  /** What this agent is for, e.g. \"Maps call sites\". */\n  role?: string;\n  status: AgentStatus;\n  /** What it is doing right now (running) or its last result (done / failed). One line. */\n  activity?: string;\n  /** 0–1. Shown as a thin bar while the agent is live. */\n  progress?: number;\n  startedAt?: Date | number;\n  /** Freezes the elapsed time at endedAt − startedAt. */\n  endedAt?: Date | number;\n  /** Total tokens used. */\n  tokens?: number;\n  /** Spend in USD. */\n  cost?: number;\n}\n\nexport interface AgentFleetProps\n  extends Omit<React.ComponentProps<\"div\">, \"onSelect\" | \"title\" | \"children\"> {\n  /** The subagents, in display order. */\n  agents: FleetAgent[];\n  /** Panel title. Default \"Subagents\". */\n  title?: React.ReactNode;\n  /** Selected agent id (controlled). */\n  selectedId?: string | null;\n  /** Initially selected agent id when uncontrolled. */\n  defaultSelectedId?: string | null;\n  /** Called with the agent id when a row is chosen (click, Enter or Space). Makes rows selectable. */\n  onSelect?: (id: string) => void;\n  /** `compact` = one 36px row per agent; `comfortable` = two lines with role and activity. Default \"comfortable\". */\n  density?: \"compact\" | \"comfortable\";\n  /** Shown when `agents` is empty. */\n  empty?: React.ReactNode;\n}\n\nconst LIVE: AgentStatus[] = [\"running\", \"waiting\", \"paused\"];\n\nconst SUMMARY_ORDER: { status: AgentStatus; word: string; tone?: string }[] = [\n  { status: \"running\", word: \"running\" },\n  { status: \"waiting\", word: \"waiting\", tone: \"text-kura-warning-text\" },\n  { status: \"paused\", word: \"paused\" },\n  { status: \"failed\", word: \"failed\", tone: \"text-kura-danger-text\" },\n  { status: \"done\", word: \"done\" },\n  { status: \"queued\", word: \"queued\" },\n  { status: \"skipped\", word: \"skipped\" },\n];\n\nconst SPOKEN: Record<AgentStatus, string> = {\n  queued: \"queued\",\n  running: \"running\",\n  waiting: \"waiting for you\",\n  paused: \"paused\",\n  done: \"done\",\n  failed: \"failed\",\n  skipped: \"skipped\",\n};\n\nfunction useAnnouncements(agents: FleetAgent[]) {\n  const prev = React.useRef<Map<string, AgentStatus> | null>(null);\n  const [message, setMessage] = React.useState(\"\");\n  React.useEffect(() => {\n    const next = new Map(agents.map((a) => [a.id, a.status]));\n    const before = prev.current;\n    prev.current = next;\n    if (!before) return;\n    const changes = agents\n      .filter((a) => before.has(a.id) && before.get(a.id) !== a.status && a.status !== \"running\")\n      .map((a) => `${a.name} ${SPOKEN[a.status]}`);\n    if (changes.length) setMessage(changes.join(\", \"));\n  }, [agents]);\n  return message;\n}\n\n/**\n * Parallel subagents at a glance: one row per agent with its status glyph, name and role, what it\n * is doing now (shimmering while it runs), a thin progress bar, elapsed time, tokens and cost.\n * The header sums the fleet (\"3 running · 1 waiting · 2 done\"). Rows are a selectable listbox\n * when `onSelect` is given.\n */\nexport function AgentFleet({\n  agents,\n  title = \"Subagents\",\n  selectedId: selectedProp,\n  defaultSelectedId = null,\n  onSelect,\n  density = \"comfortable\",\n  empty,\n  className,\n  ...props\n}: AgentFleetProps) {\n  const id = React.useId();\n  const [selected, setSelected] = useControllableState<string | null>({\n    value: selectedProp,\n    defaultValue: defaultSelectedId,\n    onChange: (v) => v != null && onSelect?.(v),\n  });\n  const selectable = onSelect != null || selectedProp !== undefined;\n  const [focusId, setFocusId] = React.useState<string | null>(null);\n  const rowRefs = React.useRef(new Map<string, HTMLLIElement>());\n  const message = useAnnouncements(agents);\n\n  const counts = new Map<AgentStatus, number>();\n  for (const a of agents) counts.set(a.status, (counts.get(a.status) ?? 0) + 1);\n  const summary = SUMMARY_ORDER.filter((s) => counts.get(s.status));\n\n  const tabStop =\n    agents.find((a) => a.id === focusId)?.id ??\n    agents.find((a) => a.id === selected)?.id ??\n    agents[0]?.id;\n\n  const moveFocus = (to: number) => {\n    const a = agents[Math.max(0, Math.min(agents.length - 1, to))];\n    if (!a) return;\n    setFocusId(a.id);\n    rowRefs.current.get(a.id)?.focus();\n  };\n\n  const onKeyDown = (e: React.KeyboardEvent<HTMLUListElement>) => {\n    const index = agents.findIndex((a) => a.id === (e.target as HTMLElement).dataset.agentId);\n    if (index < 0) return;\n    if (e.key === \"ArrowDown\" || e.key === \"j\") moveFocus(index + 1);\n    else if (e.key === \"ArrowUp\" || e.key === \"k\") moveFocus(index - 1);\n    else if (e.key === \"Home\") moveFocus(0);\n    else if (e.key === \"End\") moveFocus(agents.length - 1);\n    else if (e.key === \"Enter\" || e.key === \" \") setSelected(agents[index]?.id ?? null);\n    else return;\n    e.preventDefault();\n  };\n\n  return (\n    <div\n      data-slot=\"agent-fleet\"\n      data-density={density}\n      className={cn(\n        \"@container flex min-w-0 flex-col overflow-hidden rounded-kura-xl bg-kura-surface font-kura-sans text-kura-fg shadow-kura-sm\",\n        className,\n      )}\n      {...props}\n    >\n      <div\n        data-slot=\"agent-fleet-header\"\n        className=\"flex h-10 shrink-0 items-center gap-3 border-kura-border-subtle border-b px-3.5\"\n      >\n        <h3 id={`${id}-title`} className=\"flex items-baseline gap-2 font-medium text-[13px]\">\n          {title}\n          <span className=\"font-kura-mono font-normal text-[12px] text-kura-fg-subtle tabular-nums\">\n            {agents.length}\n          </span>\n        </h3>\n        {summary.length > 0 && (\n          <p\n            data-slot=\"agent-fleet-summary\"\n            className=\"ml-auto hidden min-w-0 truncate text-[12px] text-kura-fg-subtle @xs:block\"\n          >\n            {summary.map((s, i) => (\n              <React.Fragment key={s.status}>\n                {i > 0 && <span aria-hidden> · </span>}\n                <span className={cn(\"whitespace-nowrap\", s.tone)}>\n                  <span className=\"font-kura-mono tabular-nums\">{counts.get(s.status)}</span>{\" \"}\n                  {s.word}\n                </span>\n              </React.Fragment>\n            ))}\n          </p>\n        )}\n      </div>\n\n      {agents.length === 0 ? (\n        <div\n          data-slot=\"agent-fleet-empty\"\n          className=\"flex min-h-24 flex-col items-center justify-center gap-1 px-6 py-6 text-center\"\n        >\n          {empty ?? (\n            <>\n              <p className=\"font-medium text-[13px] text-kura-fg-muted\">No subagents yet</p>\n              <p className=\"text-[12px] text-kura-fg-subtle\">\n                Delegated work shows up here as it starts.\n              </p>\n            </>\n          )}\n        </div>\n      ) : (\n        <ul\n          role={selectable ? \"listbox\" : \"list\"}\n          aria-labelledby={`${id}-title`}\n          onKeyDown={selectable ? onKeyDown : undefined}\n          className=\"flex flex-col divide-y divide-kura-border-subtle\"\n        >\n          {agents.map((agent) => (\n            <AgentRow\n              key={agent.id}\n              agent={agent}\n              density={density}\n              selectable={selectable}\n              selected={selected === agent.id}\n              tabbable={tabStop === agent.id}\n              rowRef={(el) => {\n                if (el) rowRefs.current.set(agent.id, el);\n                else rowRefs.current.delete(agent.id);\n              }}\n              onFocus={() => setFocusId(agent.id)}\n              onChoose={() => setSelected(agent.id)}\n            />\n          ))}\n        </ul>\n      )}\n      <span role=\"status\" className=\"sr-only\">\n        {message}\n      </span>\n    </div>\n  );\n}\n\nfunction AgentRow({\n  agent,\n  density,\n  selectable,\n  selected,\n  tabbable,\n  rowRef,\n  onFocus,\n  onChoose,\n}: {\n  agent: FleetAgent;\n  density: \"compact\" | \"comfortable\";\n  selectable: boolean;\n  selected: boolean;\n  tabbable: boolean;\n  rowRef: (el: HTMLLIElement | null) => void;\n  onFocus: () => void;\n  onChoose: () => void;\n}) {\n  const live = LIVE.includes(agent.status);\n  const ticking = useElapsed(agent.startedAt, live && agent.endedAt == null);\n  const elapsed =\n    agent.startedAt != null && agent.endedAt != null\n      ? Math.max(0, +new Date(agent.endedAt) - +new Date(agent.startedAt))\n      : agent.startedAt != null\n        ? ticking\n        : null;\n  const running = agent.status === \"running\";\n  const compact = density === \"compact\";\n  const showProgress = agent.progress != null && live;\n\n  const activity = agent.activity ? (\n    <span\n      data-slot=\"agent-fleet-activity\"\n      className={cn(\n        \"block min-w-0 truncate\",\n        agent.status === \"waiting\" && \"text-kura-warning-text\",\n        agent.status === \"failed\" && \"text-kura-danger-text\",\n        (agent.status === \"done\" ||\n          agent.status === \"queued\" ||\n          agent.status === \"skipped\" ||\n          agent.status === \"paused\") &&\n          \"text-kura-fg-subtle\",\n      )}\n    >\n      {running ? (\n        <ShimmerText className=\"block max-w-full truncate\">{agent.activity}</ShimmerText>\n      ) : (\n        agent.activity\n      )}\n    </span>\n  ) : null;\n\n  const progress = showProgress ? (\n    <span\n      data-slot=\"agent-fleet-progress\"\n      aria-hidden\n      className={cn(\n        \"flex h-[3px] shrink-0 overflow-hidden rounded-full bg-kura-hover\",\n        compact ? \"w-12\" : \"w-20\",\n      )}\n    >\n      <span\n        className={cn(\n          \"h-full rounded-full transition-[width] duration-(--kura-duration-slow) ease-kura-out motion-reduce:transition-none\",\n          agent.status === \"running\" && \"bg-kura-accent\",\n          agent.status === \"waiting\" && \"bg-kura-warning\",\n          agent.status === \"paused\" && \"bg-kura-fg-subtle\",\n        )}\n        style={{ width: `${Math.min(1, Math.max(0, agent.progress ?? 0)) * 100}%` }}\n      />\n    </span>\n  ) : null;\n\n  const numbers = (\n    <span className=\"flex items-center gap-3 font-kura-mono text-[12px] text-kura-fg-subtle tabular-nums\">\n      {agent.tokens != null && (\n        <span\n          data-slot=\"agent-fleet-tokens\"\n          className={cn(\n            \"min-w-[5ch] text-right\",\n            compact ? \"hidden @md:inline-block\" : \"inline-block\",\n          )}\n        >\n          {formatTokens(agent.tokens)}\n        </span>\n      )}\n      {agent.cost != null && !compact && (\n        <span\n          data-slot=\"agent-fleet-cost\"\n          className=\"hidden min-w-[5ch] text-right @lg:inline-block\"\n        >\n          {formatCost(agent.cost)}\n        </span>\n      )}\n      <span\n        data-slot=\"agent-fleet-elapsed\"\n        className={cn(\"inline-block min-w-[4ch] text-right\", live && \"text-kura-fg-muted\")}\n      >\n        {elapsed != null ? formatClock(elapsed) : \"–\"}\n      </span>\n    </span>\n  );\n\n  const name = (\n    <span\n      data-slot=\"agent-fleet-name\"\n      className={cn(\n        \"shrink-0 font-medium text-[13px]\",\n        (agent.status === \"done\" || agent.status === \"skipped\") && \"text-kura-fg-muted\",\n      )}\n    >\n      {agent.name}\n    </span>\n  );\n\n  const progressText =\n    showProgress && agent.progress != null\n      ? `, ${Math.round(Math.min(1, Math.max(0, agent.progress)) * 100)}%`\n      : \"\";\n\n  return (\n    // biome-ignore lint/a11y/useAriaPropsSupportedByRole: role is \"option\" whenever aria-selected is set\n    // biome-ignore lint/a11y/useKeyWithClickEvents: Enter/Space are handled by the listbox's onKeyDown\n    <li\n      ref={rowRef}\n      role={selectable ? \"option\" : undefined}\n      aria-selected={selectable ? selected : undefined}\n      tabIndex={selectable ? (tabbable ? 0 : -1) : undefined}\n      data-slot=\"agent-fleet-agent\"\n      data-agent-id={agent.id}\n      data-status={agent.status}\n      data-selected={selected || undefined}\n      onClick={selectable ? onChoose : undefined}\n      onFocus={selectable ? onFocus : undefined}\n      className={cn(\n        \"relative min-w-0 px-3.5 outline-none\",\n        \"animate-[kura-rise-in_var(--kura-duration-base)_var(--ease-kura-out)_both] [--kura-rise-blur:0px] [--kura-rise-y:2px] motion-reduce:[--kura-rise-y:0px]\",\n        selectable &&\n          \"cursor-pointer transition-colors duration-(--kura-duration-instant) ease-kura-out hover:bg-kura-hover focus-visible:shadow-[inset_0_0_0_2px_var(--kura-ring)]\",\n        selected &&\n          \"bg-kura-active before:absolute before:inset-y-0 before:left-0 before:w-0.5 before:bg-kura-accent hover:bg-kura-active\",\n        compact\n          ? \"grid h-9 grid-cols-[16px_auto_minmax(0,1fr)_auto] items-center gap-x-2.5\"\n          : \"grid grid-cols-[16px_minmax(0,1fr)_auto] gap-x-3 py-2.5\",\n      )}\n    >\n      <StatusGlyph status={agent.status} className={cn(!compact && \"mt-0.5\")} />\n      <span className=\"sr-only\">{progressText}</span>\n      {compact ? (\n        <>\n          {name}\n          <span className=\"min-w-0 text-[12.5px] text-kura-fg-muted\">{activity}</span>\n          <span className=\"flex items-center gap-3\">\n            {progress && <span className=\"hidden @sm:flex\">{progress}</span>}\n            {numbers}\n          </span>\n        </>\n      ) : (\n        <>\n          <span className=\"flex min-w-0 flex-col gap-0.5\">\n            <span className=\"flex min-w-0 items-baseline gap-2 leading-5\">\n              {name}\n              {agent.role && (\n                <span\n                  data-slot=\"agent-fleet-role\"\n                  className=\"hidden min-w-0 truncate text-[12px] text-kura-fg-subtle @md:block\"\n                >\n                  {agent.role}\n                </span>\n              )}\n            </span>\n            {activity && (\n              <span className=\"min-w-0 text-[12.5px] text-kura-fg-muted leading-[18px]\">\n                {activity}\n              </span>\n            )}\n          </span>\n          <span className=\"flex flex-col items-end justify-between gap-1.5 self-stretch py-0.5\">\n            {numbers}\n            {progress}\n          </span>\n        </>\n      )}\n    </li>\n  );\n}\n"
    },
    {
      "path": "registry/kura/agent-fleet/index.ts",
      "type": "registry:ui",
      "target": "components/kura/agent-fleet/index.ts",
      "content": "export { AgentFleet, type AgentFleetProps, type FleetAgent } from \"./agent-fleet\";\n"
    }
  ],
  "categories": [
    "agent",
    "agent",
    "subagents",
    "fleet",
    "parallel"
  ],
  "docs": "Docs: https://kuro.abstergo.fyi/components/agent-fleet · Markdown for agents: https://kuro.abstergo.fyi/components/agent-fleet.md",
  "meta": {
    "kura": {
      "status": "experimental",
      "whenToUse": [
        "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')."
      ],
      "whenNotToUse": [
        "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."
      ]
    }
  }
}
