@principal-ai/subsystems-react 0.55.3 → 0.56.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 (98) hide show
  1. package/dist/index.d.ts +6 -2
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +3 -1
  4. package/dist/index.js.map +1 -1
  5. package/dist/pierre/PierreFileView.js +3 -3
  6. package/dist/pierre/PierreFileView.js.map +1 -1
  7. package/dist/pierre/PierreTrailCodeView.d.ts +4 -9
  8. package/dist/pierre/PierreTrailCodeView.d.ts.map +1 -1
  9. package/dist/pierre/PierreTrailCodeView.js +28 -25
  10. package/dist/pierre/PierreTrailCodeView.js.map +1 -1
  11. package/dist/pierre/fileAvailability.d.ts +4 -7
  12. package/dist/pierre/fileAvailability.d.ts.map +1 -1
  13. package/dist/pierre/fileAvailability.js +5 -10
  14. package/dist/pierre/fileAvailability.js.map +1 -1
  15. package/dist/pierre/trailSnippet.d.ts +10 -0
  16. package/dist/pierre/trailSnippet.d.ts.map +1 -0
  17. package/dist/pierre/trailSnippet.js +22 -0
  18. package/dist/pierre/trailSnippet.js.map +1 -0
  19. package/dist/subsystem/C4Graph.d.ts.map +1 -1
  20. package/dist/subsystem/C4Graph.js +1 -1
  21. package/dist/subsystem/C4Graph.js.map +1 -1
  22. package/dist/subsystem/FileDrawer.d.ts +3 -1
  23. package/dist/subsystem/FileDrawer.d.ts.map +1 -1
  24. package/dist/subsystem/FileDrawer.js +2 -2
  25. package/dist/subsystem/FileDrawer.js.map +1 -1
  26. package/dist/subsystem/IssueList.d.ts.map +1 -1
  27. package/dist/subsystem/IssueList.js +26 -0
  28. package/dist/subsystem/IssueList.js.map +1 -1
  29. package/dist/subsystem/SubsystemComponentGraph.d.ts +9 -5
  30. package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
  31. package/dist/subsystem/SubsystemComponentGraph.js +37 -47
  32. package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
  33. package/dist/subsystem/TrailStackBreadcrumb.d.ts +21 -0
  34. package/dist/subsystem/TrailStackBreadcrumb.d.ts.map +1 -0
  35. package/dist/subsystem/TrailStackBreadcrumb.js +77 -0
  36. package/dist/subsystem/TrailStackBreadcrumb.js.map +1 -0
  37. package/dist/subsystem/TrailStepsNav.d.ts +4 -3
  38. package/dist/subsystem/TrailStepsNav.d.ts.map +1 -1
  39. package/dist/subsystem/TrailStepsNav.js +13 -16
  40. package/dist/subsystem/TrailStepsNav.js.map +1 -1
  41. package/dist/subsystem/TrailsPanel.d.ts +9 -7
  42. package/dist/subsystem/TrailsPanel.d.ts.map +1 -1
  43. package/dist/subsystem/TrailsPanel.js +126 -76
  44. package/dist/subsystem/TrailsPanel.js.map +1 -1
  45. package/dist/subsystem/model.d.ts +5 -18
  46. package/dist/subsystem/model.d.ts.map +1 -1
  47. package/dist/subsystem/model.js +3 -11
  48. package/dist/subsystem/model.js.map +1 -1
  49. package/dist/subsystem/nodes.d.ts.map +1 -1
  50. package/dist/subsystem/nodes.js +17 -24
  51. package/dist/subsystem/nodes.js.map +1 -1
  52. package/dist/subsystem/stepEmphasis.d.ts +78 -0
  53. package/dist/subsystem/stepEmphasis.d.ts.map +1 -0
  54. package/dist/subsystem/stepEmphasis.js +89 -0
  55. package/dist/subsystem/stepEmphasis.js.map +1 -0
  56. package/dist/subsystem/trailBrief.d.ts.map +1 -1
  57. package/dist/subsystem/trailBrief.js +20 -0
  58. package/dist/subsystem/trailBrief.js.map +1 -1
  59. package/dist/subsystem/trailStack.d.ts +53 -0
  60. package/dist/subsystem/trailStack.d.ts.map +1 -0
  61. package/dist/subsystem/trailStack.js +154 -0
  62. package/dist/subsystem/trailStack.js.map +1 -0
  63. package/dist/subsystem/trailStepDisplay.d.ts +38 -0
  64. package/dist/subsystem/trailStepDisplay.d.ts.map +1 -0
  65. package/dist/subsystem/trailStepDisplay.js +41 -0
  66. package/dist/subsystem/trailStepDisplay.js.map +1 -0
  67. package/package.json +3 -3
  68. package/src/index.ts +10 -1
  69. package/src/pierre/PierreFileView.tsx +3 -3
  70. package/src/pierre/PierreTrailCodeView.tsx +24 -36
  71. package/src/pierre/fileAvailability.test.ts +0 -6
  72. package/src/pierre/fileAvailability.ts +5 -13
  73. package/src/pierre/trailSnippet.test.ts +74 -0
  74. package/src/pierre/trailSnippet.ts +35 -0
  75. package/src/stories/Canvases/Subsystem/Deprecated.stories.tsx +3 -17
  76. package/src/stories/Canvases/Subsystem/NodeAnatomy.stories.tsx +3 -31
  77. package/src/stories/Canvases/Subsystem/Trails.stories.tsx +0 -187
  78. package/src/subsystem/C4Graph.tsx +0 -1
  79. package/src/subsystem/FileDrawer.tsx +6 -0
  80. package/src/subsystem/IssueList.tsx +26 -0
  81. package/src/subsystem/SubsystemComponentGraph.tsx +68 -65
  82. package/src/subsystem/TrailStackBreadcrumb.test.tsx +109 -0
  83. package/src/subsystem/TrailStackBreadcrumb.tsx +172 -0
  84. package/src/subsystem/TrailStepsNav.test.tsx +35 -0
  85. package/src/subsystem/TrailStepsNav.tsx +18 -19
  86. package/src/subsystem/TrailsPanel.test.tsx +149 -0
  87. package/src/subsystem/TrailsPanel.tsx +165 -39
  88. package/src/subsystem/model.test.ts +7 -22
  89. package/src/subsystem/model.ts +6 -20
  90. package/src/subsystem/nodes.tsx +19 -28
  91. package/src/subsystem/stepEmphasis.ts +151 -0
  92. package/src/subsystem/trailBrief.test.ts +29 -0
  93. package/src/subsystem/trailBrief.ts +20 -0
  94. package/src/subsystem/trailStack.test.ts +133 -0
  95. package/src/subsystem/trailStack.ts +229 -0
  96. package/src/subsystem/trailStepDisplay.ts +68 -0
  97. package/src/stories/Canvases/Subsystem/Proposed.stories.tsx +0 -256
  98. package/src/stories/Canvases/Subsystem/ProposedTrails.stories.tsx +0 -184
