@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
@@ -5,7 +5,10 @@ import { toast } from "sonner";
5
5
  import * as Y from "yjs";
6
6
 
7
7
  import { trace } from "@/components/design/design-trace";
8
- import type { ElementInfo } from "@/components/design/types";
8
+ import type {
9
+ ElementInfo,
10
+ RuntimeStructureInsertRequest,
11
+ } from "@/components/design/types";
9
12
  import type { ClipboardContentMutationPublication } from "@/lib/clipboard-content-lineage";
10
13
  import {
11
14
  extractLayerPosition,
@@ -13,6 +16,7 @@ import {
13
16
  insertClonedHtmlLayer,
14
17
  insertClonedHtmlLayers,
15
18
  planLinkedComponentStructureClone,
19
+ prepareClonedHtmlLayersForLiveInsert,
16
20
  type ComponentCloneBatchContext,
17
21
  type LinkedComponentStructureClonePlan,
18
22
  } from "@/pages/design-editor/clone-and-pen-edit";
@@ -28,6 +32,7 @@ import {
28
32
  mapAcceptedSelectionNode,
29
33
  projectAcceptedSource,
30
34
  } from "@/pages/design-editor/commands/selection-publication";
35
+ import { isStandaloneHttpUrl } from "@/pages/design-editor/editor-state";
31
36
  import type { GeometryHistorySelection } from "@/pages/design-editor/history";
32
37
  import type { DesignFile } from "@/pages/design-editor/types";
33
38
 
@@ -147,10 +152,16 @@ export interface DuplicateSelectionArgs {
147
152
  nodeIdMap: Map<string, string>,
148
153
  targetFileId: string,
149
154
  ) => void;
155
+ runtimeStructureInsertRevisionRef?: RefObject<number>;
150
156
  selectionBefore?: GeometryHistorySelection;
151
157
  selectedCanvasSelector: string;
152
158
  selectedElement: ElementInfo | null;
153
159
  selectedLayerIdsState: string[];
160
+ setRuntimeStructureInsertRequest?: Dispatch<
161
+ SetStateAction<
162
+ (RuntimeStructureInsertRequest & { screenId: string }) | null
163
+ >
164
+ >;
154
165
  setOverviewSelectedScreenIds: Dispatch<SetStateAction<string[]>>;
155
166
  setSelectedElement: Dispatch<SetStateAction<ElementInfo | null>>;
156
167
  setSelectedLayerIdsState: Dispatch<SetStateAction<string[]>>;
@@ -174,10 +185,12 @@ export function runDuplicateSelection({
174
185
  lastDuplicateTransformRef,
175
186
  overviewSelectedScreenIds,
176
187
  remapMotionTracksForClone,
188
+ runtimeStructureInsertRevisionRef,
177
189
  selectionBefore,
178
190
  selectedCanvasSelector,
179
191
  selectedElement,
180
192
  selectedLayerIdsState,
193
+ setRuntimeStructureInsertRequest,
181
194
  setOverviewSelectedScreenIds,
182
195
  setSelectedElement,
183
196
  setSelectedLayerIdsState,
@@ -263,6 +276,68 @@ export function runDuplicateSelection({
263
276
  : null;
264
277
  const nextDuplicateRootNodeIds: string[] = [];
265
278
 
279
+ // A localhost screen stores its route URL, while the selected layer only
280
+ // exists in the running iframe. Reusing the design-file clone path here
281
+ // would ask the URL string for an HTML projection and silently turn Cmd+D
282
+ // into a no-op. Prepare the runtime snapshots for the same one-shot bridge
283
+ // insert lifecycle used by paste so the clone gets a fresh identity and
284
+ // participates in pending-edit undo/redo.
285
+ if (
286
+ activeFile &&
287
+ isStandaloneHttpUrl(activeFile.content ?? "") &&
288
+ snapshots.every((snapshot) => snapshot.sourceFileId === activeFile.id)
289
+ ) {
290
+ const sourcePositions = snapshots.map((snapshot) =>
291
+ extractLayerPosition(snapshot.html),
292
+ );
293
+ const prepared = prepareClonedHtmlLayersForLiveInsert(
294
+ activeFile.content ?? "",
295
+ snapshots.map((snapshot) => snapshot.html),
296
+ {
297
+ stripRootPosition: true,
298
+ positions: sourcePositions.map((position) =>
299
+ position
300
+ ? {
301
+ x: position.x + (repeatTransform?.dx ?? 0),
302
+ y: position.y + (repeatTransform?.dy ?? 0),
303
+ space: "layout" as const,
304
+ }
305
+ : undefined,
306
+ ),
307
+ },
308
+ );
309
+ if (
310
+ prepared &&
311
+ runtimeStructureInsertRevisionRef &&
312
+ setRuntimeStructureInsertRequest
313
+ ) {
314
+ const anchorSelector =
315
+ selectedElement?.runtimeSelector ??
316
+ selectedCanvasSelector ??
317
+ selectedElement?.selector;
318
+ const anchorSourceId =
319
+ selectedElement?.runtimeSourceId ?? selectedElement?.sourceId;
320
+ const hasAnchor = Boolean(anchorSelector);
321
+ runtimeStructureInsertRevisionRef.current += 1;
322
+ setRuntimeStructureInsertRequest({
323
+ requestId: runtimeStructureInsertRevisionRef.current,
324
+ screenId: activeFile.id,
325
+ html: prepared.htmlFragments[0]!,
326
+ additionalHtml: prepared.htmlFragments.slice(1),
327
+ anchor: hasAnchor
328
+ ? { selector: anchorSelector!, sourceId: anchorSourceId }
329
+ : { selector: "body" },
330
+ placement: hasAnchor ? "after" : "inside",
331
+ });
332
+ lastDuplicateTransformRef.current = {
333
+ rootNodeIds: [...prepared.rootNodeIds].sort(),
334
+ dx: repeatTransform?.dx ?? 0,
335
+ dy: repeatTransform?.dy ?? 0,
336
+ };
337
+ return;
338
+ }
339
+ }
340
+
266
341
  const sortedSnapshotsByFile = new Map(
267
342
  files.map((file) => [
268
343
  file.id,
@@ -29,6 +29,8 @@ import type { LiveScreenSnapshot } from "@/pages/design-editor/command-types";
29
29
  import type { ApplyFileContentUpdateResult } from "@/pages/design-editor/commands/apply-file-content-update";
30
30
  import type { ApplyLocalContentUpdateResult } from "@/pages/design-editor/commands/apply-local-content-update";
31
31
  import { prepareContentHistoryReplay } from "@/pages/design-editor/commands/prepare-content-history-replay";
32
+ import type { DesignDataOperation } from "@/pages/design-editor/data-operations";
33
+ import { applyDesignDataOperations } from "@/pages/design-editor/data-operations";
32
34
  import type { OverviewScreen } from "@/pages/design-editor/derive/overview-screens";
33
35
  import {
34
36
  getCanvasFrameGeometry,
@@ -153,8 +155,17 @@ export interface RedoArgs {
153
155
  fileDeletionRedoStackRef: RefObject<FileDeletionHistoryEntry[]>;
154
156
  fileDeletionUndoStackRef: RefObject<FileDeletionHistoryEntry[]>;
155
157
  fileHistoryMutationPendingRef: RefObject<boolean>;
158
+ clearPendingHistory?: () => void;
156
159
  files: DesignFile[];
157
- focusCreatedScreen: (screenId: string, geometry: FrameGeometry) => void;
160
+ filesRef?: RefObject<DesignFile[]>;
161
+ focusCreatedScreen: (
162
+ screenId: string,
163
+ geometry: FrameGeometry,
164
+ options?: {
165
+ preserveCamera?: boolean;
166
+ suppressLineupRecenter?: boolean;
167
+ },
168
+ ) => void;
158
169
  geometryRedoStackRef: RefObject<GeometryHistoryEntry[]>;
159
170
  geometryUndoStackRef: RefObject<GeometryHistoryEntry[]>;
160
171
  getFreshActiveContent: () => string;
@@ -299,6 +310,9 @@ export interface RedoArgs {
299
310
  html: string,
300
311
  options?: { recordHistory?: boolean },
301
312
  ) => boolean;
313
+ updateDesignAsync: ReturnType<
314
+ typeof useActionMutation<undefined, undefined, "update-design">
315
+ >["mutateAsync"];
302
316
  viewModeRef: RefObject<"single" | "overview">;
303
317
  writeFrameGeometrySnapshot: (
304
318
  geometryById: CanvasFrameGeometryById,
@@ -335,7 +349,9 @@ export function runRedo({
335
349
  fileDeletionRedoStackRef,
336
350
  fileDeletionUndoStackRef,
337
351
  fileHistoryMutationPendingRef,
352
+ clearPendingHistory,
338
353
  files,
354
+ filesRef,
339
355
  focusCreatedScreen,
340
356
  geometryRedoStackRef,
341
357
  geometryUndoStackRef,
@@ -391,6 +407,7 @@ export function runRedo({
391
407
  t,
392
408
  undoManagerRef,
393
409
  updateLiveScreenSnapshotContent,
410
+ updateDesignAsync,
394
411
  viewModeRef,
395
412
  writeFrameGeometrySnapshot,
396
413
  ydoc,
@@ -399,8 +416,9 @@ export function runRedo({
399
416
  selection: GeometryHistorySelection | undefined,
400
417
  replaySources: Record<string, string> = {},
401
418
  ) => {
419
+ const currentFiles = filesRef?.current ?? files;
402
420
  const actualSources = Object.fromEntries(
403
- files.map((file) => [
421
+ currentFiles.map((file) => [
404
422
  file.id,
405
423
  replaySources[file.id] ?? getScreenContent(file.id),
406
424
  ]),
@@ -1144,9 +1162,8 @@ export function runRedo({
1144
1162
  // U12: redo a screen create/duplicate by recreating the file with the
1145
1163
  // same filename/content/fileType and restoring its recorded geometry.
1146
1164
  // This is async (createFileMutation), unlike every other redo path here,
1147
- // so it optimistically reports success immediately (mirrors
1148
- // handleAddScreen's own optimistic cache write) and surfaces a toast on
1149
- // failure instead of rolling the redo stacks back.
1165
+ // so the history stacks move optimistically while the mutation runs and
1166
+ // restore the redo entry if creation or its metadata commit fails.
1150
1167
  const redoFileCreation = () => {
1151
1168
  if (!canUseOverviewHistory) return false;
1152
1169
  const entry = fileCreationRedoStackRef.current.pop();
@@ -1162,6 +1179,77 @@ export function runRedo({
1162
1179
  ];
1163
1180
  fileHistoryMutationPendingRef.current = true;
1164
1181
  syncUndoRedoState();
1182
+ const restoreFailedRedo = (error: unknown) => {
1183
+ if (
1184
+ fileCreationUndoStackRef.current[
1185
+ fileCreationUndoStackRef.current.length - 1
1186
+ ] === entry
1187
+ ) {
1188
+ fileCreationUndoStackRef.current =
1189
+ fileCreationUndoStackRef.current.slice(0, -1);
1190
+ }
1191
+ historyOrderRef.current = removeRecentUndoRedoOrderKinds(
1192
+ historyOrderRef.current,
1193
+ "file-created",
1194
+ 1,
1195
+ );
1196
+ fileCreationRedoStackRef.current = [
1197
+ ...fileCreationRedoStackRef.current.slice(-(MAX_DESIGN_UNDO_STACK - 1)),
1198
+ entry,
1199
+ ];
1200
+ redoOrderRef.current = [
1201
+ ...redoOrderRef.current.slice(-(MAX_DESIGN_UNDO_STACK - 1)),
1202
+ "file-created",
1203
+ ];
1204
+ clearPendingHistory?.();
1205
+ fileHistoryMutationPendingRef.current = false;
1206
+ syncUndoRedoState();
1207
+ toast.error(
1208
+ error instanceof Error
1209
+ ? error.message
1210
+ : t("designEditor.toasts.screenDuplicateError"),
1211
+ );
1212
+ };
1213
+ const reconcileFailedRedo = async (
1214
+ error: unknown,
1215
+ nextId?: string,
1216
+ result?: Record<string, unknown>,
1217
+ ) => {
1218
+ if (nextId) {
1219
+ try {
1220
+ await performDeleteFiles(
1221
+ [
1222
+ {
1223
+ id: nextId,
1224
+ filename: entry.filename,
1225
+ fileType: entry.fileType,
1226
+ content: entry.content,
1227
+ createdAt:
1228
+ typeof result?.createdAt === "string" ? result.createdAt : "",
1229
+ updatedAt:
1230
+ typeof result?.updatedAt === "string" ? result.updatedAt : "",
1231
+ },
1232
+ ],
1233
+ {
1234
+ preserveHistory: true,
1235
+ skipFileCreationRedoPrune: true,
1236
+ },
1237
+ );
1238
+ } catch (cleanupError) {
1239
+ trace("history", "redo-file-cleanup-failed", {
1240
+ id,
1241
+ error:
1242
+ cleanupError instanceof Error
1243
+ ? cleanupError.message
1244
+ : String(cleanupError),
1245
+ });
1246
+ }
1247
+ await queryClient.invalidateQueries({
1248
+ queryKey: ["action", "get-design"],
1249
+ });
1250
+ }
1251
+ restoreFailedRedo(error);
1252
+ };
1165
1253
  createFileMutation.mutate(
1166
1254
  {
1167
1255
  designId: id,
@@ -1170,7 +1258,7 @@ export function runRedo({
1170
1258
  fileType: entry.fileType,
1171
1259
  } as any,
1172
1260
  {
1173
- onSuccess: (result: any) => {
1261
+ onSuccess: async (result: any) => {
1174
1262
  const nextId = typeof result?.id === "string" ? result.id : null;
1175
1263
  if (nextId) {
1176
1264
  const geometry = {
@@ -1180,6 +1268,58 @@ export function runRedo({
1180
1268
  }),
1181
1269
  ...entry.geometry,
1182
1270
  };
1271
+ const dataOperations: DesignDataOperation[] = [
1272
+ {
1273
+ op: "set",
1274
+ path: ["canvasFrames", nextId],
1275
+ value: geometry,
1276
+ },
1277
+ ...(entry.screenMetadata
1278
+ ? [
1279
+ {
1280
+ op: "set" as const,
1281
+ path: ["screenMetadata", nextId] as [string, ...string[]],
1282
+ value: entry.screenMetadata,
1283
+ },
1284
+ ]
1285
+ : []),
1286
+ ...(entry.localhostScreen
1287
+ ? [
1288
+ {
1289
+ op: "set" as const,
1290
+ path: ["localhostScreens", nextId] as [
1291
+ string,
1292
+ ...string[],
1293
+ ],
1294
+ value: entry.localhostScreen,
1295
+ },
1296
+ ]
1297
+ : []),
1298
+ ];
1299
+ if (dataOperations.length > 0) {
1300
+ const nextData = applyDesignDataOperations(
1301
+ designDataJsonRef.current,
1302
+ dataOperations,
1303
+ );
1304
+ designDataJsonRef.current = nextData;
1305
+ queryClient.setQueryData(
1306
+ ["action", "get-design", { id }],
1307
+ (old: any) => {
1308
+ if (!old || typeof old !== "object") return old;
1309
+ return { ...old, data: JSON.stringify(nextData) };
1310
+ },
1311
+ );
1312
+ try {
1313
+ await updateDesignAsync({ id, dataOperations } as any);
1314
+ } catch (error) {
1315
+ trace("history", "redo-file-data-failed", {
1316
+ id,
1317
+ error: error instanceof Error ? error.message : String(error),
1318
+ });
1319
+ await reconcileFailedRedo(error, nextId, result);
1320
+ return;
1321
+ }
1322
+ }
1183
1323
  optimisticallyInsertCreatedFile({
1184
1324
  fileId: nextId,
1185
1325
  filename: entry.filename,
@@ -1187,52 +1327,19 @@ export function runRedo({
1187
1327
  content: entry.content,
1188
1328
  result,
1189
1329
  });
1190
- writeFrameGeometrySnapshot({
1191
- ...getCanvasFrameGeometry(designDataJsonRef.current),
1192
- [nextId]: geometry,
1330
+ focusCreatedScreen(nextId, geometry, {
1331
+ preserveCamera: entry.preserveCamera,
1332
+ suppressLineupRecenter: entry.preserveCamera,
1193
1333
  });
1194
- focusCreatedScreen(nextId, geometry);
1195
1334
  }
1196
1335
  fileHistoryMutationPendingRef.current = false;
1197
1336
  syncUndoRedoState();
1198
- void queryClient.invalidateQueries({
1337
+ await queryClient.invalidateQueries({
1199
1338
  queryKey: ["action", "get-design"],
1200
1339
  });
1201
1340
  },
1202
1341
  onError: (error: unknown) => {
1203
- // The optimistic history move happened before the request. Put the
1204
- // entry back exactly where it came from so a failed redo remains
1205
- // retryable and does not leave a phantom undo operation behind.
1206
- if (
1207
- fileCreationUndoStackRef.current[
1208
- fileCreationUndoStackRef.current.length - 1
1209
- ] === entry
1210
- ) {
1211
- fileCreationUndoStackRef.current =
1212
- fileCreationUndoStackRef.current.slice(0, -1);
1213
- }
1214
- historyOrderRef.current = removeRecentUndoRedoOrderKinds(
1215
- historyOrderRef.current,
1216
- "file-created",
1217
- 1,
1218
- );
1219
- fileCreationRedoStackRef.current = [
1220
- ...fileCreationRedoStackRef.current.slice(
1221
- -(MAX_DESIGN_UNDO_STACK - 1),
1222
- ),
1223
- entry,
1224
- ];
1225
- redoOrderRef.current = [
1226
- ...redoOrderRef.current.slice(-(MAX_DESIGN_UNDO_STACK - 1)),
1227
- "file-created",
1228
- ];
1229
- fileHistoryMutationPendingRef.current = false;
1230
- syncUndoRedoState();
1231
- toast.error(
1232
- error instanceof Error
1233
- ? error.message
1234
- : t("designEditor.toasts.screenDuplicateError"),
1235
- );
1342
+ restoreFailedRedo(error);
1236
1343
  },
1237
1344
  },
1238
1345
  );
@@ -1280,6 +1387,7 @@ export function runRedo({
1280
1387
  ...redoOrderRef.current.slice(-(MAX_DESIGN_UNDO_STACK - 1)),
1281
1388
  "file-deleted",
1282
1389
  ];
1390
+ clearPendingHistory?.();
1283
1391
  }
1284
1392
  fileHistoryMutationPendingRef.current = false;
1285
1393
  syncUndoRedoState();
@@ -1,4 +1,5 @@
1
1
  import { useActionMutation } from "@agent-native/core/client/hooks";
2
+ import { sourceContentHash } from "@shared/source-workspace";
2
3
  import type { QueryClient } from "@tanstack/react-query";
3
4
  import type { Dispatch, RefObject, SetStateAction } from "react";
4
5
  import { toast } from "sonner";
@@ -7,7 +8,11 @@ import type { DesignSaveOutboxEntry } from "@/lib/design-save-outbox";
7
8
  import { updateFileResultPersistedContent } from "@/lib/design-save-outbox";
8
9
  import type { PatchProofState } from "@/pages/design-editor/command-types";
9
10
  import type { FileContentSaveRequest } from "@/pages/design-editor/editor-state";
10
- import { shouldClearLatestUnloadSave } from "@/pages/design-editor/editor-state";
11
+ import {
12
+ advanceLatestUnloadSaveBase,
13
+ prepareFileContentSaveKeepalive,
14
+ shouldClearLatestUnloadSave,
15
+ } from "@/pages/design-editor/editor-state";
11
16
  import {
12
17
  classifyDesignSaveFailure,
13
18
  designSaveErrorMessage,
@@ -21,6 +26,7 @@ export interface SaveFileContentArgs {
21
26
  createFileSaveOutboxEntry: (
22
27
  pending: FileContentSaveRequest,
23
28
  ) => DesignSaveOutboxEntry | null;
29
+ designId?: string;
24
30
  fileSaveChainsRef: RefObject<Record<string, Promise<void>>>;
25
31
  journalOutboxEntry: (entry: DesignSaveOutboxEntry) => Promise<boolean>;
26
32
  latestFileSaveForUnloadRef: RefObject<Record<string, FileContentSaveRequest>>;
@@ -43,11 +49,86 @@ export interface SaveFileContentArgs {
43
49
  warnChangesWillRetry: () => void;
44
50
  }
45
51
 
52
+ type FileContentSaveKeepaliveAttempt =
53
+ | { accepted: true; completion: Promise<unknown> }
54
+ | { accepted: false; completion: null };
55
+
56
+ export interface SaveFileContentKeepaliveArgs {
57
+ acknowledgeOutboxEntry: (entry: DesignSaveOutboxEntry) => Promise<void>;
58
+ createFileSaveOutboxEntry: (
59
+ pending: FileContentSaveRequest,
60
+ ) => DesignSaveOutboxEntry | null;
61
+ journalOutboxEntry: (entry: DesignSaveOutboxEntry) => Promise<boolean>;
62
+ latestFileSaveForUnloadRef: RefObject<Record<string, FileContentSaveRequest>>;
63
+ sendKeepalive: (
64
+ payload: Record<string, unknown>,
65
+ ) => FileContentSaveKeepaliveAttempt;
66
+ }
67
+
68
+ export function runFileContentSaveKeepalive(
69
+ {
70
+ acknowledgeOutboxEntry,
71
+ createFileSaveOutboxEntry,
72
+ journalOutboxEntry,
73
+ latestFileSaveForUnloadRef,
74
+ sendKeepalive,
75
+ }: SaveFileContentKeepaliveArgs,
76
+ pending: FileContentSaveRequest,
77
+ ) {
78
+ // Keep the folded oldest-base entry durable while the direct request uses
79
+ // this edit's own base. If the predecessor never lands, replay must start
80
+ // from the oldest known version rather than the successor's base.
81
+ const durableEntry = createFileSaveOutboxEntry(pending);
82
+ const keepaliveEntry = createFileSaveOutboxEntry(
83
+ prepareFileContentSaveKeepalive(pending),
84
+ );
85
+ if (!durableEntry || !keepaliveEntry) return;
86
+ const journalPromise = journalOutboxEntry(durableEntry);
87
+ const attempt = sendKeepalive(keepaliveEntry.payload);
88
+ if (!attempt.accepted) {
89
+ void journalPromise.catch(() => {});
90
+ return;
91
+ }
92
+ void attempt.completion
93
+ .then(async (result: unknown) => {
94
+ await journalPromise;
95
+ const persistedContentMatches = updateFileResultPersistedContent(
96
+ result,
97
+ pending.content,
98
+ );
99
+ if (!persistedContentMatches) return;
100
+ const resultInfo = result as { versionHash?: string } | undefined;
101
+ const latest = latestFileSaveForUnloadRef.current[pending.id];
102
+ if (shouldClearLatestUnloadSave(latest, pending)) {
103
+ await acknowledgeOutboxEntry(durableEntry);
104
+ delete latestFileSaveForUnloadRef.current[pending.id];
105
+ return;
106
+ }
107
+ if (
108
+ advanceLatestUnloadSaveBase(
109
+ latest,
110
+ pending,
111
+ resultInfo?.versionHash ?? sourceContentHash(pending.content),
112
+ )
113
+ ) {
114
+ const advancedOutboxEntry = latest
115
+ ? createFileSaveOutboxEntry(latest)
116
+ : null;
117
+ if (advancedOutboxEntry) {
118
+ await journalOutboxEntry(advancedOutboxEntry);
119
+ }
120
+ }
121
+ await acknowledgeOutboxEntry(durableEntry);
122
+ })
123
+ .catch(() => {});
124
+ }
125
+
46
126
  export function runSaveFileContent(
47
127
  {
48
128
  acknowledgeOutboxEntry,
49
129
  canEditDesignRef,
50
130
  createFileSaveOutboxEntry,
131
+ designId,
51
132
  fileSaveChainsRef,
52
133
  journalOutboxEntry,
53
134
  latestFileSaveForUnloadRef,
@@ -120,8 +201,54 @@ export function runSaveFileContent(
120
201
  pending.content,
121
202
  t("common.genericError"),
122
203
  );
204
+ const latest = latestFileSaveForUnloadRef.current[pending.id];
205
+ if (
206
+ persistedContentMatches &&
207
+ advanceLatestUnloadSaveBase(
208
+ latest,
209
+ pending,
210
+ resultInfo?.versionHash ?? sourceContentHash(pending.content),
211
+ )
212
+ ) {
213
+ const advancedOutboxEntry = latest
214
+ ? createFileSaveOutboxEntry(latest)
215
+ : null;
216
+ if (advancedOutboxEntry)
217
+ await journalOutboxEntry(advancedOutboxEntry);
218
+ }
219
+ if (
220
+ persistedContentMatches &&
221
+ pending.identityMigrationSourceContent !== undefined &&
222
+ latestFileSaveForUnloadRef.current[pending.id] === pending
223
+ ) {
224
+ // Identity repair has landed. Retire its raw-base marker so the next
225
+ // user edit publishes against the canonical bytes it already sees.
226
+ markPendingLocalFileContent(pending.id, pending.content);
227
+ }
123
228
  if (persistedContentMatches && outboxEntry) {
124
229
  await acknowledgeOutboxEntry(outboxEntry);
230
+ }
231
+ if (persistedContentMatches && designId) {
232
+ queryClient.setQueryData(
233
+ ["action", "get-design", { id: designId }],
234
+ (old: any) => {
235
+ if (
236
+ !old ||
237
+ typeof old !== "object" ||
238
+ !Array.isArray(old.files)
239
+ ) {
240
+ return old;
241
+ }
242
+ return {
243
+ ...old,
244
+ files: old.files.map((file: { id?: unknown }) =>
245
+ file.id === pending.id
246
+ ? { ...file, content: pending.content }
247
+ : file,
248
+ ),
249
+ };
250
+ },
251
+ );
125
252
  } else if (!persistedContentMatches) {
126
253
  // A stale/no-op save result is a source conflict, not a lost
127
254
  // connection. Drop the rejected overlay before refetch — leaving
@@ -143,7 +270,6 @@ export function runSaveFileContent(
143
270
  shouldClearLatestUnloadSave(
144
271
  latestFileSaveForUnloadRef.current[pending.id],
145
272
  pending,
146
- !persistedContentMatches,
147
273
  )
148
274
  ) {
149
275
  delete latestFileSaveForUnloadRef.current[pending.id];
@@ -180,6 +306,9 @@ export function runSaveFileContent(
180
306
  if (failureKind === "conflict") {
181
307
  // Roll back our optimistic bytes before the refetch can race ahead.
182
308
  rollbackPendingLocalFileContent(pending.id, pending.content);
309
+ if (latestFileSaveForUnloadRef.current[pending.id] === pending) {
310
+ delete latestFileSaveForUnloadRef.current[pending.id];
311
+ }
183
312
  }
184
313
  void queryClient.invalidateQueries({
185
314
  queryKey: ["action", "get-design"],
@@ -33,6 +33,7 @@ export interface ScreenVisualDuplicateChangeArgs {
33
33
  forcePreviewFullDocument?: boolean;
34
34
  persist?: boolean;
35
35
  recordHistory?: boolean;
36
+ historyBeforeContent?: string;
36
37
  updatedAt?: string;
37
38
  clipboardMutation?: ClipboardContentMutationPublication;
38
39
  },
@@ -193,6 +194,9 @@ export function runScreenVisualDuplicateChange(
193
194
  });
194
195
  return false;
195
196
  }
196
- applyFileContentUpdate(screenId, nextContent, { skipPreview: true });
197
+ applyFileContentUpdate(screenId, nextContent, {
198
+ skipPreview: true,
199
+ historyBeforeContent: baseContent,
200
+ });
197
201
  return true;
198
202
  }
@@ -410,6 +410,13 @@ export interface UndoArgs {
410
410
  createFileMutation: ReturnType<
411
411
  typeof useActionMutation<undefined, undefined, "create-file">
412
412
  >;
413
+ optimisticallyInsertCreatedFile?: (args: {
414
+ fileId: string;
415
+ filename: string;
416
+ fileType: DesignFile["fileType"];
417
+ content: string;
418
+ result?: Record<string, unknown> | null;
419
+ }) => void;
413
420
  deleteFileMutation: ReturnType<
414
421
  typeof useActionMutation<undefined, undefined, "delete-file">
415
422
  >;
@@ -419,7 +426,9 @@ export interface UndoArgs {
419
426
  fileDeletionRedoStackRef: RefObject<FileDeletionHistoryEntry[]>;
420
427
  fileDeletionUndoStackRef: RefObject<FileDeletionHistoryEntry[]>;
421
428
  fileHistoryMutationPendingRef: RefObject<boolean>;
429
+ clearPendingHistory?: () => void;
422
430
  files: DesignFile[];
431
+ filesRef?: RefObject<DesignFile[]>;
423
432
  geometryRedoStackRef: RefObject<GeometryHistoryEntry[]>;
424
433
  geometryUndoStackRef: RefObject<GeometryHistoryEntry[]>;
425
434
  getFreshActiveContent: () => string;
@@ -547,6 +556,7 @@ export function runUndo({
547
556
  contentUndoSelectionStackRef,
548
557
  contentUndoStackRef,
549
558
  createFileMutation,
559
+ optimisticallyInsertCreatedFile,
550
560
  deleteFileMutation,
551
561
  designDataJsonRef,
552
562
  fileCreationRedoStackRef,
@@ -554,7 +564,9 @@ export function runUndo({
554
564
  fileDeletionRedoStackRef,
555
565
  fileDeletionUndoStackRef,
556
566
  fileHistoryMutationPendingRef,
567
+ clearPendingHistory,
557
568
  files,
569
+ filesRef,
558
570
  geometryRedoStackRef,
559
571
  geometryUndoStackRef,
560
572
  getFreshActiveContent,
@@ -609,8 +621,9 @@ export function runUndo({
609
621
  selection: GeometryHistorySelection | undefined,
610
622
  replaySources: Record<string, string> = {},
611
623
  ) => {
624
+ const currentFiles = filesRef?.current ?? files;
612
625
  const actualSources = Object.fromEntries(
613
- files.map((file) => [
626
+ currentFiles.map((file) => [
614
627
  file.id,
615
628
  replaySources[file.id] ?? getScreenContent(file.id),
616
629
  ]),
@@ -1478,6 +1491,17 @@ export function runUndo({
1478
1491
  ...file,
1479
1492
  content: preparedFiles[index]?.content ?? file.content,
1480
1493
  }));
1494
+ entry.files.forEach((file, index) => {
1495
+ const recreatedFile = recreatedEntry.files[index];
1496
+ const prepared = preparedFiles[index];
1497
+ if (!recreatedFile || !prepared) return;
1498
+ optimisticallyInsertCreatedFile?.({
1499
+ fileId: recreatedFile.id,
1500
+ filename: file.filename,
1501
+ fileType: file.fileType,
1502
+ content: prepared.content,
1503
+ });
1504
+ });
1481
1505
  const metadataRestore = restoreMetadataAndGeometry(
1482
1506
  restoreEntry,
1483
1507
  recreatedEntry,
@@ -1548,6 +1572,7 @@ export function runUndo({
1548
1572
  setSelectedLayerIdsState(recreatedEntry.files.map((file) => file.id));
1549
1573
  }
1550
1574
  } catch (error) {
1575
+ clearPendingHistory?.();
1551
1576
  const cleanupResults = await Promise.allSettled(
1552
1577
  recreatedIds.map((fileId) =>
1553
1578
  deleteFileMutation.mutateAsync({