@principal-ai/subsystems-react 0.56.0 → 0.56.2

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 (49) hide show
  1. package/dist/index.d.ts +0 -6
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +0 -3
  4. package/dist/index.js.map +1 -1
  5. package/dist/pierre/PierreTrailCodeView.d.ts +3 -3
  6. package/dist/pierre/PierreTrailCodeView.d.ts.map +1 -1
  7. package/dist/pierre/PierreTrailCodeView.js +16 -16
  8. package/dist/pierre/PierreTrailCodeView.js.map +1 -1
  9. package/dist/pierre/trailSnippet.d.ts +10 -0
  10. package/dist/pierre/trailSnippet.d.ts.map +1 -0
  11. package/dist/pierre/trailSnippet.js +22 -0
  12. package/dist/pierre/trailSnippet.js.map +1 -0
  13. package/dist/subsystem/FileDrawer.d.ts +9 -11
  14. package/dist/subsystem/FileDrawer.d.ts.map +1 -1
  15. package/dist/subsystem/FileDrawer.js +23 -23
  16. package/dist/subsystem/FileDrawer.js.map +1 -1
  17. package/dist/subsystem/GraphSidebar.d.ts.map +1 -1
  18. package/dist/subsystem/GraphSidebar.js +3 -1
  19. package/dist/subsystem/GraphSidebar.js.map +1 -1
  20. package/dist/subsystem/SubsystemComponentGraph.d.ts +4 -3
  21. package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
  22. package/dist/subsystem/SubsystemComponentGraph.js +32 -30
  23. package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
  24. package/package.json +1 -1
  25. package/src/index.ts +0 -37
  26. package/src/pierre/PierreTrailCodeView.tsx +14 -23
  27. package/src/pierre/trailSnippet.test.ts +74 -0
  28. package/src/pierre/trailSnippet.ts +35 -0
  29. package/src/stories/Canvases/Subsystem/LayoutDirection.stories.tsx +55 -3
  30. package/src/subsystem/FileDrawer.test.tsx +130 -0
  31. package/src/subsystem/FileDrawer.tsx +27 -28
  32. package/src/subsystem/GraphSidebar.test.tsx +56 -1
  33. package/src/subsystem/GraphSidebar.tsx +3 -1
  34. package/src/subsystem/SubsystemComponentGraph.tsx +65 -57
  35. package/dist/subsystem/DecorationPanel.d.ts +0 -37
  36. package/dist/subsystem/DecorationPanel.d.ts.map +0 -1
  37. package/dist/subsystem/DecorationPanel.js +0 -147
  38. package/dist/subsystem/DecorationPanel.js.map +0 -1
  39. package/dist/subsystem/decoration.d.ts +0 -112
  40. package/dist/subsystem/decoration.d.ts.map +0 -1
  41. package/dist/subsystem/decoration.js +0 -54
  42. package/dist/subsystem/decoration.js.map +0 -1
  43. package/src/stories/Canvases/Subsystem/StepEmphasis.stories.tsx +0 -528
  44. package/src/stories/Components/DecorationPanel.stories.tsx +0 -160
  45. package/src/subsystem/DecorationPanel.test.tsx +0 -128
  46. package/src/subsystem/DecorationPanel.tsx +0 -334
  47. package/src/subsystem/decoration.test.ts +0 -72
  48. package/src/subsystem/decoration.ts +0 -151
  49. package/src/subsystem/stepEmphasis.test.ts +0 -110
