@principal-ai/subsystems-react 0.42.0 → 0.43.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 (81) hide show
  1. package/dist/index.d.ts +2 -2
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +1 -1
  4. package/dist/index.js.map +1 -1
  5. package/dist/stories/Subsystem/C4Graph/c4Fixture.js +1 -1
  6. package/dist/stories/Subsystem/C4Graph/c4Fixture.js.map +1 -1
  7. package/dist/stories/Subsystem/ComponentGraph/fixtures.d.ts +6 -9
  8. package/dist/stories/Subsystem/ComponentGraph/fixtures.d.ts.map +1 -1
  9. package/dist/stories/Subsystem/ComponentGraph/fixtures.js +15 -50
  10. package/dist/stories/Subsystem/ComponentGraph/fixtures.js.map +1 -1
  11. package/dist/subsystem/C4Graph.d.ts +2 -3
  12. package/dist/subsystem/C4Graph.d.ts.map +1 -1
  13. package/dist/subsystem/C4Graph.js +2 -3
  14. package/dist/subsystem/C4Graph.js.map +1 -1
  15. package/dist/subsystem/IssueList.d.ts +5 -5
  16. package/dist/subsystem/IssueList.d.ts.map +1 -1
  17. package/dist/subsystem/IssueList.js +9 -18
  18. package/dist/subsystem/IssueList.js.map +1 -1
  19. package/dist/subsystem/SubsystemComponentGraph.d.ts +16 -12
  20. package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
  21. package/dist/subsystem/SubsystemComponentGraph.js +99 -113
  22. package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
  23. package/dist/subsystem/SubsystemFileTree.d.ts +5 -5
  24. package/dist/subsystem/SubsystemFileTree.d.ts.map +1 -1
  25. package/dist/subsystem/SubsystemFileTree.js +13 -49
  26. package/dist/subsystem/SubsystemFileTree.js.map +1 -1
  27. package/dist/subsystem/model.d.ts +67 -53
  28. package/dist/subsystem/model.d.ts.map +1 -1
  29. package/dist/subsystem/model.js +305 -55
  30. package/dist/subsystem/model.js.map +1 -1
  31. package/dist/subsystem/nodes.d.ts.map +1 -1
  32. package/dist/subsystem/nodes.js +11 -4
  33. package/dist/subsystem/nodes.js.map +1 -1
  34. package/dist/subsystem/toC4.d.ts +4 -4
  35. package/dist/subsystem/toC4.d.ts.map +1 -1
  36. package/dist/subsystem/toC4.js +3 -7
  37. package/dist/subsystem/toC4.js.map +1 -1
  38. package/dist/utils/elkLayout.d.ts.map +1 -1
  39. package/dist/utils/elkLayout.js +7 -0
  40. package/dist/utils/elkLayout.js.map +1 -1
  41. package/package.json +3 -3
  42. package/src/index.ts +0 -4
  43. package/src/stories/Subsystem/AgentsPanel.stories.tsx +3 -5
  44. package/src/stories/Subsystem/C4Graph/C4Container.stories.tsx +1 -4
  45. package/src/stories/Subsystem/C4Graph/c4Fixture.ts +1 -1
  46. package/src/stories/Subsystem/ComponentGraph/Appearance.stories.tsx +2 -4
  47. package/src/stories/Subsystem/ComponentGraph/Basics.stories.tsx +8 -8
  48. package/src/stories/Subsystem/ComponentGraph/Captures.stories.tsx +19 -19
  49. package/src/stories/Subsystem/ComponentGraph/Constructs.stories.tsx +0 -1
  50. package/src/stories/Subsystem/ComponentGraph/CustomEntities.stories.tsx +3 -3
  51. package/src/stories/Subsystem/ComponentGraph/DetailPanel.stories.tsx +0 -2
  52. package/src/stories/Subsystem/ComponentGraph/Diagnostics.stories.tsx +2 -6
  53. package/src/stories/Subsystem/ComponentGraph/EdgeViews.stories.tsx +21 -21
  54. package/src/stories/Subsystem/ComponentGraph/Flows.stories.tsx +0 -3
  55. package/src/stories/Subsystem/ComponentGraph/FrameworkStereotype.stories.tsx +30 -32
  56. package/src/stories/Subsystem/ComponentGraph/GraphTitle.stories.tsx +4 -4
  57. package/src/stories/Subsystem/ComponentGraph/IssueOverlay.stories.tsx +1 -26
  58. package/src/stories/Subsystem/ComponentGraph/Issues.stories.tsx +3 -25
  59. package/src/stories/Subsystem/ComponentGraph/ModuleBadges.stories.tsx +7 -8
  60. package/src/stories/Subsystem/ComponentGraph/ModulePathNesting.stories.tsx +116 -0
  61. package/src/stories/Subsystem/ComponentGraph/Modules.stories.tsx +9 -11
  62. package/src/stories/Subsystem/ComponentGraph/Packages.stories.tsx +3 -6
  63. package/src/stories/Subsystem/ComponentGraph/Processes.stories.tsx +3 -3
  64. package/src/stories/Subsystem/ComponentGraph/Proposed.stories.tsx +8 -6
  65. package/src/stories/Subsystem/ComponentGraph/ProposedWalkthroughs.stories.tsx +0 -1
  66. package/src/stories/Subsystem/ComponentGraph/Reorder.stories.tsx +0 -1
  67. package/src/stories/Subsystem/ComponentGraph/Scenarios.stories.tsx +26 -28
  68. package/src/stories/Subsystem/ComponentGraph/WalkthroughAutoplay.stories.tsx +0 -1
  69. package/src/stories/Subsystem/ComponentGraph/WorkspacePackageFrames.stories.tsx +2 -3
  70. package/src/stories/Subsystem/ComponentGraph/fixtures.ts +18 -58
  71. package/src/stories/Subsystem/EgoGraph/EgoGraph.stories.tsx +0 -2
  72. package/src/subsystem/C4Graph.tsx +2 -3
  73. package/src/subsystem/IssueList.tsx +12 -22
  74. package/src/subsystem/SubsystemComponentGraph.tsx +117 -135
  75. package/src/subsystem/SubsystemFileTree.tsx +17 -50
  76. package/src/subsystem/model.test.ts +155 -24
  77. package/src/subsystem/model.ts +352 -108
  78. package/src/subsystem/nodes.tsx +13 -3
  79. package/src/subsystem/toC4.test.ts +5 -12
  80. package/src/subsystem/toC4.ts +5 -9
  81. package/src/utils/elkLayout.ts +10 -0
