@principal-ai/subsystems-react 0.21.1 → 0.23.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/graphify/consolidated.d.ts +5 -24
- package/dist/graphify/consolidated.d.ts.map +1 -1
- package/dist/graphify/index.d.ts +1 -1
- package/dist/graphify/index.d.ts.map +1 -1
- package/dist/graphify/index.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/pierre/PierreFileView.d.ts +3 -1
- package/dist/pierre/PierreFileView.d.ts.map +1 -1
- package/dist/pierre/PierreFileView.js +13 -3
- package/dist/pierre/PierreFileView.js.map +1 -1
- package/dist/pierre/PierreSnippetView.d.ts.map +1 -1
- package/dist/pierre/PierreSnippetView.js +5 -26
- package/dist/pierre/PierreSnippetView.js.map +1 -1
- package/dist/pierre/PierreWalkthroughCodeView.d.ts +3 -1
- package/dist/pierre/PierreWalkthroughCodeView.d.ts.map +1 -1
- package/dist/pierre/PierreWalkthroughCodeView.js +65 -8
- package/dist/pierre/PierreWalkthroughCodeView.js.map +1 -1
- package/dist/pierre/constructColors.d.ts.map +1 -1
- package/dist/pierre/constructColors.js +0 -5
- package/dist/pierre/constructColors.js.map +1 -1
- package/dist/pierre/index.d.ts +1 -1
- package/dist/pierre/index.d.ts.map +1 -1
- package/dist/pierre/index.js +1 -1
- package/dist/pierre/index.js.map +1 -1
- package/dist/pierre/scrollAnchor.d.ts +2 -0
- package/dist/pierre/scrollAnchor.d.ts.map +1 -1
- package/dist/pierre/scrollAnchor.js +7 -0
- package/dist/pierre/scrollAnchor.js.map +1 -1
- package/dist/pierre/sliceSnippet.d.ts +6 -0
- package/dist/pierre/sliceSnippet.d.ts.map +1 -1
- package/dist/pierre/sliceSnippet.js +25 -0
- package/dist/pierre/sliceSnippet.js.map +1 -1
- package/dist/stories/Subsystem/ComponentGraph/fixtures.d.ts.map +1 -1
- package/dist/stories/Subsystem/ComponentGraph/fixtures.js +0 -4
- package/dist/stories/Subsystem/ComponentGraph/fixtures.js.map +1 -1
- package/dist/subsystem/FileDrawer.d.ts +7 -2
- package/dist/subsystem/FileDrawer.d.ts.map +1 -1
- package/dist/subsystem/FileDrawer.js +13 -9
- package/dist/subsystem/FileDrawer.js.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.d.ts +2 -0
- package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.js +97 -8
- package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
- package/dist/subsystem/declarationRef.d.ts +6 -1
- package/dist/subsystem/declarationRef.d.ts.map +1 -1
- package/dist/subsystem/formatDeclaration.d.ts.map +1 -1
- package/dist/subsystem/formatDeclaration.js +0 -15
- package/dist/subsystem/formatDeclaration.js.map +1 -1
- package/dist/subsystem/model.d.ts +61 -22
- package/dist/subsystem/model.d.ts.map +1 -1
- package/dist/subsystem/model.js +185 -65
- package/dist/subsystem/model.js.map +1 -1
- package/dist/subsystem/nodes.d.ts +3 -4
- package/dist/subsystem/nodes.d.ts.map +1 -1
- package/dist/subsystem/nodes.js +5 -6
- package/dist/subsystem/nodes.js.map +1 -1
- package/dist/utils/elkLayout.d.ts +6 -4
- package/dist/utils/elkLayout.d.ts.map +1 -1
- package/dist/utils/elkLayout.js +144 -54
- package/dist/utils/elkLayout.js.map +1 -1
- package/package.json +3 -3
- package/src/graphify/consolidated.ts +4 -26
- package/src/graphify/index.ts +0 -2
- package/src/index.ts +0 -2
- package/src/pierre/PierreFileView.tsx +25 -2
- package/src/pierre/PierreSnippetView.tsx +5 -31
- package/src/pierre/PierreWalkthroughCodeView.tsx +110 -5
- package/src/pierre/constructColors.test.ts +1 -2
- package/src/pierre/constructColors.ts +0 -5
- package/src/pierre/index.ts +1 -1
- package/src/pierre/scrollAnchor.ts +13 -0
- package/src/pierre/sliceSnippet.test.ts +44 -1
- package/src/pierre/sliceSnippet.ts +28 -0
- package/src/stories/Subsystem/ComponentDeclarationAudit.stories.tsx +0 -31
- package/src/stories/Subsystem/ComponentGraph/Appearance.stories.tsx +2 -46
- package/src/stories/Subsystem/ComponentGraph/Captures.stories.tsx +9 -24
- package/src/stories/Subsystem/ComponentGraph/DetailPanel.stories.tsx +2 -2
- package/src/stories/Subsystem/ComponentGraph/Flows.stories.tsx +2 -1
- package/src/stories/Subsystem/ComponentGraph/Modules.stories.tsx +287 -0
- package/src/stories/Subsystem/ComponentGraph/Processes.stories.tsx +1 -1
- package/src/stories/Subsystem/ComponentGraph/Scenarios.stories.tsx +6 -6
- package/src/stories/Subsystem/ComponentGraph/fixtures.ts +0 -4
- package/src/subsystem/FileDrawer.tsx +19 -7
- package/src/subsystem/SubsystemComponentGraph.tsx +153 -7
- package/src/subsystem/declarationRef.ts +6 -1
- package/src/subsystem/formatDeclaration.ts +0 -19
- package/src/subsystem/model.test.ts +190 -9
- package/src/subsystem/model.ts +219 -77
- package/src/subsystem/nodes.tsx +6 -6
- package/src/utils/elkLayout.ts +148 -58
|
@@ -0,0 +1,287 @@
|
|
|
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/Modules',
|
|
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 PURL = 'pkg:github/principal-ai/agent-monitoring';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Module frames replace `construct: module`. Each export keeps its real
|
|
29
|
+
* construct; shared `module` (the source path) draws the file as a boundary.
|
|
30
|
+
*/
|
|
31
|
+
const moduleComponents: SubsystemComponent[] = [
|
|
32
|
+
{
|
|
33
|
+
id: 'record',
|
|
34
|
+
name: 'CodexRolloutRecord',
|
|
35
|
+
construct: 'type_alias',
|
|
36
|
+
symbol: 'CodexRolloutRecord',
|
|
37
|
+
file: 'src/session/transcript.ts',
|
|
38
|
+
module: 'src/session/transcript.ts',
|
|
39
|
+
purl: PURL,
|
|
40
|
+
purpose: 'parsed rollout record shape',
|
|
41
|
+
layer: 1,
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
id: 'is-rollout',
|
|
45
|
+
name: 'isCodexRollout',
|
|
46
|
+
construct: 'function',
|
|
47
|
+
symbol: 'isCodexRollout',
|
|
48
|
+
file: 'src/session/transcript.ts',
|
|
49
|
+
module: 'src/session/transcript.ts',
|
|
50
|
+
purl: PURL,
|
|
51
|
+
purpose: 'type guard for rollout records',
|
|
52
|
+
layer: 1,
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: 'parse',
|
|
56
|
+
name: 'parseTranscript',
|
|
57
|
+
construct: 'function',
|
|
58
|
+
symbol: 'parseTranscript',
|
|
59
|
+
file: 'src/session/transcript.ts',
|
|
60
|
+
module: 'src/session/transcript.ts',
|
|
61
|
+
purl: PURL,
|
|
62
|
+
purpose: 'parses session transcript lines',
|
|
63
|
+
layer: 1,
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: 'tool-name',
|
|
67
|
+
name: 'extractToolName',
|
|
68
|
+
construct: 'function',
|
|
69
|
+
symbol: 'extractToolName',
|
|
70
|
+
file: 'src/session/paths.ts',
|
|
71
|
+
module: 'src/session/paths.ts',
|
|
72
|
+
purl: PURL,
|
|
73
|
+
purpose: 'pulls tool names from path-like strings',
|
|
74
|
+
layer: 2,
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: 'file-path',
|
|
78
|
+
name: 'extractFilePath',
|
|
79
|
+
construct: 'function',
|
|
80
|
+
symbol: 'extractFilePath',
|
|
81
|
+
file: 'src/session/paths.ts',
|
|
82
|
+
module: 'src/session/paths.ts',
|
|
83
|
+
purl: PURL,
|
|
84
|
+
purpose: 'pulls file paths from tool args',
|
|
85
|
+
layer: 2,
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
id: 'reader',
|
|
89
|
+
name: 'SessionReader',
|
|
90
|
+
construct: 'class',
|
|
91
|
+
symbol: 'SessionReader',
|
|
92
|
+
file: 'src/session/SessionReader.ts',
|
|
93
|
+
module: 'src/session/SessionReader.ts',
|
|
94
|
+
purl: PURL,
|
|
95
|
+
purpose: 'normalizes a session into universal events',
|
|
96
|
+
layer: 3,
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
id: 'normalize',
|
|
100
|
+
name: 'normalize',
|
|
101
|
+
construct: 'method',
|
|
102
|
+
symbol: 'SessionReader.normalize',
|
|
103
|
+
file: 'src/session/SessionReader.ts',
|
|
104
|
+
module: 'src/session/SessionReader.ts',
|
|
105
|
+
purl: PURL,
|
|
106
|
+
purpose: 'maps a session into universal events',
|
|
107
|
+
layer: 3,
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
id: 'registry',
|
|
111
|
+
name: 'registerAgent',
|
|
112
|
+
construct: 'function',
|
|
113
|
+
symbol: 'registerAgent',
|
|
114
|
+
file: 'src/supported-agents.ts',
|
|
115
|
+
// Singleton module — no frame (same rule as process: need 2+ members).
|
|
116
|
+
module: 'src/supported-agents.ts',
|
|
117
|
+
purl: PURL,
|
|
118
|
+
purpose: 'registers a supported agent (singleton file — no module frame)',
|
|
119
|
+
layer: 4,
|
|
120
|
+
},
|
|
121
|
+
];
|
|
122
|
+
|
|
123
|
+
const moduleEdges = graphSpecFromEdges([
|
|
124
|
+
['parse', 'record', 'references'],
|
|
125
|
+
['is-rollout', 'record', 'references'],
|
|
126
|
+
['reader', 'normalize', 'method'],
|
|
127
|
+
['normalize', 'parse', 'calls'],
|
|
128
|
+
['normalize', 'tool-name', 'calls'],
|
|
129
|
+
['normalize', 'file-path', 'calls'],
|
|
130
|
+
['reader', 'registry', 'registers-into'],
|
|
131
|
+
]);
|
|
132
|
+
|
|
133
|
+
export const ModuleBoundaries: Story = {
|
|
134
|
+
render: () => (
|
|
135
|
+
<div style={{ width: '100%', height: '100vh' }}>
|
|
136
|
+
<SubsystemComponentGraph
|
|
137
|
+
title="Module boundaries"
|
|
138
|
+
description="Concrete exports keep their real construct. Shared `module` (the source path) draws the file as a dashed frame — not `construct: module`. Singleton modules (registerAgent) stay unframed."
|
|
139
|
+
components={moduleComponents}
|
|
140
|
+
relations={moduleEdges.relations}
|
|
141
|
+
walkthroughs={moduleEdges.walkthroughs}
|
|
142
|
+
/>
|
|
143
|
+
</div>
|
|
144
|
+
),
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
/** Process → module → export nesting (host / renderer). */
|
|
148
|
+
const nestedComponents: SubsystemComponent[] = [
|
|
149
|
+
{
|
|
150
|
+
id: 'boot',
|
|
151
|
+
name: 'boot',
|
|
152
|
+
construct: 'function',
|
|
153
|
+
symbol: 'boot',
|
|
154
|
+
role: 'entry',
|
|
155
|
+
file: 'src/host/main.ts',
|
|
156
|
+
module: 'src/host/main.ts',
|
|
157
|
+
process: 'principal-studio/host',
|
|
158
|
+
purl: 'pkg:github/principal-ai/principal-studio',
|
|
159
|
+
purpose: 'Boots the host process',
|
|
160
|
+
layer: 1,
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
id: 'create-host',
|
|
164
|
+
name: 'createHost',
|
|
165
|
+
construct: 'function',
|
|
166
|
+
symbol: 'createHost',
|
|
167
|
+
file: 'src/host/main.ts',
|
|
168
|
+
module: 'src/host/main.ts',
|
|
169
|
+
process: 'principal-studio/host',
|
|
170
|
+
purl: 'pkg:github/principal-ai/principal-studio',
|
|
171
|
+
purpose: 'Constructs the host runtime',
|
|
172
|
+
layer: 1,
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
id: 'session-store',
|
|
176
|
+
name: 'SessionStore',
|
|
177
|
+
construct: 'store',
|
|
178
|
+
symbol: 'SessionStore',
|
|
179
|
+
file: 'src/host/store.ts',
|
|
180
|
+
module: 'src/host/store.ts',
|
|
181
|
+
process: 'principal-studio/host',
|
|
182
|
+
purl: 'pkg:github/principal-ai/principal-studio',
|
|
183
|
+
purpose: 'Retained host session state',
|
|
184
|
+
layer: 2,
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
id: 'write-session',
|
|
188
|
+
name: 'writeSession',
|
|
189
|
+
construct: 'function',
|
|
190
|
+
symbol: 'writeSession',
|
|
191
|
+
file: 'src/host/store.ts',
|
|
192
|
+
module: 'src/host/store.ts',
|
|
193
|
+
process: 'principal-studio/host',
|
|
194
|
+
purl: 'pkg:github/principal-ai/principal-studio',
|
|
195
|
+
purpose: 'Mutates SessionStore',
|
|
196
|
+
layer: 2,
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
id: 'bridge',
|
|
200
|
+
name: 'bridge',
|
|
201
|
+
construct: 'function',
|
|
202
|
+
symbol: 'bridge',
|
|
203
|
+
role: 'entry',
|
|
204
|
+
file: 'src/renderer/bridge.ts',
|
|
205
|
+
module: 'src/renderer/bridge.ts',
|
|
206
|
+
process: 'principal-studio/renderer',
|
|
207
|
+
purl: 'pkg:github/principal-ai/principal-studio',
|
|
208
|
+
purpose: 'Renderer IPC entry',
|
|
209
|
+
layer: 3,
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
id: 'post-to-host',
|
|
213
|
+
name: 'postToHost',
|
|
214
|
+
construct: 'function',
|
|
215
|
+
symbol: 'postToHost',
|
|
216
|
+
file: 'src/renderer/bridge.ts',
|
|
217
|
+
module: 'src/renderer/bridge.ts',
|
|
218
|
+
process: 'principal-studio/renderer',
|
|
219
|
+
purl: 'pkg:github/principal-ai/principal-studio',
|
|
220
|
+
purpose: 'Sends a message to the host',
|
|
221
|
+
layer: 3,
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
id: 'trail-view',
|
|
225
|
+
name: 'TrailView',
|
|
226
|
+
construct: 'function',
|
|
227
|
+
symbol: 'TrailView',
|
|
228
|
+
framework: 'react',
|
|
229
|
+
stereotype: 'component',
|
|
230
|
+
file: 'src/renderer/view.tsx',
|
|
231
|
+
module: 'src/renderer/view.tsx',
|
|
232
|
+
process: 'principal-studio/renderer',
|
|
233
|
+
purl: 'pkg:github/principal-ai/principal-studio',
|
|
234
|
+
purpose: 'Renders the trail',
|
|
235
|
+
layer: 4,
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
id: 'use-trail',
|
|
239
|
+
name: 'useTrail',
|
|
240
|
+
construct: 'function',
|
|
241
|
+
symbol: 'useTrail',
|
|
242
|
+
framework: 'react',
|
|
243
|
+
stereotype: 'hook',
|
|
244
|
+
file: 'src/renderer/view.tsx',
|
|
245
|
+
module: 'src/renderer/view.tsx',
|
|
246
|
+
process: 'principal-studio/renderer',
|
|
247
|
+
purl: 'pkg:github/principal-ai/principal-studio',
|
|
248
|
+
purpose: 'Subscribes the view to trail updates',
|
|
249
|
+
layer: 4,
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
id: 'telemetry',
|
|
253
|
+
name: 'telemetry',
|
|
254
|
+
construct: 'external',
|
|
255
|
+
role: 'service',
|
|
256
|
+
file: '',
|
|
257
|
+
purl: 'external',
|
|
258
|
+
purpose: 'External sink — outside every frame',
|
|
259
|
+
layer: 5,
|
|
260
|
+
},
|
|
261
|
+
];
|
|
262
|
+
|
|
263
|
+
const nestedEdges = graphSpecFromEdges([
|
|
264
|
+
['boot', 'write-session', 'imports'],
|
|
265
|
+
['trail-view', 'bridge', 'imports'],
|
|
266
|
+
['trail-view', 'use-trail', 'references'],
|
|
267
|
+
['boot', 'create-host', 'calls'],
|
|
268
|
+
['create-host', 'write-session', 'calls'],
|
|
269
|
+
['write-session', 'session-store', 'writes'],
|
|
270
|
+
['bridge', 'boot', 'calls'],
|
|
271
|
+
['trail-view', 'post-to-host', 'calls'],
|
|
272
|
+
['boot', 'telemetry', 'uses'],
|
|
273
|
+
]);
|
|
274
|
+
|
|
275
|
+
export const ProcessAndModuleNesting: Story = {
|
|
276
|
+
render: () => (
|
|
277
|
+
<div style={{ width: '100%', height: '100vh' }}>
|
|
278
|
+
<SubsystemComponentGraph
|
|
279
|
+
title="Process × module nesting"
|
|
280
|
+
description="process frames wrap module frames wrap exports. Leaves parent to module:; module groups parent to process: when every member shares that process."
|
|
281
|
+
components={nestedComponents}
|
|
282
|
+
relations={nestedEdges.relations}
|
|
283
|
+
walkthroughs={nestedEdges.walkthroughs}
|
|
284
|
+
/>
|
|
285
|
+
</div>
|
|
286
|
+
),
|
|
287
|
+
};
|
|
@@ -28,7 +28,7 @@ const processComponents = [
|
|
|
28
28
|
['main', 'main', 'function', 'src/host/main.ts', 'pkg:github/principal-ai/principal-studio', 'boots the host process', 'main'],
|
|
29
29
|
['store', 'SessionStore', 'store', 'src/host/store.ts', 'pkg:github/principal-ai/principal-studio', 'retained host state', 'SessionStore'],
|
|
30
30
|
['view', 'TrailView', 'function', 'src/renderer/view.tsx', 'pkg:github/principal-ai/principal-studio', 'renders the trail', 'TrailView'],
|
|
31
|
-
['bridge', 'bridge', '
|
|
31
|
+
['bridge', 'bridge', 'function', 'src/renderer/bridge.ts', 'pkg:github/principal-ai/principal-studio', 'IPC bridge to the host', 'bridge'],
|
|
32
32
|
]),
|
|
33
33
|
{
|
|
34
34
|
id: 'svc',
|
|
@@ -84,7 +84,7 @@ const mermaidComponents: SubsystemComponent[] = [
|
|
|
84
84
|
{
|
|
85
85
|
id: 'helpers',
|
|
86
86
|
name: 'beautifulMermaid',
|
|
87
|
-
construct: '
|
|
87
|
+
construct: 'function',
|
|
88
88
|
file: 'industryMarkdown/utils/beautifulMermaid.ts',
|
|
89
89
|
purl: mermaidPurl,
|
|
90
90
|
purpose: 'engine detection, theme\u2192options mapping, SVG post-processing',
|
|
@@ -130,11 +130,11 @@ const coreLibPurl = 'pkg:github/principal-ai/principal-view-core-library';
|
|
|
130
130
|
const graphifyPurl = 'pkg:github/Graphify-Labs/graphify';
|
|
131
131
|
|
|
132
132
|
const multiRepoComponents = components([
|
|
133
|
-
['resolver', 'resolve.ts', '
|
|
134
|
-
['reftypes', 'consolidated.ts', '
|
|
135
|
-
['detail', 'ComponentDeclaration.tsx', '
|
|
136
|
-
['engine', 'engine.py', '
|
|
137
|
-
['rewire', 'extract.py', '
|
|
133
|
+
['resolver', 'resolve.ts', 'function', 'packages/subsystems-react/src/graphify/resolve.ts', coreLibPurl, 'consumer-side resolution mirroring graphify rewire tiers', 'createGraphifyTypeResolver'],
|
|
134
|
+
['reftypes', 'consolidated.ts', 'function', 'packages/subsystems-react/src/graphify/consolidated.ts', coreLibPurl, 'drill-down payload types carrying ref.nodeId', 'GraphifyParamInfo'],
|
|
135
|
+
['detail', 'ComponentDeclaration.tsx', 'function', 'packages/subsystems-react/src/subsystem/ComponentDeclaration.tsx', coreLibPurl, 'declaration panel rendering the selected component as code, with clickable type tokens', 'ComponentDeclaration'],
|
|
136
|
+
['engine', 'engine.py', 'function', 'graphify/extractors/engine.py', graphifyPurl, 'tree-sitter walk emitting references[parameter_type] edges + sourceless stubs', 'ensure_named_node'],
|
|
137
|
+
['rewire', 'extract.py', 'function', 'graphify/extract.py', graphifyPurl, 'corpus pass folding unique-label stubs onto definitions', '_rewire_unique_stub_nodes'],
|
|
138
138
|
]);
|
|
139
139
|
|
|
140
140
|
const multiRepoEdges = graphSpecFromEdges([
|
|
@@ -29,16 +29,12 @@ export function components(
|
|
|
29
29
|
|
|
30
30
|
const RELATION_TYPES = new Set<string>([
|
|
31
31
|
'imports',
|
|
32
|
-
'imports_from',
|
|
33
|
-
're_exports',
|
|
34
|
-
'defines',
|
|
35
32
|
'extends',
|
|
36
33
|
'inherits',
|
|
37
34
|
'implements',
|
|
38
35
|
'mixes_in',
|
|
39
36
|
'method',
|
|
40
37
|
'references',
|
|
41
|
-
'contains',
|
|
42
38
|
]);
|
|
43
39
|
|
|
44
40
|
export function relations(
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* children by the graph component.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { useEffect } from 'react';
|
|
8
|
+
import { useEffect, useState } from 'react';
|
|
9
9
|
import type { ReactNode } from 'react';
|
|
10
10
|
import { useTheme } from '@principal-ade/industry-theme';
|
|
11
11
|
import { X } from 'lucide-react';
|
|
@@ -16,29 +16,37 @@ export const FILE_DRAWER_HEIGHT_MS = 200;
|
|
|
16
16
|
|
|
17
17
|
/** Bottom panel that slides up from the bottom of the graph area.
|
|
18
18
|
* Sits in normal flow (canvas shrinks while open, nothing covered)
|
|
19
|
-
* and animates via height; stays mounted so open/close animates.
|
|
19
|
+
* and animates via height; stays mounted so open/close animates.
|
|
20
|
+
* `fillHeight` expands to the full graph column (for whole-file reading). */
|
|
20
21
|
export function FileDrawer({
|
|
21
22
|
title,
|
|
22
23
|
onClose,
|
|
24
|
+
fillHeight = false,
|
|
25
|
+
suppressEscape = false,
|
|
23
26
|
children,
|
|
24
27
|
}: {
|
|
25
28
|
/** Drawer chrome title; `null` closes the drawer. */
|
|
26
29
|
title: string | null;
|
|
27
30
|
onClose: () => void;
|
|
31
|
+
/** When true and open, fill the graph column instead of ~45% height. */
|
|
32
|
+
fillHeight?: boolean;
|
|
33
|
+
/** Skip Escape handling (e.g. while a full-file overlay is on top). */
|
|
34
|
+
suppressEscape?: boolean;
|
|
28
35
|
children?: ReactNode;
|
|
29
36
|
}) {
|
|
30
37
|
const { theme } = useTheme();
|
|
31
38
|
const muted = theme.colors.textMuted ?? theme.colors.textSecondary;
|
|
32
39
|
const open = title !== null;
|
|
40
|
+
const [closeHover, setCloseHover] = useState(false);
|
|
33
41
|
|
|
34
42
|
useEffect(() => {
|
|
35
|
-
if (!open) return;
|
|
43
|
+
if (!open || suppressEscape) return;
|
|
36
44
|
const onKey = (e: KeyboardEvent) => {
|
|
37
45
|
if (e.key === 'Escape') onClose();
|
|
38
46
|
};
|
|
39
47
|
window.addEventListener('keydown', onKey);
|
|
40
48
|
return () => window.removeEventListener('keydown', onKey);
|
|
41
|
-
}, [open, onClose]);
|
|
49
|
+
}, [open, onClose, suppressEscape]);
|
|
42
50
|
|
|
43
51
|
return (
|
|
44
52
|
<div
|
|
@@ -48,7 +56,8 @@ export function FileDrawer({
|
|
|
48
56
|
// whole graph-area container including this panel's slice.
|
|
49
57
|
zIndex: 6,
|
|
50
58
|
flexShrink: 0,
|
|
51
|
-
|
|
59
|
+
flex: open && fillHeight ? 1 : undefined,
|
|
60
|
+
height: open ? (fillHeight ? undefined : '45%') : 0,
|
|
52
61
|
minHeight: 0,
|
|
53
62
|
overflow: 'hidden',
|
|
54
63
|
display: 'flex',
|
|
@@ -86,6 +95,8 @@ export function FileDrawer({
|
|
|
86
95
|
<button
|
|
87
96
|
type="button"
|
|
88
97
|
onClick={onClose}
|
|
98
|
+
onMouseEnter={() => setCloseHover(true)}
|
|
99
|
+
onMouseLeave={() => setCloseHover(false)}
|
|
89
100
|
aria-label="Close file"
|
|
90
101
|
style={{
|
|
91
102
|
display: 'inline-flex',
|
|
@@ -96,9 +107,10 @@ export function FileDrawer({
|
|
|
96
107
|
padding: 0,
|
|
97
108
|
border: 'none',
|
|
98
109
|
borderRadius: 4,
|
|
99
|
-
background: 'transparent',
|
|
100
|
-
color: theme.colors.text,
|
|
110
|
+
background: closeHover ? theme.colors.border : 'transparent',
|
|
111
|
+
color: closeHover ? theme.colors.text : muted,
|
|
101
112
|
cursor: 'pointer',
|
|
113
|
+
transition: 'background 120ms ease, color 120ms ease',
|
|
102
114
|
}}
|
|
103
115
|
>
|
|
104
116
|
<X size={14} />
|
|
@@ -69,6 +69,8 @@ export interface WalkthroughViewerContext {
|
|
|
69
69
|
walkthrough: SubsystemWalkthrough;
|
|
70
70
|
/** Focused step index; `null` means the whole flow (no specific step). */
|
|
71
71
|
stepIndex: number | null;
|
|
72
|
+
/** Open a step's full source file over the walkthrough drawer (keeps snippets mounted). */
|
|
73
|
+
onOpenFile: (path: string) => void;
|
|
72
74
|
}
|
|
73
75
|
|
|
74
76
|
type DrawerTarget =
|
|
@@ -198,16 +200,49 @@ const FileDrawerContent = memo(function FileDrawerContent({
|
|
|
198
200
|
return <>{render(file, startLine != null ? { startLine } : undefined)}</>;
|
|
199
201
|
});
|
|
200
202
|
|
|
203
|
+
function FileOverlayCloseButton({ onClose }: { onClose: () => void }) {
|
|
204
|
+
const { theme } = useTheme();
|
|
205
|
+
const muted = theme.colors.textMuted ?? theme.colors.textSecondary;
|
|
206
|
+
const [hover, setHover] = useState(false);
|
|
207
|
+
return (
|
|
208
|
+
<button
|
|
209
|
+
type="button"
|
|
210
|
+
onClick={onClose}
|
|
211
|
+
onMouseEnter={() => setHover(true)}
|
|
212
|
+
onMouseLeave={() => setHover(false)}
|
|
213
|
+
aria-label="Close file"
|
|
214
|
+
style={{
|
|
215
|
+
display: 'inline-flex',
|
|
216
|
+
alignItems: 'center',
|
|
217
|
+
justifyContent: 'center',
|
|
218
|
+
width: 22,
|
|
219
|
+
height: 22,
|
|
220
|
+
padding: 0,
|
|
221
|
+
border: 'none',
|
|
222
|
+
borderRadius: 4,
|
|
223
|
+
background: hover ? theme.colors.border : 'transparent',
|
|
224
|
+
color: hover ? theme.colors.text : muted,
|
|
225
|
+
cursor: 'pointer',
|
|
226
|
+
transition: 'background 120ms ease, color 120ms ease',
|
|
227
|
+
}}
|
|
228
|
+
>
|
|
229
|
+
<X size={14} />
|
|
230
|
+
</button>
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
201
234
|
const WalkthroughDrawerContent = memo(function WalkthroughDrawerContent({
|
|
202
235
|
render,
|
|
203
236
|
walkthrough,
|
|
204
237
|
stepIndex,
|
|
238
|
+
onOpenFile,
|
|
205
239
|
}: {
|
|
206
240
|
render: (ctx: WalkthroughViewerContext) => ReactNode;
|
|
207
241
|
walkthrough: SubsystemWalkthrough;
|
|
208
242
|
stepIndex: number | null;
|
|
243
|
+
onOpenFile: (path: string) => void;
|
|
209
244
|
}) {
|
|
210
|
-
return <>{render({ walkthrough, stepIndex })}</>;
|
|
245
|
+
return <>{render({ walkthrough, stepIndex, onOpenFile })}</>;
|
|
211
246
|
});
|
|
212
247
|
|
|
213
248
|
interface InnerProps extends SubsystemComponentGraphProps {
|
|
@@ -230,6 +265,8 @@ function Inner({ components, relations, walkthroughs, onSelect, onEdgeSelect, me
|
|
|
230
265
|
const [selected, setSelected] = useState<SubsystemComponent | null>(null);
|
|
231
266
|
/** Bottom drawer: single file or walkthrough multi-snippet mode. */
|
|
232
267
|
const [drawerTarget, setDrawerTarget] = useState<DrawerTarget | null>(null);
|
|
268
|
+
/** Full-file layer over an open walkthrough drawer — walkthrough stays mounted. */
|
|
269
|
+
const [fileOverlay, setFileOverlay] = useState<{ file: string } | null>(null);
|
|
233
270
|
// Mirror so step-focus can decide whether to wait for the drawer open
|
|
234
271
|
// transition before fitView (avoids framing against full-height canvas).
|
|
235
272
|
const drawerOpenRef = useRef(false);
|
|
@@ -316,9 +353,11 @@ function Inner({ components, relations, walkthroughs, onSelect, onEdgeSelect, me
|
|
|
316
353
|
// Ref mirror of the open file drawer target for tree-click toggle.
|
|
317
354
|
const openFileRef = useRef<{ file: string; startLine?: number } | null>(null);
|
|
318
355
|
const openFile =
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
356
|
+
fileOverlay?.file ??
|
|
357
|
+
(drawerTarget?.kind === 'file' ? drawerTarget.file : null);
|
|
358
|
+
openFileRef.current = fileOverlay
|
|
359
|
+
? { file: fileOverlay.file }
|
|
360
|
+
: drawerTarget?.kind === 'file'
|
|
322
361
|
? { file: drawerTarget.file, startLine: drawerTarget.startLine }
|
|
323
362
|
: null;
|
|
324
363
|
|
|
@@ -864,6 +903,7 @@ function Inner({ components, relations, walkthroughs, onSelect, onEdgeSelect, me
|
|
|
864
903
|
);
|
|
865
904
|
const onTreeSelectFile = useCallback(
|
|
866
905
|
(file: string) => {
|
|
906
|
+
setFileOverlay(null);
|
|
867
907
|
if (openFileRef.current?.file === file && openFileRef.current.startLine == null) {
|
|
868
908
|
setDrawerTarget(null);
|
|
869
909
|
return;
|
|
@@ -876,6 +916,7 @@ function Inner({ components, relations, walkthroughs, onSelect, onEdgeSelect, me
|
|
|
876
916
|
|
|
877
917
|
const onOpenDeclarationFile = useCallback(
|
|
878
918
|
(file: string, opts?: SubsystemOpenFileOptions) => {
|
|
919
|
+
setFileOverlay(null);
|
|
879
920
|
const startLine = opts?.startLine;
|
|
880
921
|
if (
|
|
881
922
|
openFileRef.current?.file === file &&
|
|
@@ -890,6 +931,47 @@ function Inner({ components, relations, walkthroughs, onSelect, onEdgeSelect, me
|
|
|
890
931
|
[onFileSelect],
|
|
891
932
|
);
|
|
892
933
|
|
|
934
|
+
const onOpenFileFromWalkthrough = useCallback(
|
|
935
|
+
(file: string) => {
|
|
936
|
+
setFileOverlay({ file });
|
|
937
|
+
onFileSelect?.(file);
|
|
938
|
+
},
|
|
939
|
+
[onFileSelect],
|
|
940
|
+
);
|
|
941
|
+
|
|
942
|
+
const closeFileOverlay = useCallback(() => {
|
|
943
|
+
setFileOverlay(null);
|
|
944
|
+
}, []);
|
|
945
|
+
|
|
946
|
+
const closeDrawer = useCallback(() => {
|
|
947
|
+
setFileOverlay(null);
|
|
948
|
+
setDrawerTarget(null);
|
|
949
|
+
}, []);
|
|
950
|
+
|
|
951
|
+
const drawerFillHeight =
|
|
952
|
+
drawerTarget?.kind === 'file' && drawerTarget.startLine == null;
|
|
953
|
+
const fileOverlayOpen = fileOverlay != null;
|
|
954
|
+
const collapseCanvas = drawerFillHeight || fileOverlayOpen;
|
|
955
|
+
|
|
956
|
+
// Drop the full-file overlay when the underlying walkthrough drawer changes
|
|
957
|
+
// or closes — open-file keeps the same drawerTarget so the snippets stay mounted.
|
|
958
|
+
const walkthroughDrawerKey =
|
|
959
|
+
drawerTarget?.kind === 'walkthrough'
|
|
960
|
+
? `${drawerTarget.walkthroughId}:${drawerTarget.stepIndex}`
|
|
961
|
+
: null;
|
|
962
|
+
useEffect(() => {
|
|
963
|
+
setFileOverlay(null);
|
|
964
|
+
}, [walkthroughDrawerKey]);
|
|
965
|
+
|
|
966
|
+
useEffect(() => {
|
|
967
|
+
if (!fileOverlayOpen) return;
|
|
968
|
+
const onKey = (e: KeyboardEvent) => {
|
|
969
|
+
if (e.key === 'Escape') closeFileOverlay();
|
|
970
|
+
};
|
|
971
|
+
window.addEventListener('keydown', onKey);
|
|
972
|
+
return () => window.removeEventListener('keydown', onKey);
|
|
973
|
+
}, [fileOverlayOpen, closeFileOverlay]);
|
|
974
|
+
|
|
893
975
|
// Camera helper shared by the walkthrough interactions: frames the focused
|
|
894
976
|
// edges' endpoint nodes via `fitView({ nodes })`, which uses the store's
|
|
895
977
|
// live positions + measured dims — so the frame always includes BOTH
|
|
@@ -1532,8 +1614,19 @@ function Inner({ components, relations, walkthroughs, onSelect, onEdgeSelect, me
|
|
|
1532
1614
|
{/* Graph area */}
|
|
1533
1615
|
<div style={{ flex: 1, position: 'relative', display: 'flex', flexDirection: 'column', minHeight: 0 }}>
|
|
1534
1616
|
{/* Canvas box — the edge-label overlay and legend anchor here, so
|
|
1535
|
-
overlays shift with the canvas, not the labels.
|
|
1536
|
-
|
|
1617
|
+
overlays shift with the canvas, not the labels. Collapses when the
|
|
1618
|
+
file drawer fills the column (whole-file reading). */}
|
|
1619
|
+
<div
|
|
1620
|
+
style={{
|
|
1621
|
+
position: 'relative',
|
|
1622
|
+
flex: collapseCanvas ? 0 : 1,
|
|
1623
|
+
minHeight: 0,
|
|
1624
|
+
height: collapseCanvas ? 0 : undefined,
|
|
1625
|
+
overflow: 'hidden',
|
|
1626
|
+
display: 'flex',
|
|
1627
|
+
flexDirection: 'column',
|
|
1628
|
+
}}
|
|
1629
|
+
>
|
|
1537
1630
|
{/* Edge-label overlay — sits above the ReactFlow pane so clicks land. */}
|
|
1538
1631
|
{showEdgeLabels !== false && (
|
|
1539
1632
|
<div
|
|
@@ -1796,7 +1889,12 @@ function Inner({ components, relations, walkthroughs, onSelect, onEdgeSelect, me
|
|
|
1796
1889
|
</div>
|
|
1797
1890
|
)}
|
|
1798
1891
|
</div>
|
|
1799
|
-
<FileDrawer
|
|
1892
|
+
<FileDrawer
|
|
1893
|
+
title={drawerTitle}
|
|
1894
|
+
onClose={closeDrawer}
|
|
1895
|
+
fillHeight={drawerFillHeight}
|
|
1896
|
+
suppressEscape={fileOverlayOpen}
|
|
1897
|
+
>
|
|
1800
1898
|
{drawerTarget?.kind === 'walkthrough' &&
|
|
1801
1899
|
focusedWalkthrough &&
|
|
1802
1900
|
renderWalkthroughViewer ? (
|
|
@@ -1804,6 +1902,7 @@ function Inner({ components, relations, walkthroughs, onSelect, onEdgeSelect, me
|
|
|
1804
1902
|
render={renderWalkthroughDrawerContent}
|
|
1805
1903
|
walkthrough={focusedWalkthrough}
|
|
1806
1904
|
stepIndex={drawerTarget.stepIndex}
|
|
1905
|
+
onOpenFile={onOpenFileFromWalkthrough}
|
|
1807
1906
|
/>
|
|
1808
1907
|
) : drawerTarget?.kind === 'file' ? (
|
|
1809
1908
|
<FileDrawerContent
|
|
@@ -1813,6 +1912,53 @@ function Inner({ components, relations, walkthroughs, onSelect, onEdgeSelect, me
|
|
|
1813
1912
|
/>
|
|
1814
1913
|
) : null}
|
|
1815
1914
|
</FileDrawer>
|
|
1915
|
+
{fileOverlay && (
|
|
1916
|
+
<div
|
|
1917
|
+
style={{
|
|
1918
|
+
position: 'absolute',
|
|
1919
|
+
inset: 0,
|
|
1920
|
+
zIndex: 8,
|
|
1921
|
+
display: 'flex',
|
|
1922
|
+
flexDirection: 'column',
|
|
1923
|
+
background: theme.colors.background,
|
|
1924
|
+
borderLeft: `1px solid ${theme.colors.border}`,
|
|
1925
|
+
}}
|
|
1926
|
+
>
|
|
1927
|
+
<div
|
|
1928
|
+
style={{
|
|
1929
|
+
display: 'flex',
|
|
1930
|
+
alignItems: 'center',
|
|
1931
|
+
gap: 8,
|
|
1932
|
+
padding: '6px 10px',
|
|
1933
|
+
borderBottom: `1px solid ${theme.colors.border}`,
|
|
1934
|
+
flexShrink: 0,
|
|
1935
|
+
}}
|
|
1936
|
+
>
|
|
1937
|
+
<span
|
|
1938
|
+
title={fileOverlay.file}
|
|
1939
|
+
style={{
|
|
1940
|
+
flex: 1,
|
|
1941
|
+
minWidth: 0,
|
|
1942
|
+
overflow: 'hidden',
|
|
1943
|
+
textOverflow: 'ellipsis',
|
|
1944
|
+
whiteSpace: 'nowrap',
|
|
1945
|
+
fontFamily: theme.fonts.monospace,
|
|
1946
|
+
fontSize: theme.fontSizes[0],
|
|
1947
|
+
color: theme.colors.textMuted ?? theme.colors.textSecondary,
|
|
1948
|
+
}}
|
|
1949
|
+
>
|
|
1950
|
+
{fileOverlay.file}
|
|
1951
|
+
</span>
|
|
1952
|
+
<FileOverlayCloseButton onClose={closeFileOverlay} />
|
|
1953
|
+
</div>
|
|
1954
|
+
<div style={{ flex: 1, minHeight: 0, overflow: 'auto' }}>
|
|
1955
|
+
<FileDrawerContent
|
|
1956
|
+
render={renderDrawerContent}
|
|
1957
|
+
file={fileOverlay.file}
|
|
1958
|
+
/>
|
|
1959
|
+
</div>
|
|
1960
|
+
</div>
|
|
1961
|
+
)}
|
|
1816
1962
|
{/* Startup cover — hides measurement, layout swap, and camera settle. */}
|
|
1817
1963
|
<GraphLayoutCover revealed={layoutReady} />
|
|
1818
1964
|
{canvasOverlay}
|
|
@@ -58,6 +58,11 @@ export function extractDeclarationLine(content: string, lineNumber: number): str
|
|
|
58
58
|
|
|
59
59
|
/** Options when opening a file in the subsystem graph drawer. */
|
|
60
60
|
export interface SubsystemOpenFileOptions {
|
|
61
|
-
/** Scroll/highlight this 1-based line (declaration start). */
|
|
61
|
+
/** Scroll/highlight this 1-based line (declaration start or walkthrough site). */
|
|
62
62
|
startLine?: number;
|
|
63
|
+
/**
|
|
64
|
+
* When true with `startLine`, show the whole file scrolled to that line
|
|
65
|
+
* instead of a focused snippet window.
|
|
66
|
+
*/
|
|
67
|
+
fullFile?: boolean;
|
|
63
68
|
}
|