@principal-ai/subsystems-react 0.20.5 → 0.21.0

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 (87) 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/graphify/construct.d.ts +23 -0
  9. package/dist/graphify/construct.d.ts.map +1 -0
  10. package/dist/graphify/construct.js +77 -0
  11. package/dist/graphify/construct.js.map +1 -0
  12. package/dist/graphify/index.d.ts +2 -2
  13. package/dist/graphify/index.d.ts.map +1 -1
  14. package/dist/graphify/index.js +1 -1
  15. package/dist/graphify/index.js.map +1 -1
  16. package/dist/index.d.ts +7 -7
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +3 -3
  19. package/dist/index.js.map +1 -1
  20. package/dist/pierre/PierreWalkthroughCodeView.d.ts +22 -0
  21. package/dist/pierre/PierreWalkthroughCodeView.d.ts.map +1 -0
  22. package/dist/pierre/PierreWalkthroughCodeView.js +198 -0
  23. package/dist/pierre/PierreWalkthroughCodeView.js.map +1 -0
  24. package/dist/pierre/index.d.ts +2 -2
  25. package/dist/pierre/index.js +1 -1
  26. package/dist/pierre/pierreFileLang.d.ts +1 -1
  27. package/dist/pierre/pierreFileLang.js +1 -1
  28. package/dist/stories/Subsystem/ComponentGraph/fixtures.d.ts +14 -2
  29. package/dist/stories/Subsystem/ComponentGraph/fixtures.d.ts.map +1 -1
  30. package/dist/stories/Subsystem/ComponentGraph/fixtures.js +86 -20
  31. package/dist/stories/Subsystem/ComponentGraph/fixtures.js.map +1 -1
  32. package/dist/subsystem/ComponentDeclaration.js +1 -1
  33. package/dist/subsystem/ComponentDeclaration.js.map +1 -1
  34. package/dist/subsystem/FileDrawer.d.ts +2 -2
  35. package/dist/subsystem/FileDrawer.js +2 -2
  36. package/dist/subsystem/SubsystemComponentGraph.d.ts +25 -24
  37. package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
  38. package/dist/subsystem/SubsystemComponentGraph.js +220 -194
  39. package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
  40. package/dist/subsystem/formatDeclaration.d.ts +2 -1
  41. package/dist/subsystem/formatDeclaration.d.ts.map +1 -1
  42. package/dist/subsystem/formatDeclaration.js +57 -33
  43. package/dist/subsystem/formatDeclaration.js.map +1 -1
  44. package/dist/subsystem/model.d.ts +109 -42
  45. package/dist/subsystem/model.d.ts.map +1 -1
  46. package/dist/subsystem/model.js +107 -5
  47. package/dist/subsystem/model.js.map +1 -1
  48. package/dist/subsystem/nodes.d.ts.map +1 -1
  49. package/dist/subsystem/nodes.js +43 -18
  50. package/dist/subsystem/nodes.js.map +1 -1
  51. package/dist/subsystem/tokenizeComponent.js +2 -2
  52. package/dist/subsystem/tokenizeComponent.js.map +1 -1
  53. package/package.json +3 -3
  54. package/src/graphify/anchor.test.ts +31 -2
  55. package/src/graphify/anchor.ts +46 -0
  56. package/src/graphify/consolidated.ts +43 -1
  57. package/src/graphify/{kind.test.ts → construct.test.ts} +20 -20
  58. package/src/graphify/{kind.ts → construct.ts} +17 -17
  59. package/src/graphify/index.ts +5 -2
  60. package/src/index.ts +20 -9
  61. package/src/pierre/{PierreThroughlineCodeView.tsx → PierreWalkthroughCodeView.tsx} +39 -47
  62. package/src/pierre/index.ts +2 -2
  63. package/src/pierre/pierreFileLang.ts +1 -1
  64. package/src/stories/Pierre/CodeView.stories.tsx +3 -3
  65. package/src/stories/Subsystem/ComponentDeclarationAudit.stories.tsx +14 -14
  66. package/src/stories/Subsystem/ComponentGraph/Appearance.stories.tsx +13 -15
  67. package/src/stories/Subsystem/ComponentGraph/Basics.stories.tsx +8 -8
  68. package/src/stories/Subsystem/ComponentGraph/Captures.stories.tsx +16 -20
  69. package/src/stories/Subsystem/ComponentGraph/CustomEntities.stories.tsx +11 -10
  70. package/src/stories/Subsystem/ComponentGraph/DetailPanel.stories.tsx +13 -14
  71. package/src/stories/Subsystem/ComponentGraph/Flows.stories.tsx +31 -66
  72. package/src/stories/Subsystem/ComponentGraph/FrameworkStereotype.stories.tsx +2 -2
  73. package/src/stories/Subsystem/ComponentGraph/GraphTitle.stories.tsx +4 -4
  74. package/src/stories/Subsystem/ComponentGraph/Processes.stories.tsx +3 -3
  75. package/src/stories/Subsystem/ComponentGraph/Proposed.stories.tsx +254 -0
  76. package/src/stories/Subsystem/ComponentGraph/Scenarios.stories.tsx +518 -33
  77. package/src/stories/Subsystem/ComponentGraph/Spotlights.stories.tsx +13 -13
  78. package/src/stories/Subsystem/ComponentGraph/fixtures.ts +105 -21
  79. package/src/subsystem/ComponentDeclaration.tsx +1 -1
  80. package/src/subsystem/FileDrawer.tsx +2 -2
  81. package/src/subsystem/SubsystemComponentGraph.tsx +284 -254
  82. package/src/subsystem/formatDeclaration.test.ts +76 -1
  83. package/src/subsystem/formatDeclaration.ts +57 -33
  84. package/src/subsystem/model.test.ts +49 -15
  85. package/src/subsystem/model.ts +210 -51
  86. package/src/subsystem/nodes.tsx +63 -22
  87. package/src/subsystem/tokenizeComponent.ts +2 -2