@@ -20,7 +20,7 @@ import { ReactFlow, ReactFlowProvider, useReactFlow, useViewport, applyNodeChang
20
20
  import { useTheme } from '@principal-ade/industry-theme';
21
21
  import { FileText, X } from 'lucide-react';
22
22
  import { IndustryMarkdownSlide } from 'themed-markdown';
23
- import { buildSubsystemGraph, deriveGraphEdges, isConstructsOnlyModel, moduleGroupNodeId, isRelationMechanism, isWalkthroughMechanism, edgeColor, MECHANISM_DESCRIPTIONS, subsystemGraphLayoutKey, walkthroughStepGraphEdgeId, } from './model';
23
+ import { buildSubsystemGraph, deriveGraphEdges, isConstructsOnlyModel, moduleGroupNodeId, isWalkthroughMechanism, edgeColor, MECHANISM_DESCRIPTIONS, subsystemGraphLayoutKey, walkthroughStepGraphEdgeId, } from './model';
24
24
  import { ConstructsCatalog } from './ConstructsCatalog';
25
25
  import { SubsystemAgentsPanel, } from './AgentsPanel';
26
26
  import { MaintainLivePanel, } from '../components/maintain-events/MaintainLivePanel';
@@ -122,14 +122,14 @@ const WalkthroughDrawerContent = memo(function WalkthroughDrawerContent({ render
122
122
  onSymbolClick,
123
123
  }) }));
124
124
  });
