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
@@ -1,10 +1,7 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { canvasState } from './canvas-state.js';
3
3
  import type { CanvasAnnotation, CanvasLayout, CanvasNodeState, ViewportState } from './canvas-state.js';
4
- import {
5
- normalizeCanvasNodeData,
6
- type CanvasNodeProvenance,
7
- } from './canvas-provenance.js';
4
+ import { normalizeCanvasNodeData, type CanvasNodeProvenance } from './canvas-provenance.js';
8
5
  import { getCanvasNodeKind as getSharedCanvasNodeKind } from '../shared/canvas-node-kind.js';
9
6
  import { canOpenNodeAsSurface } from '../shared/surface.js';
10
7
 
@@ -79,34 +76,32 @@ export function getCanvasNodeKind(node: CanvasNodeState, data: Record<string, un
79
76
  }
80
77
 
81
78
  export function getCanvasNodeTitle(node: CanvasNodeState): string | null {
82
- return pickString(node.data.title)
83
- ?? (node.type === 'webpage' ? pickString(node.data.pageTitle) : null)
84
- ?? null;
79
+ return pickString(node.data.title) ?? (node.type === 'webpage' ? pickString(node.data.pageTitle) : null) ?? null;
85
80
  }
86
81
 
87
82
  export function getCanvasNodeContent(node: CanvasNodeState): string | null {
88
83
  if (node.type === 'html') {
89
84
  const primitive = typeof node.data.htmlPrimitive === 'string' ? node.data.htmlPrimitive : null;
90
85
  const description = pickString(node.data.description);
91
- return pickString(node.data.agentSummary)
92
- ?? pickString(node.data.contentSummary)
93
- ?? (primitive
94
- ? (description ? `${primitive}: ${description}` : primitive)
95
- : null);
86
+ return (
87
+ pickString(node.data.agentSummary) ??
88
+ pickString(node.data.contentSummary) ??
89
+ (primitive ? (description ? `${primitive}: ${description}` : primitive) : null)
90
+ );
96
91
  }
97
- return pickString(node.data.content)
98
- ?? pickString(node.data.fileContent)
99
- ?? pickString(node.data.text)
100
- ?? (node.type === 'file' ? pickString(node.data.path) : null)
101
- ?? (node.type === 'image' ? pickString(node.data.src) : null)
102
- ?? (node.type === 'webpage' ? pickString(node.data.url) : null)
103
- ?? null;
92
+ return (
93
+ pickString(node.data.content) ??
94
+ pickString(node.data.fileContent) ??
95
+ pickString(node.data.text) ??
96
+ (node.type === 'file' ? pickString(node.data.path) : null) ??
97
+ (node.type === 'image' ? pickString(node.data.src) : null) ??
98
+ (node.type === 'webpage' ? pickString(node.data.url) : null) ??
99
+ null
100
+ );
104
101
  }
105
102
 
106
103
  export function getCanvasNodeSurfaceUrl(node: CanvasNodeState, data: Record<string, unknown>): string | null {
107
- return canOpenNodeAsSurface(node.type, data)
108
- ? `/api/canvas/surface/${encodeURIComponent(node.id)}`
109
- : null;
104
+ return canOpenNodeAsSurface(node.type, data) ? `/api/canvas/surface/${encodeURIComponent(node.id)}` : null;
110
105
  }
111
106
 
