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