@principal-ai/subsystems-react 0.28.4 → 0.28.6
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/PierreFileView.d.ts.map +1 -1
- package/dist/pierre/PierreFileView.js +11 -1
- package/dist/pierre/PierreFileView.js.map +1 -1
- package/dist/pierre/PierreWalkthroughCodeView.d.ts +6 -1
- package/dist/pierre/PierreWalkthroughCodeView.d.ts.map +1 -1
- package/dist/pierre/PierreWalkthroughCodeView.js +83 -30
- package/dist/pierre/PierreWalkthroughCodeView.js.map +1 -1
- package/dist/pierre/fileAvailability.d.ts +17 -0
- package/dist/pierre/fileAvailability.d.ts.map +1 -0
- package/dist/pierre/fileAvailability.js +31 -0
- package/dist/pierre/fileAvailability.js.map +1 -0
- package/dist/subsystem/IssueList.d.ts +3 -0
- package/dist/subsystem/IssueList.d.ts.map +1 -1
- package/dist/subsystem/IssueList.js +22 -7
- package/dist/subsystem/IssueList.js.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.d.ts +5 -0
- package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.js +3 -3
- package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
- package/package.json +1 -1
- package/src/pierre/PierreFileView.tsx +22 -0
- package/src/pierre/PierreWalkthroughCodeView.tsx +125 -39
- package/src/pierre/fileAvailability.test.ts +40 -0
- package/src/pierre/fileAvailability.ts +37 -0
- package/src/stories/Subsystem/ComponentGraph/Flows.stories.tsx +131 -16
- package/src/subsystem/IssueList.tsx +38 -6
- package/src/subsystem/SubsystemComponentGraph.tsx +9 -1
|
@@ -33,11 +33,15 @@ type CodeViewLineSelection = {
|
|
|
33
33
|
/** Metadata carried on a walkthrough step's line annotation. */
|
|
34
34
|
type WalkthroughStepAnnotation = { text: string };
|
|
35
35
|
import { useTheme } from '@principal-ade/industry-theme';
|
|
36
|
-
import type {
|
|
36
|
+
import type {
|
|
37
|
+
SubsystemWalkthrough,
|
|
38
|
+
SubsystemWalkthroughStep,
|
|
39
|
+
} from '../subsystem/model';
|
|
37
40
|
import type { SubsystemOpenFileOptions } from '../subsystem/declarationRef';
|
|
38
41
|
import { buildPierreOptions, PIERRE_FILE_STYLE } from './pierreBackground';
|
|
39
42
|
import { pierreLangForPath } from './pierreFileLang';
|
|
40
43
|
import { resolvePierreSyntaxThemeName } from './pierreSyntaxTheme';
|
|
44
|
+
import { fileUnavailableNotice, isFileUnavailableError } from './fileAvailability';
|
|
41
45
|
import {
|
|
42
46
|
remapSnippetLineNumbers,
|
|
43
47
|
sliceSnippetWindow,
|
|
@@ -56,17 +60,52 @@ export interface PierreWalkthroughCodeViewProps {
|
|
|
56
60
|
background?: string;
|
|
57
61
|
/** Open the step's full source file (header button or double-click snippet body). */
|
|
58
62
|
onOpenFile?: (path: string, opts?: SubsystemOpenFileOptions) => void;
|
|
63
|
+
/**
|
|
64
|
+
* Aliases of components marked `proposed`. A step whose file can't be read
|
|
65
|
+
* but whose endpoint is proposed reads as "planned" rather than "missing".
|
|
66
|
+
*/
|
|
67
|
+
proposedAliases?: ReadonlySet<string>;
|
|
59
68
|
}
|
|
60
69
|
|
|
61
70
|
type FileLoadState =
|
|
62
71
|
| { status: 'loading' }
|
|
63
|
-
| {
|
|
64
|
-
|
|
72
|
+
| {
|
|
73
|
+
status: 'ready';
|
|
74
|
+
byPath: Map<string, string>;
|
|
75
|
+
/** Paths the host couldn't serve, keyed like `byPath`, with the reason. */
|
|
76
|
+
unavailable: Map<string, string>;
|
|
77
|
+
};
|
|
65
78
|
|
|
66
79
|
function stepItemId(walkthroughId: string, index: number): string {
|
|
67
80
|
return `${walkthroughId}:${index}`;
|
|
68
81
|
}
|
|
69
82
|
|
|
83
|
+
/** Stable key for a step's file within a repo (mirrors `pathsKey`). */
|
|
84
|
+
function siteKey(step: Pick<SubsystemWalkthroughStep, 'purl' | 'file'>): string {
|
|
85
|
+
return `${step.purl}\0${step.file}`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Notice shown in place of a snippet whose file the host couldn't read. */
|
|
89
|
+
function unavailableNotice(
|
|
90
|
+
step: SubsystemWalkthroughStep,
|
|
91
|
+
error: string,
|
|
92
|
+
proposed: boolean,
|
|
93
|
+
): string {
|
|
94
|
+
return isFileUnavailableError(error)
|
|
95
|
+
? fileUnavailableNotice(step.file, proposed)
|
|
96
|
+
: `Couldn't load ${step.file}: ${error}`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** A single-line placeholder snippet standing in for an unreadable file. */
|
|
100
|
+
function unavailableSlice(notice: string): SnippetSlice {
|
|
101
|
+
return {
|
|
102
|
+
contents: `// ${notice}`,
|
|
103
|
+
sliceStart: 1,
|
|
104
|
+
sliceEnd: 1,
|
|
105
|
+
focusOffset: 1,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
70
109
|
function OpenFileHeaderButton({
|
|
71
110
|
file,
|
|
72
111
|
line,
|
|
@@ -117,38 +156,58 @@ export function PierreWalkthroughCodeView({
|
|
|
117
156
|
contextLines = 8,
|
|
118
157
|
background,
|
|
119
158
|
onOpenFile,
|
|
159
|
+
proposedAliases,
|
|
120
160
|
}: PierreWalkthroughCodeViewProps) {
|
|
121
161
|
const { theme, mode } = useTheme();
|
|
122
162
|
const viewRef = useRef<CodeViewHandle<undefined>>(null);
|
|
123
163
|
const [load, setLoad] = useState<FileLoadState>({ status: 'loading' });
|
|
124
164
|
|
|
165
|
+
// A hop onto a proposed component is planned work; label its missing file
|
|
166
|
+
// accordingly instead of showing a bare "not found".
|
|
167
|
+
const isProposedStep = useCallback(
|
|
168
|
+
(step: SubsystemWalkthroughStep): boolean =>
|
|
169
|
+
proposedAliases != null &&
|
|
170
|
+
(proposedAliases.has(step.from) || proposedAliases.has(step.to)),
|
|
171
|
+
[proposedAliases],
|
|
172
|
+
);
|
|
173
|
+
|
|
125
174
|
const pathsKey = useMemo(() => {
|
|
126
|
-
const keys = [...new Set(walkthrough.steps.map((s) =>
|
|
175
|
+
const keys = [...new Set(walkthrough.steps.map((s) => siteKey(s)))];
|
|
127
176
|
keys.sort();
|
|
128
177
|
return keys.join('\0');
|
|
129
178
|
}, [walkthrough.steps]);
|
|
130
179
|
|
|
180
|
+
// Load each site independently: one unreadable step (a proposed seam whose
|
|
181
|
+
// file isn't implemented yet) must not blank the whole walkthrough. Failures
|
|
182
|
+
// are kept per-path and rendered as inline placeholders below.
|
|
131
183
|
useEffect(() => {
|
|
132
184
|
let cancelled = false;
|
|
133
185
|
setLoad({ status: 'loading' });
|
|
134
|
-
const sites = [...new Map(walkthrough.steps.map((s) => [
|
|
186
|
+
const sites = [...new Map(walkthrough.steps.map((s) => [siteKey(s), s])).values()];
|
|
135
187
|
void Promise.all(
|
|
136
188
|
sites.map(async (step) => {
|
|
137
|
-
const
|
|
138
|
-
|
|
189
|
+
const key = siteKey(step);
|
|
190
|
+
try {
|
|
191
|
+
const contents = await readFile(step.file, step.purl);
|
|
192
|
+
return { key, contents, error: null as string | null };
|
|
193
|
+
} catch (err) {
|
|
194
|
+
return {
|
|
195
|
+
key,
|
|
196
|
+
contents: null,
|
|
197
|
+
error: err instanceof Error ? err.message : 'Failed to read file',
|
|
198
|
+
};
|
|
199
|
+
}
|
|
139
200
|
}),
|
|
140
|
-
)
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
});
|
|
151
|
-
});
|
|
201
|
+
).then((results) => {
|
|
202
|
+
if (cancelled) return;
|
|
203
|
+
const byPath = new Map<string, string>();
|
|
204
|
+
const unavailable = new Map<string, string>();
|
|
205
|
+
for (const row of results) {
|
|
206
|
+
if (row.contents != null) byPath.set(row.key, row.contents);
|
|
207
|
+
else unavailable.set(row.key, row.error ?? 'Failed to read file');
|
|
208
|
+
}
|
|
209
|
+
setLoad({ status: 'ready', byPath, unavailable });
|
|
210
|
+
});
|
|
152
211
|
return () => {
|
|
153
212
|
cancelled = true;
|
|
154
213
|
};
|
|
@@ -157,7 +216,14 @@ export function PierreWalkthroughCodeView({
|
|
|
157
216
|
const slices = useMemo((): SnippetSlice[] => {
|
|
158
217
|
if (load.status !== 'ready') return [];
|
|
159
218
|
return walkthrough.steps.map((step) => {
|
|
160
|
-
const
|
|
219
|
+
const key = siteKey(step);
|
|
220
|
+
const failure = load.unavailable.get(key);
|
|
221
|
+
if (failure != null) {
|
|
222
|
+
return unavailableSlice(
|
|
223
|
+
unavailableNotice(step, failure, isProposedStep(step)),
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
const contents = load.byPath.get(key) ?? '';
|
|
161
227
|
return sliceSnippetWindow(
|
|
162
228
|
contents,
|
|
163
229
|
step.line,
|
|
@@ -166,17 +232,24 @@ export function PierreWalkthroughCodeView({
|
|
|
166
232
|
step.line,
|
|
167
233
|
);
|
|
168
234
|
});
|
|
169
|
-
}, [load, walkthrough.steps, contextLines]);
|
|
235
|
+
}, [load, walkthrough.steps, contextLines, isProposedStep]);
|
|
170
236
|
|
|
171
|
-
const items = useMemo((): CodeViewItem<WalkthroughStepAnnotation>[] => {
|
|
237
|
+
const items = useMemo((): CodeViewItem<WalkthroughStepAnnotation>[] => {
|
|
172
238
|
if (load.status !== 'ready' || slices.length === 0) return [];
|
|
173
239
|
return walkthrough.steps.map((step, index) => {
|
|
174
240
|
const slice = slices[index]!;
|
|
175
241
|
const focus = slice.focusOffset;
|
|
242
|
+
const key = siteKey(step);
|
|
243
|
+
const failure = load.unavailable.get(key);
|
|
244
|
+
// The placeholder line already carries the reason, so it replaces any
|
|
245
|
+
// authored annotation rather than stacking with it.
|
|
176
246
|
const annotations:
|
|
177
247
|
| LineAnnotation<WalkthroughStepAnnotation>[]
|
|
178
248
|
| undefined =
|
|
179
|
-
|
|
249
|
+
failure == null &&
|
|
250
|
+
step.annotation != null &&
|
|
251
|
+
step.annotation.length > 0 &&
|
|
252
|
+
focus != null
|
|
180
253
|
? [
|
|
181
254
|
{
|
|
182
255
|
lineNumber: focus,
|
|
@@ -193,11 +266,11 @@ const items = useMemo((): CodeViewItem<WalkthroughStepAnnotation>[] => {
|
|
|
193
266
|
name: step.file,
|
|
194
267
|
contents: slice.contents,
|
|
195
268
|
lang: pierreLangForPath(step.file),
|
|
196
|
-
cacheKey: `${walkthrough.id}:${index}:${step.file}:${step.line}:${slice.sliceStart}-${slice.sliceEnd}`,
|
|
269
|
+
cacheKey: `${walkthrough.id}:${index}:${step.file}:${step.line}:${slice.sliceStart}-${slice.sliceEnd}${failure != null ? ':unavailable' : ''}`,
|
|
197
270
|
},
|
|
198
271
|
};
|
|
199
272
|
});
|
|
200
|
-
}, [load, slices, walkthrough.id, walkthrough.steps]);
|
|
273
|
+
}, [load, slices, walkthrough.id, walkthrough.steps, isProposedStep]);
|
|
201
274
|
|
|
202
275
|
const selectedLines = useMemo((): CodeViewLineSelection | null => {
|
|
203
276
|
if (stepIndex == null || stepIndex < 0 || stepIndex >= slices.length) {
|
|
@@ -259,6 +332,10 @@ const items = useMemo((): CodeViewItem<WalkthroughStepAnnotation>[] => {
|
|
|
259
332
|
const index = Number.parseInt(item.id.split(':').pop() ?? '', 10);
|
|
260
333
|
const step = walkthrough.steps[index];
|
|
261
334
|
if (!step) return null;
|
|
335
|
+
// Unreadable sites have nothing to open — say so instead of offering a
|
|
336
|
+
// button that would only fail again in the full-file overlay.
|
|
337
|
+
const unavailable =
|
|
338
|
+
load.status === 'ready' && load.unavailable.has(siteKey(step));
|
|
262
339
|
return (
|
|
263
340
|
<span
|
|
264
341
|
style={{
|
|
@@ -271,18 +348,26 @@ const items = useMemo((): CodeViewItem<WalkthroughStepAnnotation>[] => {
|
|
|
271
348
|
color: theme.colors.textSecondary,
|
|
272
349
|
}}
|
|
273
350
|
>
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
351
|
+
{unavailable ? (
|
|
352
|
+
<span title="This file isn't in the local checkout">
|
|
353
|
+
not in checkout
|
|
354
|
+
</span>
|
|
355
|
+
) : (
|
|
356
|
+
<>
|
|
357
|
+
L{step.line}
|
|
358
|
+
{onOpenFile && (
|
|
359
|
+
<OpenFileHeaderButton
|
|
360
|
+
file={step.file}
|
|
361
|
+
line={step.line}
|
|
362
|
+
onOpenFile={onOpenFile}
|
|
363
|
+
/>
|
|
364
|
+
)}
|
|
365
|
+
</>
|
|
281
366
|
)}
|
|
282
367
|
</span>
|
|
283
368
|
);
|
|
284
369
|
};
|
|
285
|
-
}, [walkthrough.steps, theme, onOpenFile]);
|
|
370
|
+
}, [walkthrough.steps, theme, onOpenFile, load]);
|
|
286
371
|
|
|
287
372
|
const renderAnnotation = useMemo(() => {
|
|
288
373
|
return (
|
|
@@ -324,9 +409,10 @@ const items = useMemo((): CodeViewItem<WalkthroughStepAnnotation>[] => {
|
|
|
324
409
|
const index = Number.parseInt(id.split(':').pop() ?? '', 10);
|
|
325
410
|
const step = walkthrough.steps[index];
|
|
326
411
|
if (!step) return;
|
|
412
|
+
if (load.status === 'ready' && load.unavailable.has(siteKey(step))) return;
|
|
327
413
|
onOpenFile(step.file, { startLine: step.line, fullFile: true });
|
|
328
414
|
},
|
|
329
|
-
[onOpenFile, walkthrough.steps],
|
|
415
|
+
[onOpenFile, walkthrough.steps, load],
|
|
330
416
|
);
|
|
331
417
|
|
|
332
418
|
const options = useMemo((): CodeViewReactOptions => {
|
|
@@ -369,17 +455,17 @@ const items = useMemo((): CodeViewItem<WalkthroughStepAnnotation>[] => {
|
|
|
369
455
|
items.length,
|
|
370
456
|
]);
|
|
371
457
|
|
|
372
|
-
if (load.status === '
|
|
458
|
+
if (load.status === 'loading') {
|
|
373
459
|
return (
|
|
374
|
-
<div style={{ padding: 16, color: theme.colors.
|
|
375
|
-
|
|
460
|
+
<div style={{ padding: 16, color: theme.colors.textSecondary }}>
|
|
461
|
+
Loading…
|
|
376
462
|
</div>
|
|
377
463
|
);
|
|
378
464
|
}
|
|
379
|
-
if (
|
|
465
|
+
if (items.length === 0) {
|
|
380
466
|
return (
|
|
381
467
|
<div style={{ padding: 16, color: theme.colors.textSecondary }}>
|
|
382
|
-
|
|
468
|
+
No steps in this walkthrough.
|
|
383
469
|
</div>
|
|
384
470
|
);
|
|
385
471
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { fileUnavailableNotice, isFileUnavailableError } from './fileAvailability';
|
|
3
|
+
|
|
4
|
+
describe('isFileUnavailableError', () => {
|
|
5
|
+
test('matches the host "no local checkout" family', () => {
|
|
6
|
+
expect(
|
|
7
|
+
isFileUnavailableError(
|
|
8
|
+
'file not found in graph repos: packages/subsystems-studio/src/bun/subsystem-model-runs.ts',
|
|
9
|
+
),
|
|
10
|
+
).toBe(true);
|
|
11
|
+
expect(isFileUnavailableError('graph has no local root for this file')).toBe(
|
|
12
|
+
true,
|
|
13
|
+
);
|
|
14
|
+
expect(isFileUnavailableError('no local checkout for this repo')).toBe(true);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('leaves genuine I/O errors alone', () => {
|
|
18
|
+
expect(isFileUnavailableError('EACCES: permission denied')).toBe(false);
|
|
19
|
+
expect(isFileUnavailableError('')).toBe(false);
|
|
20
|
+
expect(isFileUnavailableError(null)).toBe(false);
|
|
21
|
+
expect(isFileUnavailableError(undefined)).toBe(false);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('fileUnavailableNotice', () => {
|
|
26
|
+
test('names proposed work as planned, not missing', () => {
|
|
27
|
+
expect(
|
|
28
|
+
fileUnavailableNotice('src/a.ts', true),
|
|
29
|
+
).toBe("Proposed — src/a.ts isn't in the local checkout yet.");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('falls back to a plain not-in-checkout notice', () => {
|
|
33
|
+
expect(fileUnavailableNotice('src/a.ts')).toBe(
|
|
34
|
+
'Not in the local checkout: src/a.ts',
|
|
35
|
+
);
|
|
36
|
+
expect(fileUnavailableNotice('src/a.ts', false)).toBe(
|
|
37
|
+
'Not in the local checkout: src/a.ts',
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host file reads fail with a small, stable set of messages when a path can't
|
|
3
|
+
* be served from any local checkout — most often a *proposed* seam whose file
|
|
4
|
+
* isn't implemented yet. These helpers detect that class of failure so the
|
|
5
|
+
* Pierre views can show a proposed / not-yet-available notice instead of a raw
|
|
6
|
+
* host error.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** Substrings the host uses when a read can't resolve to a local file. */
|
|
10
|
+
const UNAVAILABLE_FRAGMENTS = [
|
|
11
|
+
'not found in graph repos',
|
|
12
|
+
'graph has no local root',
|
|
13
|
+
'no local checkout',
|
|
14
|
+
] as const;
|
|
15
|
+
|
|
16
|
+
/** True when a read failed because the file has no local checkout, not because
|
|
17
|
+
* of an I/O or permission error. */
|
|
18
|
+
export function isFileUnavailableError(
|
|
19
|
+
message: string | null | undefined,
|
|
20
|
+
): boolean {
|
|
21
|
+
if (!message) return false;
|
|
22
|
+
return UNAVAILABLE_FRAGMENTS.some((fragment) => message.includes(fragment));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* One-line notice shown in place of a snippet whose file can't be read. When
|
|
27
|
+
* the step touches a proposed component the wording names that, so a proposed
|
|
28
|
+
* seam reads as "planned" rather than "missing".
|
|
29
|
+
*/
|
|
30
|
+
export function fileUnavailableNotice(
|
|
31
|
+
path: string,
|
|
32
|
+
proposed?: boolean,
|
|
33
|
+
): string {
|
|
34
|
+
return proposed
|
|
35
|
+
? `Proposed — ${path} isn't in the local checkout yet.`
|
|
36
|
+
: `Not in the local checkout: ${path}`;
|
|
37
|
+
}
|
|
@@ -54,7 +54,11 @@ const storyFiles: Record<string, string> = {
|
|
|
54
54
|
function readStoryFile(path: string): Promise<string> {
|
|
55
55
|
const content = storyFiles[path];
|
|
56
56
|
if (content == null) {
|
|
57
|
-
|
|
57
|
+
// Mirror the host's wording so the Pierre views exercise their
|
|
58
|
+
// "not in the local checkout" handling, not just any rejection.
|
|
59
|
+
return Promise.reject(
|
|
60
|
+
new Error(`file not found in graph repos: ${path}`),
|
|
61
|
+
);
|
|
58
62
|
}
|
|
59
63
|
return Promise.resolve(content);
|
|
60
64
|
}
|
|
@@ -122,50 +126,59 @@ const drawingComponents: SubsystemComponent[] = [
|
|
|
122
126
|
},
|
|
123
127
|
];
|
|
124
128
|
|
|
129
|
+
const STORY_PURL = 'pkg:github/principal-ai/desktop-app';
|
|
130
|
+
const stepPurl = (file: string) => `${STORY_PURL}#${file}`;
|
|
131
|
+
|
|
125
132
|
const drawingWalkthroughs: SubsystemWalkthrough[] = [
|
|
126
133
|
{
|
|
127
134
|
id: 'tl-open-drawing',
|
|
128
135
|
title: 'Open drawing',
|
|
129
136
|
steps: [
|
|
130
|
-
{ from: 'panel', to: 'storage', mechanism: 'calls', file: 'src/panels/DrawingsLeftPanel.tsx', line: 56, symbol: 'DrawingsLeftPanel.scan', annotation: 'Lists the drawings directory; each row feeds an open intent.' },
|
|
131
|
-
{ from: 'panel', to: 'host', mechanism: 'produces', file: 'src/panels/DrawingsLeftPanel.tsx', line: 85, symbol: 'DrawingsLeftPanel.openDrawing', annotation: 'Emits the open intent up to the host.' },
|
|
132
|
-
{ from: 'host', to: 'storage', mechanism: 'calls', file: 'src/hooks/useDrawingsHost.ts', line: 45, symbol: 'useDrawingsHost.openDrawing', annotation: 'Host resolves the path and delegates to storage.' },
|
|
133
|
-
{ from: 'host', to: 'shell', mechanism: 'produces', file: 'src/workspace/WorkspaceShell.tsx', line: 369, symbol: 'WorkspaceShell.openTab', annotation: 'Asks the shell to surface the drawing as a tab.' },
|
|
134
|
-
{ from: 'shell', to: 'tab', mechanism: 'feeds', file: 'src/workspace/WorkspaceShell.tsx', line: 372, symbol: 'WorkspaceShell.mountTab' },
|
|
135
|
-
{ from: 'storage', to: 'fs', mechanism: 'calls', file: 'src/storage/drawingsStorage.ts', line: 90, symbol: 'DrawingsStorage.read', annotation: 'Reads the file through the sandboxed fs service.' },
|
|
136
|
-
{ from: 'tab', to: 'fs', mechanism: 'calls', file: 'src/components/DrawingTabContent.tsx', line: 83, symbol: 'DrawingTabContent.load' },
|
|
137
|
+
{ from: 'panel', to: 'storage', mechanism: 'calls', file: 'src/panels/DrawingsLeftPanel.tsx', line: 56, purl: stepPurl('src/panels/DrawingsLeftPanel.tsx'), symbol: 'DrawingsLeftPanel.scan', annotation: 'Lists the drawings directory; each row feeds an open intent.' },
|
|
138
|
+
{ from: 'panel', to: 'host', mechanism: 'produces', file: 'src/panels/DrawingsLeftPanel.tsx', line: 85, purl: stepPurl('src/panels/DrawingsLeftPanel.tsx'), symbol: 'DrawingsLeftPanel.openDrawing', annotation: 'Emits the open intent up to the host.' },
|
|
139
|
+
{ from: 'host', to: 'storage', mechanism: 'calls', file: 'src/hooks/useDrawingsHost.ts', line: 45, purl: stepPurl('src/hooks/useDrawingsHost.ts'), symbol: 'useDrawingsHost.openDrawing', annotation: 'Host resolves the path and delegates to storage.' },
|
|
140
|
+
{ from: 'host', to: 'shell', mechanism: 'produces', file: 'src/workspace/WorkspaceShell.tsx', line: 369, purl: stepPurl('src/workspace/WorkspaceShell.tsx'), symbol: 'WorkspaceShell.openTab', annotation: 'Asks the shell to surface the drawing as a tab.' },
|
|
141
|
+
{ from: 'shell', to: 'tab', mechanism: 'feeds', file: 'src/workspace/WorkspaceShell.tsx', line: 372, purl: stepPurl('src/workspace/WorkspaceShell.tsx'), symbol: 'WorkspaceShell.mountTab' },
|
|
142
|
+
{ from: 'storage', to: 'fs', mechanism: 'calls', file: 'src/storage/drawingsStorage.ts', line: 90, purl: stepPurl('src/storage/drawingsStorage.ts'), symbol: 'DrawingsStorage.read', annotation: 'Reads the file through the sandboxed fs service.' },
|
|
143
|
+
{ from: 'tab', to: 'fs', mechanism: 'calls', file: 'src/components/DrawingTabContent.tsx', line: 83, purl: stepPurl('src/components/DrawingTabContent.tsx'), symbol: 'DrawingTabContent.load' },
|
|
137
144
|
],
|
|
138
145
|
},
|
|
139
146
|
{
|
|
140
147
|
id: 'tl-save-drawing',
|
|
141
148
|
title: 'Save drawing',
|
|
142
149
|
steps: [
|
|
143
|
-
{ from: 'tab', to: 'fs', mechanism: 'calls', file: 'src/components/DrawingTabContent.tsx', line: 112, symbol: 'DrawingTabContent.save' },
|
|
144
|
-
{ from: 'tab', to: 'host', mechanism: 'produces', file: 'src/components/DrawingTabContent.tsx', line: 121, symbol: 'DrawingTabContent.emitSaved' },
|
|
145
|
-
{ from: 'host', to: 'panel', mechanism: 'produces', file: 'src/hooks/useDrawingsHost.ts', line: 64, symbol: 'useDrawingsHost.onSaved' },
|
|
146
|
-
{ from: 'panel', to: 'storage', mechanism: 'calls', file: 'src/panels/DrawingsLeftPanel.tsx', line: 56, symbol: 'DrawingsLeftPanel.scan' },
|
|
150
|
+
{ from: 'tab', to: 'fs', mechanism: 'calls', file: 'src/components/DrawingTabContent.tsx', line: 112, purl: stepPurl('src/components/DrawingTabContent.tsx'), symbol: 'DrawingTabContent.save' },
|
|
151
|
+
{ from: 'tab', to: 'host', mechanism: 'produces', file: 'src/components/DrawingTabContent.tsx', line: 121, purl: stepPurl('src/components/DrawingTabContent.tsx'), symbol: 'DrawingTabContent.emitSaved' },
|
|
152
|
+
{ from: 'host', to: 'panel', mechanism: 'produces', file: 'src/hooks/useDrawingsHost.ts', line: 64, purl: stepPurl('src/hooks/useDrawingsHost.ts'), symbol: 'useDrawingsHost.onSaved' },
|
|
153
|
+
{ from: 'panel', to: 'storage', mechanism: 'calls', file: 'src/panels/DrawingsLeftPanel.tsx', line: 56, purl: stepPurl('src/panels/DrawingsLeftPanel.tsx'), symbol: 'DrawingsLeftPanel.scan' },
|
|
147
154
|
],
|
|
148
155
|
},
|
|
149
156
|
{
|
|
150
157
|
id: 'tl-delete-drawing',
|
|
151
158
|
title: 'Delete drawing',
|
|
152
159
|
steps: [
|
|
153
|
-
{ from: 'host', to: 'fs', mechanism: 'calls', file: 'src/hooks/useDrawingsHost.ts', line: 64, symbol: 'useDrawingsHost.deleteDrawing' },
|
|
154
|
-
{ from: 'host', to: 'panel', mechanism: 'produces', file: 'src/hooks/useDrawingsHost.ts', line: 66, symbol: 'useDrawingsHost.refreshList' },
|
|
155
|
-
{ from: 'panel', to: 'storage', mechanism: 'calls', file: 'src/panels/DrawingsLeftPanel.tsx', line: 56, symbol: 'DrawingsLeftPanel.scan' },
|
|
160
|
+
{ from: 'host', to: 'fs', mechanism: 'calls', file: 'src/hooks/useDrawingsHost.ts', line: 64, purl: stepPurl('src/hooks/useDrawingsHost.ts'), symbol: 'useDrawingsHost.deleteDrawing' },
|
|
161
|
+
{ from: 'host', to: 'panel', mechanism: 'produces', file: 'src/hooks/useDrawingsHost.ts', line: 66, purl: stepPurl('src/hooks/useDrawingsHost.ts'), symbol: 'useDrawingsHost.refreshList' },
|
|
162
|
+
{ from: 'panel', to: 'storage', mechanism: 'calls', file: 'src/panels/DrawingsLeftPanel.tsx', line: 56, purl: stepPurl('src/panels/DrawingsLeftPanel.tsx'), symbol: 'DrawingsLeftPanel.scan' },
|
|
156
163
|
],
|
|
157
164
|
},
|
|
158
165
|
];
|
|
159
166
|
|
|
160
167
|
function FlowsDemo() {
|
|
161
168
|
const renderWalkthroughViewer = useCallback(
|
|
162
|
-
({
|
|
169
|
+
({
|
|
170
|
+
walkthrough,
|
|
171
|
+
stepIndex,
|
|
172
|
+
onOpenFile,
|
|
173
|
+
proposedAliases,
|
|
174
|
+
}: WalkthroughViewerContext) => (
|
|
163
175
|
<PierreWalkthroughCodeView
|
|
164
176
|
walkthrough={walkthrough}
|
|
165
177
|
stepIndex={stepIndex}
|
|
166
178
|
readFile={readStoryFile}
|
|
167
179
|
contextLines={4}
|
|
168
180
|
onOpenFile={onOpenFile}
|
|
181
|
+
proposedAliases={proposedAliases}
|
|
169
182
|
/>
|
|
170
183
|
),
|
|
171
184
|
[],
|
|
@@ -204,3 +217,105 @@ function FlowsDemo() {
|
|
|
204
217
|
export const ThreeFlows: Story = {
|
|
205
218
|
render: () => <FlowsDemo />,
|
|
206
219
|
};
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* The proposed-seam failure mode: a walkthrough hop whose component is
|
|
223
|
+
* `proposed` points at a file that isn't in the checkout yet (no story
|
|
224
|
+
* fixture). The hop must render an inline "Proposed — … isn't in the local
|
|
225
|
+
* checkout yet." placeholder with no Open-file affordance, while the live hops
|
|
226
|
+
* around it still render their snippets — one bad step must not blank the flow.
|
|
227
|
+
*/
|
|
228
|
+
const proposedComponents: SubsystemComponent[] = [
|
|
229
|
+
...drawingComponents,
|
|
230
|
+
{
|
|
231
|
+
alias: 'runs',
|
|
232
|
+
name: 'SubsystemModelRunStore',
|
|
233
|
+
construct: 'store',
|
|
234
|
+
file: 'src/runs/subsystemModelRuns.ts',
|
|
235
|
+
purl: 'pkg:github/principal-ai/desktop-app',
|
|
236
|
+
symbol: 'SubsystemModelRunStore',
|
|
237
|
+
purpose: 'proposed store associating maintain runs with a model',
|
|
238
|
+
proposed: true,
|
|
239
|
+
process: 'draw-host',
|
|
240
|
+
},
|
|
241
|
+
];
|
|
242
|
+
|
|
243
|
+
const proposedWalkthroughs: SubsystemWalkthrough[] = [
|
|
244
|
+
{
|
|
245
|
+
id: 'tl-associate-run',
|
|
246
|
+
title: 'Associate a finished run with its model (proposed)',
|
|
247
|
+
steps: [
|
|
248
|
+
{
|
|
249
|
+
from: 'host',
|
|
250
|
+
to: 'storage',
|
|
251
|
+
mechanism: 'calls',
|
|
252
|
+
file: 'src/hooks/useDrawingsHost.ts',
|
|
253
|
+
line: 45,
|
|
254
|
+
purl: stepPurl('src/hooks/useDrawingsHost.ts'),
|
|
255
|
+
symbol: 'useDrawingsHost.openDrawing',
|
|
256
|
+
annotation: 'Live hop — its snippet still renders.',
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
from: 'storage',
|
|
260
|
+
to: 'runs',
|
|
261
|
+
mechanism: 'writes',
|
|
262
|
+
file: 'src/runs/subsystemModelRuns.ts',
|
|
263
|
+
line: 1,
|
|
264
|
+
purl: stepPurl('src/runs/subsystemModelRuns.ts'),
|
|
265
|
+
symbol: 'SubsystemModelRunStore.write',
|
|
266
|
+
annotation: 'Proposed seam — the file is not in the checkout yet.',
|
|
267
|
+
},
|
|
268
|
+
],
|
|
269
|
+
},
|
|
270
|
+
];
|
|
271
|
+
|
|
272
|
+
function ProposedMissingStepDemo() {
|
|
273
|
+
const renderWalkthroughViewer = useCallback(
|
|
274
|
+
({
|
|
275
|
+
walkthrough,
|
|
276
|
+
stepIndex,
|
|
277
|
+
onOpenFile,
|
|
278
|
+
proposedAliases,
|
|
279
|
+
}: WalkthroughViewerContext) => (
|
|
280
|
+
<PierreWalkthroughCodeView
|
|
281
|
+
walkthrough={walkthrough}
|
|
282
|
+
stepIndex={stepIndex}
|
|
283
|
+
readFile={readStoryFile}
|
|
284
|
+
contextLines={4}
|
|
285
|
+
onOpenFile={onOpenFile}
|
|
286
|
+
proposedAliases={proposedAliases}
|
|
287
|
+
/>
|
|
288
|
+
),
|
|
289
|
+
[],
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
return (
|
|
293
|
+
<div style={{ width: '100%', height: '100vh', display: 'flex', flexDirection: 'column' }}>
|
|
294
|
+
<SubsystemComponentGraph
|
|
295
|
+
components={proposedComponents}
|
|
296
|
+
relations={[]}
|
|
297
|
+
walkthroughs={proposedWalkthroughs}
|
|
298
|
+
title="proposed seam with a missing file"
|
|
299
|
+
description="Expand **Associate a finished run with its model** and click either step. The proposed hop has no file in the checkout, so its snippet shows an inline *Proposed — … isn't in the local checkout yet.* placeholder instead of failing the whole flow."
|
|
300
|
+
renderWalkthroughViewer={renderWalkthroughViewer}
|
|
301
|
+
renderFileViewer={(file) => (
|
|
302
|
+
<div
|
|
303
|
+
style={{
|
|
304
|
+
padding: 12,
|
|
305
|
+
fontFamily: 'monospace',
|
|
306
|
+
fontSize: 12,
|
|
307
|
+
color: '#bbb',
|
|
308
|
+
whiteSpace: 'pre',
|
|
309
|
+
}}
|
|
310
|
+
>
|
|
311
|
+
{`// ${file}\n …`}
|
|
312
|
+
</div>
|
|
313
|
+
)}
|
|
314
|
+
/>
|
|
315
|
+
</div>
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export const ProposedMissingStep: Story = {
|
|
320
|
+
render: () => <ProposedMissingStepDemo />,
|
|
321
|
+
};
|
|
@@ -12,10 +12,16 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { useState } from 'react';
|
|
15
|
+
import type { LucideIcon } from 'lucide-react';
|
|
15
16
|
import {
|
|
16
17
|
ChevronDown,
|
|
17
18
|
ChevronRight,
|
|
18
19
|
CircleCheck,
|
|
20
|
+
Component,
|
|
21
|
+
FolderGit2,
|
|
22
|
+
Network,
|
|
23
|
+
Route,
|
|
24
|
+
Server,
|
|
19
25
|
Wrench,
|
|
20
26
|
} from 'lucide-react';
|
|
21
27
|
import { useTheme } from '@principal-ade/industry-theme';
|
|
@@ -50,6 +56,18 @@ export const SUBSYSTEM_ISSUE_CATEGORIES: SubsystemIssueCategory[] = [
|
|
|
50
56
|
'walkthrough',
|
|
51
57
|
];
|
|
52
58
|
|
|
59
|
+
/** Per-category header icon — mirrors the Maintainer tab's lane icons. */
|
|
60
|
+
export const SUBSYSTEM_ISSUE_CATEGORY_ICON: Record<
|
|
61
|
+
SubsystemIssueCategory,
|
|
62
|
+
LucideIcon
|
|
63
|
+
> = {
|
|
64
|
+
repo: FolderGit2,
|
|
65
|
+
construct: Component,
|
|
66
|
+
'static-topology': Network,
|
|
67
|
+
'runtime-topology': Server,
|
|
68
|
+
walkthrough: Route,
|
|
69
|
+
};
|
|
70
|
+
|
|
53
71
|
export const SUBSYSTEM_ISSUE_CATEGORY_LABEL: Record<
|
|
54
72
|
SubsystemIssueCategory,
|
|
55
73
|
string
|
|
@@ -306,11 +324,11 @@ export function groupIssuesByCategory(
|
|
|
306
324
|
|
|
307
325
|
function severityColor(
|
|
308
326
|
severity: SubsystemIssueSeverity,
|
|
309
|
-
colors: { error: string; info: string },
|
|
310
|
-
muted: string,
|
|
327
|
+
colors: { error: string; info: string; warning: string },
|
|
311
328
|
): string {
|
|
312
329
|
if (severity === 'error') return colors.error ?? '#e5534b';
|
|
313
|
-
|
|
330
|
+
// `info` severity = unconfirmed findings; amber, matching the lane icons.
|
|
331
|
+
return colors.warning ?? '#d4a017';
|
|
314
332
|
}
|
|
315
333
|
|
|
316
334
|
const TARGET_KIND_LABEL: Record<SubsystemIssueTargetKind, string> = {
|
|
@@ -342,7 +360,7 @@ export function SubsystemIssueCard({
|
|
|
342
360
|
const [expanded, setExpanded] = useState(false);
|
|
343
361
|
const showDetail = expanded;
|
|
344
362
|
const muted = theme.colors.textMuted ?? theme.colors.textSecondary;
|
|
345
|
-
const color = severityColor(issue.severity, theme.colors
|
|
363
|
+
const color = severityColor(issue.severity, theme.colors);
|
|
346
364
|
const activate = () => {
|
|
347
365
|
setExpanded((v) => !v);
|
|
348
366
|
onSelect?.(issue);
|
|
@@ -513,7 +531,7 @@ export function SubsystemIssueList({
|
|
|
513
531
|
// the header, no count, no cards.
|
|
514
532
|
const clean = category.count === 0;
|
|
515
533
|
const accent = category.severity
|
|
516
|
-
? severityColor(category.severity, theme.colors
|
|
534
|
+
? severityColor(category.severity, theme.colors)
|
|
517
535
|
: theme.colors.success;
|
|
518
536
|
const isCollapsed = collapsed.has(category.category);
|
|
519
537
|
const isHovered = hovered === category.category;
|
|
@@ -550,6 +568,20 @@ export function SubsystemIssueList({
|
|
|
550
568
|
fontFamily: theme.fonts.body,
|
|
551
569
|
}}
|
|
552
570
|
>
|
|
571
|
+
{(() => {
|
|
572
|
+
const Icon = SUBSYSTEM_ISSUE_CATEGORY_ICON[category.category];
|
|
573
|
+
if (Icon) {
|
|
574
|
+
return (
|
|
575
|
+
<Icon
|
|
576
|
+
size={13}
|
|
577
|
+
color={accent}
|
|
578
|
+
style={{ flexShrink: 0 }}
|
|
579
|
+
aria-hidden="true"
|
|
580
|
+
/>
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
return null;
|
|
584
|
+
})()}
|
|
553
585
|
<span
|
|
554
586
|
style={{
|
|
555
587
|
flex: 1,
|
|
@@ -573,7 +605,7 @@ export function SubsystemIssueList({
|
|
|
573
605
|
fontFamily: theme.fonts.monospace,
|
|
574
606
|
fontSize: theme.fontSizes[0],
|
|
575
607
|
fontWeight: 600,
|
|
576
|
-
color:
|
|
608
|
+
color: muted,
|
|
577
609
|
flexShrink: 0,
|
|
578
610
|
}}
|
|
579
611
|
>
|