pmx-canvas 0.2.0 → 0.2.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 (58) hide show
  1. package/CHANGELOG.md +124 -0
  2. package/Readme.md +2 -2
  3. package/dist/canvas/global.css +260 -0
  4. package/dist/canvas/index.js +76 -76
  5. package/dist/json-render/index.js +2 -2
  6. package/dist/types/client/canvas/IntentLayer.d.ts +1 -0
  7. package/dist/types/client/state/intent-bridge.d.ts +10 -0
  8. package/dist/types/client/state/intent-store.d.ts +25 -0
  9. package/dist/types/json-render/server.d.ts +1 -1
  10. package/dist/types/server/ax-state-manager.d.ts +11 -0
  11. package/dist/types/server/ax-state.d.ts +2 -0
  12. package/dist/types/server/canvas-db.d.ts +13 -0
  13. package/dist/types/server/canvas-state.d.ts +5 -0
  14. package/dist/types/server/index.d.ts +34 -4
  15. package/dist/types/server/intent-registry.d.ts +45 -0
  16. package/dist/types/server/operations/ops/intent.d.ts +2 -0
  17. package/dist/types/shared/ax-intent.d.ts +58 -0
  18. package/docs/ax-host-adapter-contract.md +19 -1
  19. package/docs/http-api.md +4 -0
  20. package/docs/mcp.md +22 -3
  21. package/docs/screenshot.png +0 -0
  22. package/package.json +1 -1
  23. package/skills/pmx-canvas/SKILL.md +197 -1283
  24. package/skills/pmx-canvas/evals/evals.json +199 -0
  25. package/skills/pmx-canvas/references/ax-html-control-surface.md +93 -0
  26. package/skills/pmx-canvas/references/full-reference.md +1441 -0
  27. package/skills/pmx-canvas/references/github-copilot-app-adapter.md +23 -7
  28. package/src/cli/index.ts +21 -4
  29. package/src/client/canvas/CanvasNode.tsx +13 -13
  30. package/src/client/canvas/CanvasViewport.tsx +2 -0
  31. package/src/client/canvas/ContextMenu.tsx +25 -19
  32. package/src/client/canvas/IntentLayer.tsx +278 -0
  33. package/src/client/nodes/ExtAppFrame.tsx +31 -22
  34. package/src/client/state/intent-bridge.ts +31 -0
  35. package/src/client/state/intent-store.ts +107 -0
  36. package/src/client/state/sse-bridge.ts +31 -0
  37. package/src/client/theme/global.css +260 -0
  38. package/src/json-render/charts/components.tsx +18 -4
  39. package/src/json-render/renderer/index.tsx +11 -2
  40. package/src/json-render/server.ts +1 -1
  41. package/src/server/ax-context.ts +8 -1
  42. package/src/server/ax-state-manager.ts +18 -0
  43. package/src/server/ax-state.ts +8 -0
  44. package/src/server/canvas-db.ts +35 -0
  45. package/src/server/canvas-state.ts +8 -0
  46. package/src/server/index.ts +240 -158
  47. package/src/server/intent-registry.ts +324 -0
  48. package/src/server/operations/composites.ts +11 -0
  49. package/src/server/operations/index.ts +2 -0
  50. package/src/server/operations/ops/edges.ts +1 -0
  51. package/src/server/operations/ops/groups.ts +3 -0
  52. package/src/server/operations/ops/intent.ts +132 -0
  53. package/src/server/operations/ops/json-render.ts +3 -0
  54. package/src/server/operations/ops/nodes.ts +3 -0
  55. package/src/server/operations/registry.ts +68 -3
  56. package/src/server/server.ts +40 -12
  57. package/src/shared/ax-intent.ts +64 -0
  58. package/src/shared/surface.ts +5 -1
@@ -3,6 +3,8 @@ 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 { intentRegistry } from './intent-registry.js';
7
+ import type { PmxAxIntent, PmxAxIntentKind } from '../shared/ax-intent.js';
6
8
  import { waitForAxResolution } from './ax-wait.js';
