@principal-ai/subsystems-react 0.20.6 → 0.21.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (270) hide show
  1. package/README.md +1 -1
  2. package/dist/graphify/anchor.d.ts +17 -0
  3. package/dist/graphify/anchor.d.ts.map +1 -1
  4. package/dist/graphify/anchor.js +41 -0
  5. package/dist/graphify/anchor.js.map +1 -1
  6. package/dist/graphify/consolidated.d.ts +40 -1
  7. package/dist/graphify/consolidated.d.ts.map +1 -1
  8. package/dist/index.d.ts +5 -5
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +2 -2
  11. package/dist/index.js.map +1 -1
  12. package/dist/pierre/{PierreThroughlineCodeView.d.ts → PierreWalkthroughCodeView.d.ts} +7 -7
  13. package/dist/pierre/{PierreThroughlineCodeView.d.ts.map → PierreWalkthroughCodeView.d.ts.map} +1 -1
  14. package/dist/pierre/{PierreThroughlineCodeView.js → PierreWalkthroughCodeView.js} +28 -36
  15. package/dist/pierre/PierreWalkthroughCodeView.js.map +1 -0
  16. package/dist/pierre/index.d.ts +2 -2
  17. package/dist/pierre/index.js +1 -1
  18. package/dist/pierre/pierreFileLang.d.ts +1 -1
  19. package/dist/pierre/pierreFileLang.js +1 -1
  20. package/dist/stories/Subsystem/ComponentGraph/fixtures.d.ts +14 -2
  21. package/dist/stories/Subsystem/ComponentGraph/fixtures.d.ts.map +1 -1
  22. package/dist/stories/Subsystem/ComponentGraph/fixtures.js +86 -20
  23. package/dist/stories/Subsystem/ComponentGraph/fixtures.js.map +1 -1
  24. package/dist/subsystem/FileDrawer.d.ts +2 -2
  25. package/dist/subsystem/FileDrawer.js +2 -2
  26. package/dist/subsystem/SubsystemComponentGraph.d.ts +25 -24
  27. package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
  28. package/dist/subsystem/SubsystemComponentGraph.js +204 -199
  29. package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
  30. package/dist/subsystem/formatDeclaration.d.ts +2 -1
  31. package/dist/subsystem/formatDeclaration.d.ts.map +1 -1
  32. package/dist/subsystem/formatDeclaration.js +57 -33
  33. package/dist/subsystem/formatDeclaration.js.map +1 -1
  34. package/dist/subsystem/model.d.ts +85 -35
  35. package/dist/subsystem/model.d.ts.map +1 -1
  36. package/dist/subsystem/model.js +85 -6
  37. package/dist/subsystem/model.js.map +1 -1
  38. package/dist/subsystem/nodes.d.ts.map +1 -1
  39. package/dist/subsystem/nodes.js +30 -13
  40. package/dist/subsystem/nodes.js.map +1 -1
  41. package/dist/subsystem/tokenizeComponent.js +2 -2
  42. package/dist/subsystem/tokenizeComponent.js.map +1 -1
  43. package/package.json +3 -3
  44. package/src/graphify/anchor.test.ts +31 -2
  45. package/src/graphify/anchor.ts +46 -0
  46. package/src/graphify/consolidated.ts +43 -1
  47. package/src/index.ts +13 -5
  48. package/src/pierre/{PierreThroughlineCodeView.tsx → PierreWalkthroughCodeView.tsx} +39 -47
  49. package/src/pierre/index.ts +2 -2
  50. package/src/pierre/pierreFileLang.ts +1 -1
  51. package/src/stories/Pierre/CodeView.stories.tsx +3 -3
  52. package/src/stories/Subsystem/ComponentDeclarationAudit.stories.tsx +14 -14
  53. package/src/stories/Subsystem/ComponentGraph/Appearance.stories.tsx +13 -15
  54. package/src/stories/Subsystem/ComponentGraph/Basics.stories.tsx +8 -8
  55. package/src/stories/Subsystem/ComponentGraph/Captures.stories.tsx +14 -14
  56. package/src/stories/Subsystem/ComponentGraph/CustomEntities.stories.tsx +11 -10
  57. package/src/stories/Subsystem/ComponentGraph/DetailPanel.stories.tsx +13 -14
  58. package/src/stories/Subsystem/ComponentGraph/Flows.stories.tsx +31 -66
  59. package/src/stories/Subsystem/ComponentGraph/FrameworkStereotype.stories.tsx +2 -2
  60. package/src/stories/Subsystem/ComponentGraph/GraphTitle.stories.tsx +4 -4
  61. package/src/stories/Subsystem/ComponentGraph/Processes.stories.tsx +3 -3
  62. package/src/stories/Subsystem/ComponentGraph/Proposed.stories.tsx +1 -1
  63. package/src/stories/Subsystem/ComponentGraph/Scenarios.stories.tsx +518 -33
  64. package/src/stories/Subsystem/ComponentGraph/Spotlights.stories.tsx +13 -13
  65. package/src/stories/Subsystem/ComponentGraph/fixtures.ts +105 -21
  66. package/src/subsystem/FileDrawer.tsx +2 -2
  67. package/src/subsystem/SubsystemComponentGraph.tsx +268 -259
  68. package/src/subsystem/formatDeclaration.test.ts +76 -1
  69. package/src/subsystem/formatDeclaration.ts +57 -33
  70. package/src/subsystem/model.test.ts +27 -15
  71. package/src/subsystem/model.ts +175 -45
  72. package/src/subsystem/nodes.tsx +47 -13
  73. package/src/subsystem/tokenizeComponent.ts +2 -2
  74. package/dist/components/ConfigurationSelector.d.ts +0 -35
  75. package/dist/components/ConfigurationSelector.d.ts.map +0 -1
  76. package/dist/components/ConfigurationSelector.js +0 -63
  77. package/dist/components/ConfigurationSelector.js.map +0 -1
  78. package/dist/components/GraphRenderer.d.ts +0 -295
  79. package/dist/components/GraphRenderer.d.ts.map +0 -1
  80. package/dist/components/GraphRenderer.js +0 -2008
  81. package/dist/components/GraphRenderer.js.map +0 -1
  82. package/dist/components/MultiCanvasRenderer.d.ts +0 -75
  83. package/dist/components/MultiCanvasRenderer.d.ts.map +0 -1
  84. package/dist/components/MultiCanvasRenderer.js +0 -203
  85. package/dist/components/MultiCanvasRenderer.js.map +0 -1
  86. package/dist/components/NodeTooltip.d.ts +0 -24
  87. package/dist/components/NodeTooltip.d.ts.map +0 -1
  88. package/dist/components/NodeTooltip.js +0 -160
  89. package/dist/components/NodeTooltip.js.map +0 -1
  90. package/dist/components/SelectionSidebar.d.ts +0 -18
  91. package/dist/components/SelectionSidebar.d.ts.map +0 -1
  92. package/dist/components/SelectionSidebar.js +0 -190
  93. package/dist/components/SelectionSidebar.js.map +0 -1
  94. package/dist/components/SequenceDiagramRenderer.d.ts +0 -77
  95. package/dist/components/SequenceDiagramRenderer.d.ts.map +0 -1
  96. package/dist/components/SequenceDiagramRenderer.js +0 -604
  97. package/dist/components/SequenceDiagramRenderer.js.map +0 -1
  98. package/dist/components/WorkflowSequenceDiagram.d.ts +0 -63
  99. package/dist/components/WorkflowSequenceDiagram.d.ts.map +0 -1
  100. package/dist/components/WorkflowSequenceDiagram.js +0 -147
  101. package/dist/components/WorkflowSequenceDiagram.js.map +0 -1
  102. package/dist/components/dashboard/DashboardRenderer.d.ts +0 -10
  103. package/dist/components/dashboard/DashboardRenderer.d.ts.map +0 -1
  104. package/dist/components/dashboard/DashboardRenderer.js +0 -179
  105. package/dist/components/dashboard/DashboardRenderer.js.map +0 -1
  106. package/dist/components/dashboard/MetricPanel.d.ts +0 -10
  107. package/dist/components/dashboard/MetricPanel.d.ts.map +0 -1
  108. package/dist/components/dashboard/MetricPanel.js +0 -103
  109. package/dist/components/dashboard/MetricPanel.js.map +0 -1
  110. package/dist/components/dashboard/MockDataProvider.d.ts +0 -30
  111. package/dist/components/dashboard/MockDataProvider.d.ts.map +0 -1
  112. package/dist/components/dashboard/MockDataProvider.js +0 -270
  113. package/dist/components/dashboard/MockDataProvider.js.map +0 -1
  114. package/dist/components/dashboard/components/BarChart.d.ts +0 -10
  115. package/dist/components/dashboard/components/BarChart.d.ts.map +0 -1
  116. package/dist/components/dashboard/components/BarChart.js +0 -167
  117. package/dist/components/dashboard/components/BarChart.js.map +0 -1
  118. package/dist/components/dashboard/components/LineChart.d.ts +0 -10
  119. package/dist/components/dashboard/components/LineChart.d.ts.map +0 -1
  120. package/dist/components/dashboard/components/LineChart.js +0 -141
  121. package/dist/components/dashboard/components/LineChart.js.map +0 -1
  122. package/dist/components/dashboard/components/MetricCard.d.ts +0 -9
  123. package/dist/components/dashboard/components/MetricCard.d.ts.map +0 -1
  124. package/dist/components/dashboard/components/MetricCard.js +0 -163
  125. package/dist/components/dashboard/components/MetricCard.js.map +0 -1
  126. package/dist/components/dashboard/components/SourceLink.d.ts +0 -9
  127. package/dist/components/dashboard/components/SourceLink.d.ts.map +0 -1
  128. package/dist/components/dashboard/components/SourceLink.js +0 -39
  129. package/dist/components/dashboard/components/SourceLink.js.map +0 -1
  130. package/dist/components/dashboard/components/TimeRangeSelector.d.ts +0 -9
  131. package/dist/components/dashboard/components/TimeRangeSelector.d.ts.map +0 -1
  132. package/dist/components/dashboard/components/TimeRangeSelector.js +0 -167
  133. package/dist/components/dashboard/components/TimeRangeSelector.js.map +0 -1
  134. package/dist/components/dashboard/components/index.d.ts +0 -6
  135. package/dist/components/dashboard/components/index.d.ts.map +0 -1
  136. package/dist/components/dashboard/components/index.js +0 -6
  137. package/dist/components/dashboard/components/index.js.map +0 -1
  138. package/dist/components/dashboard/index.d.ts +0 -6
  139. package/dist/components/dashboard/index.d.ts.map +0 -1
  140. package/dist/components/dashboard/index.js +0 -8
  141. package/dist/components/dashboard/index.js.map +0 -1
  142. package/dist/components/dashboard/types.d.ts +0 -74
  143. package/dist/components/dashboard/types.d.ts.map +0 -1
  144. package/dist/components/dashboard/types.js +0 -8
  145. package/dist/components/dashboard/types.js.map +0 -1
  146. package/dist/components/state-view/PipelineView.d.ts +0 -14
  147. package/dist/components/state-view/PipelineView.d.ts.map +0 -1
  148. package/dist/components/state-view/PipelineView.js +0 -195
  149. package/dist/components/state-view/PipelineView.js.map +0 -1
  150. package/dist/components/state-view/index.d.ts +0 -14
  151. package/dist/components/state-view/index.d.ts.map +0 -1
  152. package/dist/components/state-view/index.js +0 -12
  153. package/dist/components/state-view/index.js.map +0 -1
  154. package/dist/components/state-view/types.d.ts +0 -188
  155. package/dist/components/state-view/types.d.ts.map +0 -1
  156. package/dist/components/state-view/types.js +0 -10
  157. package/dist/components/state-view/types.js.map +0 -1
  158. package/dist/components/state-view/useStateView.d.ts +0 -32
  159. package/dist/components/state-view/useStateView.d.ts.map +0 -1
  160. package/dist/components/state-view/useStateView.js +0 -129
  161. package/dist/components/state-view/useStateView.js.map +0 -1
  162. package/dist/contexts/GraphEditContext.d.ts +0 -20
  163. package/dist/contexts/GraphEditContext.d.ts.map +0 -1
  164. package/dist/contexts/GraphEditContext.js +0 -8
  165. package/dist/contexts/GraphEditContext.js.map +0 -1
  166. package/dist/contexts/TooltipPortalContext.d.ts +0 -8
  167. package/dist/contexts/TooltipPortalContext.d.ts.map +0 -1
  168. package/dist/contexts/TooltipPortalContext.js +0 -8
  169. package/dist/contexts/TooltipPortalContext.js.map +0 -1
  170. package/dist/edges/CustomEdge.d.ts +0 -24
  171. package/dist/edges/CustomEdge.d.ts.map +0 -1
  172. package/dist/edges/CustomEdge.js +0 -222
  173. package/dist/edges/CustomEdge.js.map +0 -1
  174. package/dist/edges/GenericEdge.d.ts +0 -18
  175. package/dist/edges/GenericEdge.d.ts.map +0 -1
  176. package/dist/edges/GenericEdge.js +0 -10
  177. package/dist/edges/GenericEdge.js.map +0 -1
  178. package/dist/graphify/kind.d.ts +0 -23
  179. package/dist/graphify/kind.d.ts.map +0 -1
  180. package/dist/graphify/kind.js +0 -77
  181. package/dist/graphify/kind.js.map +0 -1
  182. package/dist/hooks/usePathBasedEvents.d.ts +0 -42
  183. package/dist/hooks/usePathBasedEvents.d.ts.map +0 -1
  184. package/dist/hooks/usePathBasedEvents.js +0 -119
  185. package/dist/hooks/usePathBasedEvents.js.map +0 -1
  186. package/dist/hooks/useSequenceLayout.d.ts +0 -186
  187. package/dist/hooks/useSequenceLayout.d.ts.map +0 -1
  188. package/dist/hooks/useSequenceLayout.js +0 -327
  189. package/dist/hooks/useSequenceLayout.js.map +0 -1
  190. package/dist/hooks/useUndoRedo.d.ts +0 -80
  191. package/dist/hooks/useUndoRedo.d.ts.map +0 -1
  192. package/dist/hooks/useUndoRedo.js +0 -70
  193. package/dist/hooks/useUndoRedo.js.map +0 -1
  194. package/dist/nodes/CustomNode.d.ts +0 -28
  195. package/dist/nodes/CustomNode.d.ts.map +0 -1
  196. package/dist/nodes/CustomNode.js +0 -820
  197. package/dist/nodes/CustomNode.js.map +0 -1
  198. package/dist/nodes/GenericNode.d.ts +0 -20
  199. package/dist/nodes/GenericNode.d.ts.map +0 -1
  200. package/dist/nodes/GenericNode.js +0 -20
  201. package/dist/nodes/GenericNode.js.map +0 -1
  202. package/dist/nodes/otel/OtelBoundaryNode.d.ts +0 -55
  203. package/dist/nodes/otel/OtelBoundaryNode.d.ts.map +0 -1
  204. package/dist/nodes/otel/OtelBoundaryNode.js +0 -81
  205. package/dist/nodes/otel/OtelBoundaryNode.js.map +0 -1
  206. package/dist/nodes/otel/OtelEventNode.d.ts +0 -59
  207. package/dist/nodes/otel/OtelEventNode.d.ts.map +0 -1
  208. package/dist/nodes/otel/OtelEventNode.js +0 -82
  209. package/dist/nodes/otel/OtelEventNode.js.map +0 -1
  210. package/dist/nodes/otel/OtelResourceNode.d.ts +0 -53
  211. package/dist/nodes/otel/OtelResourceNode.d.ts.map +0 -1
  212. package/dist/nodes/otel/OtelResourceNode.js +0 -105
  213. package/dist/nodes/otel/OtelResourceNode.js.map +0 -1
  214. package/dist/nodes/otel/OtelScopeNode.d.ts +0 -53
  215. package/dist/nodes/otel/OtelScopeNode.d.ts.map +0 -1
  216. package/dist/nodes/otel/OtelScopeNode.js +0 -81
  217. package/dist/nodes/otel/OtelScopeNode.js.map +0 -1
  218. package/dist/nodes/otel/OtelSpanConventionNode.d.ts +0 -61
  219. package/dist/nodes/otel/OtelSpanConventionNode.d.ts.map +0 -1
  220. package/dist/nodes/otel/OtelSpanConventionNode.js +0 -131
  221. package/dist/nodes/otel/OtelSpanConventionNode.js.map +0 -1
  222. package/dist/nodes/otel/index.d.ts +0 -17
  223. package/dist/nodes/otel/index.d.ts.map +0 -1
  224. package/dist/nodes/otel/index.js +0 -13
  225. package/dist/nodes/otel/index.js.map +0 -1
  226. package/dist/nodes/otel/shared/NodeBadges.d.ts +0 -10
  227. package/dist/nodes/otel/shared/NodeBadges.d.ts.map +0 -1
  228. package/dist/nodes/otel/shared/NodeBadges.js +0 -178
  229. package/dist/nodes/otel/shared/NodeBadges.js.map +0 -1
  230. package/dist/nodes/otel/shared/NodeContent.d.ts +0 -10
  231. package/dist/nodes/otel/shared/NodeContent.d.ts.map +0 -1
  232. package/dist/nodes/otel/shared/NodeContent.js +0 -96
  233. package/dist/nodes/otel/shared/NodeContent.js.map +0 -1
  234. package/dist/nodes/otel/shared/index.d.ts +0 -8
  235. package/dist/nodes/otel/shared/index.d.ts.map +0 -1
  236. package/dist/nodes/otel/shared/index.js +0 -8
  237. package/dist/nodes/otel/shared/index.js.map +0 -1
  238. package/dist/nodes/otel/shared/types.d.ts +0 -129
  239. package/dist/nodes/otel/shared/types.d.ts.map +0 -1
  240. package/dist/nodes/otel/shared/types.js +0 -5
  241. package/dist/nodes/otel/shared/types.js.map +0 -1
  242. package/dist/nodes/otel/shared/useNodeBehavior.d.ts +0 -42
  243. package/dist/nodes/otel/shared/useNodeBehavior.d.ts.map +0 -1
  244. package/dist/nodes/otel/shared/useNodeBehavior.js +0 -61
  245. package/dist/nodes/otel/shared/useNodeBehavior.js.map +0 -1
  246. package/dist/pierre/PierreThroughlineCodeView.js.map +0 -1
  247. package/dist/subsystem/SubsystemCarousel.d.ts +0 -36
  248. package/dist/subsystem/SubsystemCarousel.d.ts.map +0 -1
  249. package/dist/subsystem/SubsystemCarousel.js +0 -163
  250. package/dist/subsystem/SubsystemCarousel.js.map +0 -1
  251. package/dist/utils/animationMapping.d.ts +0 -53
  252. package/dist/utils/animationMapping.d.ts.map +0 -1
  253. package/dist/utils/animationMapping.js +0 -126
  254. package/dist/utils/animationMapping.js.map +0 -1
  255. package/dist/utils/canvasBounds.d.ts +0 -50
  256. package/dist/utils/canvasBounds.d.ts.map +0 -1
  257. package/dist/utils/canvasBounds.js +0 -88
  258. package/dist/utils/canvasBounds.js.map +0 -1
  259. package/dist/utils/graphConverter.d.ts +0 -18
  260. package/dist/utils/graphConverter.d.ts.map +0 -1
  261. package/dist/utils/graphConverter.js +0 -114
  262. package/dist/utils/graphConverter.js.map +0 -1
  263. package/dist/utils/iconResolver.d.ts +0 -29
  264. package/dist/utils/iconResolver.d.ts.map +0 -1
  265. package/dist/utils/iconResolver.js +0 -30
  266. package/dist/utils/iconResolver.js.map +0 -1
  267. package/dist/utils/orientationUtils.d.ts +0 -19
  268. package/dist/utils/orientationUtils.d.ts.map +0 -1
  269. package/dist/utils/orientationUtils.js +0 -62
  270. package/dist/utils/orientationUtils.js.map +0 -1
