pmx-canvas 0.2.7 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (189) hide show
  1. package/CHANGELOG.md +148 -1
  2. package/Readme.md +13 -11
  3. package/dist/canvas/index.js +2 -2
  4. package/dist/json-render/index.css +1 -1
  5. package/dist/types/cli/daemon.d.ts +74 -0
  6. package/dist/types/cli/watch.d.ts +2 -2
  7. package/dist/types/client/canvas/CanvasViewport.d.ts +1 -1
  8. package/dist/types/client/canvas/CommandPalette.d.ts +1 -1
  9. package/dist/types/client/canvas/Minimap.d.ts +1 -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/mcp/canvas-access.d.ts +0 -7
  17. package/dist/types/server/ax-state-manager.d.ts +3 -2
  18. package/dist/types/server/bundled-skills.d.ts +3 -3
  19. package/dist/types/server/canvas-schema.d.ts +1 -1
  20. package/dist/types/server/canvas-state.d.ts +2 -0
  21. package/dist/types/server/ext-app-lookup.d.ts +1 -3
  22. package/dist/types/server/html-primitives.d.ts +1 -1
  23. package/dist/types/server/index.d.ts +4 -4
  24. package/dist/types/server/operations/composites.d.ts +29 -23
  25. package/dist/types/server/operations/http.d.ts +2 -1
  26. package/dist/types/server/operations/index.d.ts +1 -1
  27. package/dist/types/server/operations/ops/ax-read.d.ts +2 -0
  28. package/dist/types/server/operations/ops/canvas-wire.d.ts +2 -0
  29. package/dist/types/server/operations/ops/ext-app.d.ts +2 -0
  30. package/dist/types/shared/ax-intent.d.ts +1 -1
  31. package/docs/RELEASE.md +23 -8
  32. package/docs/ax-host-adapter-contract.md +7 -7
  33. package/docs/ax-state-contract.md +7 -6
  34. package/docs/bun-webview-integration.md +5 -5
  35. package/docs/cli.md +1 -1
  36. package/docs/http-api.md +35 -3
  37. package/docs/mcp.md +121 -100
  38. package/docs/node-types.md +45 -35
  39. package/docs/plans/plan-006-mcp-tool-consolidation.md +3 -1
  40. package/docs/plans/plan-008-registry-finish.md +2 -0
  41. package/docs/plans/plan-009-tech-debt-backlog.md +51 -0
  42. package/docs/screenshot.png +0 -0
  43. package/docs/tech-debt-assessment-2026-06.md +1 -1
  44. package/docs/tech-debt-assessment-2026-07.md +135 -0
  45. package/package.json +5 -2
  46. package/skills/data-analysis/SKILL.md +3 -3
  47. package/skills/frontend-design/SKILL.md +3 -2
  48. package/skills/json-render-mcp/SKILL.md +4 -3
  49. package/skills/playwright-cli/SKILL.md +1 -1
  50. package/skills/pmx-canvas/SKILL.md +12 -7
  51. package/skills/pmx-canvas/evals/evals.json +2 -2
  52. package/skills/pmx-canvas/references/ax-html-control-surface.md +3 -1
  53. package/skills/pmx-canvas/references/excalidraw-diagram-authoring.md +2 -2
  54. package/skills/pmx-canvas/references/full-reference.md +58 -56
  55. package/skills/pmx-canvas/references/html-primitives.md +3 -3
  56. package/skills/tufte-viz/SKILL.md +5 -4
  57. package/skills/web-artifacts-builder/SKILL.md +4 -4
  58. package/src/cli/agent.ts +1861 -1548
  59. package/src/cli/daemon.ts +460 -0
  60. package/src/cli/index.ts +63 -326
  61. package/src/cli/watch.ts +2 -10
  62. package/src/client/App.tsx +48 -46
  63. package/src/client/canvas/AttentionHistory.tsx +11 -1
  64. package/src/client/canvas/CanvasNode.tsx +41 -29
  65. package/src/client/canvas/CanvasViewport.tsx +101 -66
  66. package/src/client/canvas/CommandPalette.tsx +61 -27
  67. package/src/client/canvas/ContextMenu.tsx +13 -20
  68. package/src/client/canvas/ContextPinBar.tsx +1 -5
  69. package/src/client/canvas/ContextPinHud.tsx +1 -6
  70. package/src/client/canvas/DockedNode.tsx +4 -4
  71. package/src/client/canvas/EdgeLayer.tsx +37 -36
  72. package/src/client/canvas/ExpandedNodeOverlay.tsx +69 -45
  73. package/src/client/canvas/FocusFieldLayer.tsx +20 -22
  74. package/src/client/canvas/IntentLayer.tsx +31 -14
  75. package/src/client/canvas/Minimap.tsx +11 -16
  76. package/src/client/canvas/SelectionBar.tsx +4 -11
  77. package/src/client/canvas/ShortcutOverlay.tsx +3 -1
  78. package/src/client/canvas/SnapshotPanel.tsx +77 -95
  79. package/src/client/canvas/auto-fit.ts +15 -14
  80. package/src/client/canvas/snap-guides.ts +12 -12
  81. package/src/client/canvas/use-node-resize.ts +1 -5
  82. package/src/client/canvas/use-pan-zoom.ts +25 -26
  83. package/src/client/ext-app/bridge.ts +3 -12
  84. package/src/client/icons.tsx +63 -20
  85. package/src/client/nodes/ContextNode.tsx +14 -25
  86. package/src/client/nodes/ExtAppFrame.tsx +60 -39
  87. package/src/client/nodes/FileNode.tsx +74 -62
  88. package/src/client/nodes/GroupNode.tsx +4 -6
  89. package/src/client/nodes/HtmlNode.tsx +76 -46
  90. package/src/client/nodes/ImageNode.tsx +18 -27
  91. package/src/client/nodes/InlineFormatBar.tsx +4 -21
  92. package/src/client/nodes/InlineMarkdownEditor.tsx +0 -1
  93. package/src/client/nodes/LedgerNode.tsx +10 -4
  94. package/src/client/nodes/MarkdownNode.tsx +3 -10
  95. package/src/client/nodes/McpAppNode.tsx +26 -22
  96. package/src/client/nodes/MdFormatBar.tsx +10 -7
  97. package/src/client/nodes/PromptNode.tsx +23 -51
  98. package/src/client/nodes/ResponseNode.tsx +3 -13
  99. package/src/client/nodes/StatusNode.tsx +5 -9
  100. package/src/client/nodes/StatusSummary.tsx +2 -8
  101. package/src/client/nodes/WebpageNode.tsx +20 -14
  102. package/src/client/nodes/iframe-document-url.ts +25 -16
  103. package/src/client/nodes/image-warnings.ts +1 -7
  104. package/src/client/nodes/md-format.ts +20 -5
  105. package/src/client/state/attention-bridge.ts +4 -9
  106. package/src/client/state/attention-store.ts +1 -7
  107. package/src/client/state/canvas-store.ts +52 -36
  108. package/src/client/state/intent-bridge.ts +176 -112
  109. package/src/client/state/intent-store.ts +4 -1
  110. package/src/client/state/sse-bridge.ts +53 -70
  111. package/src/json-render/catalog.ts +12 -16
  112. package/src/json-render/charts/components.tsx +16 -20
  113. package/src/json-render/charts/extra-components.tsx +8 -16
  114. package/src/json-render/charts/extra-definitions.ts +1 -2
  115. package/src/json-render/charts/tufte-components.tsx +37 -20
  116. package/src/json-render/charts/tufte-definitions.ts +8 -2
  117. package/src/json-render/renderer/index.tsx +42 -22
  118. package/src/json-render/schema.ts +6 -3
  119. package/src/json-render/server.ts +33 -39
  120. package/src/mcp/canvas-access.ts +40 -121
  121. package/src/mcp/server.ts +206 -271
  122. package/src/server/agent-context.ts +63 -36
  123. package/src/server/ax-context.ts +7 -5
  124. package/src/server/ax-interaction.ts +176 -43
  125. package/src/server/ax-state-manager.ts +185 -41
  126. package/src/server/ax-state.ts +142 -47
  127. package/src/server/bundled-skills.ts +3 -3
  128. package/src/server/canvas-db.ts +213 -95
  129. package/src/server/canvas-operations.ts +177 -120
  130. package/src/server/canvas-provenance.ts +1 -4
  131. package/src/server/canvas-schema.ts +500 -102
  132. package/src/server/canvas-serialization.ts +27 -35
  133. package/src/server/canvas-state.ts +155 -58
  134. package/src/server/chart-template.ts +4 -5
  135. package/src/server/code-graph.ts +20 -7
  136. package/src/server/diagram-presets.ts +28 -29
  137. package/src/server/ext-app-lookup.ts +4 -20
  138. package/src/server/html-node-summary.ts +19 -10
  139. package/src/server/html-primitives.ts +326 -97
  140. package/src/server/html-surface.ts +6 -9
  141. package/src/server/image-source.ts +6 -4
  142. package/src/server/index.ts +321 -218
  143. package/src/server/intent-registry.ts +3 -6
  144. package/src/server/mcp-app-candidate.ts +5 -10
  145. package/src/server/mcp-app-host.ts +14 -38
  146. package/src/server/mcp-app-runtime.ts +12 -20
  147. package/src/server/mutation-history.ts +15 -5
  148. package/src/server/operations/composites.ts +38 -40
  149. package/src/server/operations/http.ts +5 -5
  150. package/src/server/operations/index.ts +7 -1
  151. package/src/server/operations/invoker.ts +4 -3
  152. package/src/server/operations/mcp.ts +29 -36
  153. package/src/server/operations/ops/annotation.ts +122 -10
  154. package/src/server/operations/ops/app.ts +102 -77
  155. package/src/server/operations/ops/ax-await.ts +17 -10
  156. package/src/server/operations/ops/ax-read.ts +347 -0
  157. package/src/server/operations/ops/ax-shared.ts +2 -7
  158. package/src/server/operations/ops/ax-state.ts +32 -14
  159. package/src/server/operations/ops/ax-timeline.ts +32 -19
  160. package/src/server/operations/ops/ax-work.ts +54 -37
  161. package/src/server/operations/ops/batch.ts +39 -14
  162. package/src/server/operations/ops/canvas-wire.ts +91 -0
  163. package/src/server/operations/ops/edges.ts +37 -25
  164. package/src/server/operations/ops/ext-app.ts +346 -0
  165. package/src/server/operations/ops/groups.ts +49 -20
  166. package/src/server/operations/ops/intent.ts +18 -12
  167. package/src/server/operations/ops/json-render.ts +239 -98
  168. package/src/server/operations/ops/nodes.ts +300 -111
  169. package/src/server/operations/ops/query.ts +62 -33
  170. package/src/server/operations/ops/snapshots.ts +35 -26
  171. package/src/server/operations/ops/validate.ts +2 -1
  172. package/src/server/operations/ops/viewport.ts +60 -16
  173. package/src/server/operations/ops/webview.ts +44 -18
  174. package/src/server/operations/registry.ts +2 -3
  175. package/src/server/operations/types.ts +7 -5
  176. package/src/server/operations/webview-runner.ts +1 -3
  177. package/src/server/placement.ts +8 -18
  178. package/src/server/server.ts +121 -1012
  179. package/src/server/spatial-analysis.ts +39 -25
  180. package/src/server/trace-manager.ts +3 -8
  181. package/src/server/web-artifacts.ts +23 -27
  182. package/src/server/webpage-node.ts +5 -13
  183. package/src/shared/auto-arrange.ts +12 -5
  184. package/src/shared/ax-intent.ts +1 -1
  185. package/src/shared/content-height-reporter.ts +8 -6
  186. package/src/shared/ext-app-tool-result.ts +2 -6
  187. package/src/shared/placement.ts +1 -4
  188. package/src/shared/semantic-attention.ts +39 -37
  189. 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,306 +120,6 @@ 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);
