pmx-canvas 0.1.19 → 0.1.20

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 (57) hide show
  1. package/CHANGELOG.md +74 -0
  2. package/Readme.md +19 -6
  3. package/dist/canvas/global.css +35 -2
  4. package/dist/canvas/index.js +70 -69
  5. package/dist/json-render/index.js +109 -109
  6. package/dist/types/client/canvas/CanvasViewport.d.ts +1 -1
  7. package/dist/types/client/icons.d.ts +2 -0
  8. package/dist/types/client/state/canvas-store.d.ts +2 -0
  9. package/dist/types/client/types.d.ts +2 -1
  10. package/dist/types/json-render/charts/components.d.ts +5 -1
  11. package/dist/types/json-render/renderer/index.d.ts +1 -0
  12. package/dist/types/json-render/server.d.ts +1 -0
  13. package/dist/types/mcp/canvas-access.d.ts +3 -0
  14. package/dist/types/server/canvas-operations.d.ts +4 -0
  15. package/dist/types/server/canvas-schema.d.ts +19 -3
  16. package/dist/types/server/canvas-serialization.d.ts +1 -0
  17. package/dist/types/server/canvas-state.d.ts +6 -2
  18. package/dist/types/server/html-primitives.d.ts +34 -0
  19. package/dist/types/server/index.d.ts +19 -0
  20. package/docs/cli.md +4 -1
  21. package/docs/http-api.md +10 -0
  22. package/docs/mcp.md +6 -4
  23. package/docs/node-types.md +30 -2
  24. package/docs/screenshot.png +0 -0
  25. package/docs/sdk.md +11 -0
  26. package/package.json +1 -1
  27. package/skills/pmx-canvas/SKILL.md +8 -0
  28. package/src/cli/agent.ts +150 -5
  29. package/src/client/App.tsx +20 -1
  30. package/src/client/canvas/AnnotationLayer.tsx +33 -12
  31. package/src/client/canvas/CanvasViewport.tsx +88 -7
  32. package/src/client/canvas/CommandPalette.tsx +1 -1
  33. package/src/client/canvas/ContextMenu.tsx +2 -2
  34. package/src/client/canvas/ExpandedNodeOverlay.tsx +7 -1
  35. package/src/client/icons.tsx +13 -0
  36. package/src/client/nodes/McpAppNode.tsx +12 -4
  37. package/src/client/state/canvas-store.ts +15 -5
  38. package/src/client/state/sse-bridge.ts +4 -3
  39. package/src/client/theme/global.css +35 -2
  40. package/src/client/types.ts +2 -1
  41. package/src/json-render/charts/components.tsx +41 -7
  42. package/src/json-render/charts/extra-components.tsx +13 -12
  43. package/src/json-render/renderer/index.tsx +1 -0
  44. package/src/json-render/server.ts +3 -1
  45. package/src/mcp/canvas-access.ts +23 -0
  46. package/src/mcp/server.ts +83 -27
  47. package/src/server/agent-context.ts +17 -0
  48. package/src/server/canvas-operations.ts +91 -38
  49. package/src/server/canvas-schema.ts +83 -3
  50. package/src/server/canvas-serialization.ts +9 -2
  51. package/src/server/canvas-state.ts +9 -4
  52. package/src/server/demo-state.json +1143 -0
  53. package/src/server/demo.ts +25 -777
  54. package/src/server/html-primitives.ts +990 -0
  55. package/src/server/index.ts +43 -2
  56. package/src/server/server.ts +138 -14
  57. package/src/server/spatial-analysis.ts +3 -3
@@ -20,6 +20,7 @@ import { mutationHistory } from './mutation-history.js';
20
20
  import { computeGroupBounds, findOpenCanvasPosition } from './placement.js';
21
21
  import { searchNodes } from './spatial-analysis.js';
22
22
  import { getCanvasNodeTitle, serializeCanvasNode, type SerializedCanvasNode } from './canvas-serialization.js';
