{
  "schemaVersion": 1,
  "target": "react",
  "name": "run-graph",
  "version": "0.2.0",
  "category": "agent",
  "title": "Run Graph",
  "description": "A run's plan, parallel tool calls, approvals, and results as a graph, with zoom, pan, fit-to-view, a details panel, and a keyboard list view with the same information. Budget at 100 nodes and 150 edges: first render under 150 ms and selection under 50 ms on a current laptop; under 500 ms and 200 ms at 4x CPU slowdown.",
  "tags": [
    "agent",
    "graph",
    "run",
    "tool-calls",
    "approval",
    "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/run-graph",
  "props": [
    {
      "name": "nodes",
      "type": "{ id, label, kind: \"plan\" | \"step\" | \"tool\" | \"approval\" | \"result\", status, summary?, input?, output?, error?, durationMs? }[]",
      "description": "Steps with stable ids. Changing a status never moves a node; adding or removing nodes re-runs the layout."
    },
    {
      "name": "edges",
      "type": "{ id, source, target, type: \"sequence\" | \"parallel\" | \"approval\" | \"result\", label? }[]",
      "description": "Explicitly typed connections. Approval and result edges are dashed."
    },
    {
      "name": "selectedId / onSelect",
      "type": "string | null / (id: string | null) => void",
      "description": "Controlled selection shared by the graph, the list, and the details panel. Leave selectedId undefined for uncontrolled use."
    },
    {
      "name": "defaultSelectedId",
      "type": "string | null",
      "default": "null",
      "description": "Initial selection when uncontrolled."
    },
    {
      "name": "defaultView",
      "type": "\"graph\" | \"list\"",
      "default": "\"graph\"",
      "description": "Which view shows first. Both share selection."
    },
    {
      "name": "height",
      "type": "number",
      "default": "440",
      "description": "Height of the graph and list in pixels. React Flow needs a sized container."
    },
    {
      "name": "direction",
      "type": "\"LR\" | \"TB\"",
      "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."
    },
    {
      "name": "title",
      "type": "string",
      "default": "\"Run graph\"",
      "description": "Heading and accessible name."
    }
  ],
  "accessibility": [
    "The list view has the same steps as the graph, with status, kind, and connections in text. It is a listbox with one tab stop: arrow keys, Home, and End move the selection, and the graph follows.",
    "Statuses are shown with an icon and text on every node and list row, never color alone. The selected step has a solid foreground border and ring; its connections are drawn brighter.",
    "Zoom in, zoom out, and fit-to-view are labelled buttons; with reduced motion the viewport jumps instead of animating and edge dashes never move. Edges into a running step are drawn in the running color instead of animating.",
    "Related steps in the details panel are buttons, so you can walk the graph from the keyboard.",
    "Performance budget, measured in Chrome on the 100-node, 150-edge fixture: first render of the graph within 150 ms (500 ms at 4x CPU slowdown), click or arrow key to updated details within 50 ms (200 ms at 4x), and no node movement on status-only updates."
  ],
  "states": [
    "queued",
    "running",
    "waiting-for-approval",
    "succeeded",
    "failed",
    "skipped",
    "cancelled"
  ],
  "files": [
    {
      "path": "graph/run-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, GRAPH_STATUS_LABELS, type GraphDirection, type GraphStatus, layoutEdges } from \"./graph-layout\";\nimport {\n  DetailHeader,\n  DetailNote,\n  DetailSection,\n  DetailStack,\n  EdgeLegend,\n  EmptyDetails,\n  ErrorNote,\n  formatDuration,\n  GraphListbox,\n  GraphListRow,\n  GraphShell,\n  type GraphView,\n  GraphViewSwitch,\n  nameList,\n  RelatedButton,\n  StatusCounts,\n  useControllableState,\n} from \"./graph-ui\";\n\nexport type RunGraphNodeKind = \"plan\" | \"step\" | \"tool\" | \"approval\" | \"result\";\n/** sequence: runs after; parallel: one of several calls started together; approval: gated by a decision; result: output passed on. */\nexport type RunGraphEdgeType = \"sequence\" | \"parallel\" | \"approval\" | \"result\";\n\nexport interface RunGraphNode {\n  /** Stable id. Keep it the same across updates so the node keeps its place. */\n  id: string;\n  /** Tool name for tool calls, or a short label. */\n  label: string;\n  kind: RunGraphNodeKind;\n  status: GraphStatus;\n  summary?: string;\n  /** Tool arguments, rendered as formatted JSON. */\n  input?: unknown;\n  /** Result text, rendered as text, never as HTML. */\n  output?: string;\n  error?: string;\n  durationMs?: number;\n}\n\nexport interface RunGraphEdge {\n  id: string;\n  source: string;\n  target: string;\n  type: RunGraphEdgeType;\n  label?: string;\n}\n\nexport interface RunGraphProps extends Omit<ComponentProps<\"section\">, \"children\" | \"onSelect\" | \"title\"> {\n  nodes: readonly RunGraphNode[];\n  edges: readonly RunGraphEdge[];\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  defaultView?: GraphView;\n  /** Height of the graph and list area in pixels. */\n  height?: number;\n  direction?: GraphDirection;\n}\n\nexport const RUN_GRAPH_KIND_LABELS: Record<RunGraphNodeKind, string> = {\n  plan: \"Plan\",\n  step: \"Step\",\n  tool: \"Tool call\",\n  approval: \"Approval\",\n  result: \"Result\",\n};\n\nexport const RUN_GRAPH_EDGE_LABELS: Record<RunGraphEdgeType, string> = {\n  sequence: \"Sequence\",\n  parallel: \"Parallel\",\n  approval: \"Approval gate\",\n  result: \"Result\",\n};\n\nconst dashedTypes = new Set<RunGraphEdgeType>([\"approval\", \"result\"]);\n\n/** A run's plan, parallel tool calls, approvals, and results as a graph, with a matching list and a details panel. */\nexport function RunGraph({\n  nodes,\n  edges,\n  title = \"Run graph\",\n  selectedId,\n  defaultSelectedId = null,\n  onSelect,\n  defaultView = \"graph\",\n  height = 440,\n  direction = \"TB\",\n  ...props\n}: RunGraphProps) {\n  const [selected, setSelected] = useControllableState<string | null>(selectedId, defaultSelectedId, onSelect);\n  const [view, setView] = useState<GraphView>(defaultView);\n  const shownEdges = useMemo(() => layoutEdges(nodes, edges), [nodes, edges]);\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 current = selected ? byId.get(selected) : undefined;\n\n  const canvasNodes = useMemo<GraphCanvasNode[]>(\n    () =>\n      nodes.map((node) => ({\n        id: node.id,\n        title: node.label,\n        subtitle: RUN_GRAPH_KIND_LABELS[node.kind],\n        mono: node.kind === \"tool\",\n        status: node.status,\n      })),\n    [nodes],\n  );\n  const canvasEdges = useMemo<GraphCanvasEdge[]>(\n    () =>\n      shownEdges.map((edge) => ({\n        id: edge.id,\n        source: edge.source,\n        target: edge.target,\n        type: edge.type,\n        label: edge.label,\n        dashed: dashedTypes.has(edge.type),\n        animated: byId.get(edge.target)?.status === \"running\",\n        tone: selected && (edge.source === selected || edge.target === selected) ? \"highlight\" : \"default\",\n      })),\n    [shownEdges, byId, selected],\n  );\n\n  const select = (id: string) => setSelected(id);\n  const label = (id: string) => byId.get(id)?.label ?? id;\n\n  // Rows do not depend on the selection, so reuse them: selecting re-renders only the option wrappers.\n  const rows = useMemo(() => {\n    const name = (id: string) => byId.get(id)?.label ?? id;\n    return new Map(\n      nodes.map((node) => {\n        const before = links.incoming.get(node.id) ?? [];\n        const after = links.outgoing.get(node.id) ?? [];\n        return [\n          node.id,\n          <GraphListRow\n            status={node.status}\n            title={node.label}\n            mono={node.kind === \"tool\"}\n            muted={node.status === \"queued\" || node.status === \"skipped\" || node.status === \"cancelled\"}\n            struck={node.status === \"skipped\"}\n            trailing={node.durationMs != null ? formatDuration(node.durationMs) : undefined}\n            meta={\n              <>\n                {GRAPH_STATUS_LABELS[node.status]} · {RUN_GRAPH_KIND_LABELS[node.kind]}\n                {before.length ? ` · after ${nameList(before.map((edge) => name(edge.source)))}` : \"\"}\n                {after.length ? ` · then ${nameList(after.map((edge) => name(edge.target)))}` : \"\"}\n              </>\n            }\n          />,\n        ] as const;\n      }),\n    );\n  }, [nodes, links, byId]);\n\n  return (\n    <GraphShell\n      aria-label={title}\n      title={title}\n      meta={<StatusCounts lead={`${nodes.length} ${nodes.length === 1 ? \"step\" : \"steps\"}`} statuses={nodes.map((node) => node.status)} />}\n      controls={<GraphViewSwitch view={view} onChange={setView} />}\n      detailsLabel=\"Step details\"\n      height={height}\n      footer={<EdgeLegend items={[{ label: \"Sequence or parallel\" }, { label: \"Approval gate or result\", dashed: true }]} />}\n      details={\n        current ? (\n          <DetailStack>\n            <DetailHeader\n              title={current.label}\n              mono={current.kind === \"tool\"}\n              status={current.status}\n              meta={[\n                RUN_GRAPH_KIND_LABELS[current.kind],\n                current.durationMs != null ? <span className=\"font-mono tabular-nums\">{formatDuration(current.durationMs)}</span> : null,\n              ]}\n            />\n            {current.summary || (current.status === \"failed\" && current.error) ? (\n              <div className=\"space-y-3\">\n                {current.summary ? <p className=\"text-sm text-pretty text-zinc-600 dark:text-zinc-400\">{current.summary}</p> : null}\n                {current.status === \"failed\" && current.error ? <ErrorNote>{current.error}</ErrorNote> : null}\n              </div>\n            ) : null}\n            {current.input !== undefined || current.output ? (\n              <div className=\"space-y-3.5\">\n                {current.input !== undefined ? <DetailSection label=\"Input\">{JSON.stringify(current.input, null, 2)}</DetailSection> : null}\n                {current.output ? <DetailSection label=\"Output\">{current.output}</DetailSection> : null}\n              </div>\n            ) : null}\n            <div className=\"space-y-3.5\">\n              <DetailSection label=\"Comes after\">\n                {links.incoming.get(current.id)?.length ? (\n                  links.incoming.get(current.id)!.map((edge) => (\n                    <RelatedButton\n                      key={edge.id}\n                      label={label(edge.source)}\n                      mono={byId.get(edge.source)?.kind === \"tool\"}\n                      status={byId.get(edge.source)?.status}\n                      note={RUN_GRAPH_EDGE_LABELS[edge.type]}\n                      onClick={() => select(edge.source)}\n                    />\n                  ))\n                ) : (\n                  <DetailNote>Nothing. This is a starting step.</DetailNote>\n                )}\n              </DetailSection>\n              <DetailSection label=\"Leads to\">\n                {links.outgoing.get(current.id)?.length ? (\n                  links.outgoing.get(current.id)!.map((edge) => (\n                    <RelatedButton\n                      key={edge.id}\n                      label={label(edge.target)}\n                      mono={byId.get(edge.target)?.kind === \"tool\"}\n                      status={byId.get(edge.target)?.status}\n                      note={RUN_GRAPH_EDGE_LABELS[edge.type]}\n                      onClick={() => select(edge.target)}\n                    />\n                  ))\n                ) : (\n                  <DetailNote>Nothing. This is a final step.</DetailNote>\n                )}\n              </DetailSection>\n            </div>\n          </DetailStack>\n        ) : (\n          <EmptyDetails hint=\"In the list view, arrow keys, Home, and End move the selection.\">\n            Select a step in the graph or the list to see its status, input, output, and connections.\n          </EmptyDetails>\n        )\n      }\n      {...props}\n    >\n      {view === \"graph\" ? (\n        <GraphCanvas\n          label={`${title}: ${nodes.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={nodes}\n            getId={(node) => node.id}\n            selectedId={selected}\n            onSelect={select}\n            empty=\"No steps yet.\"\n            renderItem={(node) => rows.get(node.id)}\n          />\n        </div>\n      )}\n    </GraphShell>\n  );\n}\n"
    }
  ],
  "example": {
    "content": "import { RunGraph, type RunGraphEdge, type RunGraphNode } from \"@/components/seamless/graph/run-graph\";\n\nconst nodes: RunGraphNode[] = [\n  { id: \"plan\", label: \"Plan the update\", kind: \"plan\", status: \"succeeded\" },\n  { id: \"search\", label: \"search_docs\", kind: \"tool\", status: \"succeeded\", input: { query: \"rate limits\" }, output: \"Found 3 pages\", durationMs: 640 },\n  { id: \"fetch\", label: \"fetch_page\", kind: \"tool\", status: \"failed\", error: \"504 Gateway Timeout\", durationMs: 10000 },\n  { id: \"draft\", label: \"Draft the edits\", kind: \"step\", status: \"succeeded\" },\n  { id: \"approve\", label: \"Apply edits to 3 files?\", kind: \"approval\", status: \"waiting-for-approval\" },\n  { id: \"pr\", label: \"open_pull_request\", kind: \"tool\", status: \"queued\" },\n];\n\nconst edges: RunGraphEdge[] = [\n  { id: \"e1\", source: \"plan\", target: \"search\", type: \"parallel\" },\n  { id: \"e2\", source: \"plan\", target: \"fetch\", type: \"parallel\" },\n  { id: \"e3\", source: \"search\", target: \"draft\", type: \"result\" },\n  { id: \"e4\", source: \"fetch\", target: \"draft\", type: \"result\" },\n  { id: \"e5\", source: \"draft\", target: \"approve\", type: \"approval\" },\n  { id: \"e6\", source: \"approve\", target: \"pr\", type: \"sequence\" },\n];\n\nexport default function RunGraphExample() {\n  // Pass selectedId and onSelect to control the selection from your own state.\n  return <RunGraph title=\"Update the rate-limit docs\" nodes={nodes} edges={edges} defaultSelectedId=\"fetch\" height={360} />;\n}\n"
  }
}