322
- }
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);
352
- }
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);
387
- }
388
-
389
123
  function runMcpServerProcess(): Promise<void> {
390
124
  return new Promise((resolvePromise, rejectPromise) => {
391
125
  const child = spawn(process.execPath, ['run', mcpServerEntry], {
@@ -398,16 +132,16 @@ function runMcpServerProcess(): Promise<void> {
398
132
  resolvePromise();
399
133
  return;
400
134
  }
401
- rejectPromise(new Error(
402
- signal
403
- ? `MCP server exited via signal ${signal}`
404
- : `MCP server exited with code ${code ?? 'unknown'}`,
405
- ));
135
+ rejectPromise(
136
+ new Error(
137
+ signal ? `MCP server exited via signal ${signal}` : `MCP server exited with code ${code ?? 'unknown'}`,
138
+ ),
139
+ );
406
140
  });
407
141
  });
408
142
  }
409
143
 
410
- const serveSubcommand = firstArg === 'serve' ? args[1] ?? '' : '';
144
+ const serveSubcommand = firstArg === 'serve' ? (args[1] ?? '') : '';
411
145
 
412
146
  if (firstArg === 'serve' && (serveSubcommand === 'status' || serveSubcommand === 'stop')) {
413
147
  const port = parseInt(readOption('port') ?? process.env.PMX_WEB_CANVAS_PORT ?? '4313');
@@ -430,6 +164,7 @@ Usage:
430
164
  port,
431
165
  logFile: daemonLogFile,
432
166
  pidFile: daemonPidFile,
167
+ entry: fileURLToPath(import.meta.url),
433
168
  });
