{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "agent-roster",
  "title": "Agent Roster",
  "description": "A multi-agent fleet panel — per-agent state dot, model chip, current-task line and capacity bar, attention-first sorting, a real listbox keyboard path, and four data states.",
  "dependencies": [
    "lucide-react",
    "zod"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "src/registry/ui/agent-roster.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { AlertCircle, Bot, Cpu, RefreshCcw, Wrench } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport type { AgentRosterAgent, AgentRosterData, AgentRosterState } from \"./agent-roster.contract\"\n\n/**\n * State → colour, as a theme VARIABLE. The winner is published on the card as\n * `--agent-tone` and every child (dot, halo, ring) paints with that one variable,\n * so re-theming the whole roster is six lines here and nothing anywhere else.\n */\nconst TONE: Record<AgentRosterState, string> = {\n  thinking: \"var(--chart-1)\",\n  \"using-tool\": \"var(--chart-4)\",\n  waiting: \"var(--chart-3)\",\n  error: \"var(--destructive)\",\n  idle: \"var(--muted-foreground)\",\n  offline: \"var(--muted-foreground)\",\n}\n\nexport interface AgentRosterLabels {\n  thinking: string\n  /** Prefix in front of the tool name — the name itself stays its own slot. */\n  usingTool: string\n  /** Stands in for the tool name when `state: \"using-tool\"` arrives without one. */\n  unnamedTool: string\n  waiting: string\n  error: string\n  idle: string\n  offline: string\n  /** Header noun, pluralised naively (\"6 agents\"). */\n  agents: string\n  load: string\n  queued: string\n  noTask: string\n}\n\nconst DEFAULT_LABELS: AgentRosterLabels = {\n  thinking: \"Thinking\",\n  usingTool: \"Using\",\n  unnamedTool: \"a tool\",\n  waiting: \"Waiting for you\",\n  error: \"Run failed\",\n  idle: \"Idle\",\n  offline: \"Offline\",\n  agents: \"agents\",\n  load: \"Load\",\n  queued: \"queued\",\n  noTask: \"No task assigned\",\n}\n\n/** \"Burning tokens right now\" — the only states that animate and the only ones counted as working. */\nconst ACTIVE = new Set<AgentRosterState>([\"thinking\", \"using-tool\"])\n\n/** States a human has to do something about. They sort first and they are the only ones announced. */\nconst ATTENTION = new Set<AgentRosterState>([\"waiting\", \"error\"])\n\n/**\n * Attention-first ordering. A roster is a triage surface: the two agents that\n * need you must not be below the fold behind eleven happy ones.\n */\nconst RANK: Record<AgentRosterState, number> = {\n  error: 0,\n  waiting: 1,\n  \"using-tool\": 2,\n  thinking: 3,\n  idle: 4,\n  offline: 5,\n}\n\n/** Order the header summarises in — stable regardless of what the data contains. */\nconst SUMMARY_ORDER: AgentRosterState[] = [\"error\", \"waiting\", \"using-tool\", \"thinking\", \"idle\", \"offline\"]\n\nconst GRID_CLASS = \"grid gap-3 [grid-template-columns:repeat(auto-fill,minmax(min(15rem,100%),1fr))]\"\nconst LIST_CLASS = \"flex flex-col gap-2\"\n\nconst pulseClass = \"animate-pulse rounded bg-muted motion-reduce:animate-none\"\n\n/** How long a type-ahead buffer survives between keystrokes, in ms. */\nconst TYPEAHEAD_RESET_MS = 700\n\nfunction stateLabel(state: AgentRosterState, text: AgentRosterLabels): string {\n  switch (state) {\n    case \"thinking\":\n      return text.thinking\n    case \"using-tool\":\n      return text.usingTool\n    case \"waiting\":\n      return text.waiting\n    case \"error\":\n      return text.error\n    case \"offline\":\n      return text.offline\n    default:\n      return text.idle\n  }\n}\n\nconst firstChar = (word: string) => Array.from(word)[0] ?? \"\"\n\n/**\n * Initials fallback. Latin names take the first letter of the first two words;\n * a CJK name is unreadable at one character, so it takes two. `initials` on the\n * record overrides both.\n */\nfunction initialsOf(agent: Pick<AgentRosterAgent, \"initials\" | \"name\">): string {\n  if (agent.initials) return agent.initials\n  const words = agent.name.trim().split(/\\s+/).filter(Boolean)\n  if (words.length === 0) return \"?\"\n  if (words.length > 1) return words.slice(0, 2).map(firstChar).join(\"\").toUpperCase()\n  const chars = Array.from(words[0])\n  return (/[A-Za-z]/.test(words[0]) ? chars.slice(0, 1) : chars.slice(0, 2)).join(\"\").toUpperCase()\n}\n\n/** Never negative, never NaN — a bad number renders as an empty bar, not as `NaN%`. */\nfunction safeLoad(load: number): number {\n  return Number.isFinite(load) ? Math.max(0, load) : 0\n}\n\nfunction defaultFormatLoad(load: number): string {\n  return `${Math.round(safeLoad(load) * 100)}%`\n}\n\n/* -------------------------------------------------------------------- pieces */\n\n/**\n * State as a SHAPE, not only a colour: pulsing disc (thinking), spinning arc\n * (tool), ringed disc (needs you / failed), hollow disc (idle / offline). Colour\n * alone disappears in a monochrome theme and for a colour-blind reader, and the\n * shapes still differ when `prefers-reduced-motion` stops every animation.\n */\nfunction StateDot({ pulse, state }: { pulse: boolean; state: AgentRosterState }) {\n  const animated = pulse && ACTIVE.has(state)\n  const pinging = animated && state === \"thinking\"\n  const spinning = animated && state === \"using-tool\"\n  const ringed = ATTENTION.has(state)\n  const hollow = state === \"idle\" || state === \"offline\"\n\n  return (\n    <span aria-hidden=\"true\" className=\"relative flex size-2.5 shrink-0 items-center justify-center\">\n      {pinging && (\n        <span className=\"absolute inset-0 animate-ping rounded-full bg-[color:var(--agent-tone)] opacity-60 motion-reduce:hidden\" />\n      )}\n      {spinning && (\n        // Stops under reduced motion but stays visible: a lopsided ring still\n        // reads as \"a tool is running\".\n        <span className=\"absolute inset-0 animate-spin rounded-full border border-[color:color-mix(in_oklab,var(--agent-tone)_25%,transparent)] border-t-[color:var(--agent-tone)] motion-reduce:animate-none\" />\n      )}\n      {ringed && (\n        <span className=\"absolute inset-0 rounded-full border border-[color:color-mix(in_oklab,var(--agent-tone)_40%,transparent)]\" />\n      )}\n      <span\n        className={cn(\n          \"rounded-full bg-[color:var(--agent-tone)]\",\n          hollow ? \"size-2 border border-[color:var(--agent-tone)] bg-transparent\" : spinning ? \"size-1\" : \"size-2\",\n        )}\n      />\n    </span>\n  )\n}\n\nfunction AgentAvatar({ agent, size }: { agent: AgentRosterAgent; size: \"sm\" | \"md\" }) {\n  const [failed, setFailed] = React.useState(false)\n\n  /**\n   * On a pre-rendered page a cached image can fail BEFORE hydration attaches\n   * `onError`; the event never arrives and the broken glyph stays forever. The\n   * ref callback probes for that case synchronously.\n   */\n  const probe = React.useCallback((node: HTMLImageElement | null) => {\n    if (node?.complete && node.naturalWidth === 0) setFailed(true)\n  }, [])\n\n  const showImage = Boolean(agent.avatarUrl) && !failed\n\n  return (\n    <span\n      className={cn(\n        \"flex shrink-0 items-center justify-center overflow-hidden rounded-full border bg-muted font-medium text-muted-foreground\",\n        size === \"md\" ? \"size-10 text-xs\" : \"size-8 text-[11px]\",\n      )}\n    >\n      {showImage ? (\n        // eslint-disable-next-line @next/next/no-img-element -- portable registry source, not bound to next/image\n        <img\n          alt=\"\"\n          className=\"size-full object-cover\"\n          onError={() => setFailed(true)}\n          ref={probe}\n          src={agent.avatarUrl}\n        />\n      ) : (\n        // The name is right next to it — reading the initials aloud only repeats it.\n        <span aria-hidden=\"true\">{initialsOf(agent)}</span>\n      )}\n    </span>\n  )\n}\n\nfunction Chip({\n  children,\n  icon: Icon,\n  srLabel,\n  title,\n}: {\n  children: React.ReactNode\n  icon: typeof Cpu\n  srLabel: string\n  title?: string\n}) {\n  return (\n    <span\n      className=\"inline-flex min-w-0 max-w-full items-center gap-1 rounded-md border px-1.5 py-0.5 font-mono text-[11px] text-muted-foreground\"\n      title={title}\n    >\n      <Icon aria-hidden=\"true\" className=\"size-3 shrink-0\" />\n      <span className=\"sr-only\">{srLabel} </span>\n      <span className=\"truncate\">{children}</span>\n    </span>\n  )\n}\n\n/**\n * Capacity, not progress: the bar says how full this agent is, and a value over\n * 1 keeps its true figure instead of clamping to a healthy-looking 100 %.\n */\nfunction LoadBar({\n  agent,\n  className,\n  formatLoad,\n  text,\n}: {\n  agent: AgentRosterAgent\n  className?: string\n  formatLoad: (load: number) => string\n  text: AgentRosterLabels\n}) {\n  const load = safeLoad(agent.load)\n  const percent = Math.round(load * 100)\n  const width = Math.min(100, percent)\n  const over = load > 1\n  const offline = agent.state === \"offline\"\n  const tone = offline\n    ? \"var(--muted-foreground)\"\n    : over\n      ? \"var(--destructive)\"\n      : load >= 0.75\n        ? \"var(--chart-4)\"\n        : \"var(--chart-2)\"\n\n  return (\n    <div className={cn(\"flex min-w-0 flex-col gap-1\", className)}>\n      <div className=\"flex min-w-0 items-baseline justify-between gap-2 text-[11px] text-muted-foreground\">\n        <span className=\"truncate\">\n          {text.load}\n          {agent.queued !== undefined && agent.queued > 0 ? ` · ${agent.queued} ${text.queued}` : \"\"}\n        </span>\n        <span className={cn(\"shrink-0 tabular-nums\", over && \"font-medium text-destructive\")}>\n          {formatLoad(agent.load)}\n          {/* Said in TEXT, not only in the bar's aria-valuetext: `option` is\n              children-presentational, so inside the selectable roster the bar's\n              value can be pruned and the red figure — the only visual marker of\n              over-capacity — would carry no wording at all. */}\n          {over && <span className=\"sr-only\"> over capacity</span>}\n        </span>\n      </div>\n      <div\n        aria-label={`${agent.name} workload`}\n        aria-valuemax={100}\n        aria-valuemin={0}\n        aria-valuenow={width}\n        aria-valuetext={`${percent}% of capacity${over ? \", over capacity\" : \"\"}`}\n        className={cn(\"h-1.5 w-full overflow-hidden rounded-full bg-muted\", offline && \"opacity-60\")}\n        role=\"progressbar\"\n      >\n        <div\n          className=\"h-full rounded-full transition-[width] duration-500 motion-reduce:transition-none\"\n          style={{ backgroundColor: tone, width: `${width}%` }}\n        />\n      </div>\n    </div>\n  )\n}\n\n/** The body of one card, shared by both variants and by both wrapper elements. */\nfunction AgentBody({\n  agent,\n  formatLoad,\n  pulse,\n  showLoad,\n  text,\n  variant,\n}: {\n  agent: AgentRosterAgent\n  formatLoad: (load: number) => string\n  pulse: boolean\n  showLoad: boolean\n  text: AgentRosterLabels\n  variant: \"grid\" | \"list\"\n}) {\n  const label = stateLabel(agent.state, text)\n  const toolName = agent.state === \"using-tool\" ? (agent.tool ?? text.unnamedTool) : undefined\n\n  const statusLine = (\n    <span className=\"flex min-w-0 items-center gap-1.5 text-xs\">\n      <StateDot pulse={pulse} state={agent.state} />\n      <span className={cn(\"shrink-0 whitespace-nowrap\", agent.state === \"error\" && \"text-destructive\")}>{label}</span>\n      {toolName && (\n        // The one part of the line that can be arbitrarily long: it truncates\n        // instead of widening the card it sits in.\n        <span\n          className=\"flex min-w-0 items-center gap-1 font-mono text-[11px] text-muted-foreground\"\n          title={toolName}\n        >\n          <Wrench aria-hidden=\"true\" className=\"size-3 shrink-0\" />\n          <span className=\"truncate\">{toolName}</span>\n        </span>\n      )}\n    </span>\n  )\n\n  const task = agent.currentTask?.trim()\n  const taskLine = task ? (\n    // wrap-anywhere, not break-words: only overflow-wrap:anywhere lowers the\n    // min-content width, which is what stops a 90-character ticket title from\n    // making the card wider than its grid track.\n    <p\n      className={cn(\n        \"line-clamp-2 min-w-0 wrap-anywhere text-xs\",\n        agent.state === \"error\" ? \"text-destructive\" : \"text-muted-foreground\",\n      )}\n      title={task}\n    >\n      {task}\n    </p>\n  ) : (\n    <p className=\"min-w-0 truncate text-xs text-muted-foreground/70\">{text.noTask}</p>\n  )\n\n  if (variant === \"list\") {\n    return (\n      // flex-wrap, not a fixed column template: a row squeezed into a sidebar\n      // stacks its groups instead of truncating the task down to two words.\n      <div className=\"flex w-full min-w-0 flex-wrap items-center gap-x-3 gap-y-2\">\n        <AgentAvatar agent={agent} key={agent.avatarUrl ?? \"initials\"} size=\"sm\" />\n        <div className=\"flex min-w-0 flex-1 basis-40 flex-col gap-0.5\">\n          <span className=\"truncate text-sm font-medium\">{agent.name}</span>\n          <span className=\"truncate text-xs text-muted-foreground\">{agent.role}</span>\n        </div>\n        <div className=\"flex min-w-0 flex-[2] basis-56 flex-col gap-0.5\">\n          {statusLine}\n          {taskLine}\n        </div>\n        <Chip icon={Cpu} srLabel=\"Model\" title={agent.model}>\n          {agent.model}\n        </Chip>\n        {showLoad && <LoadBar agent={agent} className=\"w-24 shrink-0\" formatLoad={formatLoad} text={text} />}\n      </div>\n    )\n  }\n\n  return (\n    <>\n      <div className=\"flex min-w-0 items-start gap-2.5\">\n        <AgentAvatar agent={agent} key={agent.avatarUrl ?? \"initials\"} size=\"md\" />\n        <div className=\"flex min-w-0 flex-1 flex-col gap-0.5\">\n          <span className=\"truncate text-sm font-medium\">{agent.name}</span>\n          <span className=\"truncate text-xs text-muted-foreground\">{agent.role}</span>\n        </div>\n      </div>\n\n      <div className=\"flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1\">\n        {statusLine}\n        <Chip icon={Cpu} srLabel=\"Model\" title={agent.model}>\n          {agent.model}\n        </Chip>\n      </div>\n\n      {taskLine}\n\n      {/* mt-auto: every card in a row is stretched to the tallest one, and the\n          bars still line up along the bottom edge instead of floating. */}\n      {showLoad && <LoadBar agent={agent} className=\"mt-auto pt-1\" formatLoad={formatLoad} text={text} />}\n    </>\n  )\n}\n\n/* ---------------------------------------------------------------- component */\n\nexport interface AgentRosterProps\n  extends Omit<React.HTMLAttributes<HTMLDivElement>, \"onSelect\">,\n    AgentRosterData {\n  /** `grid` = one card per agent; `list` = compact one-line rows. @default \"grid\" */\n  variant?: \"grid\" | \"list\"\n  /**\n   * `given` keeps your order. `attention` floats the agents that need a human\n   * (failed, then waiting), then the working ones, then idle, then offline —\n   * a stable sort, so agents in the same bucket keep their original order.\n   * @default \"given\"\n   */\n  sort?: \"given\" | \"attention\"\n  /**\n   * Turns the roster into a real listbox: cards become options with a roving\n   * tabindex, arrow-key navigation, type-ahead and Enter/Space. Omit it and the\n   * cards render as an ordinary list with no cursor and no hover — an element\n   * that looks clickable but isn't is worse than a plain one.\n   */\n  onSelect?: (agentId: string) => void\n  /** The currently selected agent, owned by you. Drives `aria-selected` and the highlight. */\n  selectedId?: string | null\n  /** Shown in the `status=\"error\"` branch; omit it to hide the affordance. */\n  onRetry?: () => void\n  /** Accessible name of the roster, and the heading in the summary row. @default \"Agents\" */\n  label?: string\n  /** Render the counts-by-state header. @default true */\n  showSummary?: boolean\n  /** Render the per-agent capacity bar. @default true */\n  showLoad?: boolean\n  /** Allow the active-state motion (pulse halo / spinning arc). @default true */\n  pulse?: boolean\n  /**\n   * Announce agents that ENTER an attention state (waiting / failed) through one\n   * polite live region. The first paint is never announced. @default true\n   */\n  announce?: boolean\n  /** Placeholder cards drawn while loading. Clamped to 1–12. @default 6 */\n  skeletonCount?: number\n  /** Replaces the default `status=\"empty\"` body. */\n  emptyState?: React.ReactNode\n  /** Message shown in the `status=\"error\"` branch. */\n  errorMessage?: string\n  /** Copy overrides; `usingTool` is a prefix, the tool name stays its own slot. */\n  labels?: Partial<AgentRosterLabels>\n  /** Override the capacity wording (a raw count, \"3/4 runs\", a token budget…). */\n  formatLoad?: (load: number) => string\n}\n\n/**\n * A multi-agent fleet panel: who is deployed, what each one is doing right now,\n * on which model, and how full it is — with the two agents that need a human\n * floated to the top and a keyboard path that actually works.\n */\nexport const AgentRoster = React.forwardRef<HTMLDivElement, AgentRosterProps>(\n  (\n    {\n      status,\n      agents,\n      variant = \"grid\",\n      sort = \"given\",\n      onSelect,\n      selectedId,\n      onRetry,\n      label = \"Agents\",\n      showSummary = true,\n      showLoad = true,\n      pulse = true,\n      announce = true,\n      skeletonCount = 6,\n      emptyState,\n      errorMessage = \"The control plane didn't respond.\",\n      labels,\n      formatLoad = defaultFormatLoad,\n      className,\n      ...props\n    },\n    ref,\n  ) => {\n    const text = React.useMemo(() => ({ ...DEFAULT_LABELS, ...labels }), [labels])\n    const interactive = typeof onSelect === \"function\"\n\n    const ordered = React.useMemo(() => {\n      if (sort !== \"attention\") return agents\n      // Array.prototype.sort is stable, so agents inside one bucket keep the\n      // order your data layer gave them — the list never shuffles on a re-render.\n      return [...agents].sort((a, b) => RANK[a.state] - RANK[b.state])\n    }, [agents, sort])\n\n    const summary = React.useMemo(() => {\n      const counts = new Map<AgentRosterState, number>()\n      let loadTotal = 0\n      let loadCount = 0\n      for (const agent of agents) {\n        counts.set(agent.state, (counts.get(agent.state) ?? 0) + 1)\n        // Offline agents are excluded: averaging in their empty capacity would\n        // report a fleet as half-idle when every running agent is saturated.\n        if (agent.state !== \"offline\") {\n          loadTotal += safeLoad(agent.load)\n          loadCount += 1\n        }\n      }\n      return { counts, teamLoad: loadCount === 0 ? null : loadTotal / loadCount }\n    }, [agents])\n\n    /* ------------------------------------------------------- announcements */\n\n    /**\n     * Identity of the whole fleet's state, as one string. Empty means \"nothing to\n     * watch\" — announcements off, or no roster on screen.\n     */\n    const trackKey = React.useMemo(\n      () => (announce && status === \"ready\" ? agents.map(agent => `${agent.id}~${agent.state}`).join(\"|\") : \"\"),\n      [agents, announce, status],\n    )\n\n    // The previous fleet snapshot lives in STATE and is compared during render —\n    // no effect, so the announcement is part of the first painted frame instead\n    // of arriving one commit late (screen readers do miss that gap).\n    const [seen, setSeen] = React.useState<{\n      key: string\n      message: string\n      states: ReadonlyMap<string, AgentRosterState>\n    }>(() => ({ key: \"\", message: \"\", states: new Map() }))\n\n    let announcement = seen.message\n    if (seen.key !== trackKey) {\n      const next = new Map<string, AgentRosterState>()\n      const entered: string[] = []\n      // An empty snapshot means \"first paint after mount, or after a non-ready\n      // status\": a roster that shouts about every failure the moment it appears\n      // is a roster people mute.\n      const first = seen.states.size === 0\n      if (trackKey !== \"\") {\n        for (const agent of agents) {\n          next.set(agent.id, agent.state)\n          if (!first && seen.states.get(agent.id) !== agent.state && ATTENTION.has(agent.state)) {\n            entered.push(`${agent.name}: ${stateLabel(agent.state, text)}`)\n          }\n        }\n      }\n      announcement = entered.join(\". \")\n      setSeen({ key: trackKey, message: announcement, states: next })\n    }\n\n    /* ---------------------------------------------- keyboard / roving focus */\n\n    const [activeIndex, setActiveIndex] = React.useState(0)\n    const optionRefs = React.useRef<(HTMLDivElement | null)[]>([])\n    const searchRef = React.useRef(\"\")\n    const searchTimer = React.useRef<number | null>(null)\n\n    React.useEffect(\n      () => () => {\n        if (searchTimer.current !== null) window.clearTimeout(searchTimer.current)\n      },\n      [],\n    )\n\n    // Clamped on read instead of reset in an effect: when the fleet shrinks\n    // between two polls, the roving index must never point past the end even for\n    // the one frame an effect would take to fix it.\n    const active = ordered.length === 0 ? 0 : Math.min(activeIndex, ordered.length - 1)\n\n    const focusOption = React.useCallback((index: number, count: number) => {\n      const clamped = Math.min(count - 1, Math.max(0, index))\n      setActiveIndex(clamped)\n      // The option already exists in the DOM, so focus can move synchronously\n      // inside the handler — no effect, and therefore no chance of stealing\n      // focus on an unrelated re-render.\n      optionRefs.current[clamped]?.focus()\n    }, [])\n\n    /**\n     * Columns are decided by the container (`auto-fill`), not by a breakpoint, so\n     * the only honest way to know how many there are is to measure: everything\n     * sharing the first card's `offsetTop` is the first row. Read at keypress\n     * time, when layout is already settled — no observer, nothing to clean up.\n     */\n    const columnStep = React.useCallback(() => {\n      if (variant === \"list\") return 1\n      const nodes = optionRefs.current.slice(0, ordered.length).filter((n): n is HTMLDivElement => n !== null)\n      const head = nodes[0]\n      if (!head) return 1\n      let columns = 1\n      for (let i = 1; i < nodes.length; i += 1) {\n        if (nodes[i].offsetTop !== head.offsetTop) break\n        columns += 1\n      }\n      return columns\n    }, [ordered.length, variant])\n\n    const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {\n      if (!onSelect) return\n      if (event.altKey || event.ctrlKey || event.metaKey) return\n      const count = ordered.length\n      if (count === 0) return\n\n      const move = (index: number) => {\n        event.preventDefault()\n        focusOption(index, count)\n      }\n\n      switch (event.key) {\n        case \"ArrowDown\":\n          return move(active + columnStep())\n        case \"ArrowUp\":\n          return move(active - columnStep())\n        case \"ArrowRight\":\n          return move(active + 1)\n        case \"ArrowLeft\":\n          return move(active - 1)\n        case \"Home\":\n          return move(0)\n        case \"End\":\n          return move(count - 1)\n        case \"Enter\":\n        case \" \": {\n          event.preventDefault()\n          const agent = ordered[active]\n          if (agent) onSelect(agent.id)\n          return\n        }\n        default:\n          break\n      }\n\n      // Type-ahead: a fleet of forty agents is unusable if the only way to reach\n      // \"Refund Resolver\" is forty arrow presses.\n      if (event.key.length !== 1 || event.key === \" \") return\n      const buffer = searchRef.current + event.key.toLowerCase()\n      searchRef.current = buffer\n      if (searchTimer.current !== null) window.clearTimeout(searchTimer.current)\n      searchTimer.current = window.setTimeout(() => {\n        searchRef.current = \"\"\n        searchTimer.current = null\n      }, TYPEAHEAD_RESET_MS)\n\n      // A repeated single character walks through the matches; a longer buffer\n      // re-matches from the current position so \"re\" doesn't skip \"Refund\".\n      const from = buffer.length === 1 ? active + 1 : active\n      for (let i = 0; i < count; i += 1) {\n        const index = (from + i) % count\n        const agent = ordered[index]\n        if (agent && agent.name.toLowerCase().startsWith(buffer)) {\n          move(index)\n          return\n        }\n      }\n    }\n\n    /* ------------------------------------------------------------ envelopes */\n\n    const rootClass = cn(\"flex w-full min-w-0 flex-col gap-3 text-sm\", className)\n    const containerClass = variant === \"list\" ? LIST_CLASS : GRID_CLASS\n\n    // One region for the whole roster, permanently mounted: several screen\n    // readers skip a live region that is inserted in the same frame as its text,\n    // and one region per card would talk over itself.\n    const liveRegion = (\n      <p aria-atomic=\"true\" aria-live=\"polite\" className=\"sr-only\" role=\"status\">\n        {status === \"loading\" ? `Loading ${label}` : announcement}\n      </p>\n    )\n\n    if (status === \"loading\") {\n      const cards = Math.min(12, Math.max(1, Math.floor(Number.isFinite(skeletonCount) ? skeletonCount : 6)))\n      return (\n        <div aria-busy=\"true\" className={rootClass} data-status=\"loading\" ref={ref} {...props}>\n          {liveRegion}\n          <div aria-hidden=\"true\" className={containerClass}>\n            {Array.from({ length: cards }, (_, i) => (\n              <div\n                className={cn(\n                  \"flex min-w-0 rounded-xl border bg-card p-3\",\n                  variant === \"list\" ? \"items-center gap-3\" : \"flex-col gap-3\",\n                )}\n                key={i}\n              >\n                <div className={cn(\"shrink-0 rounded-full\", pulseClass, variant === \"list\" ? \"size-8\" : \"size-10\")} />\n                <div className=\"flex min-w-0 flex-1 flex-col gap-2\">\n                  <div className={cn(\"h-3 w-24 max-w-full\", pulseClass)} />\n                  <div className={cn(\"h-2.5 w-16 max-w-full\", pulseClass)} />\n                </div>\n                <div className={cn(\"h-1.5 w-full rounded-full\", pulseClass, variant === \"list\" && \"w-24 shrink-0\")} />\n              </div>\n            ))}\n          </div>\n        </div>\n      )\n    }\n\n    if (status === \"error\") {\n      return (\n        <div className={rootClass} data-status=\"error\" ref={ref} {...props}>\n          {liveRegion}\n          <div className=\"flex flex-col items-start gap-2 rounded-xl border bg-card p-6\" role=\"alert\">\n            <p className=\"flex items-center gap-2 font-medium\">\n              <AlertCircle aria-hidden=\"true\" className=\"size-4 shrink-0 text-destructive\" />\n              Couldn&apos;t load {label.toLowerCase()}\n            </p>\n            <p className=\"min-w-0 whitespace-pre-wrap wrap-anywhere text-muted-foreground\">{errorMessage}</p>\n            {onRetry && (\n              <button\n                className=\"inline-flex cursor-pointer items-center gap-1.5 rounded-md border px-2.5 py-1 text-xs transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring motion-reduce:transition-none\"\n                onClick={onRetry}\n                type=\"button\"\n              >\n                <RefreshCcw aria-hidden=\"true\" className=\"size-3.5\" />\n                Try again\n              </button>\n            )}\n          </div>\n        </div>\n      )\n    }\n\n    if (status === \"empty\" || ordered.length === 0) {\n      return (\n        <div className={rootClass} data-status=\"empty\" ref={ref} {...props}>\n          {liveRegion}\n          {emptyState ?? (\n            <div className=\"flex flex-col items-center gap-2 rounded-xl border bg-card px-6 py-12 text-center\">\n              <Bot aria-hidden=\"true\" className=\"size-6 text-muted-foreground\" />\n              <p className=\"font-medium\">No agents deployed</p>\n              <p className=\"text-muted-foreground\">Agents appear here as soon as one is running in this workspace.</p>\n            </div>\n          )}\n        </div>\n      )\n    }\n\n    /* ---------------------------------------------------------------- ready */\n\n    const cardClass = cn(\n      \"min-w-0 rounded-xl border bg-card text-card-foreground\",\n      variant === \"list\" ? \"px-3 py-2.5\" : \"flex h-full flex-col gap-2.5 p-3\",\n    )\n    const interactiveClass = interactive\n      ? \"cursor-pointer transition-colors hover:border-primary/40 hover:bg-muted/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring motion-reduce:transition-none\"\n      : \"\"\n\n    const body = (agent: AgentRosterAgent) => (\n      <AgentBody\n        agent={agent}\n        formatLoad={formatLoad}\n        pulse={pulse}\n        showLoad={showLoad}\n        text={text}\n        variant={variant}\n      />\n    )\n\n    return (\n      <div className={rootClass} data-status=\"ready\" ref={ref} {...props}>\n        {liveRegion}\n\n        {showSummary && (\n          <div className=\"flex min-w-0 flex-wrap items-center gap-x-3 gap-y-1.5\">\n            <p className=\"font-medium\">{label}</p>\n            <p className=\"text-xs text-muted-foreground tabular-nums\">\n              {agents.length} {text.agents}\n            </p>\n            <span aria-hidden=\"true\" className=\"h-3 w-px bg-border\" />\n            {SUMMARY_ORDER.map(state => {\n              const count = summary.counts.get(state) ?? 0\n              if (count === 0) return null\n              return (\n                <span\n                  className=\"inline-flex items-center gap-1.5 text-xs text-muted-foreground\"\n                  key={state}\n                  style={{ \"--agent-tone\": TONE[state] } as React.CSSProperties}\n                >\n                  <StateDot pulse={false} state={state} />\n                  <span className=\"tabular-nums\">{count}</span>\n                  <span>{stateLabel(state, text).toLowerCase()}</span>\n                </span>\n              )\n            })}\n            {summary.teamLoad !== null && (\n              <p className=\"ml-auto text-xs text-muted-foreground tabular-nums\">\n                Team {text.load.toLowerCase()} {formatLoad(summary.teamLoad)}\n              </p>\n            )}\n          </div>\n        )}\n\n        {interactive ? (\n          <div\n            aria-label={label}\n            className={containerClass}\n            onKeyDown={handleKeyDown}\n            role=\"listbox\"\n          >\n            {ordered.map((agent, index) => (\n              <div\n                aria-selected={agent.id === selectedId}\n                className={cn(\n                  cardClass,\n                  interactiveClass,\n                  agent.state === \"offline\" && \"bg-muted/30\",\n                  agent.id === selectedId && \"border-primary bg-primary/5 hover:bg-primary/5\",\n                )}\n                data-agent-state={agent.state}\n                key={agent.id}\n                onClick={() => {\n                  // Click also moves the roving index, so Tabbing back into the\n                  // roster returns to the card the pointer last used.\n                  setActiveIndex(index)\n                  onSelect?.(agent.id)\n                }}\n                ref={node => {\n                  optionRefs.current[index] = node\n                }}\n                role=\"option\"\n                style={{ \"--agent-tone\": TONE[agent.state] } as React.CSSProperties}\n                // Roving tabindex: the roster is ONE tab stop, and the arrow keys\n                // move inside it. Twelve agents must not cost twelve Tabs.\n                tabIndex={index === active ? 0 : -1}\n              >\n                {body(agent)}\n              </div>\n            ))}\n          </div>\n        ) : (\n          <ul className={containerClass} role=\"list\">\n            {ordered.map(agent => (\n              <li\n                className={cn(cardClass, agent.state === \"offline\" && \"bg-muted/30\")}\n                data-agent-state={agent.state}\n                key={agent.id}\n                style={{ \"--agent-tone\": TONE[agent.state] } as React.CSSProperties}\n              >\n                {body(agent)}\n              </li>\n            ))}\n          </ul>\n        )}\n      </div>\n    )\n  },\n)\n\nAgentRoster.displayName = \"AgentRoster\"\n\nexport default AgentRoster\n",
      "type": "registry:ui"
    },
    {
      "path": "src/registry/ui/agent-roster.contract.ts",
      "content": "import { z } from \"zod\"\n\n/**\n * What ONE agent in a fleet is doing right now, from the operator's point of\n * view. Six values, and every one of them changes what the roster does:\n *\n * - `thinking` / `using-tool` are the two \"burning tokens\" states — the only ones\n *   that animate, and the only ones counted as *working* in the header.\n * - `waiting` and `error` are the two ATTENTION states: they sort to the top, they\n *   get a ringed dot instead of a pulse, and entering them is the only thing the\n *   roster ever announces out loud.\n * - `idle` is deployed-but-free, `offline` is paused / undeployed — the difference\n *   matters because an offline agent is excluded from the team load average, and\n *   counting its empty capacity would flatter the fleet's real headroom.\n *\n * Named `state`, not `status`, so it never collides with the ENVELOPE status\n * below: \"the roster failed to load\" and \"this agent's last run failed\" are two\n * different failures and both have to be expressible at the same time.\n */\nexport const AGENT_ROSTER_STATES = [\"thinking\", \"using-tool\", \"waiting\", \"error\", \"idle\", \"offline\"] as const\nexport const agentRosterStateSchema = z.enum(AGENT_ROSTER_STATES)\nexport type AgentRosterState = z.infer<typeof agentRosterStateSchema>\n\nexport const agentRosterAgentSchema = z.object({\n  /** Stable identity. The React key, the selection value, and what `onSelect` hands back. */\n  id: z.string(),\n  /** Display name — \"Refund Resolver\", \"研究助手\". Also what type-ahead matches on. */\n  name: z.string(),\n  /** What this agent is FOR, one short line: \"Billing · tier-1 refunds\". */\n  role: z.string(),\n  /** Model identifier as your control plane spells it. Rendered as a monospace chip. */\n  model: z.string(),\n  state: agentRosterStateSchema,\n  /** Tool being called; only meaningful while `state: \"using-tool\"`, shown beside the label. */\n  tool: z.string().optional(),\n  /**\n   * The one line that says what it is doing *this second* — a ticket title, a\n   * query, the failure message when `state: \"error\"`. Pre-formatted by you; the\n   * roster clamps it to two lines and keeps the full text in `title`.\n   */\n  currentTask: z.string().optional(),\n  /**\n   * Share of this agent's own concurrency budget in use, as a fraction: `0.62`\n   * = 62 % full. Deliberately NOT a 0–100 integer, so a ratio you already compute\n   * as `running / limit` drops straight in.\n   *\n   * Values above 1 are legal and meaningful: an over-subscribed worker renders a\n   * full destructive-tone bar that still states the true figure (\"118 %\"), rather\n   * than silently clamping to a healthy-looking 100 %.\n   */\n  load: z.number(),\n  /** Tasks parked behind this agent. Rendered next to the load figure when above 0. */\n  queued: z.number().int().nonnegative().optional(),\n  /** Any URL. A broken one falls back to initials — the card never shows a broken image. */\n  avatarUrl: z.string().optional(),\n  /** Overrides the derived initials, for names where auto-derivation reads badly. */\n  initials: z.string().optional(),\n})\nexport type AgentRosterAgent = z.infer<typeof agentRosterAgentSchema>\n\n/**\n * The ROSTER's own render state — \"is there a fleet listing to show at all\".\n * Independent of any agent's `state`: `status: \"error\"` means the roster query\n * failed, while an agent whose last run failed is `state: \"error\"` inside a\n * perfectly healthy `status: \"ready\"` roster.\n */\nexport const agentRosterStatusSchema = z.enum([\"loading\", \"empty\", \"error\", \"ready\"])\nexport type AgentRosterStatus = z.infer<typeof agentRosterStatusSchema>\n\n/** The envelope a data layer / mock factory hands over; the demo spreads it into the props. */\nexport const agentRosterSchema = z.object({\n  status: agentRosterStatusSchema,\n  agents: z.array(agentRosterAgentSchema),\n})\nexport type AgentRosterData = z.infer<typeof agentRosterSchema>\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}