@@ -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,68 +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
949
- // drawer to that step's snippet.
962
+ // Focus a single step's edge on the canvas and open/scroll the walkthrough
963
+ // drawer to that step's snippet. Clicking the already-focused step clears
964
+ // step focus and returns to whole-flow (high-level) framing.
950
965
  // In `dim` mode: only dim non-participants (same as hovering a step) —
951
966
  // no camera move, no drawer, no hide.
952
- const focusThroughlineStep = useCallback(
953
- (tl: SubsystemThroughline, stepIndex: number) => {
967
+ const focusWalkthroughStep = useCallback(
968
+ (tl: SubsystemWalkthrough, stepIndex: number) => {
954
969
  const step = tl.steps[stepIndex];
955
970
  if (!step) return;
956
971
  setSelected(null);
957
972
  setSelectedEdgeId(null);
958
- if (throughlineStepMode === 'dim') {
973
+ if (walkthroughStepMode === 'dim') {
959
974
  setFocusedStepIndex(null);
960
- setFocusedThroughlineId(null);
961
- setHoveredThroughlineStep({ throughlineId: tl.id, stepIndex });
962
- setDrawerTarget((prev) => (prev?.kind === 'throughline' ? null : prev));
975
+ setFocusedWalkthroughId(null);
976
+ setHoveredWalkthroughStep({ walkthroughId: tl.id, stepIndex });
977
+ setDrawerTarget((prev) => (prev?.kind === 'walkthrough' ? null : prev));
978
+ return;
979
+ }
980
+ // Toggle: clicking the selected step unselects it (back to whole flow).
981
+ if (focusedWalkthroughId === tl.id && focusedStepIndex === stepIndex) {
982
+ setFocusedStepIndex(null);
983
+ // Pointer is still over the row — keep hover preview so dimming doesn't
984
+ // flash off until mouseleave.
985
+ setHoveredWalkthroughStep({ walkthroughId: tl.id, stepIndex });
986
+ setDrawerTarget((prev) => (prev?.kind === 'walkthrough' ? null : prev));
987
+ if (pendingFocusFitRef.current != null) {
988
+ window.clearTimeout(pendingFocusFitRef.current);
989
+ pendingFocusFitRef.current = null;
990
+ }
991
+ fitFocusBounds(new Set(tl.steps.map((s) => walkthroughStepGraphEdgeId(s))));
963
992
  return;
964
993
  }
965
994
  setFocusedStepIndex(stepIndex);
966
- setFocusedThroughlineId(tl.id);
967
- setHoveredThroughlineStep(null);
995
+ setFocusedWalkthroughId(tl.id);
996
+ setHoveredWalkthroughStep(null);
968
997
  // Open the drawer before fitting. If it was closed, wait for its height
969
998
  // transition so fitView uses the reduced canvas — not full height.
970
999
  const drawerWasOpen = drawerOpenRef.current;
971
- if (renderThroughlineViewer) {
1000
+ if (renderWalkthroughViewer) {
972
1001
  setDrawerTarget({
973
- kind: 'throughline',
974
- throughlineId: tl.id,
1002
+ kind: 'walkthrough',
1003
+ walkthroughId: tl.id,
975
1004
  stepIndex,
976
1005
  });
977
1006
  } else {
@@ -981,7 +1010,7 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
981
1010
  startLine: step.line,
982
1011
  });
983
1012
  }
984
- const edgeIds = new Set([step.edgeId]);
1013
+ const edgeIds = new Set([walkthroughStepGraphEdgeId(step)]);
985
1014
  if (pendingFocusFitRef.current != null) {
986
1015
  window.clearTimeout(pendingFocusFitRef.current);
987
1016
  pendingFocusFitRef.current = null;
@@ -995,7 +1024,13 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
995
1024
  }, FILE_DRAWER_HEIGHT_MS + 20);
996
1025
  }
997
1026
  },
998
- [fitFocusBounds, renderThroughlineViewer, throughlineStepMode],
1027
+ [
1028
+ fitFocusBounds,
1029
+ focusedStepIndex,
1030
+ focusedWalkthroughId,
1031
+ renderWalkthroughViewer,
1032
+ walkthroughStepMode,
1033
+ ],
999
1034
  );