434
169
  } else {
435
170
  await stopServeDaemon({
@@ -437,6 +172,7 @@ Usage:
437
172
  logFile: daemonLogFile,
438
173
  pidFile: daemonPidFile,
439
174
  waitMs: daemonWaitMs,
175
+ entry: fileURLToPath(import.meta.url),
440
176
  });
441
177
  }
442
178
  }
@@ -468,9 +204,7 @@ if (AGENT_COMMANDS.has(firstArg) && firstArg !== 'serve') {
468
204
  const daemonPidFile = resolve(readOption('pid-file') ?? `.pmx-canvas/daemon-${port}.pid`);
469
205
  const daemonWaitMs = readNumberOption('wait-ms') ?? 10_000;
470
206
  const webviewBackendOption: 'chrome' | 'webkit' | undefined =
471
- webviewBackend === 'chrome' || webviewBackend === 'webkit'
472
- ? webviewBackend
473
- : undefined;
207
+ webviewBackend === 'chrome' || webviewBackend === 'webkit' ? webviewBackend : undefined;
474
208
  if (themeArg && ['dark', 'light', 'high-contrast'].includes(themeArg)) {
475
209
  process.env.PMX_CANVAS_THEME = themeArg;
476
210
  }
@@ -587,28 +321,31 @@ Examples:
587
321
  }
