@principal-ai/subsystems-react 0.33.0 → 0.35.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 (38) hide show
  1. package/dist/index.d.ts +4 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +2 -0
  4. package/dist/index.js.map +1 -1
  5. package/dist/subsystem/ComponentDeclaration.d.ts +7 -1
  6. package/dist/subsystem/ComponentDeclaration.d.ts.map +1 -1
  7. package/dist/subsystem/ComponentDeclaration.js +169 -5
  8. package/dist/subsystem/ComponentDeclaration.js.map +1 -1
  9. package/dist/subsystem/ConstructsCatalog.d.ts +9 -1
  10. package/dist/subsystem/ConstructsCatalog.d.ts.map +1 -1
  11. package/dist/subsystem/ConstructsCatalog.js +9 -2
  12. package/dist/subsystem/ConstructsCatalog.js.map +1 -1
  13. package/dist/subsystem/IssueList.d.ts +4 -2
  14. package/dist/subsystem/IssueList.d.ts.map +1 -1
  15. package/dist/subsystem/IssueList.js +6 -6
  16. package/dist/subsystem/IssueList.js.map +1 -1
  17. package/dist/subsystem/SubsystemComponentGraph.d.ts +13 -0
  18. package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
  19. package/dist/subsystem/SubsystemComponentGraph.js +10 -3
  20. package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
  21. package/dist/subsystem/SymbolInspectionCard.d.ts +31 -0
  22. package/dist/subsystem/SymbolInspectionCard.d.ts.map +1 -0
  23. package/dist/subsystem/SymbolInspectionCard.js +247 -0
  24. package/dist/subsystem/SymbolInspectionCard.js.map +1 -0
  25. package/dist/subsystem/symbolRefs.d.ts +95 -0
  26. package/dist/subsystem/symbolRefs.d.ts.map +1 -0
  27. package/dist/subsystem/symbolRefs.js +167 -0
  28. package/dist/subsystem/symbolRefs.js.map +1 -0
  29. package/package.json +1 -1
  30. package/src/index.ts +14 -0
  31. package/src/stories/Subsystem/ComponentDeclarationSymbols.stories.tsx +269 -0
  32. package/src/subsystem/ComponentDeclaration.tsx +239 -9
  33. package/src/subsystem/ConstructsCatalog.tsx +20 -0
  34. package/src/subsystem/IssueList.tsx +10 -8
  35. package/src/subsystem/SubsystemComponentGraph.tsx +26 -1
  36. package/src/subsystem/SymbolInspectionCard.tsx +433 -0
  37. package/src/subsystem/symbolRefs.test.ts +127 -0
  38. package/src/subsystem/symbolRefs.ts +256 -0
@@ -6,7 +6,7 @@
6
6
  * comments. File content lives in the bottom FileDrawer, not here.
7
7
  */
8
8
 
9
- import { useEffect, useRef, useState, type ReactNode } from 'react';
9
+ import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from 'react';
10
10
  import { AlignLeft, FileText } from 'lucide-react';
11
11
  import { useTheme } from '@principal-ade/industry-theme';
12
12
  import { resolvePierreSyntaxThemeName } from '../pierre/pierreSyntaxTheme';
@@ -19,6 +19,38 @@ import type { SubsystemOpenFileOptions } from './declarationRef';
19
19
  import { parseSourceLocation } from './declarationRef';
20
20
  import { tokenizeComponent } from './tokenizeComponent';
21
21
  import { componentColor } from '../pierre/constructColors';
22
+ import type { DeclarationSymbolRef, SymbolInspection } from './symbolRefs';
23
+ import { extractDeclarationSymbolRefs } from './symbolRefs';
24
+ import { SymbolInspectionCard } from './SymbolInspectionCard';
25
+
26
+ /** One clickable span in the token stream, mapped to a referenced symbol. */
27
+ interface SymbolClickRange {
28
+ start: number;
29
+ end: number;
30
+ ref: DeclarationSymbolRef;
31
+ }
32
+
33
+ /** Whole-word occurrences of `name` in `text` (identifier boundaries only). */
34
+ function findSymbolOccurrences(
35
+ text: string,
36
+ name: string,
37
+ ): { start: number; end: number }[] {
38
+ const out: { start: number; end: number }[] = [];
39
+ if (!name) return out;
40
+ const isWordChar = (ch: string) => /[\w$]/.test(ch);
41
+ let from = 0;
42
+ for (;;) {
43
+ const idx = text.indexOf(name, from);
44
+ if (idx < 0) break;
45
+ const before = idx > 0 ? text[idx - 1]! : '';
46
+ const after = text[idx + name.length] ?? '';
47
+ if ((!before || !isWordChar(before)) && (!after || !isWordChar(after))) {
48
+ out.push({ start: idx, end: idx + name.length });
49
+ }
50
+ from = idx + name.length;
51
+ }
52
+ return out;
53
+ }
22
54
 
