@principal-ai/subsystems-react 0.36.2 → 0.37.1

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.
Files changed (47) hide show
  1. package/dist/index.d.ts +1 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +1 -0
  4. package/dist/index.js.map +1 -1
  5. package/dist/pierre/PierreWalkthroughCodeView.d.ts +19 -1
  6. package/dist/pierre/PierreWalkthroughCodeView.d.ts.map +1 -1
  7. package/dist/pierre/PierreWalkthroughCodeView.js +53 -2
  8. package/dist/pierre/PierreWalkthroughCodeView.js.map +1 -1
  9. package/dist/subsystem/ComponentDeclaration.d.ts +13 -1
  10. package/dist/subsystem/ComponentDeclaration.d.ts.map +1 -1
  11. package/dist/subsystem/ComponentDeclaration.js +58 -7
  12. package/dist/subsystem/ComponentDeclaration.js.map +1 -1
  13. package/dist/subsystem/ConstructsCatalog.d.ts +6 -3
  14. package/dist/subsystem/ConstructsCatalog.d.ts.map +1 -1
  15. package/dist/subsystem/ConstructsCatalog.js +206 -217
  16. package/dist/subsystem/ConstructsCatalog.js.map +1 -1
  17. package/dist/subsystem/SubsystemComponentGraph.d.ts +10 -0
  18. package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
  19. package/dist/subsystem/SubsystemComponentGraph.js +108 -31
  20. package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
  21. package/dist/subsystem/SymbolInspectionCard.d.ts.map +1 -1
  22. package/dist/subsystem/SymbolInspectionCard.js +1 -1
  23. package/dist/subsystem/SymbolInspectionCard.js.map +1 -1
  24. package/dist/subsystem/model.d.ts.map +1 -1
  25. package/dist/subsystem/model.js +7 -1
  26. package/dist/subsystem/model.js.map +1 -1
  27. package/dist/utils/edgeLabel.d.ts +51 -0
  28. package/dist/utils/edgeLabel.d.ts.map +1 -0
  29. package/dist/utils/edgeLabel.js +81 -0
  30. package/dist/utils/edgeLabel.js.map +1 -0
  31. package/dist/utils/elkLayout.d.ts +6 -0
  32. package/dist/utils/elkLayout.d.ts.map +1 -1
  33. package/dist/utils/elkLayout.js +27 -6
  34. package/dist/utils/elkLayout.js.map +1 -1
  35. package/package.json +3 -2
  36. package/src/index.ts +7 -0
  37. package/src/pierre/PierreWalkthroughCodeView.tsx +75 -1
  38. package/src/stories/Subsystem/ComponentGraph/Flows.stories.tsx +222 -0
  39. package/src/stories/Subsystem/ConstructsCatalog.stories.tsx +443 -0
  40. package/src/subsystem/ComponentDeclaration.tsx +100 -7
  41. package/src/subsystem/ConstructsCatalog.tsx +291 -323
  42. package/src/subsystem/SubsystemComponentGraph.tsx +173 -30
  43. package/src/subsystem/SymbolInspectionCard.tsx +10 -9
  44. package/src/subsystem/model.ts +7 -1
  45. package/src/utils/edgeLabel.test.ts +23 -0
  46. package/src/utils/edgeLabel.ts +95 -0
  47. package/src/utils/elkLayout.ts +41 -6
@@ -23,6 +23,7 @@ import {
23
23
  type DiffLineAnnotation,
24
24
  type LineAnnotation,
25
25
  } from '@pierre/diffs/react';
26
+ import type { TokenEventBase } from '@pierre/diffs';
26
27
  import { Maximize2 } from 'lucide-react';
27
28
 
28
29
  /** Mirrors Pierre's CodeViewLineSelection (not re-exported from the React entry). */
@@ -48,6 +49,33 @@ import {
48
49
  type SnippetSlice,
49
50
  } from './sliceSnippet';
50
51
 
