@principal-ai/subsystems-react 0.28.4 → 0.28.5

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.
@@ -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
- return Promise.reject(new Error(`No story fixture for ${path}`));
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
- ({ walkthrough, stepIndex, onOpenFile }: WalkthroughViewerContext) => (
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
+ };
@@ -78,6 +78,11 @@ export interface WalkthroughViewerContext {
78
78
  stepIndex: number | null;
79
79
  /** Open a step's full source file over the walkthrough drawer (keeps snippets mounted). */
80
80
  onOpenFile: (path: string, opts?: SubsystemOpenFileOptions) => void;
81
+ /**
82
+ * Aliases of components marked `proposed`. A step whose file can't be read
83
+ * but whose endpoint is proposed can be labelled as planned, not missing.
84
+ */
85
+ proposedAliases: ReadonlySet<string>;
81
86
  }
82
87
 
83
88
  type DrawerTarget =
@@ -355,13 +360,15 @@ const WalkthroughDrawerContent = memo(function WalkthroughDrawerContent({
355
360
  walkthrough,
356
361
  stepIndex,
357
362
  onOpenFile,
363
+ proposedAliases,
358
364
  }: {
359
365
  render: (ctx: WalkthroughViewerContext) => ReactNode;
360
366
  walkthrough: SubsystemWalkthrough;
361
367
  stepIndex: number | null;
362
368
  onOpenFile: (path: string, opts?: SubsystemOpenFileOptions) => void;
369
+ proposedAliases: ReadonlySet<string>;
363
370
  }) {
364
- return <>{render({ walkthrough, stepIndex, onOpenFile })}</>;
371
+ return <>{render({ walkthrough, stepIndex, onOpenFile, proposedAliases })}</>;
365
372
  });
366
373
 
367
374
  interface InnerProps extends SubsystemComponentGraphProps {
@@ -2152,6 +2159,7 @@ function Inner({ components, relations, walkthroughs, initialWalkthroughId, onRe
2152
2159
  walkthrough={focusedWalkthrough}
2153
2160
  stepIndex={drawerTarget.stepIndex}
2154
2161
  onOpenFile={onOpenFileFromWalkthrough}
2162
+ proposedAliases={proposedAliases}
2155
2163
  />
2156
2164
  ) : drawerTarget?.kind === 'file' ? (
2157
2165
  <FileDrawerContent