125
- function Inner({ components, relations, walkthroughs, graphifyRelations, orderByLine, initialWalkthroughId, onReorderWalkthroughs, onSelect, onEdgeSelect, measured: _measured, maxNodeWidth, showEdgeLabels, showSingletonFrames = true, edgeView, title, hideSidebar, walkthroughStepMode = 'focus', autoPlayWalkthroughs = false, walkthroughAutoPlayIntervalMs = WALKTHROUGH_PLAY_PAUSE_MS, zoomOnWalkthroughFocus = true, walkthroughFocusDurationMs = 300, graphTitle, showWalkthroughTitle = false, description, canvasOverlay, sidebarExtra, sidebarAfterDescription, diagnostic, issues, showIssues, focusIssueCategory, onSelectIssue, onApplyIssueFix, onHoverIssue, renderFileView, renderFileViewer, renderWalkthroughViewer, onFileSelect, componentVerification, onInspectSymbol, boundaryColors, hideDrawer = false, persistKey, liveEvents, agentsPanel }) {
125
+ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initialWalkthroughId, onReorderWalkthroughs, onSelect, onEdgeSelect, measured: _measured, maxNodeWidth, showEdgeLabels, showSingletonFrames = true, moduleNesting, edgeView, title, hideSidebar, walkthroughStepMode = 'focus', autoPlayWalkthroughs = false, walkthroughAutoPlayIntervalMs = WALKTHROUGH_PLAY_PAUSE_MS, zoomOnWalkthroughFocus = true, walkthroughFocusDurationMs = 300, graphTitle, showWalkthroughTitle = false, description, canvasOverlay, sidebarExtra, sidebarAfterDescription, diagnostic, issues, showIssues, focusIssueCategory, onSelectIssue, onApplyIssueFix, onHoverIssue, renderFileView, renderFileViewer, renderWalkthroughViewer, onFileSelect, componentVerification, onInspectSymbol, boundaryColors, hideDrawer = false, persistKey, liveEvents, agentsPanel }) {
126
126
  const { theme } = useTheme();
127
- const { fitView, fitBounds } = useReactFlow();
127
+ const { fitView, fitBounds, screenToFlowPosition } = useReactFlow();
128
128
  const viewport = useViewport();
129
129
  // Restored once per mount from `localStorage` (see `readViewState`). Each
130
130
  // graph is its own tab/mount, so `persistKey` is stable for a mount.
131
131
  const persisted = useMemo(() => readViewState(persistKey), [persistKey]);
132
- const graphEdges = useMemo(() => deriveGraphEdges({ relations, walkthroughs }), [relations, walkthroughs]);
132
+ const graphEdges = useMemo(() => deriveGraphEdges({ walkthroughs }), [walkthroughs]);
133
133
  const [built, setBuilt] = useState({
134
134
  nodes: [],
135
135
  edges: [],
@@ -149,6 +149,71 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
149
149
  const pendingFocusFitRef = useRef(null);
150
150
  // Component the pointer is over (null on leave) → transient tree highlight.
151
151
  const [hoveredComponentAlias, setHoveredComponentId] = useState(null);
152
+ // Boundary frame hovered on the canvas → highlight the matching folder row in
153
+ // the sidebar file tree, so a frame's place in the repo tree is visible.
154
+ const [hoveredRegion, setHoveredRegion] = useState(null);
155
+ const hoveredBoundaryPath = hoveredRegion && (hoveredRegion.kind === 'module' || hoveredRegion.kind === 'directory')
156
+ ? hoveredRegion.key
157
+ : null;
158
+ // Path frames (module / directory) as absolute flow-coord rects, hit-tested
159
+ // on pointer move. A pointer hit-test keeps the frames' interior hoverable
160
+ // WITHOUT an overlay that would block edges / child nodes.
161
+ const pathFrameRects = useMemo(() => {
162
+ const out = [];
163
+ for (const n of built.nodes) {
164
+ if (n.type !== 'subsystem-group')
165
+ continue;
166
+ const region = n.data?.region;
167
+ if (!region?.key || (region.kind !== 'module' && region.kind !== 'directory'))
168
+ continue;
169
+ const r = built.absoluteRects.get(n.id);
170
+ if (!r)
171
+ continue;
172
+ out.push({ kind: region.kind, key: region.key, x: r.x, y: r.y, w: r.width, h: r.height });
173
+ }
174
+ return out;
175
+ }, [built.nodes, built.absoluteRects]);
176
+ useEffect(() => {
177
+ if (pathFrameRects.length === 0)
178
+ return;
179
+ let raf = 0;
180
+ let last = null;
181
+ const overCanvas = (e) => e.composedPath().some((el) => el instanceof HTMLElement && el.classList.contains(GRAPH_CANVAS_CLASS));
182
+ const onMove = (e) => {
183
+ if (!overCanvas(e)) {
184
+ last = null;
185
+ setHoveredRegion((prev) => (prev == null ? prev : null));
186
+ return;
187
+ }
188
+ last = { x: e.clientX, y: e.clientY };
189
+ if (raf)
190
+ return;
191
+ raf = window.requestAnimationFrame(() => {
192
+ raf = 0;
193
+ if (!last)
194
+ return;
195
+ const p = screenToFlowPosition(last);
196
+ let best = null;
197
+ let bestArea = Number.POSITIVE_INFINITY;
198
+ for (const f of pathFrameRects) {
199
+ if (p.x >= f.x && p.x <= f.x + f.w && p.y >= f.y && p.y <= f.y + f.h) {
200
+ const area = f.w * f.h;
201
+ if (area < bestArea) {
202
+ bestArea = area;
203
+ best = { kind: f.kind, key: f.key };
204
+ }
205
+ }
206
+ }
207
+ setHoveredRegion((prev) => prev?.kind === best?.kind && prev?.key === best?.key ? prev : best);
208
+ });
209
+ };
210
+ window.addEventListener('mousemove', onMove);
211
+ return () => {
212
+ window.removeEventListener('mousemove', onMove);
213
+ if (raf)
214
+ cancelAnimationFrame(raf);
215
+ };
216
+ }, [pathFrameRects, screenToFlowPosition]);
152
217
  // Walkthrough focus — selected flow (or step) is full strength; other
153
218
  // opened-flow members stay visible but dimmed; everything else is hidden.
154
219
  // Restored from the persisted view state when a `persistKey` is set.
@@ -165,24 +230,19 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
165
230
  const [sidebarView, setSidebarView] = useState(() => persisted.sidebarView ?? (walkthroughs?.length ? 'walkthroughs' : 'files'));
166
231
  // Diagnostics area tab: issues list vs the Maintain agent pipeline.
167
232
  const [diagnosticsTab, setDiagnosticsTab] = useState(() => persisted.diagnosticsTab ?? 'issues');
168
- // Temporary edge-vocabulary override. Focusing a relation finding whose edge
169
- // belongs to the *other* vocabulary flips the canvas so the edge is actually
170
- // visible; it reverts when the focus is undone (see `unfocusIssueTarget`) or
171
- // when the diagnostics list closes. Null = use the host-pinned / tab view.
172
- const [edgeViewOverride, setEdgeViewOverride] = useState(null);
173
- // One edge vocabulary at a time. When the caller doesn't pick, the sidebar's
174
- // Files / Walkthroughs tab picks: Files draws topology relation edges,
233
+ // One edge source at a time. When the caller doesn't pick, the sidebar's
234
+ // Files / Walkthroughs tab picks: Files draws graphify static edges,
175
235
  // Walkthroughs draws runtime hop edges. Without visible tabs (no walkthroughs,
176
- // or a sidebar-less embed) fall back to the model: a flow-only graph defaults
177
- // to walkthrough edges rather than empty, everything else to relations.
236
+ // or a sidebar-less embed) fall back to walkthrough edges.
178
237
  const sidebarTabsVisible = !hideSidebar && (walkthroughs?.length ?? 0) > 0;
179
- const resolvedEdgeView = edgeViewOverride ??
180
- edgeView ??
238
+ const resolvedEdgeView = edgeView ??
181
239
  (sidebarTabsVisible
182
- ? (sidebarView === 'walkthroughs' ? 'walkthroughs' : 'relations')
183
- : relations.length === 0 && (walkthroughs?.length ?? 0) > 0
240
+ ? (sidebarView === 'walkthroughs' ? 'walkthroughs' : 'graphify')
241
+ : (walkthroughs?.length ?? 0) > 0
184
242
  ? 'walkthroughs'
185
- : 'relations');
243
+ : (graphifyRelations?.length ?? 0) > 0
244
+ ? 'graphify'
245
+ : 'walkthroughs');
186
246
  // Walkthrough flows the user has expanded (via the title row). Closed by
187
247
  // default so a graph with several flows doesn't dump every step list at once.
188
248
  // Restored from the persisted view state when a `persistKey` is set.
@@ -381,13 +441,11 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
381
441
  // Pass 1: build with estimated widths so React Flow can measure the DOM.
382
442
  // The pane stays hidden until Pass 2 completes. Key off layout-affecting
383
443
  // fields only — declarationRef updates after verify must not re-run ELK.
384
- const layoutKey = useMemo(() => subsystemGraphLayoutKey({ components, relations, walkthroughs, graphifyRelations }), [components, relations, walkthroughs, graphifyRelations]);
444
+ const layoutKey = useMemo(() => subsystemGraphLayoutKey({ components, walkthroughs, graphifyRelations }), [components, walkthroughs, graphifyRelations]);
385
445
  const componentsRef = useRef(components);
386
- const relationsRef = useRef(relations);
387
446
  const walkthroughsRef = useRef(walkthroughs);
388
447
  const graphifyRelationsRef = useRef(graphifyRelations);
389
448
  componentsRef.current = components;
390
- relationsRef.current = relations;
391
449
  walkthroughsRef.current = walkthroughs;
392
450
  graphifyRelationsRef.current = graphifyRelations;
393
451
  // Track measured dimensions from React Flow's dimension changes.
@@ -406,13 +464,13 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
406
464
  pass2GenRef.current += 1;
407
465
  const doc = {
408
466
  components: componentsRef.current,
409
- relations: relationsRef.current,
410
467
  walkthroughs: walkthroughsRef.current,
411
468
  };
412
469
  void buildSubsystemGraph(doc, {
413
470
  maxNodeWidth,
414
471
  showEdgeLabels,
415
472
  showSingletonFrames,
473
+ moduleNesting,
416
474
  graphifyRelations: graphifyRelationsRef.current,
417
475
  orderByLine,
418
476
  })
@@ -438,7 +496,7 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
438
496
  setLayoutReady(true);
439
497
  });