588
322
 
589
323
  if (daemon) {
590
- const baseArgs = stripOption(stripOption(stripOption(stripOption(args, 'daemon'), 'log-file'), 'pid-file'), 'wait-ms');
324
+ const baseArgs = stripOption(
325
+ stripOption(stripOption(stripOption(args, 'daemon'), 'log-file'), 'pid-file'),
326
+ 'wait-ms',
327
+ );
591
328
  await startDaemonMode({
592
329
  port,
593
330
  baseArgs,
594
331
  logFile: daemonLogFile,
595
332
  pidFile: daemonPidFile,
596
333
  waitMs: daemonWaitMs,
334
+ entry: fileURLToPath(import.meta.url),
597
335
  });
598
336
  }
599
337
 
600
338
  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;
339
+ const automationWebView = webviewAutomation
340
+ ? {
341
+ ...(webviewBackendOption ? { backend: webviewBackendOption } : {}),
342
+ ...(webviewChromePath ? { chromePath: webviewChromePath } : {}),
343
+ ...(webviewChromeArgv ? { chromeArgv: webviewChromeArgv } : {}),
344
+ ...(webviewDataDir ? { dataStoreDir: webviewDataDir } : {}),
345
+ ...(webviewWidth !== undefined ? { width: webviewWidth } : {}),
346
+ ...(webviewHeight !== undefined ? { height: webviewHeight } : {}),
347
+ }
348
+ : false;
612
349
  try {
613
350
  await canvas.start({ open: !noOpen, automationWebView });
614
351
  } 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
  );
@@ -42,7 +42,17 @@ export function AttentionHistory() {
42
42
  aria-label={unread > 0 ? `Recent updates — ${unread} new` : 'Recent updates'}
43
43
  title={unread > 0 ? `${unread} new updates since last viewed` : 'Recent updates'}
44
44
  >
45
- <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
45
+ <svg
46
+ width="14"
47
+ height="14"
48
+ viewBox="0 0 16 16"
49
+ fill="none"
50
+ stroke="currentColor"
51
+ stroke-width="1.5"
52
+ stroke-linecap="round"
53
+ stroke-linejoin="round"
54
+ aria-hidden="true"
55
+ >
46
56
  <rect x="1.5" y="2.5" width="13" height="11" rx="1.5" />
47
57
  <circle cx="4.5" cy="8" r="1.1" fill="currentColor" stroke="none" />
48
58
  <line x1="6.5" y1="6.5" x2="12.5" y2="6.5" />