@principal-ai/subsystems-react 0.53.6 → 0.54.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 (104) hide show
  1. package/dist/index.d.ts +8 -2
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +3 -0
  4. package/dist/index.js.map +1 -1
  5. package/dist/pierre/PierreTrailCodeView.d.ts.map +1 -1
  6. package/dist/pierre/PierreTrailCodeView.js +7 -5
  7. package/dist/pierre/PierreTrailCodeView.js.map +1 -1
  8. package/dist/stories/Subsystem/ComponentGraph/fixtures.d.ts.map +1 -1
  9. package/dist/stories/Subsystem/ComponentGraph/fixtures.js +1 -2
  10. package/dist/stories/Subsystem/ComponentGraph/fixtures.js.map +1 -1
  11. package/dist/subsystem/C4Graph.d.ts +144 -9
  12. package/dist/subsystem/C4Graph.d.ts.map +1 -1
  13. package/dist/subsystem/C4Graph.js +924 -148
  14. package/dist/subsystem/C4Graph.js.map +1 -1
  15. package/dist/subsystem/C4NodeCard.d.ts +22 -1
  16. package/dist/subsystem/C4NodeCard.d.ts.map +1 -1
  17. package/dist/subsystem/C4NodeCard.js +10 -2
  18. package/dist/subsystem/C4NodeCard.js.map +1 -1
  19. package/dist/subsystem/ComponentDeclaration.d.ts +5 -4
  20. package/dist/subsystem/ComponentDeclaration.d.ts.map +1 -1
  21. package/dist/subsystem/ComponentDeclaration.js +12 -4
  22. package/dist/subsystem/ComponentDeclaration.js.map +1 -1
  23. package/dist/subsystem/ConstructsCatalog.d.ts.map +1 -1
  24. package/dist/subsystem/ConstructsCatalog.js +10 -7
  25. package/dist/subsystem/ConstructsCatalog.js.map +1 -1
  26. package/dist/subsystem/GraphSidebar.d.ts +71 -0
  27. package/dist/subsystem/GraphSidebar.d.ts.map +1 -0
  28. package/dist/subsystem/GraphSidebar.js +145 -0
  29. package/dist/subsystem/GraphSidebar.js.map +1 -0
  30. package/dist/subsystem/GraphWithSidebar.d.ts +24 -0
  31. package/dist/subsystem/GraphWithSidebar.d.ts.map +1 -0
  32. package/dist/subsystem/GraphWithSidebar.js +25 -0
  33. package/dist/subsystem/GraphWithSidebar.js.map +1 -0
  34. package/dist/subsystem/SubsystemComponentGraph.d.ts +33 -0
  35. package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
  36. package/dist/subsystem/SubsystemComponentGraph.js +364 -411
  37. package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
  38. package/dist/subsystem/TrailStepsNav.d.ts +48 -0
  39. package/dist/subsystem/TrailStepsNav.d.ts.map +1 -0
  40. package/dist/subsystem/TrailStepsNav.js +189 -0
  41. package/dist/subsystem/TrailStepsNav.js.map +1 -0
  42. package/dist/subsystem/TrailsPanel.d.ts.map +1 -1
  43. package/dist/subsystem/TrailsPanel.js +3 -4
  44. package/dist/subsystem/TrailsPanel.js.map +1 -1
  45. package/dist/subsystem/__fixtures__/c4Drilldown.d.ts +151 -0
  46. package/dist/subsystem/__fixtures__/c4Drilldown.d.ts.map +1 -0
  47. package/dist/subsystem/__fixtures__/c4Drilldown.js +783 -0
  48. package/dist/subsystem/__fixtures__/c4Drilldown.js.map +1 -0
  49. package/dist/subsystem/c4.d.ts +39 -8
  50. package/dist/subsystem/c4.d.ts.map +1 -1
  51. package/dist/subsystem/c4.js.map +1 -1
  52. package/dist/subsystem/declarationRef.d.ts +26 -18
  53. package/dist/subsystem/declarationRef.d.ts.map +1 -1
  54. package/dist/subsystem/declarationRef.js +9 -4
  55. package/dist/subsystem/declarationRef.js.map +1 -1
  56. package/dist/subsystem/model.d.ts +19 -1
  57. package/dist/subsystem/model.d.ts.map +1 -1
  58. package/dist/subsystem/model.js +12 -2
  59. package/dist/subsystem/model.js.map +1 -1
  60. package/dist/subsystem/nodes.d.ts.map +1 -1
  61. package/dist/subsystem/nodes.js +24 -4
  62. package/dist/subsystem/nodes.js.map +1 -1
  63. package/dist/subsystem/trailBrief.d.ts.map +1 -1
  64. package/dist/subsystem/trailBrief.js +6 -1
  65. package/dist/subsystem/trailBrief.js.map +1 -1
  66. package/package.json +3 -3
  67. package/src/index.ts +9 -2
  68. package/src/pierre/PierreTrailCodeView.tsx +9 -13
  69. package/src/stories/Subsystem/C4Graph/C4ToComponentDrill.stories.tsx +496 -0
  70. package/src/stories/Subsystem/ComponentGraph/AnimatedProgression.stories.tsx +7 -7
  71. package/src/stories/Subsystem/ComponentGraph/Constructs.stories.tsx +19 -25
  72. package/src/stories/Subsystem/ComponentGraph/DetailPanel.stories.tsx +8 -10
  73. package/src/stories/Subsystem/ComponentGraph/Issues.stories.tsx +19 -19
  74. package/src/stories/Subsystem/ComponentGraph/ProposedTrails.stories.tsx +6 -6
  75. package/src/stories/Subsystem/ComponentGraph/TrailAutoplay.stories.tsx +7 -7
  76. package/src/stories/Subsystem/ComponentGraph/Trails.stories.tsx +27 -30
  77. package/src/stories/Subsystem/ComponentGraph/fixtures.ts +1 -2
  78. package/src/stories/Subsystem/ConstructsCatalog.stories.tsx +10 -13
  79. package/src/stories/Subsystem/EgoGraph/EgoGraph.stories.tsx +3 -4
  80. package/src/stories/Subsystem/Sidebar/GraphSidebar.stories.tsx +206 -0
  81. package/src/subsystem/C4Graph.groups.test.ts +96 -2
  82. package/src/subsystem/C4Graph.tsx +1377 -165
  83. package/src/subsystem/C4NodeCard.test.ts +1 -2
  84. package/src/subsystem/C4NodeCard.tsx +24 -2
  85. package/src/subsystem/ComponentDeclaration.tsx +18 -8
  86. package/src/subsystem/ConstructsCatalog.tsx +23 -8
  87. package/src/subsystem/GraphSidebar.test.tsx +152 -0
  88. package/src/subsystem/GraphSidebar.tsx +300 -0
  89. package/src/subsystem/GraphWithSidebar.test.tsx +78 -0
  90. package/src/subsystem/GraphWithSidebar.tsx +60 -0
  91. package/src/subsystem/SubsystemComponentGraph.tsx +284 -370
  92. package/src/subsystem/TrailStepsNav.test.tsx +179 -0
  93. package/src/subsystem/TrailStepsNav.tsx +372 -0
  94. package/src/subsystem/TrailsPanel.tsx +3 -4
  95. package/src/subsystem/__fixtures__/c4Drilldown.test.ts +338 -0
  96. package/src/subsystem/__fixtures__/c4Drilldown.ts +847 -0
  97. package/src/subsystem/c4.ts +41 -8
  98. package/src/subsystem/declarationRef.ts +28 -19
  99. package/src/subsystem/model.test.ts +32 -12
  100. package/src/subsystem/model.ts +30 -1
  101. package/src/subsystem/nodes.group.test.tsx +82 -0
  102. package/src/subsystem/nodes.tsx +30 -2
  103. package/src/subsystem/trailBrief.test.ts +2 -2
  104. package/src/subsystem/trailBrief.ts +6 -1