440
498
  return () => { alive = false; };
441
- }, [layoutKey, maxNodeWidth, showEdgeLabels]);
499
+ }, [layoutKey, maxNodeWidth, showEdgeLabels, moduleNesting]);
442
500
  // Pass 2: once every leaf node has a measured dimension, re-run ELK.
443
501
  // Group parents are sized by ELK, not measured — exclude them or pass 2
444
502
  // would wait forever for dimensions that never arrive.
@@ -465,7 +523,7 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
465
523
  const measuredWidths = new Map(leafNodes.map((n) => [n.id, dims.get(n.id).width]));
466
524
  const measuredHeights = new Map(leafNodes.map((n) => [n.id, dims.get(n.id).height]));
467
525
  const gen = ++pass2GenRef.current;
468
- void buildSubsystemGraph({ components, relations, walkthroughs }, { maxNodeWidth, showEdgeLabels, measuredWidths, measuredHeights, showSingletonFrames, graphifyRelations, orderByLine })
526
+ void buildSubsystemGraph({ components, walkthroughs }, { maxNodeWidth, showEdgeLabels, measuredWidths, measuredHeights, showSingletonFrames, moduleNesting, graphifyRelations, orderByLine })
469
527
  .then(({ nodes, edges: e, absoluteRects }) => {
470
528
  if (gen !== pass2GenRef.current)
471
529
  return;
@@ -479,7 +537,7 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
479
537
  // Reveal Pass 1 layout rather than leaving the cover up forever.
480
538
  setLayoutReady(true);
481
539
  });
