pmx-canvas 0.6.4 → 0.7.0

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 (204) hide show
  1. package/.github/extensions/pmx-canvas/extension.mjs +67 -3
  2. package/CHANGELOG.md +73 -0
  3. package/Readme.md +35 -5
  4. package/dist/canvas/global.css +498 -28
  5. package/dist/canvas/index.js +110 -95
  6. package/dist/canvas/mermaid-entry.js +258 -258
  7. package/dist/types/cli/agent.d.ts +4 -0
  8. package/dist/types/cli/commands/backup.d.ts +1 -0
  9. package/dist/types/cli/commands/boards.d.ts +1 -0
  10. package/dist/types/cli/commands/export.d.ts +1 -0
  11. package/dist/types/cli/commands/record.d.ts +12 -0
  12. package/dist/types/client/canvas/BoardCopyDialog.d.ts +5 -0
  13. package/dist/types/client/canvas/BoardFolderDialog.d.ts +7 -0
  14. package/dist/types/client/canvas/BoardLinkDialog.d.ts +3 -0
  15. package/dist/types/client/canvas/BoardSwitcher.d.ts +8 -0
  16. package/dist/types/client/canvas/ExportDialog.d.ts +7 -0
  17. package/dist/types/client/canvas/HomeView.d.ts +9 -0
  18. package/dist/types/client/canvas/Presentation.d.ts +13 -0
  19. package/dist/types/client/canvas/use-node-drag.d.ts +2 -1
  20. package/dist/types/client/nodes/AttachmentNode.d.ts +6 -0
  21. package/dist/types/client/nodes/BoardNode.d.ts +5 -0
  22. package/dist/types/client/nodes/McpAppNode.d.ts +24 -1
  23. package/dist/types/client/state/boards-store.d.ts +79 -0
  24. package/dist/types/client/state/canvas-store.d.ts +2 -0
  25. package/dist/types/client/state/intent-bridge.d.ts +1 -0
  26. package/dist/types/client/state/presentation.d.ts +3 -0
  27. package/dist/types/client/state/session-store.d.ts +7 -0
  28. package/dist/types/client/types.d.ts +2 -2
  29. package/dist/types/json-render/catalog.d.ts +3 -3
  30. package/dist/types/json-render/charts/definitions.d.ts +1 -1
  31. package/dist/types/json-render/server.d.ts +8 -0
  32. package/dist/types/mcp/canvas-access.d.ts +10 -3
  33. package/dist/types/mcp/context-uri-template.d.ts +6 -0
  34. package/dist/types/server/agent-presence.d.ts +1 -1
  35. package/dist/types/server/attribution.d.ts +11 -0
  36. package/dist/types/server/ax-context.d.ts +3 -1
  37. package/dist/types/server/ax-interaction.d.ts +2 -2
  38. package/dist/types/server/ax-state.d.ts +13 -3
  39. package/dist/types/server/backup.d.ts +36 -0
  40. package/dist/types/server/board-export.d.ts +45 -0
  41. package/dist/types/server/canvas-db.d.ts +65 -9
  42. package/dist/types/server/canvas-operations.d.ts +45 -0
  43. package/dist/types/server/canvas-provenance.d.ts +1 -1
  44. package/dist/types/server/canvas-schema.d.ts +1 -0
  45. package/dist/types/server/canvas-state.d.ts +146 -3
  46. package/dist/types/server/canvas-validation.d.ts +3 -0
  47. package/dist/types/server/context-brief.d.ts +111 -0
  48. package/dist/types/server/context-reads.d.ts +55 -0
  49. package/dist/types/server/document-import.d.ts +66 -0
  50. package/dist/types/server/html-surface.d.ts +1 -1
  51. package/dist/types/server/index.d.ts +107 -4
  52. package/dist/types/server/mutation-history.d.ts +1 -0
  53. package/dist/types/server/operations/invoker.d.ts +2 -0
  54. package/dist/types/server/operations/mcp.d.ts +7 -1
  55. package/dist/types/server/operations/ops/app.d.ts +2 -0
  56. package/dist/types/server/operations/ops/ax-shared.d.ts +2 -2
  57. package/dist/types/server/operations/ops/backup.d.ts +2 -0
  58. package/dist/types/server/operations/ops/boards.d.ts +10 -0
  59. package/dist/types/server/operations/ops/export.d.ts +2 -0
  60. package/dist/types/server/operations/ops/imports.d.ts +2 -0
  61. package/dist/types/server/operations/ops/json-render.d.ts +2 -0
  62. package/dist/types/server/operations/ops/nodes.d.ts +4 -2
  63. package/dist/types/server/operations/ops/query.d.ts +2 -0
  64. package/dist/types/server/operations/registry.d.ts +9 -1
  65. package/dist/types/server/operations/types.d.ts +1 -1
  66. package/dist/types/server/server.d.ts +1 -0
  67. package/dist/types/server/trace-manager.d.ts +7 -0
  68. package/dist/types/server/web-artifacts.d.ts +3 -0
  69. package/dist/types/server/webpage-node.d.ts +22 -1
  70. package/dist/types/server/workbench-auth.d.ts +3 -0
  71. package/dist/types/shared/boards.d.ts +6 -0
  72. package/dist/types/shared/tour.d.ts +72 -0
  73. package/docs/RELEASE.md +5 -0
  74. package/docs/cli.md +157 -0
  75. package/docs/http-api.md +277 -6
  76. package/docs/mcp.md +96 -7
  77. package/docs/node-types.md +106 -0
  78. package/docs/plans/plan-010-attributed-ask-answers.md +39 -0
  79. package/docs/plans/plan-011-context-read-instrumentation.md +54 -0
  80. package/docs/plans/plan-012-many-boards-stage-1.md +98 -0
  81. package/docs/plans/plan-013-static-board-export.md +47 -0
  82. package/docs/plans/plan-014-connected-board-memory.md +140 -0
  83. package/docs/product-vision-2026-09.md +118 -18
  84. package/docs/sdk.md +50 -0
  85. package/package.json +1 -1
  86. package/skills/pmx-canvas/SKILL.md +62 -5
  87. package/skills/pmx-canvas/references/full-reference.md +44 -8
  88. package/skills/pmx-canvas/references/github-copilot-app-adapter.md +3 -0
  89. package/skills/pmx-canvas/references/installing-pmx-canvas.md +4 -4
  90. package/skills/pmx-canvas-orchestration/SKILL.md +5 -0
  91. package/skills/pmx-canvas-testing/SKILL.md +4 -0
  92. package/src/cli/agent.ts +9 -0
  93. package/src/cli/commands/ax.ts +13 -0
  94. package/src/cli/commands/backup.ts +66 -0
  95. package/src/cli/commands/boards.ts +71 -0
  96. package/src/cli/commands/export.ts +30 -0
  97. package/src/cli/commands/nodes.ts +14 -1
  98. package/src/cli/commands/pins.ts +6 -2
  99. package/src/cli/commands/pump.ts +3 -0
  100. package/src/cli/commands/query.ts +33 -7
  101. package/src/cli/commands/record.ts +218 -0
  102. package/src/cli/commands/view.ts +7 -5
  103. package/src/cli/index.ts +21 -9
  104. package/src/client/App.tsx +28 -4
  105. package/src/client/canvas/AgentPresenceLayer.tsx +1 -1
  106. package/src/client/canvas/BoardCopyDialog.tsx +153 -0
  107. package/src/client/canvas/BoardFolderDialog.tsx +120 -0
  108. package/src/client/canvas/BoardLinkDialog.tsx +93 -0
  109. package/src/client/canvas/BoardSwitcher.tsx +149 -0
  110. package/src/client/canvas/CanvasNode.tsx +8 -0
  111. package/src/client/canvas/CanvasViewport.tsx +3 -0
  112. package/src/client/canvas/CommandPalette.tsx +107 -4
  113. package/src/client/canvas/ContextMenu.tsx +4 -0
  114. package/src/client/canvas/ExpandedNodeOverlay.tsx +3 -0
  115. package/src/client/canvas/ExportDialog.tsx +197 -0
  116. package/src/client/canvas/HomeView.tsx +277 -0
  117. package/src/client/canvas/IntentLayer.tsx +9 -2
  118. package/src/client/canvas/Presentation.tsx +108 -0
  119. package/src/client/canvas/SessionPanel.tsx +28 -1
  120. package/src/client/canvas/ToolRail.tsx +38 -3
  121. package/src/client/canvas/TopBar.tsx +49 -7
  122. package/src/client/canvas/import-files.ts +19 -1
  123. package/src/client/canvas/kind-colors.ts +1 -0
  124. package/src/client/canvas/use-node-drag.ts +8 -3
  125. package/src/client/icons.tsx +2 -0
  126. package/src/client/mermaid-entry.ts +39 -0
  127. package/src/client/nodes/AttachmentNode.tsx +148 -0
  128. package/src/client/nodes/BoardNode.tsx +35 -0
  129. package/src/client/nodes/FileNode.tsx +6 -0
  130. package/src/client/nodes/HtmlNode.tsx +12 -13
  131. package/src/client/nodes/MarkdownNode.tsx +47 -19
  132. package/src/client/nodes/McpAppNode.tsx +105 -13
  133. package/src/client/nodes/MermaidNode.tsx +3 -2
  134. package/src/client/nodes/ax-node-actions.ts +2 -5
  135. package/src/client/nodes/use-ax-surface-bridge.ts +1 -1
  136. package/src/client/state/boards-store.ts +167 -0
  137. package/src/client/state/canvas-store.ts +22 -0
  138. package/src/client/state/intent-bridge.ts +30 -2
  139. package/src/client/state/intent-store.ts +5 -1
  140. package/src/client/state/node-factory.ts +1 -0
  141. package/src/client/state/presentation.ts +7 -0
  142. package/src/client/state/session-store.ts +8 -0
  143. package/src/client/state/sse-bridge.ts +52 -5
  144. package/src/client/theme/global.css +498 -28
  145. package/src/client/types.ts +2 -0
  146. package/src/json-render/server.ts +45 -9
  147. package/src/mcp/canvas-access.ts +59 -22
  148. package/src/mcp/context-uri-template.ts +28 -0
  149. package/src/mcp/server.ts +122 -15
  150. package/src/server/agent-context.ts +116 -29
  151. package/src/server/agent-presence.ts +15 -9
  152. package/src/server/attribution.ts +29 -0
  153. package/src/server/ax-context.ts +4 -2
  154. package/src/server/ax-interaction.ts +1 -0
  155. package/src/server/ax-state-manager.ts +72 -3
  156. package/src/server/ax-state.ts +41 -0
  157. package/src/server/backup.ts +178 -0
  158. package/src/server/board-export.ts +686 -0
  159. package/src/server/canvas-db.ts +612 -105
  160. package/src/server/canvas-operations.ts +183 -31
  161. package/src/server/canvas-provenance.ts +1 -0
  162. package/src/server/canvas-schema.ts +22 -0
  163. package/src/server/canvas-state.ts +812 -48
  164. package/src/server/canvas-validation.ts +9 -1
  165. package/src/server/context-brief.ts +372 -0
  166. package/src/server/context-reads.ts +238 -0
  167. package/src/server/demo-state.json +670 -9
  168. package/src/server/document-import.ts +223 -0
  169. package/src/server/html-primitives.ts +199 -91
  170. package/src/server/html-surface.ts +8 -3
  171. package/src/server/index.ts +243 -5
  172. package/src/server/mutation-history.ts +10 -13
  173. package/src/server/operations/composites.ts +29 -5
  174. package/src/server/operations/http.ts +9 -3
  175. package/src/server/operations/index.ts +8 -0
  176. package/src/server/operations/invoker.ts +14 -3
  177. package/src/server/operations/mcp.ts +50 -11
  178. package/src/server/operations/ops/app.ts +15 -0
  179. package/src/server/operations/ops/ax-read.ts +10 -6
  180. package/src/server/operations/ops/ax-timeline.ts +75 -0
  181. package/src/server/operations/ops/backup.ts +134 -0
  182. package/src/server/operations/ops/batch.ts +20 -4
  183. package/src/server/operations/ops/boards.ts +351 -0
  184. package/src/server/operations/ops/export.ts +74 -0
  185. package/src/server/operations/ops/ext-app.ts +4 -0
  186. package/src/server/operations/ops/groups.ts +14 -7
  187. package/src/server/operations/ops/imports.ts +220 -0
  188. package/src/server/operations/ops/json-render.ts +37 -5
  189. package/src/server/operations/ops/nodes.ts +99 -21
  190. package/src/server/operations/ops/query.ts +206 -9
  191. package/src/server/operations/ops/viewport.ts +32 -0
  192. package/src/server/operations/registry.ts +129 -31
  193. package/src/server/operations/types.ts +1 -1
  194. package/src/server/scope-fence.ts +1 -1
  195. package/src/server/server.ts +155 -8
  196. package/src/server/spatial-analysis.ts +9 -17
  197. package/src/server/trace-manager.ts +19 -0
  198. package/src/server/web-artifacts.ts +11 -3
  199. package/src/server/webpage-node.ts +173 -13
  200. package/src/server/workbench-auth.ts +11 -0
  201. package/src/server/workboard.ts +3 -3
  202. package/src/shared/boards.ts +18 -0
  203. package/src/shared/node-sizes.ts +1 -0
  204. package/src/shared/tour.ts +87 -0