52
+ /**
53
+ * A token the user clicked or hovered inside a step's snippet. `stepIndex` is
54
+ * the zero-based index of the walkthrough step whose snippet it sits in, so a
55
+ * host can resolve the token against that step's construct endpoints.
56
+ */
57
+ export interface WalkthroughSymbolQuery {
58
+ stepIndex: number;
59
+ tokenText: string;
60
+ }
61
+
62
+ /** Ctx item id → step index, for CodeView's `walkthroughId:index` item ids. */
63
+ function stepIndexFromItemContext(context: unknown): number | null {
64
+ const id = (context as { item?: { id?: string } } | undefined)?.item?.id;
65
+ if (id == null) return null;
66
+ const index = Number.parseInt(id.split(':').pop() ?? '', 10);
67
+ return Number.isFinite(index) ? index : null;
68
+ }
69
+
70
+ /** Pointer/dotted-underline affordance for a clickable construct token. */
71
+ function paintSymbolToken(el: HTMLElement | undefined, active: boolean): void {
72
+ if (el == null) return;
73
+ el.style.cursor = active ? 'pointer' : '';
74
+ el.style.textDecorationLine = active ? 'underline' : '';
75
+ el.style.textDecorationStyle = active ? 'dotted' : '';
76
+ el.style.textUnderlineOffset = active ? '2px' : '';
77
+ }
78
+
51
79
  export interface PierreWalkthroughCodeViewProps {
52
80
  walkthrough: SubsystemWalkthrough;
53
81
  /** Focused step; `null` shows all snippets without scrolling to a step. */
@@ -65,6 +93,15 @@ export interface PierreWalkthroughCodeViewProps {
65
93
  * but whose endpoint is proposed reads as "planned" rather than "missing".
66
94
  */
67
95
  proposedAliases?: ReadonlySet<string>;
96
+ /**
97
+ * Resolve a token to a model construct key (e.g. a component alias) for the
98
+ * step its snippet belongs to. Return `null` when the token doesn't name a
99
+ * construct this flow touches. When both this and `onSymbolClick` are set,
100
+ * matching tokens read as clickable (pointer + dotted underline).
101
+ */
102
+ resolveSymbol?: (query: WalkthroughSymbolQuery) => string | null;
103
+ /** Fired when a token resolved by `resolveSymbol` is clicked. */
104
+ onSymbolClick?: (symbol: string, query: WalkthroughSymbolQuery) => void;
68
105
  }
69
106
 
70
107
  type FileLoadState =
@@ -157,11 +194,20 @@ export function PierreWalkthroughCodeView({
157
194
  background,
158
195
  onOpenFile,
159
196
  proposedAliases,
197
+ resolveSymbol,
198
+ onSymbolClick,
160
199
  }: PierreWalkthroughCodeViewProps) {
161
200
  const { theme, mode } = useTheme();
162
201
  const viewRef = useRef<CodeViewHandle<undefined>>(null);
163
202
  const [load, setLoad] = useState<FileLoadState>({ status: 'loading' });
164
203
 
204
+ // Keep the newest symbol callbacks in refs so the Pierre options object stays
205
+ // stable (host callbacks are recreated per render).
206
+ const resolveSymbolRef = useRef(resolveSymbol);
207
+ resolveSymbolRef.current = resolveSymbol;
208
+ const onSymbolClickRef = useRef(onSymbolClick);
209
+ onSymbolClickRef.current = onSymbolClick;
210
+
165
211
  // A hop onto a proposed component is planned work; label its missing file
166
212
  // accordingly instead of showing a bare "not found".
167
213
  const isProposedStep = useCallback(
@@ -415,6 +461,33 @@ export function PierreWalkthroughCodeView({
415
461
  [onOpenFile, walkthrough.steps, load],
416
462
  );
417
463
 
464
+ // Per-token interactions for construct navigation. Only wired when the host
465
+ // supplies both callbacks; providing them also switches Pierre into its
466
+ // token-transformer render path (pointer events land on token spans).
467
+ const symbolHandlers = useMemo((): Partial<CodeViewReactOptions> => {
468
+ if (resolveSymbol == null || onSymbolClick == null) return {};
469
+ const resolveAt = (
470
+ props: TokenEventBase,
471
+ context: unknown,
472
+ ): { symbol: string; query: WalkthroughSymbolQuery } | null => {
473
+ const stepIndex = stepIndexFromItemContext(context);
474
+ const tokenText = props?.tokenText;
475
+ if (stepIndex == null || !tokenText) return null;
476
+ const symbol = resolveSymbolRef.current?.({ stepIndex, tokenText });
477
+ return symbol == null ? null : { symbol, query: { stepIndex, tokenText } };
478
+ };
479
+ return {
480
+ onTokenClick: (props, _event, context) => {
481
+ const hit = resolveAt(props, context);
482
+ if (hit) onSymbolClickRef.current?.(hit.symbol, hit.query);
483
+ },
484
+ onTokenEnter: (props, _event, context) => {
485
+ if (resolveAt(props, context)) paintSymbolToken(props.tokenElement, true);
486
+ },
487
+ onTokenLeave: (props) => paintSymbolToken(props.tokenElement, false),
488
+ };
489
+ }, [resolveSymbol, onSymbolClick]);
490
+
418
491
  const options = useMemo((): CodeViewReactOptions => {
419
492
  return {
420
493
  theme: {
@@ -426,10 +499,11 @@ export function PierreWalkthroughCodeView({
426
499
  layout: { paddingTop: 0, paddingBottom: 0, gap: 4 },
427
500
  onPostRender,
428
501
  ...(onOpenFile ? { onLineClick } : {}),
502
+ ...symbolHandlers,
429
503
  ...(background ? buildPierreOptions(background) : {}),
430
504
  ...(mode === 'light' || mode === 'dark' ? { themeType: mode } : {}),
431
505
  };
432
- }, [background, mode, onPostRender, onOpenFile, onLineClick]);
506
+ }, [background, mode, onPostRender, onOpenFile, onLineClick, symbolHandlers]);
433
507
 
434
508
  useEffect(() => {
435
509
  if (load.status !== 'ready' || stepIndex == null) return;
@@ -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
+ };