@agent-native/core 0.182.1 → 0.182.2-nightly-20260917225811

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 (119) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/clips/app/root.tsx +59 -10
  3. package/corpus/templates/clips/changelog/2026-09-17-desktop-popover-closes-when-clicking-away.md +6 -0
  4. package/corpus/templates/clips/changelog/2026-09-17-self-hosted-extension-sign-in.md +6 -0
  5. package/corpus/templates/clips/chrome-extension/src/background.ts +45 -25
  6. package/corpus/templates/clips/chrome-extension/src/content-script.ts +43 -0
  7. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +55 -6
  8. package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +107 -19
  9. package/corpus/templates/design/.generated/bridge/hit-test.generated.ts +1 -0
  10. package/corpus/templates/design/actions/add-localhost-screens.ts +54 -22
  11. package/corpus/templates/design/actions/apply-component-prop-edit.ts +174 -14
  12. package/corpus/templates/design/actions/apply-visual-edit.ts +3 -1
  13. package/corpus/templates/design/actions/create-component.ts +173 -12
  14. package/corpus/templates/design/actions/create-design-from-template.ts +50 -35
  15. package/corpus/templates/design/actions/create-design-version.ts +21 -0
  16. package/corpus/templates/design/actions/create-file.ts +35 -33
  17. package/corpus/templates/design/actions/delete-design.ts +29 -28
  18. package/corpus/templates/design/actions/delete-file.ts +154 -23
  19. package/corpus/templates/design/actions/generate-design.ts +28 -16
  20. package/corpus/templates/design/actions/get-component-details.ts +272 -33
  21. package/corpus/templates/design/actions/get-design-version.ts +9 -1
  22. package/corpus/templates/design/actions/go-to-main-component.ts +37 -35
  23. package/corpus/templates/design/actions/index-components.ts +241 -126
  24. package/corpus/templates/design/actions/migrate-board-objects-to-file.ts +12 -9
  25. package/corpus/templates/design/actions/present-design-variants.ts +68 -72
  26. package/corpus/templates/design/actions/rename-component.ts +294 -0
  27. package/corpus/templates/design/actions/rename-screen.ts +10 -10
  28. package/corpus/templates/design/actions/update-file.ts +327 -330
  29. package/corpus/templates/design/actions/update-screen-source.ts +2 -43
  30. package/corpus/templates/design/app/components/design/EditPanel.tsx +17 -1
  31. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +228 -24
  32. package/corpus/templates/design/app/components/design/ResponsiveInteractBar.tsx +17 -1
  33. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +151 -32
  34. package/corpus/templates/design/app/components/design/bridge/hit-test.bridge.ts +4 -0
  35. package/corpus/templates/design/app/components/design/canvas-primitive-style.ts +1 -1
  36. package/corpus/templates/design/app/components/design/code-workbench/CodeWorkbench.tsx +8 -1
  37. package/corpus/templates/design/app/components/design/code-workbench/explorer/FileTree.tsx +55 -33
  38. package/corpus/templates/design/app/components/design/code-workbench/workspace/create-providers.ts +2 -0
  39. package/corpus/templates/design/app/components/design/code-workbench/workspace/inline-provider.ts +4 -2
  40. package/corpus/templates/design/app/components/design/edit-panel/component-section.tsx +273 -60
  41. package/corpus/templates/design/app/components/design/edit-panel/element-classification.ts +9 -2
  42. package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +57 -9
  43. package/corpus/templates/design/app/components/design/multi-screen/primitive-drop-target.ts +13 -0
  44. package/corpus/templates/design/app/components/design/multi-screen/types.ts +14 -0
  45. package/corpus/templates/design/app/components/design/types.ts +9 -1
  46. package/corpus/templates/design/app/hooks/use-design-versions.ts +3 -2
  47. package/corpus/templates/design/app/i18n-breakpoints.ts +0 -110
  48. package/corpus/templates/design/app/i18n-data.ts +0 -10
  49. package/corpus/templates/design/app/pages/DesignEditor.tsx +669 -161
  50. package/corpus/templates/design/app/pages/design-editor/clone-and-pen-edit.ts +20 -0
  51. package/corpus/templates/design/app/pages/design-editor/code-layer-state.ts +2 -0
  52. package/corpus/templates/design/app/pages/design-editor/commands/apply-design-editor-command.ts +21 -3
  53. package/corpus/templates/design/app/pages/design-editor/commands/apply-file-content-update.ts +2 -0
  54. package/corpus/templates/design/app/pages/design-editor/commands/create-component.ts +44 -3
  55. package/corpus/templates/design/app/pages/design-editor/commands/delete-files.ts +69 -47
  56. package/corpus/templates/design/app/pages/design-editor/commands/duplicate-screen.ts +157 -52
  57. package/corpus/templates/design/app/pages/design-editor/commands/duplicate-selection.ts +76 -1
  58. package/corpus/templates/design/app/pages/design-editor/commands/redo.ts +152 -44
  59. package/corpus/templates/design/app/pages/design-editor/commands/save-file-content.ts +131 -2
  60. package/corpus/templates/design/app/pages/design-editor/commands/screen-visual-duplicate-change.ts +5 -1
  61. package/corpus/templates/design/app/pages/design-editor/commands/undo.ts +26 -1
  62. package/corpus/templates/design/app/pages/design-editor/commands/visual-duplicate-change.ts +1 -0
  63. package/corpus/templates/design/app/pages/design-editor/editor-helpers.ts +58 -8
  64. package/corpus/templates/design/app/pages/design-editor/editor-state.ts +63 -2
  65. package/corpus/templates/design/app/pages/design-editor/history.ts +3 -0
  66. package/corpus/templates/design/app/pages/design-editor/nudge-intent.ts +74 -22
  67. package/corpus/templates/design/app/pages/design-editor/source-publication.ts +14 -2
  68. package/corpus/templates/design/changelog/2026-09-16-design-components-retain-linked-identities-when-edited-and-d.md +6 -0
  69. package/corpus/templates/design/changelog/2026-09-16-fix-design-nested-layer-dragging-and-screen-duplication-pari.md +6 -0
  70. package/corpus/templates/design/changelog/2026-09-17-design-drag-and-drop-keeps-duplicates-cross-screen-moves-and.md +6 -0
  71. package/corpus/templates/design/changelog/2026-09-17-design-preserves-reusable-component-identity-and-props-acros.md +6 -0
  72. package/corpus/templates/design/changelog/2026-09-17-open-vector-paths-no-longer-show-fills-and-interact-dimensio.md +6 -0
  73. package/corpus/templates/design/changelog/2026-09-17-screen-deletion-is-immediate-and-undoable-with-history-that-.md +6 -0
  74. package/corpus/templates/design/e2e/fixtures/design-component-react/index.html +98 -0
  75. package/corpus/templates/design/e2e/fixtures/design-component-react/inline-component.html +65 -0
  76. package/corpus/templates/design/e2e/helpers.ts +6 -1
  77. package/corpus/templates/design/server/lib/design-data-mutation.ts +9 -5
  78. package/corpus/templates/design/server/lib/design-snapshot.ts +28 -16
  79. package/corpus/templates/design/server/lib/design-versions.ts +208 -130
  80. package/corpus/templates/design/server/lib/fusion-screens.ts +46 -20
  81. package/corpus/templates/design/server/lib/import-design-files.ts +5 -1
  82. package/corpus/templates/design/server/lib/localhost-connection.ts +54 -0
  83. package/corpus/templates/design/server/source-workspace.ts +412 -356
  84. package/corpus/templates/design/shared/component-library.ts +36 -6
  85. package/corpus/templates/design/shared/component-model.ts +12 -2
  86. package/corpus/templates/design/shared/local-jsx-visual-edit.ts +231 -5
  87. package/corpus/templates/design/shared/source-mode.ts +22 -0
  88. package/dist/automation/index.d.ts +1 -1
  89. package/dist/client/AssistantChat.d.ts +4 -1
  90. package/dist/client/AssistantChat.js +26 -3
  91. package/dist/client/chat/agentkit-protocol.js +148 -34
  92. package/dist/client/chat/message-components.js +7 -5
  93. package/dist/client/chat/tool-call-display.js +4 -3
  94. package/dist/client/conversation/AgentConversation.js +4 -2
  95. package/dist/client/tool-display.d.ts +9 -0
  96. package/dist/client/tool-display.js +15 -0
  97. package/dist/collab/awareness.d.ts +2 -2
  98. package/dist/collab/routes.d.ts +1 -1
  99. package/dist/localization/core-messages/ar-SA.js +2 -0
  100. package/dist/localization/core-messages/de-DE.js +2 -0
  101. package/dist/localization/core-messages/en-US.d.ts +2 -0
  102. package/dist/localization/core-messages/en-US.js +2 -0
  103. package/dist/localization/core-messages/es-ES.js +2 -0
  104. package/dist/localization/core-messages/fr-FR.js +2 -0
  105. package/dist/localization/core-messages/hi-IN.js +2 -0
  106. package/dist/localization/core-messages/ja-JP.js +2 -0
  107. package/dist/localization/core-messages/ko-KR.js +2 -0
  108. package/dist/localization/core-messages/pt-BR.js +2 -0
  109. package/dist/localization/core-messages/zh-CN.js +2 -0
  110. package/dist/localization/core-messages/zh-TW.js +2 -0
  111. package/dist/localization/core-messages.d.ts +2 -0
  112. package/dist/notifications/routes.d.ts +3 -3
  113. package/dist/observability/routes.d.ts +3 -3
  114. package/dist/resources/handlers.d.ts +1 -1
  115. package/dist/secrets/routes.d.ts +3 -3
  116. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  117. package/dist/server/core-routes-plugin.d.ts +1 -1
  118. package/package.json +3 -3
  119. package/corpus/templates/design/app/components/design/editor/PendingScreenDeletionDialog.tsx +0 -66