@@ -0,0 +1,218 @@
1
+ import { mkdirSync, existsSync } from 'node:fs';
2
+ import { resolve, join } from 'node:path';
3
+ import { z } from 'zod';
4
+ import { isCanvasTheme } from '../../shared/themes.js';
5
+ import { derivedTour, tourSchema, tourFrames, type Camera, type TourNode } from '../../shared/tour.js';
6
+ import {
7
+ startCanvasAutomationWebView,
8
+ evaluateCanvasAutomationWebView,
9
+ screenshotCanvasAutomationWebView,
10
+ resizeCanvasAutomationWebView,
11
+ stopCanvasAutomationWebView,
12
+ } from '../../server/server.js';
13
+ import { cmd, getBaseUrl, invokeOperation, output, parseFlags, showCommandHelp } from '../shared.js';
14
+
15
+ export function parseRecordOptions(flags: Record<string, string | boolean>) {
16
+ const number = (key: string, fallback: number) => {
17
+ if (flags[key] === true || flags[key] === false) throw new Error(`--${key} needs a number`);
18
+ return flags[key] === undefined ? fallback : Number(flags[key]);
19
+ };
20
+ for (const key of ['tour-file', 'chrome-path']) {
21
+ if (flags[key] !== undefined && typeof flags[key] !== 'string') throw new Error(`--${key} needs a path`);
22
+ }
23
+ const resolution = String(flags.resolution ?? '1280x720').match(/^(\d+)x(\d+)$/);
24
+ if (!resolution) throw new Error('Use --resolution WIDTHxHEIGHT');
25
+ const options = z
26
+ .object({
27
+ width: z.number().int().min(64).max(7680),
28
+ height: z.number().int().min(64).max(4320),
29
+ fps: z.number().int().min(1).max(120),
30
+ duration: z.number().positive().max(86400).optional(),
31
+ mode: z.enum(['realtime', 'deterministic']),
32
+ output: z.string().min(1),
33
+ })
34
+ .parse({
35
+ width: Number(resolution[1]),
36
+ height: Number(resolution[2]),
37
+ fps: number('fps', 30),
38
+ duration: flags.duration === undefined ? undefined : number('duration', 10),
39
+ mode: flags.mode ?? 'realtime',
40
+ output: typeof flags.output === 'string' ? flags.output : '',
41
+ });
42
+ if (flags.theme !== undefined && !isCanvasTheme(flags.theme)) throw new Error('Unknown --theme');
43
+ if (options.mode === 'realtime' && !options.duration && flags['stop-on-signal'] !== true) {
44
+ throw new Error('Realtime recording needs --duration SECONDS or --stop-on-signal');
45
+ }
46
+ if (options.mode === 'deterministic' && (flags.duration !== undefined || flags['stop-on-signal'])) {
47
+ throw new Error('Deterministic duration comes from tour stops; omit --duration and --stop-on-signal');
48
+ }
49
+ if (flags['tour-file'] && options.mode !== 'deterministic')
50
+ throw new Error('--tour-file requires --mode deterministic');
51
+ return {
52
+ ...options,
53
+ theme: typeof flags.theme === 'string' ? flags.theme : undefined,
54
+ present: flags.present === true,
55
+ tourFile: typeof flags['tour-file'] === 'string' ? flags['tour-file'] : undefined,
56
+ chromePath: typeof flags['chrome-path'] === 'string' ? flags['chrome-path'] : undefined,
57
+ };
58
+ }
59
+
60
+ cmd('tour get', 'Read the persisted tour or derived group order', ['pmx-canvas tour get'], async () => {
61
+ output(await invokeOperation('tour.get', {}));
62
+ });
63
+ cmd(
64
+ 'tour set',
65
+ 'Persist camera stops from JSON (null resets to group order)',
66
+ ['pmx-canvas tour set --file tour.json'],
67
+ async (args) => {
68
+ const { flags } = parseFlags(args);
69
+ if (flags.help) return showCommandHelp('tour set');
70
+ if (typeof flags.file !== 'string') throw new Error('Missing --file');
71
+ const tour = tourSchema.nullable().parse(await Bun.file(flags.file).json());
72
+ output(await invokeOperation('tour.set', { tour }));
73
+ },
74
+ );
75
+
76
+ cmd(
77
+ 'record',
78
+ 'Optionally capture the board tour or live presentation using Bun.WebView Chrome',
79
+ [
80
+ 'pmx-canvas record --duration 10 --present --output demo.mp4',
81
+ 'pmx-canvas record --mode deterministic --tour-file tour.json --present --fps 30 --output frames',
82
+ 'pmx-canvas record --stop-on-signal --resolution 1920x1080 --output live.mp4',
83
+ ],
84
+ async (args) => {
85
+ const { flags } = parseFlags(args, { boolFlags: ['present', 'stop-on-signal'] });
86
+ if (flags.help || flags.h) return showCommandHelp('record');
87
+ const options = parseRecordOptions(flags);
88
+ const outputPath = resolve(options.output);
89
+ const video = /\.mp4$/i.test(outputPath);
90
+ const framesPath = video ? `${outputPath}.frames` : outputPath;
91
+ if (existsSync(outputPath) || existsSync(framesPath)) throw new Error('Output already exists; choose a new path');
92
+ const response = await fetch(`${getBaseUrl()}/api/canvas/state`);
93
+ if (!response.ok) throw new Error(`Cannot read board: HTTP ${response.status}`);
94
+ const board = (await response.json()) as { nodes: TourNode[]; viewport: Camera; tour?: unknown };
95
+ const tour =
96
+ options.mode === 'deterministic'
97
+ ? tourSchema.parse(
98
+ options.tourFile ? await Bun.file(options.tourFile).json() : (board.tour ?? derivedTour(board.nodes)),
99
+ )
100
+ : undefined;
101
+ if (tour && !tour.stops.length) throw new Error('No tour stops: supply --tour-file or create groups/a tour');
102
+ const url = new URL('/workbench', getBaseUrl());
103
+ if (options.present) url.searchParams.set('present', '1');
104
+ if (tour) url.searchParams.set('capture', '1');
105
+ if (options.theme) url.searchParams.set('theme', options.theme);
106
+ let stopping = false;
107
+ const stop = () => {
108
+ stopping = true;
109
+ };
110
+ process.on('SIGINT', stop);
111
+ process.on('SIGTERM', stop);
112
+ let count = 0;
113
+ let duplicated = 0;
114
+ try {
115
+ await startCanvasAutomationWebView(url.href, {
116
+ backend: 'chrome',
117
+ width: options.width,
118
+ height: options.height,
119
+ chromePath: options.chromePath,
120
+ });
121
+ const deadline = Date.now() + 30_000;
122
+ while (!(await evaluateCanvasAutomationWebView('!!window.pmxCapture?.ready()'))) {
123
+ if (Date.now() > deadline) throw new Error('Workbench did not become capture-ready in 30 seconds');
124
+ await Bun.sleep(50);
125
+ }
126
+ // Chrome launch dimensions describe the outer window. WebView.resize
127
+ // applies viewport emulation, giving screenshots the requested pixels.
128
+ await resizeCanvasAutomationWebView(options.width, options.height);
129
+ await evaluateCanvasAutomationWebView(
130
+ 'new Promise(r => requestAnimationFrame(() => requestAnimationFrame(() => r(true))))',
131
+ );
132
+ await evaluateCanvasAutomationWebView('document.fonts.ready.then(() => true)');
133
+ const area = (await evaluateCanvasAutomationWebView('window.pmxCapture.area()')) as {
134
+ width: number;
135
+ height: number;
136
+ };
137
+ const initial = (await evaluateCanvasAutomationWebView('window.pmxCapture.camera()')) as Camera;
138
+ mkdirSync(framesPath, { recursive: true });
139
+ const write = async (bytes: Uint8Array) => {
140
+ await Bun.write(join(framesPath, `frame-${String(count++).padStart(6, '0')}.png`), bytes);
141
+ };
142
+ const started = performance.now();
143
+ if (tour) {
144
+ for (const camera of tourFrames(tour, initial, board.nodes, area.width, area.height, options.fps)) {
145
+ if (stopping) break;
146
+ const applied = (await evaluateCanvasAutomationWebView(
147
+ `window.pmxCapture.frame(${JSON.stringify(camera)})`,
148
+ )) as Camera;
149
+ if (applied.x !== camera.x || applied.y !== camera.y || applied.scale !== camera.scale) {
150
+ throw new Error('Capture camera was changed before paint');
151
+ }
152
+ await write(await screenshotCanvasAutomationWebView({ format: 'png' }));
153
+ }
154
+ } else {
155
+ const limit = options.duration ? Math.ceil(options.duration * options.fps) : Infinity;
156
+ while (!stopping && count < limit) {
157
+ await Bun.sleep(Math.max(0, started + (count / options.fps) * 1000 - performance.now()));
158
+ const bytes = await screenshotCanvasAutomationWebView({ format: 'png' });
159
+ await write(bytes);
160
+ // Preserve wall-clock speed if capture is slower than requested fps.
161
+ const elapsedFrames = Math.min(limit, Math.floor(((performance.now() - started) / 1000) * options.fps));
162
+ while (count < elapsedFrames) {
163
+ await write(bytes);
164
+ duplicated++;
165
+ }
166
+ }
167
+ }
168
+ await Bun.write(
169
+ join(framesPath, 'recording.json'),
170
+ JSON.stringify(
171
+ {
172
+ mode: options.mode,
173
+ fps: options.fps,
174
+ width: options.width,
175
+ height: options.height,
176
+ frames: count,
177
+ duplicated,
178
+ seconds: count / options.fps,
179
+ },
180
+ null,
181
+ 2,
182
+ ),
183
+ );
184
+ } finally {
185
+ process.off('SIGINT', stop);
186
+ process.off('SIGTERM', stop);
187
+ await stopCanvasAutomationWebView();
188
+ }
189
+ let encoded = false;
190
+ if (video && count > 0) {
191
+ const ffmpeg = Bun.which('ffmpeg');
192
+ if (!ffmpeg) console.error(`ffmpeg not found; PNG sequence retained at ${framesPath}`);
193
+ else {
194
+ const process = Bun.spawn(
195
+ [
196
+ ffmpeg,
197
+ '-n',
198
+ '-framerate',
199
+ String(options.fps),
200
+ '-i',
201
+ join(framesPath, 'frame-%06d.png'),
202
+ '-c:v',
203
+ 'libx264',
204
+ '-pix_fmt',
205
+ 'yuv420p',
206
+ '-vf',
207
+ 'pad=ceil(iw/2)*2:ceil(ih/2)*2',
208
+ outputPath,
209
+ ],
210
+ { stdout: 'ignore', stderr: 'inherit' },
211
+ );
212
+ encoded = (await process.exited) === 0;
213
+ if (!encoded) throw new Error(`ffmpeg failed; PNG sequence retained at ${framesPath}`);
214
+ }
215
+ }
216
+ output({ ok: true, output: encoded ? outputPath : framesPath, frames: count, fps: options.fps, duplicated });
217
+ },
218
+ );
@@ -42,16 +42,17 @@ cmd('open', 'Open the current workbench in the browser', ['pmx-canvas open'], as
42
42
  cmd(
43
43
  'layout',
44
44
  'Get the full canvas layout (nodes, edges, viewport)',
45
- ['pmx-canvas layout', 'pmx-canvas layout --summary'],
45
+ ['pmx-canvas layout', 'pmx-canvas layout --board <id>', 'pmx-canvas layout --summary'],
46
46
  async (args) => {
47
47
  const { flags } = parseFlags(args, { boolFlags: ['summary'] });
48
48
  if (flags.help || flags.h) return showCommandHelp('layout');
49
49
 
50
+ const board = typeof flags.board === 'string' ? flags.board : undefined;
50
51
  if (flags.summary || flags.compact) {
51
- output(await invokeOperation('summary.get', {}));
52
+ output(await invokeOperation('summary.get', { board }));
52
53
  return;
53
54
  }
54
- const result = await invokeOperation('layout.get', {});
55
+ const result = await invokeOperation('layout.get', { board });
55
56
  output(result);
56
57
  },
57
58
  );
@@ -61,12 +62,13 @@ cmd('status', 'Quick canvas summary', ['pmx-canvas status'], async (args) => {
61
62
  const { flags } = parseFlags(args);
62
63
  if (flags.help || flags.h) return showCommandHelp('status');
63
64
 
64
- const layout = (await invokeOperation('layout.get', {})) as {
65
+ const board = typeof flags.board === 'string' ? flags.board : undefined;
66
+ const layout = (await invokeOperation('layout.get', { board })) as {
65
67
  nodes: Array<Record<string, unknown>>;
66
68
  edges: unknown[];
67
69
  viewport: unknown;
68
70
  };
69
- const pinned = (await invokeOperation('pinned-context.get', {})) as { count: number; nodeIds: string[] };
71
+ const pinned = (await invokeOperation('pinned-context.get', { board })) as { count: number; nodeIds: string[] };
70
72
 
71
73
  const typeCounts: Record<string, number> = {};
72
74
  for (const n of layout.nodes) {
package/src/cli/index.ts CHANGED
@@ -32,10 +32,13 @@ if (args.includes('--version') || args.includes('-v')) {
32
32
  // ── Agent CLI subcommands ────────────────────────────────────
33
33
  // If first arg is a known subcommand (not a --flag), route to the agent CLI.
34
34
  const AGENT_COMMANDS = new Set([
35
+ 'tour',
36
+ 'record',
35
37
  'node',
36
38
  'edge',
37
39
  'json-render',
38
40
  'search',
41
+ 'context',
39
42
  'layout',
40
43
  'status',
41
44
  'arrange',
@@ -43,6 +46,10 @@ const AGENT_COMMANDS = new Set([
43
46
  'fit',
44
47
  'screenshot',
45
48
  'pin',
49
+ 'board',
50
+ 'backup',
51
+ 'restore',
52
+ 'export',
46
53
  'ax',
47
54
  'pump',
48
55
  'undo',
@@ -304,6 +311,10 @@ Agent CLI (works against running server):
304
311
  diagram add Add an Excalidraw diagram node
305
312
  html primitive add|schema Add or inspect HTML communication primitives
306
313
  pin <ids...> | --list | --clear Manage context pins
314
+ board list|create|rename|category Manage boards (the human opens them in the workbench)
315
+ backup [status|schedule] Back up every board, now or on a schedule
316
+ restore <backup-file> Replace every board with a backup
317
+ export [--board id] [--out file] Export a board as one HTML file anyone can open
307
318
  undo / redo / history Time travel
308
319
  snapshot save|list|restore|diff|delete
309
320
  Manage snapshots
@@ -423,14 +434,15 @@ Examples:
423
434
  console.log(' pmx-canvas --help (all commands)');
424
435
  console.log('\n Press Ctrl+C to stop\n');
425
436
 
426
- process.on('SIGINT', () => {
437
+ const shutdown = () => {
427
438
  console.log('\nShutting down...');
428
- canvas.stop();
429
- process.exit(0);
430
- });
431
-
432
- process.on('SIGTERM', () => {
433
- canvas.stop();
434
- process.exit(0);
435
- });
439
+ try {
440
+ canvas.stop();
441
+ process.exit(0);
442
+ } catch (error) {
443
+ console.error('Shutdown refused: canvas changes could not be saved. Fix persistence and retry shutdown.', error);
444
+ }
445
+ };
446
+ process.on('SIGINT', shutdown);
447
+ process.on('SIGTERM', shutdown);
436
448
  }
@@ -1,4 +1,6 @@
1
1
  import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
2
+ import { Presentation } from './canvas/Presentation';
3
+ import { presenting } from './state/presentation';
2
4
  import { AttentionHistory } from './canvas/AttentionHistory';
3
5
  import { AttentionToast } from './canvas/AttentionToast';
4
6
  import { registerCanvasArea, canvasArea } from './canvas/canvas-area';
@@ -6,9 +8,13 @@ import { CanvasViewport } from './canvas/CanvasViewport';
6
8
  import { CommandBar } from './canvas/CommandBar';
7
9
  import { ConnectionBanner } from './canvas/ConnectionBanner';
8
10
  import { CommandPalette } from './canvas/CommandPalette';
11
+ import { BoardLinkDialog } from './canvas/BoardLinkDialog';
9
12
  import { ContextMenu, useContextMenu } from './canvas/ContextMenu';
10
13
  import { ContextPinBar } from './canvas/ContextPinBar';
11
14
  import { EmptyState } from './canvas/EmptyState';
15
+ import { HomeView } from './canvas/HomeView';
16
+ import { ExportDialog } from './canvas/ExportDialog';
17
+ import { activeBoardId, boardsLoaded } from './state/boards-store';
12
18
  import { createNodeInView } from './canvas/create-in-view';
13
19
  import { undoFromKeyboard } from './state/session-store';
14
20
  import { ActivityFeed, WritersSheet } from './canvas/ExternalWriters';
@@ -65,11 +71,13 @@ export function App() {
65
71
  const [minimapVisible, setMinimapVisible] = useState(true);
66
72
  const [snapshotOpen, setSnapshotOpen] = useState(false);
67
73
  const [paletteOpen, setPaletteOpen] = useState(false);
74
+ const [boardLinkOpen, setBoardLinkOpen] = useState(false);
68
75
  const [shortcutsOpen, setShortcutsOpen] = useState(false);
69
76
  const [annotationTool, setAnnotationTool] = useState<AnnotationTool>(null);
70
77
  const snapshotBtnRef = useRef<HTMLButtonElement>(null);
71
78
  const { menu, openNodeMenu, openCanvasMenu, openEdgeMenu, closeMenu } = useContextMenu();
72
79
  const hasInitialLayout = hasInitialServerLayout.value;
80
+ const onHome = boardsLoaded.value && activeBoardId.value === null;
73
81
  // rail-chrome-v2: the one selector every agent surface mounts on. Exposed
74
82
  // as a data attribute so styling and tests can key on it; the quiet board
75
83
  // (no attached session) must stay byte-clean of agent chrome.
@@ -88,6 +96,12 @@ export function App() {
88
96
  return connectSSE();
89
97
  }, []);
90
98
 
99
+ useEffect(() => {
100
+ const open = () => setBoardLinkOpen(true);
101
+ window.addEventListener('pmx-open-board-link', open);
102
+ return () => window.removeEventListener('pmx-open-board-link', open);
103
+ }, []);
104
+
91
105
  // Keep the server's idea of this window's size current (0.4.6 orb feedback
92
106
  // #2) so an agent `fit` computes a scale that actually fits the human's
93
107
  // canvas region. Connect-time reporting lives in the SSE bridge; this covers
@@ -324,7 +338,11 @@ export function App() {
324
338
  const area = canvasArea();
325
339
 
326
340
  return (
327
- <div class="app-shell" data-session-active={sessionIsActive ? 'true' : 'false'}>
341
+ <div
342
+ class={`app-shell${presenting.value ? ' is-presenting' : ''}`}
343
+ data-session-active={sessionIsActive ? 'true' : 'false'}
344
+ >
345
+ <Presentation />
328
346
  <ToolRail
329
347
  minimapVisible={minimapVisible}
330
348
  onToggleMinimap={handleToggleMinimap}
@@ -351,16 +369,21 @@ export function App() {
351
369
  <AttentionHistory />
352
370
  <ActivityFeed />
353
371
  <WritersSheet />
354
- {hasInitialLayout && allNodes.length === 0 && intents.value.size === 0 && (
355
- <EmptyState onOpenPalette={() => setPaletteOpen(true)} />
372
+ {onHome ? (
373
+ <HomeView />
374
+ ) : (
375
+ hasInitialLayout &&
376
+ allNodes.length === 0 &&
377
+ intents.value.size === 0 && <EmptyState onOpenPalette={() => setPaletteOpen(true)} />
356
378
  )}
357
379
  {selectedNodeIds.value.size > 0 && <SelectionBar />}
358
380
  {sessionIsActive ? <CommandBar /> : contextPinnedNodeIds.value.size > 0 && <ContextPinBar />}
359
381
  <SessionReceipt onOpenSnapshots={() => setSnapshotOpen(true)} />
360
382
  <TextPrompt />
383
+ <ExportDialog />
361
384
  {expandedNodeId.value && <ExpandedNodeOverlay />}
362
385
  <SnapshotPanel open={snapshotOpen} onClose={handleCloseSnapshot} anchorRef={snapshotBtnRef} />
363
- {minimapVisible && (
386
+ {minimapVisible && !onHome && (
364
387
  <Minimap
365
388
  viewport={viewport}
366
389
  nodes={nodes}
@@ -375,6 +398,7 @@ export function App() {
375
398
  {sessionIsActive && <SessionPanel />}
376
399
  {menu && <ContextMenu menu={menu} onClose={closeMenu} />}
377
400
  {paletteOpen && <CommandPalette onClose={() => setPaletteOpen(false)} onToggleMinimap={handleToggleMinimap} />}
401
+ {boardLinkOpen && <BoardLinkDialog onClose={() => setBoardLinkOpen(false)} />}
378
402
  {shortcutsOpen && <ShortcutOverlay onClose={() => setShortcutsOpen(false)} />}
379
403
  </div>
380
404
  );
@@ -45,7 +45,7 @@ export function AgentPresenceLayer() {
45
45
  {cursors.map(({ presence, position }) => (
46
46
  <div
47
47
  key={presence.sessionId}
48
- class={`agent-cursor phase-${presence.phase}${presence.attached ? '' : ' is-external'}${presence.parentAgentId ? ' is-worker' : ''}`}
48
+ class={`agent-cursor phase-${presence.phase}${presence.cursor ? ' has-explicit-cursor' : ''}${presence.attached ? '' : ' is-external'}${presence.parentAgentId ? ' is-worker' : ''}`}
49
49
  data-session-id={presence.sessionId}
50
50
  style={{
51
51
  transform: `translate(${position.x}px, ${position.y}px)`,
@@ -0,0 +1,153 @@
1
+ import { useEffect, useRef, useState } from 'preact/hooks';
2
+ import {
3
+ createBoardFrom,
4
+ openBoard,
5
+ previewBoardCopy,
6
+ type BoardCopyPreview,
7
+ type BoardSummary,
8
+ } from '../state/boards-store';
9
+ import { useFocusTrap } from './use-focus-trap';
10
+
11
+ export function BoardCopyDialog({ board, onClose }: { board: BoardSummary; onClose: () => void }) {
12
+ const dialogRef = useRef<HTMLDivElement>(null);
13
+ const nameRef = useRef<HTMLInputElement>(null);
14
+ const [preview, setPreview] = useState<BoardCopyPreview | null>(null);
15
+ const [selected, setSelected] = useState<Set<string>>(new Set());
16
+ const [name, setName] = useState(`${board.name} copy`);
17
+ const [includeReadme, setIncludeReadme] = useState(true);
18
+ const [includeStructure, setIncludeStructure] = useState(true);
19
+ const [openAfter, setOpenAfter] = useState(false);
20
+ const [error, setError] = useState('');
21
+ const [saving, setSaving] = useState(false);
22
+ useFocusTrap(dialogRef, true, { initial: nameRef });
23
+
24
+ useEffect(() => {
25
+ let active = true;
26
+ void previewBoardCopy(board.id).then((result) => {
27
+ if (!active) return;
28
+ if (!result || !Array.isArray(result.cards)) return setError('Could not load reusable cards. Try again.');
29
+ setPreview(result);
30
+ setSelected(new Set(result.cards.filter((card) => card.reusable).map((card) => card.id)));
31
+ });
32
+ return () => {
33
+ active = false;
34
+ };
35
+ }, [board.id]);
36
+
37
+ const submit = async (event: Event) => {
38
+ event.preventDefault();
39
+ if (!name.trim() || !preview) return;
40
+ setSaving(true);
41
+ setError('');
42
+ const created = await createBoardFrom({
43
+ sourceBoardId: board.id,
44
+ name: name.trim(),
45
+ ...(board.category ? { category: board.category } : {}),
46
+ nodeIds: [...selected],
47
+ includeReadme,
48
+ includeStructure,
49
+ });
50
+ setSaving(false);
51
+ if (!created) return setError('Could not create the board. Try a different name.');
52
+ if (openAfter) await openBoard(created.id);
53
+ onClose();
54
+ };
55
+
56
+ return (
57
+ <div class="text-prompt-backdrop" onMouseDown={onClose}>
58
+ <div
59
+ ref={dialogRef}
60
+ class="text-prompt board-copy-dialog"
61
+ role="dialog"
62
+ aria-modal="true"
63
+ aria-labelledby="board-copy-title"
64
+ onMouseDown={(e) => e.stopPropagation()}
65
+ >
66
+ <form onSubmit={submit}>
67
+ <h2 id="board-copy-title">Create from {board.name}</h2>
68
+ <label class="board-copy-field">
69
+ Board name
70
+ <input
71
+ ref={nameRef}
72
+ class="text-prompt-input"
73
+ value={name}
74
+ onInput={(e) => setName(e.currentTarget.value)}
75
+ />
76
+ </label>
77
+ {!preview && !error && (
78
+ <div class="board-copy-status" role="status">
79
+ Loading reusable cards…
80
+ </div>
81
+ )}
82
+ {error && (
83
+ <div class="board-copy-error" role="alert">
84
+ {error}
85
+ </div>
86
+ )}
87
+ {preview && (
88
+ <fieldset class="board-copy-cards">
89
+ <legend>Cards to copy</legend>
90
+ {preview.cards.filter((card) => card.reusable).length === 0 ? (
91
+ <div class="board-copy-status">No reusable cards on this board.</div>
92
+ ) : (
93
+ preview.cards
94
+ .filter((card) => card.reusable)
95
+ .map((card) => (
96
+ <label class="board-copy-card" key={card.id}>
97
+ <input
98
+ type="checkbox"
99
+ checked={selected.has(card.id)}
100
+ onChange={(e) => {
101
+ const next = new Set(selected);
102
+ e.currentTarget.checked ? next.add(card.id) : next.delete(card.id);
103
+ setSelected(next);
104
+ }}
105
+ />
106
+ <span>
107
+ <strong>{card.title || 'Untitled card'}</strong>
108
+ <small>{card.type}</small>
109
+ </span>
110
+ </label>
111
+ ))
112
+ )}
113
+ </fieldset>
114
+ )}
115
+ <div class="board-copy-options">
116
+ <label>
117
+ <input
118
+ type="checkbox"
119
+ checked={includeStructure}
120
+ onChange={(e) => setIncludeStructure(e.currentTarget.checked)}
121
+ />{' '}
122
+ Preserve groups and connections
123
+ </label>
124
+ <label>
125
+ <input
126
+ type="checkbox"
127
+ checked={includeReadme}
128
+ disabled={!preview?.readmeNodeId}
129
+ onChange={(e) => setIncludeReadme(e.currentTarget.checked)}
130
+ />{' '}
131
+ Include README
132
+ </label>
133
+ <label>
134
+ <input type="checkbox" checked={openAfter} onChange={(e) => setOpenAfter(e.currentTarget.checked)} /> Open
135
+ the new board after creating
136
+ </label>
137
+ </div>
138
+ <p class="board-copy-note">
139
+ Only selected cards and structure are copied. Asks, history, and context pins start clear.
140
+ </p>
141
+ <div class="text-prompt-actions">
142
+ <button type="button" onClick={onClose}>
143
+ Cancel
144
+ </button>
145
+ <button type="submit" disabled={!preview || !name.trim() || saving}>
146
+ {saving ? 'Creating…' : 'Create board'}
147
+ </button>
148
+ </div>
149
+ </form>
150
+ </div>
151
+ </div>
152
+ );
153
+ }