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

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 (66) 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 +1 -0
  9. package/corpus/templates/design/actions/add-localhost-screens.ts +54 -22
  10. package/corpus/templates/design/actions/apply-visual-edit.ts +1 -0
  11. package/corpus/templates/design/actions/create-design-from-template.ts +50 -35
  12. package/corpus/templates/design/actions/create-design-version.ts +21 -0
  13. package/corpus/templates/design/actions/create-file.ts +35 -33
  14. package/corpus/templates/design/actions/delete-design.ts +29 -28
  15. package/corpus/templates/design/actions/delete-file.ts +154 -23
  16. package/corpus/templates/design/actions/generate-design.ts +28 -16
  17. package/corpus/templates/design/actions/get-component-details.ts +187 -33
  18. package/corpus/templates/design/actions/get-design-version.ts +9 -1
  19. package/corpus/templates/design/actions/go-to-main-component.ts +37 -35
  20. package/corpus/templates/design/actions/index-components.ts +241 -126
  21. package/corpus/templates/design/actions/migrate-board-objects-to-file.ts +12 -9
  22. package/corpus/templates/design/actions/present-design-variants.ts +68 -72
  23. package/corpus/templates/design/actions/rename-component.ts +294 -0
  24. package/corpus/templates/design/actions/rename-screen.ts +10 -10
  25. package/corpus/templates/design/actions/update-file.ts +327 -330
  26. package/corpus/templates/design/actions/update-screen-source.ts +2 -43
  27. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +1 -0
  28. package/corpus/templates/design/app/components/design/code-workbench/CodeWorkbench.tsx +8 -1
  29. package/corpus/templates/design/app/components/design/code-workbench/explorer/FileTree.tsx +55 -33
  30. package/corpus/templates/design/app/components/design/code-workbench/workspace/create-providers.ts +2 -0
  31. package/corpus/templates/design/app/components/design/code-workbench/workspace/inline-provider.ts +4 -2
  32. package/corpus/templates/design/app/components/design/edit-panel/element-classification.ts +5 -1
  33. package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +57 -9
  34. package/corpus/templates/design/app/components/design/types.ts +1 -0
  35. package/corpus/templates/design/app/hooks/use-design-versions.ts +3 -2
  36. package/corpus/templates/design/app/i18n-breakpoints.ts +0 -110
  37. package/corpus/templates/design/app/i18n-data.ts +0 -10
  38. package/corpus/templates/design/app/pages/DesignEditor.tsx +250 -107
  39. package/corpus/templates/design/app/pages/design-editor/commands/apply-file-content-update.ts +2 -0
  40. package/corpus/templates/design/app/pages/design-editor/commands/delete-files.ts +69 -47
  41. package/corpus/templates/design/app/pages/design-editor/commands/redo.ts +8 -1
  42. package/corpus/templates/design/app/pages/design-editor/commands/save-file-content.ts +107 -2
  43. package/corpus/templates/design/app/pages/design-editor/commands/undo.ts +26 -1
  44. package/corpus/templates/design/app/pages/design-editor/editor-state.ts +63 -2
  45. package/corpus/templates/design/app/pages/design-editor/nudge-intent.ts +74 -22
  46. package/corpus/templates/design/app/pages/design-editor/source-publication.ts +14 -2
  47. package/corpus/templates/design/changelog/2026-09-16-design-components-retain-linked-identities-when-edited-and-d.md +6 -0
  48. package/corpus/templates/design/changelog/2026-09-17-screen-deletion-is-immediate-and-undoable-with-history-that-.md +6 -0
  49. package/corpus/templates/design/e2e/fixtures/design-component-react/index.html +98 -0
  50. package/corpus/templates/design/e2e/fixtures/design-component-react/inline-component.html +65 -0
  51. package/corpus/templates/design/server/lib/design-data-mutation.ts +9 -5
  52. package/corpus/templates/design/server/lib/design-snapshot.ts +28 -16
  53. package/corpus/templates/design/server/lib/design-versions.ts +208 -130
  54. package/corpus/templates/design/server/lib/fusion-screens.ts +46 -20
  55. package/corpus/templates/design/server/lib/import-design-files.ts +5 -1
  56. package/corpus/templates/design/server/lib/localhost-connection.ts +54 -0
  57. package/corpus/templates/design/server/source-workspace.ts +412 -356
  58. package/corpus/templates/design/shared/component-library.ts +36 -6
  59. package/corpus/templates/design/shared/component-model.ts +8 -2
  60. package/dist/automation/index.d.ts +1 -1
  61. package/dist/client/chat/agentkit-protocol.js +148 -34
  62. package/dist/collab/struct-routes.d.ts +1 -1
  63. package/dist/progress/routes.d.ts +1 -1
  64. package/dist/server/core-routes-plugin.d.ts +1 -1
  65. package/package.json +4 -4
  66. package/corpus/templates/design/app/components/design/editor/PendingScreenDeletionDialog.tsx +0 -66