@@ -36,13 +36,16 @@ import { ChevronDown, ChevronUp, Pause, Play, X } from 'lucide-react';
36
36
  import { IndustryMarkdownSlide } from 'themed-markdown';
37
37
  import {
38
38
  buildSubsystemGraph,
39
+ deriveGraphEdges,
39
40
  MECHANISM_COLOR,
40
41
  MECHANISM_DESCRIPTIONS,
41
42
  subsystemGraphLayoutKey,
43
+ walkthroughStepGraphEdgeId,
42
44
  type SubsystemComponentEdge,
43
45
  type SubsystemComponent,
44
46
  type SubsystemEdgeMechanism,
45
- type SubsystemThroughline,
47
+ type SubsystemRelation,
48
+ type SubsystemWalkthrough,
46
49
  } from './model';
47
50
  import type { SubsystemOpenFileOptions } from './declarationRef';
48
51
  import { SubsystemComponentNode, SubsystemGroupNode, SubsystemEdge, SUBSYSTEM_CALLBACKS, hexWithAlpha, EDGE_DIM_ALPHA, fileMatchForNode, flowElementVisibility } from './nodes';
@@ -58,33 +61,34 @@ const EDGE_LABEL_MAX_EDGE_FRACTION = 0.55;
58
61
  /** Rough monospace width at fontSize 10 + horizontal padding/border. */
59
62
  const EDGE_LABEL_CHAR_PX = 6.2;
60
63
  const EDGE_LABEL_PAD_PX = 18;
61
- /** Pause (ms) between steps when a throughline autoplays. */
62
- const THROUGHLINE_PLAY_PAUSE_MS = 2500;
64
+ /** Pause (ms) between steps when a walkthrough autoplays. */
65
+ const WALKTHROUGH_PLAY_PAUSE_MS = 2500;
63
66
 
