pmx-canvas 0.1.35 → 0.2.0

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 (100) hide show
  1. package/CHANGELOG.md +461 -0
  2. package/Readme.md +14 -2
  3. package/dist/canvas/index.js +82 -41
  4. package/dist/json-render/index.js +89 -334
  5. package/dist/types/client/nodes/ExtAppFrame.d.ts +2 -0
  6. package/dist/types/mcp/canvas-access.d.ts +12 -159
  7. package/dist/types/server/ax-context.d.ts +1 -1
  8. package/dist/types/server/ax-state-manager.d.ts +256 -0
  9. package/dist/types/server/ax-state.d.ts +29 -1
  10. package/dist/types/server/ax-wait.d.ts +23 -0
  11. package/dist/types/server/canvas-operations.d.ts +1 -12
  12. package/dist/types/server/canvas-state.d.ts +46 -14
  13. package/dist/types/server/html-surface.d.ts +7 -0
  14. package/dist/types/server/index.d.ts +66 -26
  15. package/dist/types/server/operations/composites.d.ts +121 -0
  16. package/dist/types/server/operations/http.d.ts +7 -0
  17. package/dist/types/server/operations/index.d.ts +8 -0
  18. package/dist/types/server/operations/invoker.d.ts +13 -0
  19. package/dist/types/server/operations/mcp.d.ts +15 -0
  20. package/dist/types/server/operations/ops/annotation.d.ts +2 -0
  21. package/dist/types/server/operations/ops/app.d.ts +33 -0
  22. package/dist/types/server/operations/ops/ax-await.d.ts +2 -0
  23. package/dist/types/server/operations/ops/ax-shared.d.ts +31 -0
  24. package/dist/types/server/operations/ops/ax-state.d.ts +2 -0
  25. package/dist/types/server/operations/ops/ax-timeline.d.ts +2 -0
  26. package/dist/types/server/operations/ops/ax-work.d.ts +2 -0
  27. package/dist/types/server/operations/ops/batch.d.ts +19 -0
  28. package/dist/types/server/operations/ops/edges.d.ts +2 -0
  29. package/dist/types/server/operations/ops/groups.d.ts +2 -0
  30. package/dist/types/server/operations/ops/json-render.d.ts +31 -0
  31. package/dist/types/server/operations/ops/nodes.d.ts +62 -0
  32. package/dist/types/server/operations/ops/query.d.ts +2 -0
  33. package/dist/types/server/operations/ops/snapshots.d.ts +2 -0
  34. package/dist/types/server/operations/ops/validate.d.ts +2 -0
  35. package/dist/types/server/operations/ops/viewport.d.ts +2 -0
  36. package/dist/types/server/operations/ops/webview.d.ts +2 -0
  37. package/dist/types/server/operations/registry.d.ts +15 -0
  38. package/dist/types/server/operations/types.d.ts +116 -0
  39. package/dist/types/server/operations/webview-runner.d.ts +69 -0
  40. package/docs/RELEASE.md +5 -0
  41. package/docs/adr-001-bun-only-runtime.md +46 -0
  42. package/docs/api-stability.md +57 -0
  43. package/docs/ax-host-adapter-contract.md +65 -0
  44. package/docs/ax-state-contract.md +72 -0
  45. package/docs/http-api.md +34 -2
  46. package/docs/mcp.md +64 -11
  47. package/docs/plans/plan-005-operation-registry.md +84 -0
  48. package/docs/plans/plan-006-mcp-tool-consolidation.md +109 -0
  49. package/docs/plans/plan-007-ax-domain.md +99 -0
  50. package/docs/plans/plan-008-registry-finish.md +91 -0
  51. package/docs/screenshot.png +0 -0
  52. package/docs/tech-debt-assessment-2026-06.md +90 -0
  53. package/package.json +3 -3
  54. package/skills/pmx-canvas/SKILL.md +233 -185
  55. package/skills/pmx-canvas/evals/evals.json +3 -3
  56. package/skills/pmx-canvas/references/codex-app-adapter.md +24 -11
  57. package/skills/pmx-canvas/references/github-copilot-app-adapter.md +31 -1
  58. package/src/cli/agent.ts +52 -31
  59. package/src/client/nodes/ExtAppFrame.tsx +73 -5
  60. package/src/client/nodes/HtmlNode.tsx +12 -3
  61. package/src/client/nodes/McpAppNode.tsx +12 -3
  62. package/src/json-render/renderer/index.tsx +3 -0
  63. package/src/mcp/canvas-access.ts +43 -774
  64. package/src/mcp/server.ts +190 -2001
  65. package/src/server/ax-context.ts +7 -1
  66. package/src/server/ax-state-manager.ts +808 -0
  67. package/src/server/ax-state.ts +89 -2
  68. package/src/server/ax-wait.ts +56 -0
  69. package/src/server/canvas-operations.ts +2 -328
  70. package/src/server/canvas-schema.ts +2 -2
  71. package/src/server/canvas-state.ts +140 -382
  72. package/src/server/html-surface.ts +49 -11
  73. package/src/server/index.ts +136 -192
  74. package/src/server/operations/composites.ts +355 -0
  75. package/src/server/operations/http.ts +103 -0
  76. package/src/server/operations/index.ts +65 -0
  77. package/src/server/operations/invoker.ts +87 -0
  78. package/src/server/operations/mcp.ts +221 -0
  79. package/src/server/operations/ops/annotation.ts +60 -0
  80. package/src/server/operations/ops/app.ts +447 -0
  81. package/src/server/operations/ops/ax-await.ts +216 -0
  82. package/src/server/operations/ops/ax-shared.ts +38 -0
  83. package/src/server/operations/ops/ax-state.ts +249 -0
  84. package/src/server/operations/ops/ax-timeline.ts +381 -0
  85. package/src/server/operations/ops/ax-work.ts +635 -0
  86. package/src/server/operations/ops/batch.ts +365 -0
  87. package/src/server/operations/ops/edges.ts +166 -0
  88. package/src/server/operations/ops/groups.ts +176 -0
  89. package/src/server/operations/ops/json-render.ts +691 -0
  90. package/src/server/operations/ops/nodes.ts +1047 -0
  91. package/src/server/operations/ops/query.ts +281 -0
  92. package/src/server/operations/ops/snapshots.ts +366 -0
  93. package/src/server/operations/ops/validate.ts +37 -0
  94. package/src/server/operations/ops/viewport.ts +219 -0
  95. package/src/server/operations/ops/webview.ts +339 -0
  96. package/src/server/operations/registry.ts +79 -0
  97. package/src/server/operations/types.ts +150 -0
  98. package/src/server/operations/webview-runner.ts +77 -0
  99. package/src/server/server.ts +253 -2170
  100. package/src/server/web-artifacts.ts +6 -2