@@ -272,7 +272,6 @@ import {
272
272
  import { HistoryPanel } from "@/components/design/editor/HistoryPanel";
273
273
  import type { DesignMigrationResult } from "@/components/design/editor/MakeRealDialog";
274
274
  import { MakeRealDialog } from "@/components/design/editor/MakeRealDialog";
275
- import { PendingScreenDeletionDialog } from "@/components/design/editor/PendingScreenDeletionDialog";
276
275
  import { PendingVisualStyleWarningDialog } from "@/components/design/editor/PendingVisualStyleWarningDialog";
277
276
  import { ReadOnlyEditorPanel } from "@/components/design/editor/ReadOnlyEditorPanel";
278
277
  import { SaveTemplateDialog } from "@/components/design/editor/SaveTemplateDialog";
@@ -490,7 +489,6 @@ import {
490
489
  discardDesignSaveOutboxEntry,
491
490
  drainDesignSaveOutbox,
492
491
  journalDesignSaveOutboxEntry,
493
- updateFileResultPersistedContent,
494
492
  type DesignSaveOutboxEntry,
495
493
  } from "@/lib/design-save-outbox";
496
494
  import { isDesignSystemUsableForGeneration } from "@/lib/design-system-data";
@@ -688,7 +686,10 @@ import { runRecordPendingLiveTextEdit } from "./design-editor/commands/record-pe
688
686
  import { runRecordPendingVisualStyleEdit } from "./design-editor/commands/record-pending-visual-style-edit";
689
687
  import { runRedo } from "./design-editor/commands/redo";
690
688
  import { runRenderPngBlob } from "./design-editor/commands/render-png-blob";
691
- import { runSaveFileContent } from "./design-editor/commands/save-file-content";
689
+ import {
690
+ runFileContentSaveKeepalive,
691
+ runSaveFileContent,
692
+ } from "./design-editor/commands/save-file-content";
692
693
  import { runScreenElementSelect } from "./design-editor/commands/screen-element-select";
693
694
  import { runScreenTextContentChange } from "./design-editor/commands/screen-text-content-change";
694
695
  import { runScreenVisualDuplicateChange } from "./design-editor/commands/screen-visual-duplicate-change";
@@ -809,6 +810,7 @@ import {
809
810
  resolveOptimisticTextDecorationLine,
810
811
  resolveServerFiles,
811
812
  shouldRetirePendingLocalFileContent,
813
+ shouldClearLatestUnloadSaveForOutboxEntry,
812
814
  shouldSendKeepalive,
813
815
  type OptimisticTextDecorationLineEntry,
814
816
  type PreviewContentReplaceResult,
@@ -2505,6 +2507,11 @@ function DesignEditor() {
2505
2507
  // Disable every history command while one of those mutations is in flight
2506
2508
  // so a rapid second Cmd+Z cannot race a create against the pending delete.
2507
2509
  const fileHistoryMutationPendingRef = useRef(false);
2510
+ const pendingHistoryDirectionsRef = useRef<Array<"undo" | "redo">>([]);
2511
+ const pendingHistoryDrainScheduledRef = useRef(false);
2512
+ const replayPendingHistoryRef = useRef<
2513
+ ((direction: "undo" | "redo") => void) | null
2514
+ >(null);
2508
2515
  const historyOrderRef = useRef<(UndoRedoOrderKind | "selection")[]>([]);
2509
2516
  const redoOrderRef = useRef<(UndoRedoOrderKind | "selection")[]>([]);
2510
2517
  // Figma parity (ground-truth Round 4): a plain selection change (no
@@ -2531,6 +2538,10 @@ function DesignEditor() {
2531
2538
  redoOrderRef.current = [];
2532
2539
  undoManagerRef.current?.clear(false, true);
2533
2540
  }, []);
2541
+ const clearPendingHistoryDirections = useCallback(() => {
2542
+ pendingHistoryDirectionsRef.current = [];
2543
+ pendingHistoryDrainScheduledRef.current = false;
2544
+ }, []);
2534
2545
  // Figma-parity undo/redo selection restore: snapshots "what's selected
2535
2546
  // right now" from the ref mirrors above (always current — see their doc
2536
2547
  // comment) into a plain GeometryHistorySelection. Reads refs only, so this
@@ -2578,19 +2589,36 @@ function DesignEditor() {
2578
2589
  selection.selectedLayerIds.length === 1
2579
2590
  ? selection.selectedLayerIds[0]
2580
2591
  : undefined;
2581
- pendingOverviewScreenSelectionRef.current = null;
2592
+ const restoredScreenSelectionId =
2593
+ selection.overviewSelectedScreenIds.length === 1 &&
2594
+ selection.selectedLayerIds.length === 1 &&
2595
+ selection.overviewSelectedScreenIds[0] === restoredLayerId
2596
+ ? restoredLayerId
2597
+ : null;
2598
+ pendingOverviewScreenSelectionRef.current = restoredScreenSelectionId;
2582
2599
  pendingOverviewLayerSelectionRef.current =
2583
2600
  restoredLayerId &&
2584
- codeLayerOwnerByNodeIdRef.current.has(restoredLayerId)
2601
+ (codeLayerOwnerByNodeIdRef.current.has(restoredLayerId) ||
2602
+ restoredLayerId === restoredScreenSelectionId)
2585
2603
  ? restoredLayerId
2586
2604
  : null;
2605
+ if (pendingOverviewLayerSelectionRef.current) {
2606
+ schedulePendingOverviewLayerSelectionClear(
2607
+ pendingOverviewLayerSelectionRef.current,
2608
+ );
2609
+ } else {
2610
+ clearPendingOverviewLayerSelectionTimer();
2611
+ }
2587
2612
  setOverviewSelectedScreenIds(selection.overviewSelectedScreenIds);
2588
2613
  setSelectedLayerIdsState(selection.selectedLayerIds);
2589
2614
  if (selection.activeFileId) {
2590
2615
  setActiveFileId(selection.activeFileId);
2591
2616
  }
2592
2617
  },
2593
- [],
2618
+ [
2619
+ clearPendingOverviewLayerSelectionTimer,
2620
+ schedulePendingOverviewLayerSelectionClear,
2621
+ ],
2594
2622
  );
2595
2623
  const syncUndoRedoState = useCallback(() => {
2596
2624
  if (fileHistoryMutationPendingRef.current) {
@@ -2598,6 +2626,33 @@ function DesignEditor() {
2598
2626
  setCanRedo(false);
2599
2627
  return;
2600
2628
  }
2629
+ if (
2630
+ pendingHistoryDirectionsRef.current.length > 0 &&
2631
+ !pendingHistoryDrainScheduledRef.current
2632
+ ) {
2633
+ pendingHistoryDrainScheduledRef.current = true;
2634
+ queueMicrotask(function drainPendingHistory() {
2635
+ if (fileHistoryMutationPendingRef.current) {
2636
+ pendingHistoryDrainScheduledRef.current = false;
2637
+ return;
2638
+ }
2639
+ const direction = pendingHistoryDirectionsRef.current.shift();
2640
+ if (!direction) {
2641
+ pendingHistoryDrainScheduledRef.current = false;
2642
+ return;
2643
+ }
2644
+ replayPendingHistoryRef.current?.(direction);
2645
+ if (fileHistoryMutationPendingRef.current) {
2646
+ pendingHistoryDrainScheduledRef.current = false;
2647
+ return;
2648
+ }
2649
+ if (pendingHistoryDirectionsRef.current.length > 0) {
2650
+ queueMicrotask(drainPendingHistory);
2651
+ } else {
2652
+ pendingHistoryDrainScheduledRef.current = false;
2653
+ }
2654
+ });
2655
+ }
2601
2656
  const undoManager = undoManagerRef.current;
2602
2657
  const canUseOverviewHistory = viewModeRef.current === "overview";
2603
2658
  const activeHistoryFileId = activeFileIdForUndoRef.current;
@@ -3066,6 +3121,7 @@ function DesignEditor() {
3066
3121
  fileDeletionUndoStackRef.current = [];
3067
3122
  fileDeletionRedoStackRef.current = [];
3068
3123
  fileHistoryMutationPendingRef.current = false;
3124
+ clearPendingHistoryDirections();
3069
3125
  selectionUndoStackRef.current = [];
3070
3126
  selectionRedoStackRef.current = [];
3071
3127
  clipboardPasteUndoStackRef.current = [];
@@ -3073,7 +3129,7 @@ function DesignEditor() {
3073
3129
  latestClipboardMutationContentRef.current.clear();
3074
3130
  historyOrderRef.current = [];
3075
3131
  redoOrderRef.current = [];
3076
- }, []);
3132
+ }, [clearPendingHistoryDirections]);
3077
3133
  // U12: record a screen create/duplicate as an undoable entry. Always pushed
3078
3134
  // to the undo stack (screen creation is only meaningful in overview mode's
3079
3135
  // shared chronological history) and clears the redo stack like any other
@@ -3679,6 +3735,7 @@ function DesignEditor() {
3679
3735
  }, [reviewAgentQueueThreadIds, reviewSendingThreadId]);
3680
3736
  const canEditDesignRef = useRef(canEditDesign);
3681
3737
  const rawServerFilesByIdRef = useRef(new Map<string, DesignFile>());
3738
+ const historyFilesRef = useRef<DesignFile[]>([]);
3682
3739
  const pendingLocalFileContentsRef = useRef<
3683
3740
  Map<string, PendingLocalFileContent>
3684
3741
  >(new Map());
@@ -3791,6 +3848,10 @@ function DesignEditor() {
3791
3848
  skipActionQueryInvalidation: true,
3792
3849
  });
