@principal-ai/subsystems-react 0.36.1 → 0.37.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/pierre/PierreWalkthroughCodeView.d.ts +19 -1
- package/dist/pierre/PierreWalkthroughCodeView.d.ts.map +1 -1
- package/dist/pierre/PierreWalkthroughCodeView.js +53 -2
- package/dist/pierre/PierreWalkthroughCodeView.js.map +1 -1
- package/dist/subsystem/SubsystemAggregateGraph.d.ts.map +1 -1
- package/dist/subsystem/SubsystemAggregateGraph.js +35 -1
- package/dist/subsystem/SubsystemAggregateGraph.js.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.d.ts +10 -0
- package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.js +86 -16
- package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
- package/dist/subsystem/model.d.ts +7 -0
- package/dist/subsystem/model.d.ts.map +1 -1
- package/dist/subsystem/model.js +18 -2
- package/dist/subsystem/model.js.map +1 -1
- package/dist/subsystem/nodes.d.ts +0 -3
- package/dist/subsystem/nodes.d.ts.map +1 -1
- package/dist/subsystem/nodes.js +39 -44
- package/dist/subsystem/nodes.js.map +1 -1
- package/dist/utils/elkLayout.d.ts.map +1 -1
- package/dist/utils/elkLayout.js +4 -1
- package/dist/utils/elkLayout.js.map +1 -1
- package/package.json +3 -2
- package/src/pierre/PierreWalkthroughCodeView.tsx +75 -1
- package/src/stories/Subsystem/ComponentGraph/Flows.stories.tsx +222 -0
- package/src/stories/Subsystem/ComponentGraph/WorkspacePackageFrames.stories.tsx +137 -0
- package/src/subsystem/SubsystemComponentGraph.tsx +99 -1
|
@@ -171,6 +171,8 @@ function FlowsDemo() {
|
|
|
171
171
|
stepIndex,
|
|
172
172
|
onOpenFile,
|
|
173
173
|
proposedAliases,
|
|
174
|
+
resolveSymbol,
|
|
175
|
+
onSymbolClick,
|
|
174
176
|
}: WalkthroughViewerContext) => (
|
|
175
177
|
<PierreWalkthroughCodeView
|
|
176
178
|
walkthrough={walkthrough}
|
|
@@ -179,6 +181,8 @@ function FlowsDemo() {
|
|
|
179
181
|
contextLines={4}
|
|
180
182
|
onOpenFile={onOpenFile}
|
|
181
183
|
proposedAliases={proposedAliases}
|
|
184
|
+
resolveSymbol={resolveSymbol}
|
|
185
|
+
onSymbolClick={onSymbolClick}
|
|
182
186
|
/>
|
|
183
187
|
),
|
|
184
188
|
[],
|
|
@@ -276,6 +280,8 @@ function ProposedMissingStepDemo() {
|
|
|
276
280
|
stepIndex,
|
|
277
281
|
onOpenFile,
|
|
278
282
|
proposedAliases,
|
|
283
|
+
resolveSymbol,
|
|
284
|
+
onSymbolClick,
|
|
279
285
|
}: WalkthroughViewerContext) => (
|
|
280
286
|
<PierreWalkthroughCodeView
|
|
281
287
|
walkthrough={walkthrough}
|
|
@@ -284,6 +290,8 @@ function ProposedMissingStepDemo() {
|
|
|
284
290
|
contextLines={4}
|
|
285
291
|
onOpenFile={onOpenFile}
|
|
286
292
|
proposedAliases={proposedAliases}
|
|
293
|
+
resolveSymbol={resolveSymbol}
|
|
294
|
+
onSymbolClick={onSymbolClick}
|
|
287
295
|
/>
|
|
288
296
|
),
|
|
289
297
|
[],
|
|
@@ -319,3 +327,217 @@ function ProposedMissingStepDemo() {
|
|
|
319
327
|
export const ProposedMissingStep: Story = {
|
|
320
328
|
render: () => <ProposedMissingStepDemo />,
|
|
321
329
|
};
|
|
330
|
+
|
|
331
|
+
// --- Clickable constructs -------------------------------------------------
|
|
332
|
+
//
|
|
333
|
+
// A walkthrough step's line is an edge between two constructs. When the host
|
|
334
|
+
// supplies `resolveSymbol`/`onSymbolClick` (the graph derives them from the
|
|
335
|
+
// step's `from`/`to` components), a token in the snippet that names either
|
|
336
|
+
// endpoint becomes clickable — clicking it opens that construct's file at its
|
|
337
|
+
// declaration line in the bottom drawer. The second endpoint here is a
|
|
338
|
+
// `method` construct (`DrawingStore.list`), so the bare `list` token is
|
|
339
|
+
// clickable and jumps to the method's declaration.
|
|
340
|
+
|
|
341
|
+
const clickableComponents: SubsystemComponent[] = [
|
|
342
|
+
{
|
|
343
|
+
alias: 'load-panel',
|
|
344
|
+
name: 'LoadPanel',
|
|
345
|
+
construct: 'function',
|
|
346
|
+
file: 'src/load/LoadPanel.tsx',
|
|
347
|
+
purl: 'pkg:github/principal-ai/desktop-app',
|
|
348
|
+
symbol: 'LoadPanel',
|
|
349
|
+
purpose: 'loads the drawing list and hands rows to the render surface',
|
|
350
|
+
process: 'draw-list',
|
|
351
|
+
declarationRef: {
|
|
352
|
+
file: 'src/load/LoadPanel.tsx',
|
|
353
|
+
startLine: 3,
|
|
354
|
+
lineHash: 'story',
|
|
355
|
+
capturedAt: '2026-01-01T00:00:00.000Z',
|
|
356
|
+
},
|
|
357
|
+
declaration: {
|
|
358
|
+
kind: 'function',
|
|
359
|
+
parameters: [],
|
|
360
|
+
returnType: 'JSX.Element',
|
|
361
|
+
callers: [],
|
|
362
|
+
callees: [],
|
|
363
|
+
},
|
|
364
|
+
declarationProvenance: 'authored',
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
alias: 'drawing-store-list',
|
|
368
|
+
name: 'list',
|
|
369
|
+
construct: 'method',
|
|
370
|
+
file: 'src/store/DrawingStore.ts',
|
|
371
|
+
purl: 'pkg:github/principal-ai/desktop-app',
|
|
372
|
+
symbol: 'DrawingStore.list',
|
|
373
|
+
purpose: 'lists stored drawings for the panel',
|
|
374
|
+
process: 'draw-list',
|
|
375
|
+
declarationRef: {
|
|
376
|
+
file: 'src/store/DrawingStore.ts',
|
|
377
|
+
startLine: 4,
|
|
378
|
+
lineHash: 'story',
|
|
379
|
+
capturedAt: '2026-01-01T00:00:00.000Z',
|
|
380
|
+
},
|
|
381
|
+
declaration: {
|
|
382
|
+
kind: 'method',
|
|
383
|
+
hostClass: 'DrawingStore',
|
|
384
|
+
parameters: [],
|
|
385
|
+
returnType: 'Drawing[]',
|
|
386
|
+
},
|
|
387
|
+
declarationProvenance: 'authored',
|
|
388
|
+
},
|
|
389
|
+
{
|
|
390
|
+
alias: 'render-surface',
|
|
391
|
+
name: 'RenderSurface',
|
|
392
|
+
construct: 'class',
|
|
393
|
+
file: 'src/render/RenderSurface.ts',
|
|
394
|
+
purl: 'pkg:github/principal-ai/desktop-app',
|
|
395
|
+
symbol: 'RenderSurface',
|
|
396
|
+
purpose: 'paints a drawing into a surface',
|
|
397
|
+
process: 'draw-host',
|
|
398
|
+
declarationRef: {
|
|
399
|
+
file: 'src/render/RenderSurface.ts',
|
|
400
|
+
startLine: 3,
|
|
401
|
+
lineHash: 'story',
|
|
402
|
+
capturedAt: '2026-01-01T00:00:00.000Z',
|
|
403
|
+
},
|
|
404
|
+
declaration: {
|
|
405
|
+
kind: 'class',
|
|
406
|
+
methods: [{ nodeId: 'RenderSurface.render', name: 'render' }],
|
|
407
|
+
properties: [],
|
|
408
|
+
extends: [],
|
|
409
|
+
implements: [],
|
|
410
|
+
instantiations: [],
|
|
411
|
+
references: [],
|
|
412
|
+
},
|
|
413
|
+
declarationProvenance: 'authored',
|
|
414
|
+
},
|
|
415
|
+
];
|
|
416
|
+
|
|
417
|
+
const clickableFiles: Record<string, string> = {
|
|
418
|
+
'src/load/LoadPanel.tsx': [
|
|
419
|
+
'// src/load/LoadPanel.tsx',
|
|
420
|
+
'',
|
|
421
|
+
'export function LoadPanel() {',
|
|
422
|
+
' const rows = DrawingStore.list();',
|
|
423
|
+
' return rows.map(RenderSurface.render);',
|
|
424
|
+
'}',
|
|
425
|
+
].join('\n'),
|
|
426
|
+
'src/store/DrawingStore.ts': [
|
|
427
|
+
'// src/store/DrawingStore.ts',
|
|
428
|
+
'',
|
|
429
|
+
'export class DrawingStore {',
|
|
430
|
+
' static list() {',
|
|
431
|
+
' return RenderSurface.read();',
|
|
432
|
+
' }',
|
|
433
|
+
'}',
|
|
434
|
+
].join('\n'),
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
function readClickableFile(path: string): Promise<string> {
|
|
438
|
+
const content = clickableFiles[path];
|
|
439
|
+
if (content == null) {
|
|
440
|
+
return Promise.reject(new Error(`file not found in graph repos: ${path}`));
|
|
441
|
+
}
|
|
442
|
+
return Promise.resolve(content);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const clickableWalkthroughs: SubsystemWalkthrough[] = [
|
|
446
|
+
{
|
|
447
|
+
id: 'tl-load',
|
|
448
|
+
title: 'Load drawings',
|
|
449
|
+
steps: [
|
|
450
|
+
{
|
|
451
|
+
from: 'load-panel',
|
|
452
|
+
to: 'drawing-store-list',
|
|
453
|
+
mechanism: 'calls',
|
|
454
|
+
file: 'src/load/LoadPanel.tsx',
|
|
455
|
+
line: 4,
|
|
456
|
+
purl: stepPurl('src/load/LoadPanel.tsx'),
|
|
457
|
+
symbol: 'LoadPanel.load',
|
|
458
|
+
annotation:
|
|
459
|
+
'LoadPanel calls DrawingStore.list — click the `list` (or `DrawingStore`) token to jump to the method.',
|
|
460
|
+
},
|
|
461
|
+
{
|
|
462
|
+
from: 'drawing-store-list',
|
|
463
|
+
to: 'render-surface',
|
|
464
|
+
mechanism: 'calls',
|
|
465
|
+
file: 'src/store/DrawingStore.ts',
|
|
466
|
+
line: 5,
|
|
467
|
+
purl: stepPurl('src/store/DrawingStore.ts'),
|
|
468
|
+
symbol: 'DrawingStore.list',
|
|
469
|
+
annotation: 'list reads through RenderSurface — click RenderSurface.',
|
|
470
|
+
},
|
|
471
|
+
],
|
|
472
|
+
},
|
|
473
|
+
];
|
|
474
|
+
|
|
475
|
+
function ClickableConstructsDemo() {
|
|
476
|
+
const [opened, setOpened] = React.useState<string | null>(null);
|
|
477
|
+
const renderWalkthroughViewer = useCallback(
|
|
478
|
+
({
|
|
479
|
+
walkthrough,
|
|
480
|
+
stepIndex,
|
|
481
|
+
onOpenFile,
|
|
482
|
+
proposedAliases,
|
|
483
|
+
resolveSymbol,
|
|
484
|
+
onSymbolClick,
|
|
485
|
+
}: WalkthroughViewerContext) => (
|
|
486
|
+
<PierreWalkthroughCodeView
|
|
487
|
+
walkthrough={walkthrough}
|
|
488
|
+
stepIndex={stepIndex}
|
|
489
|
+
readFile={readClickableFile}
|
|
490
|
+
contextLines={4}
|
|
491
|
+
onOpenFile={onOpenFile}
|
|
492
|
+
proposedAliases={proposedAliases}
|
|
493
|
+
resolveSymbol={resolveSymbol}
|
|
494
|
+
onSymbolClick={onSymbolClick}
|
|
495
|
+
/>
|
|
496
|
+
),
|
|
497
|
+
[],
|
|
498
|
+
);
|
|
499
|
+
|
|
500
|
+
return (
|
|
501
|
+
<div style={{ width: '100%', height: '100vh', display: 'flex', flexDirection: 'column' }}>
|
|
502
|
+
<div
|
|
503
|
+
style={{
|
|
504
|
+
padding: '8px 14px',
|
|
505
|
+
fontFamily: 'monospace',
|
|
506
|
+
fontSize: 12,
|
|
507
|
+
borderBottom: '1px solid #333',
|
|
508
|
+
background: '#141414',
|
|
509
|
+
color: '#ddd',
|
|
510
|
+
}}
|
|
511
|
+
>
|
|
512
|
+
Click the <strong>Load drawings</strong> step, then click the dotted-underlined{' '}
|
|
513
|
+
<code>list</code> <em>(a method)</em> or <code>RenderSurface</code> token in the
|
|
514
|
+
snippet. It opens that construct's file at its declaration line in the bottom drawer
|
|
515
|
+
— <code>list</code> jumps to <code>DrawingStore.ts:4</code>.
|
|
516
|
+
<span style={{ marginLeft: 8, color: '#8fd' }}>opened: {opened ?? '—'}</span>
|
|
517
|
+
</div>
|
|
518
|
+
<div style={{ flex: 1, minHeight: 0 }}>
|
|
519
|
+
<SubsystemComponentGraph
|
|
520
|
+
components={clickableComponents}
|
|
521
|
+
relations={[]}
|
|
522
|
+
walkthroughs={clickableWalkthroughs}
|
|
523
|
+
initialWalkthroughId="tl-load"
|
|
524
|
+
title="clickable constructs"
|
|
525
|
+
description="A walkthrough step's line is an edge to a construct. Tokens that name the step's `from`/`to` components are clickable and open that construct's declaration line in the file drawer — including a **method** endpoint (click `list` to jump to `DrawingStore.list`)."
|
|
526
|
+
renderWalkthroughViewer={renderWalkthroughViewer}
|
|
527
|
+
onFileSelect={setOpened}
|
|
528
|
+
renderFileViewer={(file, opts) => (
|
|
529
|
+
<div style={{ padding: 12, fontFamily: 'monospace', fontSize: 12, color: '#bbb' }}>
|
|
530
|
+
{`// ${file}`}
|
|
531
|
+
{opts?.startLine != null ? `\n // → focus line ${opts.startLine}` : ''}
|
|
532
|
+
{'\n …'}
|
|
533
|
+
</div>
|
|
534
|
+
)}
|
|
535
|
+
/>
|
|
536
|
+
</div>
|
|
537
|
+
</div>
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
export const ClickableConstructs: Story = {
|
|
542
|
+
render: () => <ClickableConstructsDemo />,
|
|
543
|
+
};
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import '@xyflow/react/dist/style.css';
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
3
|
+
import { ThemeProvider, defaultEditorTheme } from '@principal-ade/industry-theme';
|
|
4
|
+
import { SubsystemComponentGraph } from '../../../subsystem/SubsystemComponentGraph';
|
|
5
|
+
import type { SubsystemComponent } from '../../../subsystem/model';
|
|
6
|
+
import { graphSpecFromEdges } from './fixtures';
|
|
7
|
+
|
|
8
|
+
const meta = {
|
|
9
|
+
title: 'Subsystem/ComponentGraph/WorkspacePackages',
|
|
10
|
+
component: SubsystemComponentGraph,
|
|
11
|
+
parameters: { layout: 'fullscreen' },
|
|
12
|
+
tags: ['autodocs'],
|
|
13
|
+
decorators: [
|
|
14
|
+
(Story) => (
|
|
15
|
+
<ThemeProvider theme={defaultEditorTheme}>
|
|
16
|
+
<Story />
|
|
17
|
+
</ThemeProvider>
|
|
18
|
+
),
|
|
19
|
+
],
|
|
20
|
+
} satisfies Meta<typeof SubsystemComponentGraph>;
|
|
21
|
+
|
|
22
|
+
export default meta;
|
|
23
|
+
type Story = StoryObj<typeof meta>;
|
|
24
|
+
|
|
25
|
+
const STUDIO = 'pkg:npm/@principal-ai/subsystems-studio';
|
|
26
|
+
const REACT = 'pkg:npm/@principal-ai/subsystems-react';
|
|
27
|
+
const RENDERER = 'principal-studio/renderer';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Workspace-package preview — one repo, two packages.
|
|
31
|
+
*
|
|
32
|
+
* `purl` carries the workspace-package identity (npm purls here), so package
|
|
33
|
+
* frames draw per package instead of one frame per repo. The app package owns
|
|
34
|
+
* the runtime process: only `subsystems-studio` code sits inside
|
|
35
|
+
* `principal-studio/renderer`. The consumed library (`subsystems-react`) is a
|
|
36
|
+
* dependency, not a deployment unit, so it is housed in its own package frame
|
|
37
|
+
* outside that process — reached by cross-package edges instead of nesting.
|
|
38
|
+
*
|
|
39
|
+
* Tree: package → process → module → leaves, with the library package beside
|
|
40
|
+
* the process rather than inside it.
|
|
41
|
+
*/
|
|
42
|
+
const components: SubsystemComponent[] = [
|
|
43
|
+
// --- packages/subsystems-studio · inside process principal-studio/renderer
|
|
44
|
+
{
|
|
45
|
+
alias: 'model-view',
|
|
46
|
+
name: 'SubsystemModelView',
|
|
47
|
+
construct: 'function',
|
|
48
|
+
symbol: 'SubsystemModelView',
|
|
49
|
+
file: 'packages/subsystems-studio/src/mainview/views/SubsystemModelView.tsx',
|
|
50
|
+
module: 'packages/subsystems-studio/src/mainview/views/SubsystemModelView.tsx',
|
|
51
|
+
process: RENDERER,
|
|
52
|
+
purl: STUDIO,
|
|
53
|
+
purpose: 'renderer view that opens a subsystem model',
|
|
54
|
+
layer: 0,
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
alias: 'read-file',
|
|
58
|
+
name: 'readSubsystemFile',
|
|
59
|
+
construct: 'function',
|
|
60
|
+
symbol: 'readSubsystemFile',
|
|
61
|
+
file: 'packages/subsystems-studio/src/mainview/views/SubsystemModelView.tsx',
|
|
62
|
+
module: 'packages/subsystems-studio/src/mainview/views/SubsystemModelView.tsx',
|
|
63
|
+
process: RENDERER,
|
|
64
|
+
purl: STUDIO,
|
|
65
|
+
purpose: 'request a file slice for the drawer',
|
|
66
|
+
layer: 1,
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
alias: 'get-model',
|
|
70
|
+
name: 'getSubsystemModel',
|
|
71
|
+
construct: 'function',
|
|
72
|
+
symbol: 'getSubsystemModel',
|
|
73
|
+
file: 'packages/subsystems-studio/src/bun/subsystem-model-store.ts',
|
|
74
|
+
module: 'packages/subsystems-studio/src/bun/subsystem-model-store.ts',
|
|
75
|
+
process: RENDERER,
|
|
76
|
+
purl: STUDIO,
|
|
77
|
+
purpose: 'load a model document for the view',
|
|
78
|
+
layer: 2,
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
// --- packages/subsystems-react · consumed library, housed outside the process
|
|
82
|
+
{
|
|
83
|
+
alias: 'component-graph',
|
|
84
|
+
name: 'SubsystemComponentGraph',
|
|
85
|
+
construct: 'function',
|
|
86
|
+
symbol: 'SubsystemComponentGraph',
|
|
87
|
+
file: 'packages/subsystems-react/src/subsystem/SubsystemComponentGraph.tsx',
|
|
88
|
+
module: 'packages/subsystems-react/src/subsystem/SubsystemComponentGraph.tsx',
|
|
89
|
+
purl: REACT,
|
|
90
|
+
purpose: 'the graph canvas the view embeds',
|
|
91
|
+
layer: 1,
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
alias: 'file-drawer',
|
|
95
|
+
name: 'FileDrawer',
|
|
96
|
+
construct: 'function',
|
|
97
|
+
symbol: 'FileDrawer',
|
|
98
|
+
file: 'packages/subsystems-react/src/subsystem/FileDrawer.tsx',
|
|
99
|
+
module: 'packages/subsystems-react/src/subsystem/FileDrawer.tsx',
|
|
100
|
+
purl: REACT,
|
|
101
|
+
purpose: 'pierre-backed source drawer',
|
|
102
|
+
layer: 2,
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
alias: 'pierre-file-view',
|
|
106
|
+
name: 'PierreFileView',
|
|
107
|
+
construct: 'function',
|
|
108
|
+
symbol: 'PierreFileView',
|
|
109
|
+
file: 'packages/subsystems-react/src/pierre/PierreFileView.tsx',
|
|
110
|
+
module: 'packages/subsystems-react/src/pierre/PierreFileView.tsx',
|
|
111
|
+
purl: REACT,
|
|
112
|
+
purpose: 'renders one file slice',
|
|
113
|
+
layer: 3,
|
|
114
|
+
},
|
|
115
|
+
];
|
|
116
|
+
|
|
117
|
+
const spec = graphSpecFromEdges([
|
|
118
|
+
['read-file', 'get-model', 'calls'],
|
|
119
|
+
['model-view', 'component-graph', 'uses'],
|
|
120
|
+
['component-graph', 'file-drawer', 'calls'],
|
|
121
|
+
['file-drawer', 'pierre-file-view', 'uses'],
|
|
122
|
+
]);
|
|
123
|
+
|
|
124
|
+
export const PackageOwnedProcess: Story = {
|
|
125
|
+
name: 'Package-owned process · library outside',
|
|
126
|
+
args: {
|
|
127
|
+
components,
|
|
128
|
+
relations: spec.relations,
|
|
129
|
+
walkthroughs: spec.walkthroughs,
|
|
130
|
+
graphTitle: 'Opening a file — app package owns the process',
|
|
131
|
+
},
|
|
132
|
+
render: (args) => (
|
|
133
|
+
<div style={{ width: '100vw', height: '100vh' }}>
|
|
134
|
+
<SubsystemComponentGraph {...args} />
|
|
135
|
+
</div>
|
|
136
|
+
),
|
|
137
|
+
};
|
|
@@ -53,6 +53,7 @@ import {
|
|
|
53
53
|
} from './model';
|
|
54
54
|
import { ConstructsCatalog } from './ConstructsCatalog';
|
|
55
55
|
import type { SubsystemOpenFileOptions } from './declarationRef';
|
|
56
|
+
import type { WalkthroughSymbolQuery } from '../pierre/PierreWalkthroughCodeView';
|
|
56
57
|
import { SubsystemComponentNode, SubsystemGroupNode, SubsystemEdge, SUBSYSTEM_CALLBACKS, hexWithAlpha, EDGE_DIM_ALPHA, fileMatchForNode, flowElementVisibility } from './nodes';
|
|
57
58
|
import { SubsystemDiagnosticToggle, type SubsystemDiagnostic } from './DiagnosticToggle';
|
|
58
59
|
import {
|
|
@@ -88,6 +89,28 @@ export interface WalkthroughViewerContext {
|
|
|
88
89
|
* but whose endpoint is proposed can be labelled as planned, not missing.
|
|
89
90
|
*/
|
|
90
91
|
proposedAliases: ReadonlySet<string>;
|
|
92
|
+
/**
|
|
93
|
+
* Resolve a token in a step's snippet to a construct the step touches (its
|
|
94
|
+
* `from`/`to` component), returning that component's alias. `null` when the
|
|
95
|
+
* token names no touched construct. Forward to the code view so constructs
|
|
96
|
+
* read as clickable.
|
|
97
|
+
*/
|
|
98
|
+
resolveSymbol?: (query: WalkthroughSymbolQuery) => string | null;
|
|
99
|
+
/** A clicked construct token — open that construct's declaration line. */
|
|
100
|
+
onSymbolClick?: (symbol: string, query: WalkthroughSymbolQuery) => void;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Identifiers a token could match to name this component as a construct. */
|
|
104
|
+
function constructIdentifiers(comp: SubsystemComponent): string[] {
|
|
105
|
+
const ids = new Set<string>();
|
|
106
|
+
if (comp.name) ids.add(comp.name);
|
|
107
|
+
if (comp.symbol) {
|
|
108
|
+
ids.add(comp.symbol);
|
|
109
|
+
for (const part of comp.symbol.split('.')) {
|
|
110
|
+
if (part) ids.add(part);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return [...ids];
|
|
91
114
|
}
|
|
92
115
|
|
|
93
116
|
type DrawerTarget =
|
|
@@ -390,14 +413,29 @@ const WalkthroughDrawerContent = memo(function WalkthroughDrawerContent({
|
|
|
390
413
|
stepIndex,
|
|
391
414
|
onOpenFile,
|
|
392
415
|
proposedAliases,
|
|
416
|
+
resolveSymbol,
|
|
417
|
+
onSymbolClick,
|
|
393
418
|
}: {
|
|
394
419
|
render: (ctx: WalkthroughViewerContext) => ReactNode;
|
|
395
420
|
walkthrough: SubsystemWalkthrough;
|
|
396
421
|
stepIndex: number | null;
|
|
397
422
|
onOpenFile: (path: string, opts?: SubsystemOpenFileOptions) => void;
|
|
398
423
|
proposedAliases: ReadonlySet<string>;
|
|
424
|
+
resolveSymbol?: (query: WalkthroughSymbolQuery) => string | null;
|
|
425
|
+
onSymbolClick?: (symbol: string, query: WalkthroughSymbolQuery) => void;
|
|
399
426
|
}) {
|
|
400
|
-
return
|
|
427
|
+
return (
|
|
428
|
+
<>
|
|
429
|
+
{render({
|
|
430
|
+
walkthrough,
|
|
431
|
+
stepIndex,
|
|
432
|
+
onOpenFile,
|
|
433
|
+
proposedAliases,
|
|
434
|
+
resolveSymbol,
|
|
435
|
+
onSymbolClick,
|
|
436
|
+
})}
|
|
437
|
+
</>
|
|
438
|
+
);
|
|
401
439
|
});
|
|
402
440
|
|
|
403
441
|
interface InnerProps extends SubsystemComponentGraphProps {
|
|
@@ -570,6 +608,10 @@ function Inner({ components, relations, walkthroughs, initialWalkthroughId, onRe
|
|
|
570
608
|
if (drawerTarget?.kind !== 'walkthrough' || !walkthroughs) return null;
|
|
571
609
|
return walkthroughs.find((t) => t.id === drawerTarget.walkthroughId) ?? null;
|
|
572
610
|
}, [drawerTarget, walkthroughs]);
|
|
611
|
+
// Ref mirror of the drawer's walkthrough id so the symbol resolver stays
|
|
612
|
+
// stable across graph re-renders (the drawer is memoized on callback identity).
|
|
613
|
+
const drawerWalkthroughIdRef = useRef<string | null>(null);
|
|
614
|
+
drawerWalkthroughIdRef.current = focusedWalkthrough?.id ?? null;
|
|
573
615
|
|
|
574
616
|
// Walkthrough shown on the canvas title chip (focus or hover/autoplay highlight).
|
|
575
617
|
const overlayWalkthroughTitle = useMemo(() => {
|
|
@@ -1576,6 +1618,60 @@ function Inner({ components, relations, walkthroughs, initialWalkthroughId, onRe
|
|
|
1576
1618
|
[components, onSelect],
|
|
1577
1619
|
);
|
|
1578
1620
|
|
|
1621
|
+
// Construct tokens inside a walkthrough snippet. A step's line is an edge
|
|
1622
|
+
// between its `from`/`to` components, so a token naming either of those
|
|
1623
|
+
// constructs should navigate to that construct's declaration. Index the
|
|
1624
|
+
// matchable identifiers per step (`walkthroughId:index`).
|
|
1625
|
+
const walkthroughSymbolIndex = useMemo(() => {
|
|
1626
|
+
const byAlias = new Map(components.map((c) => [c.alias, c]));
|
|
1627
|
+
const index = new Map<string, Map<string, string>>();
|
|
1628
|
+
for (const wt of walkthroughs ?? []) {
|
|
1629
|
+
wt.steps.forEach((step, i) => {
|
|
1630
|
+
const tokens = new Map<string, string>();
|
|
1631
|
+
for (const alias of [step.from, step.to]) {
|
|
1632
|
+
const comp = byAlias.get(alias);
|
|
1633
|
+
if (!comp) continue;
|
|
1634
|
+
for (const ident of constructIdentifiers(comp)) tokens.set(ident, alias);
|
|
1635
|
+
}
|
|
1636
|
+
index.set(`${wt.id}:${i}`, tokens);
|
|
1637
|
+
});
|
|
1638
|
+
}
|
|
1639
|
+
return index;
|
|
1640
|
+
}, [components, walkthroughs]);
|
|
1641
|
+
const walkthroughSymbolIndexRef = useRef(walkthroughSymbolIndex);
|
|
1642
|
+
walkthroughSymbolIndexRef.current = walkthroughSymbolIndex;
|
|
1643
|
+
|
|
1644
|
+
// Stable resolver: reads the live index + focused walkthrough from refs so
|
|
1645
|
+
// the memoized walkthrough drawer isn't rebuilt on every graph render.
|
|
1646
|
+
const resolveWalkthroughSymbol = useCallback(
|
|
1647
|
+
(query: WalkthroughSymbolQuery): string | null => {
|
|
1648
|
+
const walkthroughId = drawerWalkthroughIdRef.current;
|
|
1649
|
+
if (walkthroughId == null) return null;
|
|
1650
|
+
return (
|
|
1651
|
+
walkthroughSymbolIndexRef.current
|
|
1652
|
+
.get(`${walkthroughId}:${query.stepIndex}`)
|
|
1653
|
+
?.get(query.tokenText) ?? null
|
|
1654
|
+
);
|
|
1655
|
+
},
|
|
1656
|
+
[],
|
|
1657
|
+
);
|
|
1658
|
+
|
|
1659
|
+
// A construct token click navigates to that construct's declaration: open
|
|
1660
|
+
// its file at the anchored declaration line (same path as the declaration
|
|
1661
|
+
// panel's file link), falling back to the file top when unanchored.
|
|
1662
|
+
const openConstructDeclaration = useCallback(
|
|
1663
|
+
(alias: string) => {
|
|
1664
|
+
const comp = components.find((c) => c.alias === alias);
|
|
1665
|
+
if (!comp?.file) return;
|
|
1666
|
+
const startLine = comp.declarationRef?.startLine;
|
|
1667
|
+
onOpenDeclarationFile(
|
|
1668
|
+
comp.file,
|
|
1669
|
+
startLine != null ? { startLine } : undefined,
|
|
1670
|
+
);
|
|
1671
|
+
},
|
|
1672
|
+
[components, onOpenDeclarationFile],
|
|
1673
|
+
);
|
|
1674
|
+
|
|
1579
1675
|
// Edge label data for the overlay (rendered OUTSIDE ReactFlow so the pane
|
|
1580
1676
|
// doesn't intercept pointer events). Uses ELK-computed label midpoints from
|
|
1581
1677
|
// the actual edge path (not node-center approximations).
|
|
@@ -2210,6 +2306,8 @@ function Inner({ components, relations, walkthroughs, initialWalkthroughId, onRe
|
|
|
2210
2306
|
stepIndex={drawerTarget.stepIndex}
|
|
2211
2307
|
onOpenFile={onOpenFileFromWalkthrough}
|
|
2212
2308
|
proposedAliases={proposedAliases}
|
|
2309
|
+
resolveSymbol={resolveWalkthroughSymbol}
|
|
2310
|
+
onSymbolClick={openConstructDeclaration}
|
|
2213
2311
|
/>
|
|
2214
2312
|
) : drawerTarget?.kind === 'file' ? (
|
|
2215
2313
|
<FileDrawerContent
|