1000
1035
 
1001
1036
  useEffect(() => {
@@ -1016,9 +1051,9 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
1016
1051
  const prev = prevPreviewEdgeIdsRef.current;
1017
1052
  prevPreviewEdgeIdsRef.current = previewEdgeIds;
1018
1053
 
1019
- if (!zoomOnThroughlineFocus || throughlineStepMode === 'dim') return;
1054
+ if (!zoomOnWalkthroughFocus || walkthroughStepMode === 'dim') return;
1020
1055
  // Camera follows hover only when a specific step is already focused.
1021
- if (focusedThroughlineId == null || focusedStepIndex == null) return;
1056
+ if (focusedWalkthroughId == null || focusedStepIndex == null) return;
1022
1057
 
1023
1058
  if (previewEdgeIds) {
1024
1059
  fitFocusBounds(previewEdgeIds);
@@ -1030,33 +1065,33 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
1030
1065
  }, [
1031
1066
  previewEdgeIds,
1032
1067
  focusEdgeIds,
1033
- focusedThroughlineId,
1068
+ focusedWalkthroughId,
1034
1069
  focusedStepIndex,
1035
1070
  fitFocusBounds,
1036
- zoomOnThroughlineFocus,
1037
- throughlineStepMode,
1071
+ zoomOnWalkthroughFocus,
1072
+ walkthroughStepMode,
1038
1073
  ]);
1039
1074
 
1040
- // Graph-only embeds: cycle throughline steps with hover-style dimming.
1075
+ // Graph-only embeds: cycle walkthrough steps with hover-style dimming.
1041
1076
  useEffect(() => {
1042
- if (!autoPlayThroughlines || !throughlines?.length || !layoutReady) return;
1043
- 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);
1044
1079
  if (playable.length === 0) return;
1045
1080
 
1046
1081
  let cancelled = false;
1047
1082
  let tlIdx = 0;
1048
1083
  let stepIdx = 0;
1049
1084
  let timer: number | null = null;
1050
- const interval = Math.max(400, throughlineAutoPlayIntervalMs);
1085
+ const interval = Math.max(400, walkthroughAutoPlayIntervalMs);
1051
1086
 
1052
1087
  const tick = () => {
1053
1088
  if (cancelled) return;
1054
1089
  const tl = playable[tlIdx]!;
1055
1090
  setSelected(null);
1056
1091
  setSelectedEdgeId(null);
1057
- setFocusedThroughlineId(null);
1092
+ setFocusedWalkthroughId(null);
1058
1093
  setFocusedStepIndex(null);
1059
- setHoveredThroughlineStep({ throughlineId: tl.id, stepIndex: stepIdx });
1094
+ setHoveredWalkthroughStep({ walkthroughId: tl.id, stepIndex: stepIdx });
1060
1095
  stepIdx += 1;
1061
1096
  if (stepIdx >= tl.steps.length) {
1062
1097
  stepIdx = 0;
@@ -1069,17 +1104,17 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
1069
1104
  return () => {
1070
1105
  cancelled = true;
1071
1106
  if (timer != null) window.clearTimeout(timer);
1072
- setHoveredThroughlineStep(null);
1107
+ setHoveredWalkthroughStep(null);
1073
1108
  };
1074
- }, [autoPlayThroughlines, throughlines, throughlineAutoPlayIntervalMs, layoutReady]);
1109
+ }, [autoPlayWalkthroughs, walkthroughs, walkthroughAutoPlayIntervalMs, layoutReady]);
1075
1110
 
1076
- // 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
1077
1112
  // (sidebar click or drawer open). Ignores typing targets and chords.