3793
3850
  const createFileAsync = createFileMutation.mutateAsync;
3851
+ const createDesignVersionMutation = useActionMutation(
3852
+ "create-design-version",
3853
+ );
3854
+ const createDesignVersionAsync = createDesignVersionMutation.mutateAsync;
3794
3855
  const deleteFileMutation = useActionMutation("delete-file");
3795
3856
  const updateDesignMutation = useActionMutation("update-design");
3796
3857
  const updateDesignAsync = updateDesignMutation.mutateAsync;
@@ -3986,6 +4047,26 @@ function DesignEditor() {
3986
4047
  toast.error(t("designEditor.toasts.saveConflict"), {
3987
4048
  id: "design-save-conflict:outbox",
3988
4049
  });
4050
+ for (const { entry } of result.rebased) {
4051
+ const content = entry.payload.content;
4052
+ if (typeof content === "string") {
4053
+ rollbackPendingLocalFileContent(entry.resourceId, content);
4054
+ }
4055
+ }
4056
+ }
4057
+ for (const entry of [
4058
+ ...result.saved,
4059
+ ...result.rebased.map(({ entry }) => entry),
4060
+ ...result.dropped.map(({ entry }) => entry),
4061
+ ]) {
4062
+ if (
4063
+ shouldClearLatestUnloadSaveForOutboxEntry(
4064
+ latestFileSaveForUnloadRef.current[entry.resourceId],
4065
+ entry,
4066
+ )
4067
+ ) {
4068
+ delete latestFileSaveForUnloadRef.current[entry.resourceId];
4069
+ }
3989
4070
  }