64
- /** Context passed to `renderThroughlineViewer` when a flow/step is focused. */
65
- export interface ThroughlineViewerContext {
66
- throughline: SubsystemThroughline;
67
+ /** Context passed to `renderWalkthroughViewer` when a flow/step is focused. */
68
+ export interface WalkthroughViewerContext {
69
+ walkthrough: SubsystemWalkthrough;
67
70
  /** Focused step index; `null` means the whole flow (no specific step). */
68
71
  stepIndex: number | null;
69
72
  }
70
73
 
71
74
  type DrawerTarget =
72
75
  | { kind: 'file'; file: string; startLine?: number }
73
- | { kind: 'throughline'; throughlineId: string; stepIndex: number | null };
76
+ | { kind: 'walkthrough'; walkthroughId: string; stepIndex: number | null };
74
77
 
75
78
  export interface SubsystemComponentGraphProps {
76
79
  components: SubsystemComponent[];
77
- edges: SubsystemComponentEdge[];
80
+ /** Topology relations (structural / module / type). Display edges are derived with walkthrough hops. */
81
+ relations: SubsystemRelation[];
78
82
  /**
79
- * Ordered execution stories over the graph's edges — one throughline per
80
- * flow. When present the sidebar's bottom half offers a Files/Flows toggle:
81
- * the flows panel lists each throughline's steps (`symbol` or `file:line`);
83
+ * Ordered runtime walkthroughs — one walkthrough per flow. When present the
84
+ * sidebar's bottom half offers a Files/Walkthroughs toggle:
85
+ * the flows panel lists each walkthrough's steps (`symbol` or `file:line`);
82
86
  * clicking a flow row toggles its steps; clicking a step focuses that
83
- * step's edge and (when `renderThroughlineViewer` is set) opens the bottom
87
+ * step's edge and (when `renderWalkthroughViewer` is set) opens the bottom
84
88
  * drawer on that flow's snippets. Opened flows stay on the canvas
85
89
  * (unselected ones dimmed); everything else is hidden.
86
90
  */
87
- throughlines?: SubsystemThroughline[];
91
+ walkthroughs?: SubsystemWalkthrough[];
88
92
  onSelect?: (componentId: string) => void;
89
93
  /** Called when an edge is clicked (relationship / mechanism + refs seam). */
90
94
  onEdgeSelect?: (edge: SubsystemComponentEdge) => void;
@@ -97,29 +101,29 @@ export interface SubsystemComponentGraphProps {
97
101
  title?: string;
98
102
  /**
99
103
  * Suppresses the sidebar entirely (title, description, file tree,
100
- * throughlines) for graph-only embeds. Pair with `graphTitle` to keep the
104
+ * walkthroughs) for graph-only embeds. Pair with `graphTitle` to keep the
101
105
  * subsystem name visible as an overlay on the canvas.
102
106
  */
103
107
  hideSidebar?: boolean;
104
108
  /**
105
- * How throughline step highlighting behaves on the canvas.
109
+ * How walkthrough step highlighting behaves on the canvas.
106
110
  * - `focus` (default): zoom to the step, hide non-participants, open drawer
107
111
  * - `dim`: keep the full graph, dim non-participants (same as hovering a step)
108
112
  */
109
- throughlineStepMode?: 'focus' | 'dim';
113
+ walkthroughStepMode?: 'focus' | 'dim';
110
114
  /**
111
- * When true, cycles throughline steps automatically using `dim` highlighting
112
- * (no zoom, no drawer). Loops across all throughlines that have steps.
115
+ * When true, cycles walkthrough steps automatically using `dim` highlighting
116
+ * (no zoom, no drawer). Loops across all walkthroughs that have steps.
113
117
  * Useful for graph-only embeds (`hideSidebar`).
114
118
  */
115
- autoPlayThroughlines?: boolean;
119
+ autoPlayWalkthroughs?: boolean;
116
120
  /** Pause between autoplay steps in ms. @default 2500 */
117
- throughlineAutoPlayIntervalMs?: number;
121
+ walkthroughAutoPlayIntervalMs?: number;
118
122
  /**
119
- * When false, focusing a throughline/step does not call `fitView`.
123
+ * When false, focusing a walkthrough/step does not call `fitView`.
120
124
  * @default true
121
125
  */
122
- zoomOnThroughlineFocus?: boolean;
126
+ zoomOnWalkthroughFocus?: boolean;
123
127
  /**
124
128
  * Subsystem title rendered as a non-interactive overlay chip on the graph
125
129
  * canvas (top-center). Does not trigger the sidebar — for graph-only
@@ -127,11 +131,11 @@ export interface SubsystemComponentGraphProps {
127
131
  */
128
132
  graphTitle?: string;
129
133
  /**
130
- * When true, shows the active throughline's title as a non-interactive
134
+ * When true, shows the active walkthrough's title as a non-interactive
131
135
  * overlay chip on the canvas (under `graphTitle` when both are set).
132
- * Uses the focused or hover-highlighted throughline.
136
+ * Uses the focused or hover-highlighted walkthrough.
133
137
  */
134
- showThroughlineTitle?: boolean;
138
+ showWalkthroughTitle?: boolean;
135
139
  /** Markdown description rendered in the sidebar. */
136
140
  description?: string;
137
141
  /** Rendered over the graph canvas only (not the title/legend sidebar). */
@@ -147,11 +151,11 @@ export interface SubsystemComponentGraphProps {
147
151
  */
148
152
  renderFileViewer?: (file: string, opts?: SubsystemOpenFileOptions) => ReactNode;
149
153
  /**
150
- * Host-injected multi-snippet viewer for a focused throughline. When set,
154
+ * Host-injected multi-snippet viewer for a focused walkthrough. When set,
151
155
  * clicking a flow step opens the bottom drawer with this content and
152
156
  * updates it as the focused step changes.
153
157
  */
154
- renderThroughlineViewer?: (ctx: ThroughlineViewerContext) => ReactNode;
158
+ renderWalkthroughViewer?: (ctx: WalkthroughViewerContext) => ReactNode;
155
159
  /**
156
160
  * Legacy component-keyed variant, kept for backward compatibility. When
157
161
  * `renderFileViewer` is absent, drawer content resolves via the first
@@ -194,33 +198,37 @@ const FileDrawerContent = memo(function FileDrawerContent({
194
198
  return <>{render(file, startLine != null ? { startLine } : undefined)}</>;
195
199
  });
196
200
 
197
- const ThroughlineDrawerContent = memo(function ThroughlineDrawerContent({
201
+ const WalkthroughDrawerContent = memo(function WalkthroughDrawerContent({
198
202
  render,
199
- throughline,
203
+ walkthrough,
200
204
  stepIndex,
201
205
  }: {
202
- render: (ctx: ThroughlineViewerContext) => ReactNode;
203
- throughline: SubsystemThroughline;
206
+ render: (ctx: WalkthroughViewerContext) => ReactNode;
207
+ walkthrough: SubsystemWalkthrough;
204
208
  stepIndex: number | null;
205
209
  }) {
206
- return <>{render({ throughline, stepIndex })}</>;
210
+ return <>{render({ walkthrough, stepIndex })}</>;
207
211
  });
208
212
 
209
213
  interface InnerProps extends SubsystemComponentGraphProps {
210
214
  measured: { w: number; h: number } | null;
211
215
  }
212
216
 
213
- function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measured: _measured, maxNodeWidth, showEdgeLabels, title, hideSidebar, throughlineStepMode = 'focus', autoPlayThroughlines = false, throughlineAutoPlayIntervalMs = THROUGHLINE_PLAY_PAUSE_MS, zoomOnThroughlineFocus = true, graphTitle, showThroughlineTitle = false, description, canvasOverlay, sidebarExtra, sidebarAfterDescription, renderFileView, renderFileViewer, renderThroughlineViewer, onFileSelect, onVerifyComponent, componentVerification }: InnerProps) {
217
+ function Inner({ components, relations, walkthroughs, onSelect, onEdgeSelect, measured: _measured, maxNodeWidth, showEdgeLabels, title, hideSidebar, walkthroughStepMode = 'focus', autoPlayWalkthroughs = false, walkthroughAutoPlayIntervalMs = WALKTHROUGH_PLAY_PAUSE_MS, zoomOnWalkthroughFocus = true, graphTitle, showWalkthroughTitle = false, description, canvasOverlay, sidebarExtra, sidebarAfterDescription, renderFileView, renderFileViewer, renderWalkthroughViewer, onFileSelect, onVerifyComponent, componentVerification }: InnerProps) {
214
218
  const { theme } = useTheme();
215
219
  const { fitView } = useReactFlow();
216
220
  const viewport = useViewport();
221
+ const graphEdges = useMemo(
222
+ () => deriveGraphEdges({ relations, walkthroughs }),
223
+ [relations, walkthroughs],
224
+ );
217
225
  const [built, setBuilt] = useState<{ nodes: Node[]; edges: Edge[] }>({
218
226
  nodes: [],
219
227
  edges: [],
220
228
  });
221
229
  const [layoutReady, setLayoutReady] = useState(false);
222
230
  const [selected, setSelected] = useState<SubsystemComponent | null>(null);
223
- /** Bottom drawer: single file or throughline multi-snippet mode. */
231
+ /** Bottom drawer: single file or walkthrough multi-snippet mode. */
224
232
  const [drawerTarget, setDrawerTarget] = useState<DrawerTarget | null>(null);
225
233
  // Mirror so step-focus can decide whether to wait for the drawer open
226
234
  // transition before fitView (avoids framing against full-height canvas).
@@ -230,27 +238,27 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
230
238
  const pendingFocusFitRef = useRef<number | null>(null);
231
239
  // Component the pointer is over (null on leave) → transient tree highlight.
232
240
  const [hoveredComponentId, setHoveredComponentId] = useState<string | null>(null);
233
- // Throughline focus — selected flow (or step) is full strength; other
241
+ // Walkthrough focus — selected flow (or step) is full strength; other
234
242
  // opened-flow members stay visible but dimmed; everything else is hidden.
235
- const [focusedThroughlineId, setFocusedThroughlineId] = useState<string | null>(null);
243
+ const [focusedWalkthroughId, setFocusedWalkthroughId] = useState<string | null>(null);
236
244
  // `null` = whole flow focused; a number = that single step's edge focused.
237
245
  const [focusedStepIndex, setFocusedStepIndex] = useState<number | null>(null);
238
- // Hovered throughline in the flows panel: dims every canvas node/edge not
246
+ // Hovered walkthrough in the flows panel: dims every canvas node/edge not
239
247
  // involved in the hover preview (or selected ∪ hovered when a step is
240
248
  // focused). `stepIndex: null` = whole flow (collapsed title hover);
241
249
  // a number = that step. Transient — no drawer. Camera only reframes when
242
250
  // a step is already selected.
243
- const [hoveredThroughlineStep, setHoveredThroughlineStep] = useState<{
244
- throughlineId: string;
251
+ const [hoveredWalkthroughStep, setHoveredWalkthroughStep] = useState<{
252
+ walkthroughId: string;
245
253
  stepIndex: number | null;
246
254
  } | null>(null);
247
- // Sidebar bottom half: which panel is shown when throughlines exist.
248
- const [sidebarView, setSidebarView] = useState<'files' | 'flows'>(() =>
249
- throughlines?.length ? 'flows' : 'files',
255
+ // Sidebar bottom half: which panel is shown when walkthroughs exist.
256
+ const [sidebarView, setSidebarView] = useState<'files' | 'walkthroughs'>(() =>
257
+ walkthroughs?.length ? 'walkthroughs' : 'files',
250
258
  );
251
- // Throughline flows the user has expanded (via the title row). Closed by
259
+ // Walkthrough flows the user has expanded (via the title row). Closed by
252
260
  // default so a graph with several flows doesn't dump every step list at once.
253
- const [expandedThroughlines, setExpandedThroughlines] = useState<Set<string>>(new Set());
261
+ const [expandedWalkthroughs, setExpandedWalkthroughs] = useState<Set<string>>(new Set());
254
262
  // Sidebar description visibility. Hidden by default so the files/flows
255
263
  // panel gets the vertical room; the title-row toggle reveals it.
256
264
  const [descriptionVisible, setDescriptionVisible] = useState(false);
@@ -314,36 +322,36 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
314
322
  ? { file: drawerTarget.file, startLine: drawerTarget.startLine }
315
323
  : null;
316
324
 
317
- const focusedThroughline = useMemo(() => {
318
- if (drawerTarget?.kind !== 'throughline' || !throughlines) return null;
319
- return throughlines.find((t) => t.id === drawerTarget.throughlineId) ?? null;
320
- }, [drawerTarget, throughlines]);
325
+ const focusedWalkthrough = useMemo(() => {
326
+ if (drawerTarget?.kind !== 'walkthrough' || !walkthroughs) return null;
327
+ return walkthroughs.find((t) => t.id === drawerTarget.walkthroughId) ?? null;
328
+ }, [drawerTarget, walkthroughs]);
321
329
 
322
- // Throughline shown on the canvas title chip (focus or hover/autoplay highlight).
323
- const overlayThroughlineTitle = useMemo(() => {
324
- if (!showThroughlineTitle || !throughlines?.length) return null;
325
- const id = focusedThroughlineId ?? hoveredThroughlineStep?.throughlineId;
330
+ // Walkthrough shown on the canvas title chip (focus or hover/autoplay highlight).
331
+ const overlayWalkthroughTitle = useMemo(() => {
332
+ if (!showWalkthroughTitle || !walkthroughs?.length) return null;
333
+ const id = focusedWalkthroughId ?? hoveredWalkthroughStep?.walkthroughId;
326
334
  if (!id) return null;
327
- return throughlines.find((t) => t.id === id)?.title ?? null;
335
+ return walkthroughs.find((t) => t.id === id)?.title ?? null;
328
336
  }, [
329
- showThroughlineTitle,
330
- throughlines,
331
- focusedThroughlineId,
332
- hoveredThroughlineStep,
337
+ showWalkthroughTitle,
338
+ walkthroughs,
339
+ focusedWalkthroughId,
340
+ hoveredWalkthroughStep,
333
341
  ]);
334
342
 
335
343
  // Active step for the bottom-of-title progress + annotation chip.
336
- const overlayThroughlineStep = useMemo(() => {
337
- if (!showThroughlineTitle || !throughlines?.length) return null;
338
- const tlId = focusedThroughlineId ?? hoveredThroughlineStep?.throughlineId ?? null;
344
+ const overlayWalkthroughStep = useMemo(() => {
345
+ if (!showWalkthroughTitle || !walkthroughs?.length) return null;
346
+ const tlId = focusedWalkthroughId ?? hoveredWalkthroughStep?.walkthroughId ?? null;
339
347
  let stepIndex: number | null = null;
340
- if (focusedThroughlineId != null) {
348
+ if (focusedWalkthroughId != null) {
341
349
  stepIndex = focusedStepIndex;
342
- } else if (hoveredThroughlineStep != null) {
343
- stepIndex = hoveredThroughlineStep.stepIndex;
350
+ } else if (hoveredWalkthroughStep != null) {
351
+ stepIndex = hoveredWalkthroughStep.stepIndex;
344
352
  }
345
353
  if (tlId == null || stepIndex == null) return null;
346
- const tl = throughlines.find((t) => t.id === tlId);
354
+ const tl = walkthroughs.find((t) => t.id === tlId);
347
355
  const step = tl?.steps[stepIndex];
348
356
  if (!step || !tl) return null;
349
357
  return {
@@ -352,24 +360,24 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
352
360
  annotation: step.annotation,
353
361
  };
354
362
  }, [
355
- showThroughlineTitle,
356
- throughlines,
357
- focusedThroughlineId,
363
+ showWalkthroughTitle,
364
+ walkthroughs,
365
+ focusedWalkthroughId,
358
366
  focusedStepIndex,
359
- hoveredThroughlineStep,
367
+ hoveredWalkthroughStep,
360
368
  ]);
361
369
 
362
370
  const drawerTitle = useMemo(() => {
363
371
  if (!drawerTarget) return null;
364
372
  if (drawerTarget.kind === 'file') return drawerTarget.file;
365
- const tl = focusedThroughline;
373
+ const tl = focusedWalkthrough;
366
374
  if (!tl) return null;
367
375
  if (drawerTarget.stepIndex == null) return tl.title;
368
376
  const step = tl.steps[drawerTarget.stepIndex];
369
377
  if (!step) return tl.title;
370
378
  const site = `${step.file.split('/').pop() ?? step.file}:${step.line}`;
371
379
  return `${tl.title} · ${site}`;
372
- }, [drawerTarget, focusedThroughline]);
380
+ }, [drawerTarget, focusedWalkthrough]);
373
381
 
374
382
  // Refresh selected component when the components list updates (e.g. verify
375
383
  // writes back declarationRef).
@@ -383,13 +391,15 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
383
391
  // The pane stays hidden until Pass 2 completes. Key off layout-affecting
384
392
  // fields only — declarationRef updates after verify must not re-run ELK.
385
393
  const layoutKey = useMemo(
386
- () => subsystemGraphLayoutKey({ components, edges }),
387
- [components, edges],
394
+ () => subsystemGraphLayoutKey({ components, relations, walkthroughs }),
395
+ [components, relations, walkthroughs],
388
396
  );
389
397
  const componentsRef = useRef(components);
390
- const edgesRef = useRef(edges);
398
+ const relationsRef = useRef(relations);
399
+ const walkthroughsRef = useRef(walkthroughs);
391
400
  componentsRef.current = components;
392
- edgesRef.current = edges;
401
+ relationsRef.current = relations;
402
+ walkthroughsRef.current = walkthroughs;
393
403
 
394
404
  // Track measured dimensions from React Flow's dimension changes.
395
405
  // These arrive as { type: 'dimensions', id, dimensions } in onNodesChange.
@@ -406,7 +416,11 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
406
416
  prevMeasuredSigRef.current = '';
407
417
  // Bump generation so any in-flight Pass 2 from the prior layoutKey is ignored.
408
418
  pass2GenRef.current += 1;
409
- const doc = { components: componentsRef.current, edges: edgesRef.current };
419
+ const doc = {
420
+ components: componentsRef.current,
421
+ relations: relationsRef.current,
422
+ walkthroughs: walkthroughsRef.current,
423
+ };
410
424
  void buildSubsystemGraph(doc, { maxNodeWidth, showEdgeLabels })
411
425
  .then(({ nodes, edges: e }) => {
412
426
  if (!alive) return;
@@ -455,7 +469,7 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
455
469
  const measuredHeights = new Map(leafNodes.map((n) => [n.id, dims.get(n.id)!.height]));
456
470
  const gen = ++pass2GenRef.current;
457
471
  void buildSubsystemGraph(
458
- { components, edges },
472
+ { components, relations, walkthroughs },
459
473
  { maxNodeWidth, showEdgeLabels, measuredWidths, measuredHeights },
460
474
  )
461
475
  .then(({ nodes, edges: e }) => {
@@ -469,7 +483,7 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
469
483
  // Reveal Pass 1 layout rather than leaving the cover up forever.
470
484
  setLayoutReady(true);
471
485
  });
472
- }, [built.nodes, components, edges, maxNodeWidth, showEdgeLabels]);
486
+ }, [built.nodes, components, relations, walkthroughs, maxNodeWidth, showEdgeLabels]);
473
487
 
474
488
  // After Pass 1 commits, try Pass 2 immediately with retained measurements.
475
489
  // Same-id live updates often get no new React Flow `dimensions` events, so
@@ -498,7 +512,7 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
498
512
  // Node components call SUBSYSTEM_CALLBACKS.onSelect on click (their inner
499
513
  // onClick stops React Flow propagation), so wire selection + width through it.
500
514
  // Lookup from edge id → the authored SubsystemComponentEdge (for onEdgeSelect).
501
- const edgeById = useMemo(() => new Map(edges.map((e) => [e.id, e])), [edges]);
515
+ const edgeById = useMemo(() => new Map(graphEdges.map((e) => [e.id, e])), [graphEdges]);
502
516
 
503
517
  // Shared edge-selection logic used by both React Flow's onEdgeClick and the
504
518
  // clickable edge label (SUBSYSTEM_CALLBACKS.onEdgeSelect).
@@ -510,8 +524,8 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
510
524
  }
511
525
  const src = edgeById.get(edgeId);
512
526
  setSelectedEdgeId(edgeId);
513
- // A direct edge selection on the canvas supersedes any throughline focus.
514
- setFocusedThroughlineId(null);
527
+ // A direct edge selection on the canvas supersedes any walkthrough focus.
528
+ setFocusedWalkthroughId(null);
515
529
  setFocusedStepIndex(null);
516
530
  if (src) onEdgeSelect?.(src);
517
531
  },
@@ -531,7 +545,7 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
531
545
  }
532
546
  setSelected(comp);
533
547
  setSelectedEdgeId(null);
534
- setFocusedThroughlineId(null);
548
+ setFocusedWalkthroughId(null);
535
549
  setFocusedStepIndex(null);
536
550
  onSelect?.(id);
537
551
  }
@@ -557,45 +571,45 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
557
571
  // without a stale closure value.
558
572
  const selectedEdgeIdRef = useRef<string | null>(null);
559
573
  selectedEdgeIdRef.current = selectedEdgeId;
560
- // Edge ids in throughline focus (an active flow's edge set, or a single
561
- // step's edge). Used to frame the camera. `null` = no throughline focus.
574
+ // Edge ids in walkthrough focus (an active flow's edge set, or a single
575
+ // step's edge). Used to frame the camera. `null` = no walkthrough focus.
562
576
  const focusEdgeIds = useMemo(() => {
563
- if (focusedThroughlineId == null || !throughlines) return null;
564
- const tl = throughlines.find((t) => t.id === focusedThroughlineId);
577
+ if (focusedWalkthroughId == null || !walkthroughs) return null;
578
+ const tl = walkthroughs.find((t) => t.id === focusedWalkthroughId);
565
579
  if (!tl) return null;
566
580
  if (focusedStepIndex != null) {
567
581
  const step = tl.steps[focusedStepIndex];
568
- return step ? new Set([step.edgeId]) : null;
582
+ return step ? new Set([walkthroughStepGraphEdgeId(step)]) : null;
569
583
  }
570
- return new Set(tl.steps.map((s) => s.edgeId));
571
- }, [throughlines, focusedThroughlineId, focusedStepIndex]);
584
+ return new Set(tl.steps.map((s) => walkthroughStepGraphEdgeId(s)));
585
+ }, [walkthroughs, focusedWalkthroughId, focusedStepIndex]);
572
586
 
573
587
  // 1-based step numbers per edge of the active flow (focused or
574
588
  // hover/autoplay-highlighted). An edge can appear in more than one step.
575
589
  const selectedFlowStepNos = useMemo(() => {
576
- const activeId = focusedThroughlineId ?? hoveredThroughlineStep?.throughlineId;
577
- if (activeId == null || !throughlines) return null;
578
- const tl = throughlines.find((t) => t.id === activeId);
590
+ const activeId = focusedWalkthroughId ?? hoveredWalkthroughStep?.walkthroughId;
591
+ if (activeId == null || !walkthroughs) return null;
592
+ const tl = walkthroughs.find((t) => t.id === activeId);
579
593
  if (!tl) return null;
580
594
  const map = new Map<string, number[]>();
581
595
  tl.steps.forEach((s, i) => {
582
- const list = map.get(s.edgeId) ?? [];
596
+ const list = map.get(walkthroughStepGraphEdgeId(s)) ?? [];
583
597
  list.push(i + 1);
584
- map.set(s.edgeId, list);
598
+ map.set(walkthroughStepGraphEdgeId(s), list);
585
599
  });
586
600
  return map;
587
- }, [throughlines, focusedThroughlineId, hoveredThroughlineStep]);
601
+ }, [walkthroughs, focusedWalkthroughId, hoveredWalkthroughStep]);
588
602
 