23
+ import { computeAutoArrange } from '../shared/auto-arrange.js';
23
24
  import {
24
25
  buildGraphSpec,
25
26
  buildGraphConfig,
@@ -102,6 +103,8 @@ interface CanvasAddNodeInput {
102
103
  strictSize?: boolean;
103
104
  }
104
105
 
106
+ export const MARKDOWN_NODE_DEFAULT_SIZE = { width: 520, height: 360 };
107
+
105
108
  interface CanvasCreateGroupInput {
106
109
  title?: string;
107
110
  childIds?: string[];
@@ -1070,62 +1073,105 @@ function collectArrangeExcludedNodeIds(nodes: CanvasNodeState[]): Set<string> {
1070
1073
  return excluded;
1071
1074
  }
1072
1075
 
1076
+ function collectGridArrangeExcludedNodeIds(nodes: CanvasNodeState[]): Set<string> {
1077
+ const nodesById = new Map(nodes.map((node) => [node.id, node]));
1078
+ const excluded = new Set<string>();
1079
+
1080
+ for (const node of nodes) {
1081
+ if (isArrangeLocked(node)) excluded.add(node.id);
1082
+ }
1083
+
1084
+ for (const node of nodes) {
1085
+ if (node.type !== 'group') continue;
1086
+ const childIds = Array.isArray(node.data.children)
1087
+ ? node.data.children.filter((id): id is string => typeof id === 'string')
1088
+ : [];
1089
+ const hasLockedChild = childIds.some((childId) => {
1090
+ const child = nodesById.get(childId);
1091
+ return child ? isArrangeLocked(child) : false;
1092
+ });
1093
+ if (!excluded.has(node.id) && !hasLockedChild) continue;
1094
+
1095
+ excluded.add(node.id);
1096
+ for (const childId of childIds) excluded.add(childId);
1097
+ }
1098
+
1099
+ return excluded;
1100
+ }
1101
+
1073
1102
  export function arrangeCanvasNodes(layout: CanvasArrangeMode): { arranged: number; layout: CanvasArrangeMode } {
1074
1103
  const nodes = canvasState.getLayout().nodes;
1075
- const excludedIds = collectArrangeExcludedNodeIds(nodes);
1104
+ const excludedIds = layout === 'grid'
1105
+ ? collectGridArrangeExcludedNodeIds(nodes)
1106
+ : collectArrangeExcludedNodeIds(nodes);
1076
1107
  const movableNodes = nodes.filter((node) => !excludedIds.has(node.id));
1077
- const gap = 24;
1078
1108
  const oldPositions = nodes.map((node) => ({ id: node.id, position: { ...node.position } }));
1079
-
1080
- canvasState.withSuppressedRecording(() => {
1081
- if (layout === 'column') {
1082
- let y = 80;
1083
- for (const node of movableNodes) {
1084
- canvasState.updateNode(node.id, { position: { x: 40, y } });
1109
+ const oldSizes = nodes.map((node) => ({ id: node.id, size: { ...node.size } }));
1110
+ const newSizesById = new Map<string, CanvasNodeUpdate['size']>();
1111
+ const oldSizesById = new Map(oldSizes.map((entry) => [entry.id, entry.size]));
1112
+ const updates: CanvasNodeUpdate[] = [];
1113
+
1114
+ if (layout === 'column' || layout === 'flow') {
1115
+ const gap = 24;
1116
+ let x = 40;
1117
+ let y = 80;
1118
+ for (const node of movableNodes) {
1119
+ updates.push({ id: node.id, position: { x, y } });
1120
+ if (layout === 'column') {
1085
1121
  y += node.size.height + gap;
1086
- }
1087
- return;
1088
- }
1089
-
1090
- if (layout === 'flow') {
1091
- let x = 40;
1092
- for (const node of movableNodes) {
1093
- canvasState.updateNode(node.id, { position: { x, y: 80 } });
1122
+ } else {
1094
1123
  x += node.size.width + gap;
1095
1124
  }
1096
- return;
1097
1125
  }
1098
-
1099
- const maxNodeWidth = movableNodes.reduce((max, node) => Math.max(max, node.size.width), 360);
1100
- const cols = Math.max(1, Math.floor(1440 / (maxNodeWidth + gap)));
1101
- let col = 0;
1102
- let rowY = 80;
1103
- let rowMaxHeight = 0;
1104
- for (const node of movableNodes) {
1105
- const x = 40 + col * (maxNodeWidth + gap);
1106
- canvasState.updateNode(node.id, { position: { x, y: rowY } });
1107
- rowMaxHeight = Math.max(rowMaxHeight, node.size.height);
1108
- col++;
1109
- if (col >= cols) {
1110
- col = 0;
1111
- rowY += rowMaxHeight + gap;
1112
- rowMaxHeight = 0;
1113
- }
1126
+ } else {
1127
+ const result = computeAutoArrange(movableNodes, canvasState.getEdges(), 'grid');
1128
+ for (const [id, position] of result.nodePositions.entries()) {
1129
+ updates.push({ id, position });
1114
1130
  }
1131
+ for (const [groupId, bounds] of result.groupBounds.entries()) {
1132
+ updates.push({
1133
+ id: groupId,
1134
+ position: { x: bounds.x, y: bounds.y },
1135
+ size: { width: bounds.width, height: bounds.height },
1136
+ });
1137
+ }
1138
+ }
1139
+
1140
+ canvasState.withSuppressedRecording(() => {
1141
+ canvasState.applyUpdates(updates, layout === 'grid' ? { skipGroupChildTranslation: true } : {});
1115
1142
  });
1116
1143
 
1117
1144
  const newPositions = nodes.map((node) => {
1118
1145
  const updated = canvasState.getNode(node.id);
1119
1146
  return { id: node.id, position: updated ? { ...updated.position } : { ...node.position } };
1120
1147
  });
1148
+ for (const node of nodes) {
1149
+ const updated = canvasState.getNode(node.id);
1150
+ const size = updated ? { ...updated.size } : { ...node.size };
1151
+ newSizesById.set(node.id, size);
1152
+ }
1121
1153
  mutationHistory.record({
1122
1154
  description: `Auto-arranged ${movableNodes.length} nodes (${layout})`,
1123
1155
  operationType: 'arrange',
1124
1156
  forward: () => canvasState.withSuppressedRecording(() => {
1125
- for (const position of newPositions) canvasState.updateNode(position.id, { position: position.position });
1157
+ canvasState.applyUpdates(newPositions.map((position) => {
1158
+ const size = newSizesById.get(position.id);
1159
+ return {
1160
+ id: position.id,
1161
+ position: position.position,
1162
+ ...(size ? { size } : {}),
1163
+ };
1164
+ }), layout === 'grid' ? { skipGroupChildTranslation: true } : {});
1126
1165
  }),
1127
1166
  inverse: () => canvasState.withSuppressedRecording(() => {
1128
- for (const position of oldPositions) canvasState.updateNode(position.id, { position: position.position });
1167
+ canvasState.applyUpdates(oldPositions.map((position) => {
1168
+ const size = oldSizesById.get(position.id);
1169
+ return {
1170
+ id: position.id,
1171
+ position: position.position,
1172
+ ...(size ? { size } : {}),
1173
+ };
1174
+ }), layout === 'grid' ? { skipGroupChildTranslation: true } : {});
1129
1175
  }),
1130
1176
  });
1131
1177
 
@@ -1477,6 +1523,9 @@ export async function executeCanvasBatch(
1477
1523
  switch (operation.op) {
1478
1524
  case 'node.add': {
1479
1525
  const type = typeof args.type === 'string' ? args.type : 'markdown';
1526
+ if (type === 'html-primitive') {
1527
+ throw new Error('Batch html-primitive creation is not supported yet. Use node.add with type "html" and generated html, or create the primitive through MCP/HTTP/CLI first.');
1528
+ }
1480
1529
  if (type === 'webpage') {
1481
1530
  const created = addCanvasNode({
1482
1531
  type: 'webpage',
@@ -1502,18 +1551,22 @@ export async function executeCanvasBatch(
1502
1551
  ...(fetch.ok ? {} : { error: fetch.error }),
1503
1552
  };
1504
1553
  } else {
1554
+ const data = isPlainRecord(args.data) ? args.data : {};
1555
+ const htmlData = type === 'html' && typeof args.html === 'string'
1556
+ ? { ...data, html: args.html }
1557
+ : data;
1505
1558
  const created = addCanvasNode({
1506
1559
  type: type as CanvasNodeState['type'],
1507
1560
  ...(typeof args.title === 'string' ? { title: args.title } : {}),
1508
1561
  ...(typeof args.content === 'string' ? { content: args.content } : {}),
1509
- ...(isPlainRecord(args.data) ? { data: args.data } : {}),
1562
+ ...(Object.keys(htmlData).length > 0 ? { data: htmlData } : {}),
1510
1563
  ...(typeof args.x === 'number' ? { x: args.x } : {}),
1511
1564
  ...(typeof args.y === 'number' ? { y: args.y } : {}),
1512
1565
  ...(typeof args.width === 'number' ? { width: args.width } : {}),
1513
1566
  ...(typeof args.height === 'number' ? { height: args.height } : {}),
1514
1567
  ...(args.strictSize === true ? { strictSize: true } : {}),
1515
- defaultWidth: 360,
1516
- defaultHeight: 200,
1568
+ defaultWidth: type === 'html' ? 720 : type === 'markdown' ? MARKDOWN_NODE_DEFAULT_SIZE.width : 360,
1569
+ defaultHeight: type === 'html' ? 640 : type === 'markdown' ? MARKDOWN_NODE_DEFAULT_SIZE.height : 200,
1517
1570
  fileMode: 'auto',
1518
1571
  });
1519
1572
  result = { ok: true, ...serializeCreatedNode(created.node) };
@@ -8,6 +8,12 @@ import {
8
8
  type GraphNodeInput,
9
9
  type JsonRenderSpec,
10
10
  } from '../json-render/server.js';
11
+ import {
12
+ buildHtmlPrimitive,
13
+ isHtmlPrimitiveKind,
14
+ listHtmlPrimitiveDescriptors,
15
+ type HtmlPrimitiveDescriptor,
16
+ } from './html-primitives.js';
11
17
 
12
18
  export interface CanvasCreateField {
13
19
  name: string;
@@ -30,8 +36,14 @@ export interface CanvasCreateTypeSchema {
30
36
 
31
37
  export interface StructuredValidationResult {
32
38
  ok: true;
33
- type: 'json-render' | 'graph';
34
- normalizedSpec: JsonRenderSpec;
39
+ type: 'json-render' | 'graph' | 'html-primitive';
40
+ normalizedSpec?: JsonRenderSpec;
41
+ normalizedPrimitive?: {
42
+ kind: string;
43
+ title: string;
44
+ htmlBytes: number;
45
+ defaultSize: { width: number; height: number };
46
+ };
35
47
  summary: Record<string, unknown>;
36
48
  }
37
49
 
@@ -231,6 +243,8 @@ const CANVAS_CREATE_TYPES: CanvasCreateTypeSchema[] = [
231
243
  mcpTool: 'canvas_add_html_node',
232
244
  fields: [
233
245
  { name: 'html', type: 'string', required: false, description: 'HTML document or fragment rendered in the sandboxed iframe.', aliases: ['content', 'stdin'] },
246
+ { name: 'primitive', type: 'HtmlPrimitiveKind', required: false, description: 'Generate HTML from a built-in communication primitive instead of passing raw HTML.', aliases: ['kind'] },
247
+ { name: 'data', type: 'record<string, unknown>', required: false, description: 'Primitive data when --primitive is used, or arbitrary node metadata.' },
234
248
  { name: 'title', type: 'string', required: false, description: 'Optional node title.' },
235
249
  { name: 'x', type: 'number', required: false, description: 'Optional X position.' },
236
250
  { name: 'y', type: 'number', required: false, description: 'Optional Y position.' },
@@ -245,9 +259,41 @@ const CANVAS_CREATE_TYPES: CanvasCreateTypeSchema[] = [
245
259
  },
246
260
  notes: [
247
261
  'The CLI accepts --content as an alias and stores it as data.html so the renderer can load it.',
262
+ 'Use `primitive` / `kind` with `data` to create reusable agent communication artifacts such as choice grids, plans, review sheets, explainers, and editors.',
248
263
  'HTML runs in a sandboxed iframe without same-origin access to the canvas host.',
249
264
  ],
250
265
  },
266
+ {
267
+ type: 'html-primitive',
268
+ kind: 'virtual-node',
269
+ description: 'Reusable sandboxed HTML communication primitive rendered as an html node.',
270
+ endpoint: '/api/canvas/node',
271
+ mcpTool: 'canvas_add_html_primitive',
272
+ fields: [
273
+ { name: 'kind', type: 'HtmlPrimitiveKind', required: true, description: 'Primitive kind. See top-level htmlPrimitives for the supported catalog.' },
274
+ { name: 'data', type: 'record<string, unknown>', required: false, description: 'Primitive-specific JSON object payload.' },
275
+ { name: 'title', type: 'string', required: false, description: 'Optional node title.' },
276
+ { name: 'x', type: 'number', required: false, description: 'Optional X position.' },
277
+ { name: 'y', type: 'number', required: false, description: 'Optional Y position.' },
278
+ { name: 'width', type: 'number', required: false, description: 'Optional node width; defaults per primitive.' },
279
+ { name: 'height', type: 'number', required: false, description: 'Optional node height; defaults per primitive.' },
280
+ { name: 'strictSize', type: 'boolean', required: false, description: 'Keep explicit width/height fixed and scroll overflowing content instead of browser auto-fitting.', aliases: ['strict-size', 'scroll-overflow'] },
281
+ ],
282
+ example: {
283
+ type: 'html-primitive',
284
+ kind: 'choice-grid',
285
+ title: 'Implementation Options',
286
+ data: {
287
+ items: [
288
+ { title: 'Small patch', summary: 'Least disruption.', pros: ['Fast'], cons: ['Limited flexibility'] },
289
+ ],
290
+ },
291
+ },
292
+ notes: [
293
+ 'HTTP callers may POST { type: "html-primitive", kind, data } or { type: "html", primitive: kind, data }; both create a normal html node with primitive metadata.',
294
+ 'Interactive editor primitives include copy/export controls so the human can send edited state back to the agent.',
295
+ ],
296
+ },
251
297
  {
252
298
  type: 'mcp-app',
253
299
  kind: 'node',
@@ -449,6 +495,7 @@ export function describeCanvasSchema(): {
449
495
  graph: {
450
496
  graphTypes: CanvasGraphType[];
451
497
  };
498
+ htmlPrimitives: HtmlPrimitiveDescriptor[];
452
499
  mcp: {
453
500
  tools: string[];
454
501
  resources: string[];
@@ -472,9 +519,12 @@ export function describeCanvasSchema(): {
472
519
  graph: {
473
520
  graphTypes: [...CANONICAL_GRAPH_TYPES],
474
521
  },
522
+ htmlPrimitives: listHtmlPrimitiveDescriptors(),
475
523
  mcp: {
476
524
  tools: [
477
525
  'canvas_add_node',
526
+ 'canvas_add_html_node',
527
+ 'canvas_add_html_primitive',
478
528
  'canvas_add_json_render_node',
479
529
  'canvas_add_graph_node',
480
530
  'canvas_build_web_artifact',
@@ -492,9 +542,10 @@ export function describeCanvasSchema(): {
492
542
  }
493
543
 
494
544
  export function validateStructuredCanvasPayload(input: {
495
- type: 'json-render' | 'graph';
545
+ type: 'json-render' | 'graph' | 'html-primitive';
496
546
  spec?: unknown;
497
547
  graph?: GraphNodeInput;
548
+ primitive?: { kind: string; title?: string; data?: Record<string, unknown> };
498
549
  }): StructuredValidationResult {
499
550
  if (input.type === 'json-render') {
500
551
  const normalizedSpec = normalizeAndValidateJsonRenderSpec(input.spec);
@@ -510,6 +561,35 @@ export function validateStructuredCanvasPayload(input: {
510
561
  };
511
562
  }
512
563
 
564
+ if (input.type === 'html-primitive') {
565
+ if (!input.primitive) {
566
+ throw new Error('HTML primitive validation requires a primitive payload.');
567
+ }
568
+ if (!isHtmlPrimitiveKind(input.primitive.kind)) {
569
+ throw new Error(`Unknown HTML primitive: ${input.primitive.kind}`);
570
+ }
571
+ const built = buildHtmlPrimitive({
572
+ kind: input.primitive.kind,
573
+ ...(typeof input.primitive.title === 'string' ? { title: input.primitive.title } : {}),
574
+ ...(input.primitive.data ? { data: input.primitive.data } : {}),
575
+ });
576
+ return {
577
+ ok: true,
578
+ type: 'html-primitive',
579
+ normalizedPrimitive: {
580
+ kind: built.kind,
581
+ title: built.title,
582
+ htmlBytes: Buffer.byteLength(built.html, 'utf-8'),
583
+ defaultSize: built.defaultSize,
584
+ },
585
+ summary: {
586
+ kind: built.kind,
587
+ title: built.title,
588
+ dataKeys: Object.keys(built.data),
589
+ },
590
+ };
591
+ }
592
+
513
593
  if (!input.graph) {
514
594
  throw new Error('Graph validation requires a graph payload.');
515
595
  }
@@ -27,6 +27,7 @@ export interface CanvasAnnotationSummary {
27
27
  color: string;
28
28
  width: number;
29
29
  pointCount: number;
30
+ text: string | null;
30
31
  label: string | null;
31
32
  createdAt: string;
32
33
  }
@@ -75,6 +76,11 @@ export function getCanvasNodeTitle(node: CanvasNodeState): string | null {
75
76
  }
76
77
 
77
78
  export function getCanvasNodeContent(node: CanvasNodeState): string | null {
79
+ if (node.type === 'html' && typeof node.data.htmlPrimitive === 'string') {
80
+ const primitive = node.data.htmlPrimitive;
81
+ const description = pickString(node.data.description);
82
+ return description ? `${primitive}: ${description}` : primitive;
83
+ }
78
84
  return pickString(node.data.content)
79
85
  ?? pickString(node.data.fileContent)
80
86
  ?? pickString(node.data.text)
@@ -180,7 +186,8 @@ export function summarizeCanvasAnnotation(annotation: CanvasAnnotation): CanvasA
180
186
  color: annotation.color,
181
187
  width: annotation.width,
182
188
  pointCount: annotation.points.length,
183
- label: annotation.label ?? null,
189
+ text: annotation.text ?? null,
190
+ label: annotation.label ?? annotation.text ?? null,
184
191
  createdAt: annotation.createdAt,
185
192
  };
186
193
  }
@@ -208,7 +215,7 @@ export function summarizeCanvasAnnotationForContext(
208
215
  const targetNodeTitles = targetNodes.map((node) => getCanvasNodeTitle(node) ?? node.id);
209
216
  return {
210
217
  id: annotation.id,
211
- label: annotation.label ?? null,
218
+ label: annotation.label ?? annotation.text ?? null,
212
219
  bounds: annotation.bounds,
213
220
  targetNodeIds: targetNodes.map((node) => node.id),
214
221
  targetNodeTitles,
@@ -162,11 +162,12 @@ export interface CanvasAnnotationPoint {
162
162
 
163
163
  export interface CanvasAnnotation {
164
164
  id: string;
165
- type: 'freehand';
165
+ type: 'freehand' | 'text';
166
166
  points: CanvasAnnotationPoint[];
167
167
  bounds: { x: number; y: number; width: number; height: number };
168
168
  color: string;
169
169
  width: number;
170
+ text?: string;
170
171
  label?: string;
171
172
  createdAt: string;
172
173
  }
@@ -201,6 +202,10 @@ interface GroupNodesOptions {
201
202
  keepGroupFrame?: boolean;
202
203
  }
203
204
 
205
+ interface ApplyUpdatesOptions {
206
+ skipGroupChildTranslation?: boolean;
207
+ }
208
+
204
209
  function formatBatchUpdateDescription(updates: CanvasNodeUpdate[]): string {
205
210
  let moved = 0;
206
211
  let resized = 0;
@@ -1216,7 +1221,7 @@ class CanvasStateManager {
1216
1221
  };
1217
1222
  }
1218
1223
 
1219
- applyUpdates(updates: CanvasNodeUpdate[]): { applied: number; skipped: number } {
1224
+ applyUpdates(updates: CanvasNodeUpdate[], options: ApplyUpdatesOptions = {}): { applied: number; skipped: number } {
1220
1225
  let applied = 0;
1221
1226
  let skipped = 0;
1222
1227
  const touchedParentGroups = new Map<string, { compact: boolean }>();
@@ -1259,7 +1264,7 @@ class CanvasStateManager {
1259
1264
  }
1260
1265
  oldSnapshots.set(update.id, structuredClone(existing));
1261
1266
  appliedUpdates.push({ id: update.id, ...structuredClone(nextPatch) });
1262
- if (existing.type === 'group' && nextPatch.position) {
1267
+ if (existing.type === 'group' && nextPatch.position && options.skipGroupChildTranslation !== true) {
1263
1268
  this.translateGroupChildren(
1264
1269
  update.id,
1265
1270
  nextPatch.position.x - existing.position.x,
@@ -1297,7 +1302,7 @@ class CanvasStateManager {
1297
1302
  operationType: 'batch',
1298
1303
  description: formatBatchUpdateDescription(appliedUpdates),
1299
1304
  forward: this.suppressed(() => {
1300
- this.applyUpdates(appliedUpdates.map((update) => structuredClone(update)));
1305
+ this.applyUpdates(appliedUpdates.map((update) => structuredClone(update)), options);
1301
1306
  }),
1302
1307
  inverse: this.suppressed(() => {
1303
1308
  for (const snapshot of inverseSnapshots) {