pmx-canvas 0.6.5 → 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 (169) hide show
  1. package/.github/extensions/pmx-canvas/extension.mjs +67 -3
  2. package/CHANGELOG.md +54 -0
  3. package/Readme.md +29 -5
  4. package/dist/canvas/global.css +440 -24
  5. package/dist/canvas/index.js +90 -75
  6. package/dist/types/cli/agent.d.ts +3 -0
  7. package/dist/types/cli/commands/backup.d.ts +1 -0
  8. package/dist/types/cli/commands/boards.d.ts +1 -0
  9. package/dist/types/cli/commands/export.d.ts +1 -0
  10. package/dist/types/client/canvas/BoardCopyDialog.d.ts +5 -0
  11. package/dist/types/client/canvas/BoardFolderDialog.d.ts +7 -0
  12. package/dist/types/client/canvas/BoardLinkDialog.d.ts +3 -0
  13. package/dist/types/client/canvas/BoardSwitcher.d.ts +8 -0
  14. package/dist/types/client/canvas/ExportDialog.d.ts +7 -0
  15. package/dist/types/client/canvas/HomeView.d.ts +9 -0
  16. package/dist/types/client/nodes/AttachmentNode.d.ts +6 -0
  17. package/dist/types/client/nodes/BoardNode.d.ts +5 -0
  18. package/dist/types/client/state/boards-store.d.ts +79 -0
  19. package/dist/types/client/state/canvas-store.d.ts +2 -0
  20. package/dist/types/client/state/intent-bridge.d.ts +1 -0
  21. package/dist/types/client/state/session-store.d.ts +7 -0
  22. package/dist/types/client/types.d.ts +2 -2
  23. package/dist/types/json-render/catalog.d.ts +1 -1
  24. package/dist/types/json-render/charts/definitions.d.ts +1 -1
  25. package/dist/types/json-render/server.d.ts +6 -0
  26. package/dist/types/mcp/canvas-access.d.ts +10 -3
  27. package/dist/types/mcp/context-uri-template.d.ts +6 -0
  28. package/dist/types/server/agent-presence.d.ts +1 -1
  29. package/dist/types/server/attribution.d.ts +11 -0
  30. package/dist/types/server/ax-context.d.ts +3 -1
  31. package/dist/types/server/ax-interaction.d.ts +2 -2
  32. package/dist/types/server/ax-state.d.ts +13 -3
  33. package/dist/types/server/backup.d.ts +36 -0
  34. package/dist/types/server/board-export.d.ts +45 -0
  35. package/dist/types/server/canvas-db.d.ts +63 -9
  36. package/dist/types/server/canvas-operations.d.ts +23 -0
  37. package/dist/types/server/canvas-provenance.d.ts +1 -1
  38. package/dist/types/server/canvas-state.d.ts +140 -3
  39. package/dist/types/server/context-brief.d.ts +111 -0
  40. package/dist/types/server/context-reads.d.ts +55 -0
  41. package/dist/types/server/document-import.d.ts +66 -0
  42. package/dist/types/server/index.d.ts +103 -4
  43. package/dist/types/server/mutation-history.d.ts +1 -0
  44. package/dist/types/server/operations/invoker.d.ts +2 -0
  45. package/dist/types/server/operations/mcp.d.ts +7 -1
  46. package/dist/types/server/operations/ops/ax-shared.d.ts +2 -2
  47. package/dist/types/server/operations/ops/backup.d.ts +2 -0
  48. package/dist/types/server/operations/ops/boards.d.ts +10 -0
  49. package/dist/types/server/operations/ops/export.d.ts +2 -0
  50. package/dist/types/server/operations/ops/imports.d.ts +2 -0
  51. package/dist/types/server/operations/ops/nodes.d.ts +1 -1
  52. package/dist/types/server/operations/ops/query.d.ts +2 -0
  53. package/dist/types/server/operations/registry.d.ts +9 -1
  54. package/dist/types/server/operations/types.d.ts +1 -1
  55. package/dist/types/server/server.d.ts +1 -0
  56. package/dist/types/server/trace-manager.d.ts +7 -0
  57. package/dist/types/server/webpage-node.d.ts +22 -1
  58. package/dist/types/server/workbench-auth.d.ts +3 -0
  59. package/dist/types/shared/boards.d.ts +6 -0
  60. package/docs/RELEASE.md +5 -0
  61. package/docs/cli.md +68 -0
  62. package/docs/http-api.md +198 -2
  63. package/docs/mcp.md +71 -6
  64. package/docs/node-types.md +26 -0
  65. package/docs/plans/plan-010-attributed-ask-answers.md +39 -0
  66. package/docs/plans/plan-011-context-read-instrumentation.md +54 -0
  67. package/docs/plans/plan-012-many-boards-stage-1.md +98 -0
  68. package/docs/plans/plan-013-static-board-export.md +47 -0
  69. package/docs/plans/plan-014-connected-board-memory.md +140 -0
  70. package/docs/product-vision-2026-09.md +118 -18
  71. package/docs/sdk.md +50 -0
  72. package/package.json +1 -1
  73. package/skills/pmx-canvas/SKILL.md +53 -4
  74. package/skills/pmx-canvas/references/full-reference.md +43 -7
  75. package/skills/pmx-canvas/references/github-copilot-app-adapter.md +3 -0
  76. package/skills/pmx-canvas/references/installing-pmx-canvas.md +4 -4
  77. package/skills/pmx-canvas-orchestration/SKILL.md +5 -0
  78. package/skills/pmx-canvas-testing/SKILL.md +4 -0
  79. package/src/cli/agent.ts +4 -0
  80. package/src/cli/commands/ax.ts +13 -0
  81. package/src/cli/commands/backup.ts +66 -0
  82. package/src/cli/commands/boards.ts +71 -0
  83. package/src/cli/commands/export.ts +30 -0
  84. package/src/cli/commands/nodes.ts +4 -1
  85. package/src/cli/commands/pins.ts +6 -2
  86. package/src/cli/commands/pump.ts +3 -0
  87. package/src/cli/commands/query.ts +33 -7
  88. package/src/cli/commands/view.ts +7 -5
  89. package/src/cli/index.ts +19 -9
  90. package/src/client/App.tsx +21 -3
  91. package/src/client/canvas/BoardCopyDialog.tsx +153 -0
  92. package/src/client/canvas/BoardFolderDialog.tsx +120 -0
  93. package/src/client/canvas/BoardLinkDialog.tsx +93 -0
  94. package/src/client/canvas/BoardSwitcher.tsx +149 -0
  95. package/src/client/canvas/CanvasViewport.tsx +3 -0
  96. package/src/client/canvas/CommandPalette.tsx +107 -4
  97. package/src/client/canvas/ContextMenu.tsx +4 -0
  98. package/src/client/canvas/ExpandedNodeOverlay.tsx +3 -0
  99. package/src/client/canvas/ExportDialog.tsx +197 -0
  100. package/src/client/canvas/HomeView.tsx +277 -0
  101. package/src/client/canvas/SessionPanel.tsx +28 -1
  102. package/src/client/canvas/ToolRail.tsx +38 -3
  103. package/src/client/canvas/TopBar.tsx +34 -18
  104. package/src/client/canvas/import-files.ts +19 -1
  105. package/src/client/canvas/kind-colors.ts +1 -0
  106. package/src/client/icons.tsx +2 -0
  107. package/src/client/nodes/AttachmentNode.tsx +148 -0
  108. package/src/client/nodes/BoardNode.tsx +35 -0
  109. package/src/client/nodes/FileNode.tsx +6 -0
  110. package/src/client/nodes/MarkdownNode.tsx +47 -19
  111. package/src/client/nodes/ax-node-actions.ts +2 -5
  112. package/src/client/nodes/use-ax-surface-bridge.ts +1 -1
  113. package/src/client/state/boards-store.ts +167 -0
  114. package/src/client/state/canvas-store.ts +22 -0
  115. package/src/client/state/intent-bridge.ts +30 -2
  116. package/src/client/state/node-factory.ts +1 -0
  117. package/src/client/state/session-store.ts +5 -0
  118. package/src/client/state/sse-bridge.ts +49 -5
  119. package/src/client/theme/global.css +440 -24
  120. package/src/client/types.ts +2 -0
  121. package/src/json-render/server.ts +16 -9
  122. package/src/mcp/canvas-access.ts +59 -22
  123. package/src/mcp/context-uri-template.ts +28 -0
  124. package/src/mcp/server.ts +122 -15
  125. package/src/server/agent-context.ts +116 -29
  126. package/src/server/attribution.ts +29 -0
  127. package/src/server/ax-context.ts +4 -2
  128. package/src/server/ax-interaction.ts +1 -0
  129. package/src/server/ax-state-manager.ts +72 -3
  130. package/src/server/ax-state.ts +41 -0
  131. package/src/server/backup.ts +178 -0
  132. package/src/server/board-export.ts +686 -0
  133. package/src/server/canvas-db.ts +599 -107
  134. package/src/server/canvas-operations.ts +68 -9
  135. package/src/server/canvas-provenance.ts +1 -0
  136. package/src/server/canvas-schema.ts +12 -0
  137. package/src/server/canvas-state.ts +774 -45
  138. package/src/server/context-brief.ts +372 -0
  139. package/src/server/context-reads.ts +238 -0
  140. package/src/server/demo-state.json +659 -2
  141. package/src/server/document-import.ts +223 -0
  142. package/src/server/index.ts +231 -5
  143. package/src/server/mutation-history.ts +10 -13
  144. package/src/server/operations/composites.ts +26 -4
  145. package/src/server/operations/http.ts +9 -3
  146. package/src/server/operations/index.ts +8 -0
  147. package/src/server/operations/invoker.ts +14 -3
  148. package/src/server/operations/mcp.ts +50 -11
  149. package/src/server/operations/ops/app.ts +5 -0
  150. package/src/server/operations/ops/ax-read.ts +10 -6
  151. package/src/server/operations/ops/ax-timeline.ts +75 -0
  152. package/src/server/operations/ops/backup.ts +134 -0
  153. package/src/server/operations/ops/batch.ts +8 -1
  154. package/src/server/operations/ops/boards.ts +351 -0
  155. package/src/server/operations/ops/export.ts +74 -0
  156. package/src/server/operations/ops/ext-app.ts +4 -0
  157. package/src/server/operations/ops/imports.ts +220 -0
  158. package/src/server/operations/ops/nodes.ts +57 -5
  159. package/src/server/operations/ops/query.ts +206 -9
  160. package/src/server/operations/registry.ts +123 -31
  161. package/src/server/operations/types.ts +1 -1
  162. package/src/server/server.ts +146 -2
  163. package/src/server/spatial-analysis.ts +9 -17
  164. package/src/server/trace-manager.ts +19 -0
  165. package/src/server/web-artifacts.ts +2 -1
  166. package/src/server/webpage-node.ts +173 -13
  167. package/src/server/workbench-auth.ts +11 -0
  168. package/src/shared/boards.ts +18 -0
  169. package/src/shared/node-sizes.ts +1 -0