589
- // Union of every expanded (opened) throughline's edges — the visible set.
603
+ // Union of every expanded (opened) walkthrough's edges — the visible set.
590
604
  const openedEdgeIds = useMemo(() => {
591
- if (!throughlines || expandedThroughlines.size === 0) return null;
605
+ if (!walkthroughs || expandedWalkthroughs.size === 0) return null;
592
606
  const ids = new Set<string>();
593
- for (const tl of throughlines) {
594
- if (!expandedThroughlines.has(tl.id)) continue;
595
- for (const s of tl.steps) ids.add(s.edgeId);
607
+ for (const tl of walkthroughs) {
608
+ if (!expandedWalkthroughs.has(tl.id)) continue;
609
+ for (const s of tl.steps) ids.add(walkthroughStepGraphEdgeId(s));
596
610
  }
597
611
  return ids.size > 0 ? ids : null;
598
- }, [throughlines, expandedThroughlines]);
612
+ }, [walkthroughs, expandedWalkthroughs]);
599
613
 
600
614
  const endpointsOf = (edgeIds: ReadonlySet<string> | null): Set<string> | null => {
601
615
  if (!edgeIds) return null;
@@ -609,15 +623,15 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
609
623
  };
610
624
  // Edge/nodes involved in the hovered step or whole flow.
