pmx-canvas 0.3.0 → 0.3.2
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/CHANGELOG.md +73 -0
- package/dist/canvas/index.js +65 -65
- package/dist/types/cli/agent.d.ts +9 -1
- package/dist/types/cli/daemon.d.ts +74 -0
- package/dist/types/cli/watch.d.ts +2 -2
- package/dist/types/client/canvas/CanvasViewport.d.ts +1 -1
- package/dist/types/client/canvas/CommandPalette.d.ts +1 -1
- package/dist/types/client/canvas/Minimap.d.ts +1 -1
- package/dist/types/client/nodes/ExtAppFrame.d.ts +9 -1
- package/dist/types/client/nodes/FileNode.d.ts +1 -1
- package/dist/types/client/nodes/ImageNode.d.ts +1 -1
- package/dist/types/client/nodes/InlineFormatBar.d.ts +1 -1
- package/dist/types/client/nodes/MarkdownNode.d.ts +1 -1
- package/dist/types/client/nodes/PromptNode.d.ts +1 -1
- package/dist/types/client/nodes/ResponseNode.d.ts +1 -1
- package/dist/types/server/canvas-schema.d.ts +1 -1
- package/dist/types/server/html-primitives.d.ts +1 -1
- package/dist/types/server/index.d.ts +4 -4
- package/dist/types/server/operations/index.d.ts +1 -1
- package/dist/types/server/operations/ops/ax-read.d.ts +2 -0
- package/dist/types/server/operations/ops/canvas-wire.d.ts +2 -0
- package/dist/types/server/operations/ops/ext-app.d.ts +2 -0
- package/dist/types/server/server.d.ts +8 -0
- package/docs/cli.md +10 -1
- package/docs/http-api.md +28 -0
- package/docs/plans/plan-009-tech-debt-backlog.md +5 -5
- package/docs/screenshot.png +0 -0
- package/docs/tech-debt-assessment-2026-07.md +2 -2
- package/package.json +5 -2
- package/skills/pmx-canvas/SKILL.md +15 -10
- package/skills/pmx-canvas/references/full-reference.md +17 -3
- package/src/cli/agent.ts +1951 -1571
- package/src/cli/daemon.ts +460 -0
- package/src/cli/index.ts +80 -323
- package/src/cli/watch.ts +2 -10
- package/src/client/App.tsx +48 -46
- package/src/client/canvas/AttentionHistory.tsx +11 -1
- package/src/client/canvas/CanvasNode.tsx +41 -29
- package/src/client/canvas/CanvasViewport.tsx +101 -66
- package/src/client/canvas/CommandPalette.tsx +61 -27
- package/src/client/canvas/ContextMenu.tsx +13 -20
- package/src/client/canvas/ContextPinBar.tsx +1 -5
- package/src/client/canvas/ContextPinHud.tsx +1 -6
- package/src/client/canvas/DockedNode.tsx +4 -4
- package/src/client/canvas/EdgeLayer.tsx +37 -36
- package/src/client/canvas/ExpandedNodeOverlay.tsx +69 -45
- package/src/client/canvas/FocusFieldLayer.tsx +20 -22
- package/src/client/canvas/IntentLayer.tsx +31 -14
- package/src/client/canvas/Minimap.tsx +11 -16
- package/src/client/canvas/SelectionBar.tsx +4 -11
- package/src/client/canvas/ShortcutOverlay.tsx +3 -1
- package/src/client/canvas/SnapshotPanel.tsx +77 -95
- package/src/client/canvas/auto-fit.ts +15 -14
- package/src/client/canvas/snap-guides.ts +12 -12
- package/src/client/canvas/use-node-resize.ts +1 -5
- package/src/client/canvas/use-pan-zoom.ts +25 -26
- package/src/client/ext-app/bridge.ts +3 -12
- package/src/client/icons.tsx +63 -20
- package/src/client/nodes/ContextNode.tsx +14 -25
- package/src/client/nodes/ExtAppFrame.tsx +194 -80
- package/src/client/nodes/FileNode.tsx +74 -62
- package/src/client/nodes/GroupNode.tsx +4 -6
- package/src/client/nodes/HtmlNode.tsx +76 -46
- package/src/client/nodes/ImageNode.tsx +18 -27
- package/src/client/nodes/InlineFormatBar.tsx +4 -21
- package/src/client/nodes/InlineMarkdownEditor.tsx +0 -1
- package/src/client/nodes/LedgerNode.tsx +10 -4
- package/src/client/nodes/MarkdownNode.tsx +3 -10
- package/src/client/nodes/McpAppNode.tsx +26 -22
- package/src/client/nodes/MdFormatBar.tsx +10 -7
- package/src/client/nodes/PromptNode.tsx +23 -51
- package/src/client/nodes/ResponseNode.tsx +3 -13
- package/src/client/nodes/StatusNode.tsx +5 -9
- package/src/client/nodes/StatusSummary.tsx +2 -8
- package/src/client/nodes/WebpageNode.tsx +20 -14
- package/src/client/nodes/iframe-document-url.ts +25 -16
- package/src/client/nodes/image-warnings.ts +1 -7
- package/src/client/nodes/md-format.ts +20 -5
- package/src/client/state/attention-bridge.ts +4 -9
- package/src/client/state/attention-store.ts +1 -7
- package/src/client/state/canvas-store.ts +52 -36
- package/src/client/state/intent-bridge.ts +176 -112
- package/src/client/state/intent-store.ts +4 -1
- package/src/client/state/sse-bridge.ts +53 -70
- package/src/json-render/catalog.ts +12 -16
- package/src/json-render/charts/components.tsx +16 -20
- package/src/json-render/charts/extra-components.tsx +8 -16
- package/src/json-render/charts/extra-definitions.ts +1 -2
- package/src/json-render/charts/tufte-components.tsx +37 -20
- package/src/json-render/charts/tufte-definitions.ts +8 -2
- package/src/json-render/renderer/index.tsx +42 -22
- package/src/json-render/schema.ts +6 -3
- package/src/json-render/server.ts +33 -39
- package/src/mcp/canvas-access.ts +35 -21
- package/src/mcp/server.ts +132 -70
- package/src/server/agent-context.ts +63 -36
- package/src/server/ax-context.ts +7 -5
- package/src/server/ax-interaction.ts +176 -43
- package/src/server/ax-state-manager.ts +182 -39
- package/src/server/ax-state.ts +142 -47
- package/src/server/canvas-db.ts +213 -95
- package/src/server/canvas-operations.ts +180 -123
- package/src/server/canvas-provenance.ts +1 -4
- package/src/server/canvas-schema.ts +454 -73
- package/src/server/canvas-serialization.ts +27 -35
- package/src/server/canvas-state.ts +150 -58
- package/src/server/chart-template.ts +4 -5
- package/src/server/code-graph.ts +19 -6
- package/src/server/diagram-presets.ts +28 -29
- package/src/server/ext-app-lookup.ts +3 -12
- package/src/server/html-node-summary.ts +19 -10
- package/src/server/html-primitives.ts +326 -97
- package/src/server/html-surface.ts +6 -9
- package/src/server/image-source.ts +6 -4
- package/src/server/index.ts +320 -217
- package/src/server/intent-registry.ts +2 -5
- package/src/server/mcp-app-candidate.ts +5 -10
- package/src/server/mcp-app-host.ts +14 -38
- package/src/server/mcp-app-runtime.ts +12 -20
- package/src/server/mutation-history.ts +15 -5
- package/src/server/operations/composites.ts +1 -3
- package/src/server/operations/http.ts +2 -3
- package/src/server/operations/index.ts +7 -1
- package/src/server/operations/invoker.ts +4 -3
- package/src/server/operations/mcp.ts +22 -30
- package/src/server/operations/ops/annotation.ts +122 -10
- package/src/server/operations/ops/app.ts +98 -73
- package/src/server/operations/ops/ax-await.ts +17 -10
- package/src/server/operations/ops/ax-read.ts +347 -0
- package/src/server/operations/ops/ax-shared.ts +2 -7
- package/src/server/operations/ops/ax-state.ts +32 -14
- package/src/server/operations/ops/ax-timeline.ts +32 -19
- package/src/server/operations/ops/ax-work.ts +54 -37
- package/src/server/operations/ops/batch.ts +41 -15
- package/src/server/operations/ops/canvas-wire.ts +91 -0
- package/src/server/operations/ops/edges.ts +37 -25
- package/src/server/operations/ops/ext-app.ts +346 -0
- package/src/server/operations/ops/groups.ts +49 -20
- package/src/server/operations/ops/intent.ts +18 -12
- package/src/server/operations/ops/json-render.ts +239 -98
- package/src/server/operations/ops/nodes.ts +298 -109
- package/src/server/operations/ops/query.ts +46 -28
- package/src/server/operations/ops/snapshots.ts +35 -26
- package/src/server/operations/ops/validate.ts +2 -1
- package/src/server/operations/ops/viewport.ts +60 -16
- package/src/server/operations/ops/webview.ts +44 -18
- package/src/server/operations/registry.ts +2 -3
- package/src/server/operations/types.ts +7 -5
- package/src/server/operations/webview-runner.ts +1 -3
- package/src/server/placement.ts +8 -18
- package/src/server/server.ts +122 -1028
- package/src/server/spatial-analysis.ts +39 -25
- package/src/server/trace-manager.ts +3 -8
- package/src/server/web-artifacts.ts +23 -27
- package/src/server/webpage-node.ts +5 -13
- package/src/shared/auto-arrange.ts +12 -5
- package/src/shared/content-height-reporter.ts +8 -6
- package/src/shared/ext-app-tool-result.ts +2 -6
- package/src/shared/placement.ts +1 -4
- package/src/shared/semantic-attention.ts +39 -37
- package/src/shared/surface.ts +8 -4
package/src/cli/index.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { spawn } from 'node:child_process';
|
|
3
|
-
import {
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
4
|
import { dirname, join, resolve } from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { showServeStatus, startDaemonMode, stopServeDaemon } from './daemon.js';
|
|
6
7
|
import { runAgentCli } from './agent.js';
|
|
7
8
|
import { createCanvas } from '../server/index.js';
|
|
8
9
|
import { seedDemoCanvas } from '../server/demo.js';
|
|
@@ -31,9 +32,39 @@ if (args.includes('--version') || args.includes('-v')) {
|
|
|
31
32
|
// ── Agent CLI subcommands ────────────────────────────────────
|
|
32
33
|
// If first arg is a known subcommand (not a --flag), route to the agent CLI.
|
|
33
34
|
const AGENT_COMMANDS = new Set([
|
|
34
|
-
'node',
|
|
35
|
-
'
|
|
36
|
-
'
|
|
35
|
+
'node',
|
|
36
|
+
'edge',
|
|
37
|
+
'json-render',
|
|
38
|
+
'search',
|
|
39
|
+
'layout',
|
|
40
|
+
'status',
|
|
41
|
+
'arrange',
|
|
42
|
+
'focus',
|
|
43
|
+
'fit',
|
|
44
|
+
'screenshot',
|
|
45
|
+
'pin',
|
|
46
|
+
'ax',
|
|
47
|
+
'undo',
|
|
48
|
+
'redo',
|
|
49
|
+
'history',
|
|
50
|
+
'snapshot',
|
|
51
|
+
'diff',
|
|
52
|
+
'group',
|
|
53
|
+
'webview',
|
|
54
|
+
'open',
|
|
55
|
+
'clear',
|
|
56
|
+
'code-graph',
|
|
57
|
+
'spatial',
|
|
58
|
+
'watch',
|
|
59
|
+
'web-artifact',
|
|
60
|
+
'external-app',
|
|
61
|
+
'diagram',
|
|
62
|
+
'graph',
|
|
63
|
+
'html',
|
|
64
|
+
'batch',
|
|
65
|
+
'validate',
|
|
66
|
+
'serve',
|
|
67
|
+
'copilot',
|
|
37
68
|
]);
|
|
38
69
|
|
|
39
70
|
const firstArg = args[0] ?? '';
|
|
@@ -66,7 +97,10 @@ function readNumberOption(name: string): number | undefined {
|
|
|
66
97
|
function readCsvOption(name: string): string[] | undefined {
|
|
67
98
|
const raw = readOption(name);
|
|
68
99
|
if (!raw) return undefined;
|
|
69
|
-
const values = raw
|
|
100
|
+
const values = raw
|
|
101
|
+
.split(',')
|
|
102
|
+
.map((value) => value.trim())
|
|
103
|
+
.filter((value) => value.length > 0);
|
|
70
104
|
return values.length > 0 ? values : undefined;
|
|
71
105
|
}
|
|
72
106
|
|
|
@@ -86,304 +120,23 @@ function stripOption(argv: string[], name: string): string[] {
|
|
|
86
120
|
return stripped;
|
|
87
121
|
}
|
|
88
122
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
return null;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function isProcessRunning(pid: number): boolean {
|
|
106
|
-
try {
|
|
107
|
-
process.kill(pid, 0);
|
|
108
|
-
return true;
|
|
109
|
-
} catch (error) {
|
|
110
|
-
if (error && typeof error === 'object' && 'code' in error) {
|
|
111
|
-
return (error as NodeJS.ErrnoException).code === 'EPERM';
|
|
112
|
-
}
|
|
113
|
-
return false;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function removePidFile(path: string): void {
|
|
118
|
-
try {
|
|
119
|
-
rmSync(path, { force: true });
|
|
120
|
-
} catch {
|
|
121
|
-
// Ignore cleanup failures for stale pid files.
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
interface HealthStatus {
|
|
126
|
-
responsive: boolean;
|
|
127
|
-
workspace: string | null;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
async function readHealthStatus(url: string): Promise<HealthStatus> {
|
|
131
|
-
try {
|
|
132
|
-
const response = await fetch(url);
|
|
133
|
-
if (!response.ok) return { responsive: false, workspace: null };
|
|
134
|
-
const payload = await response.json().catch(() => null) as unknown;
|
|
135
|
-
const workspace = payload && typeof payload === 'object' && 'workspace' in payload
|
|
136
|
-
&& typeof payload.workspace === 'string'
|
|
137
|
-
? payload.workspace
|
|
138
|
-
: null;
|
|
139
|
-
return { responsive: true, workspace };
|
|
140
|
-
} catch {
|
|
141
|
-
return { responsive: false, workspace: null };
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
async function isHealthy(url: string): Promise<boolean> {
|
|
146
|
-
return (await readHealthStatus(url)).responsive;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
function readLogTail(path: string, maxLines = 20): string | null {
|
|
150
|
-
try {
|
|
151
|
-
if (!existsSync(path)) return null;
|
|
152
|
-
const lines = readFileSync(path, 'utf-8').trim().split('\n');
|
|
153
|
-
return lines.slice(-maxLines).join('\n') || null;
|
|
154
|
-
} catch {
|
|
155
|
-
return null;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
async function waitForHealth(
|
|
160
|
-
healthUrl: string,
|
|
161
|
-
timeoutMs: number,
|
|
162
|
-
getExitMessage: () => string | null,
|
|
163
|
-
): Promise<{ ok: true } | { ok: false; reason: string }> {
|
|
164
|
-
const deadline = Date.now() + timeoutMs;
|
|
165
|
-
while (Date.now() < deadline) {
|
|
166
|
-
if (await isHealthy(healthUrl)) {
|
|
167
|
-
return { ok: true };
|
|
168
|
-
}
|
|
169
|
-
const exitMessage = getExitMessage();
|
|
170
|
-
if (exitMessage) {
|
|
171
|
-
return { ok: false, reason: exitMessage };
|
|
172
|
-
}
|
|
173
|
-
await Bun.sleep(250);
|
|
174
|
-
}
|
|
175
|
-
return { ok: false, reason: `Timed out waiting for ${healthUrl}` };
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
async function waitForShutdown(
|
|
179
|
-
healthUrl: string,
|
|
180
|
-
timeoutMs: number,
|
|
181
|
-
pid: number | null,
|
|
182
|
-
): Promise<boolean> {
|
|
183
|
-
const deadline = Date.now() + timeoutMs;
|
|
184
|
-
while (Date.now() < deadline) {
|
|
185
|
-
const responsive = await isHealthy(healthUrl);
|
|
186
|
-
const alive = pid ? isProcessRunning(pid) : false;
|
|
187
|
-
if (!responsive && !alive) {
|
|
188
|
-
return true;
|
|
189
|
-
}
|
|
190
|
-
await Bun.sleep(250);
|
|
191
|
-
}
|
|
192
|
-
return false;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
async function startDaemonMode(options: {
|
|
196
|
-
port: number;
|
|
197
|
-
baseArgs: string[];
|
|
198
|
-
logFile: string;
|
|
199
|
-
pidFile: string;
|
|
200
|
-
waitMs: number;
|
|
201
|
-
}): Promise<void> {
|
|
202
|
-
const healthUrl = `http://localhost:${options.port}/health`;
|
|
203
|
-
const workbenchUrl = `http://localhost:${options.port}/workbench`;
|
|
204
|
-
const existingPid = readPidFile(options.pidFile);
|
|
205
|
-
|
|
206
|
-
if (await isHealthy(healthUrl)) {
|
|
207
|
-
outputJson({
|
|
208
|
-
ok: true,
|
|
209
|
-
daemon: true,
|
|
210
|
-
alreadyRunning: true,
|
|
211
|
-
pid: existingPid,
|
|
212
|
-
url: workbenchUrl,
|
|
213
|
-
healthUrl,
|
|
214
|
-
logFile: options.logFile,
|
|
215
|
-
pidFile: options.pidFile,
|
|
216
|
-
});
|
|
217
|
-
process.exit(0);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
mkdirSync(dirname(options.logFile), { recursive: true });
|
|
221
|
-
const logFd = openSync(options.logFile, 'a');
|
|
222
|
-
const childArgs = options.baseArgs.includes('--no-open')
|
|
223
|
-
? options.baseArgs
|
|
224
|
-
: [...options.baseArgs, '--no-open'];
|
|
225
|
-
const child = spawn(process.execPath, ['run', fileURLToPath(import.meta.url), ...childArgs], {
|
|
226
|
-
cwd: process.cwd(),
|
|
227
|
-
detached: true,
|
|
228
|
-
env: process.env,
|
|
229
|
-
stdio: ['ignore', logFd, logFd],
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
let exitMessage: string | null = null;
|
|
233
|
-
child.once('exit', (code, signal) => {
|
|
234
|
-
exitMessage = signal
|
|
235
|
-
? `Daemon exited via signal ${signal}`
|
|
236
|
-
: `Daemon exited with code ${code ?? 'unknown'}`;
|
|
237
|
-
});
|
|
238
|
-
child.unref();
|
|
239
|
-
|
|
240
|
-
const health = await waitForHealth(healthUrl, options.waitMs, () => exitMessage);
|
|
241
|
-
if (!health.ok) {
|
|
242
|
-
const logTail = readLogTail(options.logFile);
|
|
243
|
-
const details = logTail ? `${health.reason}\n\nRecent log output:\n${logTail}` : health.reason;
|
|
244
|
-
console.error(details);
|
|
245
|
-
process.exit(1);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
mkdirSync(dirname(options.pidFile), { recursive: true });
|
|
249
|
-
writeFileSync(options.pidFile, `${child.pid}\n`, 'utf-8');
|
|
250
|
-
|
|
251
|
-
outputJson({
|
|
252
|
-
ok: true,
|
|
253
|
-
daemon: true,
|
|
254
|
-
pid: child.pid,
|
|
255
|
-
url: workbenchUrl,
|
|
256
|
-
healthUrl,
|
|
257
|
-
logFile: options.logFile,
|
|
258
|
-
pidFile: options.pidFile,
|
|
259
|
-
});
|
|
260
|
-
process.exit(0);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
async function showServeStatus(options: {
|
|
264
|
-
port: number;
|
|
265
|
-
logFile: string;
|
|
266
|
-
pidFile: string;
|
|
267
|
-
}): Promise<void> {
|
|
268
|
-
const healthUrl = `http://localhost:${options.port}/health`;
|
|
269
|
-
const url = `http://localhost:${options.port}/workbench`;
|
|
270
|
-
const pid = readPidFile(options.pidFile);
|
|
271
|
-
const pidRunning = pid ? isProcessRunning(pid) : false;
|
|
272
|
-
const health = await readHealthStatus(healthUrl);
|
|
273
|
-
const responsive = health.responsive;
|
|
274
|
-
const running = responsive || pidRunning;
|
|
275
|
-
if (!running && existsSync(options.pidFile) && !pidRunning) {
|
|
276
|
-
removePidFile(options.pidFile);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
outputJson({
|
|
280
|
-
ok: true,
|
|
281
|
-
daemon: true,
|
|
282
|
-
running,
|
|
283
|
-
responsive,
|
|
284
|
-
workspace: health.workspace,
|
|
285
|
-
pid,
|
|
286
|
-
pidRunning,
|
|
287
|
-
url,
|
|
288
|
-
healthUrl,
|
|
289
|
-
logFile: options.logFile,
|
|
290
|
-
pidFile: options.pidFile,
|
|
291
|
-
pidFileExists: existsSync(options.pidFile),
|
|
292
|
-
});
|
|
293
|
-
process.exit(0);
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
async function stopServeDaemon(options: {
|
|
297
|
-
port: number;
|
|
298
|
-
logFile: string;
|
|
299
|
-
pidFile: string;
|
|
300
|
-
waitMs: number;
|
|
301
|
-
}): Promise<void> {
|
|
302
|
-
const healthUrl = `http://localhost:${options.port}/health`;
|
|
303
|
-
const url = `http://localhost:${options.port}/workbench`;
|
|
304
|
-
const pid = readPidFile(options.pidFile);
|
|
305
|
-
const responsive = await isHealthy(healthUrl);
|
|
306
|
-
|
|
307
|
-
if (!pid) {
|
|
308
|
-
if (!responsive) {
|
|
309
|
-
removePidFile(options.pidFile);
|
|
310
|
-
outputJson({
|
|
311
|
-
ok: true,
|
|
312
|
-
daemon: true,
|
|
313
|
-
stopped: false,
|
|
314
|
-
running: false,
|
|
315
|
-
reason: 'No running daemon found.',
|
|
316
|
-
url,
|
|
317
|
-
healthUrl,
|
|
318
|
-
logFile: options.logFile,
|
|
319
|
-
pidFile: options.pidFile,
|
|
320
|
-
});
|
|
321
|
-
process.exit(0);
|
|
123
|
+
// Global target flags (--port / --server-url) may precede the subcommand, e.g.
|
|
124
|
+
// `pmx-canvas --port 4750 node list`. Skip them (and their values) when picking
|
|
125
|
+
// the subcommand to dispatch, so the agent CLI — which strips these flags via
|
|
126
|
+
// extractGlobalTargetFlags and validates them loudly — is reached instead of the
|
|
127
|
+
// server-startup fallback. Only these two flags are skipped; other leading flags
|
|
128
|
+
// (--mcp, --demo, …) still route exactly as before.
|
|
129
|
+
function commandAfterGlobalTargetFlags(argv: string[]): string {
|
|
130
|
+
for (let index = 0; index < argv.length; index++) {
|
|
131
|
+
const arg = argv[index];
|
|
132
|
+
if (arg === '--port' || arg === '--server-url') {
|
|
133
|
+
if (index + 1 < argv.length && !argv[index + 1].startsWith('-')) index++;
|
|
134
|
+
continue;
|
|
322
135
|
}
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
ok: false,
|
|
326
|
-
daemon: true,
|
|
327
|
-
error: `Server on port ${options.port} is responsive, but no pid file was found at ${options.pidFile}.`,
|
|
328
|
-
hint: 'Restart with `pmx-canvas serve --daemon` or provide the correct --pid-file.',
|
|
329
|
-
url,
|
|
330
|
-
healthUrl,
|
|
331
|
-
logFile: options.logFile,
|
|
332
|
-
pidFile: options.pidFile,
|
|
333
|
-
});
|
|
334
|
-
process.exit(1);
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
if (!isProcessRunning(pid)) {
|
|
338
|
-
removePidFile(options.pidFile);
|
|
339
|
-
outputJson({
|
|
340
|
-
ok: true,
|
|
341
|
-
daemon: true,
|
|
342
|
-
stopped: false,
|
|
343
|
-
running: responsive,
|
|
344
|
-
reason: `Removed stale pid file for ${pid}.`,
|
|
345
|
-
pid,
|
|
346
|
-
url,
|
|
347
|
-
healthUrl,
|
|
348
|
-
logFile: options.logFile,
|
|
349
|
-
pidFile: options.pidFile,
|
|
350
|
-
});
|
|
351
|
-
process.exit(0);
|
|
136
|
+
if (arg.startsWith('--port=') || arg.startsWith('--server-url=')) continue;
|
|
137
|
+
return arg;
|
|
352
138
|
}
|
|
353
|
-
|
|
354
|
-
process.kill(pid, 'SIGTERM');
|
|
355
|
-
const stopped = await waitForShutdown(healthUrl, options.waitMs, pid);
|
|
356
|
-
const stillResponsive = await isHealthy(healthUrl);
|
|
357
|
-
const pidRunning = isProcessRunning(pid);
|
|
358
|
-
if (stopped || (!stillResponsive && !pidRunning)) {
|
|
359
|
-
removePidFile(options.pidFile);
|
|
360
|
-
outputJson({
|
|
361
|
-
ok: true,
|
|
362
|
-
daemon: true,
|
|
363
|
-
stopped: true,
|
|
364
|
-
pid,
|
|
365
|
-
url,
|
|
366
|
-
healthUrl,
|
|
367
|
-
logFile: options.logFile,
|
|
368
|
-
pidFile: options.pidFile,
|
|
369
|
-
});
|
|
370
|
-
process.exit(0);
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
outputJson({
|
|
374
|
-
ok: false,
|
|
375
|
-
daemon: true,
|
|
376
|
-
stopped: false,
|
|
377
|
-
error: `Timed out waiting for daemon ${pid} to stop.`,
|
|
378
|
-
pid,
|
|
379
|
-
responsive: stillResponsive,
|
|
380
|
-
pidRunning,
|
|
381
|
-
url,
|
|
382
|
-
healthUrl,
|
|
383
|
-
logFile: options.logFile,
|
|
384
|
-
pidFile: options.pidFile,
|
|
385
|
-
});
|
|
386
|
-
process.exit(1);
|
|
139
|
+
return '';
|
|
387
140
|
}
|
|
388
141
|
|
|
389
142
|
function runMcpServerProcess(): Promise<void> {
|
|
@@ -398,16 +151,16 @@ function runMcpServerProcess(): Promise<void> {
|
|
|
398
151
|
resolvePromise();
|
|
399
152
|
return;
|
|
400
153
|
}
|
|
401
|
-
rejectPromise(
|
|
402
|
-
|
|
403
|
-
? `MCP server exited via signal ${signal}`
|
|
404
|
-
|
|
405
|
-
)
|
|
154
|
+
rejectPromise(
|
|
155
|
+
new Error(
|
|
156
|
+
signal ? `MCP server exited via signal ${signal}` : `MCP server exited with code ${code ?? 'unknown'}`,
|
|
157
|
+
),
|
|
158
|
+
);
|
|
406
159
|
});
|
|
407
160
|
});
|
|
408
161
|
}
|
|
409
162
|
|
|
410
|
-
const serveSubcommand = firstArg === 'serve' ? args[1] ?? '' : '';
|
|
163
|
+
const serveSubcommand = firstArg === 'serve' ? (args[1] ?? '') : '';
|
|
411
164
|
|
|
412
165
|
if (firstArg === 'serve' && (serveSubcommand === 'status' || serveSubcommand === 'stop')) {
|
|
413
166
|
const port = parseInt(readOption('port') ?? process.env.PMX_WEB_CANVAS_PORT ?? '4313');
|
|
@@ -430,6 +183,7 @@ Usage:
|
|
|
430
183
|
port,
|
|
431
184
|
logFile: daemonLogFile,
|
|
432
185
|
pidFile: daemonPidFile,
|
|
186
|
+
entry: fileURLToPath(import.meta.url),
|
|
433
187
|
});
|
|
434
188
|
} else {
|
|
435
189
|
await stopServeDaemon({
|
|
@@ -437,6 +191,7 @@ Usage:
|
|
|
437
191
|
logFile: daemonLogFile,
|
|
438
192
|
pidFile: daemonPidFile,
|
|
439
193
|
waitMs: daemonWaitMs,
|
|
194
|
+
entry: fileURLToPath(import.meta.url),
|
|
440
195
|
});
|
|
441
196
|
}
|
|
442
197
|
}
|
|
@@ -445,7 +200,8 @@ if (firstArg === 'serve') {
|
|
|
445
200
|
args.shift();
|
|
446
201
|
}
|
|
447
202
|
|
|
448
|
-
|
|
203
|
+
const routedCommand = commandAfterGlobalTargetFlags(args);
|
|
204
|
+
if (AGENT_COMMANDS.has(routedCommand) && routedCommand !== 'serve') {
|
|
449
205
|
await runAgentCli(args);
|
|
450
206
|
} else if (args.includes('--mcp')) {
|
|
451
207
|
// MCP server mode: stdio transport, auto-starts canvas on first tool call
|
|
@@ -468,9 +224,7 @@ if (AGENT_COMMANDS.has(firstArg) && firstArg !== 'serve') {
|
|
|
468
224
|
const daemonPidFile = resolve(readOption('pid-file') ?? `.pmx-canvas/daemon-${port}.pid`);
|
|
469
225
|
const daemonWaitMs = readNumberOption('wait-ms') ?? 10_000;
|
|
470
226
|
const webviewBackendOption: 'chrome' | 'webkit' | undefined =
|
|
471
|
-
webviewBackend === 'chrome' || webviewBackend === 'webkit'
|
|
472
|
-
? webviewBackend
|
|
473
|
-
: undefined;
|
|
227
|
+
webviewBackend === 'chrome' || webviewBackend === 'webkit' ? webviewBackend : undefined;
|
|
474
228
|
if (themeArg && ['dark', 'light', 'high-contrast'].includes(themeArg)) {
|
|
475
229
|
process.env.PMX_CANVAS_THEME = themeArg;
|
|
476
230
|
}
|
|
@@ -587,28 +341,31 @@ Examples:
|
|
|
587
341
|
}
|
|
588
342
|
|
|
589
343
|
if (daemon) {
|
|
590
|
-
const baseArgs = stripOption(
|
|
344
|
+
const baseArgs = stripOption(
|
|
345
|
+
stripOption(stripOption(stripOption(args, 'daemon'), 'log-file'), 'pid-file'),
|
|
346
|
+
'wait-ms',
|
|
347
|
+
);
|
|
591
348
|
await startDaemonMode({
|
|
592
349
|
port,
|
|
593
350
|
baseArgs,
|
|
594
351
|
logFile: daemonLogFile,
|
|
595
352
|
pidFile: daemonPidFile,
|
|
596
353
|
waitMs: daemonWaitMs,
|
|
354
|
+
entry: fileURLToPath(import.meta.url),
|
|
597
355
|
});
|
|
598
356
|
}
|
|
599
357
|
|
|
600
358
|
const canvas = createCanvas({ port });
|
|
601
|
-
const automationWebView =
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
: false;
|
|
359
|
+
const automationWebView = webviewAutomation
|
|
360
|
+
? {
|
|
361
|
+
...(webviewBackendOption ? { backend: webviewBackendOption } : {}),
|
|
362
|
+
...(webviewChromePath ? { chromePath: webviewChromePath } : {}),
|
|
363
|
+
...(webviewChromeArgv ? { chromeArgv: webviewChromeArgv } : {}),
|
|
364
|
+
...(webviewDataDir ? { dataStoreDir: webviewDataDir } : {}),
|
|
365
|
+
...(webviewWidth !== undefined ? { width: webviewWidth } : {}),
|
|
366
|
+
...(webviewHeight !== undefined ? { height: webviewHeight } : {}),
|
|
367
|
+
}
|
|
368
|
+
: false;
|
|
612
369
|
try {
|
|
613
370
|
await canvas.start({ open: !noOpen, automationWebView });
|
|
614
371
|
} catch (error) {
|
package/src/cli/watch.ts
CHANGED
|
@@ -7,16 +7,8 @@ import {
|
|
|
7
7
|
type SseMessage,
|
|
8
8
|
} from '../shared/semantic-attention.js';
|
|
9
9
|
|
|
10
|
-
export {
|
|
11
|
-
|
|
12
|
-
formatCompactWatchEvent,
|
|
13
|
-
SemanticWatchReducer,
|
|
14
|
-
};
|
|
15
|
-
export type {
|
|
16
|
-
SemanticWatchEvent,
|
|
17
|
-
SemanticWatchEventType,
|
|
18
|
-
SseMessage,
|
|
19
|
-
};
|
|
10
|
+
export { ALL_SEMANTIC_WATCH_EVENT_TYPES, formatCompactWatchEvent, SemanticWatchReducer };
|
|
11
|
+
export type { SemanticWatchEvent, SemanticWatchEventType, SseMessage };
|
|
20
12
|
|
|
21
13
|
export function parseSemanticEventFilter(raw: string | undefined): Set<SemanticWatchEventType> {
|
|
22
14
|
const all = new Set<SemanticWatchEventType>(ALL_SEMANTIC_WATCH_EVENT_TYPES);
|
package/src/client/App.tsx
CHANGED
|
@@ -165,7 +165,11 @@ function Toolbar({
|
|
|
165
165
|
|
|
166
166
|
<div class="separator" />
|
|
167
167
|
|
|
168
|
-
<ToolbarHint
|
|
168
|
+
<ToolbarHint
|
|
169
|
+
label="Canvas status"
|
|
170
|
+
detail={hasSynced ? statusLabel : 'Syncing canvas from server'}
|
|
171
|
+
align="start"
|
|
172
|
+
>
|
|
169
173
|
<span class={`connection-dot ${status}`} aria-label={`Canvas status: ${statusTitle}`} />
|
|
170
174
|
</ToolbarHint>
|
|
171
175
|
<span class="hud-collapsible-text" style={{ fontSize: '11px', color: 'var(--c-muted)' }}>
|
|
@@ -175,20 +179,12 @@ function Toolbar({
|
|
|
175
179
|
<div class="separator" />
|
|
176
180
|
|
|
177
181
|
<ToolbarHint label="Fit canvas" detail="Frame every node on screen">
|
|
178
|
-
<button
|
|
179
|
-
type="button"
|
|
180
|
-
onClick={() => fitAll(window.innerWidth, window.innerHeight)}
|
|
181
|
-
aria-label="Fit canvas"
|
|
182
|
-
>
|
|
182
|
+
<button type="button" onClick={() => fitAll(window.innerWidth, window.innerHeight)} aria-label="Fit canvas">
|
|
183
183
|
<IconFitAll />
|
|
184
184
|
</button>
|
|
185
185
|
</ToolbarHint>
|
|
186
186
|
<ToolbarHint label="Reset view" shortcut={`${MOD_KEY}+0`}>
|
|
187
|
-
<button
|
|
188
|
-
type="button"
|
|
189
|
-
onClick={() => animateViewport({ x: 0, y: 0, scale: 1 }, 250)}
|
|
190
|
-
aria-label="Reset view"
|
|
191
|
-
>
|
|
187
|
+
<button type="button" onClick={() => animateViewport({ x: 0, y: 0, scale: 1 }, 250)} aria-label="Reset view">
|
|
192
188
|
<IconResetView />
|
|
193
189
|
</button>
|
|
194
190
|
</ToolbarHint>
|
|
@@ -210,7 +206,10 @@ function Toolbar({
|
|
|
210
206
|
<IconZoomOut />
|
|
211
207
|
</button>
|
|
212
208
|
</ToolbarHint>
|
|
213
|
-
<span
|
|
209
|
+
<span
|
|
210
|
+
class="hud-collapsible-text"
|
|
211
|
+
style={{ fontSize: '10px', color: 'var(--c-dim)', minWidth: '36px', textAlign: 'center' }}
|
|
212
|
+
>
|
|
214
213
|
{Math.round(v.scale * 100)}%
|
|
215
214
|
</span>
|
|
216
215
|
|
|
@@ -220,7 +219,11 @@ function Toolbar({
|
|
|
220
219
|
label="Arrange layout"
|
|
221
220
|
detail={edgeCount > 0 ? 'Graph-aware layout for connected nodes' : 'Grid layout for loose nodes'}
|
|
222
221
|
>
|
|
223
|
-
<button
|
|
222
|
+
<button
|
|
223
|
+
type="button"
|
|
224
|
+
onClick={() => (edgeCount > 0 ? forceDirectedArrange() : autoArrange())}
|
|
225
|
+
aria-label="Arrange layout"
|
|
226
|
+
>
|
|
224
227
|
<IconArrange />
|
|
225
228
|
</button>
|
|
226
229
|
</ToolbarHint>
|
|
@@ -234,7 +237,10 @@ function Toolbar({
|
|
|
234
237
|
<IconMinimap />
|
|
235
238
|
</button>
|
|
236
239
|
</ToolbarHint>
|
|
237
|
-
<ToolbarHint
|
|
240
|
+
<ToolbarHint
|
|
241
|
+
label={`Switch to ${canvasTheme.value === 'dark' ? 'light' : 'dark'} theme`}
|
|
242
|
+
detail={`Current theme: ${canvasTheme.value}`}
|
|
243
|
+
>
|
|
238
244
|
<button
|
|
239
245
|
type="button"
|
|
240
246
|
onClick={() => {
|
|
@@ -280,13 +286,13 @@ function Toolbar({
|
|
|
280
286
|
{(isTraceOn || traceNodeCount > 0) && (
|
|
281
287
|
<ToolbarHint
|
|
282
288
|
label="Clear trace"
|
|
283
|
-
detail={
|
|
289
|
+
detail={
|
|
290
|
+
traceNodeCount > 0
|
|
291
|
+
? `Remove ${traceNodeCount} trace node${traceNodeCount === 1 ? '' : 's'}`
|
|
292
|
+
: 'Trace is enabled but still empty'
|
|
293
|
+
}
|
|
284
294
|
>
|
|
285
|
-
<button
|
|
286
|
-
type="button"
|
|
287
|
-
onClick={() => sendIntent('trace-clear')}
|
|
288
|
-
aria-label="Clear trace"
|
|
289
|
-
>
|
|
295
|
+
<button type="button" onClick={() => sendIntent('trace-clear')} aria-label="Clear trace">
|
|
290
296
|
<IconClearTrace />
|
|
291
297
|
</button>
|
|
292
298
|
</ToolbarHint>
|
|
@@ -340,25 +346,19 @@ function Toolbar({
|
|
|
340
346
|
<div class="separator" />
|
|
341
347
|
|
|
342
348
|
<ToolbarHint label="Search nodes and actions" shortcut={`${MOD_KEY}+K`}>
|
|
343
|
-
<button
|
|
344
|
-
type="button"
|
|
345
|
-
onClick={onOpenPalette}
|
|
346
|
-
aria-label="Search nodes and actions"
|
|
347
|
-
>
|
|
349
|
+
<button type="button" onClick={onOpenPalette} aria-label="Search nodes and actions">
|
|
348
350
|
<IconSearch />
|
|
349
351
|
</button>
|
|
350
352
|
</ToolbarHint>
|
|
351
353
|
<ToolbarHint label="Keyboard shortcuts" shortcut="?" align="end">
|
|
352
|
-
<button
|
|
353
|
-
type="button"
|
|
354
|
-
onClick={onOpenShortcuts}
|
|
355
|
-
aria-label="Keyboard shortcuts"
|
|
356
|
-
>
|
|
354
|
+
<button type="button" onClick={onOpenShortcuts} aria-label="Keyboard shortcuts">
|
|
357
355
|
<IconShortcuts />
|
|
358
356
|
</button>
|
|
359
357
|
</ToolbarHint>
|
|
360
358
|
|
|
361
|
-
<span class="hud-collapsible-text" style={{ fontSize: '10px', color: 'var(--c-dim)' }}>
|
|
359
|
+
<span class="hud-collapsible-text" style={{ fontSize: '10px', color: 'var(--c-dim)' }}>
|
|
360
|
+
{countsLabel}
|
|
361
|
+
</span>
|
|
362
362
|
</div>
|
|
363
363
|
</div>
|
|
364
364
|
);
|
|
@@ -370,7 +370,8 @@ function WelcomeCard({ onOpenPalette }: { onOpenPalette: () => void }) {
|
|
|
370
370
|
<div class="welcome-icon">◇</div>
|
|
371
371
|
<div class="welcome-title">Shape What The Agent Sees</div>
|
|
372
372
|
<div class="welcome-subtitle">
|
|
373
|
-
Lay out notes, files, and evidence. Bring related nodes together. Pin what matters. The board will reflect the
|
|
373
|
+
Lay out notes, files, and evidence. Bring related nodes together. Pin what matters. The board will reflect the
|
|
374
|
+
active focus.
|
|
374
375
|
</div>
|
|
375
376
|
<div class="welcome-hints">
|
|
376
377
|
<button type="button" class="welcome-hint" onClick={onOpenPalette}>
|
|
@@ -390,9 +391,7 @@ function WelcomeCard({ onOpenPalette }: { onOpenPalette: () => void }) {
|
|
|
390
391
|
<span>Shape the focus field</span>
|
|
391
392
|
</div>
|
|
392
393
|
</div>
|
|
393
|
-
<div class="welcome-footer">
|
|
394
|
-
The canvas is a shared attention surface, not just an editor.
|
|
395
|
-
</div>
|
|
394
|
+
<div class="welcome-footer">The canvas is a shared attention surface, not just an editor.</div>
|
|
396
395
|
</div>
|
|
397
396
|
);
|
|
398
397
|
}
|
|
@@ -410,9 +409,18 @@ export function App() {
|
|
|
410
409
|
const handleToggleMinimap = useCallback(() => setMinimapVisible((v) => !v), []);
|
|
411
410
|
const handleToggleSnapshot = useCallback(() => setSnapshotOpen((v) => !v), []);
|
|
412
411
|
const handleCloseSnapshot = useCallback(() => setSnapshotOpen(false), []);
|
|
413
|
-
const handleToggleAnnotationMode = useCallback(
|
|
414
|
-
|
|
415
|
-
|
|
412
|
+
const handleToggleAnnotationMode = useCallback(
|
|
413
|
+
() => setAnnotationTool((tool) => (tool === 'pen' ? null : 'pen')),
|
|
414
|
+
[],
|
|
415
|
+
);
|
|
416
|
+
const handleToggleAnnotationEraser = useCallback(
|
|
417
|
+
() => setAnnotationTool((tool) => (tool === 'eraser' ? null : 'eraser')),
|
|
418
|
+
[],
|
|
419
|
+
);
|
|
420
|
+
const handleToggleTextAnnotation = useCallback(
|
|
421
|
+
() => setAnnotationTool((tool) => (tool === 'text' ? null : 'text')),
|
|
422
|
+
[],
|
|
423
|
+
);
|
|
416
424
|
|
|
417
425
|
const handleMinimapNavigate = useCallback((x: number, y: number) => {
|
|
418
426
|
animateViewport({ x, y, scale: viewport.value.scale }, 200);
|
|
@@ -508,8 +516,7 @@ export function App() {
|
|
|
508
516
|
|
|
509
517
|
useEffect(() => {
|
|
510
518
|
if (!hasInitialLayout) return;
|
|
511
|
-
const ready = (window as Window & { __pmxCanvasBootstrapReady?: () => void })
|
|
512
|
-
.__pmxCanvasBootstrapReady;
|
|
519
|
+
const ready = (window as Window & { __pmxCanvasBootstrapReady?: () => void }).__pmxCanvasBootstrapReady;
|
|
513
520
|
if (typeof ready === 'function') ready();
|
|
514
521
|
}, [hasInitialLayout]);
|
|
515
522
|
|
|
@@ -575,12 +582,7 @@ export function App() {
|
|
|
575
582
|
/>
|
|
576
583
|
)}
|
|
577
584
|
{menu && <ContextMenu menu={menu} onClose={closeMenu} />}
|
|
578
|
-
{paletteOpen && (
|
|
579
|
-
<CommandPalette
|
|
580
|
-
onClose={() => setPaletteOpen(false)}
|
|
581
|
-
onToggleMinimap={handleToggleMinimap}
|
|
582
|
-
/>
|
|
583
|
-
)}
|
|
585
|
+
{paletteOpen && <CommandPalette onClose={() => setPaletteOpen(false)} onToggleMinimap={handleToggleMinimap} />}
|
|
584
586
|
{shortcutsOpen && <ShortcutOverlay onClose={() => setShortcutsOpen(false)} />}
|
|
585
587
|
</div>
|
|
586
588
|
);
|