@principal-ai/subsystems-react 0.55.3 → 0.56.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.
- package/dist/index.d.ts +12 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/dist/pierre/PierreFileView.js +3 -3
- package/dist/pierre/PierreFileView.js.map +1 -1
- package/dist/pierre/PierreTrailCodeView.d.ts +1 -6
- package/dist/pierre/PierreTrailCodeView.d.ts.map +1 -1
- package/dist/pierre/PierreTrailCodeView.js +21 -18
- package/dist/pierre/PierreTrailCodeView.js.map +1 -1
- package/dist/pierre/fileAvailability.d.ts +4 -7
- package/dist/pierre/fileAvailability.d.ts.map +1 -1
- package/dist/pierre/fileAvailability.js +5 -10
- package/dist/pierre/fileAvailability.js.map +1 -1
- package/dist/subsystem/C4Graph.d.ts.map +1 -1
- package/dist/subsystem/C4Graph.js +1 -1
- package/dist/subsystem/C4Graph.js.map +1 -1
- package/dist/subsystem/DecorationPanel.d.ts +37 -0
- package/dist/subsystem/DecorationPanel.d.ts.map +1 -0
- package/dist/subsystem/DecorationPanel.js +147 -0
- package/dist/subsystem/DecorationPanel.js.map +1 -0
- package/dist/subsystem/FileDrawer.d.ts +3 -1
- package/dist/subsystem/FileDrawer.d.ts.map +1 -1
- package/dist/subsystem/FileDrawer.js +2 -2
- package/dist/subsystem/FileDrawer.js.map +1 -1
- package/dist/subsystem/IssueList.d.ts.map +1 -1
- package/dist/subsystem/IssueList.js +26 -0
- package/dist/subsystem/IssueList.js.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.d.ts +9 -5
- package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.js +37 -47
- package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
- package/dist/subsystem/TrailStackBreadcrumb.d.ts +21 -0
- package/dist/subsystem/TrailStackBreadcrumb.d.ts.map +1 -0
- package/dist/subsystem/TrailStackBreadcrumb.js +77 -0
- package/dist/subsystem/TrailStackBreadcrumb.js.map +1 -0
- package/dist/subsystem/TrailStepsNav.d.ts +4 -3
- package/dist/subsystem/TrailStepsNav.d.ts.map +1 -1
- package/dist/subsystem/TrailStepsNav.js +13 -16
- package/dist/subsystem/TrailStepsNav.js.map +1 -1
- package/dist/subsystem/TrailsPanel.d.ts +9 -7
- package/dist/subsystem/TrailsPanel.d.ts.map +1 -1
- package/dist/subsystem/TrailsPanel.js +126 -76
- package/dist/subsystem/TrailsPanel.js.map +1 -1
- package/dist/subsystem/decoration.d.ts +112 -0
- package/dist/subsystem/decoration.d.ts.map +1 -0
- package/dist/subsystem/decoration.js +54 -0
- package/dist/subsystem/decoration.js.map +1 -0
- package/dist/subsystem/model.d.ts +5 -18
- package/dist/subsystem/model.d.ts.map +1 -1
- package/dist/subsystem/model.js +3 -11
- package/dist/subsystem/model.js.map +1 -1
- package/dist/subsystem/nodes.d.ts.map +1 -1
- package/dist/subsystem/nodes.js +17 -24
- package/dist/subsystem/nodes.js.map +1 -1
- package/dist/subsystem/stepEmphasis.d.ts +78 -0
- package/dist/subsystem/stepEmphasis.d.ts.map +1 -0
- package/dist/subsystem/stepEmphasis.js +89 -0
- package/dist/subsystem/stepEmphasis.js.map +1 -0
- package/dist/subsystem/trailBrief.d.ts.map +1 -1
- package/dist/subsystem/trailBrief.js +20 -0
- package/dist/subsystem/trailBrief.js.map +1 -1
- package/dist/subsystem/trailStack.d.ts +53 -0
- package/dist/subsystem/trailStack.d.ts.map +1 -0
- package/dist/subsystem/trailStack.js +154 -0
- package/dist/subsystem/trailStack.js.map +1 -0
- package/dist/subsystem/trailStepDisplay.d.ts +38 -0
- package/dist/subsystem/trailStepDisplay.d.ts.map +1 -0
- package/dist/subsystem/trailStepDisplay.js +41 -0
- package/dist/subsystem/trailStepDisplay.js.map +1 -0
- package/package.json +3 -3
- package/src/index.ts +47 -1
- package/src/pierre/PierreFileView.tsx +3 -3
- package/src/pierre/PierreTrailCodeView.tsx +25 -28
- package/src/pierre/fileAvailability.test.ts +0 -6
- package/src/pierre/fileAvailability.ts +5 -13
- package/src/stories/Canvases/Subsystem/Deprecated.stories.tsx +3 -17
- package/src/stories/Canvases/Subsystem/NodeAnatomy.stories.tsx +3 -31
- package/src/stories/Canvases/Subsystem/StepEmphasis.stories.tsx +528 -0
- package/src/stories/Canvases/Subsystem/Trails.stories.tsx +0 -187
- package/src/stories/Components/DecorationPanel.stories.tsx +160 -0
- package/src/subsystem/C4Graph.tsx +0 -1
- package/src/subsystem/DecorationPanel.test.tsx +128 -0
- package/src/subsystem/DecorationPanel.tsx +334 -0
- package/src/subsystem/FileDrawer.tsx +6 -0
- package/src/subsystem/IssueList.tsx +26 -0
- package/src/subsystem/SubsystemComponentGraph.tsx +68 -65
- package/src/subsystem/TrailStackBreadcrumb.test.tsx +109 -0
- package/src/subsystem/TrailStackBreadcrumb.tsx +172 -0
- package/src/subsystem/TrailStepsNav.test.tsx +35 -0
- package/src/subsystem/TrailStepsNav.tsx +18 -19
- package/src/subsystem/TrailsPanel.test.tsx +149 -0
- package/src/subsystem/TrailsPanel.tsx +165 -39
- package/src/subsystem/decoration.test.ts +72 -0
- package/src/subsystem/decoration.ts +151 -0
- package/src/subsystem/model.test.ts +7 -22
- package/src/subsystem/model.ts +6 -20
- package/src/subsystem/nodes.tsx +19 -28
- package/src/subsystem/stepEmphasis.test.ts +110 -0
- package/src/subsystem/stepEmphasis.ts +151 -0
- package/src/subsystem/trailBrief.test.ts +29 -0
- package/src/subsystem/trailBrief.ts +20 -0
- package/src/subsystem/trailStack.test.ts +133 -0
- package/src/subsystem/trailStack.ts +229 -0
- package/src/subsystem/trailStepDisplay.ts +68 -0
- package/src/stories/Canvases/Subsystem/Proposed.stories.tsx +0 -256
- package/src/stories/Canvases/Subsystem/ProposedTrails.stories.tsx +0 -184
|
@@ -91,6 +91,8 @@ import { GraphWithSidebar } from './GraphWithSidebar';
|
|
|
91
91
|
import type { GraphSidebarTab } from './GraphSidebar';
|
|
92
92
|
import { buildRepoGroups, repoAvatarUrl, type RepoGroup } from './paths';
|
|
93
93
|
import { TrailsPanel, TRAIL_PLAY_PAUSE_MS } from './TrailsPanel';
|
|
94
|
+
import { TrailStackBreadcrumb } from './TrailStackBreadcrumb';
|
|
95
|
+
import type { StepEmphasisMap } from './stepEmphasis';
|
|
94
96
|
import {
|
|
95
97
|
EDGE_LABEL_WIDTH,
|
|
96
98
|
EDGE_LABEL_HEIGHT,
|
|
@@ -106,11 +108,6 @@ export interface TrailViewerContext {
|
|
|
106
108
|
stepIndex: number | null;
|
|
107
109
|
/** Open a step's full source file over the trail drawer (keeps snippets mounted). */
|
|
108
110
|
onOpenFile: (path: string, opts?: SubsystemOpenFileOptions) => void;
|
|
109
|
-
/**
|
|
110
|
-
* Aliases of components marked `proposed`. A step whose file can't be read
|
|
111
|
-
* but whose endpoint is proposed can be labelled as planned, not missing.
|
|
112
|
-
*/
|
|
113
|
-
proposedAliases: ReadonlySet<string>;
|
|
114
111
|
/**
|
|
115
112
|
* Resolve a token in a step's snippet to a construct the step touches (its
|
|
116
113
|
* `from`/`to` component), returning that component's alias. `null` when the
|
|
@@ -287,6 +284,14 @@ export interface SubsystemComponentGraphProps {
|
|
|
287
284
|
* and never reorders its own prop).
|
|
288
285
|
*/
|
|
289
286
|
onReorderTrails?: (next: SubsystemTrail[]) => void;
|
|
287
|
+
/**
|
|
288
|
+
* PROTOTYPE: host-owned step emphasis augmentations, keyed by trail + claim
|
|
289
|
+
* fingerprint. Kept out of the model document; the host reads a separate
|
|
290
|
+
* augmentation store. A mark is a star plus optional markdown guidance.
|
|
291
|
+
*/
|
|
292
|
+
stepEmphasis?: StepEmphasisMap;
|
|
293
|
+
/** Toggle a step's guidance off. Omit to hide the remove action. */
|
|
294
|
+
onRemoveStepEmphasis?: (trail: SubsystemTrail, stepIndex: number) => void;
|
|
290
295
|
onSelect?: (componentAlias: string) => void;
|
|
291
296
|
/** Called when an edge is clicked (relationship / mechanism + refs seam). */
|
|
292
297
|
onEdgeSelect?: (edge: SubsystemComponentEdge) => void;
|
|
@@ -579,7 +584,6 @@ const TrailDrawerContent = memo(function TrailDrawerContent({
|
|
|
579
584
|
trail,
|
|
580
585
|
stepIndex,
|
|
581
586
|
onOpenFile,
|
|
582
|
-
proposedAliases,
|
|
583
587
|
resolveSymbol,
|
|
584
588
|
onSymbolClick,
|
|
585
589
|
onVisibleStepChange,
|
|
@@ -588,7 +592,6 @@ const TrailDrawerContent = memo(function TrailDrawerContent({
|
|
|
588
592
|
trail: SubsystemTrail;
|
|
589
593
|
stepIndex: number | null;
|
|
590
594
|
onOpenFile: (path: string, opts?: SubsystemOpenFileOptions) => void;
|
|
591
|
-
proposedAliases: ReadonlySet<string>;
|
|
592
595
|
resolveSymbol?: (query: TrailSymbolQuery) => string | null;
|
|
593
596
|
onSymbolClick?: (symbol: string, query: TrailSymbolQuery) => void;
|
|
594
597
|
onVisibleStepChange?: (index: number) => void;
|
|
@@ -599,7 +602,6 @@ const TrailDrawerContent = memo(function TrailDrawerContent({
|
|
|
599
602
|
trail,
|
|
600
603
|
stepIndex,
|
|
601
604
|
onOpenFile,
|
|
602
|
-
proposedAliases,
|
|
603
605
|
resolveSymbol,
|
|
604
606
|
onSymbolClick,
|
|
605
607
|
onVisibleStepChange,
|
|
@@ -612,7 +614,7 @@ interface InnerProps extends SubsystemComponentGraphProps {
|
|
|
612
614
|
measured: { w: number; h: number } | null;
|
|
613
615
|
}
|
|
614
616
|
|
|
615
|
-
function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirection: layoutDirectionProp, defaultLayoutDirection = 'horizontal', onLayoutDirectionChange, hideLayoutToggle = false, refocusOnLayoutFlip = true, 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) {
|
|
617
|
+
function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirection: layoutDirectionProp, defaultLayoutDirection = 'horizontal', onLayoutDirectionChange, hideLayoutToggle = false, refocusOnLayoutFlip = true, initialTrailId, onTrailSelect, onTrailStepHover, onReorderTrails, stepEmphasis, onRemoveStepEmphasis, 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) {
|
|
616
618
|
const { theme } = useTheme();
|
|
617
619
|
const { fitView, fitBounds, screenToFlowPosition } = useReactFlow();
|
|
618
620
|
const viewport = useViewport();
|
|
@@ -1431,24 +1433,42 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
|
|
|
1431
1433
|
|
|
1432
1434
|
// Per-edge step verification badge: maps edge IDs to step verification status.
|
|
1433
1435
|
// Keyed by the React Flow edge id (from → mechanism → to). An edge can have
|
|
1434
|
-
// multiple steps with issues; we show the worst status
|
|
1436
|
+
// multiple steps with issues; we show the worst status.
|
|
1435
1437
|
const stepIssueBadgeByEdgeId = useMemo(() => {
|
|
1436
|
-
|
|
1438
|
+
type StepIssueKind =
|
|
1439
|
+
| 'step_span_unconfirmed'
|
|
1440
|
+
| 'step_span_stale'
|
|
1441
|
+
| 'step_site_outside_from';
|
|
1442
|
+
// Unconfirmed is the hard failure; a miscategorized `calls` edge is the
|
|
1443
|
+
// next most actionable (it changes what the edge means); drift is last.
|
|
1444
|
+
const rank: Record<StepIssueKind, number> = {
|
|
1445
|
+
step_span_unconfirmed: 3,
|
|
1446
|
+
step_site_outside_from: 2,
|
|
1447
|
+
step_span_stale: 1,
|
|
1448
|
+
};
|
|
1449
|
+
const map = new Map<string, { kind: StepIssueKind; severity: 'info' | 'error'; count: number }>();
|
|
1437
1450
|
if (!issues?.length || !trails?.length) {
|
|
1438
1451
|
return map;
|
|
1439
1452
|
}
|
|
1440
1453
|
for (const issue of issues) {
|
|
1441
|
-
if (
|
|
1454
|
+
if (
|
|
1455
|
+
issue.kind !== 'step_span_unconfirmed' &&
|
|
1456
|
+
issue.kind !== 'step_span_stale' &&
|
|
1457
|
+
issue.kind !== 'step_site_outside_from'
|
|
1458
|
+
) {
|
|
1459
|
+
continue;
|
|
1460
|
+
}
|
|
1442
1461
|
const stepInfo = issueStep(issue);
|
|
1443
1462
|
if (!stepInfo) continue;
|
|
1444
1463
|
const step = stepInfo.trail.steps[stepInfo.stepIndex];
|
|
1445
1464
|
if (!step) continue;
|
|
1446
1465
|
const edgeId = trailStepGraphEdgeId(step);
|
|
1466
|
+
const kind = issue.kind as StepIssueKind;
|
|
1447
1467
|
const prev = map.get(edgeId);
|
|
1448
|
-
|
|
1449
|
-
const
|
|
1468
|
+
const isWorse = !prev || rank[kind] > rank[prev.kind];
|
|
1469
|
+
const nextKind = isWorse ? kind : prev!.kind;
|
|
1450
1470
|
map.set(edgeId, {
|
|
1451
|
-
kind:
|
|
1471
|
+
kind: nextKind,
|
|
1452
1472
|
severity: prev?.severity === 'error' || issue.severity === 'error' ? 'error' : 'info',
|
|
1453
1473
|
count: (prev?.count ?? 0) + 1,
|
|
1454
1474
|
});
|
|
@@ -1456,22 +1476,6 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
|
|
|
1456
1476
|
return map;
|
|
1457
1477
|
}, [issues, trails, issueStep]);
|
|
1458
1478
|
|
|
1459
|
-
// Edges backed by a step that ITSELF declares `proposed` — a planned seam
|
|
1460
|
-
// between otherwise-real components. Distinct from a proposed ENDPOINT
|
|
1461
|
-
// (the node is the placeholder there, and draws its own dashed border):
|
|
1462
|
-
// here the seam is the claim that is not real yet, so the edge label draws
|
|
1463
|
-
// dashed. Proposed steps are never verified, so this never competes with
|
|
1464
|
-
// the step-issue badge.
|
|
1465
|
-
const proposedEdgeIds = useMemo(() => {
|
|
1466
|
-
const set = new Set<string>();
|
|
1467
|
-
for (const trail of trails ?? []) {
|
|
1468
|
-
for (const step of trail.steps) {
|
|
1469
|
-
if (step.proposed) set.add(trailStepGraphEdgeId(step));
|
|
1470
|
-
}
|
|
1471
|
-
}
|
|
1472
|
-
return set;
|
|
1473
|
-
}, [trails]);
|
|
1474
|
-
|
|
1475
1479
|
// Expanded verification layers in the diagnostics list. When any layer with
|
|
1476
1480
|
// findings is expanded, the canvas dims everything that layer does not
|
|
1477
1481
|
// implicate (see `issueFocus`). The list publishes this through
|
|
@@ -1798,17 +1802,6 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
|
|
|
1798
1802
|
() => convertedEdges.map((e) => e.id).sort().join(','),
|
|
1799
1803
|
[convertedEdges],
|
|
1800
1804
|
);
|
|
1801
|
-
// Stamp `proposedStep` onto edge data so the SVG edge itself (not just its
|
|
1802
|
-
// label) draws dashed when a backing trail step declares itself proposed.
|
|
1803
|
-
const proposedStampedEdges = useMemo(
|
|
1804
|
-
() =>
|
|
1805
|
-
baseEdges.map((e) =>
|
|
1806
|
-
proposedEdgeIds.has(e.id)
|
|
1807
|
-
? { ...e, data: { ...(e.data as object), proposedStep: true } }
|
|
1808
|
-
: e,
|
|
1809
|
-
),
|
|
1810
|
-
[baseEdges, proposedEdgeIds],
|
|
1811
|
-
);
|
|
1812
1805
|
|
|
1813
1806
|
const dispEdges = useMemo(() => {
|
|
1814
1807
|
const paint = (e: Edge, dimmed: boolean): Edge => {
|
|
@@ -1837,7 +1830,7 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
|
|
|
1837
1830
|
return isTrailMechanism(mechanism);
|
|
1838
1831
|
};
|
|
1839
1832
|
if (openedEdgeIds || focusEdgeIds || previewEdgeIds || issueFocusEdgeIds) {
|
|
1840
|
-
return
|
|
1833
|
+
return baseEdges.map((e) => {
|
|
1841
1834
|
const vis = flowElementVisibility({
|
|
1842
1835
|
inOpened: openedEdgeIds?.has(e.id) === true,
|
|
1843
1836
|
inSelected: focusEdgeIds?.has(e.id) === true,
|
|
@@ -1851,13 +1844,13 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
|
|
|
1851
1844
|
});
|
|
1852
1845
|
}
|
|
1853
1846
|
if (selectedEdgeId) {
|
|
1854
|
-
return
|
|
1847
|
+
return baseEdges.map((e) => ({
|
|
1855
1848
|
...paint(e, e.id !== selectedEdgeId),
|
|
1856
1849
|
hidden: !edgeInView(e),
|
|
1857
1850
|
}));
|
|
1858
1851
|
}
|
|
1859
|
-
return
|
|
1860
|
-
}, [
|
|
1852
|
+
return baseEdges.map((e) => ({ ...e, hidden: !edgeInView(e) }));
|
|
1853
|
+
}, [baseEdges, selectedEdgeId, openedEdgeIds, focusEdgeIds, previewEdgeIds, issueFocusEdgeIds, resolvedEdgeView]);
|
|
1861
1854
|
|
|
1862
1855
|
// Gated on layoutReady so the pre-reveal Pass 1 swap never flashes a count.
|
|
1863
1856
|
const unroutedIds = useMemo(
|
|
@@ -1947,10 +1940,10 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
|
|
|
1947
1940
|
// own owner-avatar header. Clicking a header collapses that repo's tree.
|
|
1948
1941
|
const repoGroups = useMemo(() => buildRepoGroups(components), [components]);
|
|
1949
1942
|
const hasTrails = useMemo(() => (trails?.length ?? 0) > 0, [trails]);
|
|
1950
|
-
//
|
|
1951
|
-
// (and
|
|
1952
|
-
const
|
|
1953
|
-
() => new
|
|
1943
|
+
// Alias → component, so the Trails panel can show human hop names instead
|
|
1944
|
+
// of model-local aliases (and the site/`via` distinction can read).
|
|
1945
|
+
const componentByAlias = useMemo(
|
|
1946
|
+
() => new Map(components.map((c) => [c.alias, c])),
|
|
1954
1947
|
[components],
|
|
1955
1948
|
);
|
|
1956
1949
|
// Diagnostics view in the sidebar. Uncontrolled unless the host pins
|
|
@@ -2800,7 +2793,6 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
|
|
|
2800
2793
|
pathLength?: number;
|
|
2801
2794
|
} | undefined;
|
|
2802
2795
|
const stepIssue = stepIssueBadgeByEdgeId.get(e.id);
|
|
2803
|
-
const proposed = proposedEdgeIds.has(e.id);
|
|
2804
2796
|
return {
|
|
2805
2797
|
id: e.id,
|
|
2806
2798
|
mechanism: d?.mechanism ?? 'uses',
|
|
@@ -2811,11 +2803,10 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
|
|
|
2811
2803
|
pathLength: d?.pathLength ?? 0,
|
|
2812
2804
|
stepNos: selectedFlowStepNos?.get(e.id),
|
|
2813
2805
|
stepIssue,
|
|
2814
|
-
proposed,
|
|
2815
2806
|
};
|
|
2816
2807
|
});
|
|
2817
2808
|
return labels;
|
|
2818
|
-
}, [dispEdges, selectedFlowStepNos, stepIssueBadgeByEdgeId
|
|
2809
|
+
}, [dispEdges, selectedFlowStepNos, stepIssueBadgeByEdgeId]);
|
|
2819
2810
|
|
|
2820
2811
|
// Unique source files across components → sidebar file trees.
|
|
2821
2812
|
const treeFilePaths = treeFiles;
|
|
@@ -3006,7 +2997,9 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
|
|
|
3006
2997
|
onTrailStepHover?.(null);
|
|
3007
2998
|
}}
|
|
3008
2999
|
onReorder={onReorderTrails}
|
|
3009
|
-
|
|
3000
|
+
componentByAlias={componentByAlias}
|
|
3001
|
+
stepEmphasis={stepEmphasis}
|
|
3002
|
+
onRemoveStepEmphasis={onRemoveStepEmphasis}
|
|
3010
3003
|
layoutFocusedTrailId={layoutTrailId}
|
|
3011
3004
|
onFocusLayout={toggleLayoutFocus}
|
|
3012
3005
|
/>
|
|
@@ -3153,9 +3146,7 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
|
|
|
3153
3146
|
crisp rounded box; soft / ambiguous ones (uses, feeds,
|
|
3154
3147
|
watches …) get a real lobed cloud silhouette — ambiguous, not
|
|
3155
3148
|
a dashed proposal (or a pill, which border-radius can only
|
|
3156
|
-
ever make).
|
|
3157
|
-
silhouette — the seam is planned, distinct from a proposed
|
|
3158
|
-
ENDPOINT, which dashes the node instead. */}
|
|
3149
|
+
ever make). */}
|
|
3159
3150
|
{verifiable ? (
|
|
3160
3151
|
<span
|
|
3161
3152
|
aria-hidden
|
|
@@ -3163,9 +3154,7 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
|
|
|
3163
3154
|
position: 'absolute',
|
|
3164
3155
|
inset: 0,
|
|
3165
3156
|
borderRadius: 4,
|
|
3166
|
-
border:
|
|
3167
|
-
? `1.5px dashed ${color}`
|
|
3168
|
-
: `0.5px solid ${color}`,
|
|
3157
|
+
border: `0.5px solid ${color}`,
|
|
3169
3158
|
background:
|
|
3170
3159
|
theme.colors.backgroundSecondary ?? theme.colors.background,
|
|
3171
3160
|
pointerEvents: 'none',
|
|
@@ -3190,7 +3179,6 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
|
|
|
3190
3179
|
stroke={hexWithAlpha(color, 0.75)}
|
|
3191
3180
|
strokeWidth={1.2}
|
|
3192
3181
|
strokeLinejoin="round"
|
|
3193
|
-
strokeDasharray={lbl.proposed ? '4 3' : undefined}
|
|
3194
3182
|
/>
|
|
3195
3183
|
</svg>
|
|
3196
3184
|
)}
|
|
@@ -3221,9 +3209,13 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
|
|
|
3221
3209
|
return (
|
|
3222
3210
|
<span
|
|
3223
3211
|
aria-hidden
|
|
3224
|
-
title={
|
|
3225
|
-
|
|
3226
|
-
|
|
3212
|
+
title={
|
|
3213
|
+
lbl.stepIssue.kind === 'step_span_stale'
|
|
3214
|
+
? 'Call site changed since verification'
|
|
3215
|
+
: lbl.stepIssue.kind === 'step_site_outside_from'
|
|
3216
|
+
? 'Calls site is outside its source component'
|
|
3217
|
+
: 'Call site not yet verified'
|
|
3218
|
+
}
|
|
3227
3219
|
style={{
|
|
3228
3220
|
position: 'absolute',
|
|
3229
3221
|
right: -7,
|
|
@@ -3553,6 +3545,18 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
|
|
|
3553
3545
|
suppressEscape={fileOverlayOpen}
|
|
3554
3546
|
hidden={hideDrawer}
|
|
3555
3547
|
progress={drawerProgress}
|
|
3548
|
+
subheader={
|
|
3549
|
+
drawerTarget?.kind === 'trail' &&
|
|
3550
|
+
focusedTrail &&
|
|
3551
|
+
drawerTarget.stepIndex != null ? (
|
|
3552
|
+
<TrailStackBreadcrumb
|
|
3553
|
+
trail={focusedTrail}
|
|
3554
|
+
stepIndex={drawerTarget.stepIndex}
|
|
3555
|
+
componentByAlias={componentByAlias}
|
|
3556
|
+
onFocusStep={(index) => focusTrailStep(focusedTrail, index)}
|
|
3557
|
+
/>
|
|
3558
|
+
) : null
|
|
3559
|
+
}
|
|
3556
3560
|
>
|
|
3557
3561
|
{drawerTarget?.kind === 'trail' &&
|
|
3558
3562
|
focusedTrail &&
|
|
@@ -3562,7 +3566,6 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
|
|
|
3562
3566
|
trail={focusedTrail}
|
|
3563
3567
|
stepIndex={drawerTarget.stepIndex}
|
|
3564
3568
|
onOpenFile={onOpenFileFromTrail}
|
|
3565
|
-
proposedAliases={proposedAliases}
|
|
3566
3569
|
resolveSymbol={resolveTrailSymbol}
|
|
3567
3570
|
onSymbolClick={openConstructDeclaration}
|
|
3568
3571
|
onVisibleStepChange={setVisibleStepIndex}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from 'bun:test';
|
|
2
|
+
import { Window } from 'happy-dom';
|
|
3
|
+
import { cleanup, fireEvent, render } from '@testing-library/react/pure';
|
|
4
|
+
import { ThemeProvider, defaultEditorTheme } from '@principal-ade/industry-theme';
|
|
5
|
+
import type { SubsystemTrail } from './model';
|
|
6
|
+
import { TrailStackBreadcrumb } from './TrailStackBreadcrumb';
|
|
7
|
+
|
|
8
|
+
const dom = new Window();
|
|
9
|
+
const globals = globalThis as unknown as Record<string, unknown>;
|
|
10
|
+
globals.window = dom;
|
|
11
|
+
globals.document = dom.document;
|
|
12
|
+
globals.navigator = dom.navigator;
|
|
13
|
+
globals.HTMLElement = dom.HTMLElement;
|
|
14
|
+
globals.Node = dom.Node;
|
|
15
|
+
globals.Element = dom.Element;
|
|
16
|
+
globals.Event = dom.Event;
|
|
17
|
+
globals.MouseEvent = dom.MouseEvent;
|
|
18
|
+
globals.getComputedStyle = dom.getComputedStyle.bind(dom);
|
|
19
|
+
globals.requestAnimationFrame = dom.requestAnimationFrame.bind(dom);
|
|
20
|
+
globals.cancelAnimationFrame = dom.cancelAnimationFrame.bind(dom);
|
|
21
|
+
globals.localStorage = dom.localStorage;
|
|
22
|
+
globals.sessionStorage = dom.sessionStorage;
|
|
23
|
+
globals.matchMedia = dom.matchMedia.bind(dom);
|
|
24
|
+
globals.ResizeObserver = class {
|
|
25
|
+
observe() {}
|
|
26
|
+
unobserve() {}
|
|
27
|
+
disconnect() {}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
afterEach(cleanup);
|
|
31
|
+
|
|
32
|
+
const trail: SubsystemTrail = {
|
|
33
|
+
id: 't',
|
|
34
|
+
title: 'T',
|
|
35
|
+
steps: [
|
|
36
|
+
{
|
|
37
|
+
from: 'panel',
|
|
38
|
+
to: 'rpc',
|
|
39
|
+
mechanism: 'calls',
|
|
40
|
+
file: 'src/panel.ts',
|
|
41
|
+
purl: 'pkg:github/acme/app#src/panel.ts',
|
|
42
|
+
symbol: 'MaintenancePanel',
|
|
43
|
+
target: {
|
|
44
|
+
purl: 'pkg:github/acme/app#src/rpc.ts',
|
|
45
|
+
file: 'src/rpc.ts',
|
|
46
|
+
symbol: 'getRpc',
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
from: 'rpc',
|
|
51
|
+
to: 'detail',
|
|
52
|
+
mechanism: 'calls',
|
|
53
|
+
file: 'src/rpc.ts',
|
|
54
|
+
purl: 'pkg:github/acme/app#src/rpc.ts',
|
|
55
|
+
symbol: 'getRpc',
|
|
56
|
+
target: {
|
|
57
|
+
purl: 'pkg:github/acme/app#src/detail.ts',
|
|
58
|
+
file: 'src/detail.ts',
|
|
59
|
+
symbol: 'modelDetail',
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
from: 'panel',
|
|
64
|
+
to: 'merge',
|
|
65
|
+
mechanism: 'uses',
|
|
66
|
+
file: 'src/list.tsx',
|
|
67
|
+
purl: 'pkg:github/acme/app#src/list.tsx',
|
|
68
|
+
symbol: 'ModelList',
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
function renderBreadcrumb(stepIndex: number, onFocusStep?: (i: number) => void) {
|
|
74
|
+
return render(
|
|
75
|
+
<ThemeProvider theme={defaultEditorTheme}>
|
|
76
|
+
<TrailStackBreadcrumb
|
|
77
|
+
trail={trail}
|
|
78
|
+
stepIndex={stepIndex}
|
|
79
|
+
onFocusStep={onFocusStep}
|
|
80
|
+
/>
|
|
81
|
+
</ThemeProvider>,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
describe('TrailStackBreadcrumb', () => {
|
|
86
|
+
test('renders the call chain and the callee', () => {
|
|
87
|
+
const { getByText } = renderBreadcrumb(1);
|
|
88
|
+
expect(getByText('MaintenancePanel')).toBeTruthy();
|
|
89
|
+
expect(getByText('getRpc')).toBeTruthy();
|
|
90
|
+
expect(getByText('modelDetail')).toBeTruthy();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('clicking a frame focuses that step', () => {
|
|
94
|
+
let focused: number | null = null;
|
|
95
|
+
const { getByText } = renderBreadcrumb(1, (i) => {
|
|
96
|
+
focused = i;
|
|
97
|
+
});
|
|
98
|
+
fireEvent.click(getByText('MaintenancePanel'));
|
|
99
|
+
expect(focused).toBe(0);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('a uses step is an annotation, not a fourth frame', () => {
|
|
103
|
+
const { getByText, queryByText } = renderBreadcrumb(2);
|
|
104
|
+
expect(getByText('Context')).toBeTruthy();
|
|
105
|
+
expect(getByText('uses merge')).toBeTruthy();
|
|
106
|
+
// The uses step's site symbol is not rendered as a frame.
|
|
107
|
+
expect(queryByText('ModelList')).toBeNull();
|
|
108
|
+
});
|
|
109
|
+
});
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TrailStackBreadcrumb — the call stack at one trail step.
|
|
3
|
+
*
|
|
4
|
+
* Rendered above the trail code view. `calls` steps form the chain; `uses` and
|
|
5
|
+
* data/event seams are annotations on the nearest frame, never frames of their
|
|
6
|
+
* own. Clicking a frame focuses the step that created it.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { CSSProperties } from 'react';
|
|
10
|
+
import { ArrowRight, CornerDownRight } from 'lucide-react';
|
|
11
|
+
import { useTheme } from '@principal-ade/industry-theme';
|
|
12
|
+
import type { SubsystemTrail } from './model';
|
|
13
|
+
import { buildTrailStack } from './trailStack';
|
|
14
|
+
import type { TrailComponentLabels } from './trailStepDisplay';
|
|
15
|
+
|
|
16
|
+
export interface TrailStackBreadcrumbProps {
|
|
17
|
+
trail: SubsystemTrail;
|
|
18
|
+
/** Selected step; the stack is the chain into its caller frame. */
|
|
19
|
+
stepIndex: number;
|
|
20
|
+
/** Alias → component, for human frame/callee labels when no symbol resolves. */
|
|
21
|
+
componentByAlias?: TrailComponentLabels;
|
|
22
|
+
/** Focus a frame's step (usually the trail panel's onFocusStep). */
|
|
23
|
+
onFocusStep?: (stepIndex: number) => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const chipBase: CSSProperties = {
|
|
27
|
+
display: 'inline-flex',
|
|
28
|
+
alignItems: 'center',
|
|
29
|
+
minWidth: 0,
|
|
30
|
+
maxWidth: 260,
|
|
31
|
+
padding: '2px 7px',
|
|
32
|
+
borderRadius: 4,
|
|
33
|
+
lineHeight: 1.4,
|
|
34
|
+
whiteSpace: 'nowrap',
|
|
35
|
+
overflow: 'hidden',
|
|
36
|
+
textOverflow: 'ellipsis',
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export function TrailStackBreadcrumb({
|
|
40
|
+
trail,
|
|
41
|
+
stepIndex,
|
|
42
|
+
componentByAlias,
|
|
43
|
+
onFocusStep,
|
|
44
|
+
}: TrailStackBreadcrumbProps) {
|
|
45
|
+
const { theme } = useTheme();
|
|
46
|
+
const muted = theme.colors.textMuted ?? theme.colors.textSecondary;
|
|
47
|
+
const stack = buildTrailStack(trail, stepIndex, componentByAlias);
|
|
48
|
+
if (stack.frames.length === 0) return null;
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<div
|
|
52
|
+
aria-label={`${
|
|
53
|
+
stack.isCall ? 'Call stack' : 'Trail context'
|
|
54
|
+
} at step ${stepIndex + 1}`}
|
|
55
|
+
style={{
|
|
56
|
+
display: 'flex',
|
|
57
|
+
alignItems: 'center',
|
|
58
|
+
gap: 6,
|
|
59
|
+
flexShrink: 0,
|
|
60
|
+
minHeight: 28,
|
|
61
|
+
padding: '4px 10px',
|
|
62
|
+
overflowX: 'auto',
|
|
63
|
+
borderBottom: `1px solid ${theme.colors.border}`,
|
|
64
|
+
background:
|
|
65
|
+
theme.colors.backgroundSecondary ?? theme.colors.background,
|
|
66
|
+
}}
|
|
67
|
+
>
|
|
68
|
+
<span
|
|
69
|
+
style={{
|
|
70
|
+
flexShrink: 0,
|
|
71
|
+
paddingRight: 2,
|
|
72
|
+
fontSize: 9,
|
|
73
|
+
fontFamily: theme.fonts.body,
|
|
74
|
+
fontWeight: 600,
|
|
75
|
+
letterSpacing: 0.5,
|
|
76
|
+
textTransform: 'uppercase',
|
|
77
|
+
color: muted,
|
|
78
|
+
}}
|
|
79
|
+
>
|
|
80
|
+
{stack.isCall ? 'Stack' : 'Context'}
|
|
81
|
+
</span>
|
|
82
|
+
|
|
83
|
+
{stack.frames.map((frame, index) => {
|
|
84
|
+
const chipStyle: CSSProperties = {
|
|
85
|
+
...chipBase,
|
|
86
|
+
fontFamily: theme.fonts.monospace,
|
|
87
|
+
fontSize: theme.fontSizes[0],
|
|
88
|
+
border: `1px solid ${
|
|
89
|
+
frame.current ? theme.colors.primary : theme.colors.border
|
|
90
|
+
}`,
|
|
91
|
+
background: frame.current ? theme.colors.background : 'transparent',
|
|
92
|
+
color: frame.current ? theme.colors.text : theme.colors.textSecondary,
|
|
93
|
+
fontWeight: frame.current ? 600 : 400,
|
|
94
|
+
cursor: onFocusStep ? 'pointer' : 'default',
|
|
95
|
+
flexShrink: 0,
|
|
96
|
+
};
|
|
97
|
+
const label = (
|
|
98
|
+
<span
|
|
99
|
+
style={{
|
|
100
|
+
overflow: 'hidden',
|
|
101
|
+
textOverflow: 'ellipsis',
|
|
102
|
+
}}
|
|
103
|
+
>
|
|
104
|
+
{frame.label}
|
|
105
|
+
</span>
|
|
106
|
+
);
|
|
107
|
+
return (
|
|
108
|
+
<span
|
|
109
|
+
key={`${frame.stepIndex}:${index}`}
|
|
110
|
+
style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}
|
|
111
|
+
>
|
|
112
|
+
{index > 0 && (
|
|
113
|
+
<span aria-hidden style={{ color: muted, fontSize: 11 }}>
|
|
114
|
+
›
|
|
115
|
+
</span>
|
|
116
|
+
)}
|
|
117
|
+
{onFocusStep ? (
|
|
118
|
+
<button
|
|
119
|
+
type="button"
|
|
120
|
+
onClick={() => onFocusStep(frame.stepIndex)}
|
|
121
|
+
style={chipStyle}
|
|
122
|
+
>
|
|
123
|
+
{label}
|
|
124
|
+
</button>
|
|
125
|
+
) : (
|
|
126
|
+
<span style={chipStyle}>{label}</span>
|
|
127
|
+
)}
|
|
128
|
+
</span>
|
|
129
|
+
);
|
|
130
|
+
})}
|
|
131
|
+
|
|
132
|
+
{stack.callee && (
|
|
133
|
+
<span
|
|
134
|
+
style={{ display: 'inline-flex', alignItems: 'center', gap: 4 }}
|
|
135
|
+
>
|
|
136
|
+
<ArrowRight size={11} style={{ color: muted, flexShrink: 0 }} />
|
|
137
|
+
<span
|
|
138
|
+
style={{
|
|
139
|
+
...chipBase,
|
|
140
|
+
fontFamily: theme.fonts.monospace,
|
|
141
|
+
fontSize: theme.fontSizes[0],
|
|
142
|
+
border: `1px dashed ${theme.colors.border}`,
|
|
143
|
+
color: muted,
|
|
144
|
+
flexShrink: 0,
|
|
145
|
+
}}
|
|
146
|
+
>
|
|
147
|
+
{stack.callee}
|
|
148
|
+
</span>
|
|
149
|
+
</span>
|
|
150
|
+
)}
|
|
151
|
+
|
|
152
|
+
{stack.annotations.map((annotation) => (
|
|
153
|
+
<span
|
|
154
|
+
key={`annotation:${annotation.stepIndex}`}
|
|
155
|
+
style={{
|
|
156
|
+
display: 'inline-flex',
|
|
157
|
+
alignItems: 'center',
|
|
158
|
+
gap: 3,
|
|
159
|
+
flexShrink: 0,
|
|
160
|
+
fontSize: 10,
|
|
161
|
+
fontFamily: theme.fonts.monospace,
|
|
162
|
+
color: muted,
|
|
163
|
+
whiteSpace: 'nowrap',
|
|
164
|
+
}}
|
|
165
|
+
>
|
|
166
|
+
<CornerDownRight size={10} style={{ flexShrink: 0 }} />
|
|
167
|
+
{annotation.label}
|
|
168
|
+
</span>
|
|
169
|
+
))}
|
|
170
|
+
</div>
|
|
171
|
+
);
|
|
172
|
+
}
|
|
@@ -155,6 +155,41 @@ describe('TrailStepsNav', () => {
|
|
|
155
155
|
expect(cleared).toBe(true);
|
|
156
156
|
});
|
|
157
157
|
|
|
158
|
+
test('uses rows show the logical hop and the via site', () => {
|
|
159
|
+
const usesTrail: SubsystemTrail = {
|
|
160
|
+
id: 'uses',
|
|
161
|
+
title: 'Uses flow',
|
|
162
|
+
steps: [
|
|
163
|
+
{
|
|
164
|
+
from: 'panel',
|
|
165
|
+
to: 'merge',
|
|
166
|
+
mechanism: 'uses',
|
|
167
|
+
file: 'src/list.tsx',
|
|
168
|
+
span: { startLine: 10, endLine: 12, contentHash: 'h' },
|
|
169
|
+
purl: 'pkg:github/x#src/list.tsx',
|
|
170
|
+
symbol: 'ModelList',
|
|
171
|
+
},
|
|
172
|
+
],
|
|
173
|
+
};
|
|
174
|
+
const { getByText } = renderNav({
|
|
175
|
+
trails: [usesTrail],
|
|
176
|
+
focusedTrailId: 'uses',
|
|
177
|
+
focusedStepIndex: null,
|
|
178
|
+
hoveredTrailStep: null,
|
|
179
|
+
componentByAlias: new Map([
|
|
180
|
+
['panel', { alias: 'panel', name: 'MaintenancePanel' }],
|
|
181
|
+
['merge', { alias: 'merge', name: 'mergeProvenance' }],
|
|
182
|
+
]),
|
|
183
|
+
onFocusFlow: () => {},
|
|
184
|
+
onClearFocus: () => {},
|
|
185
|
+
onFocusStep: () => {},
|
|
186
|
+
onHoverStep: () => {},
|
|
187
|
+
onHoverFlow: () => {},
|
|
188
|
+
onLeaveStep: () => {},
|
|
189
|
+
});
|
|
190
|
+
expect(getByText('MaintenancePanel uses mergeProvenance')).toBeTruthy();
|
|
191
|
+
});
|
|
192
|
+
|
|
158
193
|
test('clicking the step-pane header title also goes back', () => {
|
|
159
194
|
let cleared = false;
|
|
160
195
|
const { getByRole } = renderNav({
|