3990
4071
  if (result.saved.length > 0 || result.rebased.length > 0) {
3991
4072
  void queryClient.invalidateQueries({
@@ -4006,6 +4087,7 @@ function DesignEditor() {
4006
4087
  }, [
4007
4088
  designSaveActorScope,
4008
4089
  id,
4090
+ rollbackPendingLocalFileContent,
4009
4091
  queryClient,
4010
4092
  t,
4011
4093
  warnChangesWillRetry,
@@ -4064,7 +4146,8 @@ function DesignEditor() {
4064
4146
  syncCollab: pending.syncCollab,
4065
4147
  operationSource: pending.operationSource,
4066
4148
  operationRevision: pending.operationRevision,
4067
- expectedVersionHash: pending.expectedVersionHash,
4149
+ expectedVersionHash:
4150
+ pending.unloadExpectedVersionHash ?? pending.expectedVersionHash,
4068
4151
  ...(pending.identityMigrationSourceContent !== undefined
4069
4152
  ? { identityOnly: true }
4070
4153
  : {}),
@@ -4137,21 +4220,39 @@ function DesignEditor() {
4137
4220
  },
4138
4221
  ) => {
4139
4222
  if (!canEditDesignRef.current) return;
4223
+ const queuedIdentityMigration = pendingFileSavesRef.current[fileId];
4224
+ const latestIdentityMigration =
4225
+ latestFileSaveForUnloadRef.current[fileId];
4226
+ const identityMigrationIsInFlight =
4227
+ options.identityMigrationSourceContent === undefined &&
4228
+ queuedIdentityMigration === undefined &&
4229
+ latestIdentityMigration?.identityMigrationSourceContent !== undefined;
4230
+ const expectedVersionHash = identityMigrationIsInFlight
4231
+ ? sourceContentHash(latestIdentityMigration.content)
4232
+ : options.expectedVersionHash;
4140
4233
  // Allocate the revision when the edit ENTERS the queue, not when its
4141
4234
  // debounce fires. A pagehide keepalive and the ordinary chained save
4142
4235
  // therefore carry the same idempotency key, while any newer queued edit
4143
4236
  // is guaranteed to have a higher revision even if requests arrive at
4144
4237
  // the server out of order.
4145
- const pending = coalescePendingFileContentSave(
4238
+ const nextPending = coalescePendingFileContentSave(
4146
4239
  createFileContentSaveRequest(
4147
4240
  fileId,
4148
4241
  content,
4149
4242
  options.syncCollab ?? true,
4150
- options.expectedVersionHash,
4243
+ expectedVersionHash,
4151
4244
  options.identityMigrationSourceContent,
4152
4245
  ),
4153
4246
  pendingFileSavesRef.current[fileId],
4154
4247
  );
4248
+ const pending = {
4249
+ ...nextPending,
4250
+ unloadExpectedVersionHash:
4251
+ pendingFileSavesRef.current[fileId]?.unloadExpectedVersionHash ??
4252
+ latestFileSaveForUnloadRef.current[fileId]
4253
+ ?.unloadExpectedVersionHash ??
4254
+ nextPending.expectedVersionHash,
4255
+ };
4155
4256
  markPendingLocalFileContent(
4156
4257
  fileId,
4157
4258
  content,
@@ -4253,25 +4354,17 @@ function DesignEditor() {
4253
4354
  // Keep pagehide mirrors behind the same source-version guard as normal saves.
4254
4355
  const collabLive = pending.syncCollab === false;
4255
4356
  if (!shouldSendKeepalive(true, collabLive)) return;
4256
- const entry = createFileSaveOutboxEntry(pending);
4257
- if (!entry) return;
4258
- void journalOutboxEntry(entry);
4259
- const attempt = tryCallActionKeepalive(
4260
- "update-file",
4261
- entry.payload as any,
4357
+ runFileContentSaveKeepalive(
4358
+ {
4359
+ acknowledgeOutboxEntry,
4360
+ createFileSaveOutboxEntry,
4361
+ journalOutboxEntry,
4362
+ latestFileSaveForUnloadRef,
4363
+ sendKeepalive: (payload) =>
4364
+ tryCallActionKeepalive("update-file", payload as any),
4365
+ },
4366
+ pending,
4262
4367
  );
4263
- if (!attempt.accepted) return;
4264
- void attempt.completion
4265
- .then((result: unknown) => {
4266
- if (!updateFileResultPersistedContent(result, pending.content)) {
4267
- return;
4268
- }
4269
- return acknowledgeOutboxEntry(entry);
4270
- })
4271
- // Pagehide/navigation can intentionally abort this request. The
4272
- // journaled operation remains available for replay, and there is no
4273
- // useful visible surface for a toast while the page is leaving.
4274
- .catch(() => {});
4275
4368
  },
4276
4369
  [acknowledgeOutboxEntry, createFileSaveOutboxEntry, journalOutboxEntry],
4277
4370
  );
@@ -4618,6 +4711,7 @@ function DesignEditor() {
4618
4711
  }),
4619
4712
  [pendingLocalFileContentsSnapshot, serverFiles],
4620
4713
  );
4714
+ historyFilesRef.current = files;
4621
4715
 
4622
4716
  const codeLayerSourceForScreen = useCallback(
4623
4717
  (
@@ -6233,6 +6327,29 @@ function DesignEditor() {
6233
6327
  }) => {
6234
6328
  if (!id) return;
6235
6329
  const now = new Date().toISOString();
6330
+ const optimisticFile: DesignFile = {
6331
+ id: args.fileId,
6332
+ filename: args.filename,
6333
+ fileType: args.fileType,
6334
+ content: annotateScreenHtmlForPersist(args.content, args.fileType),
6335
+ createdAt:
6336
+ typeof args.result?.createdAt === "string"
6337
+ ? args.result.createdAt
6338
+ : now,
6339
+ updatedAt:
6340
+ typeof args.result?.updatedAt === "string"
6341
+ ? args.result.updatedAt
6342
+ : now,
6343
+ };
6344
+ // Queued history replay drains in a microtask, before React can publish
6345
+ // the query-cache update through the rendered files projection.
6346
+ historyFilesRef.current = historyFilesRef.current.some(
6347
+ (file) => file.id === args.fileId,
6348
+ )
6349
+ ? historyFilesRef.current.map((file) =>
6350
+ file.id === args.fileId ? optimisticFile : file,
6351
+ )
6352
+ : [...historyFilesRef.current, optimisticFile];
6236
6353
  void queryClient.cancelQueries({
6237
6354
  queryKey: ["action", "get-design", { id }],
6238
6355
  exact: true,
@@ -6241,20 +6358,6 @@ function DesignEditor() {
6241
6358
  if (!old || typeof old !== "object" || !Array.isArray(old.files)) {
6242
6359
  return old;
6243
6360
  }
6244
- const optimisticFile: DesignFile = {
6245
- id: args.fileId,
6246
- filename: args.filename,
6247
- fileType: args.fileType,
6248
- content: annotateScreenHtmlForPersist(args.content, args.fileType),
6249
- createdAt:
6250
- typeof args.result?.createdAt === "string"
6251
- ? args.result.createdAt
6252
- : now,
6253
- updatedAt:
6254
- typeof args.result?.updatedAt === "string"
6255
- ? args.result.updatedAt
6256
- : now,
6257
- };
6258
6361
  return {
6259
6362
  ...old,
6260
6363
  files: old.files.some((file: DesignFile) => file.id === args.fileId)
@@ -7543,21 +7646,21 @@ function DesignEditor() {
7543
7646
  freshActiveContent: getFreshActiveFileContent({
7544
7647
  activeContent,
7545
7648
  pendingContent: activeFile?.id
7546
- ? pendingLocalFileContentsRef.current.get(activeFile.id)?.content
7649
+ ? (latestFileSaveForUnloadRef.current[activeFile.id]?.content ??
7650
+ pendingLocalFileContentsRef.current.get(activeFile.id)?.content)
7547
7651
  : null,
7548
7652
  latestContent: latestActiveContentRef.current,
7549
7653
  lastLocalContent: lastLocalContentRef.current,
7550
7654
  }),
7551
7655
  fileContentById,
7552
- // Same-tick freshness for NON-ACTIVE screens (see the param's doc on
7553
- // getFreshScreenContent): applyFileContentUpdate writes this ref
7554
- // synchronously via markPendingLocalFileContent, while the
7555
- // files-derived map above only refreshes on the next render. Without
7556
- // it, the second message of a bridge drop sequence (auto-layout
7557
- // conversion style → structure move) rebased off stale content and
7558
- // clobbered the first message's edit.
7656
+ // Same-tick freshness (see the param's doc on getFreshScreenContent):
7657
+ // applyFileContentUpdate writes these refs synchronously, while the
7658
+ // files-derived map above only refreshes on the next render. The
7659
+ // save outbox is also authoritative while reconciliation lags.
7559
7660
  pendingContent:
7560
- pendingLocalFileContentsRef.current.get(screenId)?.content ?? null,
7661
+ latestFileSaveForUnloadRef.current[screenId]?.content ??
7662
+ pendingLocalFileContentsRef.current.get(screenId)?.content ??
7663
+ null,
7561
7664
  }),
7562
7665
  {
7563
7666
  fileId: screenId,
@@ -9131,6 +9234,7 @@ function DesignEditor() {
9131
9234
  refreshPreview?: boolean;
9132
9235
  skipPreview?: boolean;
9133
9236
  forcePreviewFullDocument?: boolean;
9237
+ immediateSave?: boolean;
9134
9238
  persist?: boolean;
9135
9239
  recordHistory?: boolean;
9136
9240
  historyBeforeContent?: string;
@@ -14668,14 +14772,11 @@ function DesignEditor() {
14668
14772
  handleDeleteSelection();
14669
14773
  }, [handleCopySelection, handleDeleteSelection]);
14670
14774
 
14671
- const [pendingScreenDeletion, setPendingScreenDeletion] = useState<{
14672
- files: DesignFile[];
14673
- } | null>(null);
14674
- const [screenDeletionConfirming, setScreenDeletionConfirming] =
14675
- useState(false);
14676
- // Covers the gap before React re-renders after confirm — a second activation
14677
- // must not start another delete while the first mutation is still settling.
14678
- const screenDeletionConfirmingRef = useRef(false);
14775
+ const captureDeleteHistoryCheckpoint = useCallback(async () => {
14776
+ if (!id) throw new Error(t("common.genericError"));
14777
+ const version = await createDesignVersionAsync({ designId: id });
14778
+ return version.id;
14779
+ }, [createDesignVersionAsync, id, t]);
14679
14780
 
14680
14781
  const performDeleteFiles = useCallback(
14681
14782
  (
@@ -14717,6 +14818,8 @@ function DesignEditor() {
14717
14818
  fileCreationUndoStackRef,
14718
14819
  fileDeletionUndoStackRef,
14719
14820
  fileHistoryMutationPendingRef,
14821
+ captureHistoryCheckpoint: captureDeleteHistoryCheckpoint,
14822
+ clearPendingHistory: clearPendingHistoryDirections,
14720
14823
  files,
14721
14824
  geometryRedoStackRef,
14722
14825
  geometryUndoStackRef,
@@ -14746,7 +14849,9 @@ function DesignEditor() {
14746
14849
  [
14747
14850
  activeFile,
14748
14851
  canvasFrameGeometryById,
14852
+ captureDeleteHistoryCheckpoint,
14749
14853
  clearRedoStacks,
14854
+ clearPendingHistoryDirections,
14750
14855
  deleteFileMutation,
14751
14856
  queryClient,
14752
14857
  syncUndoRedoState,
@@ -14754,6 +14859,38 @@ function DesignEditor() {
14754
14859
  writeFrameGeometrySnapshot,
14755
14860
  ],
14756
14861
  );
14862
+ const handleDeleteInlineFile = useCallback(
14863
+ async (fileId: string) => {
14864
+ if (!canEditDesign) return;
14865
+ if (fileHistoryMutationPendingRef.current) {
14866
+ throw new Error(t("common.genericError"));
14867
+ }
14868
+ const queryKey = ["action", "get-design", { id }] as const;
14869
+ let file =
14870
+ files.find((candidate) => candidate.id === fileId) ??
14871
+ rawServerFilesByIdRef.current.get(fileId) ??
14872
+ historyFilesRef.current.find((candidate) => candidate.id === fileId);
14873
+ if (!file) {
14874
+ await queryClient.refetchQueries({ queryKey, exact: true });
14875
+ const refreshed = queryClient.getQueryData<{
14876
+ files?: DesignFile[];
14877
+ }>(queryKey);
14878
+ file = refreshed?.files?.find((candidate) => candidate.id === fileId);
14879
+ }
14880
+ if (!file) throw new Error(t("common.genericError"));
14881
+
14882
+ const targetFile = file;
14883
+ let deleted = false;
14884
+ await performDeleteFiles([targetFile], {
14885
+ recordDeletionHistory: true,
14886
+ onMutationSettled: (deletedFiles) => {
14887
+ deleted = deletedFiles.some((candidate) => candidate.id === fileId);
14888
+ },
14889
+ });
14890
+ if (!deleted) throw new Error(t("common.genericError"));
14891
+ },
14892
+ [canEditDesign, files, id, performDeleteFiles, queryClient, t],
14893
+ );
14757
14894
 
14758
14895
  // MultiScreenCanvas consumes arrow keys in the capture phase while a frame
14759
14896
  // is selected, and an in-screen element selection keeps its screen there.
@@ -14770,19 +14907,20 @@ function DesignEditor() {
14770
14907
  return true;
14771
14908
  }, [files, selectedElement, selectedLayerIdsState]);
14772
14909
 
14773
- // Gate screen deletion behind confirmation, then record the deleted rows as
14774
- // one grouped history entry so Cmd+Z can recreate every selected screen.
14910
+ // Delete selected screens immediately and record the rows as one grouped
14911
+ // history entry so Cmd+Z can recreate every selected screen.
14775
14912
  // Always returns false to MultiScreenCanvas so it never performs its own
14776
- // synchronous local frame-geometry delete ahead of the confirmation.
14913
+ // synchronous local frame-geometry delete ahead of the file mutation.
14777
14914
  const handleDeleteOverviewSelection = useCallback(
14778
14915
  (selectedIds: string[]) => {
14779
14916
  if (!canEditDesign) return false;
14917
+ if (fileHistoryMutationPendingRef.current) return false;
14780
14918
  // BUG-DELETE-OVERVIEW-COLLISION: MultiScreenCanvas consumes Delete in
14781
14919
  // the capture phase whenever a frame is selected, and an in-screen
14782
14920
  // element selection keeps its screen there — so deleting one node in a
14783
14921
  // screen offered to delete the whole screen and the editor's own
14784
14922
  // onDelete hotkey never ran. Route to the element delete instead; the
14785
- // screen-delete confirmation is only for a real frame selection.
14923
+ // Screen deletion is only for a real frame selection.
14786
14924
  if (
14787
14925
  overviewSelectionTargetsElement({
14788
14926
  selectedElement,
@@ -14793,59 +14931,38 @@ function DesignEditor() {
14793
14931
  handleDeleteSelection();
14794
14932
  return false;
14795
14933
  }
14796
- if (!selectedIds.length || files.length <= 1) return false;
14934
+ if (!selectedIds.length || overviewScreens.length <= 1) return false;
14797
14935
 
14798
14936
  const selectedIdSet = new Set(selectedIds);
14799
- const selectedFiles = files.filter((file) => selectedIdSet.has(file.id));
14937
+ const overviewScreenIds = new Set(
14938
+ overviewScreens.map((screen) => screen.id),
14939
+ );
14940
+ const selectedFiles = files.filter(
14941
+ (file) => selectedIdSet.has(file.id) && overviewScreenIds.has(file.id),
14942
+ );
14800
14943
  if (!selectedFiles.length) return false;
14801
14944
 
14802
14945
  const maxDeleteCount =
14803
- selectedFiles.length >= files.length
14804
- ? Math.max(0, files.length - 1)
14946
+ selectedFiles.length >= overviewScreens.length
14947
+ ? Math.max(0, overviewScreens.length - 1)
14805
14948
  : selectedFiles.length;
14806
14949
  const filesToDelete = selectedFiles.slice(0, maxDeleteCount);
14807
14950
  if (!filesToDelete.length) return false;
14808
14951
 
14809
- setPendingScreenDeletion({ files: filesToDelete });
14952
+ performDeleteFiles(filesToDelete, { recordDeletionHistory: true });
14810
14953
  return false;
14811
14954
  },
14812
14955
  [
14813
14956
  canEditDesign,
14814
14957
  files,
14958
+ performDeleteFiles,
14815
14959
  handleDeleteSelection,
14960
+ overviewScreens,
14816
14961
  selectedElement,
14817
14962
  selectedLayerIdsState,
14818
14963
  ],
14819
14964
  );
14820
14965
 
14821
- const handleCancelScreenDeletion = useCallback(() => {
14822
- if (screenDeletionConfirmingRef.current || screenDeletionConfirming) return;
14823
- setPendingScreenDeletion(null);
14824
- }, [screenDeletionConfirming]);
14825
-
14826
- const handleConfirmScreenDeletion = useCallback(() => {
14827
- const pending = pendingScreenDeletion;
14828
- if (
14829
- !pending ||
14830
- screenDeletionConfirmingRef.current ||
14831
- screenDeletionConfirming
14832
- ) {
14833
- return;
14834
- }
14835
- // Keep the dialog locked until delete settles so a rapid second confirm
14836
- // cannot start a concurrent deletion / duplicate history entry.
14837
- screenDeletionConfirmingRef.current = true;
14838
- setScreenDeletionConfirming(true);
14839
- setPendingScreenDeletion(null);
14840
- performDeleteFiles(pending.files, {
14841
- recordDeletionHistory: true,
14842
- onMutationSettled: () => {
14843
- screenDeletionConfirmingRef.current = false;
14844
- setScreenDeletionConfirming(false);
14845
- },
14846
- });
14847
- }, [pendingScreenDeletion, performDeleteFiles, screenDeletionConfirming]);
14848
-
14849
14966
  // ── Props/animation clipboard, transforms, nudge ───────────────────────────
14850
14967
  const handleCopyProps = useCallback(() => {
14851
14968
  if (!selectedElement) return;
@@ -15257,7 +15374,9 @@ function DesignEditor() {
15257
15374
  fileDeletionRedoStackRef,
15258
15375
  fileDeletionUndoStackRef,
15259
15376
  fileHistoryMutationPendingRef,
15377
+ clearPendingHistory: clearPendingHistoryDirections,
15260
15378
  files,
15379
+ filesRef: historyFilesRef,
15261
15380
  geometryRedoStackRef,
15262
15381
  geometryUndoStackRef,
15263
15382
  getFreshActiveContent,
@@ -15272,6 +15391,7 @@ function DesignEditor() {
15272
15391
  localContentRedoStackRef,
15273
15392
  localContentUndoStackRef,
15274
15393
  markPendingLocalFileContent,
15394
+ optimisticallyInsertCreatedFile,
15275
15395
  pendingLiveNonStyleEditsRef,
15276
15396
  pendingLiveNonStyleRedoStackRef,
15277
15397
  pendingLiveNonStyleUndoStackRef,
@@ -15316,6 +15436,7 @@ function DesignEditor() {
15316
15436
  applyGeometryHistoryContentChangesRef,
15317
15437
  applyLocalContentUpdate,
15318
15438
  canEditDesign,
15439
+ clearPendingHistoryDirections,
15319
15440
  createFileMutation,
15320
15441
  deleteFileMutation,
15321
15442
  files,
@@ -15325,6 +15446,7 @@ function DesignEditor() {
15325
15446
  isSynced,
15326
15447
  liveScreenSnapshotsById,
15327
15448
  markPendingLocalFileContent,
15449
+ optimisticallyInsertCreatedFile,
15328
15450
  performDeleteFiles,
15329
15451
  publishAuthoritativeClipboardMutation,
15330
15452
  queryClient,
@@ -15369,7 +15491,9 @@ function DesignEditor() {
15369
15491
  fileDeletionRedoStackRef,
15370
15492
  fileDeletionUndoStackRef,
15371
15493
  fileHistoryMutationPendingRef,
15494
+ clearPendingHistory: clearPendingHistoryDirections,
15372
15495
  files,
15496
+ filesRef: historyFilesRef,
15373
15497
  focusCreatedScreen,
15374
15498
  geometryRedoStackRef,
15375
15499
  geometryUndoStackRef,
@@ -15437,6 +15561,7 @@ function DesignEditor() {
15437
15561
  applyGeometryHistoryContentChangesRef,
15438
15562
  applyLocalContentUpdate,
15439
15563
  canEditDesign,
15564
+ clearPendingHistoryDirections,
15440
15565
  createFileMutation,
15441
15566
  deleteRuntimeElement,
15442
15567
  files,
@@ -15472,6 +15597,10 @@ function DesignEditor() {
15472
15597
  });
15473
15598
  historyDispatchRef.current = { undo: runCurrentUndo, redo: runCurrentRedo };
15474
15599
  const dispatchHistory = useCallback((direction: "undo" | "redo") => {
15600
+ if (fileHistoryMutationPendingRef.current) {
15601
+ pendingHistoryDirectionsRef.current.push(direction);
15602
+ return;
15603
+ }
15475
15604
  const pendingCountBefore =
15476
15605
  pendingVisualStyleEditsRef.current.length +
15477
15606
  pendingLiveNonStyleEditsRef.current.length;
@@ -15490,6 +15619,7 @@ function DesignEditor() {
15490
15619
  : run();
15491
15620
  if (pending) void pending.catch((error) => toast.error(String(error)));
15492
15621
  }, []);
15622
+ replayPendingHistoryRef.current = dispatchHistory;
15493
15623
  const handleUndo = useCallback(
15494
15624
  () => dispatchHistory("undo"),
15495
15625
  [dispatchHistory],
@@ -16733,7 +16863,7 @@ function DesignEditor() {
16733
16863
  // never reaches MultiScreenCanvas' capture-phase Delete.
16734
16864
  onDelete: canEditDesign
16735
16865
  ? () => {
16736
- handleDeleteOverviewSelection(selectedLayerIdsState);
16866
+ handleDeleteOverviewSelection(overviewSelectedScreenIds);
16737
16867
  }
16738
16868
  : undefined,
16739
16869
  // Screen and element rows share the Layers panel's inline rename editor.
@@ -19420,6 +19550,7 @@ function DesignEditor() {
19420
19550
  screenStylePreviewRef.current.delete(screenId);
19421
19551
  }
19422
19552
  applyFileContentUpdate(screenId, next, {
19553
+ immediateSave: !previewOnly,
19423
19554
  persist: !previewOnly,
19424
19555
  recordHistory: !previewOnly,
19425
19556
  historyBeforeContent: previewOnly ? undefined : historyBeforeContent,
@@ -22999,8 +23130,24 @@ function DesignEditor() {
22999
23130
  <DropdownMenuShortcut>{shortcut("$mod+d")}</DropdownMenuShortcut>
23000
23131
  </DropdownMenuItem>
23001
23132
  <DropdownMenuItem
23002
- onClick={handleDeleteSelection}
23003
- disabled={!selectedElement && (!activeFile || files.length <= 1)}
23133
+ onClick={() => {
23134
+ if (viewMode === "overview") {
23135
+ handleDeleteOverviewSelection(overviewSelectedScreenIds);
23136
+ } else {
23137
+ handleDeleteSelection();
23138
+ }
23139
+ }}
23140
+ disabled={
23141
+ viewMode === "overview"
23142
+ ? !overviewSelectionTargetsElement({
23143
+ selectedElement,
23144
+ selectedLayerIds: selectedLayerIdsState,
23145
+ fileIds: files.map((file) => file.id),
23146
+ }) &&
23147
+ (overviewScreens.length <= 1 ||
23148
+ overviewSelectedScreenIds.length === 0)
23149
+ : !selectedElement && !activeFile
23150
+ }
23004
23151
  >
23005
23152
  {"Delete" /* i18n-ignore design menu command */}
23006
23153
  <DropdownMenuShortcut>⌫</DropdownMenuShortcut>
@@ -24192,6 +24339,9 @@ function DesignEditor() {
24192
24339
  selectedNodeId={selectedElementLayerId}
24193
24340
  selectedSelector={selectedCanvasSelector}
24194
24341
  canEdit={canEditDesign}
24342
+ onDeleteInlineFile={
24343
+ canEditDesign ? handleDeleteInlineFile : undefined
24344
+ }
24195
24345
  onActiveFileChange={setActiveCodeFile}
24196
24346
  localhostConnections={workbenchLocalhostConnections}
24197
24347
  onRequestLocalWriteConsent={
@@ -24365,7 +24515,7 @@ function DesignEditor() {
24365
24515
  canDelete={Boolean(
24366
24516
  canEditDesign &&
24367
24517
  (selectedElement ||
24368
- (selectedScreenIds.length > 0 && files.length > 1)),
24518
+ (selectedScreenIds.length > 0 && overviewScreens.length > 1)),
24369
24519
  )}
24370
24520
  canReorder={canEditDesign && Boolean(selectedElement)}
24371
24521
  // Rename is only offered for a single selectable layer target;
@@ -25464,13 +25614,6 @@ function DesignEditor() {
25464
25614
  }}
25465
25615
  />
25466
25616
 
25467
- <PendingScreenDeletionDialog
25468
- pendingScreenDeletion={pendingScreenDeletion}
25469
- onCancel={handleCancelScreenDeletion}
25470
- onConfirm={handleConfirmScreenDeletion}
25471
- confirming={screenDeletionConfirming}
25472
- />
25473
-
25474
25617
  {/* ── Render: motion dock ── */}
25475
25618
  {/* Motion dock (§6.3) — bottom timeline mounted while opening, open, or
25476
25619
  closing. Canvas remains visible above.