482
- }, [built.nodes, components, relations, walkthroughs, graphifyRelations, orderByLine, maxNodeWidth, showEdgeLabels]);
540
+ }, [built.nodes, components, walkthroughs, graphifyRelations, orderByLine, maxNodeWidth, showEdgeLabels, moduleNesting]);
483
541
  // After Pass 1 commits, try Pass 2 immediately with retained measurements.
484
542
  // Same-id live updates often get no new React Flow `dimensions` events, so
485
543
  // waiting only on onNodesChange leaves the cover stuck.
@@ -559,7 +617,7 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
559
617
  const baseEdges = convertedEdges;
560
618
  // Resolve an issue's component target to the matching node. `id` is the
561
619
  // stable alias; `label` may be an alias, name, or symbol. Non-component
562
- // targets (relation / module / flow / repo) have no single node → null.
620
+ // targets (module / flow / repo) have no single node → null.
563
621
  const issueComponent = useCallback((issue) => {
564
622
  const target = issue.target;
565
623
  if (target?.kind !== 'component')
@@ -569,26 +627,6 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
569
627
  c.name === target.label ||
570
628
  c.symbol === target.label) ?? null);
571
629
  }, [components]);
572
- // Resolve a `relation` issue target to the display edge it flags. The target
573
- // names the two endpoints (`from → to`); match an edge joining both, in
574
- // either direction, preferring the target's `detail` mechanism when it names
575
- // one. Null when no display edge matches.
576
- const issueEdge = useCallback((issue) => {
577
- const target = issue.target;
578
- if (target?.kind !== 'relation')
579
- return null;
580
- const parts = `${target.id ?? ''} → ${target.label}`
581
- .split('→')
582
- .map((p) => p.replace(/\(\)$/, '').trim())
583
- .filter(Boolean);
584
- const joins = (e) => parts.includes(e.source) && parts.includes(e.target);
585
- const matches = baseEdges.filter(joins);
586
- const byMechanism = target.detail
587
- ? matches.filter((e) => e.data?.mechanism ===
588
- target.detail)
589
- : matches;
590
- return byMechanism[0] ?? matches[0] ?? null;
591
- }, [baseEdges]);
592
630
  // A `module` issue target names a boundary frame, so focusing the card frames