112
107
  export function serializeCanvasNode(node: CanvasNodeState): SerializedCanvasNode {
@@ -236,22 +231,21 @@ function rectsOverlap(
236
231
  a: { x: number; y: number; width: number; height: number },
237
232
  b: { x: number; y: number; width: number; height: number },
238
233
  ): boolean {
239
- return a.x <= b.x + b.width &&
240
- a.x + a.width >= b.x &&
241
- a.y <= b.y + b.height &&
242
- a.y + a.height >= b.y;
234
+ return a.x <= b.x + b.width && a.x + a.width >= b.x && a.y <= b.y + b.height && a.y + a.height >= b.y;
243
235
  }
244
236
 
245
237
  export function summarizeCanvasAnnotationForContext(
246
238
  annotation: CanvasAnnotation,
247
239
  nodes: CanvasNodeState[],
248
240
  ): CanvasAnnotationContextSummary {
249
- const targetNodes = nodes.filter((node) => rectsOverlap(annotation.bounds, {
250
- x: node.position.x,
251
- y: node.position.y,
252
- width: node.size.width,
253
- height: node.size.height,
254
- }));
241
+ const targetNodes = nodes.filter((node) =>
242
+ rectsOverlap(annotation.bounds, {
243
+ x: node.position.x,
244
+ y: node.position.y,
245
+ width: node.size.width,
246
+ height: node.size.height,
247
+ }),
248
+ );
255
249
  const targetNodeTitles = targetNodes.map((node) => getCanvasNodeTitle(node) ?? node.id);
256
250
  return {
257
251
  id: annotation.id,
@@ -284,9 +278,7 @@ export function buildCanvasSummary(): CanvasSummary {
284
278
  typeCounts[kind] = (typeCounts[kind] ?? 0) + 1;
285
279
  }
286
280
 
287
- const pinnedTitles = layout.nodes
288
- .filter((n) => pinnedIds.has(n.id))
289
- .map((n) => getCanvasNodeTitle(n) ?? n.id);
281
+ const pinnedTitles = layout.nodes.filter((n) => pinnedIds.has(n.id)).map((n) => getCanvasNodeTitle(n) ?? n.id);
290
282
 
291
283
  return {
292
284
  totalNodes: layout.nodes.length,
@@ -11,7 +11,16 @@
11
11
  */
12
12
 
13
13
  import { createHash } from 'node:crypto';
14
- import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, renameSync, rmSync, unlinkSync } from 'node:fs';
14
+ import {
15
+ existsSync,
16
+ readFileSync,
17
+ writeFileSync,
18
+ mkdirSync,
19
+ readdirSync,
20
+ renameSync,
21
+ rmSync,
22
+ unlinkSync,
23
+ } from 'node:fs';
15
24
  import { isAbsolute, join, dirname, relative } from 'node:path';
16
25
  import { gzipSync, gunzipSync } from 'node:zlib';
17
26
  import { normalizeCanvasNodeData } from './canvas-provenance.js';
@@ -123,9 +132,15 @@ interface LoadFromDiskOptions {
123
132
  }
124
133
 
125
134
  export const IMAGE_MIME_MAP: Record<string, string> = {
126
- png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg',
127
- gif: 'image/gif', svg: 'image/svg+xml', webp: 'image/webp',
128
- bmp: 'image/bmp', ico: 'image/x-icon', avif: 'image/avif',
135
+ png: 'image/png',
136
+ jpg: 'image/jpeg',
137
+ jpeg: 'image/jpeg',
138
+ gif: 'image/gif',
139
+ svg: 'image/svg+xml',
140
+ webp: 'image/webp',
141
+ bmp: 'image/bmp',
142
+ ico: 'image/x-icon',
143
+ avif: 'image/avif',
129
144
  };
130
145
 
131
146
  export interface CanvasSnapshot {
@@ -235,7 +250,34 @@ export interface CanvasNodeUpdate {
235
250
  export type CanvasChangeType = 'pins' | 'nodes' | 'ax' | 'ax-timeline';
236
251
 
237
252
  export interface MutationRecordInfo {
238
- operationType: 'addNode' | 'updateNode' | 'removeNode' | 'addEdge' | 'removeEdge' | 'addAnnotation' | 'removeAnnotation' | 'clear' | 'restoreSnapshot' | 'setPins' | 'setAxFocus' | 'addWorkItem' | 'updateWorkItem' | 'requestApproval' | 'resolveApproval' | 'addReviewAnnotation' | 'updateReviewAnnotation' | 'requestElicitation' | 'respondElicitation' | 'requestMode' | 'resolveModeRequest' | 'setPolicy' | 'arrange' | 'batch' | 'groupNodes' | 'ungroupNodes' | 'viewport';
253
+ operationType:
254
+ | 'addNode'
255
+ | 'updateNode'
256
+ | 'removeNode'
257
+ | 'addEdge'
258
+ | 'removeEdge'
259
+ | 'addAnnotation'
260
+ | 'removeAnnotation'
261
+ | 'clear'
262
+ | 'restoreSnapshot'
263
+ | 'setPins'
264
+ | 'setAxFocus'
265
+ | 'addWorkItem'
266
+ | 'updateWorkItem'
267
+ | 'requestApproval'
268
+ | 'resolveApproval'
269
+ | 'addReviewAnnotation'
270
+ | 'updateReviewAnnotation'
271
+ | 'requestElicitation'
272
+ | 'respondElicitation'
273
+ | 'requestMode'
274
+ | 'resolveModeRequest'
275
+ | 'setPolicy'
276
+ | 'arrange'
277
+ | 'batch'
278
+ | 'groupNodes'
279
+ | 'ungroupNodes'
280
+ | 'viewport';
239
281
  description: string;
240
282
  forward: () => void;
241
283
  inverse: () => void;
@@ -287,13 +329,15 @@ function replaceById<T extends { id: string }>(list: T[], item: T): T[] {
287
329
  }
288
330
 
289
331
  function isPersistedBlobRef(value: unknown): value is PersistedBlobRef {
290
- return isRecord(value) &&
332
+ return (
333
+ isRecord(value) &&
291
334
  value.__pmxCanvasBlob === 'v1' &&
292
335
  typeof value.path === 'string' &&
293
336
  typeof value.sha256 === 'string' &&
294
337
  value.encoding === 'json+gzip' &&
295
338
  typeof value.bytes === 'number' &&
296
- typeof value.jsonBytes === 'number';
339
+ typeof value.jsonBytes === 'number'
340
+ );
297
341
  }
298
342
 
299
343
  class CanvasStateManager {
@@ -360,7 +404,11 @@ class CanvasStateManager {
360
404
  /** Run a function with mutation recording suppressed (for undo/redo replay and computed edges). */
361
405
  withSuppressedRecording(fn: () => void): void {
362
406
  this._suppressRecordingDepth++;
363
- try { fn(); } finally { this._suppressRecordingDepth--; }
407
+ try {
408
+ fn();
409
+ } finally {
410
+ this._suppressRecordingDepth--;
411
+ }
364
412
  }
365
413
 
366
414
  /** Create a closure that runs with recording suppressed. */
@@ -388,9 +436,8 @@ class CanvasStateManager {
388
436
  bounds: { x: number; y: number; width: number; height: number },
389
437
  existingGroups?: CanvasPlacementRect[],
390
438
  ): void {
391
- const otherGroups = existingGroups ?? Array.from(this.nodes.values()).filter(
392
- (node) => node.id !== groupId && node.type === 'group',
393
- );
439
+ const otherGroups =
440
+ existingGroups ?? Array.from(this.nodes.values()).filter((node) => node.id !== groupId && node.type === 'group');
394
441
  const resolved = resolveGroupCollision(bounds, otherGroups);
395
442
  const deltaX = resolved.x - bounds.x;
396
443
  const deltaY = resolved.y - bounds.y;
@@ -472,7 +519,12 @@ class CanvasStateManager {
472
519
  }
473
520
  }
474
521
 
475
- private translateGroupChildren(groupId: string, deltaX: number, deltaY: number, skipIds: ReadonlySet<string> = new Set()): void {
522
+ private translateGroupChildren(
523
+ groupId: string,
524
+ deltaX: number,
525
+ deltaY: number,
526
+ skipIds: ReadonlySet<string> = new Set(),
527
+ ): void {
476
528
  if (deltaX === 0 && deltaY === 0) return;
477
529
  const snapshot = this.getGroupSnapshot(groupId);
478
530
  if (!snapshot) return;
@@ -509,9 +561,7 @@ class CanvasStateManager {
509
561
  const snapshot = this.getGroupSnapshot(groupId);
510
562
  if (!snapshot || snapshot.children.length === 0) return;
511
563
  if (snapshot.group.data.frameMode === 'manual') {
512
- const sorted = [...snapshot.children].sort(
513
- (a, b) => a.position.y - b.position.y || a.position.x - b.position.x,
514
- );
564
+ const sorted = [...snapshot.children].sort((a, b) => a.position.y - b.position.y || a.position.x - b.position.x);
515
565
  const left = snapshot.group.position.x + GROUP_PAD;
516
566
  const top = snapshot.group.position.y + GROUP_TITLEBAR_HEIGHT + GROUP_PAD;
517
567
  const right = snapshot.group.position.x + snapshot.group.size.width - GROUP_PAD;
@@ -571,6 +621,11 @@ class CanvasStateManager {
571
621
  private _db: import('bun:sqlite').Database | null = null;
572
622
  private _saveTimer: ReturnType<typeof setTimeout> | null = null;
573
623
 
624
+ /** Workspace root backing persistence and workspace-relative path resolution. */
625
+ get workspaceRoot(): string {
626
+ return this._workspaceRoot;
627
+ }
628
+
574
629
  /** Set the workspace root to enable auto-persistence. */
575
630
  setWorkspaceRoot(workspaceRoot: string): void {
576
631
  this.close();
@@ -590,9 +645,10 @@ class CanvasStateManager {
590
645
  }
591
646
 
592
647
  // Keep legacy _stateFilePath for JSON migration detection
593
- this._stateFilePath = stateFileOverride && !stateFileOverride.endsWith('.db')
594
- ? stateFileOverride
595
- : join(workspaceRoot, PMX_CANVAS_DIR, STATE_FILENAME);
648
+ this._stateFilePath =
649
+ stateFileOverride && !stateFileOverride.endsWith('.db')
650
+ ? stateFileOverride
651
+ : join(workspaceRoot, PMX_CANVAS_DIR, STATE_FILENAME);
596
652
 
597
653
  try {
598
654
  this._db = openCanvasDb(dbPath);
@@ -744,9 +800,7 @@ class CanvasStateManager {
744
800
  private externalizePersistedStateBlobs<T extends PersistedCanvasState>(state: T): T {
745
801
  return {
746
802
  ...state,
747
- nodes: Array.isArray(state.nodes)
748
- ? state.nodes.map((node) => this.externalizeNodeDataBlobs(node))
749
- : [],
803
+ nodes: Array.isArray(state.nodes) ? state.nodes.map((node) => this.externalizeNodeDataBlobs(node)) : [],
750
804
  };
751
805
  }
752
806
 
@@ -1155,7 +1209,9 @@ class CanvasStateManager {
1155
1209
  if (!dir || !existsSync(dir)) return [];
1156
1210
 
1157
1211
  try {
1158
- const files = readdirSync(dir).filter((f) => f.endsWith('.json')).sort();
1212
+ const files = readdirSync(dir)
1213
+ .filter((f) => f.endsWith('.json'))
1214
+ .sort();
1159
1215
  const snapshots: CanvasSnapshot[] = [];
1160
1216
  for (const file of files) {
1161
1217
  try {
@@ -1227,7 +1283,9 @@ class CanvasStateManager {
1227
1283
  viewport: structuredClone(resolved.state.viewport),
1228
1284
  nodes: Array.isArray(resolved.state.nodes) ? resolved.state.nodes.map((node) => structuredClone(node)) : [],
1229
1285
  edges: Array.isArray(resolved.state.edges) ? resolved.state.edges.map((edge) => structuredClone(edge)) : [],
1230
- annotations: Array.isArray(resolved.state.annotations) ? resolved.state.annotations.map((annotation) => structuredClone(annotation)) : [],
1286
+ annotations: Array.isArray(resolved.state.annotations)
1287
+ ? resolved.state.annotations.map((annotation) => structuredClone(annotation))
1288
+ : [],
1231
1289
  contextPins: Array.isArray(resolved.state.contextPins) ? [...resolved.state.contextPins] : [],
1232
1290
  ax: resolved.state.ax ? structuredClone(resolved.state.ax) : createEmptyAxState(),
1233
1291
  };
@@ -1268,7 +1326,9 @@ class CanvasStateManager {
1268
1326
  }
1269
1327
 
1270
1328
  /** Read a snapshot's data without restoring it (for diff). Resolves by ID or name. */
1271
- getSnapshotData(idOrName: string): { name: string; nodes: CanvasNodeState[]; edges: CanvasEdge[]; annotations: CanvasAnnotation[] } | null {
1329
+ getSnapshotData(
1330
+ idOrName: string,
1331
+ ): { name: string; nodes: CanvasNodeState[]; edges: CanvasEdge[]; annotations: CanvasAnnotation[] } | null {
1272
1332
  const resolved = this.readResolvedSnapshot(idOrName);
1273
1333
  if (!resolved) return null;
1274
1334
  const state = {
@@ -1281,7 +1341,9 @@ class CanvasStateManager {
1281
1341
  name: resolved.snapshot.name,
1282
1342
  nodes: Array.isArray(state.nodes) ? state.nodes.map((node) => structuredClone(node)) : [],
1283
1343
  edges: Array.isArray(state.edges) ? state.edges.map((edge) => structuredClone(edge)) : [],
1284
- annotations: Array.isArray(state.annotations) ? state.annotations.map((annotation) => structuredClone(annotation)) : [],
1344
+ annotations: Array.isArray(state.annotations)
1345
+ ? state.annotations.map((annotation) => structuredClone(annotation))
1346
+ : [],
1285
1347
  };
1286
1348
  }
1287
1349
 
@@ -1339,9 +1401,10 @@ class CanvasStateManager {
1339
1401
  // (including undock → dockPosition null) are respected (see normalizeNode).
1340
1402
  // Skip during suppressed replay (undo/redo re-add) so a deliberately-undocked
1341
1403
  // context node is restored verbatim instead of being snapped back to the dock.
1342
- const seeded = node.type === 'context' && node.dockPosition == null && this._suppressRecordingDepth === 0
1343
- ? { ...node, dockPosition: 'right' as const, collapsed: true }
1344
- : node;
1404
+ const seeded =
1405
+ node.type === 'context' && node.dockPosition == null && this._suppressRecordingDepth === 0
1406
+ ? { ...node, dockPosition: 'right' as const, collapsed: true }
1407
+ : node;
1345
1408
  const cloned = structuredClone(this.normalizeNode(seeded));
1346
1409
  this.nodes.set(node.id, cloned);
1347
1410
  this.scheduleSave();
@@ -1367,11 +1430,7 @@ class CanvasStateManager {
1367
1430
  if (!existing) return;
1368
1431
  const oldSnapshot = structuredClone(existing);
1369
1432
  if (existing.type === 'group' && patch.position) {
1370
- this.translateGroupChildren(
1371
- id,
1372
- patch.position.x - existing.position.x,
1373
- patch.position.y - existing.position.y,
1374
- );
1433
+ this.translateGroupChildren(id, patch.position.x - existing.position.x, patch.position.y - existing.position.y);
1375
1434
  }
1376
1435
  const nextNode = this.normalizeNode({ ...existing, ...patch });
1377
1436
  this.nodes.set(id, nextNode);
@@ -1390,7 +1449,11 @@ class CanvasStateManager {
1390
1449
  operationType: 'updateNode',
1391
1450
  description: `Updated node "${(existing.data.title as string) ?? id}"`,
1392
1451
  forward: this.suppressed(() => this.updateNode(id, structuredClone(patch))),
1393
- inverse: this.suppressed(() => { this.nodes.set(id, structuredClone(oldSnapshot)); this.scheduleSave(); this.notifyChange('nodes'); }),
1452
+ inverse: this.suppressed(() => {
1453
+ this.nodes.set(id, structuredClone(oldSnapshot));
1454
+ this.scheduleSave();
1455
+ this.notifyChange('nodes');
1456
+ }),
1394
1457
  });
1395
1458
  }
1396
1459
 
@@ -1443,7 +1506,8 @@ class CanvasStateManager {
1443
1506
  // original deletion already recorded the note, so replaying must NOT append a
1444
1507
  // duplicate (the timeline is append-only). `revalidateAfterNodeRemoval` above
1445
1508
  // still runs unconditionally — only the timeline note is gated.
1446
- const affected = orphaned.reanchoredIds.length > 0 || orphaned.removedReviewIds.length > 0 || orphaned.reanchoredFocus;
1509
+ const affected =
1510
+ orphaned.reanchoredIds.length > 0 || orphaned.removedReviewIds.length > 0 || orphaned.reanchoredFocus;
1447
1511
  if (existing && this._suppressRecordingDepth === 0 && affected) {
1448
1512
  const title = (existing.data.title as string) ?? id;
1449
1513
  const focusNote = orphaned.reanchoredFocus ? ' (focus anchor cleared)' : '';
@@ -1606,9 +1670,7 @@ class CanvasStateManager {
1606
1670
  const oldSnapshots = new Map<string, CanvasNodeState>();
1607
1671
  const appliedUpdates: CanvasNodeUpdate[] = [];
1608
1672
  const explicitPositionUpdateIds = new Set(
1609
- updates
1610
- .filter((update) => update.position !== undefined)
1611
- .map((update) => update.id),
1673
+ updates.filter((update) => update.position !== undefined).map((update) => update.id),
1612
1674
  );
1613
1675
 
1614
1676
  for (const update of updates) {
@@ -1618,16 +1680,10 @@ class CanvasStateManager {
1618
1680
  continue;
1619
1681
  }
1620
1682
  const nextPatch: Partial<CanvasNodeState> = {};
1621
- if (
1622
- update.position &&
1623
- (update.position.x !== existing.position.x || update.position.y !== existing.position.y)
1624
- ) {
1683
+ if (update.position && (update.position.x !== existing.position.x || update.position.y !== existing.position.y)) {
1625
1684
  nextPatch.position = update.position;
1626
1685
  }
1627
- if (
1628
- update.size &&
1629
- (update.size.width !== existing.size.width || update.size.height !== existing.size.height)
1630
- ) {
1686
+ if (update.size && (update.size.width !== existing.size.width || update.size.height !== existing.size.height)) {
1631
1687
  nextPatch.size = update.size;
1632
1688
  }
1633
1689
  if (update.collapsed !== undefined && update.collapsed !== existing.collapsed) {
@@ -1650,10 +1706,13 @@ class CanvasStateManager {
1650
1706
  explicitPositionUpdateIds,
1651
1707
  );
1652
1708
  }
1653
- this.nodes.set(update.id, this.normalizeNode({
1654
- ...existing,
1655
- ...nextPatch,
1656
- }));
1709
+ this.nodes.set(
1710
+ update.id,
1711
+ this.normalizeNode({
1712
+ ...existing,
1713
+ ...nextPatch,
1714
+ }),
1715
+ );
1657
1716
  const parentGroupId = existing.data.parentGroup as string | undefined;
1658
1717
  if (parentGroupId) {
1659
1718
  touchedParentGroups.add(parentGroupId);
@@ -1678,7 +1737,10 @@ class CanvasStateManager {
1678
1737
  operationType: 'batch',
1679
1738
  description: formatBatchUpdateDescription(appliedUpdates),
1680
1739
  forward: this.suppressed(() => {
1681
- this.applyUpdates(appliedUpdates.map((update) => structuredClone(update)), options);
1740
+ this.applyUpdates(
1741
+ appliedUpdates.map((update) => structuredClone(update)),
1742
+ options,
1743
+ );
1682
1744
  }),
1683
1745
  inverse: this.suppressed(() => {
1684
1746
  for (const snapshot of inverseSnapshots) {
@@ -1878,7 +1940,11 @@ class CanvasStateManager {
1878
1940
  }
1879
1941
 
1880
1942
  /** Invoke a registry-gated PMX command intent — records a timeline event (no execution). */
1881
- invokeCommand(name: string, args: Record<string, unknown> | null = null, options: { source?: PmxAxSource } = {}): PmxAxEvent | null {
1943
+ invokeCommand(
1944
+ name: string,
1945
+ args: Record<string, unknown> | null = null,
1946
+ options: { source?: PmxAxSource } = {},
1947
+ ): PmxAxEvent | null {
1882
1948
  return this.ax.invokeCommand(name, args, options);
1883
1949
  }
1884
1950
 
@@ -1900,14 +1966,27 @@ class CanvasStateManager {
1900
1966
 
1901
1967
  // ── Timeline (DB-direct; NOT in _axState; NOT history-recorded) ───
1902
1968
  recordAxEvent(
1903
- input: { kind: PmxAxEventKind; summary: string; detail?: string | null; nodeIds?: string[]; data?: Record<string, unknown> | null },
1969
+ input: {
1970
+ kind: PmxAxEventKind;
1971
+ summary: string;
1972
+ detail?: string | null;
1973
+ nodeIds?: string[];
1974
+ data?: Record<string, unknown> | null;
1975
+ },
1904
1976
  options: { source?: PmxAxSource } = {},
1905
1977
  ): PmxAxEvent {
1906
1978
  return this.ax.recordAxEvent(input, options);
1907
1979
  }
1908
1980
 
1909
1981
  addEvidence(
1910
- input: { kind: PmxAxEvidenceKind; title: string; body?: string | null; ref?: string | null; nodeIds?: string[]; data?: Record<string, unknown> | null },
1982
+ input: {
1983
+ kind: PmxAxEvidenceKind;
1984
+ title: string;
1985
+ body?: string | null;
1986
+ ref?: string | null;
1987
+ nodeIds?: string[];
1988
+ data?: Record<string, unknown> | null;
1989
+ },
1911
1990
  options: { source?: PmxAxSource } = {},
1912
1991
  ): PmxAxEvidence {
1913
1992
  return this.ax.addEvidence(input, options);
@@ -1933,11 +2012,23 @@ class CanvasStateManager {
1933
2012
  reactions?: {
1934
2013
  workItem?: false | { status?: PmxAxWorkItemStatus; detail?: string | null };
1935
2014
  evidence?: false | { kind?: PmxAxEvidenceKind; body?: string | null };
1936
- review?: false | { severity?: PmxAxReviewSeverity; kind?: PmxAxReviewKind; anchorType?: PmxAxReviewAnchorType; nodeId?: string | null };
2015
+ review?:
2016
+ | false
2017
+ | {
2018
+ severity?: PmxAxReviewSeverity;
2019
+ kind?: PmxAxReviewKind;
2020
+ anchorType?: PmxAxReviewAnchorType;
2021
+ nodeId?: string | null;
2022
+ };
1937
2023
  };
1938
2024
  },
1939
2025
  options: { source?: PmxAxSource } = {},
1940
- ): { event: PmxAxEvent; workItem: PmxAxWorkItem | null; evidence: PmxAxEvidence | null; review: PmxAxReviewAnnotation | null } {
2026
+ ): {
2027
+ event: PmxAxEvent;
2028
+ workItem: PmxAxWorkItem | null;
2029
+ evidence: PmxAxEvidence | null;
2030
+ review: PmxAxReviewAnnotation | null;
2031
+ } {
1941
2032
  return this.ax.ingestActivity(input, options);
1942
2033
  }
1943
2034
 
@@ -1969,7 +2060,12 @@ class CanvasStateManager {
1969
2060
  return this.ax.getAxTimelineSummary();
1970
2061
  }
1971
2062
 
1972
- getAxTimeline(q: AxTimelineQuery = {}): { events: PmxAxEvent[]; evidence: PmxAxEvidence[]; steering: PmxAxSteeringMessage[]; summary: PmxAxTimelineSummary } {
2063
+ getAxTimeline(q: AxTimelineQuery = {}): {
2064
+ events: PmxAxEvent[];
2065
+ evidence: PmxAxEvidence[];
2066
+ steering: PmxAxSteeringMessage[];
2067
+ summary: PmxAxTimelineSummary;
2068
+ } {
1973
2069
  return this.ax.getAxTimeline(q);
1974
2070
  }
1975
2071
 
@@ -2051,7 +2147,8 @@ class CanvasStateManager {
2051
2147
  const c = this.nodes.get(id);
2052
2148
  if (!c) continue;
2053
2149
  const d = { ...c.data };
2054
- if (oldParent) d.parentGroup = oldParent; else delete d.parentGroup;
2150
+ if (oldParent) d.parentGroup = oldParent;
2151
+ else delete d.parentGroup;
2055
2152
  this.nodes.set(id, { ...c, data: d });
2056
2153
  }
2057
2154
  this.scheduleSave();
@@ -15,15 +15,14 @@ import { createRequire } from 'node:module';
15
15
 
16
16
  const require = createRequire(import.meta.url);
17
17
  const extAppsPackageDir = dirname(require.resolve('@modelcontextprotocol/ext-apps/package.json'));
18
- const extAppsRuntimeSource = readFileSync(
19
- join(extAppsPackageDir, 'dist', 'src', 'app-with-deps.js'),
20
- 'utf-8',
21
- );
18
+ const extAppsRuntimeSource = readFileSync(join(extAppsPackageDir, 'dist', 'src', 'app-with-deps.js'), 'utf-8');
22
19
  const appBindingMatch = extAppsRuntimeSource.match(/([A-Za-z_$][\w$]*) as App/);
23
20
  const transportBindingMatch = extAppsRuntimeSource.match(/([A-Za-z_$][\w$]*) as PostMessageTransport/);
24
21
 
25
22
  if (!appBindingMatch || !transportBindingMatch) {
26
- throw new Error('Failed to locate App or PostMessageTransport export bindings in @modelcontextprotocol/ext-apps runtime');
23
+ throw new Error(
24
+ 'Failed to locate App or PostMessageTransport export bindings in @modelcontextprotocol/ext-apps runtime',
25
+ );
27
26
  }
28
27
 
29
28
  const extAppsBootstrapSource = `${extAppsRuntimeSource}
@@ -113,7 +113,7 @@ function resolveImportPath(specifier: string, fromFilePath: string): string | nu
113
113
  // Check if it looks like a relative project path (contains / and no @)
114
114
  if (!specifier.includes('/') || specifier.startsWith('@')) return null;
115
115
  // For non-relative paths, try workspace-relative resolution
116
- const wsRoot = process.cwd();
116
+ const wsRoot = canvasState.workspaceRoot;
117
117
  return tryResolveWithExtensions(resolve(wsRoot, specifier));
118
118
  }
119
119
 
@@ -123,9 +123,20 @@ function resolveImportPath(specifier: string, fromFilePath: string): string | nu
123
123
  }
124
124
 
125
125
  const RESOLVE_EXTENSIONS = [
126
- '', '.ts', '.tsx', '.js', '.jsx', '.mjs', '.mts',
127
- '/index.ts', '/index.tsx', '/index.js', '/index.jsx',
128
- '.py', '.go', '.rs',
126
+ '',
127
+ '.ts',
128
+ '.tsx',
129
+ '.js',
130
+ '.jsx',
131
+ '.mjs',
132
+ '.mts',
133
+ '/index.ts',
134
+ '/index.tsx',
135
+ '/index.js',
136
+ '/index.jsx',
137
+ '.py',
138
+ '.go',
139
+ '.rs',
129
140
  ];
130
141
 
131
142
  function tryResolveWithExtensions(basePath: string): string | null {
@@ -179,7 +190,7 @@ function getFileNodes(): CanvasNodeState[] {
179
190
  */
180
191
  function buildPathIndex(fileNodes?: CanvasNodeState[]): Map<string, string> {
181
192
  const index = new Map<string, string>();
182
- for (const node of (fileNodes ?? getFileNodes())) {
193
+ for (const node of fileNodes ?? getFileNodes()) {
183
194
  index.set(node.data.path as string, node.id);
184
195
  }
185
196
  return index;
@@ -299,7 +310,7 @@ export function buildCodeGraphSummary(): CodeGraphSummary {
299
310
  path: relative(process.cwd(), node.data.path as string) || (node.data.path as string),
300
311
  title: (node.data.title as string) ?? null,
301
312
  imports: importSpecs.get(node.id) ?? [],
302
- importedBy: [...(inc)].map((id) => {
313
+ importedBy: [...inc].map((id) => {
303
314
  const path = idToPath.get(id);
304
315
  return path ? relative(process.cwd(), path) : id;
305
316
  }),
@@ -350,7 +361,9 @@ export function formatCodeGraph(summary: CodeGraphSummary): string {
350
361
  }
351
362
 
352
363
  if (summary.isolatedFiles.length > 0) {
353
- lines.push(`Isolated files (${summary.isolatedFiles.length}): ${summary.isolatedFiles.map((f) => f.title ?? f.path).join(', ')}`);
364
+ lines.push(
365
+ `Isolated files (${summary.isolatedFiles.length}): ${summary.isolatedFiles.map((f) => f.title ?? f.path).join(', ')}`,
366
+ );
354
367
  lines.push('');
355
368
  }
356
369