1078
1113
  useEffect(() => {
1079
- if (focusedThroughlineId == null || focusedStepIndex == null || !throughlines) {
1114
+ if (focusedWalkthroughId == null || focusedStepIndex == null || !walkthroughs) {
1080
1115
  return;
1081
1116
  }
1082
- const tl = throughlines.find((t) => t.id === focusedThroughlineId);
1117
+ const tl = walkthroughs.find((t) => t.id === focusedWalkthroughId);
1083
1118
  if (!tl || tl.steps.length === 0) return;
1084
1119
 
1085
1120
  const onKeyDown = (e: KeyboardEvent) => {
@@ -1105,31 +1140,31 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
1105
1140
  }
1106
1141
  if (next === focusedStepIndex) return;
1107
1142
  e.preventDefault();
1108
- focusThroughlineStep(tl, next);
1143
+ focusWalkthroughStep(tl, next);
1109
1144
  };
1110
1145
 
1111
1146
  window.addEventListener('keydown', onKeyDown);
1112
1147
  return () => window.removeEventListener('keydown', onKeyDown);
1113
1148
  }, [
1114
- focusedThroughlineId,
1149
+ focusedWalkthroughId,
1115
1150
  focusedStepIndex,
1116
- throughlines,
1117
- focusThroughlineStep,
1151
+ walkthroughs,
1152
+ focusWalkthroughStep,
1118
1153
  ]);
1119
1154
 
