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.
Files changed (161) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/dist/canvas/index.js +65 -65
  3. package/dist/types/cli/agent.d.ts +9 -1
  4. package/dist/types/cli/daemon.d.ts +74 -0
  5. package/dist/types/cli/watch.d.ts +2 -2
  6. package/dist/types/client/canvas/CanvasViewport.d.ts +1 -1
  7. package/dist/types/client/canvas/CommandPalette.d.ts +1 -1
  8. package/dist/types/client/canvas/Minimap.d.ts +1 -1
  9. package/dist/types/client/nodes/ExtAppFrame.d.ts +9 -1
  10. package/dist/types/client/nodes/FileNode.d.ts +1 -1
  11. package/dist/types/client/nodes/ImageNode.d.ts +1 -1
  12. package/dist/types/client/nodes/InlineFormatBar.d.ts +1 -1
  13. package/dist/types/client/nodes/MarkdownNode.d.ts +1 -1
  14. package/dist/types/client/nodes/PromptNode.d.ts +1 -1
  15. package/dist/types/client/nodes/ResponseNode.d.ts +1 -1
  16. package/dist/types/server/canvas-schema.d.ts +1 -1
  17. package/dist/types/server/html-primitives.d.ts +1 -1
  18. package/dist/types/server/index.d.ts +4 -4
  19. package/dist/types/server/operations/index.d.ts +1 -1
  20. package/dist/types/server/operations/ops/ax-read.d.ts +2 -0
  21. package/dist/types/server/operations/ops/canvas-wire.d.ts +2 -0
  22. package/dist/types/server/operations/ops/ext-app.d.ts +2 -0
  23. package/dist/types/server/server.d.ts +8 -0
  24. package/docs/cli.md +10 -1
  25. package/docs/http-api.md +28 -0
  26. package/docs/plans/plan-009-tech-debt-backlog.md +5 -5
  27. package/docs/screenshot.png +0 -0
  28. package/docs/tech-debt-assessment-2026-07.md +2 -2
  29. package/package.json +5 -2
  30. package/skills/pmx-canvas/SKILL.md +15 -10
  31. package/skills/pmx-canvas/references/full-reference.md +17 -3
  32. package/src/cli/agent.ts +1951 -1571
  33. package/src/cli/daemon.ts +460 -0
  34. package/src/cli/index.ts +80 -323
  35. package/src/cli/watch.ts +2 -10
  36. package/src/client/App.tsx +48 -46
  37. package/src/client/canvas/AttentionHistory.tsx +11 -1
  38. package/src/client/canvas/CanvasNode.tsx +41 -29
  39. package/src/client/canvas/CanvasViewport.tsx +101 -66
  40. package/src/client/canvas/CommandPalette.tsx +61 -27
  41. package/src/client/canvas/ContextMenu.tsx +13 -20
  42. package/src/client/canvas/ContextPinBar.tsx +1 -5
  43. package/src/client/canvas/ContextPinHud.tsx +1 -6
  44. package/src/client/canvas/DockedNode.tsx +4 -4
  45. package/src/client/canvas/EdgeLayer.tsx +37 -36
  46. package/src/client/canvas/ExpandedNodeOverlay.tsx +69 -45
  47. package/src/client/canvas/FocusFieldLayer.tsx +20 -22
  48. package/src/client/canvas/IntentLayer.tsx +31 -14
  49. package/src/client/canvas/Minimap.tsx +11 -16
  50. package/src/client/canvas/SelectionBar.tsx +4 -11
  51. package/src/client/canvas/ShortcutOverlay.tsx +3 -1
  52. package/src/client/canvas/SnapshotPanel.tsx +77 -95
  53. package/src/client/canvas/auto-fit.ts +15 -14
  54. package/src/client/canvas/snap-guides.ts +12 -12
  55. package/src/client/canvas/use-node-resize.ts +1 -5
  56. package/src/client/canvas/use-pan-zoom.ts +25 -26
  57. package/src/client/ext-app/bridge.ts +3 -12
  58. package/src/client/icons.tsx +63 -20
  59. package/src/client/nodes/ContextNode.tsx +14 -25
  60. package/src/client/nodes/ExtAppFrame.tsx +194 -80
  61. package/src/client/nodes/FileNode.tsx +74 -62
  62. package/src/client/nodes/GroupNode.tsx +4 -6
  63. package/src/client/nodes/HtmlNode.tsx +76 -46
  64. package/src/client/nodes/ImageNode.tsx +18 -27
  65. package/src/client/nodes/InlineFormatBar.tsx +4 -21
  66. package/src/client/nodes/InlineMarkdownEditor.tsx +0 -1
  67. package/src/client/nodes/LedgerNode.tsx +10 -4
  68. package/src/client/nodes/MarkdownNode.tsx +3 -10
  69. package/src/client/nodes/McpAppNode.tsx +26 -22
  70. package/src/client/nodes/MdFormatBar.tsx +10 -7
  71. package/src/client/nodes/PromptNode.tsx +23 -51
  72. package/src/client/nodes/ResponseNode.tsx +3 -13
  73. package/src/client/nodes/StatusNode.tsx +5 -9
  74. package/src/client/nodes/StatusSummary.tsx +2 -8
  75. package/src/client/nodes/WebpageNode.tsx +20 -14
  76. package/src/client/nodes/iframe-document-url.ts +25 -16
  77. package/src/client/nodes/image-warnings.ts +1 -7
  78. package/src/client/nodes/md-format.ts +20 -5
  79. package/src/client/state/attention-bridge.ts +4 -9
  80. package/src/client/state/attention-store.ts +1 -7
  81. package/src/client/state/canvas-store.ts +52 -36
  82. package/src/client/state/intent-bridge.ts +176 -112
  83. package/src/client/state/intent-store.ts +4 -1
  84. package/src/client/state/sse-bridge.ts +53 -70
  85. package/src/json-render/catalog.ts +12 -16
  86. package/src/json-render/charts/components.tsx +16 -20
  87. package/src/json-render/charts/extra-components.tsx +8 -16
  88. package/src/json-render/charts/extra-definitions.ts +1 -2
  89. package/src/json-render/charts/tufte-components.tsx +37 -20
  90. package/src/json-render/charts/tufte-definitions.ts +8 -2
  91. package/src/json-render/renderer/index.tsx +42 -22
  92. package/src/json-render/schema.ts +6 -3
  93. package/src/json-render/server.ts +33 -39
  94. package/src/mcp/canvas-access.ts +35 -21
  95. package/src/mcp/server.ts +132 -70
  96. package/src/server/agent-context.ts +63 -36
  97. package/src/server/ax-context.ts +7 -5
  98. package/src/server/ax-interaction.ts +176 -43
  99. package/src/server/ax-state-manager.ts +182 -39
  100. package/src/server/ax-state.ts +142 -47
  101. package/src/server/canvas-db.ts +213 -95
  102. package/src/server/canvas-operations.ts +180 -123
  103. package/src/server/canvas-provenance.ts +1 -4
  104. package/src/server/canvas-schema.ts +454 -73
  105. package/src/server/canvas-serialization.ts +27 -35
  106. package/src/server/canvas-state.ts +150 -58
  107. package/src/server/chart-template.ts +4 -5
  108. package/src/server/code-graph.ts +19 -6
  109. package/src/server/diagram-presets.ts +28 -29
  110. package/src/server/ext-app-lookup.ts +3 -12
  111. package/src/server/html-node-summary.ts +19 -10
  112. package/src/server/html-primitives.ts +326 -97
  113. package/src/server/html-surface.ts +6 -9
  114. package/src/server/image-source.ts +6 -4
  115. package/src/server/index.ts +320 -217
  116. package/src/server/intent-registry.ts +2 -5
  117. package/src/server/mcp-app-candidate.ts +5 -10
  118. package/src/server/mcp-app-host.ts +14 -38
  119. package/src/server/mcp-app-runtime.ts +12 -20
  120. package/src/server/mutation-history.ts +15 -5
  121. package/src/server/operations/composites.ts +1 -3
  122. package/src/server/operations/http.ts +2 -3
  123. package/src/server/operations/index.ts +7 -1
  124. package/src/server/operations/invoker.ts +4 -3
  125. package/src/server/operations/mcp.ts +22 -30
  126. package/src/server/operations/ops/annotation.ts +122 -10
  127. package/src/server/operations/ops/app.ts +98 -73
  128. package/src/server/operations/ops/ax-await.ts +17 -10
  129. package/src/server/operations/ops/ax-read.ts +347 -0
  130. package/src/server/operations/ops/ax-shared.ts +2 -7
  131. package/src/server/operations/ops/ax-state.ts +32 -14
  132. package/src/server/operations/ops/ax-timeline.ts +32 -19
  133. package/src/server/operations/ops/ax-work.ts +54 -37
  134. package/src/server/operations/ops/batch.ts +41 -15
  135. package/src/server/operations/ops/canvas-wire.ts +91 -0
  136. package/src/server/operations/ops/edges.ts +37 -25
  137. package/src/server/operations/ops/ext-app.ts +346 -0
  138. package/src/server/operations/ops/groups.ts +49 -20
  139. package/src/server/operations/ops/intent.ts +18 -12
  140. package/src/server/operations/ops/json-render.ts +239 -98
  141. package/src/server/operations/ops/nodes.ts +298 -109
  142. package/src/server/operations/ops/query.ts +46 -28
  143. package/src/server/operations/ops/snapshots.ts +35 -26
  144. package/src/server/operations/ops/validate.ts +2 -1
  145. package/src/server/operations/ops/viewport.ts +60 -16
  146. package/src/server/operations/ops/webview.ts +44 -18
  147. package/src/server/operations/registry.ts +2 -3
  148. package/src/server/operations/types.ts +7 -5
  149. package/src/server/operations/webview-runner.ts +1 -3
  150. package/src/server/placement.ts +8 -18
  151. package/src/server/server.ts +122 -1028
  152. package/src/server/spatial-analysis.ts +39 -25
  153. package/src/server/trace-manager.ts +3 -8
  154. package/src/server/web-artifacts.ts +23 -27
  155. package/src/server/webpage-node.ts +5 -13
  156. package/src/shared/auto-arrange.ts +12 -5
  157. package/src/shared/content-height-reporter.ts +8 -6
  158. package/src/shared/ext-app-tool-result.ts +2 -6
  159. package/src/shared/placement.ts +1 -4
  160. package/src/shared/semantic-attention.ts +39 -37
  161. 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 { existsSync, mkdirSync, openSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
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', 'edge', 'json-render', 'search', 'layout', 'status', 'arrange', 'focus',
35
- 'fit', 'screenshot', 'pin', 'ax', 'undo', 'redo', 'history', 'snapshot', 'diff', 'group', 'webview', 'open',
36
- 'clear', 'code-graph', 'spatial', 'watch', 'web-artifact', 'external-app', 'diagram', 'graph', 'html', 'batch', 'validate', 'serve', 'copilot',
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.split(',').map((value) => value.trim()).filter((value) => value.length > 0);
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
- function outputJson(data: unknown): void {
90
- console.log(JSON.stringify(data, null, 2));
91
- }
92
-
93
- function readPidFile(path: string): number | null {
94
- try {
95
- if (!existsSync(path)) return null;
96
- const raw = readFileSync(path, 'utf-8').trim();
97
- if (!raw) return null;
98
- const pid = Number(raw);
99
- return Number.isInteger(pid) && pid > 0 ? pid : null;
100
- } catch {
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
- outputJson({
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(new Error(
402
- signal
403
- ? `MCP server exited via signal ${signal}`
404
- : `MCP server exited with code ${code ?? 'unknown'}`,
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
- if (AGENT_COMMANDS.has(firstArg) && firstArg !== 'serve') {
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(stripOption(stripOption(stripOption(args, 'daemon'), 'log-file'), 'pid-file'), 'wait-ms');
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
- webviewAutomation
603
- ? {
604
- ...(webviewBackendOption ? { backend: webviewBackendOption } : {}),
605
- ...(webviewChromePath ? { chromePath: webviewChromePath } : {}),
606
- ...(webviewChromeArgv ? { chromeArgv: webviewChromeArgv } : {}),
607
- ...(webviewDataDir ? { dataStoreDir: webviewDataDir } : {}),
608
- ...(webviewWidth !== undefined ? { width: webviewWidth } : {}),
609
- ...(webviewHeight !== undefined ? { height: webviewHeight } : {}),
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
- ALL_SEMANTIC_WATCH_EVENT_TYPES,
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);
@@ -165,7 +165,11 @@ function Toolbar({
165
165
 
166
166
  <div class="separator" />
167
167
 
168
- <ToolbarHint label="Canvas status" detail={hasSynced ? statusLabel : 'Syncing canvas from server'} align="start">
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 class="hud-collapsible-text" style={{ fontSize: '10px', color: 'var(--c-dim)', minWidth: '36px', textAlign: 'center' }}>
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 type="button" onClick={() => edgeCount > 0 ? forceDirectedArrange() : autoArrange()} aria-label="Arrange layout">
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 label={`Switch to ${canvasTheme.value === 'dark' ? 'light' : 'dark'} theme`} detail={`Current theme: ${canvasTheme.value}`}>
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={traceNodeCount > 0 ? `Remove ${traceNodeCount} trace node${traceNodeCount === 1 ? '' : 's'}` : 'Trace is enabled but still empty'}
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)' }}>{countsLabel}</span>
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 active focus.
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(() => setAnnotationTool((tool) => tool === 'pen' ? null : 'pen'), []);
414
- const handleToggleAnnotationEraser = useCallback(() => setAnnotationTool((tool) => tool === 'eraser' ? null : 'eraser'), []);
415
- const handleToggleTextAnnotation = useCallback(() => setAnnotationTool((tool) => tool === 'text' ? null : 'text'), []);
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
  );