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.
- package/.github/extensions/pmx-canvas/extension.mjs +67 -3
- package/CHANGELOG.md +54 -0
- package/Readme.md +29 -5
- package/dist/canvas/global.css +440 -24
- package/dist/canvas/index.js +90 -75
- package/dist/types/cli/agent.d.ts +3 -0
- package/dist/types/cli/commands/backup.d.ts +1 -0
- package/dist/types/cli/commands/boards.d.ts +1 -0
- package/dist/types/cli/commands/export.d.ts +1 -0
- package/dist/types/client/canvas/BoardCopyDialog.d.ts +5 -0
- package/dist/types/client/canvas/BoardFolderDialog.d.ts +7 -0
- package/dist/types/client/canvas/BoardLinkDialog.d.ts +3 -0
- package/dist/types/client/canvas/BoardSwitcher.d.ts +8 -0
- package/dist/types/client/canvas/ExportDialog.d.ts +7 -0
- package/dist/types/client/canvas/HomeView.d.ts +9 -0
- package/dist/types/client/nodes/AttachmentNode.d.ts +6 -0
- package/dist/types/client/nodes/BoardNode.d.ts +5 -0
- package/dist/types/client/state/boards-store.d.ts +79 -0
- package/dist/types/client/state/canvas-store.d.ts +2 -0
- package/dist/types/client/state/intent-bridge.d.ts +1 -0
- package/dist/types/client/state/session-store.d.ts +7 -0
- package/dist/types/client/types.d.ts +2 -2
- package/dist/types/json-render/catalog.d.ts +1 -1
- package/dist/types/json-render/charts/definitions.d.ts +1 -1
- package/dist/types/json-render/server.d.ts +6 -0
- package/dist/types/mcp/canvas-access.d.ts +10 -3
- package/dist/types/mcp/context-uri-template.d.ts +6 -0
- package/dist/types/server/agent-presence.d.ts +1 -1
- package/dist/types/server/attribution.d.ts +11 -0
- package/dist/types/server/ax-context.d.ts +3 -1
- package/dist/types/server/ax-interaction.d.ts +2 -2
- package/dist/types/server/ax-state.d.ts +13 -3
- package/dist/types/server/backup.d.ts +36 -0
- package/dist/types/server/board-export.d.ts +45 -0
- package/dist/types/server/canvas-db.d.ts +63 -9
- package/dist/types/server/canvas-operations.d.ts +23 -0
- package/dist/types/server/canvas-provenance.d.ts +1 -1
- package/dist/types/server/canvas-state.d.ts +140 -3
- package/dist/types/server/context-brief.d.ts +111 -0
- package/dist/types/server/context-reads.d.ts +55 -0
- package/dist/types/server/document-import.d.ts +66 -0
- package/dist/types/server/index.d.ts +103 -4
- package/dist/types/server/mutation-history.d.ts +1 -0
- package/dist/types/server/operations/invoker.d.ts +2 -0
- package/dist/types/server/operations/mcp.d.ts +7 -1
- package/dist/types/server/operations/ops/ax-shared.d.ts +2 -2
- package/dist/types/server/operations/ops/backup.d.ts +2 -0
- package/dist/types/server/operations/ops/boards.d.ts +10 -0
- package/dist/types/server/operations/ops/export.d.ts +2 -0
- package/dist/types/server/operations/ops/imports.d.ts +2 -0
- package/dist/types/server/operations/ops/nodes.d.ts +1 -1
- package/dist/types/server/operations/ops/query.d.ts +2 -0
- package/dist/types/server/operations/registry.d.ts +9 -1
- package/dist/types/server/operations/types.d.ts +1 -1
- package/dist/types/server/server.d.ts +1 -0
- package/dist/types/server/trace-manager.d.ts +7 -0
- package/dist/types/server/webpage-node.d.ts +22 -1
- package/dist/types/server/workbench-auth.d.ts +3 -0
- package/dist/types/shared/boards.d.ts +6 -0
- package/docs/RELEASE.md +5 -0
- package/docs/cli.md +68 -0
- package/docs/http-api.md +198 -2
- package/docs/mcp.md +71 -6
- package/docs/node-types.md +26 -0
- package/docs/plans/plan-010-attributed-ask-answers.md +39 -0
- package/docs/plans/plan-011-context-read-instrumentation.md +54 -0
- package/docs/plans/plan-012-many-boards-stage-1.md +98 -0
- package/docs/plans/plan-013-static-board-export.md +47 -0
- package/docs/plans/plan-014-connected-board-memory.md +140 -0
- package/docs/product-vision-2026-09.md +118 -18
- package/docs/sdk.md +50 -0
- package/package.json +1 -1
- package/skills/pmx-canvas/SKILL.md +53 -4
- package/skills/pmx-canvas/references/full-reference.md +43 -7
- package/skills/pmx-canvas/references/github-copilot-app-adapter.md +3 -0
- package/skills/pmx-canvas/references/installing-pmx-canvas.md +4 -4
- package/skills/pmx-canvas-orchestration/SKILL.md +5 -0
- package/skills/pmx-canvas-testing/SKILL.md +4 -0
- package/src/cli/agent.ts +4 -0
- package/src/cli/commands/ax.ts +13 -0
- package/src/cli/commands/backup.ts +66 -0
- package/src/cli/commands/boards.ts +71 -0
- package/src/cli/commands/export.ts +30 -0
- package/src/cli/commands/nodes.ts +4 -1
- package/src/cli/commands/pins.ts +6 -2
- package/src/cli/commands/pump.ts +3 -0
- package/src/cli/commands/query.ts +33 -7
- package/src/cli/commands/view.ts +7 -5
- package/src/cli/index.ts +19 -9
- package/src/client/App.tsx +21 -3
- package/src/client/canvas/BoardCopyDialog.tsx +153 -0
- package/src/client/canvas/BoardFolderDialog.tsx +120 -0
- package/src/client/canvas/BoardLinkDialog.tsx +93 -0
- package/src/client/canvas/BoardSwitcher.tsx +149 -0
- package/src/client/canvas/CanvasViewport.tsx +3 -0
- package/src/client/canvas/CommandPalette.tsx +107 -4
- package/src/client/canvas/ContextMenu.tsx +4 -0
- package/src/client/canvas/ExpandedNodeOverlay.tsx +3 -0
- package/src/client/canvas/ExportDialog.tsx +197 -0
- package/src/client/canvas/HomeView.tsx +277 -0
- package/src/client/canvas/SessionPanel.tsx +28 -1
- package/src/client/canvas/ToolRail.tsx +38 -3
- package/src/client/canvas/TopBar.tsx +34 -18
- package/src/client/canvas/import-files.ts +19 -1
- package/src/client/canvas/kind-colors.ts +1 -0
- package/src/client/icons.tsx +2 -0
- package/src/client/nodes/AttachmentNode.tsx +148 -0
- package/src/client/nodes/BoardNode.tsx +35 -0
- package/src/client/nodes/FileNode.tsx +6 -0
- package/src/client/nodes/MarkdownNode.tsx +47 -19
- package/src/client/nodes/ax-node-actions.ts +2 -5
- package/src/client/nodes/use-ax-surface-bridge.ts +1 -1
- package/src/client/state/boards-store.ts +167 -0
- package/src/client/state/canvas-store.ts +22 -0
- package/src/client/state/intent-bridge.ts +30 -2
- package/src/client/state/node-factory.ts +1 -0
- package/src/client/state/session-store.ts +5 -0
- package/src/client/state/sse-bridge.ts +49 -5
- package/src/client/theme/global.css +440 -24
- package/src/client/types.ts +2 -0
- package/src/json-render/server.ts +16 -9
- package/src/mcp/canvas-access.ts +59 -22
- package/src/mcp/context-uri-template.ts +28 -0
- package/src/mcp/server.ts +122 -15
- package/src/server/agent-context.ts +116 -29
- package/src/server/attribution.ts +29 -0
- package/src/server/ax-context.ts +4 -2
- package/src/server/ax-interaction.ts +1 -0
- package/src/server/ax-state-manager.ts +72 -3
- package/src/server/ax-state.ts +41 -0
- package/src/server/backup.ts +178 -0
- package/src/server/board-export.ts +686 -0
- package/src/server/canvas-db.ts +599 -107
- package/src/server/canvas-operations.ts +68 -9
- package/src/server/canvas-provenance.ts +1 -0
- package/src/server/canvas-schema.ts +12 -0
- package/src/server/canvas-state.ts +774 -45
- package/src/server/context-brief.ts +372 -0
- package/src/server/context-reads.ts +238 -0
- package/src/server/demo-state.json +659 -2
- package/src/server/document-import.ts +223 -0
- package/src/server/index.ts +231 -5
- package/src/server/mutation-history.ts +10 -13
- package/src/server/operations/composites.ts +26 -4
- package/src/server/operations/http.ts +9 -3
- package/src/server/operations/index.ts +8 -0
- package/src/server/operations/invoker.ts +14 -3
- package/src/server/operations/mcp.ts +50 -11
- package/src/server/operations/ops/app.ts +5 -0
- package/src/server/operations/ops/ax-read.ts +10 -6
- package/src/server/operations/ops/ax-timeline.ts +75 -0
- package/src/server/operations/ops/backup.ts +134 -0
- package/src/server/operations/ops/batch.ts +8 -1
- package/src/server/operations/ops/boards.ts +351 -0
- package/src/server/operations/ops/export.ts +74 -0
- package/src/server/operations/ops/ext-app.ts +4 -0
- package/src/server/operations/ops/imports.ts +220 -0
- package/src/server/operations/ops/nodes.ts +57 -5
- package/src/server/operations/ops/query.ts +206 -9
- package/src/server/operations/registry.ts +123 -31
- package/src/server/operations/types.ts +1 -1
- package/src/server/server.ts +146 -2
- package/src/server/spatial-analysis.ts +9 -17
- package/src/server/trace-manager.ts +19 -0
- package/src/server/web-artifacts.ts +2 -1
- package/src/server/webpage-node.ts +173 -13
- package/src/server/workbench-auth.ts +11 -0
- package/src/shared/boards.ts +18 -0
- 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
|
package/src/cli/commands/ax.ts
CHANGED
|
@@ -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', {
|
|
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)]));
|
package/src/cli/commands/pins.ts
CHANGED
|
@@ -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
|
);
|
package/src/cli/commands/pump.ts
CHANGED
|
@@ -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(
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
122
|
-
|
|
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(
|
package/src/cli/commands/view.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
437
|
+
const shutdown = () => {
|
|
429
438
|
console.log('\nShutting down...');
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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
|
}
|
package/src/client/App.tsx
CHANGED
|
@@ -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
|
-
{
|
|
361
|
-
<
|
|
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
|
+
}
|