1120
- const toggleThroughlineCollapsed = useCallback(
1155
+ const toggleWalkthroughCollapsed = useCallback(
1121
1156
  (tlId: string) => {
1122
1157
  const collapsingLast =
1123
- expandedThroughlines.has(tlId) && expandedThroughlines.size === 1;
1158
+ expandedWalkthroughs.has(tlId) && expandedWalkthroughs.size === 1;
1124
1159
 
1125
- setExpandedThroughlines((prev) => {
1160
+ setExpandedWalkthroughs((prev) => {
1126
1161
  const next = new Set(prev);
1127
1162
  if (next.has(tlId)) next.delete(tlId);
1128
1163
  else next.add(tlId);
1129
1164
  return next;
1130
1165
  });
1131
1166
 
1132
- if (!collapsingLast || !zoomOnThroughlineFocus) return;
1167
+ if (!collapsingLast || !zoomOnWalkthroughFocus) return;
1133
1168
 
1134
1169
  // Cancel any pending focus fit so it doesn't fight the overview zoom.
1135
1170
  if (pendingFocusFitRef.current != null) {
@@ -1149,7 +1184,7 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
1149
1184
  drawerWasOpen ? FILE_DRAWER_HEIGHT_MS + 20 : 20,
1150
1185
  );
1151
1186
  },
1152
- [expandedThroughlines, fitOverview, zoomOnThroughlineFocus],
1187
+ [expandedWalkthroughs, fitOverview, zoomOnWalkthroughFocus],
1153
1188
  );
1154
1189
 
1155
1190
  // Filename-badge clicks on nodes open the drawer through the same path as
@@ -1179,7 +1214,7 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
1179
1214
  if (!comp || comp.id === selectedRef.current?.id) return;
1180
1215
  setSelected(comp);
1181
1216
  setSelectedEdgeId(null);
1182
- setFocusedThroughlineId(null);
1217
+ setFocusedWalkthroughId(null);
1183
1218
  setFocusedStepIndex(null);
1184
1219
  onSelect?.(comp.id);
1185
1220
  },
@@ -1245,11 +1280,11 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
1245
1280
  [],
1246
1281
  );
1247
1282
 
1248
- const throughlineViewerRef = useRef(renderThroughlineViewer);
1249
- throughlineViewerRef.current = renderThroughlineViewer;
1250
- const renderThroughlineDrawerContent = useCallback(
1251
- (ctx: ThroughlineViewerContext) =>
1252
- 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,
1253
1288
  [],
1254
1289
  );
1255
1290
 
@@ -1258,7 +1293,7 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
1258
1293
  {/* Sidebar: scrollable title/description on top, files or flows pinned below.
1259
1294
  The description hides by default so files/flows get the room; the
1260
1295
  title-row toggle reveals it, and the lower panel yields back to 50%. */}
1261
- {!hideSidebar && (title || description || sidebarExtra || sidebarAfterDescription || treeFilePaths.length > 0 || hasThroughlines) && (
1296
+ {!hideSidebar && (title || description || sidebarExtra || sidebarAfterDescription || treeFilePaths.length > 0 || hasWalkthroughs) && (
1262
1297
  <div
1263
1298
  style={{
1264
1299
  width: sidebarWidth,
@@ -1347,7 +1382,7 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
1347
1382
  )}
1348
1383
  {sidebarAfterDescription}
1349
1384
  </div>
1350
- {(treeFilePaths.length > 0 || hasThroughlines) && (
1385
+ {(treeFilePaths.length > 0 || hasWalkthroughs) && (
1351
1386
  <div
1352
1387
  style={{
1353
1388
  ...(showDesc ? { height: '50%' as const } : { flex: 1, minHeight: 0 }),
@@ -1359,7 +1394,7 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
1359
1394
  overflow: 'hidden',
1360
1395
  }}
1361
1396
  >
1362
- {hasThroughlines && (
1397
+ {hasWalkthroughs && (
1363
1398
  <div
1364
1399
  role="tablist"
1365
1400
  aria-label="Sidebar view"
@@ -1371,7 +1406,7 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
1371
1406
  background: theme.colors.backgroundSecondary ?? theme.colors.background,
1372
1407
  }}
1373
1408
  >
1374
- {(['flows', 'files'] as const).map((view) => (
1409
+ {(['walkthroughs', 'files'] as const).map((view) => (
1375
1410
  <button
1376
1411
  key={view}
1377
1412
  type="button"
@@ -1395,12 +1430,12 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
1395
1430
  cursor: 'pointer',
1396
1431
  }}
1397
1432
  >
1398
- {view}
1433
+ {view === 'walkthroughs' ? 'Walkthroughs' : 'Files'}
1399
1434
  </button>
1400
1435
  ))}
1401
1436
  </div>
1402
1437
  )}
1403
- {sidebarView === 'flows' && throughlines && throughlines.length > 0 ? (
1438
+ {sidebarView === 'walkthroughs' && walkthroughs && walkthroughs.length > 0 ? (
1404
1439
  <div
1405
1440
  style={{
1406
1441
  flex: 1,
@@ -1409,30 +1444,29 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
1409
1444
  padding: '0 0 12px',
1410
1445
  }}
1411
1446
  >
1412
- {throughlines.map((tl) => (
1413
- <ThroughlineFlow
1447
+ {walkthroughs.map((tl) => (
1448
+ <WalkthroughFlow
1414
1449
  key={tl.id}
1415
- throughline={tl}
1416
- edges={edges}
1417
- collapsed={!expandedThroughlines.has(tl.id)}
1450
+ walkthrough={tl}
1451
+ collapsed={!expandedWalkthroughs.has(tl.id)}
1418
1452
  active={
1419
- focusedThroughlineId === tl.id
1453
+ focusedWalkthroughId === tl.id
1420
1454
  ? { stepIndex: focusedStepIndex }
1421
- : hoveredThroughlineStep?.throughlineId === tl.id
1422
- ? { stepIndex: hoveredThroughlineStep.stepIndex }
1455
+ : hoveredWalkthroughStep?.walkthroughId === tl.id
1456
+ ? { stepIndex: hoveredWalkthroughStep.stepIndex }
1423
1457
  : null
1424
1458
  }
1425
- onToggleCollapsed={toggleThroughlineCollapsed}
1426
- onFocusFlow={focusThroughlineEdges}
1427
- onClearFocus={clearThroughlineFocus}
1428
- onFocusStep={focusThroughlineStep}
1459
+ onToggleCollapsed={toggleWalkthroughCollapsed}
1460
+ onFocusFlow={focusWalkthroughEdges}
1461
+ onClearFocus={clearWalkthroughFocus}
1462
+ onFocusStep={focusWalkthroughStep}
1429
1463
  onHoverStep={(tl, i) =>
1430
- setHoveredThroughlineStep({ throughlineId: tl.id, stepIndex: i })
1464
+ setHoveredWalkthroughStep({ walkthroughId: tl.id, stepIndex: i })
1431
1465
  }
1432
1466
  onHoverFlow={(tl) =>
1433
- setHoveredThroughlineStep({ throughlineId: tl.id, stepIndex: null })
1467
+ setHoveredWalkthroughStep({ walkthroughId: tl.id, stepIndex: null })
1434
1468
  }
1435
- onLeaveStep={() => setHoveredThroughlineStep(null)}
1469
+ onLeaveStep={() => setHoveredWalkthroughStep(null)}
1436
1470
  />
1437
1471
  ))}
1438
1472
  </div>
@@ -1479,7 +1513,7 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
1479
1513
  )}
1480
1514
 
1481
1515
  {/* Drag handle between sidebar and canvas — resize the left panel. */}
1482
- {!hideSidebar && (title || description || sidebarExtra || sidebarAfterDescription || treeFilePaths.length > 0 || hasThroughlines) && (
1516
+ {!hideSidebar && (title || description || sidebarExtra || sidebarAfterDescription || treeFilePaths.length > 0 || hasWalkthroughs) && (
1483
1517
  <div
1484
1518
  onMouseDown={onSidebarResizeStart}
1485
1519
  aria-label="Resize sidebar"
@@ -1604,9 +1638,9 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
1604
1638
  <Background variant={BackgroundVariant.Dots} gap={16} size={1} />
1605
1639
  <Controls showZoom showFitView showInteractive />
1606
1640
  </ReactFlow>
1607
- {/* Graph / throughline / step titles — non-interactive chips at the top
1641
+ {/* Graph / walkthrough / step titles — non-interactive chips at the top
1608
1642
  of the canvas. Graph-only embeds use these without opening the sidebar. */}
1609
- {(graphTitle || overlayThroughlineTitle || overlayThroughlineStep) && (
1643
+ {(graphTitle || overlayWalkthroughTitle || overlayWalkthroughStep) && (
1610
1644
  <div
1611
1645
  style={{
1612
1646
  position: 'absolute',
@@ -1643,11 +1677,11 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
1643
1677
  {graphTitle}
1644
1678
  </div>
1645
1679
  )}
1646
- {overlayThroughlineTitle && (
1680
+ {overlayWalkthroughTitle && (
1647
1681
  <div
1648
1682
  style={{
1649
1683
  maxWidth: '100%',
1650
- minWidth: overlayThroughlineStep ? 160 : undefined,
1684
+ minWidth: overlayWalkthroughStep ? 160 : undefined,
1651
1685
  display: 'flex',
1652
1686
  flexDirection: 'column',
1653
1687
  background: theme.colors.backgroundSecondary ?? theme.colors.background,
@@ -1658,9 +1692,9 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
1658
1692
  opacity: 0.95,
1659
1693
  }}
1660
1694
  aria-label={
1661
- overlayThroughlineStep
1662
- ? `${overlayThroughlineTitle}, step ${overlayThroughlineStep.index} of ${overlayThroughlineStep.total}`
1663
- : overlayThroughlineTitle
1695
+ overlayWalkthroughStep
1696
+ ? `${overlayWalkthroughTitle}, step ${overlayWalkthroughStep.index} of ${overlayWalkthroughStep.total}`
1697
+ : overlayWalkthroughTitle
1664
1698
  }
1665
1699
  >
1666
1700
  <div
@@ -1676,9 +1710,9 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
1676
1710
  textAlign: 'center',
1677
1711
  }}
1678
1712
  >
1679
- {overlayThroughlineTitle}
1713
+ {overlayWalkthroughTitle}
1680
1714
  </div>
1681
- {overlayThroughlineStep && overlayThroughlineStep.total > 0 && (
1715
+ {overlayWalkthroughStep && overlayWalkthroughStep.total > 0 && (
1682
1716
  <div
1683
1717
  style={{
1684
1718
  display: 'flex',
@@ -1687,10 +1721,10 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
1687
1721
  }}
1688
1722
  aria-hidden="true"
1689
1723
  >
1690
- {Array.from({ length: overlayThroughlineStep.total }, (_, i) => {
1724
+ {Array.from({ length: overlayWalkthroughStep.total }, (_, i) => {
1691
1725
  const n = i + 1;
1692
- const active = n === overlayThroughlineStep.index;
1693
- const done = n < overlayThroughlineStep.index;
1726
+ const active = n === overlayWalkthroughStep.index;
1727
+ const done = n < overlayWalkthroughStep.index;
1694
1728
  return (
1695
1729
  <span
1696
1730
  key={n}
@@ -1713,7 +1747,7 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
1713
1747
  )}
1714
1748
  </div>
1715
1749
  )}
1716
- {overlayThroughlineStep?.annotation && (
1750
+ {overlayWalkthroughStep?.annotation && (
1717
1751
  <div
1718
1752
  style={{
1719
1753
  maxWidth: '100%',
@@ -1729,7 +1763,7 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
1729
1763
  lineHeight: 1.35,
1730
1764
  }}
1731
1765
  >
1732
- {overlayThroughlineStep.annotation}
1766
+ {overlayWalkthroughStep.annotation}
1733
1767
  </div>
1734
1768
  )}
1735
1769
  </div>
@@ -1763,12 +1797,12 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
1763
1797
  )}
1764
1798
  </div>
1765
1799
  <FileDrawer title={drawerTitle} onClose={() => setDrawerTarget(null)}>
1766
- {drawerTarget?.kind === 'throughline' &&
1767
- focusedThroughline &&
1768
- renderThroughlineViewer ? (
1769
- <ThroughlineDrawerContent
1770
- render={renderThroughlineDrawerContent}
1771
- throughline={focusedThroughline}
1800
+ {drawerTarget?.kind === 'walkthrough' &&
1801
+ focusedWalkthrough &&
1802
+ renderWalkthroughViewer ? (
1803
+ <WalkthroughDrawerContent
1804
+ render={renderWalkthroughDrawerContent}
1805
+ walkthrough={focusedWalkthrough}
1772
1806
  stepIndex={drawerTarget.stepIndex}
1773
1807
  />
1774
1808
  ) : drawerTarget?.kind === 'file' ? (
@@ -1866,13 +1900,12 @@ function RepoGroupHeader({
1866
1900
  );
1867
1901
  }
1868
1902
 
1869
- /** One collapsible throughline in the sidebar's flows panel. Clicking the
1903
+ /** One collapsible walkthrough in the sidebar's flows panel. Clicking the
1870
1904
  * title: closed → open + select; open and unselected → select; open and
1871
1905
  * selected → close + clear focus. The right-aligned close button collapses
1872
1906
  * without selecting. A step row focuses that step's edge. */
1873
- function ThroughlineFlow({
1874
- throughline,
1875
- edges,
1907
+ function WalkthroughFlow({
1908
+ walkthrough,
1876
1909
  collapsed,
1877
1910
  active,
1878
1911
  onToggleCollapsed,
@@ -1883,24 +1916,22 @@ function ThroughlineFlow({
1883
1916
  onHoverFlow,
1884
1917
  onLeaveStep,
1885
1918
  }: {
1886
- throughline: SubsystemThroughline;
1887
- edges: SubsystemComponentEdge[];
1919
+ walkthrough: SubsystemWalkthrough;
1888
1920
  collapsed: boolean;
1889
1921
  /** `{ stepIndex: null }` = whole flow focused; `{ stepIndex }` = one step. */
1890
1922
  active: { stepIndex: number | null } | null;
1891
1923
  onToggleCollapsed: (tlId: string) => void;
1892
- onFocusFlow: (tl: SubsystemThroughline) => void;
1924
+ onFocusFlow: (tl: SubsystemWalkthrough) => void;
1893
1925
  onClearFocus: () => void;
1894
- onFocusStep: (tl: SubsystemThroughline, stepIndex: number) => void;
1895
- onHoverStep: (tl: SubsystemThroughline, stepIndex: number) => void;
1926
+ onFocusStep: (tl: SubsystemWalkthrough, stepIndex: number) => void;
1927
+ onHoverStep: (tl: SubsystemWalkthrough, stepIndex: number) => void;
1896
1928
  /** Preview the whole flow on the canvas (used while the row is collapsed). */
1897
- onHoverFlow: (tl: SubsystemThroughline) => void;
1929
+ onHoverFlow: (tl: SubsystemWalkthrough) => void;
1898
1930
  onLeaveStep: () => void;
1899
1931
  }) {
1900
1932
  const { theme } = useTheme();
1901
1933
  const muted = theme.colors.textMuted ?? theme.colors.textSecondary;
1902
1934
  const hoverBg = theme.colors.background;
1903
- const edgeById = useMemo(() => new Map(edges.map((e) => [e.id, e])), [edges]);
1904
1935
  const wholeFlowActive = active !== null && active.stepIndex === null;
1905
1936
  const [headerHover, setHeaderHover] = useState(false);
1906
1937
  const [closeHover, setCloseHover] = useState(false);
@@ -1924,9 +1955,9 @@ function ThroughlineFlow({
1924
1955
  useEffect(() => () => stopPlaying(), []);
1925
1956
 
1926
1957
  const startPlaying = useCallback(() => {
1927
- if (collapsed) onToggleCollapsed(throughline.id);
1928
- if (active === null || active.stepIndex !== null) onFocusFlow(throughline);
1929
- 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;
1930
1961
  if (stepCount === 0) return;
1931
1962
  setPlaying(true);
1932
1963
  let i = 0;
@@ -1936,12 +1967,12 @@ function ThroughlineFlow({
1936
1967
  setPlaying(false);
1937
1968
  return;
1938
1969
  }
1939
- onFocusStep(throughline, i);
1970
+ onFocusStep(walkthrough, i);
1940
1971
  i += 1;
1941
- playTimerRef.current = window.setTimeout(tick, THROUGHLINE_PLAY_PAUSE_MS);
1972
+ playTimerRef.current = window.setTimeout(tick, WALKTHROUGH_PLAY_PAUSE_MS);
1942
1973
  };
1943
1974
  tick();
1944
- }, [collapsed, active, onToggleCollapsed, onFocusFlow, throughline, onFocusStep]);
1975
+ }, [collapsed, active, onToggleCollapsed, onFocusFlow, walkthrough, onFocusStep]);
1945
1976
 
1946
1977
  const togglePlay = useCallback(() => {
1947
1978
  if (playing) {
@@ -1964,7 +1995,7 @@ function ThroughlineFlow({
1964
1995
  <div
1965
1996
  onMouseEnter={() => {
1966
1997
  setHeaderHover(true);
1967
- if (collapsed) onHoverFlow(throughline);
1998
+ if (collapsed) onHoverFlow(walkthrough);
1968
1999
  }}
1969
2000
  onMouseLeave={() => {
1970
2001
  setHeaderHover(false);
@@ -1983,12 +2014,12 @@ function ThroughlineFlow({
1983
2014
  type="button"
1984
2015
  onClick={() => {
1985
2016
  if (collapsed) {
1986
- onToggleCollapsed(throughline.id);
1987
- onFocusFlow(throughline);
2017
+ onToggleCollapsed(walkthrough.id);
2018
+ onFocusFlow(walkthrough);
1988
2019
  } else if (active === null) {
1989
- onFocusFlow(throughline);
2020
+ onFocusFlow(walkthrough);
1990
2021
  } else {
1991
- onToggleCollapsed(throughline.id);
2022
+ onToggleCollapsed(walkthrough.id);
1992
2023
  onClearFocus();
1993
2024
  }
1994
2025
  }}
@@ -2015,14 +2046,14 @@ function ThroughlineFlow({
2015
2046
  color: theme.colors.text,
2016
2047
  }}
2017
2048
  >
2018
- {throughline.title}
2049
+ {walkthrough.title}
2019
2050
  </span>
2020
2051
  </button>
2021
2052
  {!collapsed && (
2022
2053
  <>
2023
2054
  <button
2024
2055
  type="button"
2025
- aria-label={playing ? `Pause ${throughline.title} autoplay` : `Play ${throughline.title}`}
2056
+ aria-label={playing ? `Pause ${walkthrough.title} autoplay` : `Play ${walkthrough.title}`}
2026
2057
  title={playing ? 'Pause' : 'Play through steps'}
2027
2058
  onMouseEnter={() => setPlayHover(true)}
2028
2059
  onMouseLeave={() => setPlayHover(false)}
@@ -2050,12 +2081,12 @@ function ThroughlineFlow({
2050
2081
  </button>
2051
2082
  <button
2052
2083
  type="button"
2053
- aria-label={`Close ${throughline.title}`}
2084
+ aria-label={`Close ${walkthrough.title}`}
2054
2085
  onMouseEnter={() => setCloseHover(true)}
2055
2086
  onMouseLeave={() => setCloseHover(false)}
2056
2087
  onClick={(e) => {
2057
2088
  e.stopPropagation();
2058
- onToggleCollapsed(throughline.id);
2089
+ onToggleCollapsed(walkthrough.id);
2059
2090
  if (active !== null) onClearFocus();
2060
2091
  }}
2061
2092
  style={{
@@ -2087,23 +2118,22 @@ function ThroughlineFlow({
2087
2118
  onLeaveStep();
2088
2119
  }}
2089
2120
  >
2090
- {throughline.steps.map((step, i) => {
2091
- const edge = edgeById.get(step.edgeId);
2092
- const mech = edge?.mechanism;
2121
+ {walkthrough.steps.map((step, i) => {
2122
+ const mech = step.mechanism;
2093
2123
  const color = mech ? MECHANISM_COLOR[mech] : muted;
2094
2124
  const stepActive = active !== null && active.stepIndex === i;
2095
2125
  return (
2096
2126
  <button
2097
- key={`${step.edgeId}-${i}`}
2127
+ key={`${walkthroughStepGraphEdgeId(step)}-${i}`}
2098
2128
  ref={(el) => {
2099
2129
  stepButtonRefs.current[i] = el;
2100
2130
  }}
2101
2131
  type="button"
2102
2132
  onMouseEnter={() => {
2103
2133
  setHoveredStep(i);
2104
- onHoverStep(throughline, i);
2134
+ onHoverStep(walkthrough, i);
2105
2135
  }}
2106
- onClick={() => onFocusStep(throughline, i)}
2136
+ onClick={() => onFocusStep(walkthrough, i)}
2107
2137
  style={{
2108
2138
  display: 'flex',
2109
2139
  alignItems: 'center',
@@ -2159,7 +2189,7 @@ function ThroughlineFlow({
2159
2189
  color,
2160
2190
  }}
2161
2191
  >
2162
- {mech ?? step.edgeId}
2192
+ {mech ?? step.mechanism}
2163
2193
  </span>
2164
2194
  <span
2165
2195
  style={{