611
625
  const hoverEdgeIds = useMemo(() => {
612
- if (!hoveredThroughlineStep || !throughlines) return null;
613
- const tl = throughlines.find((t) => t.id === hoveredThroughlineStep.throughlineId);
626
+ if (!hoveredWalkthroughStep || !walkthroughs) return null;
627
+ const tl = walkthroughs.find((t) => t.id === hoveredWalkthroughStep.walkthroughId);
614
628
  if (!tl) return null;
615
- if (hoveredThroughlineStep.stepIndex == null) {
616
- return new Set(tl.steps.map((s) => s.edgeId));
629
+ if (hoveredWalkthroughStep.stepIndex == null) {
630
+ return new Set(tl.steps.map((s) => walkthroughStepGraphEdgeId(s)));
617
631
  }
618
- const step = tl.steps[hoveredThroughlineStep.stepIndex];
619
- return step ? new Set([step.edgeId]) : null;
620
- }, [throughlines, hoveredThroughlineStep]);
632
+ const step = tl.steps[hoveredWalkthroughStep.stepIndex];
633
+ return step ? new Set([walkthroughStepGraphEdgeId(step)]) : null;
634
+ }, [walkthroughs, hoveredWalkthroughStep]);
621
635
  // While hovering with a *step* already selected, brighten the union of
622
636
  // selected + hovered participants. Whole-flow focus (or no focus) keeps
623
637
  // the old hover-replace preview so a step hover still dims the rest.
@@ -667,8 +681,8 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
667
681
  // React Flow's own selection state from updating.
