{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "agent-03",
  "title": "Agent graph",
  "description": "Agents and the tools they call as a graph where an edge draws itself when one agent calls another and a dot travels along it for each call, in a tree, flow or hub layout.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://ui.ndekocode.dev/r/agent-run.json",
    "https://ui.ndekocode.dev/r/call-status.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-presets.json",
    "https://ui.ndekocode.dev/r/motion-word-roll.json",
    "https://ui.ndekocode.dev/r/popover.json",
    "https://ui.ndekocode.dev/r/replay-button.json",
    "https://ui.ndekocode.dev/r/run-session.json",
    "https://ui.ndekocode.dev/r/segments.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",
    "https://ui.ndekocode.dev/r/words.json"
  ],
  "files": [
    {
      "path": "registry/ndeko/components/agent/agent-03.tsx",
      "content": "'use client';\n\nimport { AnimatePresence, motion } from 'motion/react';\nimport { useLayoutEffect, useState } from 'react';\n\nimport { MotionNumber } from '@/components/core/motion-number';\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  seriesColorOf,\n  type AgentStatus as AgentState,\n  type CallStatus,\n  type RunAgent,\n  type RunEvent,\n  type RunSnapshot,\n  type SeriesSlot,\n  type ToolCall,\n} from '@/registry/ndeko/lib/agent-run';\nimport { spokenUnless } from '@/registry/ndeko/lib/demonstration';\nimport { slowSpringOf } from '@/registry/ndeko/lib/motion-presets';\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 { nounOf } from '@/registry/ndeko/lib/words';\nimport {\n  CALL_STATUS_WORD,\n  CallStatusMark,\n} from '@/registry/ndeko/ui/call-status';\nimport { ClockGate, LiveDuration } from '@/registry/ndeko/ui/live-duration';\nimport { LiveRegion } from '@/registry/ndeko/ui/live-region';\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTitle,\n  PopoverTrigger,\n} from '@/registry/ndeko/ui/popover';\nimport { ReplayButton } from '@/registry/ndeko/ui/replay-button';\nimport { Segments, type Segment } from '@/registry/ndeko/ui/segments';\n\nimport {\n  AGENT_STATUS_WORD,\n  AgentStatus,\n  pulsingIdsOf,\n} from './_parts/agent-status';\nimport { AgentTile } from './_parts/agent-tile';\nimport { CODER, PLANNER, RESEARCHER, RUN_TITLE, WRITER } from './_parts/crew';\nimport { ToolTile } from './_parts/tool-tile';\n\nexport type GraphLayout = 'tree' | 'flow' | 'hub';\n\n/** The agents and the calls a run is expected to involve, when they are known before it starts. */\nexport type ExpectedGraph = {\n  agents: RunAgent[];\n  calls: ToolCall[];\n};\n\ntype GraphNode = {\n  id: string;\n  kind: 'agent' | 'tool';\n  parentId: string | null;\n  label: string;\n  series: SeriesSlot;\n  agent?: RunAgent;\n  owner?: RunAgent;\n  calls: ToolCall[];\n};\n\ntype Point = { x: number; y: number };\n\ntype Size = { width: number; height: number };\n\ntype Placement = { depth: number; slot: number };\n\ntype Plan = {\n  placements: Map<string, Placement>;\n  leafCount: number;\n  maxDepth: number;\n};\n\nconst LAYOUTS: Segment<GraphLayout>[] = [\n  { value: 'tree', label: 'Tree' },\n  { value: 'flow', label: 'Flow' },\n  { value: 'hub', label: 'Hub' },\n];\n\nconst STAGE_HEIGHT_PX = 320;\n\nconst PAD_X = 30;\n\nconst PAD_TOP = 24;\n\nconst PAD_BOTTOM = 36;\n\nconst AGENT_PX = 32;\n\nconst TOOL_PX = 28;\n\nconst INNER_RING = 0.4;\n\nconst RECENT_CALLS = 3;\n\nfunction isKnownParent(agent: RunAgent, ids: Set<string>) {\n  if (!agent.parentId) {\n    return false;\n  }\n  return ids.has(agent.parentId);\n}\n\nfunction parentIdOf(agent: RunAgent, ids: Set<string>) {\n  if (isKnownParent(agent, ids)) {\n    return agent.parentId ?? null;\n  }\n  return null;\n}\n\n/** Agents first, then one node per tool an agent called, so the same tool used by two agents sits under each. */\nfunction nodesOf(agents: RunAgent[], calls: ToolCall[]) {\n  const agentIds = agents.map((agent) => agent.id);\n  const ids = new Set(agentIds);\n  const nodes: GraphNode[] = [];\n  for (const agent of agents) {\n    const ownCalls = calls.filter((call) => call.agentId === agent.id);\n    nodes.push({\n      id: agent.id,\n      kind: 'agent',\n      parentId: parentIdOf(agent, ids),\n      label: agent.name,\n      series: agent.series,\n      agent,\n      calls: ownCalls,\n    });\n  }\n  for (const call of calls) {\n    const owner = agents.find((agent) => agent.id === call.agentId);\n    if (!owner) {\n      continue;\n    }\n    const id = `${owner.id}:${call.tool}`;\n    const existing = nodes.find((node) => node.id === id);\n    if (existing) {\n      existing.calls.push(call);\n      continue;\n    }\n    nodes.push({\n      id,\n      kind: 'tool',\n      parentId: owner.id,\n      label: call.tool,\n      series: owner.series,\n      owner,\n      calls: [call],\n    });\n  }\n  return nodes;\n}\n\nfunction childrenOf(nodes: GraphNode[], parentId: string) {\n  return nodes.filter((node) => node.parentId === parentId);\n}\n\nfunction planOf(nodes: GraphNode[]): Plan {\n  const placements = new Map<string, Placement>();\n  const roots = nodes.filter((node) => node.parentId === null);\n  let leafCount = 0;\n  let maxDepth = 0;\n\n  function place(node: GraphNode, depth: number): number {\n    maxDepth = Math.max(maxDepth, depth);\n    const children = childrenOf(nodes, node.id);\n    if (children.length === 0) {\n      const slot = leafCount + 0.5;\n      leafCount += 1;\n      placements.set(node.id, { depth, slot });\n      return slot;\n    }\n    const childSlots = children.map((child) => place(child, depth + 1));\n    const first = childSlots[0];\n    const last = childSlots[childSlots.length - 1];\n    const sum = first + last;\n    const slot = sum / 2;\n    placements.set(node.id, { depth, slot });\n    return slot;\n  }\n\n  for (const root of roots) {\n    place(root, 0);\n  }\n  return { placements, leafCount, maxDepth };\n}\n\nfunction depthShareOf(depth: number, maxDepth: number) {\n  if (maxDepth === 0) {\n    return 0.5;\n  }\n  return depth / maxDepth;\n}\n\nfunction ringOf(depth: number, maxDepth: number) {\n  if (depth === 0) {\n    return 0;\n  }\n  const ringsBeyondFirst = maxDepth - 1;\n  const outerRings = Math.max(1, ringsBeyondFirst);\n  const ringIndex = depth - 1;\n  const step = ringIndex / outerRings;\n  const spread = 1 - INNER_RING;\n  const reach = spread * step;\n  return INNER_RING + reach;\n}\n\nfunction pointOf(\n  layout: GraphLayout,\n  placement: Placement,\n  plan: Plan,\n  size: Size\n): Point {\n  const slots = Math.max(1, plan.leafCount);\n  const across = placement.slot / slots;\n  const deep = depthShareOf(placement.depth, plan.maxDepth);\n  const innerWidth = size.width - PAD_X * 2;\n  const innerHeight = size.height - PAD_TOP - PAD_BOTTOM;\n  if (layout === 'tree') {\n    const x = PAD_X + across * innerWidth;\n    const y = PAD_TOP + deep * innerHeight;\n    return { x, y };\n  }\n  if (layout === 'flow') {\n    const x = PAD_X + deep * innerWidth;\n    const y = PAD_TOP + across * innerHeight;\n    return { x, y };\n  }\n  const centerX = size.width / 2;\n  const centerY = PAD_TOP + innerHeight / 2;\n  const shortSide = Math.min(innerWidth, innerHeight);\n  const outerRadius = shortSide / 2;\n  const ring = ringOf(placement.depth, plan.maxDepth);\n  const radius = outerRadius * ring;\n  const turn = across * Math.PI * 2;\n  const angle = turn - Math.PI / 2;\n  const offsetX = radius * Math.cos(angle);\n  const offsetY = radius * Math.sin(angle);\n  return { x: centerX + offsetX, y: centerY + offsetY };\n}\n\nfunction round(value: number) {\n  const tenths = Math.round(value * 10);\n  return tenths / 10;\n}\n\nfunction between(from: number, to: number, share: number) {\n  const distance = to - from;\n  const offset = distance * share;\n  return round(from + offset);\n}\n\n/** Every edge is one cubic curve in all three layouts, so its path morphs while the nodes glide. */\nfunction edgePathOf(layout: GraphLayout, from: Point, to: Point) {\n  const fromX = round(from.x);\n  const fromY = round(from.y);\n  const toX = round(to.x);\n  const toY = round(to.y);\n  if (layout === 'tree') {\n    const middleY = between(fromY, toY, 1 / 2);\n    return `M ${fromX} ${fromY} C ${fromX} ${middleY}, ${toX} ${middleY}, ${toX} ${toY}`;\n  }\n  if (layout === 'flow') {\n    const middleX = between(fromX, toX, 1 / 2);\n    return `M ${fromX} ${fromY} C ${middleX} ${fromY}, ${middleX} ${toY}, ${toX} ${toY}`;\n  }\n  const firstX = between(fromX, toX, 1 / 3);\n  const firstY = between(fromY, toY, 1 / 3);\n  const secondX = between(fromX, toX, 2 / 3);\n  const secondY = between(fromY, toY, 2 / 3);\n  return `M ${fromX} ${fromY} C ${firstX} ${firstY}, ${secondX} ${secondY}, ${toX} ${toY}`;\n}\n\nfunction useStageSize() {\n  const [stage, setStage] = useState<HTMLDivElement | null>(null);\n  const [size, setSize] = useState<Size | null>(null);\n\n  useLayoutEffect(() => {\n    if (!stage) {\n      return;\n    }\n    const observer = new ResizeObserver(([entry]) => {\n      const box = entry.contentRect;\n      setSize({ width: box.width, height: box.height });\n    });\n    observer.observe(stage);\n    return () => observer.disconnect();\n  }, [stage]);\n\n  return { setStage, size };\n}\n\nfunction nodeStatusOf(node: GraphNode): CallStatus {\n  if (node.agent) {\n    return callStatusOfAgent(node.agent.status);\n  }\n  const latest = node.calls.at(-1);\n  return latest?.status ?? 'queued';\n}\n\nfunction callStatusOfAgent(status: AgentState): CallStatus {\n  if (status === 'idle') {\n    return 'queued';\n  }\n  return status;\n}\n\nfunction isBusy(node: GraphNode) {\n  const status = nodeStatusOf(node);\n  return status === 'running';\n}\n\nfunction nodeLabelOf(node: GraphNode, nodes: GraphNode[]) {\n  const parent = nodes.find((candidate) => candidate.id === node.parentId);\n  const status = nodeStatusOf(node);\n  const word = CALL_STATUS_WORD[status];\n  const callCount = node.calls.length;\n  const calls = `${callCount} ${nounOf(callCount, 'call')}`;\n  if (node.kind === 'tool') {\n    return `${node.label}, a tool of ${parent?.label ?? 'an agent'}, ${calls}, ${word}`;\n  }\n  if (parent) {\n    return `${node.label}, started by ${parent.label}, ${word}`;\n  }\n  return `${node.label}, the lead, ${word}`;\n}\n\nfunction NodeDetails({ node }: { node: GraphNode }) {\n  const lastCalls = node.calls.slice(-RECENT_CALLS);\n  const recent = [...lastCalls].reverse();\n  const hasCalls = recent.length > 0;\n  const isAgent = node.kind === 'agent';\n  const isTool = node.kind === 'tool';\n\n  return (\n    <div className='flex flex-col gap-3'>\n      <div className='flex items-center gap-2.5'>\n        {isAgent && (\n          <AgentTile name={node.label} series={node.series} size='md' />\n        )}\n        {isTool && <ToolTile tool={node.label} size='sm' />}\n        <div className='min-w-0 flex-1'>\n          <PopoverTitle className='truncate text-[13px] leading-5'>\n            {node.label}\n          </PopoverTitle>\n          <p className='text-muted-foreground truncate text-xs'>\n            {isAgent && (node.agent?.model ?? 'Agent')}\n            {isTool && `Called by ${node.owner?.name ?? 'an agent'}`}\n          </p>\n        </div>\n        {node.agent && <AgentStatus status={node.agent.status} isStill />}\n      </div>\n      <div className='flex flex-col gap-1'>\n        <p className='text-muted-foreground text-xs font-medium'>Last calls</p>\n        {!hasCalls && (\n          <p className='text-muted-foreground text-xs'>No calls yet</p>\n        )}\n        <ul className='flex flex-col'>\n          {recent.map((call) => (\n            <li key={call.id} className='flex items-center gap-2 py-1 text-xs'>\n              <CallStatusMark status={call.status} className='size-3.5' />\n              <span className='min-w-0 flex-1 truncate'>{call.label}</span>\n              {call.startedAt !== undefined && (\n                <LiveDuration\n                  item={call}\n                  isRunning={call.status === 'running'}\n                  className='text-muted-foreground shrink-0'\n                />\n              )}\n            </li>\n          ))}\n        </ul>\n      </div>\n    </div>\n  );\n}\n\nconst GROW_IN = { opacity: 0, scale: 0.9 };\n\nconst FADE_IN = { opacity: 0, scale: 1 };\n\nfunction growOf(shouldReduceMotion: boolean | null) {\n  if (shouldReduceMotion) {\n    return FADE_IN;\n  }\n  return GROW_IN;\n}\n\nfunction GraphNodeButton({\n  node,\n  point,\n  label,\n  isPulsing,\n}: {\n  node: GraphNode;\n  point: Point;\n  label: string;\n  /** Only the halo of a running agent breathes, and three at most in the graph. */\n  isPulsing: boolean;\n}) {\n  const shouldReduceMotion = usePrefersReducedMotion();\n  const glide = slowSpringOf(shouldReduceMotion);\n  const grow = growOf(shouldReduceMotion);\n  const isAgent = node.kind === 'agent';\n  const isTool = node.kind === 'tool';\n  const nodeSize = nodeSizeOf(node);\n  const half = nodeSize / 2;\n  const x = point.x - half;\n  const y = point.y - half;\n  const isRunning = isBusy(node);\n  const color = seriesColorOf(node.series);\n  const haloStyle = {\n    boxShadow: `0 0 0 2px color-mix(in oklab, ${color} 45%, transparent)`,\n    animationDuration: '2s',\n  };\n\n  return (\n    <motion.div\n      className='absolute start-0 top-0'\n      initial={{ ...grow, x, y }}\n      animate={{ opacity: 1, scale: 1, x, y }}\n      exit={{ opacity: 0, scale: 0.9, transition: { duration: 0.14 } }}\n      transition={glide}\n    >\n      <Popover>\n        <PopoverTrigger\n          render={\n            <button\n              type='button'\n              aria-label={label}\n              className={cn(\n                'focus-visible:ring-ring focus-visible:ring-offset-background relative flex cursor-pointer outline-none focus-visible:ring-2 focus-visible:ring-offset-2',\n                isAgent && 'rounded-lg',\n                isTool && 'rounded-md'\n              )}\n            />\n          }\n        >\n          {isRunning && (\n            <span\n              aria-hidden='true'\n              className={cn(\n                'absolute -inset-1',\n                isPulsing && 'animate-pulse',\n                isAgent && 'rounded-[14px]',\n                isTool && 'rounded-xl'\n              )}\n              style={haloStyle}\n            />\n          )}\n          {isAgent && (\n            <AgentTile name={node.label} series={node.series} size='lg' />\n          )}\n          {isTool && <ToolTile tool={node.label} size='md' />}\n          {isAgent && (\n            <span\n              aria-hidden='true'\n              className='text-muted-foreground pointer-events-none absolute top-full left-1/2 mt-1 -translate-x-1/2 text-[11px] whitespace-nowrap'\n            >\n              {node.label}\n            </span>\n          )}\n        </PopoverTrigger>\n        <PopoverContent className='w-64 p-3'>\n          <NodeDetails node={node} />\n        </PopoverContent>\n      </Popover>\n    </motion.div>\n  );\n}\n\nfunction nodeSizeOf(node: GraphNode) {\n  if (node.kind === 'agent') {\n    return AGENT_PX;\n  }\n  return TOOL_PX;\n}\n\ntype Edge = {\n  id: string;\n  path: string;\n  series: SeriesSlot;\n};\n\n/**\n * An edge draws itself once, when one agent calls another or reaches for a tool, then holds still:\n * its length is normalised to 1. The edges the graph is first shown with are there, they do not arrive.\n */\nconst EDGE_DRAW =\n  'transition-[stroke-dashoffset] duration-500 ease-(--ease-out-strong) [stroke-dasharray:1] [stroke-dashoffset:0] starting:[stroke-dashoffset:1]';\n\nfunction edgeDrawOf(edgeId: string, firstNodeIds: Set<string>) {\n  const wasThereAtFirst = firstNodeIds.has(edgeId);\n  if (wasThereAtFirst) {\n    return undefined;\n  }\n  return EDGE_DRAW;\n}\n\nconst QUICK_EXIT = { duration: 0.14 };\n\n/**\n * A call sends a dot from the agent to its tool. Each axis travels on its own curve, and the two\n * curves below are the ones that trace the edge itself: a cubic whose handles sit halfway along one\n * axis moves like a smooth step on the other. Both are transitions on `translate`, so the dot costs\n * no script and no layout.\n */\nconst HOP_TRAVEL = 'transition-[translate] duration-700';\n\nconst SMOOTH_STEP = 'cubic-bezier(0.3333, 0, 0.6667, 1)';\n\nconst HALFWAY_HANDLES = 'cubic-bezier(0.3333, 0.5, 0.6667, 0.5)';\n\ntype HopEasing = { across: string; down: string };\n\nconst HOP_EASING: Record<GraphLayout, HopEasing> = {\n  tree: { across: SMOOTH_STEP, down: HALFWAY_HANDLES },\n  flow: { across: HALFWAY_HANDLES, down: SMOOTH_STEP },\n  hub: { across: 'linear', down: 'linear' },\n};\n\ntype Hop = {\n  key: string;\n  from: Point;\n  to: Point;\n  series: SeriesSlot;\n};\n\ntype HopStyle = React.CSSProperties & {\n  '--hop-from': string;\n  '--hop-to': string;\n};\n\nfunction hopStyleOf(from: number, to: number, easing: string): HopStyle {\n  return {\n    '--hop-from': `${round(from)}px`,\n    '--hop-to': `${round(to)}px`,\n    transitionTimingFunction: easing,\n  };\n}\n\nfunction CallHop({ hop, layout }: { hop: Hop; layout: GraphLayout }) {\n  const easing = HOP_EASING[layout];\n  const acrossStyle = hopStyleOf(hop.from.x, hop.to.x, easing.across);\n  const downStyle = hopStyleOf(hop.from.y, hop.to.y, easing.down);\n\n  return (\n    <span\n      className={cn(\n        'absolute top-0 left-0 translate-x-(--hop-to) starting:translate-x-(--hop-from)',\n        HOP_TRAVEL\n      )}\n      style={acrossStyle}\n    >\n      <span\n        className={cn(\n          'block translate-y-(--hop-to) starting:translate-y-(--hop-from)',\n          HOP_TRAVEL\n        )}\n        style={downStyle}\n      >\n        <span\n          className='block size-1.5 -translate-x-1/2 -translate-y-1/2 rounded-full opacity-0 transition-opacity delay-500 duration-200 starting:opacity-100'\n          style={{ backgroundColor: seriesColorOf(hop.series) }}\n        />\n      </span>\n    </span>\n  );\n}\n\nfunction hopKeyOf(call: ToolCall) {\n  return `${call.id}-${call.startedAt ?? 0}`;\n}\n\nfunction isRunningCall(call: ToolCall) {\n  return call.status === 'running';\n}\n\n/** A dot belongs to a call in flight and leaves when the call returns, so a long run keeps no trail of them. */\nfunction hopsOf(\n  calls: ToolCall[],\n  agents: RunAgent[],\n  points: Map<string, Point>,\n  seenKeys: Set<string>\n) {\n  const hops: Hop[] = [];\n  const runningCalls = calls.filter(isRunningCall);\n  for (const call of runningCalls) {\n    const key = hopKeyOf(call);\n    const ownerId = call.agentId ?? '';\n    const owner = agents.find((agent) => agent.id === ownerId);\n    const from = points.get(ownerId);\n    const to = points.get(`${ownerId}:${call.tool}`);\n    const isSeen = seenKeys.has(key);\n    if (isSeen || !owner) {\n      continue;\n    }\n    if (!from || !to) {\n      continue;\n    }\n    hops.push({ key, from, to, series: owner.series });\n  }\n  return hops;\n}\n\n/** What was planned keeps its order, so a node that joins later takes the place kept for it. */\nfunction mergedById<Item extends { id: string }>(\n  planned: Item[],\n  present: Item[]\n) {\n  const plannedIds = planned.map((item) => item.id);\n  const known = new Set(plannedIds);\n  const extras = present.filter((item) => !known.has(item.id));\n  return [...planned, ...extras];\n}\n\nfunction edgesOf(\n  nodes: GraphNode[],\n  points: Map<string, Point>,\n  layout: GraphLayout\n) {\n  const edges: Edge[] = [];\n  for (const node of nodes) {\n    const from = points.get(node.parentId ?? '');\n    const to = points.get(node.id);\n    if (!from || !to) {\n      continue;\n    }\n    const parent = nodes.find((candidate) => candidate.id === node.parentId);\n    const series = parent?.series ?? node.series;\n    const path = edgePathOf(layout, from, to);\n    edges.push({ id: node.id, path, series });\n  }\n  return edges;\n}\n\nfunction initialHopKeys(calls: ToolCall[]) {\n  const keys = calls.map(hopKeyOf);\n  return new Set(keys);\n}\n\nfunction initialNodeIds(agents: RunAgent[], calls: ToolCall[]) {\n  const nodes = nodesOf(agents, calls);\n  const ids = nodes.map((node) => node.id);\n  return new Set(ids);\n}\n\nconst NO_CALLS: ToolCall[] = [];\n\nconst NOTHING_EXPECTED: ExpectedGraph = { agents: [], calls: [] };\n\nexport type AgentGraphProps = {\n  agents: RunAgent[];\n  calls?: ToolCall[];\n  /**\n   * What the run is expected to involve. Every node then has its place from the start and appears\n   * there when it joins, so the rest of the graph holds still. Without it the graph is laid out\n   * again each time a node joins.\n   */\n  expected?: ExpectedGraph;\n  defaultLayout?: GraphLayout;\n  className?: string;\n};\n\nexport function AgentGraph({\n  agents,\n  calls = NO_CALLS,\n  expected = NOTHING_EXPECTED,\n  defaultLayout = 'tree',\n  className,\n}: AgentGraphProps) {\n  const [layout, setLayout] = useState<GraphLayout>(defaultLayout);\n  const [seenHopKeys] = useState(() => initialHopKeys(calls));\n  const [firstNodeIds] = useState(() => initialNodeIds(agents, calls));\n  const { setStage, size } = useStageSize();\n  const shouldReduceMotion = usePrefersReducedMotion();\n  const glide = slowSpringOf(shouldReduceMotion);\n  const nodes = nodesOf(agents, calls);\n  const placedAgents = mergedById(expected.agents, agents);\n  const placedCalls = mergedById(expected.calls, calls);\n  const placedNodes = nodesOf(placedAgents, placedCalls);\n  const plan = planOf(placedNodes);\n  const points = new Map<string, Point>();\n  if (size) {\n    for (const node of placedNodes) {\n      const placement = plan.placements.get(node.id);\n      if (placement) {\n        const point = pointOf(layout, placement, plan, size);\n        points.set(node.id, point);\n      }\n    }\n  }\n  const edges = edgesOf(nodes, points, layout);\n  const hops = hopsOf(calls, agents, points, seenHopKeys);\n  const pulsingIds = pulsingIdsOf(agents);\n  const agentCount = agents.length;\n  const toolCount = nodes.length - agentCount;\n  const callCount = calls.length;\n\n  return (\n    <div className={cn('flex flex-col gap-3', className)}>\n      <div className='flex flex-wrap items-center justify-between gap-2'>\n        <p className='text-muted-foreground text-xs tabular-nums'>\n          <MotionNumber value={agentCount} /> {nounOf(agentCount, 'agent')},{' '}\n          <MotionNumber value={toolCount} /> {nounOf(toolCount, 'tool')},{' '}\n          <MotionNumber value={callCount} /> {nounOf(callCount, 'call')}\n        </p>\n        <Segments\n          value={layout}\n          segments={LAYOUTS}\n          onValueChange={setLayout}\n          label='Layout'\n          className='w-44'\n        />\n      </div>\n      <div\n        ref={setStage}\n        className='bg-foreground/[0.025] dark:bg-foreground/[0.04] relative overflow-hidden rounded-lg'\n        style={{ height: STAGE_HEIGHT_PX }}\n      >\n        {size && (\n          <svg\n            aria-hidden='true'\n            width={size.width}\n            height={size.height}\n            className='pointer-events-none absolute inset-0'\n          >\n            <AnimatePresence initial={false}>\n              {edges.map((edge) => (\n                <motion.path\n                  key={edge.id}\n                  pathLength={1}\n                  fill='none'\n                  strokeWidth={1.25}\n                  strokeOpacity={0.5}\n                  className={edgeDrawOf(edge.id, firstNodeIds)}\n                  style={{ stroke: seriesColorOf(edge.series) }}\n                  initial={{ d: edge.path }}\n                  animate={{ d: edge.path }}\n                  exit={{ opacity: 0, transition: QUICK_EXIT }}\n                  transition={glide}\n                />\n              ))}\n            </AnimatePresence>\n          </svg>\n        )}\n        <div\n          aria-hidden='true'\n          className='pointer-events-none absolute inset-0'\n        >\n          {hops.map((hop) => (\n            <CallHop key={hop.key} hop={hop} layout={layout} />\n          ))}\n        </div>\n        {size && (\n          <ul aria-label='Agents and tools' className='absolute inset-0'>\n            <AnimatePresence initial={false}>\n              {nodes.map((node) => {\n                const point = points.get(node.id);\n                if (!point) {\n                  return null;\n                }\n                return (\n                  <li key={node.id}>\n                    <GraphNodeButton\n                      node={node}\n                      point={point}\n                      label={nodeLabelOf(node, nodes)}\n                      isPulsing={pulsingIds.has(node.id)}\n                    />\n                  </li>\n                );\n              })}\n            </AnimatePresence>\n          </ul>\n        )}\n      </div>\n    </div>\n  );\n}\n\nconst INITIAL_RUN = emptyRun({\n  id: 'brightmoor-graph',\n  title: RUN_TITLE,\n  agents: [{ ...PLANNER, status: 'running' }],\n});\n\nconst PAGE_READER: RunAgent = {\n  id: 'page-reader',\n  name: 'Page reader',\n  series: 2,\n  status: 'running',\n  model: 'Claude Haiku',\n  parentId: RESEARCHER.id,\n};\n\nfunction spawned(agent: RunAgent): RunEvent {\n  return { type: 'agent.added', agent: { ...agent, status: 'running' } };\n}\n\nfunction settled(id: string, status: RunAgent['status']): RunEvent {\n  return { type: 'agent.updated', id, patch: { status } };\n}\n\nfunction called(\n  id: string,\n  agentId: string,\n  tool: string,\n  label: string\n): RunEvent {\n  return {\n    type: 'call.started',\n    call: { id, agentId, tool, label, status: 'running' },\n  };\n}\n\nfunction returned(id: string, status: CallStatus = 'done'): RunEvent {\n  return { type: 'call.updated', id, patch: { status } };\n}\n\nconst SCRIPT: RunStep[] = [\n  batch(500, { type: 'run.started' }),\n  batch(400, spawned(RESEARCHER)),\n  batch(\n    500,\n    called(\n      'search-1',\n      RESEARCHER.id,\n      'web_search',\n      'Searched for Brightmoor prices'\n    )\n  ),\n  batch(900, returned('search-1')),\n  batch(300, spawned(CODER)),\n  batch(\n    400,\n    called('sheet-1', CODER.id, 'read_sheet', 'Read q3-invoices.xlsx')\n  ),\n  batch(\n    400,\n    called('fetch-1', RESEARCHER.id, 'fetch_page', 'Read the pricing page')\n  ),\n  batch(700, returned('sheet-1')),\n  batch(300, returned('fetch-1', 'failed')),\n  batch(400, spawned(PAGE_READER)),\n  batch(\n    500,\n    called(\n      'fetch-2',\n      PAGE_READER.id,\n      'fetch_page',\n      'Read the cached pricing page'\n    )\n  ),\n  batch(\n    600,\n    called('python-1', CODER.id, 'run_python', 'Ran compare_prices.py')\n  ),\n  batch(800, returned('fetch-2')),\n  batch(300, settled(PAGE_READER.id, 'done')),\n  batch(\n    400,\n    called(\n      'search-2',\n      RESEARCHER.id,\n      'web_search',\n      'Searched for the contract terms'\n    )\n  ),\n  batch(700, returned('python-1')),\n  batch(300, returned('search-2')),\n  batch(200, settled(RESEARCHER.id, 'done')),\n  batch(\n    400,\n    called('python-2', CODER.id, 'run_python', 'Summed what was overpaid')\n  ),\n  batch(900, returned('python-2')),\n  batch(200, settled(CODER.id, 'done')),\n  batch(300, spawned(WRITER)),\n  batch(\n    500,\n    called('draft-1', WRITER.id, 'draft_email', 'Drafted the credit request')\n  ),\n  batch(1400, returned('draft-1')),\n  batch(200, settled(WRITER.id, 'done')),\n  batch(300, settled(PLANNER.id, 'done'), {\n    type: 'run.finished',\n    status: 'done',\n  }),\n];\n\n/** The script is known ahead, so every agent and every tool has its place before it joins. */\nfunction expectedOf(initial: RunSnapshot, script: RunStep[]): ExpectedGraph {\n  const agents = [...initial.agents];\n  const calls: ToolCall[] = [];\n  for (const step of script) {\n    for (const event of step.action.events) {\n      if (event.type === 'agent.added') {\n        agents.push(event.agent);\n      }\n      if (event.type === 'call.started') {\n        calls.push(event.call);\n      }\n    }\n  }\n  return { agents, calls };\n}\n\nconst EXPECTED = expectedOf(INITIAL_RUN, SCRIPT);\n\nconst SESSION = {\n  reducer: reduceBatch,\n  initial: INITIAL_RUN,\n  resting: restingOf(INITIAL_RUN, SCRIPT, applyBatchAt),\n  script: SCRIPT,\n  loopAfter: 3000,\n};\n\nfunction announcementOf(run: RunSnapshot) {\n  const latest = run.calls.at(-1);\n  if (run.status === 'done') {\n    return 'The run is done';\n  }\n  if (!latest) {\n    return `${PLANNER.name} leads the run`;\n  }\n  const agent = run.agents.find((candidate) => candidate.id === latest.agentId);\n  const word = CALL_STATUS_WORD[latest.status];\n  return `${agent?.name ?? 'An agent'} called ${latest.tool}: ${word}`;\n}\n\nfunction crewWordOf(run: RunSnapshot) {\n  const running = run.agents.filter((agent) => agent.status === 'running');\n  const count = running.length;\n  if (count === 0) {\n    return AGENT_STATUS_WORD.done;\n  }\n  return `${count} ${nounOf(count, 'agent')} running`;\n}\n\nexport default function Agent03() {\n  const { ref, isInView, isAutoplaying, state, restart } = useSession(SESSION);\n  const isEnded = state.endedAt !== undefined;\n  const canReplay = isEnded && !isAutoplaying;\n  const announcement = announcementOf(state);\n\n  useKeptHeight(ref);\n\n  return (\n    <div\n      ref={ref}\n      className='flex min-h-[28.75rem] w-full max-w-[44rem] flex-col gap-2'\n    >\n      <ClockGate isActive={isInView}>\n        <section\n          aria-label='Agent graph'\n          className='bg-card hairline-ring ring-border flex flex-col gap-3 rounded-xl p-4 shadow-[0_1px_2px_rgb(0_0_0/0.04)]'\n        >\n          <header className='flex items-baseline justify-between gap-3'>\n            <h3 className='truncate text-sm font-medium'>{state.title}</h3>\n            <span className='text-muted-foreground shrink-0 text-xs'>\n              {crewWordOf(state)}\n            </span>\n          </header>\n          <AgentGraph\n            agents={state.agents}\n            calls={state.calls}\n            expected={EXPECTED}\n          />\n        </section>\n      </ClockGate>\n      <div className='flex h-7 items-center justify-end'>\n        <ReplayButton isShown={canReplay} onReplay={restart} />\n      </div>\n      <LiveRegion message={spokenUnless(isAutoplaying, announcement)} />\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/agent/agent-03.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/tool-tile.tsx",
      "content": "import {\n  BlocksIcon,\n  FileSpreadsheetIcon,\n  FileTextIcon,\n  GlobeIcon,\n  GitBranchIcon,\n  ListChecksIcon,\n  MailIcon,\n  MemoryIcon,\n  PencilIcon,\n  SearchIcon,\n  TerminalIcon,\n} from '@/registry/ndeko/lib/icons';\nimport { cn } from '@/registry/ndeko/lib/utils';\n\nconst TOOL_GLYPHS: Record<string, React.ReactNode> = {\n  web_search: <SearchIcon />,\n  fetch_page: <GlobeIcon />,\n  read_sheet: <FileSpreadsheetIcon />,\n  read_file: <FileTextIcon />,\n  edit_file: <PencilIcon />,\n  write_doc: <FileTextIcon />,\n  send_email: <MailIcon />,\n  draft_email: <MailIcon />,\n  run_python: <TerminalIcon />,\n  shell: <TerminalIcon />,\n  run_tests: <ListChecksIcon />,\n  recall: <MemoryIcon />,\n  git: <GitBranchIcon />,\n};\n\nconst ANY_TOOL_GLYPH = <BlocksIcon />;\n\nexport function toolGlyphOf(tool: string) {\n  return TOOL_GLYPHS[tool] ?? ANY_TOOL_GLYPH;\n}\n\nconst SIZE_CLASS = {\n  sm: 'size-6 rounded-md [&_svg]:size-3.5',\n  md: 'size-7 rounded-md [&_svg]:size-3.5',\n  lg: 'size-8 rounded-lg [&_svg]:size-4',\n} as const;\n\ntype ToolTileProps = {\n  tool: string;\n  size?: keyof typeof SIZE_CLASS;\n  className?: string;\n};\n\n/** The tool name sits beside the tile or in the label of its control, so screen readers skip the glyph. */\nexport function ToolTile({ tool, size = 'md', className }: ToolTileProps) {\n  const glyph = toolGlyphOf(tool);\n\n  return (\n    <span\n      aria-hidden='true'\n      className={cn(\n        'keycap-3d text-muted-foreground inline-flex shrink-0 items-center justify-center',\n        SIZE_CLASS[size],\n        className\n      )}\n    >\n      {glyph}\n    </span>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/agent/_parts/tool-tile.tsx"
    }
  ],
  "categories": [
    "components",
    "agent"
  ],
  "type": "registry:component"
}