593
631
  // that region on the canvas. Null when the target names no frame (a singleton
594
632
  // dropped from the layout, or a region key nothing declares).
@@ -692,7 +730,6 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
692
730
  // the canvas keeps while everything else dims. Derived from each finding's
693
731
  // target:
694
732
  // component → that node
695
- // relation → the edge it flags (+ its two endpoint nodes)
696
733
  // module → every component in that module
697
734
  // walkthrough → the flow's nodes + its hop edges
698
735
  // repo → every component of that repo
@@ -717,23 +754,6 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
717
754
  if (a)
718
755
  nodeIds.add(a);
719
756
  }
720
- else if (t.kind === 'relation') {
721
- // The target names the two endpoints (`from → to`); light the edge and
722
- // its endpoints. Falls back to endpoints alone if no edge matches.
723
- const edge = issueEdge(issue);
724
- if (edge) {
725
- edgeIds.add(edge.id);
726
- nodeIds.add(edge.source);
727
- nodeIds.add(edge.target);
728
- }
729
- else {
730
- for (const part of `${t.id ?? ''} → ${t.label}`.split('→')) {
731
- const a = resolveAlias(part);
732
- if (a)
733
- nodeIds.add(a);
734
- }
735
- }
736
- }
737
757
  else if (t.kind === 'module') {
738
758
  const key = t.id ?? t.label;
739
759
  const prefix = `${key.replace(/\/$/, '')}/`;
@@ -773,7 +793,7 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
773
793
  edgeIds.add(e.id);
774
794
  }
775
795
  return { nodeIds, edgeIds };
776
- }, [issues, expandedIssueCategories, components, walkthroughs, baseEdges, resolveAlias, issueEdge]);
796
+ }, [issues, expandedIssueCategories, components, walkthroughs, resolveAlias]);
777
797
  const issueFocusNodeIds = issueFocus?.nodeIds ?? null;
778
798
  const issueFocusEdgeIds = issueFocus?.edgeIds ?? null;
779
799
  // When an edge is selected, dim every other edge + its label to focus it.
@@ -1016,13 +1036,13 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
1016
1036
  };
1017
1037
  };
1018
1038
  // Edges outside the selected view are hidden entirely (labels included).
1019
- // graphify-native edges belong to the relations view (they are static
1039
+ // graphify-native edges belong to the graphify view (they are static
1020
1040
  // topology, not runtime hops) regardless of their verb.