@@ -2,8 +2,8 @@
2
2
  * PierreTrailCodeView — multi-file step snippets via `@pierre/diffs` CodeView.
3
3
  *
4
4
  * Renders one sliced window per trail step in a single virtualized
5
- * scroll; when `stepIndex` changes, scrolls that step's site line into view
6
- * and highlights it via CodeView `selectedLines` (same mechanism as
5
+ * scroll; when `stepIndex` changes, scrolls that step's call into view
6
+ * and highlights its full range via CodeView `selectedLines` (same mechanism as
7
7
  * `PierreSnippetView`).
8
8
  */
9
9
 
@@ -46,9 +46,9 @@ import { resolveVisibleStepIndex, type VisibleStepRect } from './visibleStep';
46
46
  import { fileUnavailableNotice, isFileUnavailableError } from './fileAvailability';
47
47
  import {
48
48
  remapSnippetLineNumbers,
49
- sliceSnippetWindow,
50
49
  type SnippetSlice,
51
50
  } from './sliceSnippet';
51
+ import { sliceTrailStep, trailCallSelection } from './trailSnippet';
52
52
 
53
53
  /**
54
54
  * A token the user clicked or hovered inside a step's snippet. `stepIndex` is
@@ -88,7 +88,7 @@ export interface PierreTrailCodeViewProps {
88
88
  stepIndex: number | null;
89
89
  /** Read a step site; `purl` names the seam site's repo for checkout resolution. */
90
90
  readFile: (path: string, purl?: string) => Promise<string>;
91
- /** Context lines above/below each step site; defaults to 8. */
91
+ /** Fallback context when a step has no caller construct span; defaults to 8. */
92
92
  contextLines?: number;
93
93
  /** Override Pierre's container background. */
94
94
  background?: string;
@@ -100,11 +100,6 @@ export interface PierreTrailCodeViewProps {
100
100
  * rather than the selected step. Only fires on change, not per scroll event.
101
101
  */
102
102
  onVisibleStepChange?: (index: number) => void;
103
- /**
104
- * Aliases of components marked `proposed`. A step whose file can't be read
105
- * but whose endpoint is proposed reads as "planned" rather than "missing".
106
- */
107
- proposedAliases?: ReadonlySet<string>;
108
103
  /**
109
104
  * Resolve a token to a model construct key (e.g. a component alias) for the
110
105
  * step its snippet belongs to. Return `null` when the token doesn't name a
@@ -138,10 +133,9 @@ function siteKey(step: Pick<SubsystemTrailStep, 'purl' | 'file'>): string {
138
133
  function unavailableNotice(
139
134
  step: SubsystemTrailStep,
140
135
  error: string,
141
- proposed: boolean,
142
136
  ): string {
143
137
  return isFileUnavailableError(error)
144
- ? fileUnavailableNotice(step.file, proposed)
138
+ ? fileUnavailableNotice(step.file)
145
139
  : `Couldn't load ${step.file}: ${error}`;
146
140
  }
147
141
 
@@ -206,7 +200,6 @@ export function PierreTrailCodeView({
206
200
  background: backgroundProp,
207
201
  onOpenFile,
208
202
  onVisibleStepChange,
209
- proposedAliases,
210
203
  resolveSymbol,
211
204
  onSymbolClick,
212
205
  }: PierreTrailCodeViewProps) {
@@ -292,24 +285,15 @@ export function PierreTrailCodeView({
292
285
  scheduleVisibleStepReport();
293
286
  }, [scheduleVisibleStepReport]);
294
287
 
295
- // A step onto a proposed component is planned work; label its missing file
296
- // accordingly instead of showing a bare "not found".
297
- const isProposedStep = useCallback(
298
- (step: SubsystemTrailStep): boolean =>
299
- proposedAliases != null &&
300
- (proposedAliases.has(step.from) || proposedAliases.has(step.to)),
301
- [proposedAliases],
302
- );
303
-
304
288
  const pathsKey = useMemo(() => {
305
289
  const keys = [...new Set(trail.steps.map((s) => siteKey(s)))];
306
290
  keys.sort();
307
291
  return keys.join('\0');
308
292
  }, [trail.steps]);
309
293
 
310
- // Load each site independently: one unreadable step (a proposed seam whose
311
- // file isn't implemented yet) must not blank the whole trail. Failures
312
- // are kept per-path and rendered as inline placeholders below.
294
+ // Load each site independently: one unreadable step must not blank the
295
+ // whole trail. Failures are kept per-path and rendered as inline
296
+ // placeholders below.
313
297
  useEffect(() => {
314
298
  let cancelled = false;
315
299
  setLoad({ status: 'loading' });
@@ -350,15 +334,13 @@ export function PierreTrailCodeView({
350
334
  const failure = load.unavailable.get(key);
351
335
  if (failure != null) {
352
336
  return unavailableSlice(
353
- unavailableNotice(step, failure, isProposedStep(step)),
337
+ unavailableNotice(step, failure),
354
338
  );
355
339
  }
356
340
  const contents = load.byPath.get(key) ?? '';
357
- const start = step.span?.startLine ?? 1;
358
- const end = step.span?.endLine ?? start;
359
- return sliceSnippetWindow(contents, start, end, contextLines, start);
341
+ return sliceTrailStep(contents, step, contextLines);
360
342
  });
361
- }, [load, trail.steps, contextLines, isProposedStep]);
343
+ }, [load, trail.steps, contextLines]);
362
344
 
363
345
  const items = useMemo((): CodeViewItem<TrailStepAnnotation>[] => {
364
346
  if (load.status !== 'ready' || slices.length === 0) return [];
@@ -392,23 +374,27 @@ export function PierreTrailCodeView({
392
374
  name: step.file,
393
375
  contents: slice.contents,
394
376
  lang: pierreLangForPath(step.file),
395
- cacheKey: `${trail.id}:${index}:${step.file}:${step.span?.startLine ?? ''}:${slice.sliceStart}-${slice.sliceEnd}${failure != null ? ':unavailable' : ''}`,
377
+ cacheKey: `${trail.id}:${index}:${step.file}:${step.span?.startLine ?? ''}:${step.site?.span?.startLine ?? ''}-${step.site?.span?.endLine ?? ''}:${slice.sliceStart}-${slice.sliceEnd}${failure != null ? ':unavailable' : ''}`,
396
378
  },
397
379
  };
398
380
  });
399
- }, [load, slices, trail.id, trail.steps, isProposedStep]);
381
+ }, [load, slices, trail.id, trail.steps]);
400
382
 
401
383
  const selectedLines = useMemo((): CodeViewLineSelection | null => {
402
384
  if (stepIndex == null || stepIndex < 0 || stepIndex >= slices.length) {
403
385
  return null;
404
386
  }
405
- const focus = slices[stepIndex]?.focusOffset;
406
- if (focus == null) return null;
387
+ const step = trail.steps[stepIndex];
388
+ const slice = slices[stepIndex];
389
+ if (step == null || slice == null || load.status !== 'ready') return null;
390
+ if (load.unavailable.has(siteKey(step))) return null;
391
+ const range = trailCallSelection(step, slice);
392
+ if (range == null) return null;
407
393
  return {
408
394
  id: stepItemId(trail.id, stepIndex),
409
- range: { start: focus, end: focus },
395
+ range,
410
396
  };
411
- }, [stepIndex, slices, trail.id]);
397
+ }, [stepIndex, slices, trail.id, trail.steps, load]);
412
398
 
413
399
  const sliceStartByItemId = useMemo(() => {
414
400
  const map = new Map<string, number>();
@@ -468,7 +454,9 @@ export function PierreTrailCodeView({
468
454
  </span>
469
455
  ) : (
470
456
  <>
471
- L{step.span?.startLine ?? '?'}
457
+ {step.site?.span
458
+ ? `${step.site.symbol} · L${step.site.span.startLine}-${step.site.span.endLine}`
459
+ : `L${step.span?.startLine ?? '?'}`}
472
460
  {onOpenFile && (
473
461
  <OpenFileHeaderButton
474
462
  file={step.file}
@@ -23,12 +23,6 @@ describe('isFileUnavailableError', () => {
23
23
  });
24
24
 
25
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
26
  test('falls back to a plain not-in-checkout notice', () => {
33
27
  expect(fileUnavailableNotice('src/a.ts')).toBe(
34
28
  'Not in the local checkout: src/a.ts',
@@ -1,8 +1,7 @@
1
1
  /**
2
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
3
+ * be served from any local checkout. These helpers detect that class of failure
4
+ * so the Pierre views can show a not-yet-available notice instead of a raw
6
5
  * host error.
7
6
  */
8
7
 
@@ -23,15 +22,8 @@ export function isFileUnavailableError(
23
22
  }
24
23
 
25
24
  /**
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".
25
+ * One-line notice shown in place of a snippet whose file can't be read.
29
26
  */
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}`;
27
+ export function fileUnavailableNotice(path: string): string {
28
+ return `Not in the local checkout: ${path}`;
37
29
  }
@@ -0,0 +1,74 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import type { SubsystemTrailStep } from '../subsystem/model';
3
+ import { sliceTrailStep, trailCallSelection } from './trailSnippet';
4
+
5
+ const contents = Array.from({ length: 100 }, (_, i) => `line ${i + 1}`).join('\n');
6
+ const step: SubsystemTrailStep = {
7
+ from: 'caller',
8
+ to: 'callee',
9
+ mechanism: 'calls',
10
+ file: 'src/caller.ts',
11
+ purl: 'pkg:github/example/repo#src/caller.ts',
12
+ symbol: 'loadModel',
13
+ site: {
14
+ symbol: 'loadModel',
15
+ span: { startLine: 10, endLine: 70, contentHash: 'caller' },
16
+ },
17
+ span: { startLine: 35, endLine: 38, contentHash: 'call' },
18
+ target: {
19
+ file: 'src/callee.ts',
20
+ purl: 'pkg:github/example/repo#src/callee.ts',
21
+ symbol: 'readFile',
22
+ span: { startLine: 2, endLine: 8, contentHash: 'callee' },
23
+ },
24
+ };
25
+
26
+ describe('trail construct snippets', () => {
27
+ test('shows the full caller span without extra context, not the callee span', () => {
28
+ const slice = sliceTrailStep(contents, step, 8);
29
+ expect(slice.sliceStart).toBe(10);
30
+ expect(slice.sliceEnd).toBe(70);
31
+ expect(slice.contents).toBe(contents.split('\n').slice(9, 70).join('\n'));
32
+ expect(slice.focusOffset).toBe(26);
33
+ });
34
+
35
+ test('highlights every line of a multiline call inside the construct', () => {
36
+ const slice = sliceTrailStep(contents, step, 8);
37
+ expect(trailCallSelection(step, slice)).toEqual({ start: 26, end: 29 });
38
+ });
39
+
40
+ test('preserves single-line call highlighting', () => {
41
+ const single = { ...step, span: { ...step.span!, endLine: 35 } };
42
+ expect(trailCallSelection(single, sliceTrailStep(contents, single, 8)))
43
+ .toEqual({ start: 26, end: 26 });
44
+ });
45
+
46
+ test('keeps a context window for older steps without a site construct', () => {
47
+ const legacy = { ...step, site: undefined };
48
+ const slice = sliceTrailStep(contents, legacy, 8);
49
+ expect(slice.sliceStart).toBe(27);
50
+ expect(slice.sliceEnd).toBe(46);
51
+ expect(slice.focusOffset).toBe(9);
52
+ expect(trailCallSelection(legacy, slice)).toEqual({ start: 9, end: 12 });
53
+ });
54
+
55
+ test('does not highlight the declaration as a call when call evidence is missing', () => {
56
+ const unverified = { ...step, span: undefined };
57
+ const slice = sliceTrailStep(contents, unverified, 8);
58
+ expect(slice.sliceStart).toBe(10);
59
+ expect(slice.sliceEnd).toBe(70);
60
+ expect(slice.focusOffset).toBe(1);
61
+ expect(trailCallSelection(unverified, slice)).toBeNull();
62
+ });
63
+
64
+ test('clips stale call ranges to the displayed construct', () => {
65
+ const stale = { ...step, span: { ...step.span!, startLine: 68, endLine: 75 } };
66
+ expect(trailCallSelection(stale, sliceTrailStep(contents, stale, 8)))
67
+ .toEqual({ start: 59, end: 61 });
68
+ });
69
+
70
+ test('does not highlight unrelated lines when the call falls outside the construct', () => {
71
+ const stale = { ...step, span: { ...step.span!, startLine: 80, endLine: 82 } };
72
+ expect(trailCallSelection(stale, sliceTrailStep(contents, stale, 8))).toBeNull();
73
+ });
74
+ });
@@ -0,0 +1,35 @@
1
+ import type { SubsystemTrailStep } from '../subsystem/model';
2
+ import { sliceSnippetWindow, type SnippetSlice } from './sliceSnippet';
3
+
4
+ /** Show the caller's whole declaration, keeping focus on its call site. */
5
+ export function sliceTrailStep(
6
+ contents: string,
7
+ step: SubsystemTrailStep,
8
+ contextLines: number,
9
+ ): SnippetSlice {
10
+ const site = step.site?.span;
11
+ const start = site?.startLine ?? step.span?.startLine ?? 1;
12
+ const end = site?.endLine ?? step.span?.endLine ?? start;
13
+ return sliceSnippetWindow(
14
+ contents,
15
+ start,
16
+ end,
17
+ site ? 0 : contextLines,
18
+ step.span?.startLine ?? start,
19
+ );
20
+ }
21
+
22
+ /** Convert the complete call range to snippet-local lines, never inventing evidence. */
23
+ export function trailCallSelection(
24
+ step: SubsystemTrailStep,
25
+ slice: SnippetSlice,
26
+ ): { start: number; end: number } | null {
27
+ if (step.span == null) return null;
28
+ const start = Math.max(step.span.startLine, slice.sliceStart);
29
+ const end = Math.min(step.span.endLine, slice.sliceEnd);
30
+ if (start > end) return null;
31
+ return {
32
+ start: start - slice.sliceStart + 1,
33
+ end: end - slice.sliceStart + 1,
34
+ };
35
+ }
@@ -12,9 +12,8 @@ import type {
12
12
  import { trailFromSteps } from './fixtures';
13
13
 
14
14
  /**
15
- * `deprecated` is the inverse of `proposed`: the claim was real, its source is
16
- * gone upstream. Mirrors Proposed.stories.tsx so the two temporary states read
17
- * as siblings — proposed = not built yet, deprecated = built once, removed since.
15
+ * `deprecated` marks a claim whose source is gone upstream. The node keeps a
16
+ * transitional marker until a cleanup pass removes it.
18
17
  */
19
18
  const meta = {
20
19
  title: 'Canvases/Subsystem/Deprecated',
@@ -154,22 +153,9 @@ const spotlightRows: Array<{ label: string; component: SubsystemComponent }> = [
154
153
  role: 'entry',
155
154
  },
156
155
  },
157
- {
158
- label: 'proposed (the sibling temporary state)',
159
- component: {
160
- alias: 'prop-fn',
161
- name: 'OpenCodeV2Lifecycle',
162
- construct: 'function',
163
- file: '',
164
- purl: PURL,
165
- symbol: 'OpenCodeV2Lifecycle',
166
- purpose: 'Proposed Studio host lifecycle',
167
- proposed: true,
168
- },
169
- },
170
156
  ];
171
157
 
172
- /** Spotlights: slate deprecated badge + border vs live, proposed, and entry. */
158
+ /** Spotlights: slate deprecated badge + border vs live and entry. */
173
159
  export const Spotlights: Story = {
174
160
  render: () => (
175
161
  <div
@@ -142,33 +142,6 @@ const CHANNELS: Channel[] = [
142
142
  declaration: storeDetail,
143
143
  },
144
144
  },
145
- {
146
- label: 'proposed',
147
- note: 'dashed goldenrod border + right badge; fileless.',
148
- component: {
149
- alias: 'prop',
150
- name: 'OpenCodeV2Lifecycle',
151
- construct: 'function',
152
- file: '',
153
- purl: PURL,
154
- symbol: 'OpenCodeV2Lifecycle',
155
- proposed: true,
156
- },
157
- },
158
- {
159
- label: 'proposed + role',
160
- note: 'proposed wins the right badge; role stays on the model.',
161
- component: {
162
- alias: 'prop-role',
163
- name: 'startMaintainFlow',
164
- construct: 'function',
165
- file: '',
166
- purl: PURL,
167
- symbol: 'startMaintainFlow',
168
- proposed: true,
169
- role: 'service',
170
- },
171
- },
172
145
  {
173
146
  label: 'framework stereotype',
174
147
  note: 'left badge prefers "framework · stereotype"; border stays construct.',
@@ -230,11 +203,11 @@ function ChannelCell({ channel }: { channel: Channel }) {
230
203
 
231
204
  const LEGEND: Array<[string, string]> = [
232
205
  ['border color', 'construct kind (Pierre palette) — or primary when a file is open (spotlight)'],
233
- ['border style', 'solid normally; dashed goldenrod when `proposed`; dotted when the node has findings'],
206
+ ['border style', 'solid normally; dotted when the node has findings'],
234
207
  ['border width', '2px; 4px when selected or file-matched'],
235
208
  ['corners', '8px rounded; square for `external`'],
236
209
  ['left badge (top edge)', 'construct / framework stereotype / entityKind'],
237
- ['right badge (top edge)', 'role, or `proposed` (wins); storage sits beside it'],
210
+ ['right badge (top edge)', 'role; storage sits beside it'],
238
211
  ['bottom strip (bottom edge)', 'filename — hover only, or while that file is open'],
239
212
  ['fill', 'backgroundSecondary; opacity 0.18 when dimmed'],
240
213
  ['OCCUPIED', 'top-left · top-right · bottom-center · border · fill'],
@@ -439,8 +412,7 @@ export const IssueOverlayProposal: Story = {
439
412
  </div>
440
413
  <ul style={{ margin: 0, paddingLeft: 18, fontSize: 12, lineHeight: 1.7, color: '#b0b0b0' }}>
441
414
  <li>
442
- <b>Dotted border</b> (keeps the construct color) = the node has findings — a sibling of
443
- <b> proposed</b>&apos;s dashed, kept distinct so the two don&apos;t read alike.
415
+ <b>Dotted border</b> (keeps the construct color) = the node has findings.
444
416
  </li>
445
417
  <li>
446
418
  <b>Corner chip</b> carries severity (<b>red</b> = error / contradicts source,{' '}
@@ -170,7 +170,6 @@ function TrailsDemo() {
170
170
  trail,
171
171
  stepIndex,
172
172
  onOpenFile,
173
- proposedAliases,
174
173
  resolveSymbol,
175
174
  onSymbolClick,
176
175
  onVisibleStepChange,
@@ -181,7 +180,6 @@ function TrailsDemo() {
181
180
  readFile={readStoryFile}
182
181
  contextLines={4}
183
182
  onOpenFile={onOpenFile}
184
- proposedAliases={proposedAliases}
185
183
  resolveSymbol={resolveSymbol}
186
184
  onSymbolClick={onSymbolClick}
187
185
  onVisibleStepChange={onVisibleStepChange}
@@ -223,189 +221,6 @@ export const ThreeTrails: Story = {
223
221
  render: () => <TrailsDemo />,
224
222
  };
225
223
 
226
- /**
227
- * The proposed-seam failure mode: a trail step whose component is
228
- * `proposed` points at a file that isn't in the checkout yet (no story
229
- * fixture). The step must render an inline "Proposed — … isn't in the local
230
- * checkout yet." placeholder with no Open-file affordance, while the live steps
231
- * around it still render their snippets — one bad step must not blank the flow.
232
- */
233
- const proposedComponents: SubsystemComponent[] = [
234
- ...drawingComponents,
235
- {
236
- alias: 'runs',
237
- name: 'SubsystemModelRunStore',
238
- construct: 'store',
239
- file: 'src/runs/subsystemModelRuns.ts',
240
- purl: 'pkg:github/principal-ai/desktop-app',
241
- symbol: 'SubsystemModelRunStore',
242
- purpose: 'proposed store associating maintain runs with a model',
243
- proposed: true,
244
- process: 'draw-host',
245
- },
246
- ];
247
-
248
- const proposedTrails: SubsystemTrail[] = [
249
- {
250
- id: 'tl-associate-run',
251
- title: 'Associate a finished run with its model (proposed)',
252
- steps: [
253
- {
254
- from: 'host',
255
- to: 'storage',
256
- mechanism: 'calls',
257
- file: 'src/hooks/useDrawingsHost.ts',
258
- span: { startLine: 45, endLine: 45, contentHash: 'fixture' },
259
- purl: stepPurl('src/hooks/useDrawingsHost.ts'),
260
- symbol: 'useDrawingsHost.openDrawing',
261
- annotation: 'Live step — its snippet still renders.',
262
- },
263
- {
264
- from: 'storage',
265
- to: 'runs',
266
- mechanism: 'writes',
267
- file: 'src/runs/subsystemModelRuns.ts',
268
- span: { startLine: 1, endLine: 1, contentHash: 'fixture' },
269
- purl: stepPurl('src/runs/subsystemModelRuns.ts'),
270
- symbol: 'SubsystemModelRunStore.write',
271
- annotation: 'Proposed seam — the file is not in the checkout yet.',
272
- },
273
- ],
274
- },
275
- // Neighbours for the wrapped title above: a couple of one-line titles and
276
- // one that wraps to two, so the header's padding reads against the rows on
277
- // either side of it rather than in isolation.
278
- {
279
- id: 'tl-open-drawing',
280
- title: 'Open drawing',
281
- steps: [
282
- {
283
- from: 'panel',
284
- to: 'host',
285
- mechanism: 'produces',
286
- file: 'src/panels/DrawingsLeftPanel.tsx',
287
- span: { startLine: 85, endLine: 85, contentHash: 'fixture' },
288
- purl: stepPurl('src/panels/DrawingsLeftPanel.tsx'),
289
- symbol: 'DrawingsLeftPanel.openDrawing',
290
- annotation: 'Emits the open intent up to the host.',
291
- },
292
- {
293
- from: 'host',
294
- to: 'storage',
295
- mechanism: 'calls',
296
- file: 'src/hooks/useDrawingsHost.ts',
297
- span: { startLine: 45, endLine: 45, contentHash: 'fixture' },
298
- purl: stepPurl('src/hooks/useDrawingsHost.ts'),
299
- symbol: 'useDrawingsHost.openDrawing',
300
- },
301
- ],
302
- },
303
- {
304
- id: 'tl-save-drawing',
305
- title: 'Save the drawing and refresh the list',
306
- steps: [
307
- {
308
- from: 'tab',
309
- to: 'fs',
310
- mechanism: 'calls',
311
- file: 'src/components/DrawingTabContent.tsx',
312
- span: { startLine: 112, endLine: 112, contentHash: 'fixture' },
313
- purl: stepPurl('src/components/DrawingTabContent.tsx'),
314
- symbol: 'DrawingTabContent.save',
315
- },
316
- {
317
- from: 'host',
318
- to: 'panel',
319
- mechanism: 'produces',
320
- file: 'src/hooks/useDrawingsHost.ts',
321
- span: { startLine: 64, endLine: 64, contentHash: 'fixture' },
322
- purl: stepPurl('src/hooks/useDrawingsHost.ts'),
323
- symbol: 'useDrawingsHost.onSaved',
324
- },
325
- ],
326
- },
327
- {
328
- id: 'tl-delete-drawing',
329
- title: 'Delete drawing',
330
- steps: [
331
- {
332
- from: 'host',
333
- to: 'fs',
334
- mechanism: 'calls',
335
- file: 'src/hooks/useDrawingsHost.ts',
336
- span: { startLine: 66, endLine: 66, contentHash: 'fixture' },
337
- purl: stepPurl('src/hooks/useDrawingsHost.ts'),
338
- symbol: 'useDrawingsHost.deleteDrawing',
339
- },
340
- {
341
- from: 'panel',
342
- to: 'storage',
343
- mechanism: 'calls',
344
- file: 'src/panels/DrawingsLeftPanel.tsx',
345
- span: { startLine: 56, endLine: 56, contentHash: 'fixture' },
346
- purl: stepPurl('src/panels/DrawingsLeftPanel.tsx'),
347
- symbol: 'DrawingsLeftPanel.scan',
348
- },
349
- ],
350
- },
351
- ];
352
-
353
- function ProposedMissingStepDemo() {
354
- const renderTrailViewer = useCallback(
355
- ({
356
- trail,
357
- stepIndex,
358
- onOpenFile,
359
- proposedAliases,
360
- resolveSymbol,
361
- onSymbolClick,
362
- onVisibleStepChange,
363
- }: TrailViewerContext) => (
364
- <PierreTrailCodeView
365
- trail={trail}
366
- stepIndex={stepIndex}
367
- readFile={readStoryFile}
368
- contextLines={4}
369
- onOpenFile={onOpenFile}
370
- proposedAliases={proposedAliases}
371
- resolveSymbol={resolveSymbol}
372
- onSymbolClick={onSymbolClick}
373
- onVisibleStepChange={onVisibleStepChange}
374
- />
375
- ),
376
- [],
377
- );
378
-
379
- return (
380
- <div style={{ width: '100%', height: '100vh', display: 'flex', flexDirection: 'column' }}>
381
- <SubsystemComponentGraph
382
- components={proposedComponents}
383
- trails={proposedTrails}
384
- title="proposed seam with a missing file"
385
- description="The **Trails** panel lists four flows so the long, wrapped title (*Associate a finished run with its model (proposed)*) reads against its neighbours — two one-line titles and one (*Save the drawing and refresh the list*) that also wraps. Expand **Associate a finished run with its model** and click either step: the proposed step 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."
386
- renderTrailViewer={renderTrailViewer}
387
- renderFileViewer={(file) => (
388
- <div
389
- style={{
390
- padding: 12,
391
- fontFamily: 'monospace',
392
- fontSize: 12,
393
- color: '#bbb',
394
- whiteSpace: 'pre',
395
- }}
396
- >
397
- {`// ${file}\n …`}
398
- </div>
399
- )}
400
- />
401
- </div>
402
- );
403
- }
404
-
405
- export const ProposedMissingStep: Story = {
406
- render: () => <ProposedMissingStepDemo />,
407
- };
408
-
409
224
  // --- Clickable constructs -------------------------------------------------