@@ -85,6 +85,8 @@ import { ComponentDeclaration } from './ComponentDeclaration';
85
85
  import type { ComponentVerificationState } from './ComponentDeclaration';
86
86
  import type { DeclarationSymbolRef, SymbolInspection } from './symbolRefs';
87
87
  import { FileDrawer, FILE_DRAWER_HEIGHT_MS } from './FileDrawer';
88
+ import { GraphWithSidebar } from './GraphWithSidebar';
89
+ import type { GraphSidebarTab } from './GraphSidebar';
88
90
  import { buildRepoGroups, repoAvatarUrl, type RepoGroup } from './paths';
89
91
  import { TrailsPanel, TRAIL_PLAY_PAUSE_MS } from './TrailsPanel';
90
92
  import {
@@ -138,7 +140,7 @@ function constructIdentifiers(comp: SubsystemComponent): string[] {
138
140
  }
139
141
 
140
142
  type DrawerTarget =
141
- | { kind: 'file'; file: string; startLine?: number }
143
+ | { kind: 'file'; file: string; startLine?: number; endLine?: number }
142
144
  | { kind: 'trail'; trailId: string; stepIndex: number | null };
143
145
 
144
146
  /**
@@ -152,6 +154,9 @@ interface PersistedViewState {
152
154
  expandedTrails?: string[];
153
155
  focusedTrailId?: string | null;
154
156
  focusedStepIndex?: number | null;
157
+ /** Sidebar width moved to the shared GraphWithSidebar chrome (persisted
158
+ * under `principal.subsystems.sidebarWidth.<persistKey>`); reading the old
159
+ * field still restores a width saved before that move. */
155
160
  sidebarWidth?: number;
156
161
  sidebarView?: 'files' | 'trails';
157
162
  diagnosticsTab?: 'issues' | 'agents';
@@ -219,6 +224,24 @@ export interface SubsystemComponentGraphProps {
219
224
  * graph.
220
225
  */
221
226
  initialTrailId?: string | null;
227
+ /**
228
+ * Reports trail focus changes from inside the graph — a trail (or step)
229
+ * clicked in the Trails panel, or focus cleared. This is the graph-driven
230
+ * mirror of `initialTrailId`: hosts that render a summary alongside the
231
+ * graph (e.g. a C4 view) can keep both in agreement. Fired on every focus
232
+ * change, including re-applying a host `initialTrailId` on mount — a host
233
+ * that itself drives the deep-link receives an idempotent echo of the same
234
+ * trail.
235
+ */
236
+ onTrailSelect?: (ref: { trailId: string; stepIndices?: number[] } | null) => void;
237
+ /**
238
+ * Reports which trail step is currently hovered in the Trails panel —
239
+ * the transient preview mirror of `onTrailSelect`. `null` when nothing is
240
+ * hovered (or a whole flow row is). Hosts rendering a summary (e.g. a C4
241
+ * view) can dim to a single step's participants while the pointer is over
242
+ * a step row, and fall back to the focused trail on leave.
243
+ */
244
+ onTrailStepHover?: (ref: { trailId: string; stepIndex: number } | null) => void;
222
245
  /**
223
246
  * Called with the next trail order after a drag in the sidebar's
224
247
  * flows panel. When set, each row grows a drag grip and a drop emits the
@@ -429,6 +452,15 @@ export interface SubsystemComponentGraphProps {
429
452
  * instead of accepting the hash. Unmapped regions keep the derived color.
430
453
  */
431
454
  boundaryColors?: Record<string, string>;
455
+ /**
456
+ * Process keys whose frames draw package chrome (dashed, rounded) instead
457
+ * of runtime chrome (solid, square): the claim groups code organisation —
458
+ * a library, an area, a proposal — not an accepted deployment unit. Hosts
459
+ * derive this from their accepted C4 element set (a process no element
460
+ * backs is code, not a box). Presentation only — ids, issue targeting and
461
+ * membership are unchanged. See `BoundaryFrameOptions.codeProcessKeys`.
462
+ */
463
+ codeProcessKeys?: ReadonlySet<string>;
432
464
  }
433
465
 
434
466
  const nodeTypes: NodeTypes = {
@@ -448,11 +480,13 @@ const FileDrawerContent = memo(function FileDrawerContent({
448
480
  render,
449
481
  file,
450
482
  startLine,
483
+ endLine,
451
484
  fullFile,
452
485
  }: {
453
486
  render: (file: string, opts?: SubsystemOpenFileOptions) => ReactNode;
454
487
  file: string;
455
488
  startLine?: number;
489
+ endLine?: number;
456
490
  fullFile?: boolean;
457
491
  }) {
458
492
  return (
@@ -460,7 +494,11 @@ const FileDrawerContent = memo(function FileDrawerContent({
460
494
  {render(
461
495
  file,
462
496
  startLine != null
463
- ? { startLine, ...(fullFile ? { fullFile: true } : {}) }
497
+ ? {
498
+ startLine,
499
+ ...(endLine != null ? { endLine } : {}),
500
+ ...(fullFile ? { fullFile: true } : {}),
501
+ }
464
502
  : undefined,
465
503
  )}
466
504
  </>
@@ -536,7 +574,7 @@ interface InnerProps extends SubsystemComponentGraphProps {
536
574
  measured: { w: number; h: number } | null;
537
575
  }
538
576
 
539
- function Inner({ components, trails, graphifyRelations, orderByLine, initialTrailId, onReorderTrails, onSelect, onEdgeSelect, measured: _measured, maxNodeWidth, showEdgeLabels, showSingletonFrames = true, moduleNesting, edgeView, title, hideSidebar, trailStepMode = 'focus', autoPlayTrails = false, trailAutoPlayIntervalMs = TRAIL_PLAY_PAUSE_MS, zoomOnTrailFocus = true, trailFocusDurationMs = 300, graphTitle, showTrailTitle = false, description, canvasOverlay, sidebarExtra, sidebarAfterDescription, diagnostic, issues, showIssues, focusIssueCategory, laneStatus, onSelectIssue, onApplyIssueFix, onHoverIssue, renderFileViewer, renderTrailViewer, onFileSelect, componentVerification, onInspectSymbol, boundaryColors, hideDrawer = false, persistKey, liveEvents, agentsPanel }: InnerProps) {
577
+ function Inner({ components, trails, graphifyRelations, orderByLine, initialTrailId, onTrailSelect, onTrailStepHover, onReorderTrails, onSelect, onEdgeSelect, measured: _measured, maxNodeWidth, showEdgeLabels, showSingletonFrames = true, moduleNesting, edgeView, title, hideSidebar, trailStepMode = 'focus', autoPlayTrails = false, trailAutoPlayIntervalMs = TRAIL_PLAY_PAUSE_MS, zoomOnTrailFocus = true, trailFocusDurationMs = 300, graphTitle, showTrailTitle = false, description, canvasOverlay, sidebarExtra, sidebarAfterDescription, diagnostic, issues, showIssues, focusIssueCategory, laneStatus, onSelectIssue, onApplyIssueFix, onHoverIssue, renderFileViewer, renderTrailViewer, onFileSelect, componentVerification, onInspectSymbol, boundaryColors, codeProcessKeys, hideDrawer = false, persistKey, liveEvents, agentsPanel }: InnerProps) {
540
578
  const { theme } = useTheme();
541
579
  const { fitView, fitBounds, screenToFlowPosition } = useReactFlow();
542
580
  const viewport = useViewport();
@@ -564,6 +602,7 @@ function Inner({ components, trails, graphifyRelations, orderByLine, initialTrai
564
602
  const [fileOverlay, setFileOverlay] = useState<{
565
603
  file: string;
566
604
  startLine?: number;
605
+ endLine?: number;
567
606
  } | null>(null);
568
607
  // Mirror so step-focus can decide whether to wait for the drawer open
569
608
  // transition before fitView (avoids framing against full-height canvas).
@@ -699,57 +738,14 @@ function Inner({ components, trails, graphifyRelations, orderByLine, initialTrai
699
738
  const [descToggleHover, setDescToggleHover] = useState(false);
700
739
  // `true` only when a description exists AND the user opened it.
701
740
  const showDesc = !!description && descriptionVisible;
702
- // Sidebar width (px). Draggable via the resize handle between the sidebar
703
- // and the graph canvas; clamps to sensible bounds while dragging. Restored
704
- // from the persisted view state when a `persistKey` is set.
705
- const [sidebarWidth, setSidebarWidth] = useState(persisted.sidebarWidth ?? 450);
706
- const [sidebarDrag, setSidebarDrag] = useState(false);
707
- const sidebarDragStartX = useRef(0);
708
- const sidebarDragStartWidth = useRef(450);
709
- // Mirror of `sidebarWidth` for the drag-end persist (the mouseup listener
710
- // closes over the drag-start render).
711
- const sidebarWidthRef = useRef(sidebarWidth);
712
- sidebarWidthRef.current = sidebarWidth;
713
- const sidebarMinWidth = 240;
741
+ // Sidebar width lives in the shared GraphWithSidebar chrome — draggable and
742
+ // persisted under `principal.subsystems.sidebarWidth.<persistKey>`. The graph
743
+ // only bounds how wide the panel may get so the canvas keeps room to breathe;
744
+ // a width saved under the old view-state key still seeds the panel once.
714
745
  const sidebarMaxWidth = useMemo(
715
- () => Math.max(Math.min((_measured?.w ?? 680) * 0.5, 600), sidebarMinWidth),
746
+ () => Math.max(Math.min((_measured?.w ?? 680) * 0.5, 600), 240),
716
747
  [_measured?.w],
717
748
  );
718
- // Resize drag: capture the drag state so document-level move/up listeners
719
- // stay attached for the duration of the gesture, then release on mouseup.
720
- const onSidebarResizeStart = useCallback(
721
- (e: ReactMouseEvent) => {
722
- e.preventDefault();
723
- sidebarDragStartX.current = e.clientX;
724
- sidebarDragStartWidth.current = sidebarWidth;
725
- setSidebarDrag(true);
726
- },
727
- [sidebarWidth],
728
- );
729
- useEffect(() => {
730
- if (!sidebarDrag) return;
731
- const onMove = (e: globalThis.MouseEvent) => {
732
- const delta = e.clientX - sidebarDragStartX.current;
733
- const next = Math.min(
734
- Math.max(sidebarDragStartWidth.current + delta, sidebarMinWidth),
735
- sidebarMaxWidth,
736
- );
737
- setSidebarWidth(next);
738
- };
739
- const onUp = () => {
740
- setSidebarDrag(false);
741
- // Persist only on drag end — not on every mousemove.
742
- writeViewState(persistKey, { sidebarWidth: sidebarWidthRef.current });
743
- // Re-fit the canvas so the graph re-centers in the new available space.
744
- requestAnimationFrame(() => fitView());
745
- };
746
- document.addEventListener('mousemove', onMove);
747
- document.addEventListener('mouseup', onUp);
748
- return () => {
749
- document.removeEventListener('mousemove', onMove);
750
- document.removeEventListener('mouseup', onUp);
751
- };
752
- }, [sidebarDrag, sidebarMaxWidth, fitView, persistKey]);
753
749
  // Persist the trail working set per model. Each write merges into the
754
750
  // stored blob, so the three fields never clobber one another.
755
751
  useEffect(() => {
@@ -944,6 +940,7 @@ function Inner({ components, trails, graphifyRelations, orderByLine, initialTrai
944
940
  moduleNesting,
945
941
  graphifyRelations: graphifyRelationsRef.current,
946
942
  orderByLine,
943
+ codeProcessKeys,
947
944
  })
948
945
  .then(({ nodes, edges: e, absoluteRects }) => {
949
946
  if (!alive) return;
@@ -993,7 +990,7 @@ function Inner({ components, trails, graphifyRelations, orderByLine, initialTrai
993
990
  const gen = ++pass2GenRef.current;
994
991
  void buildSubsystemGraph(
995
992
  { components, trails },
996
- { maxNodeWidth, showEdgeLabels, measuredWidths, measuredHeights, showSingletonFrames, moduleNesting, graphifyRelations, orderByLine },
993
+ { maxNodeWidth, showEdgeLabels, measuredWidths, measuredHeights, showSingletonFrames, moduleNesting, graphifyRelations, orderByLine, codeProcessKeys },
997
994
  )
998
995
  .then(({ nodes, edges: e, absoluteRects }) => {
999
996
  if (gen !== pass2GenRef.current) return;
@@ -1788,6 +1785,7 @@ function Inner({ components, trails, graphifyRelations, orderByLine, initialTrai
1788
1785
  (file: string, opts?: SubsystemOpenFileOptions) => {
1789
1786
  setFileOverlay(null);
1790
1787
  const startLine = opts?.startLine;
1788
+ const endLine = opts?.endLine;
1791
1789
  if (
1792
1790
  openFileRef.current?.file === file &&
1793
1791
  openFileRef.current.startLine === startLine
@@ -1795,16 +1793,16 @@ function Inner({ components, trails, graphifyRelations, orderByLine, initialTrai
1795
1793
  setDrawerTarget(null);
1796
1794
  return;
1797
1795
  }
1798
- setDrawerTarget({ kind: 'file', file, startLine });
1796
+ setDrawerTarget({ kind: 'file', file, startLine, endLine });
1799
1797
  onFileSelect?.(file);
1800
1798
  },
1801
1799
  [onFileSelect],
1802
1800
  );
1803
1801
 
1804
1802
  // Double-click a component opens its declaration in the file drawer — the
1805
- // `file` + anchored `declarationRef.startLine` when verify resolved one,
1806
- // else the file top. Uses the same open path as the declaration panel's
1807
- // file link, so the drawer and file tree stay in sync.
1803
+ // `file` + anchored `declarationSpan` range when verify resolved one, else
1804
+ // the file top. Uses the same open path as the declaration panel's file
1805
+ // link, so the drawer and file tree stay in sync.
1808
1806
  // Boundary region currently framed by a double-click, so a second
1809
1807
  // double-click on the same boundary zooms back out to the whole graph.
1810
1808
  const [focusedBoundaryKey, setFocusedBoundaryKey] = useState<string | null>(null);
@@ -1841,15 +1839,21 @@ function Inner({ components, trails, graphifyRelations, orderByLine, initialTrai
1841
1839
  if (node.type !== 'subsystem-component') return;
1842
1840
  const comp = (node.data as { component?: SubsystemComponent } | undefined)?.component;
1843
1841
  if (!comp?.file) return;
1844
- const startLine = comp.declarationRef?.startLine;
1845
- onOpenDeclarationFile(comp.file, startLine != null ? { startLine } : undefined);
1842
+ const startLine = comp.declarationSpan?.startLine;
1843
+ const endLine = comp.declarationSpan?.endLine;
1844
+ onOpenDeclarationFile(
1845
+ comp.file,
1846
+ startLine != null
1847
+ ? { startLine, ...(endLine != null ? { endLine } : {}) }
1848
+ : undefined,
1849
+ );
1846
1850
  },
1847
1851
  [onOpenDeclarationFile, fitView, focusedBoundaryKey],
1848
1852
  );
1849
1853
 
1850
1854
  const onOpenFileFromTrail = useCallback(
1851
1855
  (file: string, opts?: SubsystemOpenFileOptions) => {
1852
- setFileOverlay({ file, startLine: opts?.startLine });
1856
+ setFileOverlay({ file, startLine: opts?.startLine, endLine: opts?.endLine });
1853
1857
  onFileSelect?.(file);
1854
1858
  },
1855
1859
  [onFileSelect],
@@ -2043,14 +2047,16 @@ function Inner({ components, trails, graphifyRelations, orderByLine, initialTrai
2043
2047
  setFocusedStepIndex(null);
2044
2048
  setFocusedTrailId(null);
2045
2049
  setDrawerTarget((prev) => (prev?.kind === 'trail' ? null : prev));
2050
+ onTrailSelect?.(null);
2046
2051
  return;
2047
2052
  }
2048
2053
  setFocusedStepIndex(null);
2049
2054
  setFocusedTrailId(tl.id);
2055
+ onTrailSelect?.({ trailId: tl.id });
2050
2056
  fitFocusBounds(new Set(tl.steps.map((s) => trailStepGraphEdgeId(s))));
2051
2057
  setDrawerTarget((prev) => (prev?.kind === 'trail' ? null : prev));
2052
2058
  },
2053
- [fitFocusBounds, trailStepMode],
2059
+ [fitFocusBounds, trailStepMode, onTrailSelect],
2054
2060
  );
2055
2061
 
2056
2062
  const clearTrailFocus = useCallback(() => {
@@ -2058,7 +2064,8 @@ function Inner({ components, trails, graphifyRelations, orderByLine, initialTrai
2058
2064
  setFocusedStepIndex(null);
2059
2065
  setHoveredTrailStep(null);
2060
2066
  setDrawerTarget((prev) => (prev?.kind === 'trail' ? null : prev));
2061
- }, []);
2067
+ onTrailSelect?.(null);
2068
+ }, [onTrailSelect]);
2062
2069
 
2063
2070
  // Host deep-link: opening the graph from a trail row selects that
2064
2071
  // flow — expand its steps, focus its edges, and switch the sidebar to
@@ -2108,6 +2115,7 @@ function Inner({ components, trails, graphifyRelations, orderByLine, initialTrai
2108
2115
  setFocusedTrailId(null);
2109
2116
  setHoveredTrailStep({ trailId: tl.id, stepIndex });
2110
2117
  setDrawerTarget((prev) => (prev?.kind === 'trail' ? null : prev));
2118
+ onTrailSelect?.(null);
2111
2119
  return;
2112
2120
  }
2113
2121
  // Toggle: clicking the selected step unselects it (back to whole flow).
@@ -2127,6 +2135,7 @@ function Inner({ components, trails, graphifyRelations, orderByLine, initialTrai
2127
2135
  setFocusedStepIndex(stepIndex);
2128
2136
  setFocusedTrailId(tl.id);
2129
2137
  setHoveredTrailStep(null);
2138
+ onTrailSelect?.({ trailId: tl.id, stepIndices: [stepIndex] });
2130
2139
  // Open the drawer before fitting. If it was closed, wait for its height
2131
2140
  // transition so fitView uses the reduced canvas — not full height.
2132
2141
  const drawerWasOpen = drawerOpenRef.current;
@@ -2140,7 +2149,8 @@ function Inner({ components, trails, graphifyRelations, orderByLine, initialTrai
2140
2149
  setDrawerTarget({
2141
2150
  kind: 'file',
2142
2151
  file: step.file,
2143
- startLine: step.line,
2152
+ startLine: step.span?.startLine,
2153
+ endLine: step.span?.endLine,
2144
2154
  });
2145
2155
  }
2146
2156
  const edgeIds = new Set([trailStepGraphEdgeId(step)]);
@@ -2163,6 +2173,7 @@ function Inner({ components, trails, graphifyRelations, orderByLine, initialTrai
2163
2173
  focusedTrailId,
2164
2174
  renderTrailViewer,
2165
2175
  trailStepMode,
2176
+ onTrailSelect,
2166
2177
  ],
2167
2178
  );
2168
2179
 
@@ -2249,7 +2260,7 @@ function Inner({ components, trails, graphifyRelations, orderByLine, initialTrai
2249
2260
  if (renderTrailViewer) {
2250
2261
  setDrawerTarget({ kind: 'trail', trailId: tl.id, stepIndex: stepIdx });
2251
2262
  } else {
2252
- setDrawerTarget({ kind: 'file', file: step.file, startLine: step.line });
2263
+ setDrawerTarget({ kind: 'file', file: step.file, startLine: step.span?.startLine, endLine: step.span?.endLine });
2253
2264
  }
2254
2265
  }
2255
2266
  }
@@ -2541,10 +2552,13 @@ function Inner({ components, trails, graphifyRelations, orderByLine, initialTrai
2541
2552
  (alias: string) => {
2542
2553
  const comp = components.find((c) => c.alias === alias);
2543
2554
  if (!comp?.file) return;
2544
- const startLine = comp.declarationRef?.startLine;
2555
+ const startLine = comp.declarationSpan?.startLine;
2556
+ const endLine = comp.declarationSpan?.endLine;
2545
2557
  onOpenDeclarationFile(
2546
2558
  comp.file,
2547
- startLine != null ? { startLine } : undefined,
2559
+ startLine != null
2560
+ ? { startLine, ...(endLine != null ? { endLine } : {}) }
2561
+ : undefined,
2548
2562
  );
2549
2563
  },
2550
2564
  [components, onOpenDeclarationFile],
@@ -2616,323 +2630,221 @@ function Inner({ components, trails, graphifyRelations, orderByLine, initialTrai
2616
2630
  [],
2617
2631
  );
2618
2632
 
2619
- return (
2620
- <div style={{ width: '100%', height: '100%', display: 'flex', flexDirection: 'row', userSelect: sidebarDrag ? 'none' : undefined }}>
2621
- {/* Sidebar: scrollable title/description on top, files or flows pinned below.
2622
- The description hides by default so files/flows get the room; the
2623
- title-row toggle reveals it, and the lower panel yields back to 50%. */}
2624
- {!hideSidebar && (title || description || diagnostic || issuesActive || sidebarExtra || sidebarAfterDescription || treeFilePaths.length > 0 || hasTrails) && (
2625
- <div
2626
- style={{
2627
- width: sidebarWidth,
2628
- minWidth: sidebarWidth,
2629
- borderRight: `1px solid ${theme.colors.border}`,
2630
- background: theme.colors.backgroundSecondary ?? theme.colors.background,
2631
- display: 'flex',
2632
- flexDirection: 'column',
2633
- overflow: 'hidden',
2634
- }}
2635
- >
2633
+ // Sidebar chrome — title/description header, Files/Trails (or Issues/Agents)
2634
+ // tab strip, and the lower panels. Everything renders through the shared
2635
+ // GraphWithSidebar; the graph keeps owning every bit of *state* (focused
2636
+ // trail, active tab, description visibility) because the canvas dims from
2637
+ // the same facts the sidebar renders.
2638
+ const sidebarVisible =
2639
+ !hideSidebar &&
2640
+ (title || description || diagnostic || issuesActive || sidebarExtra || sidebarAfterDescription ||
2641
+ treeFilePaths.length > 0 || hasTrails);
2642
+ const sidebarBodyVisible = treeFilePaths.length > 0 || hasTrails || issuesActive;
2643
+ const sidebarTabs: GraphSidebarTab[] | undefined = issuesActive
2644
+ ? agentsPanel
2645
+ ? [
2646
+ { id: 'issues', label: 'Issues' },
2647
+ { id: 'agents', label: 'Agents' },
2648
+ ]
2649
+ : undefined
2650
+ : hasTrails
2651
+ ? [
2652
+ { id: 'trails', label: 'Trails' },
2653
+ { id: 'files', label: 'Files' },
2654
+ ]
2655
+ : undefined;
2656
+ const activeSidebarTab = issuesActive ? diagnosticsTab : sidebarView;
2657
+ // Header block: the extras, then the title/actions row (diagnostic chip +
2658
+ // description toggle), then the description that toggles out from under it.
2659
+ const sidebarHeader = (
2660
+ <>
2661
+ {sidebarExtra}
2662
+ {(title || description || diagnostic) && (
2663
+ <div style={{ display: 'flex', alignItems: 'flex-start', gap: 8 }}>
2664
+ {title && (
2665
+ <h2
2666
+ style={{
2667
+ margin: 0,
2668
+ flex: 1,
2669
+ minWidth: 0,
2670
+ fontSize: theme.fontSizes[2],
2671
+ fontWeight: 600,
2672
+ color: theme.colors.text,
2673
+ fontFamily: theme.fonts.monospace,
2674
+ }}
2675
+ >
2676
+ {title}
2677
+ </h2>
2678
+ )}
2636
2679
  <div
2637
2680
  style={{
2638
- flex: showDesc ? 1 : '0 0 auto',
2639
- minHeight: 0,
2640
- overflowY: 'auto',
2641
- padding: '16px',
2642
2681
  display: 'flex',
2643
- flexDirection: 'column',
2644
- gap: 12,
2682
+ alignItems: 'center',
2683
+ gap: 4,
2684
+ flexShrink: 0,
2685
+ // Title's flex:1 already pushes this cluster right; without a
2686
+ // title, keep the actions flush right instead of flush-left.
2687
+ marginLeft: title ? undefined : 'auto',
2645
2688
  }}
2646
2689
  >
2647
- {sidebarExtra}
2648
- {(title || description || diagnostic) && (
2649
- <div style={{ display: 'flex', alignItems: 'flex-start', gap: 8 }}>
2650
- {title && (
2651
- <h2
2652
- style={{
2653
- margin: 0,
2654
- flex: 1,
2655
- minWidth: 0,
2656
- fontSize: theme.fontSizes[2],
2657
- fontWeight: 600,
2658
- color: theme.colors.text,
2659
- fontFamily: theme.fonts.monospace,
2660
- }}
2661
- >
2662
- {title}
2663
- </h2>
2664
- )}
2665
- <div
2690
+ {diagnostic && (
2691
+ <SubsystemDiagnosticToggle
2692
+ {...diagnostic}
2693
+ active={issuesActive}
2694
+ onToggle={toggleIssues}
2695
+ />
2696
+ )}
2697
+ {description && (
2698
+ <button
2699
+ type="button"
2700
+ aria-expanded={descriptionVisible}
2701
+ aria-label={descriptionVisible ? 'Hide description' : 'Show description'}
2702
+ title={descriptionVisible ? 'Hide description' : 'Show description'}
2703
+ onMouseEnter={() => setDescToggleHover(true)}
2704
+ onMouseLeave={() => setDescToggleHover(false)}
2705
+ onClick={() => setDescriptionVisible((v) => !v)}
2666
2706
  style={{
2667
- display: 'flex',
2707
+ display: 'inline-flex',
2668
2708
  alignItems: 'center',
2669
- gap: 4,
2709
+ justifyContent: 'center',
2670
2710
  flexShrink: 0,
2671
- // Title's flex:1 already pushes this cluster right; without a
2672
- // title, keep the actions flush right instead of flush-left.
2673
- marginLeft: title ? undefined : 'auto',
2711
+ width: 22,
2712
+ height: 22,
2713
+ padding: 0,
2714
+ border: 'none',
2715
+ borderRadius: 4,
2716
+ background: descriptionVisible || descToggleHover ? theme.colors.border : 'transparent',
2717
+ color: descriptionVisible || descToggleHover ? theme.colors.text : (theme.colors.textMuted ?? theme.colors.textSecondary),
2718
+ cursor: 'pointer',
2719
+ transition: 'background 120ms ease, color 120ms ease',
2674
2720
  }}
2675
2721
  >
2676
- {diagnostic && (
2677
- <SubsystemDiagnosticToggle
2678
- {...diagnostic}
2679
- active={issuesActive}
2680
- onToggle={toggleIssues}
2681
- />
2682
- )}
2683
- {description && (
2684
- <button
2685
- type="button"
2686
- aria-expanded={descriptionVisible}
2687
- aria-label={descriptionVisible ? 'Hide description' : 'Show description'}
2688
- title={descriptionVisible ? 'Hide description' : 'Show description'}
2689
- onMouseEnter={() => setDescToggleHover(true)}
2690
- onMouseLeave={() => setDescToggleHover(false)}
2691
- onClick={() => setDescriptionVisible((v) => !v)}
2692
- style={{
2693
- display: 'inline-flex',
2694
- alignItems: 'center',
2695
- justifyContent: 'center',
2696
- flexShrink: 0,
2697
- width: 22,
2698
- height: 22,
2699
- padding: 0,
2700
- border: 'none',
2701
- borderRadius: 4,
2702
- background: descriptionVisible || descToggleHover ? theme.colors.border : 'transparent',
2703
- color: descriptionVisible || descToggleHover ? theme.colors.text : (theme.colors.textMuted ?? theme.colors.textSecondary),
2704
- cursor: 'pointer',
2705
- transition: 'background 120ms ease, color 120ms ease',
2706
- }}
2707
- >
2708
- <FileText size={14} />
2709
- </button>
2710
- )}
2711
- </div>
2712
- </div>
2713
- )}
2714
- {description && descriptionVisible && (
2715
- <div style={{ fontSize: theme.fontSizes[0], lineHeight: 1.5 }}>
2716
- <IndustryMarkdownSlide
2717
- content={description}
2718
- slideIdPrefix="subsystem-desc"
2719
- slideIndex={0}
2720
- isVisible={true}
2721
- theme={theme}
2722
- disableScroll={true}
2723
- disableBasePadding
2724
- enableKeyboardScrolling={false}
2725
- autoFocusOnVisible={false}
2726
- />
2727
- </div>
2728
- )}
2729
- {sidebarAfterDescription}
2722
+ <FileText size={14} />
2723
+ </button>
2724
+ )}
2730
2725
  </div>
2731
- {(treeFilePaths.length > 0 || hasTrails || issuesActive) && (
2732
- <div
2733
- style={{
2734
- ...(showDesc ? { height: '50%' as const } : { flex: 1, minHeight: 0 }),
2735
- minHeight: 160,
2736
- flexShrink: 0,
2737
- borderTop: `1px solid ${theme.colors.border}`,
2738
- display: 'flex',
2739
- flexDirection: 'column',
2740
- overflow: 'hidden',
2741
- }}
2742
- >
2743
- {issuesActive ? (
2744
- agentsPanel ? (
2745
- <div style={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column' }}>
2746
- <div
2747
- role="tablist"
2748
- aria-label="Diagnostics view"
2749
- style={{
2750
- display: 'flex',
2751
- width: '100%',
2752
- flexShrink: 0,
2753
- borderBottom: `1px solid ${theme.colors.border}`,
2754
- background: theme.colors.backgroundSecondary ?? theme.colors.background,
2755
- }}
2756
- >
2757
- {(['issues', 'agents'] as const).map((tab) => (
2758
- <button
2759
- key={tab}
2760
- type="button"
2761
- role="tab"
2762
- aria-selected={diagnosticsTab === tab}
2763
- onClick={() => setDiagnosticsTab(tab)}
2764
- style={{
2765
- flex: 1,
2766
- minWidth: 0,
2767
- padding: '8px 8px',
2768
- border: 'none',
2769
- borderRadius: 0,
2770
- background: diagnosticsTab === tab ? theme.colors.background : 'transparent',
2771
- color:
2772
- diagnosticsTab === tab ? theme.colors.text : theme.colors.textSecondary,
2773
- fontSize: theme.fontSizes[1],
2774
- fontFamily: theme.fonts.monospace,
2775
- textTransform: 'capitalize',
2776
- cursor: 'pointer',
2777
- }}
2778
- >
2779
- {tab === 'issues' ? 'Issues' : 'Agents'}
2780
- </button>
2781
- ))}
2782
- </div>
2783
- {diagnosticsTab === 'agents' ? (
2784
- <SubsystemAgentsPanel {...agentsPanel} />
2785
- ) : (
2786
- <SubsystemIssueList
2787
- issues={issues ?? []}
2788
- focusCategory={focusIssueCategory}
2789
- laneStatus={laneStatus}
2790
- onSelectIssue={focusIssueTarget}
2791
- onDeselectIssue={unfocusIssueTarget}
2792
- onApplyFix={onApplyIssueFix}
2793
- onHoverIssue={onHoverIssue}
2794
- onExpandedCategoriesChange={handleExpandedCategoriesChange}
2795
- />
2796
- )}
2797
- </div>
2798
- ) : (
2799
- <SubsystemIssueList
2800
- issues={issues ?? []}
2801
- focusCategory={focusIssueCategory}
2802
- laneStatus={laneStatus}
2803
- onSelectIssue={focusIssueTarget}
2804
- onDeselectIssue={unfocusIssueTarget}
2805
- onApplyFix={onApplyIssueFix}
2806
- onHoverIssue={onHoverIssue}
2807
- onExpandedCategoriesChange={handleExpandedCategoriesChange}
2808
- />
2809
- )
2810
- ) : (
2811
- <>
2812
- {hasTrails && (
2813
- <div
2814
- role="tablist"
2815
- aria-label="Sidebar view"
2816
- style={{
2817
- display: 'flex',
2818
- width: '100%',
2819
- flexShrink: 0,
2820
- borderBottom: `1px solid ${theme.colors.border}`,
2821
- background: theme.colors.backgroundSecondary ?? theme.colors.background,
2822
- }}
2823
- >
2824
- {(['trails', 'files'] as const).map((view) => (
2825
- <button
2826
- key={view}
2827
- type="button"
2828
- role="tab"
2829
- aria-selected={sidebarView === view}
2830
- onClick={() => changeSidebarView(view)}
2831
- style={{
2832
- flex: 1,
2833
- minWidth: 0,
2834
- padding: '8px 8px',
2835
- border: 'none',
2836
- borderRadius: 0,
2837
- background: sidebarView === view ? theme.colors.background : 'transparent',
2838
- color:
2839
- sidebarView === view
2840
- ? theme.colors.text
2841
- : theme.colors.textSecondary,
2842
- fontSize: theme.fontSizes[1],
2843
- fontFamily: theme.fonts.monospace,
2844
- textTransform: 'capitalize',
2845
- cursor: 'pointer',
2846
- }}
2847
- >
2848
- {view === 'trails' ? 'Trails' : 'Files'}
2849
- </button>
2850
- ))}
2851
- </div>
2852
- )}
2853
- {sidebarView === 'trails' && trails && trails.length > 0 ? (
2854
- <TrailsPanel
2855
- trails={trails}
2856
- expandedTrails={expandedTrails}
2857
- focusedTrailId={focusedTrailId}
2858
- focusedStepIndex={focusedStepIndex}
2859
- hoveredTrailStep={hoveredTrailStep}
2860
- onToggleCollapsed={toggleTrailCollapsed}
2861
- onFocusFlow={focusTrailEdges}
2862
- onClearFocus={clearTrailFocus}
2863
- onFocusStep={focusTrailStep}
2864
- onHoverStep={(tl, i) =>
2865
- setHoveredTrailStep({ trailId: tl.id, stepIndex: i })
2866
- }
2867
- onHoverFlow={(tl) =>
2868
- setHoveredTrailStep({ trailId: tl.id, stepIndex: null })
2869
- }
2870
- onLeaveStep={() => setHoveredTrailStep(null)}
2871
- onReorder={onReorderTrails}
2872
- proposedAliases={proposedAliases}
2873
- />
2874
- ) : treeFilePaths.length > 0 ? (
2875
- <>
2876
- {repoGroups.groups.map((group, i) => {
2877
- const groupKey = group.repoKey ?? '__no-repo__';
2878
- const collapsed = collapsedRepos.has(groupKey);
2879
- return (
2880
- <div
2881
- key={groupKey}
2882
- style={{
2883
- flex: collapsed ? '0 0 auto' : 1,
2884
- minHeight: collapsed ? 0 : undefined,
2885
- display: 'flex',
2886
- flexDirection: 'column',
2887
- borderTop: i > 0 ? `1px solid ${theme.colors.border}` : undefined,
2888
- }}
2889
- >
2890
- {repoGroups.multiRepo && (
2891
- <RepoGroupHeader
2892
- group={group}
2893
- collapsed={collapsed}
2894
- onToggle={() => toggleRepoCollapsed(groupKey)}
2895
- />
2896
- )}
2897
- {!collapsed && (
2898
- <SubsystemFileTree
2899
- files={group.entries.map((e) => e.displayPath)}
2900
- selectedFile={selected?.file ?? openFile}
2901
- hoveredFile={hoveredFile}
2902
- onSelectFile={onTreeSelectFile}
2903
- hoveredFolder={hoveredBoundaryPath}
2904
- headerless={repoGroups.multiRepo}
2905
- />
2906
- )}
2907
- </div>
2908
- );
2909
- })}
2910
- </>
2911
- ) : null}
2912
- </>
2913
- )}
2914
- </div>
2915
- )}
2916
2726
  </div>
2917
2727
  )}
2918
-
2919
- {/* Drag handle between sidebar and canvas — resize the left panel. */}
2920
- {!hideSidebar && (title || description || diagnostic || issuesActive || sidebarExtra || sidebarAfterDescription || treeFilePaths.length > 0 || hasTrails) && (
2921
- <div
2922
- onMouseDown={onSidebarResizeStart}
2923
- aria-label="Resize sidebar"
2924
- title="Drag to resize"
2925
- style={{
2926
- width: 3,
2927
- flexShrink: 0,
2928
- cursor: 'col-resize',
2929
- background: theme.colors.border,
2930
- transition: 'background 120ms ease',
2931
- zIndex: 1,
2932
- }}
2933
- />
2728
+ {description && descriptionVisible && (
2729
+ <div style={{ fontSize: theme.fontSizes[0], lineHeight: 1.5 }}>
2730
+ <IndustryMarkdownSlide
2731
+ content={description}
2732
+ slideIdPrefix="subsystem-desc"
2733
+ slideIndex={0}
2734
+ isVisible={true}
2735
+ theme={theme}
2736
+ disableScroll={true}
2737
+ disableBasePadding
2738
+ enableKeyboardScrolling={false}
2739
+ autoFocusOnVisible={false}
2740
+ />
2741
+ </div>
2934
2742
  )}
2935
-
2743
+ {sidebarAfterDescription}
2744
+ </>
2745
+ );
2746
+ // Lower panel: the Issues/Agents diagnostics stack, or the Files/Trails
2747
+ // stack with the trail flow list / repo file tree.
2748
+ const sidebarBody = issuesActive ? (
2749
+ diagnosticsTab === 'agents' && agentsPanel ? (
2750
+ <SubsystemAgentsPanel {...agentsPanel} />
2751
+ ) : (
2752
+ <SubsystemIssueList
2753
+ issues={issues ?? []}
2754
+ focusCategory={focusIssueCategory}
2755
+ laneStatus={laneStatus}
2756
+ onSelectIssue={focusIssueTarget}
2757
+ onDeselectIssue={unfocusIssueTarget}
2758
+ onApplyFix={onApplyIssueFix}
2759
+ onHoverIssue={onHoverIssue}
2760
+ onExpandedCategoriesChange={handleExpandedCategoriesChange}
2761
+ />
2762
+ )
2763
+ ) : sidebarView === 'trails' && trails && trails.length > 0 ? (
2764
+ <TrailsPanel
2765
+ trails={trails}
2766
+ expandedTrails={expandedTrails}
2767
+ focusedTrailId={focusedTrailId}
2768
+ focusedStepIndex={focusedStepIndex}
2769
+ hoveredTrailStep={hoveredTrailStep}
2770
+ onToggleCollapsed={toggleTrailCollapsed}
2771
+ onFocusFlow={focusTrailEdges}
2772
+ onClearFocus={clearTrailFocus}
2773
+ onFocusStep={focusTrailStep}
2774
+ onHoverStep={(tl, i) => {
2775
+ setHoveredTrailStep({ trailId: tl.id, stepIndex: i });
2776
+ onTrailStepHover?.({ trailId: tl.id, stepIndex: i });
2777
+ }}
2778
+ onHoverFlow={(tl) => {
2779
+ setHoveredTrailStep({ trailId: tl.id, stepIndex: null });
2780
+ onTrailStepHover?.(null);
2781
+ }}
2782
+ onLeaveStep={() => {
2783
+ setHoveredTrailStep(null);
2784
+ onTrailStepHover?.(null);
2785
+ }}
2786
+ onReorder={onReorderTrails}
2787
+ proposedAliases={proposedAliases}
2788
+ />
2789
+ ) : treeFilePaths.length > 0 ? (
2790
+ <>
2791
+ {repoGroups.groups.map((group, i) => {
2792
+ const groupKey = group.repoKey ?? '__no-repo__';
2793
+ const collapsed = collapsedRepos.has(groupKey);
2794
+ return (
2795
+ <div
2796
+ key={groupKey}
2797
+ style={{
2798
+ flex: collapsed ? '0 0 auto' : 1,
2799
+ minHeight: collapsed ? 0 : undefined,
2800
+ display: 'flex',
2801
+ flexDirection: 'column',
2802
+ borderTop: i > 0 ? `1px solid ${theme.colors.border}` : undefined,
2803
+ }}
2804
+ >
2805
+ {repoGroups.multiRepo && (
2806
+ <RepoGroupHeader
2807
+ group={group}
2808
+ collapsed={collapsed}
2809
+ onToggle={() => toggleRepoCollapsed(groupKey)}
2810
+ />
2811
+ )}
2812
+ {!collapsed && (
2813
+ <SubsystemFileTree
2814
+ files={group.entries.map((e) => e.displayPath)}
2815
+ selectedFile={selected?.file ?? openFile}
2816
+ hoveredFile={hoveredFile}
2817
+ onSelectFile={onTreeSelectFile}
2818
+ hoveredFolder={hoveredBoundaryPath}
2819
+ headerless={repoGroups.multiRepo}
2820
+ />
2821
+ )}
2822
+ </div>
2823
+ );
2824
+ })}
2825
+ </>
2826
+ ) : null;
2827
+ return (
2828
+ <GraphWithSidebar
2829
+ hidden={!sidebarVisible}
2830
+ bodyHidden={!sidebarBodyVisible}
2831
+ stretchHeader={showDesc}
2832
+ header={sidebarHeader}
2833
+ tabs={sidebarTabs}
2834
+ activeTab={activeSidebarTab}
2835
+ onTabChange={(id) =>
2836
+ issuesActive
2837
+ ? setDiagnosticsTab(id as 'issues' | 'agents')
2838
+ : changeSidebarView(id as 'files' | 'trails')
2839
+ }
2840
+ sidebarBody={sidebarBody}
2841
+ tabListLabel={issuesActive ? 'Diagnostics view' : 'Sidebar view'}
2842
+ defaultWidth={persisted.sidebarWidth ?? 450}
2843
+ minWidth={240}
2844
+ maxWidth={sidebarMaxWidth}
2845
+ persistKey={persistKey}
2846
+ onResizeEnd={() => requestAnimationFrame(() => fitView())}
2847
+ >
2936
2848
  {/* Graph area */}
2937
2849
  <div style={{ flex: 1, position: 'relative', display: 'flex', flexDirection: 'column', minHeight: 0 }}>
2938
2850
  {/* Canvas box — the edge-label overlay and legend anchor here, so
@@ -3324,7 +3236,7 @@ function Inner({ components, trails, graphifyRelations, orderByLine, initialTrai
3324
3236
  drawerTarget.startLine != null &&
3325
3237
  !!selected.file &&
3326
3238
  selected.file === drawerTarget.file &&
3327
- selected.declarationRef?.startLine === drawerTarget.startLine
3239
+ selected.declarationSpan?.startLine === drawerTarget.startLine
3328
3240
  }
3329
3241
  />
3330
3242
  </div>
@@ -3356,6 +3268,7 @@ function Inner({ components, trails, graphifyRelations, orderByLine, initialTrai
3356
3268
  render={renderDrawerContent}
3357
3269
  file={drawerTarget.file}
3358
3270
  startLine={drawerTarget.startLine}
3271
+ endLine={drawerTarget.endLine}
3359
3272
  />
3360
3273
  ) : null}
3361
3274
  </FileDrawer>
@@ -3403,6 +3316,7 @@ function Inner({ components, trails, graphifyRelations, orderByLine, initialTrai
3403
3316
  render={renderDrawerContent}
3404
3317
  file={fileOverlay.file}
3405
3318
  startLine={fileOverlay.startLine}
3319
+ endLine={fileOverlay.endLine}
3406
3320
  fullFile
3407
3321
  />
3408
3322
  </div>
@@ -3413,7 +3327,7 @@ function Inner({ components, trails, graphifyRelations, orderByLine, initialTrai
3413
3327
  {canvasOverlay}
3414
3328
  {liveEvents ? <MaintainLivePanel {...liveEvents} /> : null}
3415
3329
  </div>
3416
- </div>
3330
+ </GraphWithSidebar>
3417
3331
  );
3418
3332
  }
3419
3333