{
  "schemaVersion": 1,
  "target": "react",
  "name": "dependency-graph",
  "version": "0.2.0",
  "category": "agent",
  "title": "Dependency Graph",
  "description": "Which steps wait on other steps. Selecting a step highlights its upstream and downstream dependencies; focus on a branch to show only its ancestors and descendants.",
  "tags": [
    "agent",
    "graph",
    "dependencies",
    "dag",
    "plan",
    "inspector"
  ],
  "requires": {
    "projectTypes": [
      "next",
      "vite-react"
    ],
    "styling": "tailwind-v4"
  },
  "npmDependencies": {},
  "registryDependencies": [
    {
      "name": "graph-canvas",
      "version": "0.2.0"
    },
    {
      "name": "agent-run",
      "version": "0.1.1"
    },
    {
      "name": "step-list",
      "version": "0.2.0"
    }
  ],
  "docsPath": "/components/dependency-graph",
  "props": [
    {
      "name": "nodes",
      "type": "{ id, label, kind?, status, summary? }[]",
      "description": "Steps with stable ids."
    },
    {
      "name": "edges",
      "type": "{ id, source, target, type: \"requires\" | \"optional\", label? }[]",
      "description": "target waits on source. Optional edges are dashed and never block."
    },
    {
      "name": "selectedId / onSelect",
      "type": "string | null / (id: string | null) => void",
      "description": "Controlled selection. Leave selectedId undefined for uncontrolled use."
    },
    {
      "name": "focusedId / onFocusChange",
      "type": "string | null / (id: string | null) => void",
      "description": "Controlled branch focus. null shows the full graph. Focusing hides other nodes without moving any."
    },
    {
      "name": "defaultSelectedId / defaultFocusedId",
      "type": "string | null",
      "default": "null",
      "description": "Initial values when uncontrolled."
    },
    {
      "name": "defaultView",
      "type": "\"graph\" | \"list\"",
      "default": "\"graph\"",
      "description": "Which view shows first."
    },
    {
      "name": "height",
      "type": "number",
      "default": "440",
      "description": "Height of the graph and list in pixels."
    },
    {
      "name": "waitingOn(nodes, edges)",
      "type": "=> Map<string, string[]>",
      "description": "Unfinished required prerequisites per step, as shown in the list and details."
    },
    {
      "name": "direction",
      "type": "\"TB\" | \"LR\"",
      "default": "\"TB\"",
      "description": "Layout direction. Top to bottom suits deep, narrow runs in a side-by-side layout; left to right suits wide graphs such as the 100-node fixture."
    }
  ],
  "accessibility": [
    "Upstream and downstream steps get a text tag (Upstream, Downstream) on the node and in the list; unrelated steps and edges are dimmed.",
    "The list view shows each step's status, kind, dependencies, and what it is waiting on, and supports arrow keys, Home, and End.",
    "Focus on branch and Show full graph are buttons in the header, reachable by Tab; the header text states which branch is shown.",
    "Requires and optional edges differ by solid and dashed lines, explained in a text legend."
  ],
  "states": [
    "queued",
    "running",
    "waiting-for-approval",
    "succeeded",
    "failed",
    "skipped",
    "cancelled"
  ],
  "files": [
    {
      "path": "graph/dependency-graph.tsx",
      "content": "\"use client\";\n\nimport { type ComponentProps, useMemo, useState } from \"react\";\nimport { GraphCanvas, type GraphCanvasEdge, type GraphCanvasNode, useGraphLayout } from \"./graph-canvas\";\nimport { adjacency, ancestorsOf, descendantsOf, GRAPH_STATUS_LABELS, type GraphDirection, type GraphStatus, layoutEdges } from \"./graph-layout\";\nimport {\n  DetailHeader,\n  DetailNote,\n  DetailProperties,\n  DetailSection,\n  DetailStack,\n  EdgeLegend,\n  EmptyDetails,\n  graphButtonClass,\n  GraphListbox,\n  GraphListRow,\n  GraphShell,\n  type GraphView,\n  GraphViewSwitch,\n  nameList,\n  RelatedButton,\n  StatusCounts,\n  useControllableState,\n} from \"./graph-ui\";\n\n/** requires: the target cannot start until the source is done. optional: the target uses the source's output if it is ready. */\nexport type DependencyEdgeType = \"requires\" | \"optional\";\n\nexport interface DependencyNode {\n  /** Stable id. Keep it the same across updates so the node keeps its place. */\n  id: string;\n  label: string;\n  /** Free-form kind shown after the status, for example \"task\" or \"build\". */\n  kind?: string;\n  status: GraphStatus;\n  summary?: string;\n}\n\n/** `target` waits on `source`. */\nexport interface DependencyEdge {\n  id: string;\n  source: string;\n  target: string;\n  type: DependencyEdgeType;\n  label?: string;\n}\n\nexport interface DependencyGraphProps extends Omit<ComponentProps<\"section\">, \"children\" | \"onSelect\" | \"title\"> {\n  nodes: readonly DependencyNode[];\n  edges: readonly DependencyEdge[];\n  title?: string;\n  /** Controlled selection. Pass `null` for none; leave undefined to let the component manage it. */\n  selectedId?: string | null;\n  defaultSelectedId?: string | null;\n  onSelect?: (id: string | null) => void;\n  /** Controlled branch focus: the node whose ancestors and descendants are shown alone. `null` shows the full graph. */\n  focusedId?: string | null;\n  defaultFocusedId?: string | null;\n  onFocusChange?: (id: string | null) => void;\n  defaultView?: GraphView;\n  height?: number;\n  direction?: GraphDirection;\n}\n\nconst done = new Set<GraphStatus>([\"succeeded\", \"skipped\"]);\n\n/** For each node, the required prerequisites that have not finished: what it is waiting on. */\nexport function waitingOn(nodes: readonly DependencyNode[], edges: readonly DependencyEdge[]): Map<string, string[]> {\n  const status = new Map(nodes.map((node) => [node.id, node.status]));\n  const result = new Map<string, string[]>();\n  for (const edge of edges) {\n    if (edge.type !== \"requires\" || done.has(status.get(edge.source) ?? \"queued\")) continue;\n    (result.get(edge.target) ?? result.set(edge.target, []).get(edge.target)!).push(edge.source);\n  }\n  return result;\n}\n\nfunction StepCount({ count }: { count: number }) {\n  return (\n    <>\n      <span className=\"font-mono tabular-nums\">{count}</span> {count === 1 ? \"step\" : \"steps\"}\n    </>\n  );\n}\n\n/** Which steps wait on which. Selecting a step highlights what it depends on and what depends on it; focus narrows the graph to that branch. */\nexport function DependencyGraph({\n  nodes,\n  edges,\n  title = \"Dependencies\",\n  selectedId,\n  defaultSelectedId = null,\n  onSelect,\n  focusedId,\n  defaultFocusedId = null,\n  onFocusChange,\n  defaultView = \"graph\",\n  height = 440,\n  direction = \"TB\",\n  ...props\n}: DependencyGraphProps) {\n  const [selected, setSelected] = useControllableState<string | null>(selectedId, defaultSelectedId, onSelect);\n  const [focused, setFocused] = useControllableState<string | null>(focusedId, defaultFocusedId, onFocusChange);\n  const [view, setView] = useState<GraphView>(defaultView);\n  const shownEdges = useMemo(() => layoutEdges(nodes, edges), [nodes, edges]);\n  // Laid out once for the full graph: focusing hides nodes instead of moving them.\n  const layout = useGraphLayout(nodes, shownEdges, { direction });\n  const byId = useMemo(() => new Map(nodes.map((node) => [node.id, node])), [nodes]);\n  const links = useMemo(() => adjacency(shownEdges), [shownEdges]);\n  const waiting = useMemo(() => waitingOn(nodes, shownEdges), [nodes, shownEdges]);\n\n  const upstream = useMemo(() => (selected ? ancestorsOf(selected, shownEdges) : new Set<string>()), [selected, shownEdges]);\n  const downstream = useMemo(() => (selected ? descendantsOf(selected, shownEdges) : new Set<string>()), [selected, shownEdges]);\n  const visible = useMemo(() => {\n    if (!focused || !byId.has(focused)) return null;\n    return new Set([focused, ...ancestorsOf(focused, shownEdges), ...descendantsOf(focused, shownEdges)]);\n  }, [focused, byId, shownEdges]);\n  const shownNodes = useMemo(() => (visible ? nodes.filter((node) => visible.has(node.id)) : nodes), [nodes, visible]);\n  const current = selected ? byId.get(selected) : undefined;\n  const label = (id: string) => byId.get(id)?.label ?? id;\n\n  const canvasNodes = useMemo<GraphCanvasNode[]>(\n    () =>\n      nodes.map((node) => {\n        const relation = !selected || node.id === selected ? null : upstream.has(node.id) ? \"upstream\" : downstream.has(node.id) ? \"downstream\" : \"muted\";\n        return {\n          id: node.id,\n          title: node.label,\n          subtitle: node.kind,\n          status: node.status,\n          tone: relation ?? \"default\",\n          tag: relation === \"upstream\" ? \"Upstream\" : relation === \"downstream\" ? \"Downstream\" : undefined,\n          hidden: visible ? !visible.has(node.id) : undefined,\n        };\n      }),\n    [nodes, selected, upstream, downstream, visible],\n  );\n  const canvasEdges = useMemo<GraphCanvasEdge[]>(\n    () =>\n      shownEdges.map((edge) => {\n        const onPath =\n          selected != null &&\n          ((upstream.has(edge.source) && (edge.target === selected || upstream.has(edge.target))) ||\n            (downstream.has(edge.target) && (edge.source === selected || downstream.has(edge.source))));\n        return {\n          id: edge.id,\n          source: edge.source,\n          target: edge.target,\n          type: edge.type,\n          label: edge.label,\n          dashed: edge.type === \"optional\",\n          tone: !selected ? \"default\" : onPath ? \"highlight\" : \"muted\",\n          hidden: visible ? !(visible.has(edge.source) && visible.has(edge.target)) : undefined,\n        };\n      }),\n    [shownEdges, selected, upstream, downstream, visible],\n  );\n\n  const select = (id: string) => setSelected(id);\n  return (\n    <GraphShell\n      aria-label={title}\n      title={title}\n      meta={\n        visible && focused ? (\n          <span>\n            Branch of <span className=\"font-medium text-zinc-700 dark:text-zinc-300\">{label(focused)}</span>:{\" \"}\n            <span className=\"font-mono tabular-nums\">{visible.size}</span> of <span className=\"font-mono tabular-nums\">{nodes.length}</span> steps\n          </span>\n        ) : (\n          <StatusCounts lead={`${nodes.length} ${nodes.length === 1 ? \"step\" : \"steps\"}`} statuses={nodes.map((node) => node.status)} />\n        )\n      }\n      controls={\n        <>\n          {visible ? (\n            <button type=\"button\" className={graphButtonClass} onClick={() => setFocused(null)}>\n              <svg aria-hidden=\"true\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\" className=\"size-3.5\">\n                <path d=\"M9.5 2.75h3.75V6.5M6.5 13.25H2.75V9.5M13.25 2.75 9 7M2.75 13.25 7 9\" />\n              </svg>\n              Show full graph\n            </button>\n          ) : (\n            <button type=\"button\" className={graphButtonClass} disabled={!current} onClick={() => current && setFocused(current.id)}>\n              <svg aria-hidden=\"true\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\" className=\"size-3.5\">\n                <circle cx=\"8\" cy=\"8\" r=\"2.25\" />\n                <path d=\"M8 1.75v2.5M8 11.75v2.5M1.75 8h2.5M11.75 8h2.5\" />\n              </svg>\n              Focus on branch\n            </button>\n          )}\n          <GraphViewSwitch view={view} onChange={setView} />\n        </>\n      }\n      detailsLabel=\"Step dependencies\"\n      height={height}\n      footer={\n        <div className=\"flex flex-wrap items-center justify-between gap-x-4 gap-y-1.5\">\n          <EdgeLegend items={[{ label: \"Requires\" }, { label: \"Optional\", dashed: true }]} />\n          {selected ? <p className=\"text-xs text-zinc-500\">Upstream: it waits on them. Downstream: they wait on it.</p> : null}\n        </div>\n      }\n      details={\n        current ? (\n          <DetailStack>\n            <DetailHeader title={current.label} status={current.status} meta={[current.kind]} />\n            {current.summary ? <p className=\"text-sm text-pretty text-zinc-600 dark:text-zinc-400\">{current.summary}</p> : null}\n            <DetailProperties\n              items={[\n                { label: \"Upstream\", value: <StepCount count={upstream.size} /> },\n                { label: \"Downstream\", value: <StepCount count={downstream.size} /> },\n                {\n                  label: \"Blocked\",\n                  value: waiting.get(current.id)?.length ? (\n                    <span className=\"text-amber-700 dark:text-amber-400\">\n                      Yes, on <StepCount count={waiting.get(current.id)!.length} />\n                    </span>\n                  ) : (\n                    \"No\"\n                  ),\n                },\n              ]}\n            />\n            <div className=\"space-y-3.5\">\n              <DetailSection label=\"Waiting on\">\n                {waiting.get(current.id)?.length ? (\n                  waiting.get(current.id)!.map((id) => <RelatedButton key={id} label={label(id)} status={byId.get(id)?.status} onClick={() => select(id)} />)\n                ) : (\n                  <DetailNote>Nothing. Every required step is done.</DetailNote>\n                )}\n              </DetailSection>\n              <DetailSection label=\"Depends on\">\n                {links.incoming.get(current.id)?.length ? (\n                  links.incoming.get(current.id)!.map((edge) => (\n                    <RelatedButton key={edge.id} label={label(edge.source)} status={byId.get(edge.source)?.status} note={edge.type === \"requires\" ? \"Required\" : \"Optional\"} onClick={() => select(edge.source)} />\n                  ))\n                ) : (\n                  <DetailNote>No dependencies.</DetailNote>\n                )}\n              </DetailSection>\n              <DetailSection label=\"Needed by\">\n                {links.outgoing.get(current.id)?.length ? (\n                  links.outgoing.get(current.id)!.map((edge) => (\n                    <RelatedButton key={edge.id} label={label(edge.target)} status={byId.get(edge.target)?.status} note={edge.type === \"requires\" ? \"Required\" : \"Optional\"} onClick={() => select(edge.target)} />\n                  ))\n                ) : (\n                  <DetailNote>Nothing waits on this step.</DetailNote>\n                )}\n              </DetailSection>\n            </div>\n          </DetailStack>\n        ) : (\n          <EmptyDetails hint=\"Then use Focus on branch to hide every unrelated step.\">Select a step to highlight what it waits on and what waits on it.</EmptyDetails>\n        )\n      }\n      {...props}\n    >\n      {view === \"graph\" ? (\n        <GraphCanvas\n          label={`${title}: ${shownNodes.length} steps. Use the list view to move through steps with the keyboard.`}\n          nodes={canvasNodes}\n          edges={canvasEdges}\n          layout={layout}\n          selectedNodeId={selected}\n          onNodeClick={select}\n          onPaneClick={() => setSelected(null)}\n          height={height}\n        />\n      ) : (\n        <div style={{ height }} className=\"overflow-y-auto\">\n          <GraphListbox\n            label={`${title} steps`}\n            items={shownNodes}\n            getId={(node) => node.id}\n            selectedId={selected}\n            onSelect={select}\n            empty=\"No steps yet.\"\n            renderItem={(node) => {\n              const relation = !selected || node.id === selected ? null : upstream.has(node.id) ? \"Upstream\" : downstream.has(node.id) ? \"Downstream\" : null;\n              const waits = waiting.get(node.id) ?? [];\n              const needs = links.incoming.get(node.id) ?? [];\n              return (\n                <GraphListRow\n                  status={node.status}\n                  title={node.label}\n                  muted={node.status === \"queued\" || node.status === \"skipped\" || node.status === \"cancelled\"}\n                  struck={node.status === \"skipped\"}\n                  trailing={\n                    relation ? (\n                      <span className=\"inline-block rounded-[5px] border border-zinc-950/15 px-1.5 font-sans text-[10px] leading-4 font-medium text-zinc-700 dark:border-white/20 dark:text-zinc-300\">\n                        {relation}\n                      </span>\n                    ) : undefined\n                  }\n                  meta={\n                    <>\n                      {GRAPH_STATUS_LABELS[node.status]}\n                      {node.kind ? ` · ${node.kind}` : \"\"}\n                      {needs.length ? ` · depends on ${nameList(needs.map((edge) => label(edge.source) + (edge.type === \"optional\" ? \" (optional)\" : \"\")))}` : \"\"}\n                    </>\n                  }\n                >\n                  {waits.length ? <p className=\"mt-0.5 truncate text-xs text-amber-700 dark:text-amber-400\">Waiting on {nameList(waits.map(label))}</p> : null}\n                </GraphListRow>\n              );\n            }}\n          />\n        </div>\n      )}\n    </GraphShell>\n  );\n}\n"
    }
  ],
  "example": {
    "content": "import { DependencyGraph, type DependencyEdge, type DependencyNode } from \"@/components/seamless/graph/dependency-graph\";\n\nconst nodes: DependencyNode[] = [\n  { id: \"schema\", label: \"Design the schema\", kind: \"design\", status: \"succeeded\" },\n  { id: \"migration\", label: \"Write the migration\", kind: \"code\", status: \"running\" },\n  { id: \"api\", label: \"Update the API\", kind: \"code\", status: \"failed\" },\n  { id: \"docs\", label: \"Update the docs\", kind: \"docs\", status: \"queued\" },\n  { id: \"tests\", label: \"Write tests\", kind: \"test\", status: \"queued\" },\n  { id: \"deploy\", label: \"Deploy\", kind: \"release\", status: \"queued\" },\n];\n\n// target waits on source.\nconst edges: DependencyEdge[] = [\n  { id: \"e1\", source: \"schema\", target: \"migration\", type: \"requires\" },\n  { id: \"e2\", source: \"schema\", target: \"api\", type: \"requires\" },\n  { id: \"e3\", source: \"api\", target: \"docs\", type: \"optional\" },\n  { id: \"e4\", source: \"migration\", target: \"tests\", type: \"requires\" },\n  { id: \"e5\", source: \"api\", target: \"tests\", type: \"requires\" },\n  { id: \"e6\", source: \"tests\", target: \"deploy\", type: \"requires\" },\n];\n\nexport default function DependencyGraphExample() {\n  return <DependencyGraph title=\"Invoice feature\" nodes={nodes} edges={edges} defaultSelectedId=\"api\" height={360} />;\n}\n"
  }
}