668
682
  const dispNodes = useMemo(() => {
669
683
  // While a step/flow is hovered, selected + hovered participants stay
670
- // bright; everything else among the currently-visible (opened-throughline)
671
- // nodes is dimmed. Nodes outside the opened-throughline set stay hidden.
684
+ // bright; everything else among the currently-visible (opened-walkthrough)
685
+ // nodes is dimmed. Nodes outside the opened-walkthrough set stay hidden.
672
686
  return xyflowNodesBase.map((n) => {
673
687
  // Boundary frames follow their members: hidden when no member is
674
688
  // visible, dimmed when members are dimmed. Never selectable.
@@ -800,7 +814,7 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
800
814
  (_e, node: Node) => {
801
815
  const comp = (node.data as { component?: SubsystemComponent } | undefined)?.component;
802
816
  setSelectedEdgeId(null);
803
- setFocusedThroughlineId(null);
817
+ setFocusedWalkthroughId(null);
804
818
  setFocusedStepIndex(null);
805
819
  if (node.type === 'subsystem-component' && comp) {
806
820
  // Clicking the already-selected node unselects it (toggle off).
@@ -827,14 +841,14 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
827
841
  const onPaneClick = useCallback(() => {
828
842
  setSelected(null);
829
843
  setSelectedEdgeId(null);
830
- setFocusedThroughlineId(null);
844
+ setFocusedWalkthroughId(null);
831
845
  setFocusedStepIndex(null);
832
846
  }, []);
833
847
 
834
848
  // Sidebar file trees — one per repo on multi-repo graphs, each under its
835
849
  // own owner-avatar header. Clicking a header collapses that repo's tree.
836
850
  const repoGroups = useMemo(() => buildRepoGroups(components), [components]);
837
- const hasThroughlines = useMemo(() => (throughlines?.length ?? 0) > 0, [throughlines]);
851
+ const hasWalkthroughs = useMemo(() => (walkthroughs?.length ?? 0) > 0, [walkthroughs]);
838
852
  const [collapsedRepos, setCollapsedRepos] = useState<Set<string>>(new Set());
839
853
  const toggleRepoCollapsed = useCallback((key: string) => {
840
854
  setCollapsedRepos((prev) => {
@@ -876,13 +890,13 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
876
890
  [onFileSelect],
877
891
  );
878
892
 
879
- // Camera helper shared by the throughline interactions: frames the focused
893
+ // Camera helper shared by the walkthrough interactions: frames the focused
880
894
  // edges' endpoint nodes via `fitView({ nodes })`, which uses the store's
881
895
  // live positions + measured dims — so the frame always includes BOTH
882
896
  // components the edge attaches to (and therefore the edge line between them).
883
897
  const fitFocusBounds = useCallback(
884
898
  (ids: ReadonlySet<string>) => {
885
- if (!zoomOnThroughlineFocus) return;
899
+ if (!zoomOnWalkthroughFocus) return;
886
900
  const nodeIds = new Set<string>();
887
901
  for (const e of baseEdges) {
888
902
  if (!ids.has(e.id)) continue;
@@ -896,13 +910,13 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
896
910
  duration: 300,
897
911
  });
898
912
  },
899
- [baseEdges, fitView, zoomOnThroughlineFocus],
913
+ [baseEdges, fitView, zoomOnWalkthroughFocus],
900
914
  );
901
915
 
902
- // Zoom back out to the full diagram after the last expanded throughline
916
+ // Zoom back out to the full diagram after the last expanded walkthrough
903
917
  // closes (visibility restores non-flow nodes that were hidden).
904
918
  const fitOverview = useCallback(() => {
905
- if (!zoomOnThroughlineFocus) return;
919
+ if (!zoomOnWalkthroughFocus) return;
906
920
  fitView({
907
921
  padding: 0.1,
908
922
  includeHiddenNodes: false,
@@ -910,83 +924,83 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
910
924
  maxZoom: 2,
911
925
  duration: 300,
912
926
  });
913
- }, [fitView, zoomOnThroughlineFocus]);
927
+ }, [fitView, zoomOnWalkthroughFocus]);
914
928
 
915
929
  // Focus an entire flow: hide everything but the flow's nodes and edges, and
916
930
  // frame the flow on the canvas. Selection state is cleared — the graph now
917
931
  // reads as the narrative. No drawer: the code view only opens on a step
918
- // click. A stale throughline drawer (from a previously focused flow's step)
932
+ // click. A stale walkthrough drawer (from a previously focused flow's step)
919
933
  // closes; an explicitly opened file drawer stays.
920
934
  // In `dim` mode: clear step highlight and leave the full graph visible
921
935
  // (no zoom / hide) — matching "step away from a hovered step".
922
- const focusThroughlineEdges = useCallback(
923
- (tl: SubsystemThroughline) => {
936
+ const focusWalkthroughEdges = useCallback(
937
+ (tl: SubsystemWalkthrough) => {
924
938
  setSelected(null);
925
939
  setSelectedEdgeId(null);
926
- setHoveredThroughlineStep(null);
927
- if (throughlineStepMode === 'dim') {
940
+ setHoveredWalkthroughStep(null);
941
+ if (walkthroughStepMode === 'dim') {
928
942
  setFocusedStepIndex(null);
929
- setFocusedThroughlineId(null);
930
- setDrawerTarget((prev) => (prev?.kind === 'throughline' ? null : prev));
943
+ setFocusedWalkthroughId(null);
944
+ setDrawerTarget((prev) => (prev?.kind === 'walkthrough' ? null : prev));
931
945
  return;
932
946
  }
933
947
  setFocusedStepIndex(null);
934
- setFocusedThroughlineId(tl.id);
935
- fitFocusBounds(new Set(tl.steps.map((s) => s.edgeId)));
936
- setDrawerTarget((prev) => (prev?.kind === 'throughline' ? null : prev));
948
+ setFocusedWalkthroughId(tl.id);
949
+ fitFocusBounds(new Set(tl.steps.map((s) => walkthroughStepGraphEdgeId(s))));
950
+ setDrawerTarget((prev) => (prev?.kind === 'walkthrough' ? null : prev));
937
951
  },
938
- [fitFocusBounds, throughlineStepMode],
952
+ [fitFocusBounds, walkthroughStepMode],
939
953
  );
940
954
 
941
- const clearThroughlineFocus = useCallback(() => {
942
- setFocusedThroughlineId(null);
955
+ const clearWalkthroughFocus = useCallback(() => {
956
+ setFocusedWalkthroughId(null);
943
957
  setFocusedStepIndex(null);
944
- setHoveredThroughlineStep(null);
945
- setDrawerTarget((prev) => (prev?.kind === 'throughline' ? null : prev));
958
+ setHoveredWalkthroughStep(null);
959
+ setDrawerTarget((prev) => (prev?.kind === 'walkthrough' ? null : prev));
946
960
  }, []);
947
961
 
948
- // Focus a single step's edge on the canvas and open/scroll the throughline
962
+ // Focus a single step's edge on the canvas and open/scroll the walkthrough
949
963
  // drawer to that step's snippet. Clicking the already-focused step clears
950
964
  // step focus and returns to whole-flow (high-level) framing.
951
965
  // In `dim` mode: only dim non-participants (same as hovering a step) —
952
966
  // no camera move, no drawer, no hide.
953
- const focusThroughlineStep = useCallback(
954
- (tl: SubsystemThroughline, stepIndex: number) => {
967
+ const focusWalkthroughStep = useCallback(
968
+ (tl: SubsystemWalkthrough, stepIndex: number) => {
955
969
  const step = tl.steps[stepIndex];
956
970
  if (!step) return;
957
971
  setSelected(null);
958
972
  setSelectedEdgeId(null);
959
- if (throughlineStepMode === 'dim') {
973
+ if (walkthroughStepMode === 'dim') {
960
974
  setFocusedStepIndex(null);
961
- setFocusedThroughlineId(null);
962
- setHoveredThroughlineStep({ throughlineId: tl.id, stepIndex });
963
- setDrawerTarget((prev) => (prev?.kind === 'throughline' ? null : prev));
975
+ setFocusedWalkthroughId(null);
976
+ setHoveredWalkthroughStep({ walkthroughId: tl.id, stepIndex });
977
+ setDrawerTarget((prev) => (prev?.kind === 'walkthrough' ? null : prev));
964
978
  return;
965
979
  }
966
980
  // Toggle: clicking the selected step unselects it (back to whole flow).
967
- if (focusedThroughlineId === tl.id && focusedStepIndex === stepIndex) {
981
+ if (focusedWalkthroughId === tl.id && focusedStepIndex === stepIndex) {
968
982
  setFocusedStepIndex(null);
969
983
  // Pointer is still over the row — keep hover preview so dimming doesn't
970
984
  // flash off until mouseleave.
971
- setHoveredThroughlineStep({ throughlineId: tl.id, stepIndex });
972
- setDrawerTarget((prev) => (prev?.kind === 'throughline' ? null : prev));
985
+ setHoveredWalkthroughStep({ walkthroughId: tl.id, stepIndex });
986
+ setDrawerTarget((prev) => (prev?.kind === 'walkthrough' ? null : prev));
973
987
  if (pendingFocusFitRef.current != null) {
974
988
  window.clearTimeout(pendingFocusFitRef.current);
975
989
  pendingFocusFitRef.current = null;
976
990
  }
977
- fitFocusBounds(new Set(tl.steps.map((s) => s.edgeId)));
991
+ fitFocusBounds(new Set(tl.steps.map((s) => walkthroughStepGraphEdgeId(s))));
978
992
  return;
979
993
  }
980
994
  setFocusedStepIndex(stepIndex);
981
- setFocusedThroughlineId(tl.id);
982
- setHoveredThroughlineStep(null);
995
+ setFocusedWalkthroughId(tl.id);
996
+ setHoveredWalkthroughStep(null);
983
997
  // Open the drawer before fitting. If it was closed, wait for its height
984
998
  // transition so fitView uses the reduced canvas — not full height.
985
999
  const drawerWasOpen = drawerOpenRef.current;
986
- if (renderThroughlineViewer) {
1000
+ if (renderWalkthroughViewer) {
987
1001
  setDrawerTarget({
988
- kind: 'throughline',
989
- throughlineId: tl.id,
1002
+ kind: 'walkthrough',
1003
+ walkthroughId: tl.id,
990
1004
  stepIndex,
991
1005
  });
992
1006
  } else {
@@ -996,7 +1010,7 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
996
1010
  startLine: step.line,
997
1011
  });
998
1012
  }
999
- const edgeIds = new Set([step.edgeId]);
1013
+ const edgeIds = new Set([walkthroughStepGraphEdgeId(step)]);
1000
1014
  if (pendingFocusFitRef.current != null) {
1001
1015
  window.clearTimeout(pendingFocusFitRef.current);
1002
1016
  pendingFocusFitRef.current = null;
@@ -1013,9 +1027,9 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
1013
1027
  [
1014
1028
  fitFocusBounds,
1015
1029
  focusedStepIndex,
1016
- focusedThroughlineId,
1017
- renderThroughlineViewer,
1018
- throughlineStepMode,
1030
+ focusedWalkthroughId,
1031
+ renderWalkthroughViewer,
1032
+ walkthroughStepMode,
1019
1033
  ],
1020
1034
  );
1021
1035
 
@@ -1037,9 +1051,9 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
1037
1051
  const prev = prevPreviewEdgeIdsRef.current;
1038
1052
  prevPreviewEdgeIdsRef.current = previewEdgeIds;
1039
1053
 
1040
- if (!zoomOnThroughlineFocus || throughlineStepMode === 'dim') return;
1054
+ if (!zoomOnWalkthroughFocus || walkthroughStepMode === 'dim') return;
1041
1055
  // Camera follows hover only when a specific step is already focused.
1042
- if (focusedThroughlineId == null || focusedStepIndex == null) return;
1056
+ if (focusedWalkthroughId == null || focusedStepIndex == null) return;
1043
1057
 
1044
1058
  if (previewEdgeIds) {
1045
1059
  fitFocusBounds(previewEdgeIds);
@@ -1051,33 +1065,33 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
1051
1065
  }, [
1052
1066
  previewEdgeIds,
1053
1067
  focusEdgeIds,
1054
- focusedThroughlineId,
1068
+ focusedWalkthroughId,
1055
1069
  focusedStepIndex,
1056
1070
  fitFocusBounds,
1057
- zoomOnThroughlineFocus,
1058
- throughlineStepMode,
1071
+ zoomOnWalkthroughFocus,
1072
+ walkthroughStepMode,
1059
1073
  ]);
1060
1074
 
1061
- // Graph-only embeds: cycle throughline steps with hover-style dimming.
1075
+ // Graph-only embeds: cycle walkthrough steps with hover-style dimming.
1062
1076
  useEffect(() => {
1063
- if (!autoPlayThroughlines || !throughlines?.length || !layoutReady) return;
1064
- const playable = throughlines.filter((tl) => tl.steps.length > 0);
1077
+ if (!autoPlayWalkthroughs || !walkthroughs?.length || !layoutReady) return;
1078
+ const playable = walkthroughs.filter((tl) => tl.steps.length > 0);
1065
1079
  if (playable.length === 0) return;
1066
1080
 
1067
1081
  let cancelled = false;
1068
1082
  let tlIdx = 0;
1069
1083
  let stepIdx = 0;
1070
1084
  let timer: number | null = null;
1071
- const interval = Math.max(400, throughlineAutoPlayIntervalMs);
1085
+ const interval = Math.max(400, walkthroughAutoPlayIntervalMs);
1072
1086
 
1073
1087
  const tick = () => {
1074
1088
  if (cancelled) return;
1075
1089
  const tl = playable[tlIdx]!;
1076
1090
  setSelected(null);
1077
1091
  setSelectedEdgeId(null);
1078
- setFocusedThroughlineId(null);
1092
+ setFocusedWalkthroughId(null);
1079
1093
  setFocusedStepIndex(null);
1080
- setHoveredThroughlineStep({ throughlineId: tl.id, stepIndex: stepIdx });
1094
+ setHoveredWalkthroughStep({ walkthroughId: tl.id, stepIndex: stepIdx });
1081
1095
  stepIdx += 1;
1082
1096
  if (stepIdx >= tl.steps.length) {
1083
1097
  stepIdx = 0;
@@ -1090,17 +1104,17 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
1090
1104
  return () => {
1091
1105
  cancelled = true;
1092
1106
  if (timer != null) window.clearTimeout(timer);
1093
- setHoveredThroughlineStep(null);
1107
+ setHoveredWalkthroughStep(null);
1094
1108
  };
1095
- }, [autoPlayThroughlines, throughlines, throughlineAutoPlayIntervalMs, layoutReady]);
1109
+ }, [autoPlayWalkthroughs, walkthroughs, walkthroughAutoPlayIntervalMs, layoutReady]);
1096
1110
 
1097
- // Arrow keys step through the focused throughline once a step is active
1111
+ // Arrow keys step through the focused walkthrough once a step is active
1098
1112
  // (sidebar click or drawer open). Ignores typing targets and chords.
1099
1113
  useEffect(() => {
1100
- if (focusedThroughlineId == null || focusedStepIndex == null || !throughlines) {
1114
+ if (focusedWalkthroughId == null || focusedStepIndex == null || !walkthroughs) {
1101
1115
  return;
1102
1116
  }
1103
- const tl = throughlines.find((t) => t.id === focusedThroughlineId);
1117
+ const tl = walkthroughs.find((t) => t.id === focusedWalkthroughId);
1104
1118
  if (!tl || tl.steps.length === 0) return;
1105
1119
 
1106
1120
  const onKeyDown = (e: KeyboardEvent) => {
@@ -1126,31 +1140,31 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
1126
1140
  }
1127
1141
  if (next === focusedStepIndex) return;
1128
1142
  e.preventDefault();
1129
- focusThroughlineStep(tl, next);
1143
+ focusWalkthroughStep(tl, next);
1130
1144
  };
1131
1145
 
1132
1146
  window.addEventListener('keydown', onKeyDown);
1133
1147
  return () => window.removeEventListener('keydown', onKeyDown);
1134
1148
  }, [
1135
- focusedThroughlineId,
1149
+ focusedWalkthroughId,
1136
1150
  focusedStepIndex,
1137
- throughlines,
1138
- focusThroughlineStep,
1151
+ walkthroughs,
1152
+ focusWalkthroughStep,
1139
1153
  ]);
1140
1154
 
1141
- const toggleThroughlineCollapsed = useCallback(
1155
+ const toggleWalkthroughCollapsed = useCallback(
1142
1156
  (tlId: string) => {
1143
1157
  const collapsingLast =
1144
- expandedThroughlines.has(tlId) && expandedThroughlines.size === 1;
1158
+ expandedWalkthroughs.has(tlId) && expandedWalkthroughs.size === 1;
1145
1159
 
1146
- setExpandedThroughlines((prev) => {
1160
+ setExpandedWalkthroughs((prev) => {
1147
1161
  const next = new Set(prev);
1148
1162
  if (next.has(tlId)) next.delete(tlId);
1149
1163
  else next.add(tlId);
1150
1164
  return next;
1151
1165
  });
1152
1166
 
1153
- if (!collapsingLast || !zoomOnThroughlineFocus) return;
1167
+ if (!collapsingLast || !zoomOnWalkthroughFocus) return;
1154
1168
 
1155
1169
  // Cancel any pending focus fit so it doesn't fight the overview zoom.
1156
1170
  if (pendingFocusFitRef.current != null) {
@@ -1170,7 +1184,7 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
1170
1184
  drawerWasOpen ? FILE_DRAWER_HEIGHT_MS + 20 : 20,
1171
1185
  );
1172
1186
  },
1173
- [expandedThroughlines, fitOverview, zoomOnThroughlineFocus],
1187
+ [expandedWalkthroughs, fitOverview, zoomOnWalkthroughFocus],
1174
1188
  );
1175
1189
 
1176
1190
  // Filename-badge clicks on nodes open the drawer through the same path as
@@ -1200,7 +1214,7 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
1200
1214
  if (!comp || comp.id === selectedRef.current?.id) return;
1201
1215
  setSelected(comp);
1202
1216
  setSelectedEdgeId(null);
1203
- setFocusedThroughlineId(null);
1217
+ setFocusedWalkthroughId(null);
1204
1218
  setFocusedStepIndex(null);
1205
1219
  onSelect?.(comp.id);
1206
1220
  },
@@ -1266,11 +1280,11 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
1266
1280
  [],
1267
1281
  );
1268
1282
 
1269
- const throughlineViewerRef = useRef(renderThroughlineViewer);
1270
- throughlineViewerRef.current = renderThroughlineViewer;
1271
- const renderThroughlineDrawerContent = useCallback(
1272
- (ctx: ThroughlineViewerContext) =>
1273
- throughlineViewerRef.current?.(ctx) ?? null,
1283
+ const walkthroughViewerRef = useRef(renderWalkthroughViewer);
1284
+ walkthroughViewerRef.current = renderWalkthroughViewer;
1285
+ const renderWalkthroughDrawerContent = useCallback(
1286
+ (ctx: WalkthroughViewerContext) =>
1287
+ walkthroughViewerRef.current?.(ctx) ?? null,
1274
1288
  [],
1275
1289
  );
1276
1290
 
@@ -1279,7 +1293,7 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
1279
1293
  {/* Sidebar: scrollable title/description on top, files or flows pinned below.
1280
1294
  The description hides by default so files/flows get the room; the
1281
1295
  title-row toggle reveals it, and the lower panel yields back to 50%. */}
1282
- {!hideSidebar && (title || description || sidebarExtra || sidebarAfterDescription || treeFilePaths.length > 0 || hasThroughlines) && (
1296
+ {!hideSidebar && (title || description || sidebarExtra || sidebarAfterDescription || treeFilePaths.length > 0 || hasWalkthroughs) && (
1283
1297
  <div
1284
1298
  style={{
1285
1299
  width: sidebarWidth,
@@ -1368,7 +1382,7 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
1368
1382
  )}
1369
1383
  {sidebarAfterDescription}
1370
1384
  </div>
1371
- {(treeFilePaths.length > 0 || hasThroughlines) && (
1385
+ {(treeFilePaths.length > 0 || hasWalkthroughs) && (
1372
1386
  <div
1373
1387
  style={{
1374
1388
  ...(showDesc ? { height: '50%' as const } : { flex: 1, minHeight: 0 }),
@@ -1380,7 +1394,7 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
1380
1394
  overflow: 'hidden',
1381
1395
  }}
1382
1396
  >
1383
- {hasThroughlines && (
1397
+ {hasWalkthroughs && (
1384
1398
  <div
1385
1399
  role="tablist"
1386
1400
  aria-label="Sidebar view"
@@ -1392,7 +1406,7 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
1392
1406
  background: theme.colors.backgroundSecondary ?? theme.colors.background,
1393
1407
  }}
1394
1408
  >
1395
- {(['flows', 'files'] as const).map((view) => (
1409
+ {(['walkthroughs', 'files'] as const).map((view) => (
1396
1410
  <button
1397
1411
  key={view}
1398
1412
  type="button"
@@ -1416,12 +1430,12 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
1416
1430
  cursor: 'pointer',
1417
1431
  }}
1418
1432
  >
1419
- {view}
1433
+ {view === 'walkthroughs' ? 'Walkthroughs' : 'Files'}
1420
1434
  </button>
1421
1435
  ))}
1422
1436
  </div>
1423
1437
  )}
1424
- {sidebarView === 'flows' && throughlines && throughlines.length > 0 ? (
1438
+ {sidebarView === 'walkthroughs' && walkthroughs && walkthroughs.length > 0 ? (
1425
1439
  <div
1426
1440
  style={{
1427
1441
  flex: 1,
@@ -1430,30 +1444,29 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
1430
1444
  padding: '0 0 12px',
1431
1445
  }}
1432
1446
  >
1433
- {throughlines.map((tl) => (
1434
- <ThroughlineFlow
1447
+ {walkthroughs.map((tl) => (
1448
+ <WalkthroughFlow
1435
1449
  key={tl.id}
1436
- throughline={tl}
1437
- edges={edges}
1438
- collapsed={!expandedThroughlines.has(tl.id)}
1450
+ walkthrough={tl}
1451
+ collapsed={!expandedWalkthroughs.has(tl.id)}
1439
1452
  active={
1440
- focusedThroughlineId === tl.id
1453
+ focusedWalkthroughId === tl.id
1441
1454
  ? { stepIndex: focusedStepIndex }
1442
- : hoveredThroughlineStep?.throughlineId === tl.id
1443
- ? { stepIndex: hoveredThroughlineStep.stepIndex }
1455
+ : hoveredWalkthroughStep?.walkthroughId === tl.id
1456
+ ? { stepIndex: hoveredWalkthroughStep.stepIndex }
1444
1457
  : null
1445
1458
  }
1446
- onToggleCollapsed={toggleThroughlineCollapsed}
1447
- onFocusFlow={focusThroughlineEdges}
1448
- onClearFocus={clearThroughlineFocus}
1449
- onFocusStep={focusThroughlineStep}
1459
+ onToggleCollapsed={toggleWalkthroughCollapsed}
1460
+ onFocusFlow={focusWalkthroughEdges}
1461
+ onClearFocus={clearWalkthroughFocus}
1462
+ onFocusStep={focusWalkthroughStep}
1450
1463
  onHoverStep={(tl, i) =>
1451
- setHoveredThroughlineStep({ throughlineId: tl.id, stepIndex: i })
1464
+ setHoveredWalkthroughStep({ walkthroughId: tl.id, stepIndex: i })
1452
1465
  }
1453
1466
  onHoverFlow={(tl) =>
1454
- setHoveredThroughlineStep({ throughlineId: tl.id, stepIndex: null })
1467
+ setHoveredWalkthroughStep({ walkthroughId: tl.id, stepIndex: null })
1455
1468
  }
1456
- onLeaveStep={() => setHoveredThroughlineStep(null)}
1469
+ onLeaveStep={() => setHoveredWalkthroughStep(null)}
1457
1470
  />
1458
1471
  ))}
1459
1472
  </div>
@@ -1500,7 +1513,7 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
1500
1513
  )}
1501
1514
 
1502
1515
  {/* Drag handle between sidebar and canvas — resize the left panel. */}
1503
- {!hideSidebar && (title || description || sidebarExtra || sidebarAfterDescription || treeFilePaths.length > 0 || hasThroughlines) && (
1516
+ {!hideSidebar && (title || description || sidebarExtra || sidebarAfterDescription || treeFilePaths.length > 0 || hasWalkthroughs) && (
1504
1517
  <div
1505
1518
  onMouseDown={onSidebarResizeStart}
1506
1519
  aria-label="Resize sidebar"
@@ -1625,9 +1638,9 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
1625
1638
  <Background variant={BackgroundVariant.Dots} gap={16} size={1} />
1626
1639
  <Controls showZoom showFitView showInteractive />
1627
1640
  </ReactFlow>
1628
- {/* Graph / throughline / step titles — non-interactive chips at the top
1641
+ {/* Graph / walkthrough / step titles — non-interactive chips at the top
1629
1642
  of the canvas. Graph-only embeds use these without opening the sidebar. */}
1630
- {(graphTitle || overlayThroughlineTitle || overlayThroughlineStep) && (
1643
+ {(graphTitle || overlayWalkthroughTitle || overlayWalkthroughStep) && (
1631
1644
  <div
1632
1645
  style={{
1633
1646
  position: 'absolute',
@@ -1664,11 +1677,11 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
1664
1677
  {graphTitle}
1665
1678
  </div>
1666
1679
  )}
1667
- {overlayThroughlineTitle && (
1680
+ {overlayWalkthroughTitle && (
1668
1681
  <div
1669
1682
  style={{
1670
1683
  maxWidth: '100%',
1671
- minWidth: overlayThroughlineStep ? 160 : undefined,
1684
+ minWidth: overlayWalkthroughStep ? 160 : undefined,
1672
1685
  display: 'flex',
1673
1686
  flexDirection: 'column',
1674
1687
  background: theme.colors.backgroundSecondary ?? theme.colors.background,
@@ -1679,9 +1692,9 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
1679
1692
  opacity: 0.95,
1680
1693
  }}
1681
1694
  aria-label={
1682
- overlayThroughlineStep
1683
- ? `${overlayThroughlineTitle}, step ${overlayThroughlineStep.index} of ${overlayThroughlineStep.total}`
1684
- : overlayThroughlineTitle
1695
+ overlayWalkthroughStep
1696
+ ? `${overlayWalkthroughTitle}, step ${overlayWalkthroughStep.index} of ${overlayWalkthroughStep.total}`
1697
+ : overlayWalkthroughTitle
1685
1698
  }
1686
1699
  >
1687
1700
  <div
@@ -1697,9 +1710,9 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
1697
1710
  textAlign: 'center',
1698
1711
  }}
1699
1712
  >
1700
- {overlayThroughlineTitle}
1713
+ {overlayWalkthroughTitle}
1701
1714
  </div>
1702
- {overlayThroughlineStep && overlayThroughlineStep.total > 0 && (
1715
+ {overlayWalkthroughStep && overlayWalkthroughStep.total > 0 && (
1703
1716
  <div
1704
1717
  style={{
1705
1718
  display: 'flex',
@@ -1708,10 +1721,10 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
1708
1721
  }}
1709
1722
  aria-hidden="true"
1710
1723
  >
1711
- {Array.from({ length: overlayThroughlineStep.total }, (_, i) => {
1724
+ {Array.from({ length: overlayWalkthroughStep.total }, (_, i) => {
1712
1725
  const n = i + 1;
1713
- const active = n === overlayThroughlineStep.index;
1714
- const done = n < overlayThroughlineStep.index;
1726
+ const active = n === overlayWalkthroughStep.index;
1727
+ const done = n < overlayWalkthroughStep.index;
1715
1728
  return (
1716
1729
  <span
1717
1730
  key={n}
@@ -1734,7 +1747,7 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
1734
1747
  )}
1735
1748
  </div>
1736
1749
  )}
1737
- {overlayThroughlineStep?.annotation && (
1750
+ {overlayWalkthroughStep?.annotation && (
1738
1751
  <div
1739
1752
  style={{
1740
1753
  maxWidth: '100%',
@@ -1750,7 +1763,7 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
1750
1763
  lineHeight: 1.35,
1751
1764
  }}
1752
1765
  >
1753
- {overlayThroughlineStep.annotation}
1766
+ {overlayWalkthroughStep.annotation}
1754
1767
  </div>
1755
1768
  )}
1756
1769
  </div>
@@ -1784,12 +1797,12 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
1784
1797
  )}
1785
1798
  </div>
1786
1799
  <FileDrawer title={drawerTitle} onClose={() => setDrawerTarget(null)}>
1787
- {drawerTarget?.kind === 'throughline' &&
1788
- focusedThroughline &&
1789
- renderThroughlineViewer ? (
1790
- <ThroughlineDrawerContent
1791
- render={renderThroughlineDrawerContent}
1792
- throughline={focusedThroughline}
1800
+ {drawerTarget?.kind === 'walkthrough' &&
1801
+ focusedWalkthrough &&
1802
+ renderWalkthroughViewer ? (
1803
+ <WalkthroughDrawerContent
1804
+ render={renderWalkthroughDrawerContent}
1805
+ walkthrough={focusedWalkthrough}
1793
1806
  stepIndex={drawerTarget.stepIndex}
1794
1807
  />
1795
1808
  ) : drawerTarget?.kind === 'file' ? (
@@ -1887,13 +1900,12 @@ function RepoGroupHeader({
1887
1900
  );
1888
1901
  }
1889
1902
 
1890
- /** One collapsible throughline in the sidebar's flows panel. Clicking the
1903
+ /** One collapsible walkthrough in the sidebar's flows panel. Clicking the
1891
1904
  * title: closed → open + select; open and unselected → select; open and
1892
1905
  * selected → close + clear focus. The right-aligned close button collapses
1893
1906
  * without selecting. A step row focuses that step's edge. */
1894
- function ThroughlineFlow({
1895
- throughline,
1896
- edges,
1907
+ function WalkthroughFlow({
1908
+ walkthrough,
1897
1909
  collapsed,
1898
1910
  active,
1899
1911
  onToggleCollapsed,
@@ -1904,24 +1916,22 @@ function ThroughlineFlow({
1904
1916
  onHoverFlow,
1905
1917
  onLeaveStep,
1906
1918
  }: {
1907
- throughline: SubsystemThroughline;
1908
- edges: SubsystemComponentEdge[];
1919
+ walkthrough: SubsystemWalkthrough;
1909
1920
  collapsed: boolean;
1910
1921
  /** `{ stepIndex: null }` = whole flow focused; `{ stepIndex }` = one step. */
1911
1922
  active: { stepIndex: number | null } | null;
1912
1923
  onToggleCollapsed: (tlId: string) => void;
1913
- onFocusFlow: (tl: SubsystemThroughline) => void;
1924
+ onFocusFlow: (tl: SubsystemWalkthrough) => void;
1914
1925
  onClearFocus: () => void;
1915
- onFocusStep: (tl: SubsystemThroughline, stepIndex: number) => void;
1916
- onHoverStep: (tl: SubsystemThroughline, stepIndex: number) => void;
1926
+ onFocusStep: (tl: SubsystemWalkthrough, stepIndex: number) => void;
1927
+ onHoverStep: (tl: SubsystemWalkthrough, stepIndex: number) => void;
1917
1928
  /** Preview the whole flow on the canvas (used while the row is collapsed). */
1918
- onHoverFlow: (tl: SubsystemThroughline) => void;
1929
+ onHoverFlow: (tl: SubsystemWalkthrough) => void;
1919
1930
  onLeaveStep: () => void;
1920
1931
  }) {
1921
1932
  const { theme } = useTheme();
1922
1933
  const muted = theme.colors.textMuted ?? theme.colors.textSecondary;
1923
1934
  const hoverBg = theme.colors.background;
1924
- const edgeById = useMemo(() => new Map(edges.map((e) => [e.id, e])), [edges]);
1925
1935
  const wholeFlowActive = active !== null && active.stepIndex === null;
1926
1936
  const [headerHover, setHeaderHover] = useState(false);
1927
1937
  const [closeHover, setCloseHover] = useState(false);
@@ -1945,9 +1955,9 @@ function ThroughlineFlow({
1945
1955
  useEffect(() => () => stopPlaying(), []);
1946
1956
 
1947
1957
  const startPlaying = useCallback(() => {
1948
- if (collapsed) onToggleCollapsed(throughline.id);
1949
- if (active === null || active.stepIndex !== null) onFocusFlow(throughline);
1950
- const stepCount = throughline.steps.length;
1958
+ if (collapsed) onToggleCollapsed(walkthrough.id);
1959
+ if (active === null || active.stepIndex !== null) onFocusFlow(walkthrough);
1960
+ const stepCount = walkthrough.steps.length;
1951
1961
  if (stepCount === 0) return;
1952
1962
  setPlaying(true);
1953
1963
  let i = 0;
@@ -1957,12 +1967,12 @@ function ThroughlineFlow({
1957
1967
  setPlaying(false);
1958
1968
  return;
1959
1969
  }
1960
- onFocusStep(throughline, i);
1970
+ onFocusStep(walkthrough, i);
1961
1971
  i += 1;
1962
- playTimerRef.current = window.setTimeout(tick, THROUGHLINE_PLAY_PAUSE_MS);
1972
+ playTimerRef.current = window.setTimeout(tick, WALKTHROUGH_PLAY_PAUSE_MS);
1963
1973
  };
1964
1974
  tick();
1965
- }, [collapsed, active, onToggleCollapsed, onFocusFlow, throughline, onFocusStep]);
1975
+ }, [collapsed, active, onToggleCollapsed, onFocusFlow, walkthrough, onFocusStep]);
1966
1976
 
1967
1977
  const togglePlay = useCallback(() => {
1968
1978
  if (playing) {
@@ -1985,7 +1995,7 @@ function ThroughlineFlow({
1985
1995
  <div
1986
1996
  onMouseEnter={() => {
1987
1997
  setHeaderHover(true);
1988
- if (collapsed) onHoverFlow(throughline);
1998
+ if (collapsed) onHoverFlow(walkthrough);
1989
1999
  }}
1990
2000
  onMouseLeave={() => {
1991
2001
  setHeaderHover(false);
@@ -2004,12 +2014,12 @@ function ThroughlineFlow({
2004
2014
  type="button"
2005
2015
  onClick={() => {
2006
2016
  if (collapsed) {
2007
- onToggleCollapsed(throughline.id);
2008
- onFocusFlow(throughline);
2017
+ onToggleCollapsed(walkthrough.id);
2018
+ onFocusFlow(walkthrough);
2009
2019
  } else if (active === null) {
2010
- onFocusFlow(throughline);
2020
+ onFocusFlow(walkthrough);
2011
2021
  } else {
2012
- onToggleCollapsed(throughline.id);
2022
+ onToggleCollapsed(walkthrough.id);
2013
2023
  onClearFocus();
2014
2024
  }
2015
2025
  }}
@@ -2036,14 +2046,14 @@ function ThroughlineFlow({
2036
2046
  color: theme.colors.text,
2037
2047
  }}
2038
2048
  >
2039
- {throughline.title}
2049
+ {walkthrough.title}
2040
2050
  </span>
2041
2051
  </button>
2042
2052
  {!collapsed && (
2043
2053
  <>
2044
2054
  <button
2045
2055
  type="button"
2046
- aria-label={playing ? `Pause ${throughline.title} autoplay` : `Play ${throughline.title}`}
2056
+ aria-label={playing ? `Pause ${walkthrough.title} autoplay` : `Play ${walkthrough.title}`}
2047
2057
  title={playing ? 'Pause' : 'Play through steps'}
2048
2058
  onMouseEnter={() => setPlayHover(true)}
2049
2059
  onMouseLeave={() => setPlayHover(false)}
@@ -2071,12 +2081,12 @@ function ThroughlineFlow({
2071
2081
  </button>
2072
2082
  <button
2073
2083
  type="button"
2074
- aria-label={`Close ${throughline.title}`}
2084
+ aria-label={`Close ${walkthrough.title}`}
2075
2085
  onMouseEnter={() => setCloseHover(true)}
2076
2086
  onMouseLeave={() => setCloseHover(false)}
2077
2087
  onClick={(e) => {
2078
2088
  e.stopPropagation();
2079
- onToggleCollapsed(throughline.id);
2089
+ onToggleCollapsed(walkthrough.id);
2080
2090
  if (active !== null) onClearFocus();
2081
2091
  }}
2082
2092
  style={{
@@ -2108,23 +2118,22 @@ function ThroughlineFlow({
2108
2118
  onLeaveStep();
2109
2119
  }}
2110
2120
  >
2111
- {throughline.steps.map((step, i) => {
2112
- const edge = edgeById.get(step.edgeId);
2113
- const mech = edge?.mechanism;
2121
+ {walkthrough.steps.map((step, i) => {
2122
+ const mech = step.mechanism;
2114
2123
  const color = mech ? MECHANISM_COLOR[mech] : muted;
2115
2124
  const stepActive = active !== null && active.stepIndex === i;
2116
2125
  return (
2117
2126
  <button
2118
- key={`${step.edgeId}-${i}`}
2127
+ key={`${walkthroughStepGraphEdgeId(step)}-${i}`}
2119
2128
  ref={(el) => {
2120
2129
  stepButtonRefs.current[i] = el;
2121
2130
  }}
2122
2131
  type="button"
2123
2132
  onMouseEnter={() => {
2124
2133
  setHoveredStep(i);
2125
- onHoverStep(throughline, i);
2134
+ onHoverStep(walkthrough, i);
2126
2135
  }}
2127
- onClick={() => onFocusStep(throughline, i)}
2136
+ onClick={() => onFocusStep(walkthrough, i)}
2128
2137
  style={{
2129
2138
  display: 'flex',
2130
2139
  alignItems: 'center',
@@ -2180,7 +2189,7 @@ function ThroughlineFlow({
2180
2189
  color,
2181
2190
  }}
2182
2191
  >
2183
- {mech ?? step.edgeId}
2192
+ {mech ?? step.mechanism}
2184
2193
  </span>
2185
2194
  <span
2186
2195
  style={{