23
55
  /** Live / result state for the declaration-panel Verify control. */
24
56
  export type ComponentVerificationPhase =
@@ -113,6 +145,14 @@ export interface ComponentDeclarationProps {
113
145
  /** Related-name click (types, callers/callees, implementors, …) → select
114
146
  * that component if one matches; unmatched refs no-op. */
115
147
  onRelatedSelect?: (ref: string) => void;
148
+ /** Referenced-symbol click → resolve it against graphify. When absent,
149
+ * symbols are not clickable. Host-injected; stories pass a fixture. */
150
+ onInspectSymbol?: (
151
+ symbol: string,
152
+ ref: DeclarationSymbolRef,
153
+ ) => Promise<SymbolInspection | null> | SymbolInspection | null;
154
+ /** "Add to references" from the inspection card. */
155
+ onAddToModel?: (ref: DeclarationSymbolRef, info?: SymbolInspection) => void;
116
156
  /** Max width of the declaration panel (CSS value). Defaults to none. */
117
157
  maxWidth?: string | number;
118
158
  /** Live verification status for the selected component. */
@@ -267,6 +307,8 @@ export function ComponentDeclaration({
267
307
  component,
268
308
  onOpenFile,
269
309
  onRelatedSelect: _onRelatedSelect,
310
+ onInspectSymbol,
311
+ onAddToModel,
270
312
  maxWidth,
271
313
  verification,
272
314
  defaultShowFile = false,
@@ -300,6 +342,53 @@ export function ComponentDeclaration({
300
342
  /** Fallback when wire tokens omit Shiki colors. */
301
343
  const declarationTextColor = muted;
302
344
 
345
+ // Symbols the declaration references — made clickable for a graphify lookup.
346
+ const symbolRefs = useMemo(
347
+ () => extractDeclarationSymbolRefs(component.declaration),
348
+ [component.declaration],
349
+ );
350
+ const [inspection, setInspection] = useState<
351
+ | {
352
+ ref: DeclarationSymbolRef;
353
+ loading: boolean;
354
+ info?: SymbolInspection;
355
+ error?: string;
356
+ x: number;
357
+ y: number;
358
+ }
359
+ | null
360
+ >(null);
361
+ // Symbol name currently hovered, so every occurrence of the same symbol can
362
+ // highlight together (and stay lit while its popover is open).
363
+ const [hoveredSymbol, setHoveredSymbol] = useState<string | null>(null);
364
+ const activeSymbol = inspection?.ref.name ?? null;
365
+
366
+ // Dismiss the inspection card on outside click / Escape.
367
+ useEffect(() => {
368
+ if (!inspection) return;
369
+ const onDown = (e: MouseEvent) => {
370
+ const t = e.target as HTMLElement | null;
371
+ // The popover, and the symbol spans themselves (they toggle on click),
372
+ // must not be treated as outside.
373
+ if (t?.closest('[data-symbol-inspection]')) return;
374
+ if (t?.closest('[data-symbol-ref]')) return;
375
+ setInspection(null);
376
+ };
377
+ const onKey = (e: KeyboardEvent) => {
378
+ if (e.key === 'Escape') {
379
+ // Drop focus from the symbol span so no focus ring lingers after close.
380
+ (document.activeElement as HTMLElement | null)?.blur?.();
381
+ setInspection(null);
382
+ }
383
+ };
384
+ document.addEventListener('mousedown', onDown);
385
+ document.addEventListener('keydown', onKey);
386
+ return () => {
387
+ document.removeEventListener('mousedown', onDown);
388
+ document.removeEventListener('keydown', onKey);
389
+ };
390
+ }, [inspection]);
391
+
303
392
  const declarationStartLine =
304
393
  component.declarationRef?.startLine ??
305
394
  verification?.declaration?.ref?.startLine ??
@@ -729,22 +818,140 @@ export function ComponentDeclaration({
729
818
  cancelled = true;
730
819
  };
731
820
  }, [component, printWidth, pierreSyntaxTheme]);
821
+
822
+ // Map character offsets in the concatenated token text to referenced symbols,
823
+ // so inline occurrences render as clickable spans. `-1` = not a symbol.
824
+ const clickIndex = useMemo(() => {
825
+ const full = tokens.map((t) => t.text).join('');
826
+ const rangeAt = new Int32Array(full.length).fill(-1);
827
+ if (!onInspectSymbol || symbolRefs.length === 0) {
828
+ return { ranges: [] as SymbolClickRange[], rangeAt };
829
+ }
830
+ const ranges: SymbolClickRange[] = [];
831
+ for (const ref of symbolRefs) {
832
+ for (const occ of findSymbolOccurrences(full, ref.name)) {
833
+ ranges.push({ start: occ.start, end: occ.end, ref });
834
+ }
835
+ }
836
+ ranges.sort((a, b) => a.start - b.start);
837
+ ranges.forEach((r, i) => {
838
+ for (let k = r.start; k < r.end && k < rangeAt.length; k++) {
839
+ if (rangeAt[k] === -1) rangeAt[k] = i;
840
+ }
841
+ });
842
+ return { ranges, rangeAt };
843
+ }, [onInspectSymbol, symbolRefs, tokens]);
844
+
845
+ const handleInspect = useCallback(
846
+ async (ref: DeclarationSymbolRef, el: HTMLElement) => {
847
+ if (!onInspectSymbol) return;
848
+ // Toggle: clicking the symbol whose popover is already open closes it.
849
+ if (inspection?.ref.name === ref.name) {
850
+ setInspection(null);
851
+ return;
852
+ }
853
+ let x = 0;
854
+ let y = 0;
855
+ const cont = containerRef.current;
856
+ if (cont) {
857
+ const crect = cont.getBoundingClientRect();
858
+ const r = el.getBoundingClientRect();
859
+ x = r.left - crect.left;
860
+ y = r.bottom - crect.top + 4;
861
+ }
862
+ setInspection({ ref, loading: true, x, y });
863
+ try {
864
+ const info = await onInspectSymbol(ref.name, ref);
865
+ setInspection((cur) =>
866
+ cur?.ref.name === ref.name
867
+ ? { ref, loading: false, info: info ?? undefined, x, y }
868
+ : cur,
869
+ );
870
+ } catch (err) {
871
+ setInspection((cur) =>
872
+ cur?.ref.name === ref.name
873
+ ? {
874
+ ref,
875
+ loading: false,
876
+ error: err instanceof Error ? err.message : String(err),
877
+ x,
878
+ y,
879
+ }
880
+ : cur,
881
+ );
882
+ }
883
+ },
884
+ [onInspectSymbol, inspection],
885
+ );
886
+
732
887
  const declLines: ReactNode[][] = [[]];
733
888
  let di = 0;
889
+ let offset = 0;
734
890
  for (const tok of tokens) {
735
891
  if (tok.kind === 'newline') {
736
892
  declLines.push([]);
737
893
  di++;
738
894
  continue;
739
895
  }
740
- declLines[di].push(
741
- <span
742
- key={`${di}-${declLines[di].length}`}
743
- style={{ color: tok.color ?? tokenColor[tok.kind] ?? declarationTextColor }}
744
- >
745
- {tok.text}
746
- </span>,
747
- );
896
+ const color = tok.color ?? tokenColor[tok.kind] ?? declarationTextColor;
897
+ let i = 0;
898
+ while (i < tok.text.length) {
899
+ const rIdx = clickIndex.rangeAt[offset + i] ?? -1;
900
+ let j = i + 1;
901
+ while (j < tok.text.length && (clickIndex.rangeAt[offset + j] ?? -1) === rIdx) {
902
+ j++;
903
+ }
904
+ const piece = tok.text.slice(i, j);
905
+ const key = `${di}-${declLines[di].length}`;
906
+ const range = rIdx >= 0 ? clickIndex.ranges[rIdx] : undefined;
907
+ if (range) {
908
+ const ref = range.ref;
909
+ const hovered = hoveredSymbol === ref.name;
910
+ // Active (popover open for this symbol) is the only state that gets a
911
+ // solid underline; hover just shifts the color.
912
+ const active = activeSymbol === ref.name;
913
+ const lit = hovered || active;
914
+ declLines[di].push(
915
+ <span
916
+ key={key}
917
+ role="button"
918
+ tabIndex={0}
919
+ data-symbol-ref={ref.name}
920
+ // Don't take focus on mouse click — otherwise closing the popover
921
+ // (Escape) leaves a focus ring around the clicked symbol.
922
+ onMouseDown={(e) => e.preventDefault()}
923
+ onMouseEnter={() => setHoveredSymbol(ref.name)}
924
+ onMouseLeave={(e) => {
925
+ // Keep it lit when moving straight to another occurrence of the
926
+ // same symbol; otherwise clear on unhover.
927
+ const to = e.relatedTarget as HTMLElement | null;
928
+ const next = to?.closest('[data-symbol-ref]')?.getAttribute('data-symbol-ref');
929
+ if (next === ref.name) return;
930
+ setHoveredSymbol((s) => (s === ref.name ? null : s));
931
+ }}
932
+ onClick={(e) => handleInspect(ref, e.currentTarget)}
933
+ style={{
934
+ // Lit occurrences shift to the accent color (no background fill).
935
+ color: lit ? theme.colors.accent ?? theme.colors.secondary : color,
936
+ cursor: 'pointer',
937
+ textDecorationLine: 'underline',
938
+ textDecorationStyle: active ? 'solid' : 'dotted',
939
+ textUnderlineOffset: 2,
940
+ }}
941
+ >
942
+ {piece}
943
+ </span>,
944
+ );
945
+ } else {
946
+ declLines[di].push(
947
+ <span key={key} style={{ color }}>
948
+ {piece}
949
+ </span>,
950
+ );
951
+ }
952
+ i = j;
953
+ }
954
+ offset += tok.text.length;
748
955
  }
749
956
  for (let li = 0; li < declLines.length; li++) {
750
957
  if (declLines[li].length === 0) continue;
@@ -783,10 +990,22 @@ export function ComponentDeclaration({
783
990
  );
784
991
  }
785
992
 
993
+ const inspectionLeft = inspection
994
+ ? Math.max(
995
+ 0,
996
+ Math.min(
997
+ inspection.x,
998
+ Math.max(0, (containerRef.current?.clientWidth ?? 320) - 320),
999
+ ),
1000
+ )
1001
+ : 0;
1002
+
786
1003
  return (
787
1004
  <div
788
1005
  ref={containerRef}
1006
+ onMouseLeave={() => setHoveredSymbol(null)}
789
1007
  style={{
1008
+ position: 'relative',
790
1009
  width: '100%',
791
1010
  minWidth: 0,
792
1011
  boxSizing: 'border-box',
@@ -800,6 +1019,17 @@ export function ComponentDeclaration({
800
1019
  }}
801
1020
  >
802
1021
  {lines}
1022
+ {inspection && !inspection.loading && (
1023
+ <SymbolInspectionCard
1024
+ symbolRef={inspection.ref}
1025
+ inspection={inspection.info}
1026
+ error={inspection.error}
1027
+ onClose={() => setInspection(null)}
1028
+ onAddToModel={onAddToModel}
1029
+ onOpenFile={onOpenFile}
1030
+ style={{ left: inspectionLeft, top: inspection.y }}
1031
+ />
1032
+ )}
803
1033
  </div>
804
1034
  );
805
1035
  }
@@ -22,6 +22,7 @@ import {
22
22
  import type { SubsystemOpenFileOptions } from './declarationRef';
23
23
  import { ComponentDeclaration } from './ComponentDeclaration';
24
24
  import type { ComponentVerificationState } from './ComponentDeclaration';
25
+ import type { DeclarationSymbolRef, SymbolInspection } from './symbolRefs';
25
26
  import { SubsystemDiagnosticToggle, type SubsystemDiagnostic } from './DiagnosticToggle';
26
27
  import { FileDrawer } from './FileDrawer';
27
28
  import { componentColor } from '../pierre/constructColors';
@@ -40,6 +41,13 @@ export interface ConstructsCatalogProps {
40
41
  renderFileView?: (component: SubsystemComponent) => ReactNode;
41
42
  onFileSelect?: (file: string) => void;
42
43
  componentVerification?: ComponentVerificationState | null;
44
+ /** Referenced-symbol click → host graphify lookup (purl/file from the card). */
45
+ onInspectSymbol?: (req: {
46
+ purl: string;
47
+ file: string;
48
+ symbol: string;
49
+ ref: DeclarationSymbolRef;
50
+ }) => Promise<SymbolInspection | null> | SymbolInspection | null;
43
51
  }
44
52
 
45
53
  const FileDrawerContent = memo(function FileDrawerContent({
@@ -145,6 +153,7 @@ export function ConstructsCatalog({
145
153
  renderFileView,
146
154
  onFileSelect,
147
155
  componentVerification,
156
+ onInspectSymbol,
148
157
  }: ConstructsCatalogProps) {
149
158
  const { theme, mode } = useTheme();
150
159
  const pierreTheme = resolvePierreSyntaxThemeName(mode);
@@ -948,6 +957,17 @@ export function ConstructsCatalog({
948
957
  onOpenFile={fileViewer ? onOpenFile : undefined}
949
958
  defaultShowFile={!!fileViewer}
950
959
  onRelatedSelect={onRelatedSelect}
960
+ onInspectSymbol={
961
+ onInspectSymbol
962
+ ? (symbol, ref) =>
963
+ onInspectSymbol({
964
+ purl: c.purl,
965
+ file: c.file,
966
+ symbol,
967
+ ref,
968
+ })
969
+ : undefined
970
+ }
951
971
  fileOpen={fileOpen}
952
972
  declarationOpen={lineOpen}
953
973
  showRepoIdentity={false}
@@ -38,13 +38,15 @@ export type SubsystemIssueTargetKind =
38
38
 
39
39
  /**
40
40
  * The four verification layers, in order. Mirrors the docs' progression:
41
- * constructs → static topology → runtime topology → walkthrough.
41
+ * constructs → static topology → dynamic topology → walkthrough.
42
+ * Static topology = relations; dynamic topology = process (runtime) +
43
+ * package/module (containment).
42
44
  */
43
45
  export type SubsystemIssueCategory =
44
46
  | 'repo'
45
47
  | 'construct'
46
48
  | 'static-topology'
47
- | 'runtime-topology'
49
+ | 'dynamic-topology'
48
50
  | 'walkthrough';
49
51
 
50
52
  /** Layer order + display names, shared by the list and its headers. */
@@ -52,7 +54,7 @@ export const SUBSYSTEM_ISSUE_CATEGORIES: SubsystemIssueCategory[] = [
52
54
  'repo',
53
55
  'construct',
54
56
  'static-topology',
55
- 'runtime-topology',
57
+ 'dynamic-topology',
56
58
  'walkthrough',
57
59
  ];
58
60
 
@@ -64,7 +66,7 @@ export const SUBSYSTEM_ISSUE_CATEGORY_ICON: Record<
64
66
  repo: FolderGit2,
65
67
  construct: Component,
66
68
  'static-topology': Network,
67
- 'runtime-topology': Server,
69
+ 'dynamic-topology': Server,
68
70
  walkthrough: Route,
69
71
  };
70
72
 
@@ -75,11 +77,11 @@ export const SUBSYSTEM_ISSUE_CATEGORY_LABEL: Record<
75
77
  repo: 'Repositories',
76
78
  construct: 'Constructs',
77
79
  'static-topology': 'Static topology',
78
- 'runtime-topology': 'Runtime topology',
80
+ 'dynamic-topology': 'Dynamic topology',
79
81
  walkthrough: 'Walkthrough',
80
82
  };
81
83
 
82
- /** Default `kind` → layer. `boundary_*` splits by module (static) vs process (runtime). */
84
+ /** Default `kind` → layer. `boundary_*` (module containment + process) is dynamic. */
83
85
  const KIND_CATEGORY: Record<string, SubsystemIssueCategory> = {
84
86
  missing_file: 'construct',
85
87
  symbol_ambiguous: 'construct',
@@ -94,8 +96,8 @@ const KIND_CATEGORY: Record<string, SubsystemIssueCategory> = {
94
96
  topology_broken_endpoint: 'static-topology',
95
97
  topology_import_unconfirmed: 'static-topology',
96
98
  topology_relation_unconfirmed: 'static-topology',
97
- boundary_module_file_mismatch: 'static-topology',
98
- boundary_process_nest_disagree: 'runtime-topology',
99
+ boundary_module_file_mismatch: 'dynamic-topology',
100
+ boundary_process_nest_disagree: 'dynamic-topology',
99
101
  walkthrough: 'walkthrough',
100
102
  };
101
103
 
@@ -65,6 +65,7 @@ import { GraphLayoutCover } from './GraphLayoutCover';
65
65
  import { GRAPH_NAV_PROPS, GraphChrome } from './graphChrome';
66
66
  import { ComponentDeclaration } from './ComponentDeclaration';
67
67
  import type { ComponentVerificationState } from './ComponentDeclaration';
68
+ import type { DeclarationSymbolRef, SymbolInspection } from './symbolRefs';
68
69
  import { FileDrawer, FILE_DRAWER_HEIGHT_MS } from './FileDrawer';
69
70
  import { buildRepoGroups, repoAvatarUrl, type RepoGroup } from './paths';
70
71
  import { WalkthroughsPanel, WALKTHROUGH_PLAY_PAUSE_MS } from './WalkthroughsPanel';
@@ -287,6 +288,18 @@ export interface SubsystemComponentGraphProps {
287
288
  onFileSelect?: (file: string) => void;
288
289
  /** Live verification status for the selected component. */
289
290
  componentVerification?: ComponentVerificationState | null;
291
+ /**
292
+ * Referenced-symbol click in the declaration panel → resolve it against the
293
+ * host's graphify cache. The graph supplies the selected component's
294
+ * purl/file so the host can pick the right repo graph; the host returns what
295
+ * graphify knows (declaration / source / candidates).
296
+ */
297
+ onInspectSymbol?: (req: {
298
+ purl: string;
299
+ file: string;
300
+ symbol: string;
301
+ ref: DeclarationSymbolRef;
302
+ }) => Promise<SymbolInspection | null> | SymbolInspection | null;
290
303
  /**
291
304
  * When set, the graph's walkthrough working set — expanded flows and the
292
305
  * selected flow/step — is persisted to `localStorage` under this key and
@@ -383,7 +396,7 @@ interface InnerProps extends SubsystemComponentGraphProps {
383
396
  measured: { w: number; h: number } | null;
384
397
  }
385
398
 
386
- function Inner({ components, relations, walkthroughs, initialWalkthroughId, onReorderWalkthroughs, onSelect, onEdgeSelect, measured: _measured, maxNodeWidth, showEdgeLabels, edgeView, title, hideSidebar, walkthroughStepMode = 'focus', autoPlayWalkthroughs = false, walkthroughAutoPlayIntervalMs = WALKTHROUGH_PLAY_PAUSE_MS, zoomOnWalkthroughFocus = true, graphTitle, showWalkthroughTitle = false, description, canvasOverlay, sidebarExtra, sidebarAfterDescription, diagnostic, issues, showIssues, focusIssueCategory, onSelectIssue, onApplyIssueFix, onHoverIssue, renderFileView, renderFileViewer, renderWalkthroughViewer, onFileSelect, componentVerification, persistKey }: InnerProps) {
399
+ function Inner({ components, relations, walkthroughs, initialWalkthroughId, onReorderWalkthroughs, onSelect, onEdgeSelect, measured: _measured, maxNodeWidth, showEdgeLabels, edgeView, title, hideSidebar, walkthroughStepMode = 'focus', autoPlayWalkthroughs = false, walkthroughAutoPlayIntervalMs = WALKTHROUGH_PLAY_PAUSE_MS, zoomOnWalkthroughFocus = true, graphTitle, showWalkthroughTitle = false, description, canvasOverlay, sidebarExtra, sidebarAfterDescription, diagnostic, issues, showIssues, focusIssueCategory, onSelectIssue, onApplyIssueFix, onHoverIssue, renderFileView, renderFileViewer, renderWalkthroughViewer, onFileSelect, componentVerification, onInspectSymbol, persistKey }: InnerProps) {
387
400
  const { theme } = useTheme();
388
401
  const { fitView } = useReactFlow();
389
402
  const viewport = useViewport();
@@ -2136,6 +2149,17 @@ function Inner({ components, relations, walkthroughs, initialWalkthroughId, onRe
2136
2149
  component={selected}
2137
2150
  onOpenFile={onOpenDeclarationFile}
2138
2151
  onRelatedSelect={resolveRelatedComponent}
2152
+ onInspectSymbol={
2153
+ onInspectSymbol
2154
+ ? (symbol, ref) =>
2155
+ onInspectSymbol({
2156
+ purl: selected.purl,
2157
+ file: selected.file,
2158
+ symbol,
2159
+ ref,
2160
+ })
2161
+ : undefined
2162
+ }
2139
2163
  verification={componentVerification}
2140
2164
  fileOpen={
2141
2165
  drawerTarget?.kind === 'file' &&
@@ -2365,6 +2389,7 @@ export function SubsystemComponentGraph(props: SubsystemComponentGraphProps) {
2365
2389
  renderFileView={props.renderFileView}
2366
2390
  onFileSelect={props.onFileSelect}
2367
2391
  componentVerification={props.componentVerification}
2392
+ onInspectSymbol={props.onInspectSymbol}
2368
2393
  />
2369
2394
  ) : (
2370
2395
  <ReactFlowProvider>