pmx-canvas 0.6.5 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/extensions/pmx-canvas/extension.mjs +139 -14
- package/CHANGELOG.md +70 -0
- package/Readme.md +32 -6
- package/dist/canvas/global.css +510 -31
- package/dist/canvas/index.js +92 -77
- 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/cli/daemon.d.ts +5 -5
- package/dist/types/cli/shared.d.ts +3 -1
- 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 +73 -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 +68 -11
- package/skills/pmx-canvas/evals/evals.json +130 -0
- 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/references/tours-and-recording.md +69 -0
- package/skills/pmx-canvas-orchestration/SKILL.md +5 -0
- package/skills/pmx-canvas-testing/SKILL.md +11 -2
- package/src/cli/agent.ts +4 -0
- package/src/cli/commands/apps.ts +1 -1
- 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 +37 -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/daemon.ts +37 -7
- package/src/cli/index.ts +19 -9
- package/src/cli/shared.ts +11 -2
- package/src/client/App.tsx +21 -4
- package/src/client/canvas/AttentionToast.tsx +5 -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/CanvasNode.tsx +0 -1
- package/src/client/canvas/CanvasViewport.tsx +3 -0
- package/src/client/canvas/CommandPalette.tsx +107 -4
- package/src/client/canvas/ContextMenu.tsx +4 -1
- package/src/client/canvas/ExpandedNodeOverlay.tsx +3 -0
- package/src/client/canvas/ExportDialog.tsx +199 -0
- package/src/client/canvas/HomeView.tsx +277 -0
- package/src/client/canvas/Presentation.tsx +39 -9
- package/src/client/canvas/SelectionBar.tsx +0 -2
- 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 +145 -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/attention-bridge.ts +3 -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 +52 -6
- package/src/client/theme/global.css +510 -31
- 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 +236 -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/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/daemon.ts
CHANGED
|
@@ -49,14 +49,24 @@ export function isProcessRunning(pid: number): boolean {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
/**
|
|
52
|
-
* Whether pid's command line contains `needle`. Returns null when
|
|
53
|
-
* answer (no such process
|
|
54
|
-
*
|
|
52
|
+
* Whether pid's command line contains `needle`. Returns null when the OS probe
|
|
53
|
+
* cannot answer (no such process or unavailable command). Stop must not treat
|
|
54
|
+
* an unknown identity as permission to signal a live process.
|
|
55
55
|
*/
|
|
56
56
|
export function processCommandMatches(pid: number, needle: string): boolean | null {
|
|
57
57
|
if (!Number.isInteger(pid) || pid <= 0) return null;
|
|
58
58
|
try {
|
|
59
|
-
const
|
|
59
|
+
const executable = process.platform === 'win32' ? 'powershell.exe' : 'ps';
|
|
60
|
+
const args =
|
|
61
|
+
process.platform === 'win32'
|
|
62
|
+
? [
|
|
63
|
+
'-NoProfile',
|
|
64
|
+
'-NonInteractive',
|
|
65
|
+
'-Command',
|
|
66
|
+
`(Get-CimInstance Win32_Process -Filter 'ProcessId = ${pid}').CommandLine`,
|
|
67
|
+
]
|
|
68
|
+
: ['-o', 'command=', '-p', String(pid)];
|
|
69
|
+
const command = execFileSync(executable, args, {
|
|
60
70
|
encoding: 'utf-8',
|
|
61
71
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
62
72
|
}).trim();
|
|
@@ -69,8 +79,8 @@ export function processCommandMatches(pid: number, needle: string): boolean | nu
|
|
|
69
79
|
|
|
70
80
|
/**
|
|
71
81
|
* Liveness with a PID-recycling guard: the pid must be alive AND its command
|
|
72
|
-
* line must still look like our daemon
|
|
73
|
-
*
|
|
82
|
+
* line must still look like our daemon. Status falls back to plain liveness
|
|
83
|
+
* when the command probe is unavailable; stop requires a verified match.
|
|
74
84
|
*/
|
|
75
85
|
export function isOwnDaemonProcess(pid: number, entryNeedle: string): boolean {
|
|
76
86
|
if (!isProcessRunning(pid)) return false;
|
|
@@ -439,7 +449,27 @@ export async function stopServeDaemon(
|
|
|
439
449
|
process.exit(1);
|
|
440
450
|
}
|
|
441
451
|
|
|
442
|
-
|
|
452
|
+
const commandMatches = processCommandMatches(pid, options.entry);
|
|
453
|
+
if (commandMatches !== true) {
|
|
454
|
+
if (isProcessRunning(pid)) {
|
|
455
|
+
outputJson({
|
|
456
|
+
ok: false,
|
|
457
|
+
daemon: true,
|
|
458
|
+
stopped: false,
|
|
459
|
+
running: responsive,
|
|
460
|
+
error:
|
|
461
|
+
commandMatches === false
|
|
462
|
+
? `PID ${pid} is live but does not belong to this installation. The pid file was preserved.`
|
|
463
|
+
: `PID ${pid} is live but its installation could not be verified. The pid file was preserved.`,
|
|
464
|
+
hint: 'Use the matching installation and workspace to stop the daemon; verify the process before removing its pid file.',
|
|
465
|
+
pid,
|
|
466
|
+
url,
|
|
467
|
+
healthUrl,
|
|
468
|
+
logFile: options.logFile,
|
|
469
|
+
pidFile: options.pidFile,
|
|
470
|
+
});
|
|
471
|
+
process.exit(1);
|
|
472
|
+
}
|
|
443
473
|
removePidFile(options.pidFile);
|
|
444
474
|
outputJson({
|
|
445
475
|
ok: true,
|
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/cli/shared.ts
CHANGED
|
@@ -133,7 +133,8 @@ async function invokeOperation(name: string, input: Record<string, unknown>): Pr
|
|
|
133
133
|
// ── Flag parsing ─────────────────────────────────────────────
|
|
134
134
|
|
|
135
135
|
/**
|
|
136
|
-
* `options.boolFlags` marks flags that are boolean FOR THIS COMMAND
|
|
136
|
+
* `options.boolFlags` marks flags that are boolean FOR THIS COMMAND, while
|
|
137
|
+
* `options.valueFlags` overrides a globally boolean flag for one command. Value
|
|
137
138
|
* flags consume the next token verbatim (so a unified diff beginning with
|
|
138
139
|
* `--- a/file` survives), which makes a flag that is boolean here and
|
|
139
140
|
* value-taking elsewhere ambiguous — `--summary` is boolean in `history` /
|
|
@@ -144,7 +145,7 @@ async function invokeOperation(name: string, input: Record<string, unknown>): Pr
|
|
|
144
145
|
*/
|
|
145
146
|
function parseFlags(
|
|
146
147
|
args: string[],
|
|
147
|
-
options: { boolFlags?: readonly string[] } = {},
|
|
148
|
+
options: { boolFlags?: readonly string[]; valueFlags?: readonly string[] } = {},
|
|
148
149
|
): { positional: string[]; flags: Record<string, string | true> } {
|
|
149
150
|
const positional: string[] = [];
|
|
150
151
|
const flags: Record<string, string | true> = {};
|
|
@@ -187,6 +188,7 @@ function parseFlags(
|
|
|
187
188
|
'check',
|
|
188
189
|
'skip-mcp',
|
|
189
190
|
]);
|
|
191
|
+
for (const flag of options.valueFlags ?? []) BOOL_FLAGS.delete(flag);
|
|
190
192
|
for (let i = 0; i < args.length; i++) {
|
|
191
193
|
const arg = args[i];
|
|
192
194
|
// Conventional end-of-flags separator: the rest is positional, never flags.
|
|
@@ -847,6 +849,13 @@ function showCommandHelp(name: string): void {
|
|
|
847
849
|
console.log('\nAlias:');
|
|
848
850
|
console.log(' Equivalent to: pmx-canvas external-app add --kind excalidraw ...');
|
|
849
851
|
}
|
|
852
|
+
if (name === 'export') {
|
|
853
|
+
console.log('\nOptions:');
|
|
854
|
+
console.log(' --board <id> Export this board instead of the open board');
|
|
855
|
+
console.log(' --out <path> Copy the export to this path');
|
|
856
|
+
console.log(' --include-files Include local file contents (default: excluded)');
|
|
857
|
+
console.log(' --include-derived-text Include imported document text (default: excluded)');
|
|
858
|
+
}
|
|
850
859
|
console.log('');
|
|
851
860
|
}
|
|
852
861
|
|
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
|
|
@@ -221,7 +233,6 @@ export function App() {
|
|
|
221
233
|
if (ids.length === 0) return;
|
|
222
234
|
e.preventDefault();
|
|
223
235
|
for (const id of ids) {
|
|
224
|
-
removeNode(id);
|
|
225
236
|
void removeNodeFromClient(id).catch((error) => logAppError('delete', error));
|
|
226
237
|
}
|
|
227
238
|
clearSelection();
|
|
@@ -357,16 +368,21 @@ export function App() {
|
|
|
357
368
|
<AttentionHistory />
|
|
358
369
|
<ActivityFeed />
|
|
359
370
|
<WritersSheet />
|
|
360
|
-
{
|
|
361
|
-
<
|
|
371
|
+
{onHome ? (
|
|
372
|
+
<HomeView />
|
|
373
|
+
) : (
|
|
374
|
+
hasInitialLayout &&
|
|
375
|
+
allNodes.length === 0 &&
|
|
376
|
+
intents.value.size === 0 && <EmptyState onOpenPalette={() => setPaletteOpen(true)} />
|
|
362
377
|
)}
|
|
363
378
|
{selectedNodeIds.value.size > 0 && <SelectionBar />}
|
|
364
379
|
{sessionIsActive ? <CommandBar /> : contextPinnedNodeIds.value.size > 0 && <ContextPinBar />}
|
|
365
380
|
<SessionReceipt onOpenSnapshots={() => setSnapshotOpen(true)} />
|
|
366
381
|
<TextPrompt />
|
|
382
|
+
<ExportDialog />
|
|
367
383
|
{expandedNodeId.value && <ExpandedNodeOverlay />}
|
|
368
384
|
<SnapshotPanel open={snapshotOpen} onClose={handleCloseSnapshot} anchorRef={snapshotBtnRef} />
|
|
369
|
-
{minimapVisible && (
|
|
385
|
+
{minimapVisible && !onHome && (
|
|
370
386
|
<Minimap
|
|
371
387
|
viewport={viewport}
|
|
372
388
|
nodes={nodes}
|
|
@@ -381,6 +397,7 @@ export function App() {
|
|
|
381
397
|
{sessionIsActive && <SessionPanel />}
|
|
382
398
|
{menu && <ContextMenu menu={menu} onClose={closeMenu} />}
|
|
383
399
|
{paletteOpen && <CommandPalette onClose={() => setPaletteOpen(false)} onToggleMinimap={handleToggleMinimap} />}
|
|
400
|
+
{boardLinkOpen && <BoardLinkDialog onClose={() => setBoardLinkOpen(false)} />}
|
|
384
401
|
{shortcutsOpen && <ShortcutOverlay onClose={() => setShortcutsOpen(false)} />}
|
|
385
402
|
</div>
|
|
386
403
|
);
|
|
@@ -9,11 +9,13 @@ export function AttentionToast() {
|
|
|
9
9
|
type="button"
|
|
10
10
|
class={`attention-toast attention-tone-${toast.tone}`}
|
|
11
11
|
onClick={openAttentionHistory}
|
|
12
|
-
aria-label={`${toast.title} — open change history`}
|
|
13
|
-
title={toast.detail || toast.title}
|
|
12
|
+
aria-label={`${toast.title}${toast.detail ? `: ${toast.detail}` : ''} — open change history`}
|
|
14
13
|
>
|
|
15
14
|
<span class="attention-toast-dot" aria-hidden="true" />
|
|
16
|
-
<span class="attention-toast-
|
|
15
|
+
<span class="attention-toast-copy">
|
|
16
|
+
<span class="attention-toast-title">{toast.title}</span>
|
|
17
|
+
{toast.detail && <span class="attention-toast-detail">{toast.detail}</span>}
|
|
18
|
+
</span>
|
|
17
19
|
</button>
|
|
18
20
|
);
|
|
19
21
|
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'preact/hooks';
|
|
2
|
+
import { normalizeBoardCategory } from '../../shared/boards.js';
|
|
3
|
+
import { boardList, updateBoard, type BoardSummary } from '../state/boards-store';
|
|
4
|
+
import { useFocusTrap } from './use-focus-trap';
|
|
5
|
+
|
|
6
|
+
/** A destination picker: select an existing folder, optionally create a child, then move. */
|
|
7
|
+
export function BoardFolderDialog({
|
|
8
|
+
board,
|
|
9
|
+
folders,
|
|
10
|
+
onClose,
|
|
11
|
+
}: {
|
|
12
|
+
board: BoardSummary;
|
|
13
|
+
folders: string[];
|
|
14
|
+
onClose: () => void;
|
|
15
|
+
}) {
|
|
16
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
17
|
+
const [parent, setParent] = useState(board.category ?? '');
|
|
18
|
+
const [name, setName] = useState('');
|
|
19
|
+
const [busy, setBusy] = useState(false);
|
|
20
|
+
const [error, setError] = useState('');
|
|
21
|
+
useFocusTrap(ref, true);
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
const onEscape = (event: KeyboardEvent) => {
|
|
24
|
+
if (event.key !== 'Escape') return;
|
|
25
|
+
event.preventDefault();
|
|
26
|
+
event.stopPropagation();
|
|
27
|
+
if (!busy) onClose();
|
|
28
|
+
};
|
|
29
|
+
window.addEventListener('keydown', onEscape, true);
|
|
30
|
+
return () => window.removeEventListener('keydown', onEscape, true);
|
|
31
|
+
}, [busy, onClose]);
|
|
32
|
+
|
|
33
|
+
const destination = name.trim() ? [parent, name.trim()].filter(Boolean).join('/') : parent;
|
|
34
|
+
const move = async () => {
|
|
35
|
+
let category: string | null;
|
|
36
|
+
try {
|
|
37
|
+
if (name.includes('/')) throw new Error('Use one folder name; choose its parent above.');
|
|
38
|
+
category = normalizeBoardCategory(destination);
|
|
39
|
+
} catch (cause) {
|
|
40
|
+
setError(cause instanceof Error ? cause.message : String(cause));
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
setBusy(true);
|
|
44
|
+
setError('');
|
|
45
|
+
await updateBoard(board.id, { category });
|
|
46
|
+
if (boardList.value.find((entry) => entry.id === board.id)?.category === category) onClose();
|
|
47
|
+
else setError('Could not move this board. Try again.');
|
|
48
|
+
setBusy(false);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<div class="text-prompt-backdrop" onPointerDown={() => !busy && onClose()}>
|
|
53
|
+
<div
|
|
54
|
+
ref={ref}
|
|
55
|
+
class="text-prompt home-folder-dialog"
|
|
56
|
+
role="dialog"
|
|
57
|
+
aria-modal="true"
|
|
58
|
+
aria-label={`Move ${board.name}`}
|
|
59
|
+
onPointerDown={(event) => event.stopPropagation()}
|
|
60
|
+
>
|
|
61
|
+
<div class="text-prompt-title">Move “{board.name}”</div>
|
|
62
|
+
<p class="home-folder-hint">Choose a folder, or create one inside it. The board keeps its content and links.</p>
|
|
63
|
+
<fieldset class="home-folder-options" disabled={busy}>
|
|
64
|
+
<legend>Destination folder</legend>
|
|
65
|
+
{['', ...folders].map((path) => (
|
|
66
|
+
<label
|
|
67
|
+
key={path}
|
|
68
|
+
class="home-folder-option"
|
|
69
|
+
style={{ paddingLeft: `${8 + Math.min(path.split('/').length - 1, 5) * 14}px` }}
|
|
70
|
+
>
|
|
71
|
+
<input
|
|
72
|
+
type="radio"
|
|
73
|
+
name="folder-destination"
|
|
74
|
+
value={path}
|
|
75
|
+
checked={parent === path}
|
|
76
|
+
onChange={() => {
|
|
77
|
+
setParent(path);
|
|
78
|
+
setError('');
|
|
79
|
+
}}
|
|
80
|
+
/>
|
|
81
|
+
<span>{path || 'Home (no folder)'}</span>
|
|
82
|
+
</label>
|
|
83
|
+
))}
|
|
84
|
+
</fieldset>
|
|
85
|
+
<label class="home-folder-new">
|
|
86
|
+
New folder inside {parent || 'Home'} <span class="home-folder-hint">(optional)</span>
|
|
87
|
+
<input
|
|
88
|
+
class="text-prompt-input"
|
|
89
|
+
aria-label="New folder name"
|
|
90
|
+
placeholder="e.g. Decisions"
|
|
91
|
+
maxLength={60}
|
|
92
|
+
value={name}
|
|
93
|
+
disabled={busy}
|
|
94
|
+
onInput={(event) => {
|
|
95
|
+
setName(event.currentTarget.value);
|
|
96
|
+
setError('');
|
|
97
|
+
}}
|
|
98
|
+
/>
|
|
99
|
+
</label>
|
|
100
|
+
<div class="home-folder-hint">
|
|
101
|
+
Move to: <strong>{destination || 'Home'}</strong>
|
|
102
|
+
</div>
|
|
103
|
+
{error && <p role="alert">{error}</p>}
|
|
104
|
+
<div class="text-prompt-actions">
|
|
105
|
+
<button type="button" class="text-prompt-cancel" disabled={busy} onClick={onClose}>
|
|
106
|
+
Cancel
|
|
107
|
+
</button>
|
|
108
|
+
<button
|
|
109
|
+
type="button"
|
|
110
|
+
class="text-prompt-confirm"
|
|
111
|
+
disabled={busy || destination === (board.category ?? '')}
|
|
112
|
+
onClick={() => void move()}
|
|
113
|
+
>
|
|
114
|
+
{busy ? 'Moving…' : 'Move board'}
|
|
115
|
+
</button>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
);
|
|
120
|
+
}
|