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
@@ -2,12 +2,7 @@ function logRequestError(action: string, error: unknown): void {
2
2
  console.error(`[intent-bridge] ${action} failed`, error);
3
3
  }
4
4
 
5
- async function requestJson<T>(
6
- action: string,
7
- url: string,
8
- fallback: T,
9
- init?: RequestInit,
10
- ): Promise<T> {
5
+ async function requestJson<T>(action: string, url: string, fallback: T, init?: RequestInit): Promise<T> {
11
6
  try {
12
7
  const res = await fetch(url, init);
13
8
  return (await res.json()) as T;
@@ -17,11 +12,7 @@ async function requestJson<T>(
17
12
  }
18
13
  }
19
14
 
20
- async function requestOk(
21
- action: string,
22
- url: string,
23
- init?: RequestInit,
24
- ): Promise<{ ok: boolean }> {
15
+ async function requestOk(action: string, url: string, init?: RequestInit): Promise<{ ok: boolean }> {
25
16
  try {
26
17
  const res = await fetch(url, init);
27
18
  return { ok: res.ok };
@@ -31,11 +22,7 @@ async function requestOk(
31
22
  }
32
23
  }
33
24
 
34
- async function requestBestEffort(
35
- action: string,
36
- url: string,
37
- init?: RequestInit,
38
- ): Promise<void> {
25
+ async function requestBestEffort(action: string, url: string, init?: RequestInit): Promise<void> {
39
26
  try {
40
27
  await fetch(url, init);
41
28
  } catch (error) {
@@ -44,15 +31,17 @@ async function requestBestEffort(
44
31
  }
45
32
 
46
33
  /** Dispatch user intents from the canvas to the server (for TUI consumption). */
47
- export async function sendIntent(
48
- type: string,
49
- payload: Record<string, unknown> = {},
50
- ): Promise<{ ok: boolean }> {
51
- return requestJson('sendIntent', '/api/workbench/intent', { ok: false }, {
52
- method: 'POST',
53
- headers: { 'Content-Type': 'application/json' },
54
- body: JSON.stringify({ type, payload }),
55
- });
34
+ export async function sendIntent(type: string, payload: Record<string, unknown> = {}): Promise<{ ok: boolean }> {
35
+ return requestJson(
36
+ 'sendIntent',
37
+ '/api/workbench/intent',
38
+ { ok: false },
39
+ {
40
+ method: 'POST',
41
+ headers: { 'Content-Type': 'application/json' },
42
+ body: JSON.stringify({ type, payload }),
43
+ },
44
+ );
56
45
  }
57
46
 
58
47
  /**
@@ -88,11 +77,16 @@ export async function vetoGhostIntent(intent: {
88
77
 
89
78
  /** Fetch rendered markdown HTML from the server. */
90
79
  export async function renderMarkdown(markdown: string): Promise<string> {
91
- const data = await requestJson<{ html?: string }>('renderMarkdown', '/api/render', {}, {
92
- method: 'POST',
93
- headers: { 'Content-Type': 'application/json' },
94
- body: JSON.stringify({ markdown }),
95
- });
80
+ const data = await requestJson<{ html?: string }>(
81
+ 'renderMarkdown',
82
+ '/api/render',
83
+ {},
84
+ {
85
+ method: 'POST',
86
+ headers: { 'Content-Type': 'application/json' },
87
+ body: JSON.stringify({ markdown }),
88
+ },
89
+ );
96
90
  return data.html ?? '';
97
91
  }
98
92
 
@@ -105,15 +99,17 @@ export async function fetchFile(path: string): Promise<{
105
99
  }
106
100
 
107
101
  /** Save file content to the server. */
108
- export async function saveFile(
109
- path: string,
110
- content: string,
111
- ): Promise<{ ok: boolean; updatedAt?: string }> {
112
- return requestJson('saveFile', '/api/file/save', { ok: false }, {
113
- method: 'POST',
114
- headers: { 'Content-Type': 'application/json' },
115
- body: JSON.stringify({ path, content }),
116
- });
102
+ export async function saveFile(path: string, content: string): Promise<{ ok: boolean; updatedAt?: string }> {
103
+ return requestJson(
104
+ 'saveFile',
105
+ '/api/file/save',
106
+ { ok: false },
107
+ {
108
+ method: 'POST',
109
+ headers: { 'Content-Type': 'application/json' },
110
+ body: JSON.stringify({ path, content }),
111
+ },
112
+ );
117
113
  }
118
114
 
119
115
  /** Fetch current workbench state. */
@@ -136,11 +132,16 @@ export async function fetchCanvasState(): Promise<Record<string, unknown>> {
136
132
  }
137
133
 
138
134
  export async function saveCanvasTheme(theme: string): Promise<{ ok: boolean; theme?: string }> {
139
- return requestJson('saveCanvasTheme', '/api/canvas/theme', { ok: false }, {
140
- method: 'POST',
141
- headers: { 'Content-Type': 'application/json' },
142
- body: JSON.stringify({ theme }),
143
- });
135
+ return requestJson(
136
+ 'saveCanvasTheme',
137
+ '/api/canvas/theme',
138
+ { ok: false },
139
+ {
140
+ method: 'POST',
141
+ headers: { 'Content-Type': 'application/json' },
142
+ body: JSON.stringify({ theme }),
143
+ },
144
+ );
144
145
  }
145
146
 
146
147
  /** Fetch available slash commands for prompt completion. */
@@ -157,17 +158,22 @@ export async function submitCanvasPrompt(
157
158
  threadNodeId?: string,
158
159
  ): Promise<{ ok: boolean; nodeId?: string; error?: string }> {
159
160
  if (!text.trim()) return { ok: false, error: 'Prompt text is required' };
160
- return requestJson('submitCanvasPrompt', '/api/canvas/prompt', { ok: false, error: 'Network error' }, {
161
- method: 'POST',
162
- headers: { 'Content-Type': 'application/json' },
163
- body: JSON.stringify({
164
- text,
165
- ...(position ? { position } : {}),
166
- ...(parentNodeId ? { parentNodeId } : {}),
167
- ...(contextNodeIds && contextNodeIds.length > 0 ? { contextNodeIds } : {}),
168
- ...(threadNodeId ? { threadNodeId } : {}),
169
- }),
170
- });
161
+ return requestJson(
162
+ 'submitCanvasPrompt',
163
+ '/api/canvas/prompt',
164
+ { ok: false, error: 'Network error' },
165
+ {
166
+ method: 'POST',
167
+ headers: { 'Content-Type': 'application/json' },
168
+ body: JSON.stringify({
169
+ text,
170
+ ...(position ? { position } : {}),
171
+ ...(parentNodeId ? { parentNodeId } : {}),
172
+ ...(contextNodeIds && contextNodeIds.length > 0 ? { contextNodeIds } : {}),
173
+ ...(threadNodeId ? { threadNodeId } : {}),
174
+ }),
175
+ },
176
+ );
171
177
  }
172
178
 
173
179
  /** Submit a reply into an existing prompt thread. */
@@ -203,11 +209,16 @@ export async function createEdgeFromClient(
203
209
  type: string,
204
210
  label?: string,
205
211
  ): Promise<{ ok: boolean; id?: string }> {
206
- return requestJson('createEdgeFromClient', '/api/canvas/edge', { ok: false }, {
207
- method: 'POST',
208
- headers: { 'Content-Type': 'application/json' },
209
- body: JSON.stringify({ from, to, type, label }),
210
- });
212
+ return requestJson(
213
+ 'createEdgeFromClient',
214
+ '/api/canvas/edge',
215
+ { ok: false },
216
+ {
217
+ method: 'POST',
218
+ headers: { 'Content-Type': 'application/json' },
219
+ body: JSON.stringify({ from, to, type, label }),
220
+ },
221
+ );
211
222
  }
212
223
 
213
224
  /** Create a canvas node via the server. Returns the new node ID. */
@@ -220,11 +231,16 @@ export async function createNodeFromClient(opts: {
220
231
  width?: number;
221
232
  height?: number;
222
233
  }): Promise<{ ok: boolean; id?: string }> {
223
- return requestJson('createNodeFromClient', '/api/canvas/node', { ok: false }, {
224
- method: 'POST',
225
- headers: { 'Content-Type': 'application/json' },
226
- body: JSON.stringify(opts),
227
- });
234
+ return requestJson(
235
+ 'createNodeFromClient',
236
+ '/api/canvas/node',
237
+ { ok: false },
238
+ {
239
+ method: 'POST',
240
+ headers: { 'Content-Type': 'application/json' },
241
+ body: JSON.stringify(opts),
242
+ },
243
+ );
228
244
  }
229
245
 
230
246
  /** Update a canvas node via the server. */
@@ -241,11 +257,16 @@ export async function updateNodeFromClient(
241
257
  data?: Record<string, unknown>;
242
258
  },
243
259
  ): Promise<{ ok: boolean; id?: string }> {
244
- return requestJson('updateNodeFromClient', `/api/canvas/node/${encodeURIComponent(id)}`, { ok: false }, {
245
- method: 'PATCH',
246
- headers: { 'Content-Type': 'application/json' },
247
- body: JSON.stringify(patch),
248
- });
260
+ return requestJson(
261
+ 'updateNodeFromClient',
262
+ `/api/canvas/node/${encodeURIComponent(id)}`,
263
+ { ok: false },
264
+ {
265
+ method: 'PATCH',
266
+ headers: { 'Content-Type': 'application/json' },
267
+ body: JSON.stringify(patch),
268
+ },
269
+ );
249
270
  }
250
271
 
251
272
  /** Refresh a webpage node from its persisted URL on the server. */
@@ -253,18 +274,28 @@ export async function refreshWebpageNodeFromClient(
253
274
  id: string,
254
275
  url?: string,
255
276
  ): Promise<{ ok: boolean; id?: string; error?: string }> {
256
- return requestJson('refreshWebpageNodeFromClient', `/api/canvas/node/${encodeURIComponent(id)}/refresh`, { ok: false }, {
257
- method: 'POST',
258
- headers: { 'Content-Type': 'application/json' },
259
- body: JSON.stringify(url ? { url } : {}),
260
- });
277
+ return requestJson(
278
+ 'refreshWebpageNodeFromClient',
279
+ `/api/canvas/node/${encodeURIComponent(id)}/refresh`,
280
+ { ok: false },
281
+ {
282
+ method: 'POST',
283
+ headers: { 'Content-Type': 'application/json' },
284
+ body: JSON.stringify(url ? { url } : {}),
285
+ },
286
+ );
261
287
  }
262
288
 
263
289
  /** Remove a canvas node via the server. */
264
290
  export async function removeNodeFromClient(id: string): Promise<{ ok: boolean; removed?: string }> {
265
- return requestJson('removeNodeFromClient', `/api/canvas/node/${encodeURIComponent(id)}`, { ok: false }, {
266
- method: 'DELETE',
267
- });
291
+ return requestJson(
292
+ 'removeNodeFromClient',
293
+ `/api/canvas/node/${encodeURIComponent(id)}`,
294
+ { ok: false },
295
+ {
296
+ method: 'DELETE',
297
+ },
298
+ );
268
299
  }
269
300
 
270
301
  // ── PMX-AX node interactions ──────────────────────────────────
@@ -292,7 +323,10 @@ export async function fetchAxSurfaceState(): Promise<unknown> {
292
323
  }
293
324
 
294
325
  /** Ask the server to open a node's surface in the system browser. */
295
- export async function openNodeInSystemBrowserRequest(nodeId: string, url?: string): Promise<{ ok: boolean; opened: boolean }> {
326
+ export async function openNodeInSystemBrowserRequest(
327
+ nodeId: string,
328
+ url?: string,
329
+ ): Promise<{ ok: boolean; opened: boolean }> {
296
330
  return requestJson<{ ok: boolean; opened: boolean }>(
297
331
  'openNodeInSystemBrowserRequest',
298
332
  '/api/canvas/open-external',
@@ -324,14 +358,19 @@ export async function updateViewportFromClient(
324
358
  viewport: { x: number; y: number; scale: number },
325
359
  options: { recordHistory?: boolean } = {},
326
360
  ): Promise<{ ok: boolean }> {
327
- return requestJson('updateViewportFromClient', '/api/canvas/viewport', { ok: false }, {
328
- method: 'POST',
329
- headers: { 'Content-Type': 'application/json' },
330
- body: JSON.stringify({
331
- ...viewport,
332
- ...(options.recordHistory === false ? { recordHistory: false } : {}),
333
- }),
334
- });
361
+ return requestJson(
362
+ 'updateViewportFromClient',
363
+ '/api/canvas/viewport',
364
+ { ok: false },
365
+ {
366
+ method: 'POST',
367
+ headers: { 'Content-Type': 'application/json' },
368
+ body: JSON.stringify({
369
+ ...viewport,
370
+ ...(options.recordHistory === false ? { recordHistory: false } : {}),
371
+ }),
372
+ },
373
+ );
335
374
  }
336
375
 
337
376
  // ── Group API ─────────────────────────────────────────────────
@@ -346,29 +385,44 @@ export async function createGroupFromClient(opts: {
346
385
  width?: number;
347
386
  height?: number;
348
387
  }): Promise<{ ok: boolean; id?: string }> {
349
- return requestJson('createGroupFromClient', '/api/canvas/group', { ok: false }, {
350
- method: 'POST',
351
- headers: { 'Content-Type': 'application/json' },
352
- body: JSON.stringify(opts),
353
- });
388
+ return requestJson(
389
+ 'createGroupFromClient',
390
+ '/api/canvas/group',
391
+ { ok: false },
392
+ {
393
+ method: 'POST',
394
+ headers: { 'Content-Type': 'application/json' },
395
+ body: JSON.stringify(opts),
396
+ },
397
+ );
354
398
  }
355
399
 
356
400
  /** Add nodes to an existing group. */
357
401
  export async function addToGroupFromClient(groupId: string, childIds: string[]): Promise<{ ok: boolean }> {
358
- return requestJson('addToGroupFromClient', '/api/canvas/group/add', { ok: false }, {
359
- method: 'POST',
360
- headers: { 'Content-Type': 'application/json' },
361
- body: JSON.stringify({ groupId, childIds }),
362
- });
402
+ return requestJson(
403
+ 'addToGroupFromClient',
404
+ '/api/canvas/group/add',
405
+ { ok: false },
406
+ {
407
+ method: 'POST',
408
+ headers: { 'Content-Type': 'application/json' },
409
+ body: JSON.stringify({ groupId, childIds }),
410
+ },
411
+ );
363
412
  }
364
413
 
365
414
  /** Ungroup all children from a group. */
366
415
  export async function ungroupFromClient(groupId: string): Promise<{ ok: boolean }> {
367
- return requestJson('ungroupFromClient', '/api/canvas/group/ungroup', { ok: false }, {
368
- method: 'POST',
369
- headers: { 'Content-Type': 'application/json' },
370
- body: JSON.stringify({ groupId }),
371
- });
416
+ return requestJson(
417
+ 'ungroupFromClient',
418
+ '/api/canvas/group/ungroup',
419
+ { ok: false },
420
+ {
421
+ method: 'POST',
422
+ headers: { 'Content-Type': 'application/json' },
423
+ body: JSON.stringify({ groupId }),
424
+ },
425
+ );
372
426
  }
373
427
 
374
428
  // ── Snapshot API ──────────────────────────────────────────────
@@ -386,11 +440,16 @@ export async function listSnapshots(): Promise<CanvasSnapshotInfo[]> {
386
440
  }
387
441
 
388
442
  export async function saveSnapshot(name: string): Promise<{ ok: boolean; snapshot?: CanvasSnapshotInfo }> {
389
- return requestJson('saveSnapshot', '/api/canvas/snapshots', { ok: false }, {
390
- method: 'POST',
391
- headers: { 'Content-Type': 'application/json' },
392
- body: JSON.stringify({ name }),
393
- });
443
+ return requestJson(
444
+ 'saveSnapshot',
445
+ '/api/canvas/snapshots',
446
+ { ok: false },
447
+ {
448
+ method: 'POST',
449
+ headers: { 'Content-Type': 'application/json' },
450
+ body: JSON.stringify({ name }),
451
+ },
452
+ );
394
453
  }
395
454
 
396
455
  export async function restoreSnapshot(id: string): Promise<{ ok: boolean }> {
@@ -403,9 +462,14 @@ export async function deleteSnapshot(id: string): Promise<{ ok: boolean }> {
403
462
 
404
463
  /** Remove a canvas edge via the server. */
405
464
  export async function removeEdgeFromClient(edgeId: string): Promise<{ ok: boolean }> {
406
- return requestJson('removeEdgeFromClient', '/api/canvas/edge', { ok: false }, {
407
- method: 'DELETE',
408
- headers: { 'Content-Type': 'application/json' },
409
- body: JSON.stringify({ edge_id: edgeId }),
410
- });
465
+ return requestJson(
466
+ 'removeEdgeFromClient',
467
+ '/api/canvas/edge',
468
+ { ok: false },
469
+ {
470
+ method: 'DELETE',
471
+ headers: { 'Content-Type': 'application/json' },
472
+ body: JSON.stringify({ edge_id: edgeId }),
473
+ },
474
+ );
411
475
  }
@@ -63,7 +63,10 @@ function setPhase(id: string, phase: IntentPhase, ms: number, settledNodeId?: st
63
63
  next.set(id, { ...current, phase, ...(settledNodeId ? { settledNodeId } : {}) });
64
64
  writeIntents(next);
65
65
  clearExitTimer(id);
66
- exitTimers.set(id, setTimeout(() => removeIntent(id), ms));
66
+ exitTimers.set(
67
+ id,
68
+ setTimeout(() => removeIntent(id), ms),
69
+ );
67
70
  }
68
71
 
69
72
  /** Resolve a ghost into a real node — the settle morph, then removal. */
@@ -72,10 +72,8 @@ function applyLayoutOverrides(node: CanvasNodeState): CanvasNodeState {
72
72
  }
73
73
 
74
74
  // ── Default positions by type ─────────────────────────────────
75
- const DEFAULT_POSITIONS: Record<
76
- CanvasNodeState['type'],
77
- { x: number; y: number; w: number; h: number }
78
- > & Record<'prompt' | 'response', { x: number; y: number; w: number; h: number }> = {
75
+ const DEFAULT_POSITIONS: Record<CanvasNodeState['type'], { x: number; y: number; w: number; h: number }> &
76
+ Record<'prompt' | 'response', { x: number; y: number; w: number; h: number }> = {
79
77
  status: { x: 40, y: 80, w: 300, h: 120 },
80
78
  markdown: { x: 380, y: 80, w: 720, h: 600 },
81
79
  context: { x: 1130, y: 80, w: 320, h: 400 },
@@ -114,11 +112,7 @@ function makeNode(
114
112
  }
115
113
 
116
114
  function getMarkdownPlacement(): { x: number; y: number } {
117
- return findOpenCanvasPosition(
118
- [...nodes.value.values()],
119
- DEFAULT_POSITIONS.markdown.w,
120
- DEFAULT_POSITIONS.markdown.h,
121
- );
115
+ return findOpenCanvasPosition([...nodes.value.values()], DEFAULT_POSITIONS.markdown.w, DEFAULT_POSITIONS.markdown.h);
122
116
  }
123
117
 
124
118
  // ── Node ensure helpers ───────────────────────────────────────
@@ -243,11 +237,7 @@ function findExtAppNodeId(toolCallId: string): string | null {
243
237
  const legacyDirectId = `ext-app-${toolCallId}`;
244
238
  if (legacyDirectId !== directId && nodes.value.has(legacyDirectId)) return legacyDirectId;
245
239
  for (const [nodeId, node] of nodes.value.entries()) {
246
- if (
247
- node.type === 'mcp-app' &&
248
- node.data.mode === 'ext-app' &&
249
- node.data.toolCallId === toolCallId
250
- ) {
240
+ if (node.type === 'mcp-app' && node.data.mode === 'ext-app' && node.data.toolCallId === toolCallId) {
251
241
  return nodeId;
252
242
  }
253
243
  }
@@ -301,28 +291,27 @@ function applyCanvasTheme(theme: string): void {
301
291
  }
302
292
 
303
293
  function isCanvasNodeType(value: unknown): value is CanvasNodeState['type'] {
304
- return value === 'markdown'
305
- || value === 'mcp-app'
306
- || value === 'webpage'
307
- || value === 'json-render'
308
- || value === 'graph'
309
- || value === 'prompt'
310
- || value === 'response'
311
- || value === 'status'
312
- || value === 'context'
313
- || value === 'ledger'
314
- || value === 'trace'
315
- || value === 'file'
316
- || value === 'image'
317
- || value === 'html'
318
- || value === 'group';
294
+ return (
295
+ value === 'markdown' ||
296
+ value === 'mcp-app' ||
297
+ value === 'webpage' ||
298
+ value === 'json-render' ||
299
+ value === 'graph' ||
300
+ value === 'prompt' ||
301
+ value === 'response' ||
302
+ value === 'status' ||
303
+ value === 'context' ||
304
+ value === 'ledger' ||
305
+ value === 'trace' ||
306
+ value === 'file' ||
307
+ value === 'image' ||
308
+ value === 'html' ||
309
+ value === 'group'
310
+ );
319
311
  }
320
312
 
321
313
  function isCanvasEdgeType(value: unknown): value is CanvasEdge['type'] {
322
- return value === 'relation'
323
- || value === 'depends-on'
324
- || value === 'flow'
325
- || value === 'references';
314
+ return value === 'relation' || value === 'depends-on' || value === 'flow' || value === 'references';
326
315
  }
327
316
 
328
317
  function parseCanvasPosition(value: unknown): { x: number; y: number } | null {
@@ -353,10 +342,8 @@ function parseCanvasNode(raw: Record<string, unknown>): CanvasNodeState | null {
353
342
  const size = parseCanvasSize(raw.size);
354
343
  if (!position || !size) return null;
355
344
 
356
- const dockPosition =
357
- raw.dockPosition === 'left' || raw.dockPosition === 'right' ? raw.dockPosition : null;
358
- const data =
359
- raw.data && typeof raw.data === 'object' ? Object.fromEntries(Object.entries(raw.data)) : {};
345
+ const dockPosition = raw.dockPosition === 'left' || raw.dockPosition === 'right' ? raw.dockPosition : null;
346
+ const data = raw.data && typeof raw.data === 'object' ? Object.fromEntries(Object.entries(raw.data)) : {};
360
347
 
361
348
  return {
362
349
  id: raw.id,
@@ -383,9 +370,7 @@ function parseCanvasEdge(raw: Record<string, unknown>): CanvasEdge | null {
383
370
  to: raw.to,
384
371
  type: raw.type,
385
372
  ...(typeof raw.label === 'string' ? { label: raw.label } : {}),
386
- ...(raw.style === 'solid' || raw.style === 'dashed' || raw.style === 'dotted'
387
- ? { style: raw.style }
388
- : {}),
373
+ ...(raw.style === 'solid' || raw.style === 'dashed' || raw.style === 'dotted' ? { style: raw.style } : {}),
389
374
  ...(raw.animated === true ? { animated: true } : {}),
390
375
  };
391
376
  }
@@ -429,8 +414,7 @@ function handleWorkbenchOpen(data: Record<string, unknown>): void {
429
414
  // H6: Guard — path must be a string for node ID stability
430
415
  if (typeof data.path !== 'string' || !data.path) return;
431
416
  const path = data.path;
432
- const title =
433
- (typeof data.title === 'string' ? data.title : '') || path.split('/').pop() || 'Untitled';
417
+ const title = (typeof data.title === 'string' ? data.title : '') || path.split('/').pop() || 'Untitled';
434
418
 
435
419
  ensureMarkdownNode(path, title);
436
420
  if (data.ledgerSummary) {
@@ -515,9 +499,7 @@ function handleAuxClose(data: Record<string, unknown>): void {
515
499
  } else {
516
500
  const existing = nodes.value.get(id);
517
501
  if (!existing) return;
518
- const auxTabs = ((existing.data.auxTabs as Array<Record<string, unknown>>) ?? []).filter(
519
- (t) => t.id !== data.id,
520
- );
502
+ const auxTabs = ((existing.data.auxTabs as Array<Record<string, unknown>>) ?? []).filter((t) => t.id !== data.id);
521
503
  updateNodeData(id, { auxTabs });
522
504
  }
523
505
  }
@@ -587,8 +569,7 @@ function handleExtAppOpen(data: Record<string, unknown>): void {
587
569
 
588
570
  function handleExtAppUpdate(data: Record<string, unknown>): void {
589
571
  if (typeof data.toolCallId !== 'string' || !data.toolCallId) return;
590
- const id =
591
- findExtAppEventNodeId(data) ?? findOnlyPendingExtAppNodeId(data.serverName, data.toolName);
572
+ const id = findExtAppEventNodeId(data) ?? findOnlyPendingExtAppNodeId(data.serverName, data.toolName);
592
573
  if (!id) return;
593
574
  if (nodes.value.has(id)) {
594
575
  updateNodeData(id, { html: data.html });
@@ -597,8 +578,7 @@ function handleExtAppUpdate(data: Record<string, unknown>): void {
597
578
 
598
579
  function handleExtAppResult(data: Record<string, unknown>): void {
599
580
  if (typeof data.toolCallId !== 'string' || !data.toolCallId) return;
600
- const id =
601
- findExtAppEventNodeId(data) ?? findOnlyPendingExtAppNodeId(data.serverName, data.toolName);
581
+ const id = findExtAppEventNodeId(data) ?? findOnlyPendingExtAppNodeId(data.serverName, data.toolName);
602
582
  if (!id) return;
603
583
  if (nodes.value.has(id)) {
604
584
  if (data.success === false) {
@@ -611,8 +591,7 @@ function handleExtAppResult(data: Record<string, unknown>): void {
611
591
  success: typeof data.success === 'boolean' ? data.success : undefined,
612
592
  error: typeof data.error === 'string' ? data.error : undefined,
613
593
  content: typeof data.content === 'string' ? data.content : undefined,
614
- detailedContent:
615
- typeof data.detailedContent === 'string' ? data.detailedContent : undefined,
594
+ detailedContent: typeof data.detailedContent === 'string' ? data.detailedContent : undefined,
616
595
  }),
617
596
  });
618
597
  }
@@ -843,14 +822,14 @@ function handleCanvasLayoutUpdate(data: Record<string, unknown>): void {
843
822
  const shouldApplyViewport = !hasInitialServerLayout.value;
844
823
  hasInitialServerLayout.value = true;
845
824
 
846
- const serverNodes = layout.nodes
847
- .map(parseCanvasNode)
848
- .filter((node): node is CanvasNodeState => node !== null);
825
+ const serverNodes = layout.nodes.map(parseCanvasNode).filter((node): node is CanvasNodeState => node !== null);
849
826
  const serverEdges = Array.isArray(layout.edges)
850
827
  ? layout.edges.map(parseCanvasEdge).filter((edge): edge is CanvasEdge => edge !== null)
851
828
  : Array.from(edges.value.values());
852
829
  const serverAnnotations = Array.isArray(layout.annotations)
853
- ? layout.annotations.map(parseCanvasAnnotation).filter((annotation): annotation is CanvasAnnotation => annotation !== null)
830
+ ? layout.annotations
831
+ .map(parseCanvasAnnotation)
832
+ .filter((annotation): annotation is CanvasAnnotation => annotation !== null)
854
833
  : undefined;
855
834
  const nextViewport = layout.viewport
856
835
  ? {
@@ -861,12 +840,15 @@ function handleCanvasLayoutUpdate(data: Record<string, unknown>): void {
861
840
  : undefined;
862
841
 
863
842
  cancelViewportAnimation();
864
- applyServerCanvasLayout({
865
- ...(nextViewport ? { viewport: nextViewport } : {}),
866
- nodes: serverNodes,
867
- edges: serverEdges,
868
- ...(serverAnnotations ? { annotations: serverAnnotations } : {}),
869
- }, { applyViewport: shouldApplyViewport });
843
+ applyServerCanvasLayout(
844
+ {
845
+ ...(nextViewport ? { viewport: nextViewport } : {}),
846
+ nodes: serverNodes,
847
+ edges: serverEdges,
848
+ ...(serverAnnotations ? { annotations: serverAnnotations } : {}),
849
+ },
850
+ { applyViewport: shouldApplyViewport },
851
+ );
870
852
 
871
853
  syncAttentionFromSse({ event: 'canvas-layout-update', data });
872
854
  }
@@ -923,9 +905,7 @@ function handleThemeChanged(data: Record<string, unknown>): void {
923
905
  }
924
906
 
925
907
  function handleContextPinsChanged(data: Record<string, unknown>): void {
926
- const nodeIds = Array.isArray(data.nodeIds)
927
- ? data.nodeIds.filter((id): id is string => typeof id === 'string')
928
- : [];
908
+ const nodeIds = Array.isArray(data.nodeIds) ? data.nodeIds.filter((id): id is string => typeof id === 'string') : [];
929
909
  replaceContextPinsFromServer(nodeIds);
930
910
  syncAttentionFromSse({ event: 'context-pins-changed', data });
931
911
  }
@@ -939,7 +919,9 @@ function handleAxStateChanged(): void {
939
919
  if (axRefreshTimer) clearTimeout(axRefreshTimer);
940
920
  axRefreshTimer = setTimeout(() => {
941
921
  axRefreshTimer = null;
942
- void fetchAxSurfaceState().then((state) => { axSurfaceState.value = state; });
922
+ void fetchAxSurfaceState().then((state) => {
923
+ axSurfaceState.value = state;
924
+ });
943
925
  }, 150);
944
926
  }
945
927
 
@@ -951,11 +933,12 @@ function handleAxIntent(data: Record<string, unknown>): void {
951
933
  // be pruned if its `clear` frame were dropped. The server always sets it, so this
952
934
  // only rejects a malformed frame — keeping the backstop's guarantee real.
953
935
  if (
954
- !intent
955
- || typeof intent.id !== 'string'
956
- || typeof intent.kind !== 'string'
957
- || typeof intent.expiresAt !== 'number'
958
- ) return;
936
+ !intent ||
937
+ typeof intent.id !== 'string' ||
938
+ typeof intent.kind !== 'string' ||
939
+ typeof intent.expiresAt !== 'number'
940
+ )
941
+ return;
959
942
  upsertIntent(intent);
960
943
  }
961
944