{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "agent-10",
  "title": "Agent status",
  "description": "A mark and a word per agent that change together: the dot of a running agent sends a ring out, and a hand, a cross or a check takes its place when the agent waits, fails or ends, above a legend of all five states.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://ui.ndekocode.dev/r/agent-run.json",
    "https://ui.ndekocode.dev/r/chat-motion.json",
    "https://ui.ndekocode.dev/r/demonstration.json",
    "https://ui.ndekocode.dev/r/icons.json",
    "https://ui.ndekocode.dev/r/live-duration.json",
    "https://ui.ndekocode.dev/r/live-region.json",
    "https://ui.ndekocode.dev/r/mesh-tile.json",
    "https://ui.ndekocode.dev/r/motion-number.json",
    "https://ui.ndekocode.dev/r/motion-word-roll.json",
    "https://ui.ndekocode.dev/r/replay-button.json",
    "https://ui.ndekocode.dev/r/run-session.json",
    "https://ui.ndekocode.dev/r/theme.json",
    "https://ui.ndekocode.dev/r/use-kept-height.json",
    "https://ui.ndekocode.dev/r/use-prefers-reduced-motion.json",
    "https://ui.ndekocode.dev/r/use-session.json",
    "https://ui.ndekocode.dev/r/utils.json"
  ],
  "files": [
    {
      "path": "registry/ndeko/components/agent/agent-10.tsx",
      "content": "'use client';\n\nimport { AnimatePresence, motion } from 'motion/react';\n\nimport { MotionNumber } from '@/components/core/motion-number';\nimport { WordRoll, type WordRollWord } from '@/components/core/word-roll';\nimport { useKeptHeight } from '@/registry/ndeko/hooks/use-kept-height';\nimport { usePrefersReducedMotion } from '@/registry/ndeko/hooks/use-prefers-reduced-motion';\nimport { useSession } from '@/registry/ndeko/hooks/use-session';\nimport {\n  emptyRun,\n  type AgentStatus as AgentState,\n  type RunAgent,\n} from '@/registry/ndeko/lib/agent-run';\nimport { spokenUnless } from '@/registry/ndeko/lib/demonstration';\nimport {\n  applyBatchAt,\n  batch,\n  reduceBatch,\n  restingOf,\n  type RunStep,\n} from '@/registry/ndeko/lib/run-session';\nimport { cn } from '@/registry/ndeko/lib/utils';\nimport { ClockGate } from '@/registry/ndeko/ui/live-duration';\nimport { LiveRegion } from '@/registry/ndeko/ui/live-region';\nimport { ReplayButton } from '@/registry/ndeko/ui/replay-button';\n\nimport {\n  AGENT_STATES,\n  AGENT_STATUS_WORD,\n  AgentStatus,\n  pulsingIdsOf,\n} from './_parts/agent-status';\nimport { AgentTile } from './_parts/agent-tile';\nimport { CODER, PLANNER, RESEARCHER, REVIEWER, WRITER } from './_parts/crew';\nimport { useAgentAnnouncement } from './_parts/use-agent-announcement';\n\nexport {\n  AGENT_STATUS_WORD,\n  AgentStatus,\n  AgentStatusDot,\n  AgentStatusMark,\n  type AgentStatusProps,\n} from './_parts/agent-status';\n\nconst TASK_SWAP = {\n  initial: { opacity: 0, y: 4, filter: 'blur(2px)' },\n  animate: { opacity: 1, y: 0, filter: 'blur(0px)' },\n  exit: { opacity: 0, y: -4, filter: 'blur(2px)' },\n  transition: { duration: 0.2, ease: [0.23, 1, 0.32, 1] },\n} as const;\n\nconst TASK_FADE = {\n  initial: { opacity: 0 },\n  animate: { opacity: 1 },\n  exit: { opacity: 0 },\n  transition: { duration: 0.15 },\n} as const;\n\nfunction taskMotionOf(shouldReduceMotion: boolean | null) {\n  if (shouldReduceMotion) {\n    return TASK_FADE;\n  }\n  return TASK_SWAP;\n}\n\nfunction TaskLine({ task }: { task: string }) {\n  const shouldReduceMotion = usePrefersReducedMotion();\n  const taskMotion = taskMotionOf(shouldReduceMotion);\n\n  return (\n    <span className='text-muted-foreground relative block min-w-0 text-xs leading-5'>\n      <AnimatePresence initial={false} mode='popLayout'>\n        <motion.span key={task} className='block truncate' {...taskMotion}>\n          {task}\n        </motion.span>\n      </AnimatePresence>\n    </span>\n  );\n}\n\ntype SummaryPart = {\n  key: string;\n  count: number;\n  words: string[];\n};\n\nconst SUMMARY_PARTS: { status: AgentState; words: string[] }[] = [\n  { status: 'running', words: ['running'] },\n  { status: 'waiting', words: ['waiting', 'on', 'you'] },\n  { status: 'failed', words: ['failed'] },\n];\n\nfunction countWithStatus(agents: RunAgent[], status: AgentState) {\n  const matching = agents.filter((agent) => agent.status === status);\n  return matching.length;\n}\n\nfunction summaryPartsOf(agents: RunAgent[]) {\n  const parts: SummaryPart[] = [];\n  for (const part of SUMMARY_PARTS) {\n    const count = countWithStatus(agents, part.status);\n    if (count > 0) {\n      parts.push({ key: part.status, count, words: part.words });\n    }\n  }\n  return parts;\n}\n\nfunction withTrailingComma(words: string[]) {\n  const last = words.length - 1;\n  return words.map((word, index) => {\n    if (index === last) {\n      return `${word},`;\n    }\n    return word;\n  });\n}\n\nfunction settledWordsOf(agents: RunAgent[]): WordRollWord[] {\n  const doneCount = countWithStatus(agents, 'done');\n  const isAllDone = doneCount === agents.length;\n  if (isAllDone) {\n    return ['All', 'done'];\n  }\n  return ['All', 'idle'];\n}\n\nfunction summaryWordsOf(agents: RunAgent[]): WordRollWord[] {\n  const parts = summaryPartsOf(agents);\n  if (parts.length === 0) {\n    return settledWordsOf(agents);\n  }\n  const words: WordRollWord[] = [];\n  const lastIndex = parts.length - 1;\n  parts.forEach((part, index) => {\n    const countWord = {\n      key: `${part.key}-count`,\n      text: String(part.count),\n      node: <MotionNumber value={part.count} />,\n    };\n    words.push(countWord);\n    const isLast = index === lastIndex;\n    if (isLast) {\n      words.push(...part.words);\n      return;\n    }\n    const wordsWithComma = withTrailingComma(part.words);\n    words.push(...wordsWithComma);\n  });\n  return words;\n}\n\nexport type AgentRosterProps = {\n  title: string;\n  agents: RunAgent[];\n  className?: string;\n};\n\nexport function AgentRoster({ title, agents, className }: AgentRosterProps) {\n  const summaryWords = summaryWordsOf(agents);\n  const pulsingIds = pulsingIdsOf(agents);\n\n  return (\n    <section\n      aria-label={title}\n      className={cn(\n        'bg-card hairline-ring ring-border flex flex-col gap-2 rounded-xl p-2 shadow-[0_1px_2px_rgb(0_0_0/0.04)]',\n        className\n      )}\n    >\n      <header className='flex items-baseline justify-between gap-3 px-2 pt-1.5'>\n        <h3 className='text-sm font-medium'>{title}</h3>\n        <span className='text-muted-foreground text-xs'>\n          <WordRoll words={summaryWords} />\n        </span>\n      </header>\n      <ul aria-label='Agents' className='flex flex-col'>\n        {agents.map((agent) => (\n          <li\n            key={agent.id}\n            className='flex min-h-12 items-center gap-3 rounded-lg px-2 py-1.5'\n          >\n            <AgentTile name={agent.name} series={agent.series} />\n            <div className='min-w-0 flex-1'>\n              <p className='text-[13px] leading-5 font-medium'>{agent.name}</p>\n              <TaskLine task={agent.task ?? ''} />\n            </div>\n            <AgentStatus\n              status={agent.status}\n              reserveWidth\n              isStill={!pulsingIds.has(agent.id)}\n              className='shrink-0'\n            />\n          </li>\n        ))}\n      </ul>\n    </section>\n  );\n}\n\n/** A legend names the states, it runs nothing: its running mark holds still. */\nexport function AgentStatusLegend({ className }: { className?: string }) {\n  return (\n    <ul\n      aria-label='What each state means'\n      className={cn('flex flex-wrap gap-x-4 gap-y-2', className)}\n    >\n      {AGENT_STATES.map((state) => (\n        <li key={state}>\n          <AgentStatus status={state} isStill />\n        </li>\n      ))}\n    </ul>\n  );\n}\n\nfunction update(\n  after: number,\n  id: string,\n  status: AgentState,\n  task: string\n): RunStep {\n  return batch(after, { type: 'agent.updated', id, patch: { status, task } });\n}\n\nconst ROSTER: RunAgent[] = [\n  { ...PLANNER, status: 'running', task: 'Splitting the review into tasks' },\n  { ...RESEARCHER, task: 'Waits for a brief' },\n  { ...CODER, task: 'Waits for the price lists' },\n  { ...WRITER, task: 'Waits for the findings' },\n  { ...REVIEWER, task: 'Waits for a draft' },\n];\n\nconst INITIAL_RUN = emptyRun({ id: 'crew-status', agents: ROSTER });\n\n/**\n * The round stops while the review is still under way, with one agent in each of the five states:\n * that is the picture the crew rests on, under reduced motion too.\n */\nconst SCRIPT: RunStep[] = [\n  update(1400, 'researcher', 'running', 'Collecting vendor price lists'),\n  update(1400, 'planner', 'waiting', 'Asks to raise the budget to $40'),\n  update(2600, 'planner', 'running', 'Budget raised, tracking progress'),\n  update(1200, 'researcher', 'done', 'Found 14 vendors with prices'),\n  update(200, 'coder', 'running', 'Comparing invoices with list prices'),\n  update(1600, 'writer', 'running', 'Drafting the credit request'),\n  update(1600, 'coder', 'failed', 'The invoice sheet lacks a SKU column'),\n  update(1400, 'planner', 'waiting', 'Asks if items may be matched by name'),\n];\n\nconst SESSION = {\n  reducer: reduceBatch,\n  initial: INITIAL_RUN,\n  resting: restingOf(INITIAL_RUN, SCRIPT, applyBatchAt),\n  script: SCRIPT,\n  loopAfter: 3200,\n};\n\nfunction announcementOf(agent: RunAgent) {\n  const word = AGENT_STATUS_WORD[agent.status];\n  return `${agent.name}: ${word}. ${agent.task ?? ''}`;\n}\n\nexport default function Agent10() {\n  const { ref, isInView, isAutoplaying, state, isFinished, restart } =\n    useSession(SESSION);\n  const agents = state.agents;\n  const message = useAgentAnnouncement(agents, announcementOf);\n  const canReplay = isFinished && !isAutoplaying;\n\n  useKeptHeight(ref);\n\n  return (\n    <div\n      ref={ref}\n      className='flex min-h-[23.5rem] w-full max-w-[26rem] flex-col gap-3'\n    >\n      <ClockGate isActive={isInView}>\n        <AgentRoster title='Brightmoor review crew' agents={agents} />\n      </ClockGate>\n      <AgentStatusLegend className='px-2' />\n      <div className='flex h-7 items-center justify-end'>\n        <ReplayButton\n          isShown={canReplay}\n          onReplay={restart}\n          label='Replay the crew'\n        />\n      </div>\n      <LiveRegion message={spokenUnless(isAutoplaying, message)} />\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/agent/agent-10.tsx"
    },
    {
      "path": "registry/ndeko/components/agent/_parts/agent-status.tsx",
      "content": "'use client';\n\nimport { WordRoll } from '@/components/core/word-roll';\nimport type {\n  AgentStatus as AgentState,\n  RunAgent,\n} from '@/registry/ndeko/lib/agent-run';\nimport {\n  CircleCheckIcon,\n  CircleXIcon,\n  HandIcon,\n} from '@/registry/ndeko/lib/icons';\nimport { cn } from '@/registry/ndeko/lib/utils';\nimport { Swap } from '@/registry/ndeko/ui/chat-motion';\n\nexport const AGENT_STATUS_WORD: Record<AgentState, string> = {\n  idle: 'Idle',\n  running: 'Running',\n  waiting: 'Waiting on you',\n  failed: 'Failed',\n  done: 'Done',\n};\n\nexport const AGENT_STATES: AgentState[] = [\n  'idle',\n  'running',\n  'waiting',\n  'failed',\n  'done',\n];\n\nconst STATUS_WORDS = AGENT_STATES.map((state) => AGENT_STATUS_WORD[state]);\n\nconst DOT_TONE: Record<AgentState, string> = {\n  idle: 'bg-muted-foreground/45',\n  running: 'bg-info',\n  waiting: 'bg-warning',\n  failed: 'bg-destructive',\n  done: 'bg-success',\n};\n\nconst LABEL_TONE: Record<AgentState, string> = {\n  idle: 'text-muted-foreground',\n  running: 'text-foreground',\n  waiting: 'text-warning-foreground',\n  failed: 'text-destructive-ink',\n  done: 'text-muted-foreground',\n};\n\nconst DOT_SIZE = {\n  sm: 'size-1.5',\n  md: 'size-2',\n  lg: 'size-2.5',\n} as const;\n\nconst MARK_SIZE = {\n  sm: 'size-3',\n  md: 'size-3.5',\n  lg: 'size-4',\n} as const;\n\nexport type AgentStatusSize = keyof typeof DOT_SIZE;\n\nconst RING_TIMING: React.CSSProperties = { animationDuration: '2s' };\n\n/** A piece lets this many dots pulse at once. Past it, a running agent is told by its colour and its word. */\nexport const MAX_PULSING = 3;\n\n/** The running agents whose dot may pulse: the first ones, in the order given. */\nexport function pulsingIdsOf(\n  agents: Pick<RunAgent, 'id' | 'status'>[],\n  limit = MAX_PULSING\n) {\n  const running = agents.filter((agent) => agent.status === 'running');\n  const pulsing = running.slice(0, limit);\n  const ids = pulsing.map((agent) => agent.id);\n  return new Set(ids);\n}\n\nfunction reservedLabelsOf(shouldReserve: boolean) {\n  if (shouldReserve) {\n    return STATUS_WORDS;\n  }\n  return undefined;\n}\n\ntype AgentStatusDotProps = {\n  status: AgentState;\n  size?: AgentStatusSize;\n  /** Without a visible word next to it, the dot says its state to screen readers. */\n  isLabelled?: boolean;\n  /** Holds a running dot still: where three others already pulse, or where nothing is really running. */\n  isStill?: boolean;\n  className?: string;\n};\n\n/**\n * A running dot sends a soft ring out: one CSS keyframe on transform and opacity, which the theme\n * stops under reduced motion. Every other state holds still and is told by its colour.\n */\nexport function AgentStatusDot({\n  status,\n  size = 'md',\n  isLabelled = false,\n  isStill = false,\n  className,\n}: AgentStatusDotProps) {\n  const isRunning = status === 'running';\n  const isPulsing = isRunning && !isStill;\n  const word = AGENT_STATUS_WORD[status];\n\n  return (\n    <span\n      data-status={status}\n      className={cn('relative inline-flex shrink-0', DOT_SIZE[size], className)}\n    >\n      {isPulsing && (\n        <span\n          aria-hidden='true'\n          style={RING_TIMING}\n          className='bg-info/40 absolute inset-0 animate-ping rounded-full motion-reduce:hidden'\n        />\n      )}\n      <span\n        aria-hidden='true'\n        className={cn(\n          'relative size-full rounded-full transition-colors duration-200',\n          DOT_TONE[status]\n        )}\n      />\n      {isLabelled && <span className='sr-only'>{word}</span>}\n    </span>\n  );\n}\n\ntype StatusGlyphProps = {\n  status: AgentState;\n  size: AgentStatusSize;\n  isStill: boolean;\n};\n\nfunction StatusGlyph({ status, size, isStill }: StatusGlyphProps) {\n  const box = MARK_SIZE[size];\n  if (status === 'waiting') {\n    return <HandIcon className={cn('text-warning-foreground', box)} />;\n  }\n  if (status === 'failed') {\n    return <CircleXIcon className={cn('text-destructive-ink', box)} />;\n  }\n  if (status === 'done') {\n    return <CircleCheckIcon className={cn('text-success-foreground', box)} />;\n  }\n  return (\n    <span className={cn('flex items-center justify-center', box)}>\n      <AgentStatusDot status={status} size={size} isStill={isStill} />\n    </span>\n  );\n}\n\ntype AgentStatusMarkProps = {\n  status: AgentState;\n  size?: AgentStatusSize;\n  isStill?: boolean;\n  className?: string;\n};\n\n/**\n * The mark of a state, drawn so it reads without its colour: a dot while the agent is idle or runs,\n * a hand while it waits, a cross when it failed, a check when it ended. It swaps when the state changes.\n */\nexport function AgentStatusMark({\n  status,\n  size = 'md',\n  isStill = false,\n  className,\n}: AgentStatusMarkProps) {\n  const box = MARK_SIZE[size];\n\n  return (\n    <span\n      data-status={status}\n      aria-hidden='true'\n      className={cn('inline-flex shrink-0', box, className)}\n    >\n      <Swap swapKey={status} className={box}>\n        <StatusGlyph status={status} size={size} isStill={isStill} />\n      </Swap>\n    </span>\n  );\n}\n\nexport type AgentStatusProps = {\n  status: AgentState;\n  /** Leave the word out where space is short: a dot then carries it for screen readers. */\n  showLabel?: boolean;\n  /** Keeps the width of the longest word, so the row beside it never shifts. */\n  reserveWidth?: boolean;\n  size?: AgentStatusSize;\n  /** Holds a running mark still: a piece lets three pulse at most. */\n  isStill?: boolean;\n  className?: string;\n};\n\nexport function AgentStatus({\n  status,\n  showLabel = true,\n  reserveWidth = false,\n  size = 'md',\n  isStill = false,\n  className,\n}: AgentStatusProps) {\n  const word = AGENT_STATUS_WORD[status];\n  const labels = reservedLabelsOf(reserveWidth);\n\n  if (!showLabel) {\n    return (\n      <AgentStatusDot\n        status={status}\n        size={size}\n        isLabelled\n        isStill={isStill}\n        className={className}\n      />\n    );\n  }\n\n  return (\n    <span\n      className={cn(\n        'inline-flex items-center gap-1 text-xs transition-colors duration-200',\n        LABEL_TONE[status],\n        className\n      )}\n    >\n      <AgentStatusMark status={status} size={size} isStill={isStill} />\n      <WordRoll text={word} labels={labels} />\n    </span>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/agent/_parts/agent-status.tsx"
    },
    {
      "path": "registry/ndeko/components/agent/_parts/agent-tile.tsx",
      "content": "import { seriesColorOf, type SeriesSlot } from '@/registry/ndeko/lib/agent-run';\nimport { cn } from '@/registry/ndeko/lib/utils';\nimport { MeshTile } from '@/registry/ndeko/ui/mesh-tile';\n\nconst SIZE_CLASS = {\n  sm: 'size-5 rounded-[5px]',\n  md: 'size-6 rounded-md',\n  lg: 'size-8 rounded-lg',\n} as const;\n\nexport type AgentTileSize = keyof typeof SIZE_CLASS;\n\ntype AgentTileProps = {\n  name: string;\n  series: SeriesSlot;\n  size?: AgentTileSize;\n  className?: string;\n};\n\n/** The picture of an agent: a gradient drawn from its name, in its series colour. Its name always sits beside it. */\nexport function AgentTile({\n  name,\n  series,\n  size = 'md',\n  className,\n}: AgentTileProps) {\n  const color = seriesColorOf(series);\n  const sizeClass = SIZE_CLASS[size];\n\n  return (\n    <MeshTile seed={name} color={color} className={cn(sizeClass, className)} />\n  );\n}\n",
      "type": "registry:component",
      "target": "components/agent/_parts/agent-tile.tsx"
    },
    {
      "path": "registry/ndeko/components/agent/_parts/crew.ts",
      "content": "import type { RunAgent } from '@/registry/ndeko/lib/agent-run';\n\nexport const RUN_TITLE = 'Q3 vendor review for Brightmoor Supply';\n\nexport const PLANNER: RunAgent = {\n  id: 'planner',\n  name: 'Planner',\n  series: 1,\n  status: 'idle',\n  role: 'Lead',\n  model: 'Claude Opus',\n};\n\nexport const RESEARCHER: RunAgent = {\n  id: 'researcher',\n  name: 'Researcher',\n  series: 2,\n  status: 'idle',\n  model: 'Claude Sonnet',\n  parentId: 'planner',\n};\n\nexport const WRITER: RunAgent = {\n  id: 'writer',\n  name: 'Writer',\n  series: 3,\n  status: 'idle',\n  model: 'Claude Haiku',\n  parentId: 'planner',\n};\n\nexport const REVIEWER: RunAgent = {\n  id: 'reviewer',\n  name: 'Reviewer',\n  series: 4,\n  status: 'idle',\n  model: 'Claude Sonnet',\n  parentId: 'planner',\n};\n\nexport const CODER: RunAgent = {\n  id: 'coder',\n  name: 'Coder',\n  series: 5,\n  status: 'idle',\n  model: 'Claude Opus',\n  parentId: 'planner',\n};\n\nexport const CREW: RunAgent[] = [PLANNER, RESEARCHER, WRITER, REVIEWER, CODER];\n\nexport const PAGE_ERROR = 'The pricing page refused the reader (403)';\n",
      "type": "registry:component",
      "target": "components/agent/_parts/crew.ts"
    },
    {
      "path": "registry/ndeko/components/agent/_parts/use-agent-announcement.ts",
      "content": "'use client';\n\nimport { useState } from 'react';\n\nimport type { RunAgent } from '@/registry/ndeko/lib/agent-run';\n\nfunction changedAgentOf(previous: RunAgent[], next: RunAgent[]) {\n  return next.find((agent) => {\n    const before = previous.find((candidate) => candidate.id === agent.id);\n    if (!before) {\n      return true;\n    }\n    return before.status !== agent.status;\n  });\n}\n\nexport function useAgentAnnouncement(\n  agents: RunAgent[],\n  describe: (agent: RunAgent) => string\n) {\n  const [seenAgents, setSeenAgents] = useState(agents);\n  const [message, setMessage] = useState('');\n\n  const hasNewAgents = seenAgents !== agents;\n  if (hasNewAgents) {\n    const changed = changedAgentOf(seenAgents, agents);\n    setSeenAgents(agents);\n    if (changed) {\n      setMessage(describe(changed));\n    }\n  }\n\n  return message;\n}\n",
      "type": "registry:component",
      "target": "components/agent/_parts/use-agent-announcement.ts"
    }
  ],
  "categories": [
    "components",
    "agent"
  ],
  "type": "registry:component"
}