@agent-native/core 0.81.2 → 0.81.3

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 (52) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +19 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/client/frame.ts +55 -0
  5. package/corpus/core/src/mcp/build-server.ts +30 -4
  6. package/corpus/core/src/mcp/embed-app.ts +33 -18
  7. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +8 -1
  8. package/corpus/templates/design/app/components/design/LayersPanel.tsx +11 -2
  9. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +89 -16
  10. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +21 -14
  11. package/corpus/templates/design/app/pages/DesignEditor.tsx +557 -211
  12. package/corpus/templates/design/changelog/2026-06-30-design-studio-canvas-layers-can-be-copied-pasted-and-reparen.md +6 -0
  13. package/corpus/templates/design/e2e/global-setup.ts +3 -1
  14. package/corpus/templates/design/e2e/helpers.ts +4 -1
  15. package/corpus/templates/design/playwright.config.ts +4 -1
  16. package/corpus/templates/plan/actions/consume-plan-feedback.ts +1 -9
  17. package/corpus/templates/plan/actions/convert-visual-plan-to-prototype.ts +1 -1
  18. package/corpus/templates/plan/actions/create-plan-design.ts +1 -1
  19. package/corpus/templates/plan/actions/create-prototype-plan.ts +1 -1
  20. package/corpus/templates/plan/actions/create-ui-plan.ts +1 -1
  21. package/corpus/templates/plan/actions/create-visual-plan.ts +36 -19
  22. package/corpus/templates/plan/actions/create-visual-questions.ts +1 -1
  23. package/corpus/templates/plan/actions/create-visual-recap.ts +1 -1
  24. package/corpus/templates/plan/actions/delete-plan-comment.ts +1 -9
  25. package/corpus/templates/plan/actions/delete-visual-plan.ts +1 -9
  26. package/corpus/templates/plan/actions/get-plan-feedback.ts +1 -9
  27. package/corpus/templates/plan/actions/get-visual-plan.ts +1 -9
  28. package/corpus/templates/plan/actions/import-visual-plan-source.ts +1 -1
  29. package/corpus/templates/plan/actions/list-visual-plans.ts +1 -9
  30. package/corpus/templates/plan/actions/reply-to-plan-comment.ts +1 -9
  31. package/corpus/templates/plan/actions/resolve-plan-comment.ts +1 -9
  32. package/corpus/templates/plan/actions/update-visual-plan.ts +1 -1
  33. package/corpus/templates/plan/actions/visual-answer.ts +1 -1
  34. package/corpus/templates/plan/app/components/layout/Layout.tsx +15 -0
  35. package/corpus/templates/plan/app/entry.client.tsx +10 -0
  36. package/corpus/templates/plan/app/global.css +28 -0
  37. package/corpus/templates/plan/shared/plan-content.ts +10 -6
  38. package/dist/client/frame.d.ts.map +1 -1
  39. package/dist/client/frame.js +59 -0
  40. package/dist/client/frame.js.map +1 -1
  41. package/dist/collab/awareness.d.ts +2 -2
  42. package/dist/collab/awareness.d.ts.map +1 -1
  43. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  44. package/dist/mcp/build-server.d.ts.map +1 -1
  45. package/dist/mcp/build-server.js +31 -4
  46. package/dist/mcp/build-server.js.map +1 -1
  47. package/dist/mcp/embed-app.d.ts.map +1 -1
  48. package/dist/mcp/embed-app.js +33 -18
  49. package/dist/mcp/embed-app.js.map +1 -1
  50. package/dist/notifications/routes.d.ts +3 -3
  51. package/dist/observability/routes.d.ts +5 -5
  52. package/package.json +1 -1
package/corpus/README.md CHANGED
@@ -28,4 +28,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
28
28
  ## Generated Counts
29
29
 
30
30
  - core files: 2043
31
- - template files: 4803
31
+ - template files: 4804
@@ -1,5 +1,24 @@
1
1
  # @agent-native/core
2
2
 