7
9
  import type {
8
10
  PmxAxActivityKind,
@@ -143,6 +145,24 @@ export class PmxCanvas extends EventEmitter {
143
145
  this._port = options?.port ?? 4313;
144
146
  }
145
147
 
148
+ private runIntentCommit<T>(
149
+ intentId: string | undefined,
150
+ allowedKinds: readonly PmxAxIntentKind[],
151
+ mutate: () => T,
152
+ settledNodeId: (result: T) => string | undefined,
153
+ ): T {
154
+ if (intentId === undefined) return mutate();
155
+ intentRegistry.beginCommit(intentId, allowedKinds);
156
+ try {
157
+ const result = mutate();
158
+ intentRegistry.completeCommit(intentId, settledNodeId(result));
159
+ return result;
160
+ } catch (error) {
161
+ intentRegistry.abortCommit(intentId);
162
+ throw error;
163
+ }
164
+ }
165
+
146
166
  async start(options?: {
147
167
  open?: boolean;
148
168
  automationWebView?: boolean | CanvasAutomationWebViewOptions;
@@ -216,6 +236,7 @@ export class PmxCanvas extends EventEmitter {
216
236
  * or keep the whole node — both work. (Previously returned a bare id string.)
217
237
  */
218
238
  addNode(input: {
239
+ intentId?: string;
219
240
  type: CanvasNodeState['type'];
220
241
  title?: string;
221
242
  content?: string;
@@ -235,40 +256,43 @@ export class PmxCanvas extends EventEmitter {
235
256
  height?: number;
236
257
  strictSize?: boolean;
237
258
  }): SdkCanvasNode {
238
- if (input.type === 'webpage') {
239
- throw new Error('Use addWebpageNode for webpage nodes so page content is fetched and cached on the server.');
240
- }
241
- if (input.type === 'group') {
242
- const groupId = this.createGroup({
243
- ...(typeof input.title === 'string' ? { title: input.title } : {}),
244
- childIds: input.childIds ?? input.children ?? [],
245
- ...(typeof input.x === 'number' ? { x: input.x } : {}),
246
- ...(typeof input.y === 'number' ? { y: input.y } : {}),
247
- ...(typeof input.width === 'number' ? { width: input.width } : {}),
248
- ...(typeof input.height === 'number' ? { height: input.height } : {}),
249
- ...(typeof input.color === 'string' ? { color: input.color } : {}),
250
- ...(input.childLayout ? { childLayout: input.childLayout } : {}),
251
- });
252
- const groupNode = canvasState.getNode(groupId);
253
- if (!groupNode) throw new Error(`Group node "${groupId}" was not created.`);
254
- return toSdkNode(groupNode);
255
- }
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' });
259
+ return this.runIntentCommit(input.intentId, ['create'], () => {
260
+ if (input.type === 'webpage') {
261
+ throw new Error('Use addWebpageNode for webpage nodes so page content is fetched and cached on the server.');
262
+ }
263
+ if (input.type === 'group') {
264
+ const groupId = this.createGroup({
265
+ ...(typeof input.title === 'string' ? { title: input.title } : {}),
266
+ childIds: input.childIds ?? input.children ?? [],
267
+ ...(typeof input.x === 'number' ? { x: input.x } : {}),
268
+ ...(typeof input.y === 'number' ? { y: input.y } : {}),
269
+ ...(typeof input.width === 'number' ? { width: input.width } : {}),
270
+ ...(typeof input.height === 'number' ? { height: input.height } : {}),
271
+ ...(typeof input.color === 'string' ? { color: input.color } : {}),
272
+ ...(input.childLayout ? { childLayout: input.childLayout } : {}),
273
+ });
274
+ const groupNode = canvasState.getNode(groupId);
275
+ if (!groupNode) throw new Error(`Group node "${groupId}" was not created.`);
276
+ return toSdkNode(groupNode);
277
+ }
278
+ // Thin wrapper over the shared operation core (plan-005); the SDK keeps
279
+ // fileMode 'path' as an explicit visible parameter instead of forked code.
280
+ const { node, needsCodeGraphRecompute } = createBasicCanvasNode(input, { fileMode: 'path' });
259
281
 
260
- emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
282
+ emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
261
283
 
262
- if (needsCodeGraphRecompute) {
263
- scheduleCodeGraphRecompute(() => {
264
- emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
265
- });
266
- }
284
+ if (needsCodeGraphRecompute) {
285
+ scheduleCodeGraphRecompute(() => {
286
+ emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
287
+ });
288
+ }
267
289
 
268
- return toSdkNode(node);
290
+ return toSdkNode(node);
291
+ }, (node) => node.id);
269
292
  }
270
293
 
271
294
  async addWebpageNode(input: {
295
+ intentId?: string;
272
296
  title?: string;
273
297
  url: string;
274
298
  x?: number;
@@ -277,29 +301,33 @@ export class PmxCanvas extends EventEmitter {
277
301
  height?: number;
278
302
  strictSize?: boolean;
279
303
  }): Promise<{ ok: boolean; id: string; error?: string; fetch: { ok: boolean; error?: string } }> {
280
- const { id } = addCanvasNode({
281
- type: 'webpage',
282
- ...(typeof input.title === 'string' ? { title: input.title } : {}),
283
- content: input.url,
284
- ...(typeof input.x === 'number' ? { x: input.x } : {}),
285
- ...(typeof input.y === 'number' ? { y: input.y } : {}),
286
- ...(typeof input.width === 'number' ? { width: input.width } : {}),
287
- ...(typeof input.height === 'number' ? { height: input.height } : {}),
288
- ...(input.strictSize ? { strictSize: true } : {}),
289
- defaultWidth: 520,
290
- defaultHeight: 420,
291
- });
292
- emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
293
- const result = await refreshCanvasWebpageNode(id);
294
- emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
295
- return {
296
- ok: true,
297
- id,
298
- fetch: result.ok
299
- ? { ok: true }
300
- : { ok: false, error: result.error ?? 'Failed to fetch webpage content.' },
301
- ...(result.ok ? {} : { error: result.error }),
304
+ const mutate = async () => {
305
+ const { id } = addCanvasNode({
306
+ type: 'webpage',
307
+ ...(typeof input.title === 'string' ? { title: input.title } : {}),
308
+ content: input.url,
309
+ ...(typeof input.x === 'number' ? { x: input.x } : {}),
310
+ ...(typeof input.y === 'number' ? { y: input.y } : {}),
311
+ ...(typeof input.width === 'number' ? { width: input.width } : {}),
312
+ ...(typeof input.height === 'number' ? { height: input.height } : {}),
313
+ ...(input.strictSize ? { strictSize: true } : {}),
314
+ defaultWidth: 520,
315
+ defaultHeight: 420,
316
+ });
317
+ emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
318
+ const result = await refreshCanvasWebpageNode(id);
319
+ emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
320
+ return {
321
+ ok: true,
322
+ id,
323
+ fetch: result.ok
324
+ ? { ok: true }
325
+ : { ok: false, error: result.error ?? 'Failed to fetch webpage content.' },
326
+ ...(result.ok ? {} : { error: result.error }),
327
+ };
302
328
  };
329
+ if (input.intentId === undefined) return await mutate();
330
+ return await intentRegistry.runCommit(input.intentId, ['create'], mutate, (result) => result.id);
303
331
  }
304
332
 
305
333
  async refreshWebpageNode(id: string, url?: string): Promise<{ ok: boolean; id: string; error?: string }> {
@@ -309,30 +337,39 @@ export class PmxCanvas extends EventEmitter {
309
337
  }
310
338
 
311
339
  updateNode(id: string, patch: Partial<CanvasNodeState> & Record<string, unknown>): void {
312
- const existing = canvasState.getNode(id);
313
- if (!existing) return;
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);
318
- canvasState.updateNode(id, resolvedPatch);
319
- if (groupChildIds !== undefined) setGroupChildrenFromApi(id, groupChildIds);
320
- emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
340
+ const intentId = typeof patch.intentId === 'string' ? patch.intentId : undefined;
341
+ this.runIntentCommit(intentId, ['move', 'edit'], () => {
342
+ const existing = canvasState.getNode(id);
343
+ if (!existing) {
344
+ if (intentId !== undefined) throw new Error(`Node "${id}" not found.`);
345
+ return;
346
+ }
347
+ // Thin wrapper over the shared patch core (plan-005): the SDK now carries
348
+ // the same superset semantics as HTTP/MCP (webpage titleSource/url, html
349
+ // top-level fields, axCapabilities merge, group children).
350
+ const { patch: resolvedPatch, groupChildIds } = buildNodePatch(existing, patch);
351
+ canvasState.updateNode(id, resolvedPatch);
352
+ if (groupChildIds !== undefined) setGroupChildrenFromApi(id, groupChildIds);
353
+ emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
354
+ }, () => id);
321
355
  }
322
356
 
323
357
  /** Remove a node. Missing id throws (plan-005 unifies this across surfaces). */
324
- removeNode(id: string): void {
325
- const { needsCodeGraphRecompute } = removeNodeCore(id);
326
- emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
358
+ removeNode(id: string, options?: { intentId?: string }): void {
359
+ this.runIntentCommit(options?.intentId, ['remove'], () => {
360
+ const { needsCodeGraphRecompute } = removeNodeCore(id);
361
+ emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
327
362
 
328
- if (needsCodeGraphRecompute) {
329
- scheduleCodeGraphRecompute(() => {
330
- emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
331
- });
332
- }
363
+ if (needsCodeGraphRecompute) {
364
+ scheduleCodeGraphRecompute(() => {
365
+ emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
366
+ });
367
+ }
368
+ }, () => undefined);
333
369
  }
334
370
 
335
371
  addEdge(input: {
372
+ intentId?: string;
336
373
  from?: string;
337
374
  to?: string;
338
375
  fromSearch?: string;
@@ -342,9 +379,11 @@ export class PmxCanvas extends EventEmitter {
342
379
  style?: CanvasEdge['style'];
343
380
  animated?: boolean;
344
381
  }): string {
345
- const { id } = addCanvasEdge(input);
346
- emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
347
- return id;
382
+ return this.runIntentCommit(input.intentId, ['connect'], () => {
383
+ const { id } = addCanvasEdge(input);
384
+ emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
385
+ return id;
386
+ }, () => undefined);
348
387
  }
349
388
 
350
389
  addAnnotation(input: Omit<CanvasAnnotation, 'id' | 'createdAt'> & { id?: string; createdAt?: string }): string {
@@ -374,6 +413,7 @@ export class PmxCanvas extends EventEmitter {
374
413
  * If childIds are provided, the group auto-sizes to contain them with padding.
375
414
  */
376
415
  createGroup(input: {
416
+ intentId?: string;
377
417
  title?: string;
378
418
  childIds?: string[];
379
419
  x?: number;
@@ -383,28 +423,39 @@ export class PmxCanvas extends EventEmitter {
383
423
  color?: string;
384
424
  childLayout?: 'grid' | 'column' | 'flow';
385
425
  }): string {
386
- const { id } = createCanvasGroup(input);
387
-
388
- emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
389
- return id;
426
+ return this.runIntentCommit(input.intentId, ['create'], () => {
427
+ const { id } = createCanvasGroup(input);
428
+ emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
429
+ return id;
430
+ }, (id) => id);
390
431
  }
391
432
 
392
433
  /** Add nodes to an existing group. */
393
- groupNodes(groupId: string, childIds: string[], options?: { childLayout?: 'grid' | 'column' | 'flow' }): boolean {
394
- const { ok } = groupCanvasNodes(groupId, childIds, options);
395
- if (ok) {
396
- emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
397
- }
398
- return ok;
434
+ groupNodes(groupId: string, childIds: string[], options?: { childLayout?: 'grid' | 'column' | 'flow'; intentId?: string }): boolean {
435
+ return this.runIntentCommit(options?.intentId, ['edit'], () => {
436
+ const { ok } = groupCanvasNodes(groupId, childIds, options);
437
+ if (!ok && options?.intentId !== undefined) {
438
+ throw new Error(`Group "${groupId}" could not be updated.`);
439
+ }
440
+ if (ok) {
441
+ emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
442
+ }
443
+ return ok;
444
+ }, () => groupId);
399
445
  }
400
446
 
401
447
  /** Remove all children from a group (the group node remains). */
402
- ungroupNodes(groupId: string): boolean {
403
- const { ok } = ungroupCanvasNodes(groupId);
404
- if (ok) {
405
- emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
406
- }
407
- return ok;
448
+ ungroupNodes(groupId: string, options?: { intentId?: string }): boolean {
449
+ return this.runIntentCommit(options?.intentId, ['edit'], () => {
450
+ const { ok } = ungroupCanvasNodes(groupId);
451
+ if (!ok && options?.intentId !== undefined) {
452
+ throw new Error(`Group "${groupId}" could not be updated.`);
453
+ }
454
+ if (ok) {
455
+ emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
456
+ }
457
+ return ok;
458
+ }, () => groupId);
408
459
  }
409
460
 
410
461
  clear(): void {
@@ -477,6 +528,24 @@ export class PmxCanvas extends EventEmitter {
477
528
  return ok;
478
529
  }
479
530
 
531
+ /**
532
+ * Ghost Cursor of Intent — announce a spatial move before making it. The ghost
533
+ * is ephemeral presence (auto-expiring, never snapshotted); the registry emits
534
+ * the `ax-intent` SSE frame so the browser paints a pre-commit placeholder.
535
+ */
536
+ signalIntent(input: Record<string, unknown>): PmxAxIntent {
537
+ return intentRegistry.signal({ source: 'sdk', ...input });
538
+ }
539
+
540
+ updateIntent(id: string, patch: Record<string, unknown>): PmxAxIntent {
541
+ return intentRegistry.update(id, patch);
542
+ }
543
+
544
+ /** Dissolve a ghost; pass `settledNodeId` once the real node has landed. */
545
+ clearIntent(id: string, options?: { settledNodeId?: string; vetoed?: boolean }): boolean {
546
+ return intentRegistry.clear(id, options ?? {});
547
+ }
548
+
480
549
  /** Undelivered steering for a consumer (loop-safe; excludes consumer-originated). */
481
550
  getPendingSteering(options?: { consumer?: string; limit?: number }): PmxAxSteeringMessage[] {
482
551
  return canvasState.getPendingSteering(options ?? {});
@@ -924,11 +993,13 @@ export class PmxCanvas extends EventEmitter {
924
993
  }
925
994
 
926
995
  addJsonRenderNode(
927
- input: JsonRenderNodeInput,
996
+ input: JsonRenderNodeInput & { intentId?: string },
928
997
  ): { id: string; url: string; spec: JsonRenderSpec } {
929
- const result = createCanvasJsonRenderNode(input);
930
- emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
931
- return result;
998
+ return this.runIntentCommit(input.intentId, ['create'], () => {
999
+ const result = createCanvasJsonRenderNode(input);
1000
+ emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
1001
+ return result;
1002
+ }, (result) => result.id);
932
1003
  }
933
1004
 
934
1005
  /**
@@ -938,6 +1009,7 @@ export class PmxCanvas extends EventEmitter {
938
1009
  * reloads the viewer as the specVersion bumps.
939
1010
  */
940
1011
  streamJsonRenderNode(input: {
1012
+ intentId?: string;
941
1013
  nodeId?: string;
942
1014
  title?: string;
943
1015
  patches?: unknown[];
@@ -962,20 +1034,23 @@ export class PmxCanvas extends EventEmitter {
962
1034
  // `mutates` path). `streamJsonRenderCore` throws OperationError (an Error
963
1035
  // subclass with the same message) on a bad append target. The core's
964
1036
  // result carries an extra `ok: true`; the SDK's wire shape omits it.
965
- const result = streamJsonRenderCore(input);
966
- emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
967
- return {
968
- id: result.id,
969
- url: result.url,
970
- applied: result.applied,
971
- skipped: result.skipped,
972
- specVersion: result.specVersion,
973
- elementCount: result.elementCount,
974
- streamStatus: result.streamStatus,
975
- };
1037
+ return this.runIntentCommit(input.intentId, input.nodeId ? ['edit'] : ['create'], () => {
1038
+ const result = streamJsonRenderCore(input);
1039
+ emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
1040
+ return {
1041
+ id: result.id,
1042
+ url: result.url,
1043
+ applied: result.applied,
1044
+ skipped: result.skipped,
1045
+ specVersion: result.specVersion,
1046
+ elementCount: result.elementCount,
1047
+ streamStatus: result.streamStatus,
1048
+ };
1049
+ }, (result) => result.id);
976
1050
  }
977
1051
 
978
1052
  addHtmlNode(input: {
1053
+ intentId?: string;
979
1054
  html: string;
980
1055
  title?: string;
981
1056
  summary?: string;
@@ -994,35 +1069,38 @@ export class PmxCanvas extends EventEmitter {
994
1069
  * the html capability ceiling server-side; cannot escalate. */
995
1070
  axCapabilities?: { enabled?: boolean; allowed?: string[] };
996
1071
  }): SdkCanvasNode {
997
- const { id } = addCanvasNode({
998
- type: 'html',
999
- ...(typeof input.title === 'string' ? { title: input.title } : {}),
1000
- data: {
1001
- html: resolveHtmlContent(input.html),
1002
- ...(typeof input.summary === 'string' ? { summary: input.summary } : {}),
1003
- ...(typeof input.agentSummary === 'string' ? { agentSummary: input.agentSummary } : {}),
1004
- ...(typeof input.description === 'string' ? { description: input.description } : {}),
1005
- ...(input.presentation === true ? { presentation: true } : {}),
1006
- ...(Array.isArray(input.slideTitles) ? { slideTitles: input.slideTitles } : {}),
1007
- ...(Array.isArray(input.embeddedNodeIds) ? { embeddedNodeIds: input.embeddedNodeIds } : {}),
1008
- ...(Array.isArray(input.embeddedUrls) ? { embeddedUrls: input.embeddedUrls } : {}),
1009
- ...(input.axCapabilities ? { axCapabilities: input.axCapabilities } : {}),
1010
- },
1011
- ...(typeof input.x === 'number' ? { x: input.x } : {}),
1012
- ...(typeof input.y === 'number' ? { y: input.y } : {}),
1013
- ...(typeof input.width === 'number' ? { width: input.width } : {}),
1014
- ...(typeof input.height === 'number' ? { height: input.height } : {}),
1015
- ...(input.strictSize ? { strictSize: true } : {}),
1016
- defaultWidth: 720,
1017
- defaultHeight: 640,
1018
- });
1019
- emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
1020
- const node = canvasState.getNode(id);
1021
- if (!node) throw new Error(`HTML node "${id}" was not created.`);
1022
- return toSdkNode(node);
1072
+ return this.runIntentCommit(input.intentId, ['create'], () => {
1073
+ const { id } = addCanvasNode({
1074
+ type: 'html',
1075
+ ...(typeof input.title === 'string' ? { title: input.title } : {}),
1076
+ data: {
1077
+ html: resolveHtmlContent(input.html),
1078
+ ...(typeof input.summary === 'string' ? { summary: input.summary } : {}),
1079
+ ...(typeof input.agentSummary === 'string' ? { agentSummary: input.agentSummary } : {}),
1080
+ ...(typeof input.description === 'string' ? { description: input.description } : {}),
1081
+ ...(input.presentation === true ? { presentation: true } : {}),
1082
+ ...(Array.isArray(input.slideTitles) ? { slideTitles: input.slideTitles } : {}),
1083
+ ...(Array.isArray(input.embeddedNodeIds) ? { embeddedNodeIds: input.embeddedNodeIds } : {}),
1084
+ ...(Array.isArray(input.embeddedUrls) ? { embeddedUrls: input.embeddedUrls } : {}),
1085
+ ...(input.axCapabilities ? { axCapabilities: input.axCapabilities } : {}),
1086
+ },
1087
+ ...(typeof input.x === 'number' ? { x: input.x } : {}),
1088
+ ...(typeof input.y === 'number' ? { y: input.y } : {}),
1089
+ ...(typeof input.width === 'number' ? { width: input.width } : {}),
1090
+ ...(typeof input.height === 'number' ? { height: input.height } : {}),
1091
+ ...(input.strictSize ? { strictSize: true } : {}),
1092
+ defaultWidth: 720,
1093
+ defaultHeight: 640,
1094
+ });
1095
+ emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
1096
+ const node = canvasState.getNode(id);
1097
+ if (!node) throw new Error(`HTML node "${id}" was not created.`);
1098
+ return toSdkNode(node);
1099
+ }, (node) => node.id);
1023
1100
  }
1024
1101
 
1025
1102
  addHtmlPrimitive(input: {
1103
+ intentId?: string;
1026
1104
  kind: HtmlPrimitiveKind;
1027
1105
  title?: string;
1028
1106
  data?: Record<string, unknown>;
@@ -1032,39 +1110,43 @@ export class PmxCanvas extends EventEmitter {
1032
1110
  height?: number;
1033
1111
  strictSize?: boolean;
1034
1112
  }): { id: string; kind: HtmlPrimitiveKind; title: string; htmlBytes: number } {
1035
- const built = buildHtmlPrimitive({
1036
- kind: input.kind,
1037
- ...(typeof input.title === 'string' ? { title: input.title } : {}),
1038
- ...(input.data ? { data: input.data } : {}),
1039
- });
1040
- const { id } = addCanvasNode({
1041
- type: 'html',
1042
- title: built.title,
1043
- data: {
1044
- html: built.html,
1045
- htmlPrimitive: built.kind,
1046
- primitiveData: built.data,
1047
- description: built.summary,
1048
- agentSummary: typeof input.data?.agentSummary === 'string' ? input.data.agentSummary : built.summary,
1049
- ...(typeof input.data?.summary === 'string' ? { summary: input.data.summary } : {}),
1050
- ...getHtmlPrimitiveSemanticMetadata(built.data),
1051
- },
1052
- ...(typeof input.x === 'number' ? { x: input.x } : {}),
1053
- ...(typeof input.y === 'number' ? { y: input.y } : {}),
1054
- ...(typeof input.width === 'number' ? { width: input.width } : {}),
1055
- ...(typeof input.height === 'number' ? { height: input.height } : {}),
1056
- ...(input.strictSize ? { strictSize: true } : {}),
1057
- defaultWidth: built.defaultSize.width,
1058
- defaultHeight: built.defaultSize.height,
1059
- });
1060
- emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
1061
- return { id, kind: built.kind, title: built.title, htmlBytes: Buffer.byteLength(built.html, 'utf-8') };
1113
+ return this.runIntentCommit(input.intentId, ['create'], () => {
1114
+ const built = buildHtmlPrimitive({
1115
+ kind: input.kind,
1116
+ ...(typeof input.title === 'string' ? { title: input.title } : {}),
1117
+ ...(input.data ? { data: input.data } : {}),
1118
+ });
1119
+ const { id } = addCanvasNode({
1120
+ type: 'html',
1121
+ title: built.title,
1122
+ data: {
1123
+ html: built.html,
1124
+ htmlPrimitive: built.kind,
1125
+ primitiveData: built.data,
1126
+ description: built.summary,
1127
+ agentSummary: typeof input.data?.agentSummary === 'string' ? input.data.agentSummary : built.summary,
1128
+ ...(typeof input.data?.summary === 'string' ? { summary: input.data.summary } : {}),
1129
+ ...getHtmlPrimitiveSemanticMetadata(built.data),
1130
+ },
1131
+ ...(typeof input.x === 'number' ? { x: input.x } : {}),
1132
+ ...(typeof input.y === 'number' ? { y: input.y } : {}),
1133
+ ...(typeof input.width === 'number' ? { width: input.width } : {}),
1134
+ ...(typeof input.height === 'number' ? { height: input.height } : {}),
1135
+ ...(input.strictSize ? { strictSize: true } : {}),
1136
+ defaultWidth: built.defaultSize.width,
1137
+ defaultHeight: built.defaultSize.height,
1138
+ });
1139
+ emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
1140
+ return { id, kind: built.kind, title: built.title, htmlBytes: Buffer.byteLength(built.html, 'utf-8') };
1141
+ }, (result) => result.id);
1062
1142
  }
1063
1143
 
1064
- addGraphNode(input: GraphNodeInput): { id: string; url: string; spec: JsonRenderSpec } {
1065
- const result = createCanvasGraphNode(input);
1066
- emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
1067
- return result;
1144
+ addGraphNode(input: GraphNodeInput & { intentId?: string }): { id: string; url: string; spec: JsonRenderSpec } {
1145
+ return this.runIntentCommit(input.intentId, ['create'], () => {
1146
+ const result = createCanvasGraphNode(input);
1147
+ emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
1148
+ return result;
1149
+ }, (result) => result.id);
1068
1150
  }
1069
1151
 
1070
1152
  get port(): number {