@@ -108,22 +108,60 @@ function injectIntoHead(html: string, content: string): string {
108
108
  * injected when the node's AX capabilities are enabled (opt-in for `html`), and
109
109
  * the server re-validates every interaction — so this is a convenience surface,
110
110
  * not a trust boundary.
111
+ *
112
+ * `emit` returns a Promise that resolves with the interaction result once the
113
+ * parent acks it (report #55 — built-in confirmation so a click no longer looks
114
+ * like "nothing happened"). Authors can also `window.PMX_AX.on('ack', cb)` or
115
+ * listen for the `pmx-ax-ack` CustomEvent. Resolves with an `ax-ack-timeout`
116
+ * result after 10s if no ack arrives (e.g. an older parent), so `await emit()`
117
+ * never hangs.
111
118
  */
112
119
  export function buildAxBridge(axToken: string, nodeId: string): string {
113
120
  const token = JSON.stringify(axToken);
114
121
  const node = JSON.stringify(nodeId);
115
122
  return `<script data-pmx-canvas-ax-bridge>
116
- const PMX_AX_TOKEN = ${token};
117
- const PMX_AX_NODE_ID = ${node};
118
- window.PMX_AX = window.PMX_AX || {};
119
- window.PMX_AX.emit = function (type, payload) {
120
- window.parent.postMessage({
121
- source: 'pmx-canvas-ax',
122
- token: PMX_AX_TOKEN,
123
- nodeId: PMX_AX_NODE_ID,
124
- interaction: { type: String(type), payload: payload && typeof payload === 'object' ? payload : {} },
125
- }, '*');
126
- };
123
+ (function () {
124
+ const PMX_AX_TOKEN = ${token};
125
+ const PMX_AX_NODE_ID = ${node};
126
+ window.PMX_AX = window.PMX_AX || {};
127
+ const pending = new Map();
128
+ const ackListeners = [];
129
+ let seq = 0;
130
+ window.PMX_AX.emit = function (type, payload) {
131
+ seq += 1;
132
+ const correlationId = PMX_AX_NODE_ID + '-' + seq + '-' + (Date.now ? Date.now() : 0);
133
+ window.parent.postMessage({
134
+ source: 'pmx-canvas-ax',
135
+ token: PMX_AX_TOKEN,
136
+ nodeId: PMX_AX_NODE_ID,
137
+ correlationId: correlationId,
138
+ interaction: { type: String(type), payload: payload && typeof payload === 'object' ? payload : {} },
139
+ }, '*');
140
+ return new Promise(function (resolve) {
141
+ const timer = setTimeout(function () {
142
+ pending.delete(correlationId);
143
+ // Match the real reject shape ({ ok:false, status, code:string, error }) so a
144
+ // surface inspecting r.code/r.status sees a consistent type on timeout vs reject.
145
+ resolve({ ok: false, status: 504, code: 'ax-ack-timeout', error: 'ax-ack-timeout' });
146
+ }, 10000);
147
+ pending.set(correlationId, function (result) { clearTimeout(timer); resolve(result); });
148
+ });
149
+ };
150
+ window.PMX_AX.on = function (eventType, cb) {
151
+ if (eventType === 'ack' && typeof cb === 'function') ackListeners.push(cb);
152
+ };
153
+ window.addEventListener('message', function (event) {
154
+ const m = event.data;
155
+ if (!m || m.source !== 'pmx-canvas-ax-ack' || m.token !== PMX_AX_TOKEN) return;
156
+ const result = m.result || { ok: false };
157
+ const resolver = m.correlationId ? pending.get(m.correlationId) : undefined;
158
+ if (resolver) { pending.delete(m.correlationId); resolver(result); }
159
+ for (let i = 0; i < ackListeners.length; i += 1) {
160
+ try { ackListeners[i](result, m.interaction || null); } catch (e) {}
161
+ }
162
+ try { window.dispatchEvent(new CustomEvent('pmx-ax-ack', { detail: { result: result, interaction: m.interaction || null } })); } catch (e) {}
163
+ });
164
+ })();
127
165
  </script>`;
128
166
  }
129
167
 
@@ -3,7 +3,9 @@ import { canvasState, IMAGE_MIME_MAP } from './canvas-state.js';
3
3
  import type { CanvasAnnotation, CanvasNodeState, CanvasEdge, CanvasLayout, ViewportState } from './canvas-state.js';
4
4
  import { buildCanvasAxContext } from './ax-context.js';
5
5
  import { applyAxInteraction, type AxInteractionInput, type AxInteractionPublicResult } from './ax-interaction.js';
6
+ import { waitForAxResolution } from './ax-wait.js';
6
7
  import type {
8
+ PmxAxActivityKind,
7
9
  PmxAxApprovalGate,
8
10
  PmxAxCommandDescriptor,
9
11
  PmxAxContext,
@@ -29,7 +31,6 @@ import type {
29
31
  PmxAxWorkItemStatus,
30
32
  } from './ax-state.js';
31
33
  import type { AxTimelineQuery } from './canvas-db.js';
32
- import { findCanvasExtAppNodeId } from './ext-app-lookup.js';
33
34
  import { onFileNodeChanged } from './file-watcher.js';
34
35
  import { findOpenCanvasPosition, computeGroupBounds } from './placement.js';
35
36
  import { searchNodes, buildSpatialContext } from './spatial-analysis.js';
@@ -38,27 +39,18 @@ import { recomputeCodeGraph, buildCodeGraphSummary, formatCodeGraph } from './co
38
39
  import {
39
40
  addCanvasNode,
40
41
  addCanvasEdge,
41
- appendCanvasJsonRenderStream,
42
- createCanvasStreamingJsonRenderNode,
43
- MARKDOWN_NODE_DEFAULT_SIZE,
44
- MCP_APP_NODE_DEFAULT_SIZE,
45
- IMAGE_NODE_DEFAULT_SIZE,
46
- LEDGER_NODE_DEFAULT_SIZE,
47
42
  applyCanvasNodeUpdates,
48
43
  arrangeCanvasNodes,
49
44
  clearCanvas,
50
45
  createCanvasGraphNode,
51
46
  createCanvasGroup,
52
47
  createCanvasJsonRenderNode,
53
- buildStructuredNodeUpdate,
54
48
  fitCanvasView,
55
49
  deleteCanvasSnapshot,
56
- executeCanvasBatch,
57
50
  gcCanvasSnapshots,
58
51
  groupCanvasNodes,
59
52
  listCanvasSnapshots,
60
53
  refreshCanvasWebpageNode,
61
- removeCanvasNode,
62
54
  removeCanvasEdge,
63
55
  resolveHtmlContent,
64
56
  restoreCanvasSnapshot,
@@ -67,11 +59,19 @@ import {
67
59
  syncCanvasRuntimeBackends,
68
60
  setCanvasContextPins,
69
61
  ungroupCanvasNodes,
70
- validateCanvasNodePatch,
71
- hasStructuredNodeUpdateFields,
72
- hasTraceNodeDataFields,
73
- mergeTraceNodeDataFields,
74
62
  } from './canvas-operations.js';
63
+ import {
64
+ buildNodePatch,
65
+ createBasicCanvasNode,
66
+ removeNodeCore,
67
+ setGroupChildrenFromApi,
68
+ } from './operations/ops/nodes.js';
69
+ import { streamJsonRenderCore } from './operations/ops/json-render.js';
70
+ import {
71
+ executeOperation,
72
+ runCanvasBatchOperation,
73
+ type OpenMcpAppCoreResult,
74
+ } from './operations/index.js';
75
75
  import { validateCanvasLayout } from './canvas-validation.js';
76
76
  import { describeCanvasSchema, validateStructuredCanvasPayload } from './canvas-schema.js';
77
77
  import { serializeCanvasNode, type SerializedCanvasNode } from './canvas-serialization.js';
@@ -84,13 +84,9 @@ import {
84
84
  } from './web-artifacts.js';
85
85
  import {
86
86
  closeMcpAppSession,
87
- openMcpApp as openExternalMcpApp,
88
87
  type ExternalMcpTransportConfig,
89
88
  } from './mcp-app-runtime.js';
90
89
  import {
91
- buildExcalidrawOpenMcpAppInput,
92
- ensureExcalidrawCheckpointId,
93
- isExcalidrawCreateView,
94
90
  type DiagramPresetOpenInput,
95
91
  } from './diagram-presets.js';
96
92
  import {
@@ -257,29 +253,9 @@ export class PmxCanvas extends EventEmitter {
257
253
  if (!groupNode) throw new Error(`Group node "${groupId}" was not created.`);
258
254
  return toSdkNode(groupNode);
259
255
  }
260
- const { id, needsCodeGraphRecompute } = addCanvasNode({
261
- ...input,
262
- defaultWidth: input.type === 'markdown'
263
- ? MARKDOWN_NODE_DEFAULT_SIZE.width
264
- : input.type === 'mcp-app'
265
- ? MCP_APP_NODE_DEFAULT_SIZE.width
266
- : input.type === 'image'
267
- ? IMAGE_NODE_DEFAULT_SIZE.width
268
- : input.type === 'ledger'
269
- ? LEDGER_NODE_DEFAULT_SIZE.width
270
- : 360,
271
- defaultHeight: input.type === 'markdown'
272
- ? MARKDOWN_NODE_DEFAULT_SIZE.height
273
- : input.type === 'mcp-app'
274
- ? MCP_APP_NODE_DEFAULT_SIZE.height
275
- : input.type === 'image'
276
- ? IMAGE_NODE_DEFAULT_SIZE.height
277
- : input.type === 'ledger'
278
- ? LEDGER_NODE_DEFAULT_SIZE.height
279
- : 200,
280
- fileMode: 'path',
281
- ...(input.strictSize ? { strictSize: true } : {}),
282
- });
256
+ // Thin wrapper over the shared operation core (plan-005); the SDK keeps
257
+ // fileMode 'path' as an explicit visible parameter instead of forked code.
258
+ const { node, needsCodeGraphRecompute } = createBasicCanvasNode(input, { fileMode: 'path' });
283
259
 
284
260
  emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
285
261
 
@@ -289,8 +265,6 @@ export class PmxCanvas extends EventEmitter {
289
265
  });
290
266
  }
291
267
 
292
- const node = canvasState.getNode(id);
293
- if (!node) throw new Error(`Node "${id}" was not created.`);
294
268
  return toSdkNode(node);
295
269
  }
296
270
 
@@ -337,58 +311,18 @@ export class PmxCanvas extends EventEmitter {
337
311
  updateNode(id: string, patch: Partial<CanvasNodeState> & Record<string, unknown>): void {
338
312
  const existing = canvasState.getNode(id);
339
313
  if (!existing) return;
340
- const resolvedPatch: Partial<CanvasNodeState> = {};
341
- if (patch.position) resolvedPatch.position = patch.position;
342
- if (patch.size) resolvedPatch.size = patch.size;
343
- if (patch.collapsed !== undefined) resolvedPatch.collapsed = patch.collapsed;
344
- if (patch.pinned !== undefined) resolvedPatch.pinned = patch.pinned;
345
- if (patch.dockPosition !== undefined) resolvedPatch.dockPosition = patch.dockPosition;
346
-
347
- if (hasStructuredNodeUpdateFields(patch)) {
348
- resolvedPatch.data = buildStructuredNodeUpdate(existing, patch).data;
349
- } else if (
350
- patch.data !== undefined ||
351
- patch.title !== undefined ||
352
- patch.content !== undefined ||
353
- typeof patch.arrangeLocked === 'boolean' ||
354
- typeof patch.strictSize === 'boolean' ||
355
- (existing.type === 'trace' && hasTraceNodeDataFields(patch))
356
- ) {
357
- const nextData = {
358
- ...existing.data,
359
- ...(patch.data && typeof patch.data === 'object' && !Array.isArray(patch.data) ? patch.data : {}),
360
- ...(typeof patch.title === 'string' ? { title: patch.title } : {}),
361
- ...(typeof patch.content === 'string' ? { content: patch.content } : {}),
362
- ...(typeof patch.arrangeLocked === 'boolean' ? { arrangeLocked: patch.arrangeLocked } : {}),
363
- ...(typeof patch.strictSize === 'boolean' ? { strictSize: patch.strictSize } : {}),
364
- };
365
- resolvedPatch.data = existing.type === 'trace'
366
- ? mergeTraceNodeDataFields(nextData, patch)
367
- : nextData;
368
- }
369
-
370
- const error = validateCanvasNodePatch({
371
- ...(resolvedPatch.position ? { position: resolvedPatch.position } : {}),
372
- ...(resolvedPatch.size ? { size: resolvedPatch.size } : {}),
373
- });
374
- if (error) {
375
- throw new Error(error);
376
- }
314
+ // Thin wrapper over the shared patch core (plan-005): the SDK now carries
315
+ // the same superset semantics as HTTP/MCP (webpage titleSource/url, html
316
+ // top-level fields, axCapabilities merge, group children).
317
+ const { patch: resolvedPatch, groupChildIds } = buildNodePatch(existing, patch);
377
318
  canvasState.updateNode(id, resolvedPatch);
319
+ if (groupChildIds !== undefined) setGroupChildrenFromApi(id, groupChildIds);
378
320
  emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
379
321
  }
380
322
 
323
+ /** Remove a node. Missing id throws (plan-005 unifies this across surfaces). */
381
324
  removeNode(id: string): void {
382
- const existing = canvasState.getNode(id);
383
- const appSessionId =
384
- existing?.type === 'mcp-app' && typeof existing.data.appSessionId === 'string'
385
- ? existing.data.appSessionId
386
- : null;
387
- if (appSessionId) {
388
- closeMcpAppSession(appSessionId);
389
- }
390
- const { removed, needsCodeGraphRecompute } = removeCanvasNode(id);
391
- if (!removed) return;
325
+ const { needsCodeGraphRecompute } = removeNodeCore(id);
392
326
  emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
393
327
 
394
328
  if (needsCodeGraphRecompute) {
@@ -512,8 +446,8 @@ export class PmxCanvas extends EventEmitter {
512
446
  return canvasState.getAxState();
513
447
  }
514
448
 
515
- getAxContext(): PmxAxContext {
516
- return buildCanvasAxContext();
449
+ getAxContext(options?: { consumer?: string }): PmxAxContext {
450
+ return buildCanvasAxContext(options?.consumer);
517
451
  }
518
452
 
519
453
  setAxFocus(nodeIds: string[], options?: { source?: PmxAxSource }): PmxAxFocusState {
@@ -710,6 +644,84 @@ export class PmxCanvas extends EventEmitter {
710
644
  return modeRequest;
711
645
  }
712
646
 
647
+ // ── Activity ingestion (primitive A — bidirectional board) ────────
648
+ ingestActivity(
649
+ input: {
650
+ kind: PmxAxActivityKind;
651
+ title: string;
652
+ summary?: string | null;
653
+ outcome?: 'success' | 'failure';
654
+ ref?: string | null;
655
+ nodeIds?: string[];
656
+ data?: Record<string, unknown> | null;
657
+ reactions?: {
658
+ workItem?: false | { status?: PmxAxWorkItemStatus; detail?: string | null };
659
+ evidence?: false | { kind?: PmxAxEvidenceKind; body?: string | null };
660
+ review?: false | { severity?: PmxAxReviewSeverity; kind?: PmxAxReviewKind; anchorType?: PmxAxReviewAnchorType; nodeId?: string | null };
661
+ };
662
+ },
663
+ options?: { source?: PmxAxSource },
664
+ ): { event: PmxAxEvent; workItem: PmxAxWorkItem | null; evidence: PmxAxEvidence | null; review: PmxAxReviewAnnotation | null } {
665
+ const result = canvasState.ingestActivity(input, { source: options?.source ?? 'sdk' });
666
+ emitPrimaryWorkbenchEvent('ax-event-created', { event: result.event });
667
+ if (result.workItem) emitPrimaryWorkbenchEvent('ax-state-changed', { workItem: result.workItem });
668
+ if (result.evidence) emitPrimaryWorkbenchEvent('ax-event-created', { evidence: result.evidence });
669
+ if (result.review) emitPrimaryWorkbenchEvent('ax-state-changed', { reviewAnnotation: result.review });
670
+ return result;
671
+ }
672
+
673
+ // ── Single-item readers + blocking waits (primitive D — gates that gate) ──
674
+ getApproval(id: string): PmxAxApprovalGate | null {
675
+ return canvasState.getApproval(id);
676
+ }
677
+
678
+ getElicitation(id: string): PmxAxElicitation | null {
679
+ return canvasState.getElicitation(id);
680
+ }
681
+
682
+ getModeRequest(id: string): PmxAxModeRequest | null {
683
+ return canvasState.getModeRequest(id);
684
+ }
685
+
686
+ async awaitApproval(
687
+ id: string,
688
+ options?: { timeoutMs?: number; signal?: AbortSignal },
689
+ ): Promise<{ approvalGate: PmxAxApprovalGate | null; pending: boolean }> {
690
+ const { value, pending } = await waitForAxResolution<PmxAxApprovalGate>({
691
+ read: () => canvasState.getApproval(id),
692
+ isResolved: (g) => g.status !== 'pending',
693
+ timeoutMs: options?.timeoutMs ?? 30000,
694
+ ...(options?.signal ? { signal: options.signal } : {}),
695
+ });
696
+ return { approvalGate: value, pending };
697
+ }
698
+
699
+ async awaitElicitation(
700
+ id: string,
701
+ options?: { timeoutMs?: number; signal?: AbortSignal },
702
+ ): Promise<{ elicitation: PmxAxElicitation | null; pending: boolean }> {
703
+ const { value, pending } = await waitForAxResolution<PmxAxElicitation>({
704
+ read: () => canvasState.getElicitation(id),
705
+ isResolved: (e) => e.status !== 'pending',
706
+ timeoutMs: options?.timeoutMs ?? 30000,
707
+ ...(options?.signal ? { signal: options.signal } : {}),
708
+ });
709
+ return { elicitation: value, pending };
710
+ }
711
+
712
+ async awaitMode(
713
+ id: string,
714
+ options?: { timeoutMs?: number; signal?: AbortSignal },
715
+ ): Promise<{ modeRequest: PmxAxModeRequest | null; pending: boolean }> {
716
+ const { value, pending } = await waitForAxResolution<PmxAxModeRequest>({
717
+ read: () => canvasState.getModeRequest(id),
718
+ isResolved: (m) => m.status !== 'pending',
719
+ timeoutMs: options?.timeoutMs ?? 30000,
720
+ ...(options?.signal ? { signal: options.signal } : {}),
721
+ });
722
+ return { modeRequest: value, pending };
723
+ }
724
+
713
725
  getCommandRegistry(): PmxAxCommandDescriptor[] {
714
726
  return canvasState.getCommandRegistry();
715
727
  }
@@ -850,13 +862,6 @@ export class PmxCanvas extends EventEmitter {
850
862
  return validateCanvasLayout(canvasState.getLayout());
851
863
  }
852
864
 
853
- private findCanvasExtAppNodeId(toolCallId: string): string | null {
854
- return findCanvasExtAppNodeId(toolCallId, {
855
- getNode: (id) => canvasState.getNode(id),
856
- listNodes: () => canvasState.getLayout().nodes,
857
- });
858
- }
859
-
860
865
  describeSchema() {
861
866
  return describeCanvasSchema();
862
867
  }
@@ -870,14 +875,22 @@ export class PmxCanvas extends EventEmitter {
870
875
  }
871
876
 
872
877
  async runBatch(operations: Array<{ op: string; assign?: string; args?: Record<string, unknown> }>) {
873
- const result = await executeCanvasBatch(operations);
874
- emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
875
- return result;
878
+ // Delegates to the canvas.batch registry meta-op (plan-008 Wave 2). The op
879
+ // emits the single final canvas-layout-update itself (via the registry
880
+ // emitter, which server.ts wires to emitPrimaryWorkbenchEvent) — do NOT
881
+ // emit again here or the frame would fire twice.
882
+ return await runCanvasBatchOperation(operations);
876
883
  }
877
884
 
878
885
  async buildWebArtifact(
879
- input: WebArtifactBuildInput & { openInCanvas?: boolean },
886
+ input: WebArtifactBuildInput & { openInCanvas?: boolean; includeLogs?: boolean },
880
887
  ): Promise<WebArtifactCanvasBuildResult> {
888
+ // The registry's webartifact.build op wraps buildWebArtifactOnCanvas and
889
+ // returns a wire ENVELOPE (path/bytes/…); the SDK's documented return is the
890
+ // full WebArtifactCanvasBuildResult, so the SDK calls the build runtime
891
+ // directly here (the op core is the same buildWebArtifactOnCanvas; the node
892
+ // creation emits its own canvas-layout-update). The op and the SDK share the
893
+ // single build runtime — no behavior divergence.
881
894
  return buildWebArtifactOnCanvas(input);
882
895
  }
883
896
 
@@ -893,75 +906,21 @@ export class PmxCanvas extends EventEmitter {
893
906
  width?: number;
894
907
  height?: number;
895
908
  timeoutMs?: number;
896
- }): Promise<{ ok: true; id?: string; nodeId: string | null; toolCallId: string; sessionId: string; resourceUri: string }> {
897
- const targetNode = input.nodeId ? canvasState.getNode(input.nodeId) : undefined;
898
- if (input.nodeId && !targetNode) {
899
- throw new Error(`Node "${input.nodeId}" not found.`);
900
- }
901
- if (targetNode && (targetNode.type !== 'mcp-app' || targetNode.data.mode !== 'ext-app')) {
902
- throw new Error(`Node "${input.nodeId}" is not an external app node.`);
903
- }
904
-
905
- const opened = await openExternalMcpApp({
906
- transport: input.transport,
907
- toolName: input.toolName,
908
- ...(input.toolArguments ? { toolArguments: input.toolArguments } : {}),
909
- ...(input.serverName ? { serverName: input.serverName } : {}),
910
- ...(typeof input.timeoutMs === 'number' ? { timeoutMs: input.timeoutMs } : {}),
911
- });
912
- const toolCallId = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
913
- const previousSessionId = targetNode?.data.appSessionId;
914
- if (typeof previousSessionId === 'string' && previousSessionId.trim().length > 0) {
915
- closeMcpAppSession(previousSessionId);
916
- }
917
- const nodeIdSeed = input.nodeId ?? `ext-app-${toolCallId}`;
918
- const toolResult = isExcalidrawCreateView(opened.serverName, opened.toolName)
919
- ? ensureExcalidrawCheckpointId(opened.toolResult, nodeIdSeed)
920
- : opened.toolResult;
921
- emitPrimaryWorkbenchEvent('ext-app-open', {
922
- toolCallId,
923
- nodeId: nodeIdSeed,
924
- title: input.title ?? opened.tool.title ?? opened.tool.name,
925
- html: opened.html,
926
- toolInput: opened.toolInput,
927
- serverName: opened.serverName,
928
- toolName: opened.toolName,
929
- appSessionId: opened.sessionId,
930
- transportConfig: input.transport,
931
- resourceUri: opened.resourceUri,
932
- toolDefinition: opened.tool,
933
- sessionStatus: 'ready',
934
- sessionError: null,
935
- ...(opened.resourceMeta ? { resourceMeta: opened.resourceMeta } : {}),
936
- ...(typeof input.x === 'number' ? { x: input.x } : {}),
937
- ...(typeof input.y === 'number' ? { y: input.y } : {}),
938
- ...(typeof input.width === 'number' ? { width: input.width } : {}),
939
- ...(typeof input.height === 'number' ? { height: input.height } : {}),
940
- });
941
- emitPrimaryWorkbenchEvent('ext-app-result', {
942
- toolCallId,
943
- nodeId: nodeIdSeed,
944
- serverName: opened.serverName,
945
- toolName: opened.toolName,
946
- success: toolResult.isError !== true,
947
- result: toolResult,
948
- });
949
- const nodeId = input.nodeId ?? this.findCanvasExtAppNodeId(toolCallId);
950
- return {
951
- ok: true,
952
- ...(nodeId ? { id: nodeId } : {}),
953
- nodeId,
954
- toolCallId,
955
- sessionId: opened.sessionId,
956
- resourceUri: opened.resourceUri,
957
- };
909
+ }): Promise<OpenMcpAppCoreResult> {
910
+ // Delegate to the mcpapp.open registry op (plan-008 Wave 4). The op handler
911
+ // is the relocated legacy body (toolCallId, openExternalMcpApp, prior-session
912
+ // close, ext-app-open + ext-app-result via ctx.emit the registry emitter
913
+ // wired to emitPrimaryWorkbenchEvent). mutates:false, so the registry adds no
914
+ // canvas-layout-update; the two ext-app-* frames fire exactly once.
915
+ return await executeOperation('mcpapp.open', input) as OpenMcpAppCoreResult;
958
916
  }
959
917
 
960
918
  async addDiagram(
961
919
  input: DiagramPresetOpenInput,
962
- ): Promise<{ ok: true; id?: string; nodeId: string | null; toolCallId: string; sessionId: string; resourceUri: string }> {
963
- const built = buildExcalidrawOpenMcpAppInput(input);
964
- return this.openMcpApp(built);
920
+ ): Promise<OpenMcpAppCoreResult> {
921
+ // Delegate to the diagram.open registry op, which builds the Excalidraw
922
+ // OpenMcpApp input and dispatches to the shared open core (one ext-app-* pair).
923
+ return await executeOperation('diagram.open', input) as OpenMcpAppCoreResult;
965
924
  }
966
925
 
967
926
  addJsonRenderNode(
@@ -997,32 +956,17 @@ export class PmxCanvas extends EventEmitter {
997
956
  elementCount: number;
998
957
  streamStatus: 'open' | 'closed';
999
958
  } {
1000
- let nodeId = input.nodeId;
1001
- let url = '';
1002
- if (!nodeId) {
1003
- const created = createCanvasStreamingJsonRenderNode({
1004
- ...(input.title !== undefined ? { title: input.title } : {}),
1005
- ...(input.x !== undefined ? { x: input.x } : {}),
1006
- ...(input.y !== undefined ? { y: input.y } : {}),
1007
- ...(input.width !== undefined ? { width: input.width } : {}),
1008
- ...(input.height !== undefined ? { height: input.height } : {}),
1009
- ...(input.strictSize ? { strictSize: true } : {}),
1010
- });
1011
- nodeId = created.id;
1012
- url = created.url;
1013
- } else {
1014
- url = String(canvasState.getNode(nodeId)?.data.url ?? '');
1015
- }
1016
- const result = appendCanvasJsonRenderStream(
1017
- nodeId,
1018
- Array.isArray(input.patches) ? input.patches : [],
1019
- input.done === true,
1020
- );
1021
- if (!result.ok) throw new Error(result.error);
959
+ // Thin wrapper over the shared create-or-append core (plan-005). The op
960
+ // handler and this SDK method now share one implementation; the SDK emits
961
+ // the layout update itself (it does not flow through the registry's
962
+ // `mutates` path). `streamJsonRenderCore` throws OperationError (an Error
963
+ // subclass with the same message) on a bad append target. The core's
964
+ // result carries an extra `ok: true`; the SDK's wire shape omits it.
965
+ const result = streamJsonRenderCore(input);
1022
966
  emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
1023
967
  return {
1024
- id: nodeId,
1025
- url,
968
+ id: result.id,
969
+ url: result.url,
1026
970
  applied: result.applied,
1027
971
  skipped: result.skipped,
1028
972
  specVersion: result.specVersion,