package/src/cli/agent.ts CHANGED
@@ -26,6 +26,9 @@ import './commands/groups.js';
26
26
  import './commands/view.js';
27
27
  import './commands/query.js';
28
28
  import './commands/pins.js';
29
+ import './commands/boards.js';
30
+ import './commands/backup.js';
31
+ import './commands/export.js';
29
32
  import './commands/history.js';
30
33
  import './commands/ax.js';
31
34
  import './commands/pump.js';
@@ -78,6 +81,7 @@ Canvas commands:
78
81
  pmx-canvas layout Full canvas state (JSON)
79
82
  pmx-canvas status Quick summary
80
83
  pmx-canvas search <query> Search nodes by content
84
+ pmx-canvas context [options] Budgeted cross-board context brief
81
85
  pmx-canvas open Open the current workbench in a browser
82
86
  pmx-canvas arrange [--layout MODE] Auto-arrange (grid|column|flow)
83
87
  pmx-canvas batch [--file FILE] Run many canvas operations at once
@@ -384,6 +384,19 @@ cmd(
384
384
  },
385
385
  );
386
386
 
387
+ cmd(
388
+ 'ax reads',
389
+ 'Read the context read log (which context each agent read, and whether pinned nodes reached it)',
390
+ ['pmx-canvas ax reads', 'pmx-canvas ax reads --limit 200'],
391
+ async (args) => {
392
+ const { flags } = parseFlags(args);
393
+ if (flags.help || flags.h) return showCommandHelp('ax reads');
394
+
395
+ const limit = optionalNumberFlag(flags, 'limit', 'pmx-canvas ax reads --limit <n>');
396
+ output(await invokeOperation('ax.reads.list', limit ? { limit } : {}));
397
+ },
398
+ );
399
+
387
400
  cmd(
388
401
  'ax work add',
389
402
  'Add a canvas-bound AX work item',
@@ -0,0 +1,66 @@
1
+ // Library backup commands (plan 012): backup, backup status, backup schedule, restore.
2
+
3
+ import { resolve } from 'node:path';
4
+ import {
5
+ cmd,
6
+ die,
7
+ getStringFlag,
8
+ invokeOperation,
9
+ optionalNumberFlag,
10
+ output,
11
+ parseFlags,
12
+ showCommandHelp,
13
+ } from '../shared.js';
14
+
15
+ cmd(
16
+ 'backup',
17
+ 'Back up every board to a folder (safe while the server runs)',
18
+ ['pmx-canvas backup', 'pmx-canvas backup --to ~/Backups/pmx --keep 14'],
19
+ async (args) => {
20
+ const { flags } = parseFlags(args);
21
+ if (flags.help || flags.h) return showCommandHelp('backup');
22
+ const to = getStringFlag(flags, 'to');
23
+ const keep = optionalNumberFlag(flags, 'keep', 'pmx-canvas backup --keep <n>');
24
+ output(await invokeOperation('backup.run', { ...(to ? { to: resolve(to) } : {}), ...(keep ? { keep } : {}) }));
25
+ },
26
+ );
27
+
28
+ cmd('backup status', 'Show the backup folder, schedule and backups', ['pmx-canvas backup status'], async (args) => {
29
+ const { flags } = parseFlags(args);
30
+ if (flags.help || flags.h) return showCommandHelp('backup status');
31
+ output(await invokeOperation('backup.status', {}));
32
+ });
33
+
34
+ cmd(
35
+ 'backup schedule',
36
+ 'Back up on a schedule, run by the server',
37
+ ['pmx-canvas backup schedule --every 24h --keep 14 --to ~/Backups/pmx', 'pmx-canvas backup schedule --off'],
38
+ async (args) => {
39
+ const { flags } = parseFlags(args);
40
+ if (flags.help || flags.h) return showCommandHelp('backup schedule');
41
+ const every = flags.off ? 'off' : getStringFlag(flags, 'every');
42
+ if (!every) die('Usage: pmx-canvas backup schedule --every <30m|24h|7d> [--keep <n>] [--to <folder>] | --off');
43
+ const to = getStringFlag(flags, 'to');
44
+ const keep = optionalNumberFlag(flags, 'keep', 'pmx-canvas backup schedule --keep <n>');
45
+ output(
46
+ await invokeOperation('backup.schedule', {
47
+ every,
48
+ ...(to ? { to: resolve(to) } : {}),
49
+ ...(keep ? { keep } : {}),
50
+ }),
51
+ );
52
+ },
53
+ );
54
+
55
+ cmd(
56
+ 'restore',
57
+ 'Replace every board with a backup (the current file is kept as canvas.db.before-restore)',
58
+ ['pmx-canvas restore ~/Backups/pmx/canvas-20260927T090000000Z.db'],
59
+ async (args) => {
60
+ const { positional, flags } = parseFlags(args);
61
+ if (flags.help || flags.h) return showCommandHelp('restore');
62
+ const file = positional[0];
63
+ if (!file) die('Usage: pmx-canvas restore <backup-file>');
64
+ output(await invokeOperation('backup.restore', { file: resolve(file) }));
65
+ },
66
+ );
@@ -0,0 +1,71 @@
1
+ // Board commands (plan 012): board list, board create, board rename, board category.
2
+ // Opening, switching and deleting boards is the human's, in the workbench.
3
+
4
+ import { cmd, die, getStringFlag, invokeOperation, output, parseFlags, showCommandHelp } from '../shared.js';
5
+
6
+ cmd('board list', 'List boards, most recently opened first', ['pmx-canvas board list'], async (args) => {
7
+ const { flags } = parseFlags(args);
8
+ if (flags.help || flags.h) return showCommandHelp('board list');
9
+ output(await invokeOperation('board.list', {}));
10
+ });
11
+
12
+ cmd(
13
+ 'board from',
14
+ 'Preview or create a board from selected cards on another board',
15
+ ['pmx-canvas board from <source-id> "Follow-up" --nodes id1,id2 --readme --structure'],
16
+ async (args) => {
17
+ const { positional, flags } = parseFlags(args, { boolFlags: ['preview', 'readme', 'structure'] });
18
+ if (flags.help || flags.h) return showCommandHelp('board from');
19
+ const [sourceBoardId, ...nameParts] = positional;
20
+ const name = nameParts.join(' ').trim() || 'New board';
21
+ if (!sourceBoardId) die('Usage: pmx-canvas board from <source-board-id> <name>');
22
+ const nodes = getStringFlag(flags, 'nodes');
23
+ output(
24
+ await invokeOperation('board.create-from', {
25
+ sourceBoardId,
26
+ name,
27
+ ...(nodes ? { nodeIds: nodes.split(',').filter(Boolean) } : {}),
28
+ includeReadme: !!flags.readme,
29
+ includeStructure: !!flags.structure,
30
+ preview: !!flags.preview,
31
+ }),
32
+ );
33
+ },
34
+ );
35
+
36
+ cmd(
37
+ 'board create',
38
+ 'Create an empty board (the human opens it in the workbench)',
39
+ ['pmx-canvas board create "Q4 planning"', 'pmx-canvas board create "Q4 planning" --category Planning'],
40
+ async (args) => {
41
+ const { positional, flags } = parseFlags(args);
42
+ if (flags.help || flags.h) return showCommandHelp('board create');
43
+ const name = positional.join(' ').trim();
44
+ if (!name) die('Usage: pmx-canvas board create <name> [--category <category>]');
45
+ const category = getStringFlag(flags, 'category');
46
+ output(await invokeOperation('board.create', { name, ...(category ? { category } : {}) }));
47
+ },
48
+ );
49
+
50
+ cmd('board rename', 'Rename a board', ['pmx-canvas board rename board-abc123 "Q4 planning"'], async (args) => {
51
+ const { positional, flags } = parseFlags(args);
52
+ if (flags.help || flags.h) return showCommandHelp('board rename');
53
+ const [id, ...rest] = positional;
54
+ const name = rest.join(' ').trim();
55
+ if (!id || !name) die('Usage: pmx-canvas board rename <board-id> <name>');
56
+ output(await invokeOperation('board.update', { id, name }));
57
+ });
58
+
59
+ cmd(
60
+ 'board category',
61
+ 'File a board under a category on Home (--clear removes it)',
62
+ ['pmx-canvas board category board-abc123 "Planning"', 'pmx-canvas board category board-abc123 --clear'],
63
+ async (args) => {
64
+ const { positional, flags } = parseFlags(args);
65
+ if (flags.help || flags.h) return showCommandHelp('board category');
66
+ const [id, ...rest] = positional;
67
+ const category = flags.clear ? '' : rest.join(' ').trim();
68
+ if (!id || (!category && !flags.clear)) die('Usage: pmx-canvas board category <board-id> <category> | --clear');
69
+ output(await invokeOperation('board.update', { id, category }));
70
+ },
71
+ );
@@ -0,0 +1,30 @@
1
+ // Static board export (plan 013): export.
2
+
3
+ import { copyFileSync } from 'node:fs';
4
+ import { resolve } from 'node:path';
5
+ import { cmd, getStringFlag, invokeOperation, output, parseFlags, showCommandHelp } from '../shared.js';
6
+
7
+ cmd(
8
+ 'export',
9
+ 'Export a board as one self-contained HTML file anyone can open',
10
+ [
11
+ 'pmx-canvas export',
12
+ 'pmx-canvas export --board board-abc123 --out ~/Desktop/okrs.html',
13
+ 'pmx-canvas export --include-files',
14
+ ],
15
+ async (args) => {
16
+ const { flags } = parseFlags(args);
17
+ if (flags.help || flags.h) return showCommandHelp('export');
18
+ const board = getStringFlag(flags, 'board');
19
+ const out = getStringFlag(flags, 'out');
20
+ const result = (await invokeOperation('export.run', {
21
+ ...(board ? { board } : {}),
22
+ includeFiles: flags['include-files'] === true,
23
+ })) as { path: string };
24
+ if (out) {
25
+ copyFileSync(result.path, resolve(out));
26
+ result.path = resolve(out);
27
+ }
28
+ output(result);
29
+ },
30
+ );
@@ -796,7 +796,10 @@ cmd(
796
796
  const id = positional[0];
797
797
  if (!id) die('Missing node ID', 'pmx-canvas node get <node-id>');
798
798
 
799
- const result = (await invokeOperation('node.get', { id })) as Record<string, unknown>;
799
+ const result = (await invokeOperation('node.get', {
800
+ id,
801
+ board: typeof flags.board === 'string' ? flags.board : undefined,
802
+ })) as Record<string, unknown>;
800
803
  const requestedFields = collectRequestedFields(args, flags);
801
804
  if (requestedFields.length > 0) {
802
805
  const picked = Object.fromEntries(requestedFields.map((field) => [field, resolveNodeFieldValue(result, field)]));
@@ -17,7 +17,9 @@ cmd(
17
17
  if (flags.help || flags.h) return showCommandHelp('pin');
18
18
 
19
19
  if (flags.list) {
20
- const result = await invokeOperation('pinned-context.get', {});
20
+ const result = await invokeOperation('pinned-context.get', {
21
+ board: typeof flags.board === 'string' ? flags.board : undefined,
22
+ });
21
23
  output(result);
22
24
  return;
23
25
  }
@@ -36,7 +38,9 @@ cmd(
36
38
  }
37
39
 
38
40
  // Default: list
39
- const result = await invokeOperation('pinned-context.get', {});
41
+ const result = await invokeOperation('pinned-context.get', {
42
+ board: typeof flags.board === 'string' ? flags.board : undefined,
43
+ });
40
44
  output(result);
41
45
  },
42
46
  );
@@ -91,6 +91,9 @@ function runExec(template: string, steer: PendingSteer, consumer: string): Promi
91
91
  PMX_STEER_CONSUMER: consumer,
92
92
  },
93
93
  });