410
225
  //
411
226
  // A trail step's line is an edge between two constructs. When the host
@@ -554,7 +369,6 @@ function ClickableConstructsDemo() {
554
369
  trail,
555
370
  stepIndex,
556
371
  onOpenFile,
557
- proposedAliases,
558
372
  resolveSymbol,
559
373
  onSymbolClick,
560
374
  onVisibleStepChange,
@@ -565,7 +379,6 @@ function ClickableConstructsDemo() {
565
379
  readFile={readClickableFile}
566
380
  contextLines={4}
567
381
  onOpenFile={onOpenFile}
568
- proposedAliases={proposedAliases}
569
382
  resolveSymbol={resolveSymbol}
570
383
  onSymbolClick={onSymbolClick}
571
384
  onVisibleStepChange={onVisibleStepChange}
@@ -2711,7 +2711,6 @@ function Inner({ model, onSelectNode, onSelectEdge, onTrailClick, onToggleContai
2711
2711
  >
2712
2712
  {selectedConstruct.component.alias}
2713
2713
  {selectedConstruct.role ? ` · ${selectedConstruct.role}` : ''}
2714
- {selectedConstruct.component.proposed ? ' · proposed' : ''}
2715
2714
  </div>
2716
2715
  </div>
2717
2716
  )}
@@ -26,6 +26,7 @@ export function FileDrawer({
26
26
  suppressEscape = false,
27
27
  hidden = false,
28
28
  progress = null,
29
+ subheader = null,
29
30
  children,
30
31
  }: {
31
32
  /** Drawer chrome title; `null` closes the drawer. */
@@ -43,6 +44,8 @@ export function FileDrawer({
43
44
  * content with no position (a single file).
44
45
  */
45
46
  progress?: { index: number; total: number } | null;
47
+ /** Optional chrome row below the title (e.g. the trail call stack). */
48
+ subheader?: ReactNode;
46
49
  children?: ReactNode;
47
50
  }) {
48
51
  const { theme } = useTheme();
@@ -223,6 +226,9 @@ export function FileDrawer({
223
226
  </button>
224
227
  </div>
225
228
  </div>
229
+ {subheader != null && open && (
230
+ <div style={{ flexShrink: 0, minHeight: 0 }}>{subheader}</div>
231
+ )}
226
232
  <div style={{ flex: 1, minHeight: 0, overflow: "auto" }}>
227
233
  {children}
228
234
  </div>