1021
1041
  const edgeInView = (e) => {
1022
1042
  const d = e.data;
1023
1043
  const mechanism = d?.mechanism ?? 'uses';
1024
- if (resolvedEdgeView === 'relations') {
1025
- return d?.provenance === 'graphify' || isRelationMechanism(mechanism);
1044
+ if (resolvedEdgeView === 'graphify') {
1045
+ return d?.provenance === 'graphify';
1026
1046
  }
1027
1047
  return isWalkthroughMechanism(mechanism);
1028
1048
  };
@@ -1103,7 +1123,6 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
1103
1123
  setSelectedEdgeId(null);
1104
1124
  setFocusedWalkthroughId(null);
1105
1125
  setFocusedStepIndex(null);
1106
- setEdgeViewOverride(null);
1107
1126
  issueModuleFocusRef.current = null;
1108
1127
  }, []);
1109
1128
  // Sidebar file trees — one per repo on multi-repo graphs, each under its
@@ -1123,7 +1142,6 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
1123
1142
  useEffect(() => {
1124
1143
  if (!issuesActive) {
1125
1144
  setExpandedIssueCategories([]);
1126
- setEdgeViewOverride(null);
1127
1145
  }
1128
1146
  }, [issuesActive]);
1129
1147
  const toggleIssues = () => {
@@ -1551,9 +1569,7 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
1551
1569
  if (playable.length === 0)
1552
1570
  return;
1553
1571
  // Only run the cycles that can actually be shown in the current edge view.
1554
- const inView = playable.filter((tl) => tl.steps.some((s) => resolvedEdgeView === 'relations'
1555
- ? isRelationMechanism(s.mechanism)
1556
- : isWalkthroughMechanism(s.mechanism)));
1572
+ const inView = playable.filter((tl) => tl.steps.some((s) => isWalkthroughMechanism(s.mechanism)));
1557
1573
  if (inView.length === 0)
1558
1574
  return;
1559
1575
  const cyc = inView;
@@ -1706,15 +1722,13 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
1706
1722
  onSelect?.(comp.alias);
1707
1723
  }, [components, onSelect]);
1708
1724
  // Issue click → select + frame the target on the canvas. Component targets
1709
- // frame their node; relation targets frame their edge (endpoints + the routed
1710
- // line); other kinds (module / flow / repo) have no single element and just
1711
- // forward. Selections / focus that would hide the target are cleared first,
1712
- // then the camera flies to it on the next frame. The host's `onSelectIssue`
1713
- // still fires afterwards for its own detail.
1725
+ // frame their node; other kinds (module / flow / repo) have no single element
1726
+ // and just forward. Selections / focus that would hide the target are cleared
1727
+ // first, then the camera flies to it on the next frame. The host's
1728
+ // `onSelectIssue` still fires afterwards for its own detail.
1714
1729
  const focusIssueTarget = useCallback((issue) => {
1715
1730
  const comp = issueComponent(issue);
1716
- const edge = comp ? null : issueEdge(issue);
1717
- const step = comp || edge ? null : issueStep(issue);
1731
+ const step = comp ? null : issueStep(issue);
1718
1732
  if (comp) {
1719
1733
  setSelected(comp);
1720
1734
  setSelectedEdgeId(null);
@@ -1733,24 +1747,6 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
1733
1747
  });
1734
1748
  });
1735
1749
  }
1736
- else if (edge) {
1737
- setSelected(null);
1738
- setFocusedWalkthroughId(null);
1739
- setFocusedStepIndex(null);
1740
- setHoveredWalkthroughStep(null);
1741
- setSelectedEdgeId(edge.id);
1742
- issueModuleFocusRef.current = null;
1743
- // Flip to the vocabulary this edge belongs to, so a hop flagged in the
1744
- // relations view (or vice versa) is actually drawn while focused.
1745
- const d = edge.data;
1746
- const mechanism = d?.mechanism ?? 'uses';
1747
- setEdgeViewOverride(d?.provenance === 'graphify' || isRelationMechanism(mechanism)
1748
- ? 'relations'
1749
- : 'walkthroughs');
1750
- requestAnimationFrame(() => {
1751
- fitFocusBounds(new Set([edge.id]));
1752
- });
1753
- }
1754
1750
  else if (step) {
1755
1751
  // A step finding focuses the step itself, through the same entry point
1756
1752
  // the walkthrough panel uses — so the drawer, the step numbering, the
@@ -1778,14 +1774,12 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
1778
1774
  onSelectIssue?.(issue);
1779
1775
  }, [
1780
1776
  issueComponent,
1781
- issueEdge,
1782
1777
  issueStep,
1783
1778
  issueModuleNodeId,
1784
1779
  focusWalkthroughStep,
1785
1780
  onSelect,
1786
1781
  onSelectIssue,
1787
1782
  fitView,
1788
- fitFocusBounds,
1789
1783
  fitNodeRects,
1790
1784
  ]);