@@ -1,528 +0,0 @@
1
- import React, { useCallback, useMemo, useState } from 'react';
2
- import '@xyflow/react/dist/style.css';
3
- import type { Meta, StoryObj } from '@storybook/react';
4
- import { ThemeProvider, defaultEditorTheme } from '@principal-ade/industry-theme';
5
- import { Star } from 'lucide-react';
6
- import { SubsystemComponentGraph } from '../../../subsystem/SubsystemComponentGraph';
7
- import type { SubsystemComponent, SubsystemTrail } from '../../../subsystem/model';
8
- import {
9
- buildStepEmphasisMap,
10
- orphanedEmphasis,
11
- stepClaimKey,
12
- type StepEmphasisDocument,
13
- type StepEmphasisRecord,
14
- } from '../../../subsystem/stepEmphasis';
15
-
16
- /**
17
- * PROTOTYPE: step emphasis as a separate augmentation document.
18
- *
19
- * The point of the story is to make the *data model* concrete before the store
20
- * lands. The marks below live in story state, keyed by trail + claim
21
- * fingerprint — not on the portable `SubsystemTrail`. Reorder the steps or edit
22
- * a claim and watch what the keys do.
23
- */
24
- const meta = {
25
- title: 'Canvases/Subsystem/Step emphasis',
26
- component: SubsystemComponentGraph,
27
- parameters: {
28
- layout: 'fullscreen',
29
- docs: {
30
- description: {
31
- component:
32
- 'Prototype for the focus-guidance augmentation: star a walkthrough step with a kind + optional note, stored separately from the model document.',
33
- },
34
- },
35
- },
36
- decorators: [
37
- (Story) => (
38
- <ThemeProvider theme={defaultEditorTheme}>
39
- <Story />
40
- </ThemeProvider>
41
- ),
42
- ],
43
- } satisfies Meta<typeof SubsystemComponentGraph>;
44
-
45
- export default meta;
46
- type Story = StoryObj<typeof meta>;
47
-
48
- const STORY_PURL = 'pkg:github/principal-ai/desktop-app';
49
- const stepPurl = (file: string) => `${STORY_PURL}#${file}`;
50
-
51
- const components: SubsystemComponent[] = [
52
- {
53
- alias: 'panel',
54
- name: 'DrawingsLeftPanel',
55
- construct: 'class',
56
- file: 'src/panels/DrawingsLeftPanel.tsx',
57
- purl: STORY_PURL,
58
- purpose: 'lists drawings; emits open/delete intents',
59
- symbol: 'DrawingsLeftPanel',
60
- process: 'draw-list',
61
- },
62
- {
63
- alias: 'host',
64
- name: 'useDrawingsHost',
65
- construct: 'function',
66
- file: 'src/hooks/useDrawingsHost.ts',
67
- purl: STORY_PURL,
68
- purpose: 'hosts drawing CRUD; dispatches to storage and shell',
69
- symbol: 'useDrawingsHost',
70
- process: 'draw-host',
71
- },
72
- {
73
- alias: 'storage',
74
- name: 'DrawingsStorage',
75
- construct: 'class',
76
- file: 'src/storage/drawingsStorage.ts',
77
- purl: STORY_PURL,
78
- purpose: 'persists drawings as files',
79
- symbol: 'DrawingsStorage',
80
- process: 'draw-host',
81
- },
82
- {
83
- alias: 'fs',
84
- name: 'FileSystemService',
85
- construct: 'external',
86
- file: 'src/services/fileSystem.ts',
87
- purl: STORY_PURL,
88
- purpose: 'sandboxed host fd/fs access',
89
- symbol: 'FileSystemService',
90
- role: 'service',
91
- },
92
- {
93
- alias: 'shell',
94
- name: 'WorkspaceShell',
95
- construct: 'class',
96
- file: 'src/workspace/WorkspaceShell.tsx',
97
- purl: STORY_PURL,
98
- purpose: 'mounts tabs for opened drawings',
99
- symbol: 'WorkspaceShell',
100
- process: 'draw-host',
101
- },
102
- {
103
- alias: 'tab',
104
- name: 'DrawingTabContent',
105
- construct: 'class',
106
- file: 'src/components/DrawingTabContent.tsx',
107
- purl: STORY_PURL,
108
- purpose: 'loads/saves an open drawing',
109
- symbol: 'DrawingTabContent',
110
- process: 'draw-host',
111
- },
112
- ];
113
-
114
- const seedTrails: SubsystemTrail[] = [
115
- {
116
- id: 'tl-open-drawing',
117
- title: 'Open drawing',
118
- steps: [
119
- {
120
- from: 'panel',
121
- to: 'storage',
122
- mechanism: 'calls',
123
- file: 'src/panels/DrawingsLeftPanel.tsx',
124
- span: { startLine: 56, endLine: 56, contentHash: 'fixture' },
125
- purl: stepPurl('src/panels/DrawingsLeftPanel.tsx'),
126
- symbol: 'DrawingsLeftPanel.scan',
127
- annotation: 'Lists the drawings directory; each row feeds an open intent.',
128
- },
129
- {
130
- from: 'panel',
131
- to: 'host',
132
- mechanism: 'produces',
133
- file: 'src/panels/DrawingsLeftPanel.tsx',
134
- span: { startLine: 85, endLine: 85, contentHash: 'fixture' },
135
- purl: stepPurl('src/panels/DrawingsLeftPanel.tsx'),
136
- symbol: 'DrawingsLeftPanel.openDrawing',
137
- annotation: 'Emits the open intent up to the host.',
138
- },
139
- {
140
- from: 'host',
141
- to: 'storage',
142
- mechanism: 'calls',
143
- file: 'src/hooks/useDrawingsHost.ts',
144
- span: { startLine: 45, endLine: 45, contentHash: 'fixture' },
145
- purl: stepPurl('src/hooks/useDrawingsHost.ts'),
146
- symbol: 'useDrawingsHost.openDrawing',
147
- annotation: 'Host resolves the path and delegates to storage.',
148
- },
149
- {
150
- from: 'host',
151
- to: 'shell',
152
- mechanism: 'produces',
153
- file: 'src/workspace/WorkspaceShell.tsx',
154
- span: { startLine: 369, endLine: 369, contentHash: 'fixture' },
155
- purl: stepPurl('src/workspace/WorkspaceShell.tsx'),
156
- symbol: 'WorkspaceShell.openTab',
157
- annotation: 'Asks the shell to surface the drawing as a tab.',
158
- },
159
- {
160
- from: 'shell',
161
- to: 'tab',
162
- mechanism: 'feeds',
163
- file: 'src/workspace/WorkspaceShell.tsx',
164
- span: { startLine: 372, endLine: 372, contentHash: 'fixture' },
165
- purl: stepPurl('src/workspace/WorkspaceShell.tsx'),
166
- symbol: 'WorkspaceShell.mountTab',
167
- },
168
- {
169
- from: 'storage',
170
- to: 'fs',
171
- mechanism: 'calls',
172
- file: 'src/storage/drawingsStorage.ts',
173
- span: { startLine: 90, endLine: 90, contentHash: 'fixture' },
174
- purl: stepPurl('src/storage/drawingsStorage.ts'),
175
- symbol: 'DrawingsStorage.read',
176
- annotation: 'Reads the file through the sandboxed fs service.',
177
- },
178
- {
179
- from: 'tab',
180
- to: 'fs',
181
- mechanism: 'calls',
182
- file: 'src/components/DrawingTabContent.tsx',
183
- span: { startLine: 83, endLine: 83, contentHash: 'fixture' },
184
- purl: stepPurl('src/components/DrawingTabContent.tsx'),
185
- symbol: 'DrawingTabContent.load',
186
- },
187
- ],
188
- },
189
- {
190
- id: 'tl-save-drawing',
191
- title: 'Save drawing',
192
- steps: [
193
- {
194
- from: 'tab',
195
- to: 'fs',
196
- mechanism: 'calls',
197
- file: 'src/components/DrawingTabContent.tsx',
198
- span: { startLine: 112, endLine: 112, contentHash: 'fixture' },
199
- purl: stepPurl('src/components/DrawingTabContent.tsx'),
200
- symbol: 'DrawingTabContent.save',
201
- },
202
- {
203
- from: 'tab',
204
- to: 'host',
205
- mechanism: 'produces',
206
- file: 'src/components/DrawingTabContent.tsx',
207
- span: { startLine: 121, endLine: 121, contentHash: 'fixture' },
208
- purl: stepPurl('src/components/DrawingTabContent.tsx'),
209
- symbol: 'DrawingTabContent.emitSaved',
210
- },
211
- {
212
- from: 'host',
213
- to: 'panel',
214
- mechanism: 'produces',
215
- file: 'src/hooks/useDrawingsHost.ts',
216
- span: { startLine: 64, endLine: 64, contentHash: 'fixture' },
217
- purl: stepPurl('src/hooks/useDrawingsHost.ts'),
218
- symbol: 'useDrawingsHost.onSaved',
219
- },
220
- {
221
- from: 'panel',
222
- to: 'storage',
223
- mechanism: 'calls',
224
- file: 'src/panels/DrawingsLeftPanel.tsx',
225
- span: { startLine: 56, endLine: 56, contentHash: 'fixture' },
226
- purl: stepPurl('src/panels/DrawingsLeftPanel.tsx'),
227
- symbol: 'DrawingsLeftPanel.scan',
228
- },
229
- ],
230
- },
231
- ];
232
-
233
- const SEEDED_AT = '2026-10-19T12:00:00.000Z';
234
-
235
- function seedEmphasis(trails: SubsystemTrail[]): StepEmphasisRecord[] {
236
- const trail = trails[0]!;
237
- const at = (stepIndex: number, guidance: string): StepEmphasisRecord => ({
238
- trailId: trail.id,
239
- stepKey: stepClaimKey(trail.steps[stepIndex]!),
240
- guidance,
241
- provenance: { source: 'author', at: SEEDED_AT, author: 'you' },
242
- });
243
- return [
244
- at(
245
- 0,
246
- 'The **scan** is where a drawing row becomes an open intent. Everything below depends on this hand-off.',
247
- ),
248
- at(
249
- 3,
250
- 'The shell is the **hinge**: the tab only exists after this call, so this is where the flow becomes visible.',
251
- ),
252
- at(
253
- 5,
254
- '**Risky seam.** First filesystem touch — a sandbox denial lands here rather than in storage.',
255
- ),
256
- ];
257
- }
258
-
259
- const muted = '#9aa4b2';
260
- const border = '#2a2f3a';
261
- const panelBg = '#0f1115';
262
-
263
- function InspectorRow({
264
- label,
265
- value,
266
- color,
267
- }: {
268
- label: string;
269
- value: string;
270
- color?: string;
271
- }) {
272
- return (
273
- <div style={{ display: 'flex', gap: 8, fontSize: 11, lineHeight: 1.5 }}>
274
- <span style={{ color: muted, minWidth: 74 }}>{label}</span>
275
- <span style={{ color: color ?? '#e6edf3', overflowWrap: 'anywhere' }}>{value}</span>
276
- </div>
277
- );
278
- }
279
-
280
- function Prototype() {
281
- const [trails, setTrails] = useState<SubsystemTrail[]>(seedTrails);
282
- const [records, setRecords] = useState<StepEmphasisRecord[]>(() =>
283
- seedEmphasis(seedTrails),
284
- );
285
- const emphasis = useMemo(() => buildStepEmphasisMap(records), [records]);
286
- const orphans = useMemo(() => orphanedEmphasis(records, trails), [records, trails]);
287
-
288
- const removeGuidance = useCallback(
289
- (trail: SubsystemTrail, stepIndex: number) => {
290
- const step = trail.steps[stepIndex];
291
- if (!step) return;
292
- const stepKey = stepClaimKey(step);
293
- setRecords((prev) =>
294
- prev.filter((r) => !(r.trailId === trail.id && r.stepKey === stepKey)),
295
- );
296
- },
297
- [],
298
- );
299
-
300
- const swapFirstSteps = useCallback(() => {
301
- setTrails((prev) =>
302
- prev.map((trail, trailIndex) =>
303
- trailIndex === 0 && trail.steps.length > 2
304
- ? {
305
- ...trail,
306
- steps: [trail.steps[1]!, trail.steps[0]!, ...trail.steps.slice(2)],
307
- }
308
- : trail,
309
- ),
310
- );
311
- }, []);
312
-
313
- const editStepClaim = useCallback(() => {
314
- setTrails((prev) =>
315
- prev.map((trail, trailIndex) =>
316
- trailIndex === 0
317
- ? {
318
- ...trail,
319
- steps: trail.steps.map((step, stepIndex) =>
320
- stepIndex === 1
321
- ? {
322
- ...step,
323
- span: step.span
324
- ? { ...step.span, startLine: step.span.startLine + 1, contentHash: 'edited' }
325
- : { startLine: 1, endLine: 1, contentHash: 'edited' },
326
- }
327
- : step,
328
- ),
329
- }
330
- : trail,
331
- ),
332
- );
333
- }, []);
334
-
335
- const addAgentMark = useCallback(() => {
336
- const trail = trails[1];
337
- const step = trail?.steps[3];
338
- if (!trail || !step) return;
339
- const stepKey = stepClaimKey(step);
340
- setRecords((prev) =>
341
- prev.some((r) => r.trailId === trail.id && r.stepKey === stepKey)
342
- ? prev
343
- : [
344
- ...prev,
345
- {
346
- trailId: trail.id,
347
- stepKey,
348
- guidance:
349
- 'Marked by **opencode** while tracing the save path. The panel scan runs again after the write to refresh the list.',
350
- provenance: {
351
- source: 'agent',
352
- at: new Date().toISOString(),
353
- author: 'opencode',
354
- },
355
- },
356
- ],
357
- );
358
- }, [trails]);
359
-
360
- const pruneOrphans = useCallback(() => {
361
- setRecords((prev) => {
362
- const dead = new Set(orphans.map((r) => `${r.trailId}:${r.stepKey}`));
363
- return prev.filter((r) => !dead.has(`${r.trailId}:${r.stepKey}`));
364
- });
365
- }, [orphans]);
366
-
367
- const documentModel: StepEmphasisDocument = {
368
- version: 1,
369
- modelId: 'sg-prototype',
370
- emphasis: records,
371
- };
372
-
373
- const button = (label: string, onClick: () => void, disabled = false) => (
374
- <button
375
- type="button"
376
- onClick={onClick}
377
- disabled={disabled}
378
- style={{
379
- padding: '5px 9px',
380
- borderRadius: 5,
381
- border: `1px solid ${border}`,
382
- background: '#171a21',
383
- color: disabled ? '#5b636e' : '#e6edf3',
384
- fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace',
385
- fontSize: 11,
386
- cursor: disabled ? 'default' : 'pointer',
387
- }}
388
- >
389
- {label}
390
- </button>
391
- );
392
-
393
- return (
394
- <div style={{ display: 'flex', height: '100vh', background: panelBg }}>
395
- <div style={{ flex: 1, minWidth: 0, position: 'relative' }}>
396
- <SubsystemComponentGraph
397
- components={components}
398
- trails={trails}
399
- initialTrailId="tl-open-drawing"
400
- persistKey="step-emphasis-prototype"
401
- stepEmphasis={emphasis}
402
- onRemoveStepEmphasis={removeGuidance}
403
- title="Step emphasis prototype"
404
- description="Key steps carry a star and markdown guidance. The mark lives in a separate augmentation document, keyed by the step's claim."
405
- />
406
- </div>
407
-
408
- <aside
409
- style={{
410
- width: 380,
411
- flexShrink: 0,
412
- display: 'flex',
413
- flexDirection: 'column',
414
- borderLeft: `1px solid ${border}`,
415
- background: '#0b0d11',
416
- overflow: 'hidden',
417
- }}
418
- >
419
- <header style={{ padding: '14px 16px 10px', borderBottom: `1px solid ${border}` }}>
420
- <div
421
- style={{
422
- display: 'flex',
423
- alignItems: 'center',
424
- gap: 6,
425
- fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace',
426
- fontSize: 12,
427
- color: '#e6edf3',
428
- }}
429
- >
430
- <Star size={13} fill="#f5a524" color="#f5a524" />
431
- Step emphasis · prototype
432
- </div>
433
- <p style={{ margin: '6px 0 0', fontSize: 11, lineHeight: 1.5, color: muted }}>
434
- A separate augmentation document, not the portable model. Marks key on the
435
- step&apos;s <em>claim</em>, so reorder keeps them and a claim edit orphans them.
436
- </p>
437
- </header>
438
-
439
- <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, padding: '10px 16px' }}>
440
- {button('Reorder steps', swapFirstSteps)}
441
- {button('Edit step 2 claim', editStepClaim)}
442
- {button('Add agent mark', addAgentMark)}
443
- {button(
444
- `Prune ${orphans.length} orphan${orphans.length === 1 ? '' : 's'}`,
445
- pruneOrphans,
446
- orphans.length === 0,
447
- )}
448
- </div>
449
-
450
- <div style={{ padding: '0 16px 10px', display: 'grid', gap: 3 }}>
451
- <InspectorRow label="store" value="~/.principal/subsystem-model-augmentations/" />
452
- <InspectorRow label="modelId" value={documentModel.modelId} />
453
- <InspectorRow
454
- label="marks"
455
- value={`${records.length} total · ${orphans.length} orphaned`}
456
- color={orphans.length > 0 ? '#e5534b' : undefined}
457
- />
458
- </div>
459
-
460
- <div
461
- style={{
462
- display: 'flex',
463
- alignItems: 'center',
464
- gap: 6,
465
- padding: '0 16px 10px',
466
- fontSize: 10,
467
- color: muted,
468
- }}
469
- >
470
- <Star size={10} fill="#f5a524" color="#f5a524" />
471
- A mark is a single star plus markdown guidance.
472
- </div>
473
-
474
- {orphans.length > 0 && (
475
- <div style={{ padding: '0 16px 10px', display: 'grid', gap: 4 }}>
476
- <span style={{ fontSize: 10, letterSpacing: '0.08em', color: '#e5534b' }}>
477
- ORPHANED — the claim changed
478
- </span>
479
- {orphans.map((r) => (
480
- <div
481
- key={`${r.trailId}:${r.stepKey}`}
482
- style={{ fontSize: 11, color: muted, fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace' }}
483
- >
484
- <span style={{ color: '#f5a524' }}>Key</span> {r.trailId} · {r.stepKey}
485
- </div>
486
- ))}
487
- </div>
488
- )}
489
-
490
- <div
491
- style={{
492
- flex: 1,
493
- minHeight: 0,
494
- overflow: 'auto',
495
- borderTop: `1px solid ${border}`,
496
- padding: '10px 16px 16px',
497
- }}
498
- >
499
- <div
500
- style={{
501
- fontSize: 10,
502
- letterSpacing: '0.08em',
503
- color: muted,
504
- marginBottom: 6,
505
- }}
506
- >
507
- AUGMENTATION DOCUMENT
508
- </div>
509
- <pre
510
- style={{
511
- margin: 0,
512
- fontSize: 10,
513
- lineHeight: 1.45,
514
- color: '#c9d1d9',
515
- whiteSpace: 'pre-wrap',
516
- }}
517
- >
518
- {JSON.stringify(documentModel, null, 2)}
519
- </pre>
520
- </div>
521
- </aside>
522
- </div>
523
- );
524
- }
525
-
526
- export const Interactive: Story = {
527
- render: () => <Prototype />,
528
- };
@@ -1,160 +0,0 @@
1
- import React, { useState } from 'react';
2
- import type { Meta, StoryObj } from '@storybook/react';
3
- import { ThemeProvider, defaultEditorTheme } from '@principal-ade/industry-theme';
4
- import {
5
- DecorationPanel,
6
- type DecorationPanelProps,
7
- } from '../../subsystem/DecorationPanel';
8
- import {
9
- decorationTrailKey,
10
- type SubsystemDecoration,
11
- } from '../../subsystem/decoration';
12
-
13
- /**
14
- * PROTOTYPE: the decoration container as a reading surface.
15
- *
16
- * The fixture deliberately spans two models (desktop + sync service) and
17
- * includes one draft trail + draft structure, to show that a decoration is
18
- * additive and unverified — the base models stay untouched.
19
- */
20
- const meta = {
21
- title: 'Components/DecorationPanel',
22
- component: DecorationPanel,
23
- parameters: { layout: 'fullscreen' },
24
- decorators: [
25
- (Story) => (
26
- <ThemeProvider theme={defaultEditorTheme}>
27
- <Story />
28
- </ThemeProvider>
29
- ),
30
- ],
31
- } satisfies Meta<typeof DecorationPanel>;
32
-
33
- export default meta;
34
- type Story = StoryObj<typeof meta>;
35
-
36
- const decoration: SubsystemDecoration = {
37
- version: 1,
38
- id: 'dec-local-first-sync',
39
- title: 'Local-first sync',
40
- description: [
41
- 'How a drawing reaches disk **and** the sync service, in the order a reader should follow it.',
42
- '',
43
- '- The desktop stays the source of truth for the open tab.',
44
- '- The service is a projection; it never blocks the save.',
45
- '',
46
- '> This is a reading path, not a verified model claim.',
47
- ].join('\n'),
48
- trails: [
49
- {
50
- kind: 'base',
51
- modelId: 'sg-desktop',
52
- trailId: 'tl-save',
53
- stepAnchors: [{ stepKey: 'fixture', note: 'Write first; the sync hop is optional.' }],
54
- },
55
- { kind: 'base', modelId: 'sg-server', trailId: 'tl-persist' },
56
- {
57
- kind: 'draft',
58
- modelId: 'sg-desktop',
59
- trail: {
60
- id: 'tl-planned-sync',
61
- title: 'Planned sync (draft)',
62
- steps: [
63
- {
64
- from: 'tab',
65
- to: 'sync-agent',
66
- mechanism: 'produces',
67
- file: 'src/components/DrawingTabContent.tsx',
68
- purl: 'pkg:github/principal-ai/desktop-app#src/components/DrawingTabContent.tsx',
69
- symbol: 'DrawingTabContent.emitSaved',
70
- },
71
- {
72
- from: 'sync-agent',
73
- to: 'sync-service',
74
- mechanism: 'calls',
75
- file: 'src/sync/syncAgent.ts',
76
- purl: 'pkg:github/principal-ai/desktop-app#src/sync/syncAgent.ts',
77
- symbol: 'SyncAgent.push',
78
- },
79
- ],
80
- },
81
- },
82
- ],
83
- draft: {
84
- components: [
85
- {
86
- alias: 'sync-agent',
87
- name: 'SyncAgent',
88
- construct: 'class',
89
- purpose: 'queues a saved drawing for the sync service',
90
- process: 'draw-host',
91
- },
92
- {
93
- alias: 'sync-service',
94
- name: 'SyncService',
95
- construct: 'external',
96
- purpose: 'remote projection of saved drawings',
97
- },
98
- ],
99
- edges: [
100
- { from: 'tab', to: 'sync-agent', mechanism: 'produces' },
101
- { from: 'sync-agent', to: 'sync-service', mechanism: 'calls' },
102
- ],
103
- },
104
- provenance: {
105
- source: 'author',
106
- author: 'you',
107
- at: '2026-10-20T12:00:00.000Z',
108
- },
109
- };
110
-
111
- const resolveTrail: NonNullable<DecorationPanelProps['resolveTrail']> = (ref) => {
112
- if (ref.kind === 'draft') return undefined;
113
- const key = decorationTrailKey(ref);
114
- if (key === 'sg-desktop:tl-save') return { title: 'Save drawing', stepCount: 4 };
115
- if (key === 'sg-server:tl-persist') return { title: 'Persist file', stepCount: 5 };
116
- return undefined;
117
- };
118
-
119
- const modelLabels = new Map([
120
- ['sg-desktop', 'desktop-app'],
121
- ['sg-server', 'sync-service'],
122
- ]);
123
-
124
- function Demo() {
125
- const [active, setActive] = useState(true);
126
- const [selected, setSelected] = useState<string>('none');
127
-
128
- return (
129
- <div style={{ display: 'flex', height: '100vh', background: '#0f1115' }}>
130
- <div style={{ width: 420, height: '100%', borderRight: '1px solid #2a2f3a' }}>
131
- <DecorationPanel
132
- decoration={decoration}
133
- active={active}
134
- onToggleActive={setActive}
135
- resolveTrail={resolveTrail}
136
- modelLabels={modelLabels}
137
- onSelectTrail={(ref) => setSelected(decorationTrailKey(ref))}
138
- onSelectDraftComponent={(component) => setSelected(component.alias)}
139
- />
140
- </div>
141
- <div
142
- style={{
143
- flex: 1,
144
- padding: 24,
145
- color: '#9aa4b2',
146
- fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace',
147
- fontSize: 12,
148
- lineHeight: 1.6,
149
- }}
150
- >
151
- <div>active: {String(active)}</div>
152
- <div>selected: {selected}</div>
153
- </div>
154
- </div>
155
- );
156
- }
157
-
158
- export const TwoModels: Story = {
159
- render: () => <Demo />,
160
- };