3
+ ## 0.81.3
4
+
5
+ ### Patch Changes
6
+
7
+ - 3807702: Fix MCP App embed rendering in ChatGPT/Codex hosts:
8
+ - Stop the `openai:set_globals` storm that left embeds stuck blanking by
9
+ guarding the bridge sync on a signature that ignores host `maxHeight`, and
10
+ by not re-blanking once the app frame has launched.
11
+ - Size embeds to their content: the embedded app reports its real
12
+ `scrollHeight` via `agentNative.contentHeight` and the shell sizes the iframe
13
+ to that (plus chrome) instead of the host max, so plans no longer render far
14
+ too tall or too short.
15
+ - Suppress empty embeds: when a tool whose descriptor declares an embed widget
16
+ produces no embeddable content, the result is marked `isError` so the host
17
+ shows the text result without an empty widget box. This also keeps read-only
18
+ and comment-mutation tools from rendering an embed for results that produce
19
+ no plan surface.
20
+ - Bump the embed shell resource version so hosts refetch the updated shell.
21
+
3
22
  ## 0.81.2
4
23
 
5
24
  ### Patch Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.81.2",
3
+ "version": "0.81.3",
4
4
  "description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -83,6 +83,60 @@ export function isTrustedFrameMessage(event: MessageEvent): boolean {
83
83
  return event.source === window.parent || event.source === window;
84
84
  }
85
85
 
86
+ // ---------------------------------------------------------------------------
87
+ // Content-height reporting (for MCP App embeds)
88
+ // ---------------------------------------------------------------------------
89
+ //
90
+ // The embed shell renders this app in a cross-origin iframe and can't measure
91
+ // its content, so we post the real content height for the shell to size to.
92
+ // Only shrinks when the document flows to its natural height — embed layouts
93
+ // must not force `100vh`/`h-screen` on the root, or scrollHeight just equals
94
+ // the iframe height.
95
+ let _contentHeightReportingStarted = false;
96
+ let _lastReportedContentHeight = 0;
97
+
98
+ function measureContentHeight(): number {
99
+ if (typeof document === "undefined") return 0;
100
+ const doc = document.documentElement;
101
+ const body = document.body;
102
+ return Math.ceil(
103
+ Math.max(doc ? doc.scrollHeight : 0, body ? body.scrollHeight : 0),
104
+ );
105
+ }
106
+
107
+ function reportContentHeight(): void {
108
+ if (typeof window === "undefined" || window.parent === window) return;
109
+ const height = measureContentHeight();
110
+ if (!height || Math.abs(height - _lastReportedContentHeight) < 2) return;
111
+ _lastReportedContentHeight = height;
112
+ sendToFrame("agentNative.contentHeight", { height });
113
+ }
114
+
115
+ function startContentHeightReporting(): void {
116
+ if (_contentHeightReportingStarted || typeof window === "undefined") return;
117
+ if (window.parent === window) return;
118
+ _contentHeightReportingStarted = true;
119
+ let scheduled = false;
120
+ const schedule = () => {
121
+ if (scheduled) return;
122
+ scheduled = true;
123
+ window.requestAnimationFrame(() => {
124
+ scheduled = false;
125
+ reportContentHeight();
126
+ });
127
+ };
128
+ schedule();
129
+ for (const delay of [100, 300, 800, 1500]) window.setTimeout(schedule, delay);
130
+ try {
131
+ const observer = new ResizeObserver(schedule);
132
+ if (document.documentElement) observer.observe(document.documentElement);
133
+ if (document.body) observer.observe(document.body);
134
+ } catch {
135
+ window.addEventListener("resize", schedule, { passive: true });
136
+ }
137
+ window.addEventListener("load", schedule, { passive: true });
138
+ }
139
+
86
140
  // Listen for frame origin message and cache it.
87
141
  // Only accept from the direct parent frame, and only set once.