94
+ // A command that exits without reading stdin (`exit 1`, a notifier) closes
95
+ // the pipe first; the EPIPE is not the command's result — its exit code is.
96
+ child.stdin.on('error', () => {});
94
97
  child.stdin.write(steer.message);
95
98
  child.stdin.end();
96
99
  child.on('close', (code) => resolve(code ?? 1));
@@ -36,11 +36,30 @@ cmd(
36
36
  const query = positional[0] || (typeof flags.query === 'string' ? flags.query : '');
37
37
  if (!query) die('Missing search query', 'pmx-canvas search "query"');
38
38
 
39
- const result = await invokeOperation('search', { q: query });
39
+ const result = await invokeOperation('search', { q: query, ...(flags.library ? { scope: 'library' } : {}) });
40
40
  output(result);
41
41
  },
42
42
  );
43
43
 
44
+ cmd(
45
+ 'context',
46
+ 'Read the budgeted cross-board context brief',
47
+ ['pmx-canvas context --consumer codex --budget 16000'],
48
+ async (args) => {
49
+ const { flags } = parseFlags(args);
50
+ if (flags.help || flags.h) return showCommandHelp('context');
51
+ output(
52
+ await invokeOperation('context.get', {
53
+ ...(typeof flags.consumer === 'string' ? { consumer: flags.consumer } : {}),
54
+ ...(typeof flags.since === 'string' || typeof flags.since === 'number' ? { since: Number(flags.since) } : {}),
55
+ ...(typeof flags.budget === 'string' || typeof flags.budget === 'number'
56
+ ? { budget: Number(flags.budget) }
57
+ : {}),
58
+ }),
59
+ );
60
+ },
61
+ );
62
+
44
63
  // ── validate ─────────────────────────────────────────────────