1791
1785
  // Collapsing the issue card again undoes the focus: drop the node / edge
@@ -1803,16 +1797,9 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
1803
1797
  setSelectedEdgeId(null);
1804
1798
  }
1805
1799
  else {
1806
- const edge = issueEdge(issue);
1807
- const step = edge ? null : issueStep(issue);
1808
- const moduleNodeId = edge || step ? null : issueModuleNodeId(issue);
1809
- if (edge) {
1810
- if (selectedEdgeIdRef.current !== edge.id)
1811
- return;
1812
- setSelectedEdgeId(null);
1813
- setEdgeViewOverride(null);
1814
- }
1815
- else if (step) {
1800
+ const step = issueStep(issue);
1801
+ const moduleNodeId = step ? null : issueModuleNodeId(issue);
1802
+ if (step) {
1816
1803
  // Only unwind a step focus this card still owns. A dim-mode graph
1817
1804
  // keeps its step focus in hover state instead (owned by the pointer,
1818
1805
  // not the card), and correctly leaves that alone here.
@@ -1838,7 +1825,7 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
1838
1825
  requestAnimationFrame(() => {
1839
1826
  fitView({ padding: 0.1, duration: 300, minZoom: 0.05, maxZoom: 2 });
1840
1827
  });
1841
- }, [issueComponent, issueEdge, issueStep, issueModuleNodeId, fitView]);
1828
+ }, [issueComponent, issueStep, issueModuleNodeId, fitView]);
1842
1829
  // Construct tokens inside a walkthrough snippet. A step's line is an edge
1843
1830
  // between its `from`/`to` components, so a token naming either of those
1844
1831
  // constructs should navigate to that construct's declaration. Index the
@@ -2037,7 +2024,7 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
2037
2024
  display: 'flex',
2038
2025
  flexDirection: 'column',
2039
2026
  borderTop: i > 0 ? `1px solid ${theme.colors.border}` : undefined,
2040
- }, children: [repoGroups.multiRepo && (_jsx(RepoGroupHeader, { group: group, collapsed: collapsed, onToggle: () => toggleRepoCollapsed(groupKey) })), !collapsed && (_jsx(SubsystemFileTree, { files: group.entries.map((e) => e.displayPath), selectedFile: selected?.file ?? openFile, hoveredFile: hoveredFile, onSelectFile: onTreeSelectFile, headerless: repoGroups.multiRepo }))] }, groupKey));
2027
+ }, children: [repoGroups.multiRepo && (_jsx(RepoGroupHeader, { group: group, collapsed: collapsed, onToggle: () => toggleRepoCollapsed(groupKey) })), !collapsed && (_jsx(SubsystemFileTree, { files: group.entries.map((e) => e.displayPath), selectedFile: selected?.file ?? openFile, hoveredFile: hoveredFile, onSelectFile: onTreeSelectFile, hoveredFolder: hoveredBoundaryPath, headerless: repoGroups.multiRepo }))] }, groupKey));
2041
2028
  }) })) : null] })) }))] })), !hideSidebar && (title || description || diagnostic || issuesActive || sidebarExtra || sidebarAfterDescription || treeFilePaths.length > 0 || hasWalkthroughs) && (_jsx("div", { onMouseDown: onSidebarResizeStart, "aria-label": "Resize sidebar", title: "Drag to resize", style: {
2042
2029
  width: 3,
2043
2030
  flexShrink: 0,
@@ -2326,7 +2313,6 @@ export function SubsystemComponentGraph(props) {
2326
2313
  }, []);
2327
2314
  const constructsOnly = isConstructsOnlyModel({
2328
2315
  components: props.components,
2329
- relations: props.relations,
2330
2316
  walkthroughs: props.walkthroughs,
2331
2317
  graphifyRelations: props.graphifyRelations,
2332
2318
  });