@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
@@ -1130,6 +1130,26 @@ export function prepareClonedHtmlLayer(
1130
1130
  // silently misapplying edits meant for the duplicate.
1131
1131
  reassignClonedAuthoredIds(clone, () => uniqueLayerId("copy-id"));
1132
1132
  reassignClonedSourceIdentity(clone, () => uniqueLayerId("copy-child"));
1133
+ // Runtime snapshots carry the source component identity separately from
1134
+ // the DOM node id. Keep the component boundary stable across a clone, but
1135
+ // mint its instance handle with the same fresh id used for the cloned node.
1136
+ // Leaving the old handle in place makes two live instances address the same
1137
+ // runtime component when the next snapshot is serialized.
1138
+ for (const element of [clone, ...Array.from(clone.querySelectorAll("*"))]) {
1139
+ const runtimeInstanceId = element.getAttribute(
1140
+ "data-agent-native-runtime-instance-id",
1141
+ );
1142
+ if (!runtimeInstanceId) continue;
1143
+ const nextInstanceId =
1144
+ nodeIdMap.get(runtimeInstanceId) ??
1145
+ element.getAttribute("data-agent-native-node-id");
1146
+ if (nextInstanceId) {
1147
+ element.setAttribute(
1148
+ "data-agent-native-runtime-instance-id",
1149
+ nextInstanceId,
1150
+ );
1151
+ }
1152
+ }
1133
1153
  const linkedClone = linkedCloneSubtree(
1134
1154
  source,
1135
1155
  clone,
@@ -499,6 +499,8 @@ export function elementInfoFromCodeLayerNode(node: CodeLayerNode): ElementInfo {
499
499
  return {
500
500
  tagName: node.tag,
501
501
  id: typeof node.attributes.id === "string" ? node.attributes.id : undefined,
502
+ componentAnnotation:
503
+ node.dataAttributes["data-agent-native-component"]?.trim() || undefined,
502
504
  sourceId: bridgeSourceIdForCodeLayerNode(node),
503
505
  provenance: provenanceForCodeLayerNode(node),
504
506
  selector: preferredCodeLayerSelector(node),
@@ -53,14 +53,19 @@ export interface ApplyDesignEditorCommandArgs {
53
53
  >;
54
54
  setMode: Dispatch<SetStateAction<EditorMode>>;
55
55
  setPinMode: Dispatch<SetStateAction<boolean>>;
56
+ setOverviewSelectedScreenIds: Dispatch<SetStateAction<string[]>>;
56
57
  setScreenZoom: Dispatch<SetStateAction<number>>;
57
58
  setSelectedElement: Dispatch<SetStateAction<ElementInfo | null>>;
58
59
  setSelectedLayerIdsState: Dispatch<SetStateAction<string[]>>;
59
60
  setViewMode: Dispatch<SetStateAction<"single" | "overview">>;
61
+ pendingOverviewScreenSelectionRef?: RefObject<string | null>;
60
62
  setZoomForView: (
61
63
  targetView: "single" | "overview",
62
64
  update: SetStateAction<number>,
63
65
  ) => void;
66
+ /** The design payload is loaded, so overview zoom conversion has a real
67
+ * screen list and persisted frame scale rather than the pre-load fallback. */
68
+ overviewDataReady?: boolean;
64
69
  viewModeRef: RefObject<"single" | "overview">;
65
70
  /**
66
71
  * Reveals a screen on the overview canvas the same way a freshly-created
@@ -87,12 +92,15 @@ export function runApplyDesignEditorCommand(
87
92
  setInteractDeviceName,
88
93
  setInteractDeviceSize,
89
94
  setMode,
95
+ setOverviewSelectedScreenIds,
90
96
  setPinMode,
91
97
  setScreenZoom,
92
98
  setSelectedElement,
93
99
  setSelectedLayerIdsState,
94
100
  setViewMode,
95
101
  setZoomForView,
102
+ pendingOverviewScreenSelectionRef,
103
+ overviewDataReady = true,
96
104
  viewModeRef,
97
105
  requestCameraFit,
98
106
  }: ApplyDesignEditorCommandArgs,
@@ -132,6 +140,8 @@ export function runApplyDesignEditorCommand(
132
140
  // zero files. Otherwise the navigate is silently consumed and dropped.
133
141
  if (target && !targetFile) return false;
134
142
 
143
+ const targetView = editorView ?? viewModeRef.current;
144
+
135
145
  const inspectorTab =
136
146
  command.inspectorTab === "design" ||
137
147
  command.inspectorTab === "comments" ||
@@ -171,6 +181,12 @@ export function runApplyDesignEditorCommand(
171
181
 
172
182
  if (targetFile) {
173
183
  setActiveFileId(targetFile.id);
184
+ if (targetView === "overview") {
185
+ if (pendingOverviewScreenSelectionRef) {
186
+ pendingOverviewScreenSelectionRef.current = targetFile.id;
187
+ }
188
+ setOverviewSelectedScreenIds([targetFile.id]);
189
+ }
174
190
  }
175
191
  if (selectionId) {
176
192
  setSelectedLayerIdsState([selectionId]);
@@ -180,7 +196,6 @@ export function runApplyDesignEditorCommand(
180
196
  typeof command.zoom === "number" && Number.isFinite(command.zoom)
181
197
  ? clampZoom(command.zoom)
182
198
  : null;
183
- const targetView = editorView ?? viewModeRef.current;
184
199
  // Zoom-compounding fix — see shouldDeferOverviewZoomCommand's doc
185
200
  // comment: converting a persisted overview zoom back to canvas units
186
201
  // needs the REAL overviewZoomScale (known only once `files` loads), so
@@ -193,7 +208,10 @@ export function runApplyDesignEditorCommand(
193
208
  shouldDeferOverviewZoomCommand({
194
209
  hasZoomCommand: commandZoom !== null,
195
210
  targetView,
196
- filesLoaded: files.length > 0,
211
+ filesLoaded:
212
+ files.length > 0 &&
213
+ (targetView !== "overview" ||
214
+ (overviewDataReady && overviewScreens.length > 0)),
197
215
  })
198
216
  ) {
199
217
  return false;
@@ -216,7 +234,7 @@ export function runApplyDesignEditorCommand(
216
234
  const targetScreen = targetFile
217
235
  ? overviewScreens.find((screen) => screen.id === targetFile.id)
218
236
  : undefined;
219
- if (targetScreen && requestCameraFit) {
237
+ if (targetScreen && requestCameraFit && commandZoom === null) {
220
238
  const geometry = resolveFrameGeometrySync({
221
239
  screens: overviewScreens.map((screen) => ({
222
240
  id: screen.id,
@@ -37,6 +37,7 @@ export interface ApplyFileContentUpdateArgs {
37
37
  refreshPreview?: boolean;
38
38
  skipPreview?: boolean;
39
39
  forcePreviewFullDocument?: boolean;
40
+ immediateSave?: boolean;
40
41
  persist?: boolean;
41
42
  recordHistory?: boolean;
42
43
  historyBeforeContent?: string;
@@ -126,6 +127,7 @@ export function runApplyFileContentUpdate(
126
127
  refreshPreview?: boolean;
127
128
  skipPreview?: boolean;
128
129
  forcePreviewFullDocument?: boolean;
130
+ immediateSave?: boolean;
129
131
  persist?: boolean;
130
132
  recordHistory?: boolean;
131
133
  historyBeforeContent?: string;
@@ -1,3 +1,4 @@
1
+ import type { LocalJsxSourceAnchor } from "@shared/local-jsx-visual-edit";
1
2
  import { sourceContentHash } from "@shared/source-workspace";
2
3
 
3
4
  import type {
@@ -10,6 +11,13 @@ export interface CreateComponentSourcePreimage {
10
11
  versionHash: string;
11
12
  }
12
13
 
14
+ export interface CreateComponentLocalSource extends LocalJsxSourceAnchor {
15
+ connectionId: string;
16
+ path: string;
17
+ expectedVersionHash?: string;
18
+ propStamps?: Array<{ name: string; value: string }>;
19
+ }
20
+
13
21
  export interface CreateComponentRequest {
14
22
  designId: string;
15
23
  nodeId?: string;
@@ -17,8 +25,9 @@ export interface CreateComponentRequest {
17
25
  name: string;
18
26
  fileId?: string;
19
27
  source?: {
20
- currentContent: string;
21
- expectedVersionHash: string;
28
+ currentContent?: string;
29
+ expectedVersionHash?: string;
30
+ local?: CreateComponentLocalSource;
22
31
  };
23
32
  }
24
33
 
@@ -49,6 +58,12 @@ export interface CreateComponentActionResult {
49
58
  versionHash: string;
50
59
  updatedAt: string;
51
60
  }>;
61
+ source?: {
62
+ kind: "local-file";
63
+ connectionId: string;
64
+ path: string;
65
+ versionHash: string;
66
+ };
52
67
  updatedAt?: string;
53
68
  }
54
69
 
@@ -155,10 +170,26 @@ export function createComponentActionChange(
155
170
  /** Build the command-side request and hand it to the queue-owned transaction. */
156
171
  export async function runCreateComponent(
157
172
  args: CreateComponentArgs,
158
- request: Omit<CreateComponentRequest, "designId" | "fileId" | "source">,
173
+ request: Omit<CreateComponentRequest, "designId" | "fileId" | "source"> & {
174
+ source?: { local?: CreateComponentLocalSource };
175
+ },
159
176
  ): Promise<CreateComponentCommandOutcome | undefined> {
160
177
  if (!args.canEditDesign) return undefined;
161
178
 
179
+ if (request.source?.local) {
180
+ const result = await args.createComponent({
181
+ ...request,
182
+ designId: args.designId,
183
+ fileId: args.fileId,
184
+ source: { local: request.source.local },
185
+ });
186
+ return {
187
+ result,
188
+ historyRecorded: result.persisted === true,
189
+ hostSync: result.persisted === true ? "accepted" : "skipped",
190
+ };
191
+ }
192
+
162
193
  return args.mutationTransaction.enqueue({
163
194
  fileId: args.fileId,
164
195
  selectionBefore: args.selectionBefore,
@@ -170,6 +201,16 @@ export async function runCreateComponent(
170
201
  source: {
171
202
  currentContent: source.content,
172
203
  expectedVersionHash: source.versionHash,
204
+ ...(request.source?.local
205
+ ? {
206
+ local: {
207
+ ...request.source.local,
208
+ expectedVersionHash:
209
+ request.source.local.expectedVersionHash ??
210
+ source.versionHash,
211
+ },
212
+ }
213
+ : {}),
173
214
  },
174
215
  }),
175
216
  });
@@ -109,6 +109,8 @@ export interface DeleteFilesArgs {
109
109
  fileCreationUndoStackRef: RefObject<FileCreationHistoryEntry[]>;
110
110
  fileDeletionUndoStackRef: RefObject<FileDeletionHistoryEntry[]>;
111
111
  fileHistoryMutationPendingRef: RefObject<boolean>;
112
+ captureHistoryCheckpoint?: () => Promise<string>;
113
+ clearPendingHistory?: () => void;
112
114
  files: DesignFile[];
113
115
  geometryRedoStackRef: RefObject<GeometryHistoryEntry[]>;
114
116
  geometryUndoStackRef: RefObject<GeometryHistoryEntry[]>;
@@ -132,7 +134,7 @@ export interface DeleteFilesArgs {
132
134
  ) => void;
133
135
  }
134
136
 
135
- export function runDeleteFiles(
137
+ export async function runDeleteFiles(
136
138
  {
137
139
  activeFile,
138
140
  canvasFrameGeometryById,
@@ -149,6 +151,8 @@ export function runDeleteFiles(
149
151
  fileCreationUndoStackRef,
150
152
  fileDeletionUndoStackRef,
151
153
  fileHistoryMutationPendingRef,
154
+ captureHistoryCheckpoint,
155
+ clearPendingHistory,
152
156
  files,
153
157
  geometryRedoStackRef,
154
158
  geometryUndoStackRef,
@@ -186,8 +190,11 @@ export function runDeleteFiles(
186
190
  failedFiles: DesignFile[],
187
191
  ) => void;
188
192
  },
189
- ) {
193
+ ): Promise<void> {
190
194
  if (!filesToDelete.length) return;
195
+ if (options?.recordDeletionHistory && fileHistoryMutationPendingRef.current) {
196
+ throw new Error(t("common.genericError"));
197
+ }
191
198
  const deleteIds = new Set(filesToDelete.map((file) => file.id));
192
199
  const nextActiveFile = files.find((file) => !deleteIds.has(file.id));
193
200
  const nextGeometry = cloneCanvasFrameGeometry(canvasFrameGeometryById);
@@ -201,10 +208,24 @@ export function runDeleteFiles(
201
208
  })),
202
209
  }
203
210
  : null;
211
+ let historyCheckpointId: string | undefined;
204
212
  if (deletionHistoryEntry) {
205
- clearRedoStacks();
206
213
  fileHistoryMutationPendingRef.current = true;
207
214
  syncUndoRedoState();
215
+ if (captureHistoryCheckpoint) {
216
+ try {
217
+ historyCheckpointId = await captureHistoryCheckpoint();
218
+ } catch (error) {
219
+ fileHistoryMutationPendingRef.current = false;
220
+ clearPendingHistory?.();
221
+ syncUndoRedoState();
222
+ toast.error(
223
+ error instanceof Error ? error.message : t("common.genericError"),
224
+ );
225
+ return;
226
+ }
227
+ }
228
+ clearRedoStacks();
208
229
  }
209
230
  filesToDelete.forEach((file) => {
210
231
  delete nextGeometry[file.id];
@@ -371,61 +392,62 @@ export function runDeleteFiles(
371
392
  setSelectedElement(null);
372
393
  setSelectedLayerIdsState([]);
373
394
 
374
- void Promise.allSettled(
395
+ const results = await Promise.allSettled(
375
396
  filesToDelete.map((file) =>
376
397
  deleteFileMutation.mutateAsync({
377
398
  id: file.id,
378
399
  allowLockedLayers: true,
400
+ ...(historyCheckpointId ? { historyCheckpointId } : {}),
379
401
  } as any),
380
402
  ),
381
- ).then((results) => {
382
- const deletedFiles = filesToDelete.filter((_, index) => {
383
- const result = results[index];
384
- return (
385
- result?.status === "fulfilled" &&
386
- (result.value as { deleted?: boolean } | undefined)?.deleted !== false
387
- );
388
- });
389
- const deletedIds = new Set(deletedFiles.map((file) => file.id));
390
- const failedFiles = filesToDelete.filter(
391
- (file) => !deletedIds.has(file.id),
403
+ );
404
+ const deletedFiles = filesToDelete.filter((_, index) => {
405
+ const result = results[index];
406
+ return (
407
+ result?.status === "fulfilled" &&
408
+ (result.value as { deleted?: boolean } | undefined)?.deleted !== false
392
409
  );
410
+ });
411
+ const deletedIds = new Set(deletedFiles.map((file) => file.id));
412
+ const failedFiles = filesToDelete.filter((file) => !deletedIds.has(file.id));
393
413
 
394
- if (deletionHistoryEntry && deletedFiles.length > 0) {
395
- fileDeletionUndoStackRef.current = [
396
- ...fileDeletionUndoStackRef.current.slice(-(MAX_DESIGN_UNDO_STACK - 1)),
397
- filterFileDeletionHistoryEntry(deletionHistoryEntry, deletedIds),
398
- ];
399
- historyOrderRef.current = [
400
- ...historyOrderRef.current.slice(-(MAX_DESIGN_UNDO_STACK - 1)),
401
- "file-deleted",
402
- ];
403
- }
414
+ if (deletionHistoryEntry && deletedFiles.length > 0) {
415
+ fileDeletionUndoStackRef.current = [
416
+ ...fileDeletionUndoStackRef.current.slice(-(MAX_DESIGN_UNDO_STACK - 1)),
417
+ filterFileDeletionHistoryEntry(deletionHistoryEntry, deletedIds),
418
+ ];
419
+ historyOrderRef.current = [
420
+ ...historyOrderRef.current.slice(-(MAX_DESIGN_UNDO_STACK - 1)),
421
+ "file-deleted",
422
+ ];
423
+ }
404
424
 
405
- const rejected = results.find(
406
- (result): result is PromiseRejectedResult => result.status === "rejected",
407
- );
408
- if (failedFiles.length > 0) {
409
- void queryClient.invalidateQueries({
410
- queryKey: ["action", "get-design"],
411
- });
412
- if (rejected) {
413
- toast.error(
414
- rejected.reason instanceof Error
415
- ? rejected.reason.message
416
- : t("common.genericError"),
417
- );
418
- }
425
+ const rejected = results.find(
426
+ (result): result is PromiseRejectedResult => result.status === "rejected",
427
+ );
428
+ if (failedFiles.length > 0) {
429
+ void queryClient.invalidateQueries({
430
+ queryKey: ["action", "get-design"],
431
+ });
432
+ if (rejected) {
433
+ toast.error(
434
+ rejected.reason instanceof Error
435
+ ? rejected.reason.message
436
+ : t("common.genericError"),
437
+ );
419
438
  }
439
+ // A partial/failed batch is not a safe boundary for a queued undo/redo.
440
+ // The surviving user intent must be reissued explicitly after refresh.
441
+ if (deletionHistoryEntry) clearPendingHistory?.();
442
+ }
420
443
 
421
- if (deletionHistoryEntry) {
422
- fileHistoryMutationPendingRef.current = false;
423
- for (const fileId of deletedIds)
424
- latestClipboardMutationContentRef.current.delete(fileId);
425
- }
426
- options?.onMutationSettled?.(deletedFiles, failedFiles);
427
- syncUndoRedoState();
428
- });
444
+ if (deletionHistoryEntry) {
445
+ fileHistoryMutationPendingRef.current = false;
446
+ for (const fileId of deletedIds)
447
+ latestClipboardMutationContentRef.current.delete(fileId);
448
+ }
449
+ options?.onMutationSettled?.(deletedFiles, failedFiles);
450
+ syncUndoRedoState();
429
451
 
430
452
  // File-backed screen deletion is not a geometry-only edit. The screen rows
431
453
  // are hard-deleted, so suppress MultiScreenCanvas' local frame-history
@@ -1,5 +1,8 @@
1
1
  import { useActionMutation } from "@agent-native/core/client/hooks";
2
- import type { CanvasFrameGeometryById } from "@shared/canvas-frames";
2
+ import type {
3
+ CanvasFrameGeometry,
4
+ CanvasFrameGeometryById,
5
+ } from "@shared/canvas-frames";
3
6
  import type { QueryClient } from "@tanstack/react-query";
4
7
  import type { RefObject } from "react";
5
8
  import { toast } from "sonner";
@@ -21,6 +24,41 @@ import {
21
24
  import type { FileCreationHistoryEntry } from "@/pages/design-editor/history";
22
25
  import type { DesignFile } from "@/pages/design-editor/types";
23
26
 
27
+ const DUPLICATE_SCREEN_GAP = 56;
28
+
29
+ function isCompleteFrameGeometry(
30
+ geometry: CanvasFrameGeometry | undefined,
31
+ ): geometry is FrameGeometry {
32
+ return (
33
+ geometry !== undefined &&
34
+ [geometry.x, geometry.y, geometry.width, geometry.height].every(
35
+ (value) => typeof value === "number" && Number.isFinite(value),
36
+ )
37
+ );
38
+ }
39
+
40
+ export function getDuplicateScreenGeometry(
41
+ sourceGeometry: FrameGeometry,
42
+ occupiedGeometries: readonly FrameGeometry[],
43
+ ): FrameGeometry {
44
+ const rowBottom = sourceGeometry.y + sourceGeometry.height;
45
+ const rowGeometries = occupiedGeometries.filter(
46
+ (geometry) =>
47
+ geometry.y < rowBottom && geometry.y + geometry.height > sourceGeometry.y,
48
+ );
49
+ const x = Math.max(
50
+ sourceGeometry.x + sourceGeometry.width + DUPLICATE_SCREEN_GAP,
51
+ ...rowGeometries.map(
52
+ (geometry) => geometry.x + geometry.width + DUPLICATE_SCREEN_GAP,
53
+ ),
54
+ );
55
+ const z = Math.max(
56
+ sourceGeometry.z ?? 0,
57
+ ...occupiedGeometries.map((geometry) => geometry.z ?? 0),
58
+ );
59
+ return { ...sourceGeometry, x, y: sourceGeometry.y, z: z + 1 };
60
+ }
61
+
24
62
  export interface DuplicateScreenArgs {
25
63
  canEditDesign: boolean;
26
64
  createFileAsync: ReturnType<
@@ -28,7 +66,14 @@ export interface DuplicateScreenArgs {
28
66
  >["mutateAsync"];
29
67
  designDataJsonRef: RefObject<Record<string, unknown>>;
30
68
  files: DesignFile[];
31
- focusCreatedScreen: (screenId: string, geometry: FrameGeometry) => void;
69
+ focusCreatedScreen: (
70
+ screenId: string,
71
+ geometry: FrameGeometry,
72
+ options?: {
73
+ preserveCamera?: boolean;
74
+ suppressLineupRecenter?: boolean;
75
+ },
76
+ ) => void;
32
77
  id: string | undefined;
33
78
  liveFrameGeometryRef: RefObject<CanvasFrameGeometryById>;
34
79
  optimisticallyInsertCreatedFile: (args: {
@@ -71,6 +116,7 @@ export function runDuplicateScreen(
71
116
  screenId: string,
72
117
  request?: {
73
118
  canvasPosition?: { x: number; y: number };
119
+ preserveCamera?: boolean;
74
120
  },
75
121
  ) {
76
122
  if (!id || !canEditDesign) return;
@@ -86,15 +132,30 @@ export function runDuplicateScreen(
86
132
  width: sourceOverviewScreen?.width ?? 1280,
87
133
  height: sourceOverviewScreen?.height ?? 2560,
88
134
  });
135
+ const persistedGeometry = getCanvasFrameGeometry(designDataJsonRef.current);
136
+ const sourceGeometry =
137
+ [liveFrameGeometryRef.current[screenId], persistedGeometry[screenId]].find(
138
+ isCompleteFrameGeometry,
139
+ ) ?? fallbackGeometry;
140
+ const occupiedGeometries = overviewScreens
141
+ .filter((screen) => screen.id !== screenId)
142
+ .map(
143
+ (screen) =>
144
+ liveFrameGeometryRef.current[screen.id] ?? persistedGeometry[screen.id],
145
+ )
146
+ .filter(isCompleteFrameGeometry);
147
+ const adjacentGeometry = getDuplicateScreenGeometry(
148
+ sourceGeometry,
149
+ occupiedGeometries,
150
+ );
89
151
  const createdGeometry: FrameGeometry = request?.canvasPosition
90
152
  ? {
91
- ...fallbackGeometry,
92
- ...liveFrameGeometryRef.current[screenId],
153
+ ...sourceGeometry,
93
154
  x: request.canvasPosition.x,
94
155
  y: request.canvasPosition.y,
156
+ z: adjacentGeometry.z,
95
157
  }
96
- : fallbackGeometry;
97
-
158
+ : adjacentGeometry;
98
159
  // Per-call mutate callbacks, not a promise, would silently strand every
99
160
  // duplicate but the last: a second mutate() detaches the observer from
100
161
  // the first mutation, so only the newest call's onSuccess ever runs.
@@ -104,7 +165,7 @@ export function runDuplicateScreen(
104
165
  content,
105
166
  fileType,
106
167
  } as any)
107
- .then((result: any) => {
168
+ .then(async (result: any) => {
108
169
  const nextId = typeof result?.id === "string" ? result.id : null;
109
170
  // Refetch only when there is no created id to insert optimistically:
110
171
  // a whole-design refetch re-downloads every screen's HTML, which is
@@ -114,30 +175,19 @@ export function runDuplicateScreen(
114
175
  queryKey: ["action", "get-design"],
115
176
  });
116
177
  } else {
117
- optimisticallyInsertCreatedFile({
118
- fileId: nextId,
119
- filename,
120
- fileType,
121
- content,
122
- result,
123
- });
124
178
  // Optimistic geometry keeps frame, selection, and camera agreeing
125
- // before the refetch. Base it on the map writeFrameGeometrySnapshot
126
- // diffs against, or a sibling duplicate's placement is deleted.
179
+ // before the refetch. Write it before the file enters `screens`, or
180
+ // the geometry-sync effect can render a fallback frame first and
181
+ // preserve that stale position over the requested drop point.
127
182
  writeFrameGeometrySnapshot({
128
183
  ...getCanvasFrameGeometry(designDataJsonRef.current),
129
184
  [nextId]: createdGeometry,
130
185
  });
131
- focusCreatedScreen(nextId, createdGeometry);
132
- recordFileCreationHistoryEntry({
133
- filename,
134
- content,
135
- fileType,
136
- geometry: createdGeometry,
137
- });
138
- // A duplicated localhost/fusion screen stays URL-backed only if its
139
- // metadata comes along, and the carry must be path-addressed or it
140
- // replaces a peer's metadata for every other screen.
186
+ // Carry screen dimensions/height mode for every duplicate so the new
187
+ // frame uses the same overview scale. Runtime metadata also keeps
188
+ // localhost/fusion duplicates URL-backed. The carry must be
189
+ // path-addressed or it replaces a peer's metadata for every other
190
+ // screen.
141
191
  const sourceMetadataById = getDesignDataRecord(
142
192
  designDataJsonRef.current,
143
193
  "screenMetadata",
@@ -147,29 +197,61 @@ export function runDuplicateScreen(
147
197
  screenId,
148
198
  );
149
199
  const sourceType = sourceMetadata.sourceType;
150
- if (sourceType === "localhost" || sourceType === "fusion") {
151
- const dataOperations: DesignDataOperation[] = [
152
- {
153
- op: "set",
154
- path: ["screenMetadata", nextId],
155
- value: { ...sourceMetadata },
156
- },
157
- ];
158
- const sourceLocalhostScreensById = getDesignDataRecord(
159
- designDataJsonRef.current,
160
- "localhostScreens",
161
- );
162
- const sourceLocalhostScreen = getDesignDataRecord(
163
- sourceLocalhostScreensById,
164
- screenId,
165
- );
166
- if (Object.keys(sourceLocalhostScreen).length > 0) {
167
- dataOperations.push({
168
- op: "set",
169
- path: ["localhostScreens", nextId],
170
- value: { ...sourceLocalhostScreen },
171
- });
172
- }
200
+ const carriesRuntimeMetadata =
201
+ sourceType === "localhost" || sourceType === "fusion";
202
+ const metadataToCopy = carriesRuntimeMetadata
203
+ ? sourceMetadata
204
+ : Object.fromEntries(
205
+ [
206
+ "sourceType",
207
+ "width",
208
+ "height",
209
+ "heightPinned",
210
+ "heightMode",
211
+ "breakpointHeights",
212
+ ].flatMap((key) =>
213
+ key in sourceMetadata ? [[key, sourceMetadata[key]]] : [],
214
+ ),
215
+ );
216
+ const screenMetadata =
217
+ Object.keys(metadataToCopy).length > 0
218
+ ? { ...metadataToCopy }
219
+ : undefined;
220
+ const sourceLocalhostScreen = carriesRuntimeMetadata
221
+ ? getDesignDataRecord(
222
+ getDesignDataRecord(
223
+ designDataJsonRef.current,
224
+ "localhostScreens",
225
+ ),
226
+ screenId,
227
+ )
228
+ : {};
229
+ const localhostScreen =
230
+ Object.keys(sourceLocalhostScreen).length > 0
231
+ ? { ...sourceLocalhostScreen }
232
+ : undefined;
233
+ const dataOperations: DesignDataOperation[] = [
234
+ {
235
+ op: "set",
236
+ path: ["canvasFrames", nextId],
237
+ value: createdGeometry,
238
+ },
239
+ ];
240
+ if (screenMetadata) {
241
+ dataOperations.push({
242
+ op: "set",
243
+ path: ["screenMetadata", nextId],
244
+ value: screenMetadata,
245
+ });
246
+ }
247
+ if (localhostScreen) {
248
+ dataOperations.push({
249
+ op: "set",
250
+ path: ["localhostScreens", nextId],
251
+ value: localhostScreen,
252
+ });
253
+ }
254
+ if (dataOperations.length > 0) {
173
255
  const nextData = applyDesignDataOperations(
174
256
  designDataJsonRef.current,
175
257
  dataOperations,
@@ -182,12 +264,35 @@ export function runDuplicateScreen(
182
264
  return { ...old, data: JSON.stringify(nextData) };
183
265
  },
184
266
  );
185
- void updateDesignAsync({ id, dataOperations } as any).catch(() => {
186
- void queryClient.invalidateQueries({
267
+ try {
268
+ await updateDesignAsync({ id, dataOperations } as any);
269
+ } catch (error) {
270
+ await queryClient.invalidateQueries({
187
271
  queryKey: ["action", "get-design"],
188
272
  });
189
- });
273
+ throw error;
274
+ }
190
275
  }
276
+ optimisticallyInsertCreatedFile({
277
+ fileId: nextId,
278
+ filename,
279
+ fileType,
280
+ content,
281
+ result,
282
+ });
283
+ focusCreatedScreen(nextId, createdGeometry, {
284
+ preserveCamera: request?.preserveCamera,
285
+ suppressLineupRecenter: request?.preserveCamera,
286
+ });
287
+ recordFileCreationHistoryEntry({
288
+ filename,
289
+ content,
290
+ fileType,
291
+ geometry: createdGeometry,
292
+ preserveCamera: request?.preserveCamera,
293
+ screenMetadata,
294
+ localhostScreen,
295
+ });
191
296
  }
192
297
  toast.success(t("designEditor.toasts.screenDuplicated"));
193
298
  })