88
142
  if (typeof window !== "undefined") {
@@ -102,6 +156,7 @@ if (typeof window !== "undefined") {
102
156
  { type: "agentNative.embeddedAppReady" },
103
157
  getFramePostMessageTargetOrigin() ?? window.location.origin,
104
158
  );
159
+ startContentHeightReporting();
105
160
  }
106
161
  });
107
162
  }
@@ -448,6 +448,18 @@ function purgeEmbedStartUrls(value: unknown, depth = 0): unknown {
448
448
  return value;
449
449
  }
450
450
 
451
+ // True when a tool result carries SOME content. An errored/no-plan result comes
452
+ // back empty (`{}` / null) and would render an empty embed box. We gate on "has
453
+ // content", not "has a URL" — valid embeds often carry data but no URL (the
454
+ // shell mints the embed-start itself).
455
+ function mcpResultHasContent(result: unknown): boolean {
456
+ if (result == null) return false;
457
+ if (typeof result === "string") return result.trim().length > 0;
458
+ if (Array.isArray(result)) return result.length > 0;
459
+ if (typeof result === "object") return Object.keys(result).length > 0;
460
+ return Boolean(result);
461
+ }
462
+
451
463
  function mcpAppEmbedOpenLinkMeta(
452
464
  result: unknown,
453
465
  resource: ResolvedMcpAppResource,
@@ -750,7 +762,7 @@ function safeUiSegment(value: string | undefined, fallback: string): string {
750
762
 
751
763
  // ChatGPT and Claude cache MCP App resource HTML by `ui://` URI. Bump this
752
764
  // when the shared shell changes in a way that must invalidate host caches.
753
- const MCP_APP_RESOURCE_SHELL_VERSION = "shell-v54";
765
+ const MCP_APP_RESOURCE_SHELL_VERSION = "shell-v64";
754
766
 
755
767
  function legacyDefaultMcpAppUri(config: MCPConfig, actionName: string): string {
756
768
  const app = safeUiSegment(config.appId ?? config.name, "agent-native");
@@ -1546,12 +1558,24 @@ export async function createMCPServerForRequest(
1546
1558
  // branch below degrades to the plain deep-link artifacts the tool would
1547
1559
  // otherwise return — no `openai/outputTemplate`, no minted embed-start,
1548
1560
  // no embed structuredContent — so the host shows a link, not an iframe.
1549
- const mcpAppResource = requestMeta?.inlineMcpApps
1561
+ const mcpAppResourceCandidate = requestMeta?.inlineMcpApps
1550
1562
  ? await resolveMcpAppResourceSafely(config, name, entry, requestMeta)
1551
1563
  : null;
1552
- const rawResultForClient = mcpAppResource
1564
+ const rawResultForClient = mcpAppResourceCandidate
1553
1565
  ? await withServerMintedMcpAppEmbedStart(rawResult, requestMeta)
1554
1566
  : rawResult;
1567
+ // Only attach the embed widget for a non-error result that has content.
1568
+ const embedHasContent = mcpResultHasContent(rawResultForClient);
1569
+ const mcpAppResource =
1570
+ mcpAppResourceCandidate && !mcpResultIsError && embedHasContent
1571
+ ? mcpAppResourceCandidate
1572
+ : null;
1573
+ // `openai/outputTemplate` is declared at the tool level, so the host
1574
+ // renders a widget for every call regardless of result _meta. The only
1575
+ // per-result signal it honors is `isError` (shows error text, no widget),
1576
+ // so treat an embed tool that produced nothing as an error.
1577
+ const embedProducedNothing =
1578
+ !!mcpAppResourceCandidate && !mcpResultIsError && !embedHasContent;
1555
1579
  const { block, _meta } = buildLinkArtifacts(
1556
1580
  entry,
1557
1581
  (args as Record<string, any>) ?? {},
@@ -1590,7 +1614,9 @@ export async function createMCPServerForRequest(
1590
1614
  if (block) content.push(block);
1591
1615
  return {
1592
1616
  content,
1593
- ...(mcpResultIsError ? { isError: true } : {}),
1617
+ ...(mcpResultIsError || embedProducedNothing
1618
+ ? { isError: true }
1619
+ : {}),
1594
1620
  ...(structuredContent ? { structuredContent } : {}),
1595
1621
  ...(Object.keys(responseMeta).length > 0
1596
1622
  ? { _meta: responseMeta }
@@ -138,6 +138,7 @@ export function embedApp(
138
138
  let appFrameLoadTimer = null;
139
139
  let lastFrameSrc = "";
140
140
  let embedSessionRefreshAttempts = 0;
141
+ let reportedContentHeight = 0;
141
142
 
142
143
  function esc(value) {
143
144
  return String(value ?? "")
@@ -174,10 +175,18 @@ export function embedApp(
174
175
  function visibleIntrinsicHeight() {
175
176
  const context = hostState().context || {};
176
177
  const hostMaxHeight = contextMaxHeight(context);
178
+ // Size to the app-reported content height (the shell can't measure the
179
+ // cross-origin frame). Don't clamp to the host maxHeight — on Codex it's a
180
+ // small inline hint (~360) that clips the plan; applyIntrinsicHeight still
181
+ // caps at defaultIntrinsicHeight.
182
+ if (reportedContentHeight > 0) {
183
+ return Math.floor(reportedContentHeight + chromeHeight);
184
+ }
177
185
  if (hostMaxHeight) return Math.floor(hostMaxHeight);
178
- const viewportHeight = finiteNumber(window.visualViewport && window.visualViewport.height) ||
179
- finiteNumber(window.innerHeight);
180
- return Math.floor(viewportHeight || defaultIntrinsicHeight);
186
+ // No host maxHeight and no content height yet: use the CONFIGURED height,
187
+ // never window.innerHeight. Echoing the iframe's own size pins us to the
188
+ // current frame height (circular) and locks the embed at the cap forever.
189
+ return Math.floor(defaultIntrinsicHeight);
181
190
  }
182
191
 
183
192
  function applyIntrinsicHeight(nextHeight) {
@@ -1356,7 +1365,8 @@ export function embedApp(
1356
1365
  }
1357
1366
 
1358
1367
  function notifyHostHeight() {
1359
- const height = applyIntrinsicHeight(visibleIntrinsicHeight());
1368
+ const intrinsic = visibleIntrinsicHeight();
1369
+ const height = applyIntrinsicHeight(intrinsic);
1360
1370
  if (!openAiBridge || typeof openAiBridge.notifyIntrinsicHeight !== "function") {
1361
1371
  if (app && typeof app.sendSizeChanged === "function") {
1362
1372
  try {
@@ -1487,6 +1497,14 @@ export function embedApp(
1487
1497
  notifyOuterMcpAppReady();
1488
1498
  return;
1489
1499
  }
1500
+ if (event.data.type === "agentNative.contentHeight") {
1501
+ const next = finiteNumber(data && data.height);
1502
+ if (next && Math.abs(next - reportedContentHeight) >= 1) {
1503
+ reportedContentHeight = next;
1504
+ notifyHostHeightSoon();
1505
+ }
1506
+ return;
1507
+ }
1490
1508
  if (event.data.type === "agentNative.embedSessionExpired") {
1491
1509
  refreshExpiredEmbedSession();
1492
1510
  return;
@@ -1538,12 +1556,9 @@ export function embedApp(
1538
1556
  const selfNavigate = shouldSelfNavigateToApp();
1539
1557
  if (startedFor === launchUrl) return;
1540
1558
  startedFor = launchUrl;
1541
- // Only show the loading message when no frame is mounted yet. ChatGPT
1542
- // fires openai:set_globals (and thus re-launch) constantly; calling
1543
- // setMessage() unconditionally here wiped the just-mounted iframe out of
1544
- // the stage on every redundant launch, so the app rendered and was
1545
- // instantly blanked back to "Loading app" forever. renderFrame()/
1546
- // transplant replace the stage themselves when they actually (re)mount.
1559
+ // Only show "Loading app" when no frame is mounted. set_globals
1560
+ // re-launches constantly, so calling it every time would wipe the
1561
+ // just-mounted iframe; renderFrame/transplant own the stage on (re)mount.
1547
1562
  if (!appFrame) {
1548
1563
  setMessage("Loading app");
1549
1564
  }
@@ -1671,14 +1686,11 @@ export function embedApp(
1671
1686
  toolResultData = objectValue(data);
1672
1687
  openUrl = openLinkFrom(params, data);
1673
1688
  openStartUrl = embedStartUrlFrom(params, data);
1674
- // ChatGPT/Codex fire openai:set_globals extremely often, and the listener
1675
- // that calls this re-syncs on each one. Because this function calls
1676
- // notifyHostHeight()/sendHostContext() (which the host reflects back as a
1677
- // fresh set_globals), an unguarded sync becomes an infinite feedback storm
1678
- // that starves the host until it shows its sad-face placeholder. Only do
1679
- // the host round-trips + (re)launch when something we care about changed.
1680
- // Deliberately EXCLUDE bridge.maxHeight: the host mutates it on every
1681
- // set_globals as it echoes our height back, which would defeat the guard.
1689
+ // set_globals fires constantly, and this sync calls notifyHostHeight/
1690
+ // sendHostContext which the host echoes back as another set_globals an
1691
+ // infinite storm. Only do the host round-trips + (re)launch when something
1692
+ // we care about changed. Exclude maxHeight: the host mutates it every
1693
+ // event, which would defeat the guard.
1682
1694
  let signature;
1683
1695
  try {
1684
1696
  signature = JSON.stringify([
@@ -1704,6 +1716,9 @@ export function embedApp(
1704
1716
  if (openUrl || openStartUrl) {
1705
1717
  void launchEmbed();
1706
1718
  } else if (!appFrame) {
1719
+ // No open URL yet — a content-less result no longer reaches the host as
1720
+ // a widget (build-server marks it isError), so this is the transient
1721
+ // "waiting for the embed to resolve" state, not an empty box.
1707
1722
  setMessage("Waiting for app result");
1708
1723
  }
1709
1724
  return true;
@@ -374,6 +374,8 @@ interface DesignCanvasProps {
374
374
  designTitle?: string;
375
375
  /** Stable id for comment pins, usually scoped to the active screen. */
376
376
  commentContextId?: string;
377
+ /** Stable id of the screen containing this canvas when rendered in overview. */
378
+ screenId?: string;
377
379
  /** Human-readable label for comment-pin prompts. */
378
380
  commentContextLabel?: string;
379
381
  /**
@@ -506,6 +508,7 @@ export function DesignCanvas({
506
508
  designId,
507
509
  designTitle,
508
510
  commentContextId,
511
+ screenId,
509
512
  commentContextLabel,
510
513
  onPrototypeNavigate,
511
514
  motionTracks,
@@ -1098,7 +1101,10 @@ export function DesignCanvas({
1098
1101
  const beginTextEdit = useCallback((nodeId: string) => {
1099
1102
  const iframe = iframeRef.current;
1100
1103
  if (!iframe?.contentWindow || !nodeId) return;
1101
- iframe.contentWindow.postMessage({ type: "begin-text-edit", nodeId }, "*");
1104
+ iframe.contentWindow.postMessage(
1105
+ { type: "begin-text-edit", nodeId, force: true },
1106
+ "*",
1107
+ );
1102
1108
  }, []);
1103
1109
 
1104
1110
  const sendStyleChange = useCallback(
@@ -1342,6 +1348,7 @@ export function DesignCanvas({
1342
1348
  : "allow-scripts allow-popups allow-popups-to-escape-sandbox allow-same-origin"
1343
1349
  }
1344
1350
  data-design-preview-iframe
1351
+ data-screen-iframe-id={screenId ?? undefined}
1345
1352
  data-design-source-type={
1346
1353
  sourceType ??
1347
1354
  (externalPreviewUrl
@@ -1000,7 +1000,11 @@ function LayerRow({
1000
1000
  const hideable = node.hideable !== false && Boolean(onToggleHidden);
1001
1001
  const dragEligible = selectable && !node.locked && !node.hidden;
1002
1002
  const draggable = dragEligible && Boolean(onMoveLayer);
1003
- const canDropInside = layerCanDropInside(node, hasChildren);
1003
+ const canDropInside = layerCanDropInside(
1004
+ node,
1005
+ hasChildren,
1006
+ canAcceptChildren,
1007
+ );
1004
1008
  const activeDrop =
1005
1009
  dropIndicator?.targetId === node.id ? dropIndicator.placement : null;
1006
1010
  // Tracks whether the user pressed Escape to cancel rename so that the
@@ -1868,9 +1872,14 @@ function ElementLayerGlyph({ className }: { className?: string }) {
1868
1872
  );
1869
1873
  }
1870
1874
 
1871
- function layerCanDropInside(node: LayersPanelNode, hasChildren: boolean) {
1875
+ function layerCanDropInside(
1876
+ node: LayersPanelNode,
1877
+ hasChildren: boolean,
1878
+ canAcceptChildren: boolean,
1879
+ ) {
1872
1880
  return (
1873
1881
  hasChildren ||
1882
+ canAcceptChildren ||
1874
1883
  Boolean(node.layout?.isFlexContainer || node.layout?.isGridContainer) ||
1875
1884
  node.type === "file" ||
1876
1885
  node.type === "screen" ||
@@ -3365,15 +3365,26 @@ export function MultiScreenCanvas({
3365
3365
  [flushPendingWheelGesture],
3366
3366
  );
3367
3367
 
3368
- const handleWheelEvent = useCallback(
3369
- (event: WheelEvent) => {
3368
+ const enqueueWheelGestureFromClient = useCallback(
3369
+ (args: {
3370
+ deltaX: number;
3371
+ deltaY: number;
3372
+ deltaMode: number;
3373
+ clientX: number;
3374
+ clientY: number;
3375
+ ctrlKey: boolean;
3376
+ metaKey: boolean;
3377
+ shiftKey: boolean;
3378
+ }) => {
3370
3379
  const rect = surfaceRef.current?.getBoundingClientRect();
3371
3380
  if (!rect) return;
3372
- event.preventDefault();
3373
- event.stopPropagation();
3374
- const delta = getWheelDelta(event);
3381
+ const delta = getWheelDeltaFromValues(
3382
+ args.deltaX,
3383
+ args.deltaY,
3384
+ args.deltaMode,
3385
+ );
3375
3386
 
3376
- if (event.ctrlKey || event.metaKey) {
3387
+ if (args.ctrlKey || args.metaKey) {
3377
3388
  const zoomDeltaY = clamp(
3378
3389
  delta.y,
3379
3390
  -MAX_WHEEL_ZOOM_DELTA,
@@ -3382,20 +3393,23 @@ export function MultiScreenCanvas({
3382
3393
  enqueueWheelGesture({
3383
3394
  mode: "zoom",
3384
3395
  deltaY: zoomDeltaY,
3385
- cursor: { x: event.clientX - rect.left, y: event.clientY - rect.top },
3386
- clientX: event.clientX,
3387
- clientY: event.clientY,
3396
+ cursor: {
3397
+ x: args.clientX - rect.left,
3398
+ y: args.clientY - rect.top,
3399
+ },
3400
+ clientX: args.clientX,
3401
+ clientY: args.clientY,
3388
3402
  });
3389
3403
  return;
3390
3404
  }
3391
3405
 
3392
3406
  const deltaX = clamp(
3393
- event.shiftKey && delta.x === 0 ? delta.y : delta.x,
3407
+ args.shiftKey && delta.x === 0 ? delta.y : delta.x,
3394
3408
  -MAX_WHEEL_PAN_DELTA,
3395
3409
  MAX_WHEEL_PAN_DELTA,
3396
3410
  );
3397
3411
  const deltaY = clamp(
3398
- event.shiftKey && delta.x === 0 ? 0 : delta.y,
3412
+ args.shiftKey && delta.x === 0 ? 0 : delta.y,
3399
3413
  -MAX_WHEEL_PAN_DELTA,
3400
3414
  MAX_WHEEL_PAN_DELTA,
3401
3415
  );
@@ -3404,6 +3418,24 @@ export function MultiScreenCanvas({
3404
3418
  [enqueueWheelGesture],
3405
3419
  );
3406
3420
 
3421
+ const handleWheelEvent = useCallback(
3422
+ (event: WheelEvent) => {
3423
+ event.preventDefault();
3424
+ event.stopPropagation();
3425
+ enqueueWheelGestureFromClient({
3426
+ deltaX: event.deltaX,
3427
+ deltaY: event.deltaY,
3428
+ deltaMode: event.deltaMode,
3429
+ clientX: event.clientX,
3430
+ clientY: event.clientY,
3431
+ ctrlKey: event.ctrlKey,
3432
+ metaKey: event.metaKey,
3433
+ shiftKey: event.shiftKey,
3434
+ });
3435
+ },
3436
+ [enqueueWheelGestureFromClient],
3437
+ );
3438
+
3407
3439
  useEffect(() => {
3408
3440
  const surface = surfaceRef.current;
3409
3441
  if (!surface) return;
@@ -3418,6 +3450,44 @@ export function MultiScreenCanvas({
3418
3450
  };
3419
3451
  }, [handleWheelEvent]);
3420
3452
 
3453
+ useEffect(() => {
3454
+ const handleEmbeddedWheelMessage = (event: MessageEvent) => {
3455
+ if (!event.data || event.data.type !== "embedded-canvas-wheel") return;
3456
+ const surface = surfaceRef.current;
3457
+ if (!surface) return;
3458
+ const sourceIframe = Array.from(
3459
+ surface.querySelectorAll<HTMLIFrameElement>(
3460
+ "iframe[data-design-preview-iframe]",
3461
+ ),
3462
+ ).find((iframe) => iframe.contentWindow === event.source);
3463
+ if (!sourceIframe) return;
3464
+
3465
+ const rect = sourceIframe.getBoundingClientRect();
3466
+ const scaleX =
3467
+ sourceIframe.clientWidth > 0
3468
+ ? rect.width / sourceIframe.clientWidth
3469
+ : 1;
3470
+ const scaleY =
3471
+ sourceIframe.clientHeight > 0
3472
+ ? rect.height / sourceIframe.clientHeight
3473
+ : 1;
3474
+ enqueueWheelGestureFromClient({
3475
+ deltaX: Number(event.data.deltaX) || 0,
3476
+ deltaY: Number(event.data.deltaY) || 0,
3477
+ deltaMode: Number(event.data.deltaMode) || WheelEvent.DOM_DELTA_PIXEL,
3478
+ clientX: rect.left + (Number(event.data.clientX) || 0) * scaleX,
3479
+ clientY: rect.top + (Number(event.data.clientY) || 0) * scaleY,
3480
+ ctrlKey: Boolean(event.data.ctrlKey),
3481
+ metaKey: Boolean(event.data.metaKey),
3482
+ shiftKey: Boolean(event.data.shiftKey),
3483
+ });
3484
+ };
3485
+
3486
+ window.addEventListener("message", handleEmbeddedWheelMessage);
3487
+ return () =>
3488
+ window.removeEventListener("message", handleEmbeddedWheelMessage);
3489
+ }, [enqueueWheelGestureFromClient]);
3490
+
3421
3491
  useEffect(() => {
3422
3492
  const handleKeyDown = (event: KeyboardEvent) => {
3423
3493
  if (activePenPathRef.current) return;
@@ -6060,12 +6130,15 @@ function getSelectableBounds(geometry: FrameGeometry): BoundsRect {
6060
6130
  };
6061
6131
  }
6062
6132
 
6063
- function getWheelDelta(event: WheelEvent) {
6064
- const multiplier =
6065
- event.deltaMode === 1 ? 16 : event.deltaMode === 2 ? 800 : 1;
6133
+ function getWheelDeltaFromValues(
6134
+ deltaX: number,
6135
+ deltaY: number,
6136
+ deltaMode: number,
6137
+ ) {
6138
+ const multiplier = deltaMode === 1 ? 16 : deltaMode === 2 ? 800 : 1;
6066
6139
  return {
6067
- x: event.deltaX * multiplier,
6068
- y: event.deltaY * multiplier,
6140
+ x: deltaX * multiplier,
6141
+ y: deltaY * multiplier,
6069
6142
  };
6070
6143
  }
6071
6144
 
@@ -3468,17 +3468,20 @@ declare var __EDITOR_CHROME_SCALE_Y__: string;
3468
3468
  }
3469
3469
  }
3470
3470
 
3471
- function beginTextEditingFromEvent(e) {
3471
+ function beginTextEditingFromEvent(e, forceTextEditing) {
3472
3472
  if (activeTextEditEl && e.target && activeTextEditEl.contains(e.target))
3473
3473
  return;
3474
- if (!textEditingEnabled) {
3474
+ if (!textEditingEnabled && !forceTextEditing) {
3475
3475
  stopNativeInteraction(e);
3476
3476
  return;
3477
3477
  }
3478
3478
  stopNativeInteraction(e);
3479
- var target = findTextEditTarget(
3480
- elementFromEditorPoint(e.clientX, e.clientY),
3481
- );
3479
+ var eventTarget =
3480
+ e && e.target && e.target.nodeType === 1 ? e.target : null;
3481
+ var target =
3482
+ findTextEditTarget(elementFromEditorPoint(e.clientX, e.clientY)) ||
3483
+ findTextEditTarget(eventTarget) ||
3484
+ eventTarget;
3482
3485
  if (!target || target.nodeType !== 1) return;
3483
3486
  // Anchor the selection identity to the nearest source-backed element. Text
3484
3487
  // editing still operates on the actual target text node, but a later
@@ -3725,7 +3728,8 @@ declare var __EDITOR_CHROME_SCALE_Y__: string;
3725
3728
  // text-element creation so the user can type right away and the autosize
3726
3729
  // CSS (width:max-content or similar) takes effect from the first keystroke.
3727
3730
  if (e.data.type === "begin-text-edit") {
3728
- if (readOnly || !textEditingEnabled) return;
3731
+ var forceBeginTextEdit = e.data.force === true;
3732
+ if ((readOnly || !textEditingEnabled) && !forceBeginTextEdit) return;
3729
3733
  var nodeId: string =
3730
3734
  typeof e.data.nodeId === "string" ? e.data.nodeId : "";
3731
3735
  if (!nodeId) return;
@@ -3749,14 +3753,17 @@ declare var __EDITOR_CHROME_SCALE_Y__: string;
3749
3753
  var bteCenterY = bteRect.top + bteRect.height / 2;
3750
3754
  // Delegate to the canonical path so all state, events, and postMessages
3751
3755
  // stay consistent with a normal double-click text edit.
3752
- beginTextEditingFromEvent({
3753
- clientX: bteCenterX,
3754
- clientY: bteCenterY,
3755
- target: textTarget,
3756
- preventDefault: function () {},
3757
- stopPropagation: function () {},
3758
- stopImmediatePropagation: function () {},
3759
- } as unknown as MouseEvent);
3756
+ beginTextEditingFromEvent(
3757
+ {
3758
+ clientX: bteCenterX,
3759
+ clientY: bteCenterY,
3760
+ target: textTarget,
3761
+ preventDefault: function () {},
3762
+ stopPropagation: function () {},
3763
+ stopImmediatePropagation: function () {},
3764
+ } as unknown as MouseEvent,
3765
+ forceBeginTextEdit,
3766
+ );
3760
3767
  return;
3761
3768
  }
3762
3769
  if (e.data.type === "set-editor-chrome-scale") {