@@ -10,7 +10,7 @@ import {
10
10
  } from "../server/lib/design-data-mutation.js";
11
11
  import { snapshotDesignBeforeAgentEdit } from "../server/lib/design-versions.js";
12
12
  import {
13
- localhostBridgeRequestError,
13
+ fetchLocalhostSnapshot,
14
14
  resolveLocalhostConnectionScope,
15
15
  } from "../server/lib/localhost-connection.js";
16
16
  import {
@@ -113,47 +113,6 @@ export function screenSourceMetadataForStatic(
113
113
  return next;
114
114
  }
115
115
 
116
- async function fetchFallbackSnapshot(args: {
117
- bridgeUrl: string;
118
- previewToken: string | null;
119
- url: string;
120
- }): Promise<string> {
121
- if (!args.previewToken) {
122
- throw new Error(
123
- "This URL-backed screen has no preview token. Reload the frame or reconnect the localhost app before switching it to static HTML.",
124
- );
125
- }
126
- const endpoint = new URL("/snapshot", args.bridgeUrl);
127
- endpoint.searchParams.set("url", args.url);
128
- let response: Response;
129
- try {
130
- response = await fetch(endpoint, {
131
- headers: {
132
- accept: "application/json",
133
- "x-design-preview-token": args.previewToken,
134
- },
135
- });
136
- } catch (error) {
137
- throw new Error(
138
- `Could not reach the localhost bridge for snapshot (${error instanceof Error ? error.message : String(error)}).`,
139
- );
140
- }
141
- if (!response.ok) {
142
- const errorText = await response.text().catch(() => response.statusText);
143
- throw localhostBridgeRequestError("snapshot", response.status, errorText);
144
- }
145
- let payload: { html?: unknown } | null;
146
- try {
147
- payload = (await response.json()) as { html?: unknown } | null;
148
- } catch {
149
- throw new Error("The localhost bridge returned invalid snapshot JSON.");
150
- }
151
- if (!payload || typeof payload.html !== "string") {
152
- throw new Error("The localhost bridge returned no HTML snapshot.");
153
- }
154
- return payload.html;
155
- }
156
-
157
116
  export default defineAction({
158
117
  description:
159
118
  "Change one Design screen between static HTML and a live localhost URL. " +
@@ -348,7 +307,7 @@ export default defineAction({
348
307
  }
349
308
  const rawSnapshot =
350
309
  snapshotHtml ??
351
- (await fetchFallbackSnapshot({
310
+ (await fetchLocalhostSnapshot({
352
311
  bridgeUrl: connection.bridgeUrl,
353
312
  previewToken: connection.previewToken,
354
313
  url: currentUrl,
@@ -72,7 +72,10 @@ import {
72
72
  truncateOpeningTag,
73
73
  vscodeDeepLink,
74
74
  } from "./edit-panel/code-inspect-helpers";
75
- import { ComponentSection } from "./edit-panel/component-section";
75
+ import {
76
+ ComponentSection,
77
+ type RuntimeComponentDetails,
78
+ } from "./edit-panel/component-section";
76
79
  import {
77
80
  type DocumentColorSourceFile,
78
81
  type SelectionColorValue,
@@ -188,6 +191,7 @@ import {
188
191
  } from "./inspector";
189
192
  import { IconText } from "./inspector/design-icons";
190
193
  import { type GlslShaderPanelContext } from "./inspector/GlslShaderPanel";
194
+ import type { LocalhostWriteConsentPayload } from "./LocalhostWriteConsentDialog";
191
195
  import { getActiveScreenIframeId } from "./multi-screen/iframe-targeting";
192
196
  import type { ScreenHeightMode } from "./multi-screen/screen-height";
193
197
  import {
@@ -452,6 +456,14 @@ interface EditPanelProps {
452
456
  * and an Edit component action.
453
457
  */
454
458
  componentNodeId?: string;
459
+ /** Runtime component metadata for URL-backed React selections. */
460
+ componentRuntime?: RuntimeComponentDetails;
461
+ /** Request the existing localhost write-consent dialog before source writes. */
462
+ requestLocalhostWrite?: (opts: {
463
+ files: string[];
464
+ onGranted: LocalhostWriteConsentPayload["onGranted"];
465
+ onCancel?: () => void;
466
+ }) => void;
455
467
  /** True when the selected component node has reached the accepted source. */
456
468
  componentDetailsReady?: boolean;
457
469
  /** True when the selected linked component instance stores local overrides. */
@@ -2429,6 +2441,8 @@ export const EditPanel = memo(function EditPanel({
2429
2441
  reviewCommentsPanelProps,
2430
2442
  reviewCommentsCount = 0,
2431
2443
  componentNodeId,
2444
+ componentRuntime,
2445
+ requestLocalhostWrite,
2432
2446
  componentDetailsReady = true,
2433
2447
  componentInstanceHasLocalOverrides = false,
2434
2448
  onResetComponentInstanceOverrides,
@@ -3064,6 +3078,8 @@ export const EditPanel = memo(function EditPanel({
3064
3078
  activeFileUpdatedAt={activeFileUpdatedAt}
3065
3079
  componentDetailsReady={componentDetailsReady}
3066
3080
  nodeId={componentNodeId}
3081
+ runtime={componentRuntime}
3082
+ requestLocalhostWrite={requestLocalhostWrite}
3067
3083
  hasLocalOverrides={componentInstanceHasLocalOverrides}
3068
3084
  swapPickerRequest={componentSwapPickerRequest}
3069
3085
  onResetOverrides={
@@ -582,6 +582,7 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
582
582
  getScreenMetadata,
583
583
  onDuplicate,
584
584
  geometryById,
585
+ geometryOverridesById,
585
586
  onGeometryChange,
586
587
  onGeometryCommit,
587
588
  onBreakpointContentHeightChange,
@@ -649,6 +650,9 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
649
650
  gradientEditTarget,
650
651
  onDropFiles,
651
652
  cameraCommand,
653
+ suppressLineupRecenter,
654
+ preserveCameraOnScreenCountChange = false,
655
+ deferLineupZoomChange = false,
652
656
  chromeInsetLeft = 0,
653
657
  chromeInsetRight = 0,
654
658
  }: MultiScreenCanvasProps) {
@@ -680,6 +684,7 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
680
684
  const [surfaceSize, setSurfaceSize] = useState({ width: 0, height: 0 });
681
685
  const [frameGeometry, setFrameGeometry] = useState<FrameGeometryById>({});
682
686
  const frameGeometryRef = useRef(frameGeometry);
687
+ const renderedScreenIdsRef = useRef<Set<string>>(new Set());
683
688
  // Resize commits must start from what the user sees, including content-fit
684
689
  // height, because the parent pins the committed viewport dimensions.
685
690
  const renderedFrameGeometryRef = useRef<FrameGeometryById>({});
@@ -982,6 +987,7 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
982
987
  const lineupRecenterSuppressRef = useRef<LineupRecenterDuplicateArm | null>(
983
988
  null,
984
989
  );
990
+ const lastSuppressLineupRecenterNonceRef = useRef<number | null>(null);
985
991
  const lineupRecenterDeviceFrameRef = useRef(previewDeviceFrame);
986
992
  const lineupRecenterPrevCountRef = useRef<number | null>(null);
987
993
  const handledSelectAllRequestRef = useRef(selectAllRequest);
@@ -1095,9 +1101,13 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
1095
1101
  id: string;
1096
1102
  geometry: FrameGeometry;
1097
1103
  }
1098
- /** Last claim value sent to the source iframe, so a move tick that changes
1099
- * nothing does not spam postMessage. Reset when a drag ends. */
1100
- const crossScreenClaimSentRef = useRef<boolean | null>(null);
1104
+ /** Last claim sent to the source iframe, so a move tick that changes
1105
+ * nothing does not spam postMessage. Keep the source id with it so a
1106
+ * cleared claim can be revoked after the payload is gone. */
1107
+ const crossScreenClaimSentRef = useRef<{
1108
+ sourceScreenId: string;
1109
+ claimed: boolean;
1110
+ } | null>(null);
1101
1111
  const crossScreenProxyTraceRef = useRef<string | null>(null);
1102
1112
  const crossScreenResolveTraceRef = useRef<string | null>(null);
1103
1113
  const [crossScreenGhost, setCrossScreenGhost] =
@@ -1709,7 +1719,7 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
1709
1719
  zoomRef.current = canvasZoom;
1710
1720
  }, [canvasZoom]);
1711
1721
 
1712
- useEffect(() => {
1722
+ useLayoutEffect(() => {
1713
1723
  frameGeometryRef.current = frameGeometry;
1714
1724
  }, [frameGeometry]);
1715
1725
 
@@ -1918,7 +1928,7 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
1918
1928
  zoom,
1919
1929
  ]);
1920
1930
 
1921
- useEffect(() => {
1931
+ useLayoutEffect(() => {
1922
1932
  const selectableIds = new Set(selectableScreens.map((screen) => screen.id));
1923
1933
  // B5-9: see resolveFrameGeometrySync's doc comment — this used to notify
1924
1934
  // the parent (onGeometryChange -> queueFrameGeometrySave) with a brand
@@ -1959,6 +1969,10 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
1959
1969
  updateSelectedIds,
1960
1970
  ]);
1961
1971
 
1972
+ useLayoutEffect(() => {
1973
+ renderedScreenIdsRef.current = new Set(screens.map((screen) => screen.id));
1974
+ }, [screens]);
1975
+
1962
1976
  useEffect(() => {
1963
1977
  const previous = previousPreviewDeviceFrameRef.current;
1964
1978
  previousPreviewDeviceFrameRef.current = previewDeviceFrame;
@@ -2030,8 +2044,25 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
2030
2044
  setTransformBadge(null);
2031
2045
  }, [clearSelectionRequest, updateSelectedDraftIds, updateSelectedIds]);
2032
2046
 
2047
+ useEffect(() => {
2048
+ if (
2049
+ !suppressLineupRecenter ||
2050
+ lastSuppressLineupRecenterNonceRef.current ===
2051
+ suppressLineupRecenter.nonce
2052
+ ) {
2053
+ return;
2054
+ }
2055
+ lastSuppressLineupRecenterNonceRef.current = suppressLineupRecenter.nonce;
2056
+ lineupRecenterSuppressRef.current = {
2057
+ atMs: Date.now(),
2058
+ fromCount: suppressLineupRecenter.fromCount,
2059
+ addedCount: suppressLineupRecenter.addedCount,
2060
+ };
2061
+ }, [suppressLineupRecenter]);
2062
+
2033
2063
  // Center the lineup when the screen footprint changes so new frames stay reachable.
2034
2064
  useEffect(() => {
2065
+ if (preserveCameraOnScreenCountChange) return;
2035
2066
  const deviceFrameChanged =
2036
2067
  lineupRecenterDeviceFrameRef.current !== previewDeviceFrame;
2037
2068
  lineupRecenterDeviceFrameRef.current = previewDeviceFrame;
@@ -2166,8 +2197,10 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
2166
2197
  totalHeight > 0
2167
2198
  ? Math.max(minFitScale, (rect.height - 96) / totalHeight)
2168
2199
  : scale;
2169
- const nextScale = Math.min(scale, widthFitScale, heightFitScale);
2170
- if (nextScale < scale) {
2200
+ const nextScale = deferLineupZoomChange
2201
+ ? scale
2202
+ : Math.min(scale, widthFitScale, heightFitScale);
2203
+ if (!deferLineupZoomChange && nextScale < scale) {
2171
2204
  const nextZoom = nextScale * 100;
2172
2205
  zoomRef.current = nextZoom;
2173
2206
  setCanvasZoom(nextZoom);
@@ -2192,7 +2225,14 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
2192
2225
  };
2193
2226
  // Only on mount, screen-count or chrome-inset changes, or device-preview changes.
2194
2227
  // eslint-disable-next-line react-hooks/exhaustive-deps
2195
- }, [chromeInsetLeft, chromeInsetRight, previewDeviceFrame, screens.length]);
2228
+ }, [
2229
+ chromeInsetLeft,
2230
+ chromeInsetRight,
2231
+ preserveCameraOnScreenCountChange,
2232
+ deferLineupZoomChange,
2233
+ previewDeviceFrame,
2234
+ screens.length,
2235
+ ]);
2196
2236
 
2197
2237
  useEffect(() => {
2198
2238
  return () => {
@@ -2556,9 +2596,30 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
2556
2596
  crossScreenParentDragCleanupRef.current = null;
2557
2597
  };
2558
2598
 
2599
+ const postCrossScreenClaim = (sourceScreenId: string, claimed: boolean) => {
2600
+ const sourceScreen = screensRef.current.find(
2601
+ (item) => item.id === sourceScreenId,
2602
+ );
2603
+ const iframeId = sourceScreen
2604
+ ? getActiveScreenIframeId(sourceScreen)
2605
+ : sourceScreenId;
2606
+ findCanvasIframeForScreen(
2607
+ surfaceRef.current,
2608
+ iframeId,
2609
+ boardFileId,
2610
+ )?.contentWindow?.postMessage(
2611
+ { type: "agent-native:cross-screen-claim", claimed },
2612
+ "*",
2613
+ );
2614
+ };
2615
+
2559
2616
  const clearCrossScreenDrag = () => {
2560
2617
  stopParentCrossScreenDrag();
2561
2618
  clearCrossScreenPreviewGuide();
2619
+ const previousClaim = crossScreenClaimSentRef.current;
2620
+ if (previousClaim?.claimed) {
2621
+ postCrossScreenClaim(previousClaim.sourceScreenId, false);
2622
+ }
2562
2623
  crossScreenClaimSentRef.current = null;
2563
2624
  crossScreenProxyTraceRef.current = null;
2564
2625
  crossScreenResolveTraceRef.current = null;
@@ -2897,22 +2958,15 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
2897
2958
  * knows where the screens are, and the board surface iframe covers all of
2898
2959
  * them, so without this both sides commit the same gesture. */
2899
2960
  const claimCrossScreenDrop = (sourceScreenId: string, claimed: boolean) => {
2900
- if (crossScreenClaimSentRef.current === claimed) return;
2901
- crossScreenClaimSentRef.current = claimed;
2902
- const sourceScreen = screensRef.current.find(
2903
- (item) => item.id === sourceScreenId,
2904
- );
2905
- const iframeId = sourceScreen
2906
- ? getActiveScreenIframeId(sourceScreen)
2907
- : sourceScreenId;
2908
- findCanvasIframeForScreen(
2909
- surfaceRef.current,
2910
- iframeId,
2911
- boardFileId,
2912
- )?.contentWindow?.postMessage(
2913
- { type: "agent-native:cross-screen-claim", claimed },
2914
- "*",
2915
- );
2961
+ const previousClaim = crossScreenClaimSentRef.current;
2962
+ if (
2963
+ previousClaim?.sourceScreenId === sourceScreenId &&
2964
+ previousClaim.claimed === claimed
2965
+ ) {
2966
+ return;
2967
+ }
2968
+ crossScreenClaimSentRef.current = { sourceScreenId, claimed };
2969
+ postCrossScreenClaim(sourceScreenId, claimed);
2916
2970
  };
2917
2971
 
2918
2972
  const updateCrossScreenTargetFromBoardPoint = (
@@ -3465,6 +3519,7 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
3465
3519
  // every screen frame, so board-origin drags must still be checked
3466
3520
  // against screen drop targets while technically "inside" the source.
3467
3521
  if (pointerInsideSourceIframe && !sourceIsBoard) {
3522
+ claimCrossScreenDrop(sourceScreenId, false);
3468
3523
  clearCrossScreenPreviewGuide();
3469
3524
  setCrossScreenGhost(null);
3470
3525
  setCrossScreenTarget(null);
@@ -6715,6 +6770,7 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
6715
6770
  mode: "alt-drag",
6716
6771
  screen: target.screen,
6717
6772
  canvasPosition,
6773
+ preserveCamera: true,
6718
6774
  canvasOffset: {
6719
6775
  x: dropCanvasPosition.x - canvasPosition.x,
6720
6776
  y: dropCanvasPosition.y - canvasPosition.y,
@@ -7640,6 +7696,133 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
7640
7696
  ],
7641
7697
  );
7642
7698
 
7699
+ const beginBoardElementDrag = useCallback(
7700
+ (e: React.MouseEvent) => {
7701
+ if (readOnly) return;
7702
+ if (e.button !== 0) return;
7703
+ if (!boardFileId || !boardSurfaceRenderGeometry) return;
7704
+ const iframe = findCanvasIframeForScreen(
7705
+ surfaceRef.current,
7706
+ boardFileId,
7707
+ boardFileId,
7708
+ );
7709
+ const iframeDoc = iframe?.contentWindow?.document;
7710
+ if (!iframeDoc) {
7711
+ dndHostLog("board-move:no-iframe", { boardFileId });
7712
+ return;
7713
+ }
7714
+ const selectionOverlay = iframeDoc.querySelector<HTMLElement>(
7715
+ '[data-agent-native-edit-overlay="selection"]',
7716
+ );
7717
+ if (!selectionOverlay) {
7718
+ dndHostLog("board-move:no-selection-overlay", { boardFileId });
7719
+ return;
7720
+ }
7721
+ e.preventDefault();
7722
+ e.stopPropagation();
7723
+
7724
+ const toIframePoint = (clientX: number, clientY: number) =>
7725
+ boardPointToBoardSurfaceLocalPoint(
7726
+ getCanvasPoint(clientX, clientY),
7727
+ boardSurfaceRenderGeometryRef.current ?? boardSurfaceRenderGeometry,
7728
+ );
7729
+ const dispatchAt = (
7730
+ target: EventTarget,
7731
+ type: string,
7732
+ point: { x: number; y: number },
7733
+ source: {
7734
+ shiftKey: boolean;
7735
+ altKey: boolean;
7736
+ metaKey: boolean;
7737
+ ctrlKey: boolean;
7738
+ },
7739
+ buttons: number,
7740
+ ) => {
7741
+ target.dispatchEvent(
7742
+ new MouseEvent(type, {
7743
+ clientX: point.x,
7744
+ clientY: point.y,
7745
+ shiftKey: source.shiftKey,
7746
+ altKey: source.altKey,
7747
+ metaKey: source.metaKey,
7748
+ ctrlKey: source.ctrlKey,
7749
+ buttons,
7750
+ bubbles: true,
7751
+ cancelable: true,
7752
+ }),
7753
+ );
7754
+ };
7755
+
7756
+ setIsDragging(true);
7757
+ dispatchAt(
7758
+ selectionOverlay,
7759
+ "mousedown",
7760
+ toIframePoint(e.clientX, e.clientY),
7761
+ e,
7762
+ 1,
7763
+ );
7764
+
7765
+ const handleMouseMove = (ev: MouseEvent) => {
7766
+ dispatchAt(
7767
+ iframeDoc,
7768
+ "mousemove",
7769
+ toIframePoint(ev.clientX, ev.clientY),
7770
+ ev,
7771
+ 1,
7772
+ );
7773
+ };
7774
+ const cancelMove = (pressedAt: number) => {
7775
+ iframe.contentWindow?.postMessage(
7776
+ { type: "agent-native:cancel-active-drag", pressedAt },
7777
+ "*",
7778
+ );
7779
+ };
7780
+ const handleMouseUp = (ev: MouseEvent) => {
7781
+ // The host selection box sits above every Screen and follows the
7782
+ // moving board object, so DOM hit testing sees the chrome instead of
7783
+ // the card underneath it. Resolve the release in board-space geometry
7784
+ // so selection chrome cannot steal a board-to-screen drop.
7785
+ const droppedOnScreen = Boolean(
7786
+ getFrameEntryAtPoint(getCanvasPoint(ev.clientX, ev.clientY)),
7787
+ );
7788
+ if (droppedOnScreen) {
7789
+ // The host owns board-to-screen drops. Cancel the iframe gesture
7790
+ // after its cross-screen writer has taken the release.
7791
+ cancelMove(performance.timeOrigin + performance.now());
7792
+ } else {
7793
+ // No screen owns this release, so let the board bridge commit its
7794
+ // ordinary board-to-board move or duplicate.
7795
+ iframe.contentWindow?.postMessage(
7796
+ { type: "agent-native:cross-screen-claim", claimed: false },
7797
+ "*",
7798
+ );
7799
+ dispatchAt(
7800
+ iframeDoc,
7801
+ "mouseup",
7802
+ toIframePoint(ev.clientX, ev.clientY),
7803
+ ev,
7804
+ 0,
7805
+ );
7806
+ }
7807
+ finishDrag();
7808
+ };
7809
+ boardElementResizeCancel.current = cancelMove;
7810
+ installDragListeners(handleMouseMove, handleMouseUp, () => {
7811
+ cancelMove(performance.timeOrigin + performance.now());
7812
+ finishDrag();
7813
+ });
7814
+ },
7815
+ [
7816
+ boardFileId,
7817
+ boardSurfaceRenderGeometry,
7818
+ finishDrag,
7819
+ getCanvasPoint,
7820
+ getFrameEntryAtPoint,
7821
+ installDragListeners,
7822
+ readOnly,
7823
+ ],
7824
+ );
7825
+
7643
7826
  const beginRotate = useCallback(
7644
7827
  (id: string, e: React.MouseEvent) => {
7645
7828
  if (readOnly) return;
@@ -9051,6 +9234,7 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
9051
9234
  mode: "alt-click",
9052
9235
  screen,
9053
9236
  canvasPosition,
9237
+ preserveCamera: true,
9054
9238
  dropCanvasPosition: canvasPosition,
9055
9239
  });
9056
9240
  dispatched += 1;
@@ -9272,8 +9456,25 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
9272
9456
  const next = renderedScreens.map((screen) => {
9273
9457
  nextIds.add(screen.id);
9274
9458
  const metadata = getResolvedMetadata(screen);
9459
+ const geometryOverride = geometryOverridesById?.[screen.id];
9460
+ const persistedGeometry = geometryById?.[screen.id];
9461
+ const persistedGeometryIsComplete =
9462
+ persistedGeometry &&
9463
+ typeof persistedGeometry.x === "number" &&
9464
+ typeof persistedGeometry.y === "number" &&
9465
+ typeof persistedGeometry.width === "number" &&
9466
+ typeof persistedGeometry.height === "number";
9275
9467
  const rawGeometry =
9468
+ geometryOverride ??
9469
+ (persistedGeometryIsComplete &&
9470
+ (!frameGeometry[screen.id] ||
9471
+ !renderedScreenIdsRef.current.has(screen.id))
9472
+ ? (persistedGeometry as FrameGeometry)
9473
+ : undefined) ??
9276
9474
  frameGeometry[screen.id] ??
9475
+ (persistedGeometryIsComplete
9476
+ ? (persistedGeometry as FrameGeometry)
9477
+ : undefined) ??
9277
9478
  getInitialFrameGeometry(screenIndexById.get(screen.id) ?? 0, metadata);
9278
9479
  // Content-fit height, applied ONLY once the frame's own content has been
9279
9480
  // measured, and only for inline (srcdoc) screens — URL-backed
@@ -9334,6 +9535,8 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
9334
9535
  return next;
9335
9536
  }, [
9336
9537
  frameGeometry,
9538
+ geometryById,
9539
+ geometryOverridesById,
9337
9540
  getResolvedMetadata,
9338
9541
  measuredIframeHeights,
9339
9542
  measuredIframeNaturalHeights,
@@ -10258,6 +10461,7 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
10258
10461
  boardObject
10259
10462
  onStartResize={beginBoardElementResize}
10260
10463
  onStartRotate={() => {}}
10464
+ onStartDrag={beginBoardElementDrag}
10261
10465
  />
10262
10466
  ) : null}
10263
10467
 
@@ -404,7 +404,23 @@ export function ResponsiveInteractBar({
404
404
  <div className="flex shrink-0 items-center bg-[var(--design-editor-panel-bg)] pl-1">
405
405
  <ResponsiveInteractExitButton onClose={onClose} />
406
406
  </div>
407
- ) : null}
407
+ ) : (
408
+ <div
409
+ aria-hidden="true"
410
+ className="invisible flex shrink-0 items-center pl-1"
411
+ >
412
+ <Button
413
+ variant="ghost"
414
+ size="sm"
415
+ disabled
416
+ tabIndex={-1}
417
+ className="h-7 shrink-0 gap-1.5 rounded-md px-2 !text-[12px]"
418
+ >
419
+ <IconX className="size-4" />
420
+ {t("designEditor.responsiveInteract.exit")}
421
+ </Button>
422
+ </div>
423
+ )}
408
424
  </div>
409
425
  );
410
426
  }