45
64
  cmd(
46
65
  'validate',
@@ -114,13 +133,20 @@ cmd('code-graph', 'Show auto-detected file dependency graph', ['pmx-canvas code-
114
133
  });
115
134
 
116
135
  // ── spatial ──────────────────────────────────────────────────
117
- cmd('spatial', 'Spatial analysis: clusters, reading order, neighborhoods', ['pmx-canvas spatial'], async (args) => {
118
- const { flags } = parseFlags(args);
119
- if (flags.help || flags.h) return showCommandHelp('spatial');
136
+ cmd(
137
+ 'spatial',
138
+ 'Spatial analysis: clusters, reading order, neighborhoods',
139
+ ['pmx-canvas spatial', 'pmx-canvas spatial --board <id>'],
140
+ async (args) => {
141
+ const { flags } = parseFlags(args);
142
+ if (flags.help || flags.h) return showCommandHelp('spatial');
120
143
 
121
- const result = await invokeOperation('spatial.get', {});
122
- output(result);
123
- });
144
+ const result = await invokeOperation('spatial.get', {
145
+ board: typeof flags.board === 'string' ? flags.board : undefined,
146
+ });
147
+ output(result);
148
+ },
149
+ );
124
150
 
125
151
  // ── watch ────────────────────────────────────────────────────
126
152
  cmd(
@@ -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
@@ -38,6 +38,7 @@ const AGENT_COMMANDS = new Set([
38
38
  'edge',
39
39
  'json-render',
40
40
  'search',
41
+ 'context',
41
42
  'layout',
42
43
  'status',
43
44
  'arrange',
@@ -45,6 +46,10 @@ const AGENT_COMMANDS = new Set([
45
46
  'fit',
46
47
  'screenshot',
47
48
  'pin',
49
+ 'board',
50
+ 'backup',
51
+ 'restore',
52
+ 'export',
48
53
  'ax',
49
54
  'pump',
50
55
  'undo',
@@ -306,6 +311,10 @@ Agent CLI (works against running server):
306
311
  diagram add Add an Excalidraw diagram node
307
312
  html primitive add|schema Add or inspect HTML communication primitives
308
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
309
318
  undo / redo / history Time travel
310
319
  snapshot save|list|restore|diff|delete
311
320
  Manage snapshots
@@ -425,14 +434,15 @@ Examples:
425
434
  console.log(' pmx-canvas --help (all commands)');
426
435
  console.log('\n Press Ctrl+C to stop\n');
427
436
 
428
- process.on('SIGINT', () => {
437
+ const shutdown = () => {
429
438
  console.log('\nShutting down...');
430
- canvas.stop();
431
- process.exit(0);
432
- });
433
-
434
- process.on('SIGTERM', () => {
435
- canvas.stop();
436
- process.exit(0);
437
- });
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);
438
448
  }
@@ -8,9 +8,13 @@ import { CanvasViewport } from './canvas/CanvasViewport';
8
8
  import { CommandBar } from './canvas/CommandBar';
9
9
  import { ConnectionBanner } from './canvas/ConnectionBanner';
10
10
  import { CommandPalette } from './canvas/CommandPalette';
11
+ import { BoardLinkDialog } from './canvas/BoardLinkDialog';
11
12
  import { ContextMenu, useContextMenu } from './canvas/ContextMenu';
12
13
  import { ContextPinBar } from './canvas/ContextPinBar';
13
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';
14
18
  import { createNodeInView } from './canvas/create-in-view';
15
19
  import { undoFromKeyboard } from './state/session-store';
16
20
  import { ActivityFeed, WritersSheet } from './canvas/ExternalWriters';
@@ -67,11 +71,13 @@ export function App() {
67
71
  const [minimapVisible, setMinimapVisible] = useState(true);
68
72
  const [snapshotOpen, setSnapshotOpen] = useState(false);
69
73
  const [paletteOpen, setPaletteOpen] = useState(false);
74
+ const [boardLinkOpen, setBoardLinkOpen] = useState(false);
70
75
  const [shortcutsOpen, setShortcutsOpen] = useState(false);
71
76
  const [annotationTool, setAnnotationTool] = useState<AnnotationTool>(null);
72
77
  const snapshotBtnRef = useRef<HTMLButtonElement>(null);
73
78
  const { menu, openNodeMenu, openCanvasMenu, openEdgeMenu, closeMenu } = useContextMenu();
74
79
  const hasInitialLayout = hasInitialServerLayout.value;
80
+ const onHome = boardsLoaded.value && activeBoardId.value === null;
75
81
  // rail-chrome-v2: the one selector every agent surface mounts on. Exposed
76
82
  // as a data attribute so styling and tests can key on it; the quiet board
77
83
  // (no attached session) must stay byte-clean of agent chrome.
@@ -90,6 +96,12 @@ export function App() {
90
96
  return connectSSE();
91
97
  }, []);
92
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
+
93
105
  // Keep the server's idea of this window's size current (0.4.6 orb feedback
94
106
  // #2) so an agent `fit` computes a scale that actually fits the human's
95
107
  // canvas region. Connect-time reporting lives in the SSE bridge; this covers
@@ -357,16 +369,21 @@ export function App() {
357
369
  <AttentionHistory />
358
370
  <ActivityFeed />
359
371
  <WritersSheet />
360
- {hasInitialLayout && allNodes.length === 0 && intents.value.size === 0 && (
361
- <EmptyState onOpenPalette={() => setPaletteOpen(true)} />
372
+ {onHome ? (
373
+ <HomeView />
374
+ ) : (
375
+ hasInitialLayout &&
376
+ allNodes.length === 0 &&
377
+ intents.value.size === 0 && <EmptyState onOpenPalette={() => setPaletteOpen(true)} />
362
378
  )}
363
379
  {selectedNodeIds.value.size > 0 && <SelectionBar />}
364
380
  {sessionIsActive ? <CommandBar /> : contextPinnedNodeIds.value.size > 0 && <ContextPinBar />}
365
381
  <SessionReceipt onOpenSnapshots={() => setSnapshotOpen(true)} />
366
382
  <TextPrompt />
383
+ <ExportDialog />
367
384
  {expandedNodeId.value && <ExpandedNodeOverlay />}
368
385
  <SnapshotPanel open={snapshotOpen} onClose={handleCloseSnapshot} anchorRef={snapshotBtnRef} />
369
- {minimapVisible && (
386
+ {minimapVisible && !onHome && (
370
387
  <Minimap
371
388
  viewport={viewport}
372
389
  nodes={nodes}
@@ -381,6 +398,7 @@ export function App() {
381
398
  {sessionIsActive && <SessionPanel />}
382
399
  {menu && <ContextMenu menu={menu} onClose={closeMenu} />}
383
400
  {paletteOpen && <CommandPalette onClose={() => setPaletteOpen(false)} onToggleMinimap={handleToggleMinimap} />}
401
+ {boardLinkOpen && <BoardLinkDialog onClose={() => setBoardLinkOpen(false)} />}
384
402
  {shortcutsOpen && <ShortcutOverlay onClose={() => setShortcutsOpen(false)} />}
385
403
  </div>
386
404
  );
@@ -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
+ }