@agent-native/core 0.163.4 → 0.164.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (228) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/app/components/agents/FeatureFlagsFleetPanel.tsx +78 -19
  3. package/corpus/templates/analytics/app/hooks/use-dashboard-chat-context.ts +29 -0
  4. package/corpus/templates/analytics/app/lib/use-auto-focus-select.ts +4 -4
  5. package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/index.tsx +3 -0
  6. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +6 -0
  7. package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +1 -2
  8. package/corpus/templates/analytics/app/routes/_index.tsx +3 -2
  9. package/corpus/templates/analytics/app/routes/adhoc.$id.tsx +7 -2
  10. package/corpus/templates/analytics/app/routes/dashboard.tsx +3 -2
  11. package/corpus/templates/analytics/app/routes/overview.tsx +3 -2
  12. package/corpus/templates/analytics/app/routes/traffic.tsx +3 -2
  13. package/corpus/templates/analytics/changelog/2026-08-18-verify-cross-app-feature-flag-changes.md +6 -0
  14. package/corpus/templates/analytics/netlify.toml +2 -0
  15. package/corpus/templates/analytics/server/lib/workspace-feature-flags.ts +196 -48
  16. package/corpus/templates/analytics/server/plugins/feature-flags.ts +5 -0
  17. package/corpus/templates/analytics/shared/feature-flags.ts +10 -0
  18. package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +5 -1
  19. package/corpus/templates/assets/app/routes/brand-kits._index.tsx +4 -1
  20. package/corpus/templates/assets/app/routes/libraries.tsx +4 -1
  21. package/corpus/templates/assets/app/routes/picker.tsx +4 -1
  22. package/corpus/templates/assets/netlify.toml +2 -0
  23. package/corpus/templates/brain/netlify.toml +2 -0
  24. package/corpus/templates/calendar/app/components/calendar/DayView.tsx +2 -1
  25. package/corpus/templates/calendar/changelog/2026-08-19-calendar-page-loads-stay-fast-after-idle-periods.md +6 -0
  26. package/corpus/templates/calendar/netlify.toml +2 -0
  27. package/corpus/templates/chat/netlify.toml +2 -0
  28. package/corpus/templates/clips/app/components/player/player-controls.tsx +3 -3
  29. package/corpus/templates/clips/app/components/player/reaction-emojis.ts +12 -0
  30. package/corpus/templates/clips/app/components/player/reactions-tray.tsx +90 -9
  31. package/corpus/templates/clips/app/components/player/scrubber.tsx +43 -10
  32. package/corpus/templates/clips/app/components/player/video-player.tsx +105 -15
  33. package/corpus/templates/clips/app/routes/_app.settings._index.tsx +1 -2
  34. package/corpus/templates/clips/app/routes/_app.wispr.tsx +2 -1
  35. package/corpus/templates/clips/app/routes/_index.tsx +7 -3
  36. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +13 -8
  37. package/corpus/templates/clips/app/routes/share.$shareId.tsx +15 -8
  38. package/corpus/templates/clips/netlify.toml +2 -0
  39. package/corpus/templates/clips/server/routes/[...page].head.ts +5 -0
  40. package/corpus/templates/content/netlify.toml +2 -0
  41. package/corpus/templates/design/.agents/skills/design-editor-architecture/SKILL.md +103 -0
  42. package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +2 -1
  43. package/corpus/templates/design/AGENTS.md +3 -1
  44. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +3 -2
  45. package/corpus/templates/design/app/components/design/editor/AgentNativeMenuMark.tsx +25 -0
  46. package/corpus/templates/design/app/components/design/editor/DesignBottomToolbar.tsx +411 -0
  47. package/corpus/templates/design/app/components/design/editor/DesignCollaborators.tsx +153 -0
  48. package/corpus/templates/design/app/components/design/editor/DesignWorkspaceRail.tsx +231 -0
  49. package/corpus/templates/design/app/components/design/editor/MakeRealDialog.tsx +223 -0
  50. package/corpus/templates/design/app/components/design/editor/PendingScreenDeletionDialog.tsx +58 -0
  51. package/corpus/templates/design/app/components/design/editor/PendingVisualStyleWarningDialog.tsx +56 -0
  52. package/corpus/templates/design/app/components/design/editor/ReadOnlyEditorPanel.tsx +23 -0
  53. package/corpus/templates/design/app/components/design/editor/SaveTemplateDialog.tsx +161 -0
  54. package/corpus/templates/design/app/components/design/editor/toolbar-controls.tsx +185 -0
  55. package/corpus/templates/design/app/pages/DesignEditor.tsx +20913 -0
  56. package/corpus/templates/design/app/pages/design-editor/command-types.ts +120 -0
  57. package/corpus/templates/design/app/pages/design-editor/commands/README.md +193 -0
  58. package/corpus/templates/design/app/pages/design-editor/commands/add-auto-layout.ts +416 -0
  59. package/corpus/templates/design/app/pages/design-editor/commands/add-screen.ts +112 -0
  60. package/corpus/templates/design/app/pages/design-editor/commands/align-selection.ts +183 -0
  61. package/corpus/templates/design/app/pages/design-editor/commands/apply-design-editor-command.ts +243 -0
  62. package/corpus/templates/design/app/pages/design-editor/commands/apply-file-content-update.ts +236 -0
  63. package/corpus/templates/design/app/pages/design-editor/commands/apply-local-content-update.ts +304 -0
  64. package/corpus/templates/design/app/pages/design-editor/commands/apply-pending-visual-styles-with-agent.ts +376 -0
  65. package/corpus/templates/design/app/pages/design-editor/commands/apply-to-source.ts +132 -0
  66. package/corpus/templates/design/app/pages/design-editor/commands/can-move-layer.ts +117 -0
  67. package/corpus/templates/design/app/pages/design-editor/commands/change-selected-z-index.ts +212 -0
  68. package/corpus/templates/design/app/pages/design-editor/commands/commit-relative-style-delta-to-selected-layers.ts +176 -0
  69. package/corpus/templates/design/app/pages/design-editor/commands/commit-styles-to-selected-layers.ts +175 -0
  70. package/corpus/templates/design/app/pages/design-editor/commands/commit-visual-styles.ts +710 -0
  71. package/corpus/templates/design/app/pages/design-editor/commands/confirm-make-real.ts +99 -0
  72. package/corpus/templates/design/app/pages/design-editor/commands/copy-as-figma-svg.ts +95 -0
  73. package/corpus/templates/design/app/pages/design-editor/commands/copy-selection.ts +156 -0
  74. package/corpus/templates/design/app/pages/design-editor/commands/create-primitive.ts +291 -0
  75. package/corpus/templates/design/app/pages/design-editor/commands/create-screen-frame.ts +131 -0
  76. package/corpus/templates/design/app/pages/design-editor/commands/cross-screen-element-drop.ts +577 -0
  77. package/corpus/templates/design/app/pages/design-editor/commands/delete-files.ts +364 -0
  78. package/corpus/templates/design/app/pages/design-editor/commands/delete-selection.ts +521 -0
  79. package/corpus/templates/design/app/pages/design-editor/commands/detach-instance-menu-action.ts +88 -0
  80. package/corpus/templates/design/app/pages/design-editor/commands/distribute-selection.ts +114 -0
  81. package/corpus/templates/design/app/pages/design-editor/commands/download-all-screens-pdf.ts +116 -0
  82. package/corpus/templates/design/app/pages/design-editor/commands/download-pdf.ts +103 -0
  83. package/corpus/templates/design/app/pages/design-editor/commands/download-svg.ts +159 -0
  84. package/corpus/templates/design/app/pages/design-editor/commands/duplicate-screen.ts +201 -0
  85. package/corpus/templates/design/app/pages/design-editor/commands/duplicate-selection.ts +279 -0
  86. package/corpus/templates/design/app/pages/design-editor/commands/editor-paste.ts +81 -0
  87. package/corpus/templates/design/app/pages/design-editor/commands/enter-hotkey.ts +155 -0
  88. package/corpus/templates/design/app/pages/design-editor/commands/enter-single-screen.ts +168 -0
  89. package/corpus/templates/design/app/pages/design-editor/commands/escape-hotkey.ts +212 -0
  90. package/corpus/templates/design/app/pages/design-editor/commands/frame-selection.ts +119 -0
  91. package/corpus/templates/design/app/pages/design-editor/commands/geometry-commit.ts +169 -0
  92. package/corpus/templates/design/app/pages/design-editor/commands/get-selected-layer-snapshots.ts +177 -0
  93. package/corpus/templates/design/app/pages/design-editor/commands/group-selection.ts +127 -0
  94. package/corpus/templates/design/app/pages/design-editor/commands/iframe-context-menu.ts +121 -0
  95. package/corpus/templates/design/app/pages/design-editor/commands/import-figma-clipboard-into-design.ts +119 -0
  96. package/corpus/templates/design/app/pages/design-editor/commands/layer-marquee-selection-change.ts +163 -0
  97. package/corpus/templates/design/app/pages/design-editor/commands/layer-move-to-screen.ts +331 -0
  98. package/corpus/templates/design/app/pages/design-editor/commands/layer-move.ts +517 -0
  99. package/corpus/templates/design/app/pages/design-editor/commands/layer-rename.ts +247 -0
  100. package/corpus/templates/design/app/pages/design-editor/commands/layer-selection-change.ts +161 -0
  101. package/corpus/templates/design/app/pages/design-editor/commands/mode-change.ts +122 -0
  102. package/corpus/templates/design/app/pages/design-editor/commands/nudge-selection.ts +213 -0
  103. package/corpus/templates/design/app/pages/design-editor/commands/overview-primitive-reparent.ts +318 -0
  104. package/corpus/templates/design/app/pages/design-editor/commands/paste-copied-screens.ts +114 -0
  105. package/corpus/templates/design/app/pages/design-editor/commands/paste-over-selection.ts +70 -0
  106. package/corpus/templates/design/app/pages/design-editor/commands/paste-selection.ts +436 -0
  107. package/corpus/templates/design/app/pages/design-editor/commands/paste-to-replace.ts +135 -0
  108. package/corpus/templates/design/app/pages/design-editor/commands/pasted-image-files.ts +209 -0
  109. package/corpus/templates/design/app/pages/design-editor/commands/persist-frame-geometry-save.ts +113 -0
  110. package/corpus/templates/design/app/pages/design-editor/commands/persist-tweak-save.ts +160 -0
  111. package/corpus/templates/design/app/pages/design-editor/commands/primitive-created.ts +168 -0
  112. package/corpus/templates/design/app/pages/design-editor/commands/record-pending-live-layer-state-edit.ts +149 -0
  113. package/corpus/templates/design/app/pages/design-editor/commands/record-pending-live-structure-edit.ts +200 -0
  114. package/corpus/templates/design/app/pages/design-editor/commands/record-pending-live-text-edit.ts +152 -0
  115. package/corpus/templates/design/app/pages/design-editor/commands/record-pending-visual-style-edit.ts +247 -0
  116. package/corpus/templates/design/app/pages/design-editor/commands/redo.ts +1086 -0
  117. package/corpus/templates/design/app/pages/design-editor/commands/render-png-blob.ts +200 -0
  118. package/corpus/templates/design/app/pages/design-editor/commands/save-file-content.ts +207 -0
  119. package/corpus/templates/design/app/pages/design-editor/commands/screen-element-select.ts +262 -0
  120. package/corpus/templates/design/app/pages/design-editor/commands/screen-text-content-change.ts +216 -0
  121. package/corpus/templates/design/app/pages/design-editor/commands/screen-visual-duplicate-change.ts +108 -0
  122. package/corpus/templates/design/app/pages/design-editor/commands/screen-visual-structure-change.ts +280 -0
  123. package/corpus/templates/design/app/pages/design-editor/commands/screen-visual-style-change.ts +151 -0
  124. package/corpus/templates/design/app/pages/design-editor/commands/send-overview-annotations.ts +88 -0
  125. package/corpus/templates/design/app/pages/design-editor/commands/send-runtime-layer-move-semantic-handoff.ts +122 -0
  126. package/corpus/templates/design/app/pages/design-editor/commands/send-runtime-layer-semantic-handoff.ts +143 -0
  127. package/corpus/templates/design/app/pages/design-editor/commands/send-runtime-layer-state-semantic-handoff.ts +115 -0
  128. package/corpus/templates/design/app/pages/design-editor/commands/start-retry-generation.ts +152 -0
  129. package/corpus/templates/design/app/pages/design-editor/commands/start-sidebar-resize.ts +99 -0
  130. package/corpus/templates/design/app/pages/design-editor/commands/style-change.ts +152 -0
  131. package/corpus/templates/design/app/pages/design-editor/commands/styles-change.ts +155 -0
  132. package/corpus/templates/design/app/pages/design-editor/commands/suggest-auto-layout.ts +125 -0
  133. package/corpus/templates/design/app/pages/design-editor/commands/text-content-change.ts +201 -0
  134. package/corpus/templates/design/app/pages/design-editor/commands/tidy-up.ts +123 -0
  135. package/corpus/templates/design/app/pages/design-editor/commands/toggle-layer-hidden.ts +167 -0
  136. package/corpus/templates/design/app/pages/design-editor/commands/toggle-layer-locked.ts +175 -0
  137. package/corpus/templates/design/app/pages/design-editor/commands/toggle-motion-keyframe.ts +99 -0
  138. package/corpus/templates/design/app/pages/design-editor/commands/tweak-prompt-submit.ts +84 -0
  139. package/corpus/templates/design/app/pages/design-editor/commands/undo.ts +906 -0
  140. package/corpus/templates/design/app/pages/design-editor/commands/ungroup-selection.ts +170 -0
  141. package/corpus/templates/design/app/pages/design-editor/commands/visual-duplicate-change.ts +119 -0
  142. package/corpus/templates/design/app/pages/design-editor/commands/visual-structure-change.ts +274 -0
  143. package/corpus/templates/design/app/pages/design-editor/commands/write-frame-geometry-snapshot.ts +99 -0
  144. package/corpus/templates/design/app/pages/design-editor/derive/design-breakpoints.ts +56 -0
  145. package/corpus/templates/design/app/pages/design-editor/derive/overview-screens.ts +146 -0
  146. package/corpus/templates/design/app/pages/design-editor/domains/use-tweaks.ts +326 -0
  147. package/corpus/templates/design/app/pages/design-editor/editor-constants.ts +54 -0
  148. package/corpus/templates/design/app/pages/design-editor/editor-helpers.ts +183 -0
  149. package/corpus/templates/design/app/pages/design-editor/effects/adopt-db-file-content.ts +270 -0
  150. package/corpus/templates/design/app/pages/design-editor/effects/mirror-selection-to-agent-chat.ts +134 -0
  151. package/corpus/templates/design/app/pages/design-editor/effects/motion-autosave.ts +335 -0
  152. package/corpus/templates/design/app/pages/design-editor/effects/observe-collab-text.ts +183 -0
  153. package/corpus/templates/design/app/pages/design-editor/effects/publish-agent-selection-context.ts +252 -0
  154. package/corpus/templates/design/app/pages/design-editor/effects/resume-pending-generation.ts +178 -0
  155. package/corpus/templates/design/app/pages/design-editor/effects/seed-collab-content.ts +157 -0
  156. package/corpus/templates/design/app/pages/design-editor/preview-html.ts +19 -0
  157. package/corpus/templates/design/app/routes/examples.tsx +2 -1
  158. package/corpus/templates/design/netlify.toml +2 -0
  159. package/corpus/templates/design/shared/board-file.ts +2 -1
  160. package/corpus/templates/dispatch/netlify.toml +2 -0
  161. package/corpus/templates/forms/app/routes/_index.tsx +3 -2
  162. package/corpus/templates/forms/netlify.toml +2 -0
  163. package/corpus/templates/mail/app/routes/_index.tsx +3 -2
  164. package/corpus/templates/mail/netlify.toml +2 -0
  165. package/corpus/templates/plan/netlify.toml +2 -0
  166. package/corpus/templates/slides/netlify.toml +2 -0
  167. package/corpus/templates/tasks/app/routes/_index.tsx +2 -1
  168. package/dist/agent/engine/builder-engine.d.ts +6 -4
  169. package/dist/agent/engine/builder-engine.js +91 -26
  170. package/dist/agent/engine/registry.js +16 -0
  171. package/dist/cli/code-agent-agent-tools.d.ts +2 -0
  172. package/dist/cli/code-agent-agent-tools.js +188 -0
  173. package/dist/cli/code-agent-collaboration.d.ts +34 -0
  174. package/dist/cli/code-agent-collaboration.js +104 -0
  175. package/dist/cli/code-agent-executor.js +6 -0
  176. package/dist/cli/code-agent-schedules.d.ts +54 -0
  177. package/dist/cli/code-agent-schedules.js +198 -0
  178. package/dist/client/AgentPanel.js +1 -1
  179. package/dist/client/ConnectBuilderCard.js +5 -2
  180. package/dist/client/EnvironmentBadge.js +6 -5
  181. package/dist/client/chat/run-recovery.js +3 -2
  182. package/dist/client/chat/tool-call-display.js +2 -3
  183. package/dist/client/components/CodeRequiredDialog.js +4 -2
  184. package/dist/client/i18n.js +5 -4
  185. package/dist/client/settings/SettingsPanel.js +1 -1
  186. package/dist/client/settings/VoiceTranscriptionSection.js +1 -1
  187. package/dist/client/settings/useBuilderStatus.d.ts +6 -1
  188. package/dist/client/settings/useBuilderStatus.js +22 -33
  189. package/dist/client/transcription/BuilderTranscriptionCta.js +1 -1
  190. package/dist/code-agents/index.d.ts +2 -0
  191. package/dist/code-agents/index.js +2 -0
  192. package/dist/collab/awareness.d.ts +2 -2
  193. package/dist/collab/routes.d.ts +1 -1
  194. package/dist/deploy/build.js +10 -4
  195. package/dist/localization/shared.d.ts +2 -0
  196. package/dist/localization/shared.js +5 -0
  197. package/dist/mcp-client/oauth-client.d.ts +1 -0
  198. package/dist/mcp-client/oauth-client.js +50 -1
  199. package/dist/notifications/routes.d.ts +3 -3
  200. package/dist/observability/routes.d.ts +1 -1
  201. package/dist/provider-api/actions/provider-api.d.ts +2 -2
  202. package/dist/server/auth-marketing-locales.d.ts +7 -0
  203. package/dist/server/auth-marketing-locales.js +1322 -0
  204. package/dist/server/auth-marketing.d.ts +1 -0
  205. package/dist/server/auth-marketing.js +35 -6
  206. package/dist/server/auth.js +4 -17
  207. package/dist/server/builder-browser.d.ts +22 -0
  208. package/dist/server/builder-browser.js +77 -1
  209. package/dist/server/builder-oauth.d.ts +50 -0
  210. package/dist/server/builder-oauth.js +263 -0
  211. package/dist/server/core-routes-plugin.d.ts +11 -0
  212. package/dist/server/core-routes-plugin.js +291 -415
  213. package/dist/server/credential-provider.d.ts +1 -1
  214. package/dist/server/credential-provider.js +1 -1
  215. package/dist/server/csrf.js +2 -2
  216. package/dist/server/onboarding-html.js +52 -120
  217. package/dist/server/ssr-handler.js +13 -4
  218. package/dist/settings/store.d.ts +9 -0
  219. package/dist/settings/store.js +19 -0
  220. package/dist/shared/cache-control.d.ts +22 -0
  221. package/dist/shared/cache-control.js +25 -0
  222. package/dist/shared/index.d.ts +1 -0
  223. package/dist/shared/index.js +1 -0
  224. package/dist/templates/chat/netlify.toml +2 -0
  225. package/docs/content/deployment.mdx +2 -0
  226. package/docs/content/multi-app-workspace.mdx +1 -1
  227. package/package.json +2 -2
  228. package/src/templates/chat/netlify.toml +2 -0
@@ -0,0 +1,906 @@
1
+ import { useActionMutation } from "@agent-native/core/client/hooks";
2
+ import type { CanvasFrameGeometryById } from "@shared/canvas-frames";
3
+ import type { QueryClient } from "@tanstack/react-query";
4
+ import type { Dispatch, RefObject, SetStateAction } from "react";
5
+ import { toast } from "sonner";
6
+ import * as Y from "yjs";
7
+
8
+ import { trace } from "@/components/design/design-trace";
9
+ import type { ElementInfo } from "@/components/design/types";
10
+ import type {
11
+ ClipboardContentLineage,
12
+ ClipboardContentMutationOrigin,
13
+ ClipboardContentMutationPublication,
14
+ } from "@/lib/clipboard-content-lineage";
15
+ import {
16
+ refreshElementInfoFromContent,
17
+ refreshSelectedLayerIdsFromContent,
18
+ } from "@/pages/design-editor/code-layer-state";
19
+ import type { LiveScreenSnapshot } from "@/pages/design-editor/command-types";
20
+ import {
21
+ getCanvasFrameGeometry,
22
+ staleGeometryFrameIds,
23
+ viewportChangedFrameIds,
24
+ } from "@/pages/design-editor/design-data-geometry-utils";
25
+ import type {
26
+ PreviewContentReplaceResult,
27
+ UndoRedoOrderKind,
28
+ } from "@/pages/design-editor/editor-state";
29
+ import { previewContentReplaceNeedsRenderFallback } from "@/pages/design-editor/editor-state";
30
+ import type {
31
+ ContentHistoryChange,
32
+ ContentHistoryEntry,
33
+ FileCreationHistoryEntry,
34
+ FileDeletionHistoryEntry,
35
+ GeometryHistoryEntry,
36
+ GeometryHistorySelection,
37
+ } from "@/pages/design-editor/history";
38
+ import {
39
+ MAX_DESIGN_UNDO_STACK,
40
+ applyGeometryHistoryDiff,
41
+ findLastContentHistoryChangeIndex,
42
+ getAvailableContentHistoryChanges,
43
+ remapFileDeletionHistoryEntryIds,
44
+ } from "@/pages/design-editor/history";
45
+ import type {
46
+ PendingLiveNonStyleEdit,
47
+ PendingLiveNonStyleUndoEntry,
48
+ PendingVisualStyleEdit,
49
+ PendingVisualStyleUndoEntry,
50
+ } from "@/pages/design-editor/pending-edits";
51
+ import {
52
+ mergePendingLiveNonStyleEdits,
53
+ mergePendingVisualStyleEdits,
54
+ } from "@/pages/design-editor/pending-edits";
55
+ import { pendingEditTargetsSelectedElement } from "@/pages/design-editor/selection-state";
56
+ import type { DesignFile } from "@/pages/design-editor/types";
57
+
58
+ export interface UndoArgs {
59
+ activeEditorDragRef: RefObject<boolean>;
60
+ activeFile: DesignFile;
61
+ applyFileContentUpdate: (
62
+ fileId: string,
63
+ nextContent: string,
64
+ options?: {
65
+ refreshPreview?: boolean;
66
+ skipPreview?: boolean;
67
+ forcePreviewFullDocument?: boolean;
68
+ persist?: boolean;
69
+ recordHistory?: boolean;
70
+ updatedAt?: string;
71
+ clipboardMutation?: ClipboardContentMutationPublication;
72
+ },
73
+ ) => void;
74
+ applyLocalContentUpdate: (
75
+ nextContent: string,
76
+ options?: {
77
+ refreshPreview?: boolean;
78
+ skipPreview?: boolean;
79
+ forcePreviewFullDocument?: boolean;
80
+ immediateSave?: boolean;
81
+ persist?: boolean;
82
+ recordHistory?: boolean;
83
+ historyBeforeContent?: string;
84
+ updatedAt?: string;
85
+ clipboardMutation?: ClipboardContentMutationPublication;
86
+ },
87
+ ) => void;
88
+ canEditDesign: boolean;
89
+ clipboardPasteRedoStackRef: RefObject<ContentHistoryChange[]>;
90
+ clipboardPasteUndoStackRef: RefObject<ContentHistoryChange[]>;
91
+ contentRedoSelectionStackRef: RefObject<
92
+ (GeometryHistorySelection | undefined)[]
93
+ >;
94
+ contentRedoStackRef: RefObject<ContentHistoryEntry[]>;
95
+ contentUndoSelectionStackRef: RefObject<
96
+ (GeometryHistorySelection | undefined)[]
97
+ >;
98
+ contentUndoStackRef: RefObject<ContentHistoryEntry[]>;
99
+ createFileMutation: ReturnType<
100
+ typeof useActionMutation<undefined, undefined, "create-file">
101
+ >;
102
+ deleteFileMutation: ReturnType<
103
+ typeof useActionMutation<undefined, undefined, "delete-file">
104
+ >;
105
+ designDataJsonRef: RefObject<Record<string, unknown>>;
106
+ fileCreationRedoStackRef: RefObject<FileCreationHistoryEntry[]>;
107
+ fileCreationUndoStackRef: RefObject<FileCreationHistoryEntry[]>;
108
+ fileDeletionRedoStackRef: RefObject<FileDeletionHistoryEntry[]>;
109
+ fileDeletionUndoStackRef: RefObject<FileDeletionHistoryEntry[]>;
110
+ fileHistoryMutationPendingRef: RefObject<boolean>;
111
+ files: DesignFile[];
112
+ geometryRedoStackRef: RefObject<GeometryHistoryEntry[]>;
113
+ geometryUndoStackRef: RefObject<GeometryHistoryEntry[]>;
114
+ getFreshActiveContent: () => string;
115
+ getScreenContent: (screenId: string) => string;
116
+ historyOrderRef: RefObject<UndoRedoOrderKind[]>;
117
+ id: string | undefined;
118
+ isSynced: boolean;
119
+ lastLocalContentRef: RefObject<string | null>;
120
+ latestClipboardMutationContentRef: RefObject<
121
+ Map<string, ClipboardContentLineage>
122
+ >;
123
+ liveFrameGeometryRef: RefObject<CanvasFrameGeometryById>;
124
+ liveScreenSnapshotsById: Record<string, LiveScreenSnapshot>;
125
+ localContentRedoStackRef: RefObject<ContentHistoryChange[]>;
126
+ localContentUndoStackRef: RefObject<ContentHistoryChange[]>;
127
+ markPendingLocalFileContent: (
128
+ fileId: string,
129
+ content: string,
130
+ baseUpdatedAt?: string | null,
131
+ ) => void;
132
+ pendingLiveNonStyleEditsRef: RefObject<PendingLiveNonStyleEdit[]>;
133
+ pendingLiveNonStyleRedoStackRef: RefObject<PendingLiveNonStyleUndoEntry[]>;
134
+ pendingLiveNonStyleUndoStackRef: RefObject<PendingLiveNonStyleUndoEntry[]>;
135
+ pendingLocalFileContentsRef: RefObject<
136
+ Map<
137
+ string,
138
+ { content: string; startedAt: number; baseUpdatedAt?: string | null }
139
+ >
140
+ >;
141
+ pendingVisualStyleEditsRef: RefObject<PendingVisualStyleEdit[]>;
142
+ pendingVisualStyleRedoStackRef: RefObject<PendingVisualStyleUndoEntry[]>;
143
+ pendingVisualStyleUndoStackRef: RefObject<PendingVisualStyleUndoEntry[]>;
144
+ performDeleteFiles: (
145
+ filesToDelete: DesignFile[],
146
+ options?: {
147
+ skipFileCreationRedoPrune?: boolean;
148
+ recordDeletionHistory?: boolean;
149
+ onMutationSettled?: (
150
+ deletedFiles: DesignFile[],
151
+ failedFiles: DesignFile[],
152
+ ) => void;
153
+ },
154
+ ) => void;
155
+ publishAuthoritativeClipboardMutation: (args: {
156
+ fileId: string;
157
+ baseContent: string;
158
+ nextContent: string;
159
+ origin: ClipboardContentMutationOrigin;
160
+ }) => ClipboardContentMutationPublication | null;
161
+ queryClient: QueryClient;
162
+ queueFileContentSave: (
163
+ fileId: string,
164
+ content: string,
165
+ options?: { syncCollab?: boolean; immediate?: boolean },
166
+ ) => void;
167
+ redoOrderRef: RefObject<UndoRedoOrderKind[]>;
168
+ replacePreviewContent: (
169
+ nextContent: string,
170
+ selector?: string | null,
171
+ options?: { forceFullDocument?: boolean },
172
+ ) => PreviewContentReplaceResult;
173
+ requestPendingLiveNonStyleRevert: (
174
+ edits: readonly PendingLiveNonStyleEdit[],
175
+ ) => void;
176
+ requestPendingVisualStyleRevert: (
177
+ edits: readonly PendingVisualStyleEdit[],
178
+ ) => void;
179
+ restoreSelectionSnapshot: (
180
+ selection: GeometryHistorySelection | undefined,
181
+ ) => void;
182
+ setActiveFileId: Dispatch<SetStateAction<string | null>>;
183
+ setContentRenderRevision: Dispatch<SetStateAction<number>>;
184
+ setHoveredElement: Dispatch<SetStateAction<ElementInfo | null>>;
185
+ setOverviewSelectedScreenIds: Dispatch<SetStateAction<string[]>>;
186
+ setPendingLiveNonStyleEdits: Dispatch<
187
+ SetStateAction<PendingLiveNonStyleEdit[]>
188
+ >;
189
+ setPendingVisualStyleEdits: Dispatch<
190
+ SetStateAction<PendingVisualStyleEdit[]>
191
+ >;
192
+ setSelectedElement: Dispatch<SetStateAction<ElementInfo | null>>;
193
+ setSelectedLayerIdsState: Dispatch<SetStateAction<string[]>>;
194
+ suppressContentHistoryRef: RefObject<boolean>;
195
+ syncLiveScreenSnapshotPreview: (screenId: string, html: string) => void;
196
+ syncUndoRedoState: () => void;
197
+ t: (key: string, options?: Record<string, unknown>) => string;
198
+ undoManagerRef: RefObject<Y.UndoManager | null>;
199
+ updateLiveScreenSnapshotContent: (
200
+ screenId: string,
201
+ html: string,
202
+ options?: { recordHistory?: boolean },
203
+ ) => boolean;
204
+ viewModeRef: RefObject<"single" | "overview">;
205
+ writeFrameGeometrySnapshot: (
206
+ geometryById: CanvasFrameGeometryById,
207
+ options?: { syncViewportFrameIds?: string[]; pinHeightFrameIds?: string[] },
208
+ ) => void;
209
+ ydoc: Y.Doc | null;
210
+ }
211
+
212
+ export function runUndo({
213
+ activeEditorDragRef,
214
+ activeFile,
215
+ applyFileContentUpdate,
216
+ applyLocalContentUpdate,
217
+ canEditDesign,
218
+ clipboardPasteRedoStackRef,
219
+ clipboardPasteUndoStackRef,
220
+ contentRedoSelectionStackRef,
221
+ contentRedoStackRef,
222
+ contentUndoSelectionStackRef,
223
+ contentUndoStackRef,
224
+ createFileMutation,
225
+ deleteFileMutation,
226
+ designDataJsonRef,
227
+ fileCreationRedoStackRef,
228
+ fileCreationUndoStackRef,
229
+ fileDeletionRedoStackRef,
230
+ fileDeletionUndoStackRef,
231
+ fileHistoryMutationPendingRef,
232
+ files,
233
+ geometryRedoStackRef,
234
+ geometryUndoStackRef,
235
+ getFreshActiveContent,
236
+ getScreenContent,
237
+ historyOrderRef,
238
+ id,
239
+ isSynced,
240
+ lastLocalContentRef,
241
+ latestClipboardMutationContentRef,
242
+ liveFrameGeometryRef,
243
+ liveScreenSnapshotsById,
244
+ localContentRedoStackRef,
245
+ localContentUndoStackRef,
246
+ markPendingLocalFileContent,
247
+ pendingLiveNonStyleEditsRef,
248
+ pendingLiveNonStyleRedoStackRef,
249
+ pendingLiveNonStyleUndoStackRef,
250
+ pendingLocalFileContentsRef,
251
+ pendingVisualStyleEditsRef,
252
+ pendingVisualStyleRedoStackRef,
253
+ pendingVisualStyleUndoStackRef,
254
+ performDeleteFiles,
255
+ publishAuthoritativeClipboardMutation,
256
+ queryClient,
257
+ queueFileContentSave,
258
+ redoOrderRef,
259
+ replacePreviewContent,
260
+ requestPendingLiveNonStyleRevert,
261
+ requestPendingVisualStyleRevert,
262
+ restoreSelectionSnapshot,
263
+ setActiveFileId,
264
+ setContentRenderRevision,
265
+ setHoveredElement,
266
+ setOverviewSelectedScreenIds,
267
+ setPendingLiveNonStyleEdits,
268
+ setPendingVisualStyleEdits,
269
+ setSelectedElement,
270
+ setSelectedLayerIdsState,
271
+ suppressContentHistoryRef,
272
+ syncLiveScreenSnapshotPreview,
273
+ syncUndoRedoState,
274
+ t,
275
+ undoManagerRef,
276
+ updateLiveScreenSnapshotContent,
277
+ viewModeRef,
278
+ writeFrameGeometrySnapshot,
279
+ ydoc,
280
+ }: UndoArgs) {
281
+ trace("history", "undo", {});
282
+ if (!canEditDesign) return;
283
+ // U10: an in-progress drag hasn't been committed yet (onGeometryCommit /
284
+ // the content update fires on drag END), so undoing mid-drag would pop a
285
+ // PRIOR entry while the live-but-uncommitted drag is still moving the
286
+ // element — the drag's eventual commit would then stomp the undo. Block
287
+ // until the drag finishes (or is cancelled).
288
+ if (activeEditorDragRef.current) return;
289
+ if (fileHistoryMutationPendingRef.current) return;
290
+ const pendingStyleUndoStack = pendingVisualStyleUndoStackRef.current;
291
+ const pendingStyleUndo =
292
+ pendingStyleUndoStack[pendingStyleUndoStack.length - 1];
293
+ const pendingNonStyleUndoStack = pendingLiveNonStyleUndoStackRef.current;
294
+ const pendingNonStyleUndo =
295
+ pendingNonStyleUndoStack[pendingNonStyleUndoStack.length - 1];
296
+ if (
297
+ pendingNonStyleUndo &&
298
+ (!pendingStyleUndo ||
299
+ pendingNonStyleUndo.edit.updatedAt > pendingStyleUndo.edit.updatedAt)
300
+ ) {
301
+ const nextUndoStack = pendingNonStyleUndoStack.slice(0, -1);
302
+ pendingLiveNonStyleUndoStackRef.current = nextUndoStack;
303
+ const nextPending = mergePendingLiveNonStyleEdits(
304
+ nextUndoStack.map((entry) => entry.edit),
305
+ );
306
+ pendingLiveNonStyleEditsRef.current = nextPending;
307
+ pendingLiveNonStyleRedoStackRef.current = [
308
+ ...pendingLiveNonStyleRedoStackRef.current.slice(
309
+ -(MAX_DESIGN_UNDO_STACK - 1),
310
+ ),
311
+ pendingNonStyleUndo,
312
+ ];
313
+ requestPendingLiveNonStyleRevert([
314
+ pendingNonStyleUndo.kind === "text"
315
+ ? {
316
+ ...pendingNonStyleUndo.edit,
317
+ originalValue: pendingNonStyleUndo.revertValue,
318
+ originalHtml: pendingNonStyleUndo.revertHtml,
319
+ }
320
+ : pendingNonStyleUndo.kind === "layer-state"
321
+ ? {
322
+ ...pendingNonStyleUndo.edit,
323
+ originalEnabled: pendingNonStyleUndo.revertEnabled,
324
+ }
325
+ : pendingNonStyleUndo.edit,
326
+ ]);
327
+ setPendingLiveNonStyleEdits(nextPending);
328
+ // Bug fix — undo reverted the DOM via requestPendingLiveNonStyleRevert
329
+ // above but never resynced the inspector panel's selectedElement, so
330
+ // the right panel kept showing pre-undo text until deselect/reselect.
331
+ // Mirrors recordPendingVisualStyleEdit's direct object-patch resync
332
+ // (~line 9514): a plain merge of the revert payload already on this
333
+ // undo entry, not a DOM re-query or content-string rebuild (those
334
+ // don't exist for pending live edits, which never touch
335
+ // ydoc/activeFile.content).
336
+ if (
337
+ pendingNonStyleUndo.kind === "text" &&
338
+ pendingNonStyleUndo.edit.screenId === activeFile?.id
339
+ ) {
340
+ const { sourceId: revertedSourceId, selector: revertedSelector } =
341
+ pendingNonStyleUndo.edit;
342
+ const { revertValue, revertHtml } = pendingNonStyleUndo;
343
+ setSelectedElement((prev) => {
344
+ if (!prev) return prev;
345
+ if (
346
+ !pendingEditTargetsSelectedElement({
347
+ editSourceId: revertedSourceId,
348
+ editSelector: revertedSelector,
349
+ selectedSourceId: prev.sourceId,
350
+ selectedSelector: prev.selector,
351
+ })
352
+ ) {
353
+ return prev;
354
+ }
355
+ return {
356
+ ...prev,
357
+ textContent: revertValue,
358
+ htmlContent: revertHtml ?? prev.htmlContent,
359
+ };
360
+ });
361
+ }
362
+ syncUndoRedoState();
363
+ return;
364
+ }
365
+ if (pendingStyleUndo) {
366
+ const nextUndoStack = pendingStyleUndoStack.slice(0, -1);
367
+ pendingVisualStyleUndoStackRef.current = nextUndoStack;
368
+ const nextPending = mergePendingVisualStyleEdits(
369
+ nextUndoStack.map((entry) => entry.edit),
370
+ );
371
+ pendingVisualStyleEditsRef.current = nextPending;
372
+ pendingVisualStyleRedoStackRef.current = [
373
+ ...pendingVisualStyleRedoStackRef.current.slice(
374
+ -(MAX_DESIGN_UNDO_STACK - 1),
375
+ ),
376
+ pendingStyleUndo,
377
+ ];
378
+ requestPendingVisualStyleRevert([
379
+ {
380
+ ...pendingStyleUndo.edit,
381
+ originalStyles: pendingStyleUndo.revertStyles,
382
+ },
383
+ ]);
384
+ setPendingVisualStyleEdits(nextPending);
385
+ // Bug fix — same stale-inspector-panel issue as the pendingNonStyleUndo
386
+ // branch above, for style undo. Merge the reverted style values
387
+ // (already computed as pendingStyleUndo.revertStyles) into
388
+ // selectedElement.computedStyles, guarded to the currently-selected
389
+ // element so an undo on a different/background screen doesn't
390
+ // clobber the panel for whatever the user has selected right now.
391
+ if (pendingStyleUndo.edit.screenId === activeFile?.id) {
392
+ const {
393
+ sourceId: revertedStyleSourceId,
394
+ selector: revertedStyleSelector,
395
+ } = pendingStyleUndo.edit;
396
+ const revertedStyles = pendingStyleUndo.edit.interactionState
397
+ ? Object.fromEntries(
398
+ Object.entries(pendingStyleUndo.revertStyles).map(
399
+ ([property, value]) => [
400
+ property,
401
+ value || pendingStyleUndo.edit.baseStyles?.[property] || "",
402
+ ],
403
+ ),
404
+ )
405
+ : pendingStyleUndo.revertStyles;
406
+ setSelectedElement((prev) => {
407
+ if (!prev) return prev;
408
+ if (
409
+ !pendingEditTargetsSelectedElement({
410
+ editSourceId: revertedStyleSourceId,
411
+ editSelector: revertedStyleSelector,
412
+ selectedSourceId: prev.sourceId,
413
+ selectedSelector: prev.selector,
414
+ })
415
+ ) {
416
+ return prev;
417
+ }
418
+ return {
419
+ ...prev,
420
+ computedStyles: {
421
+ ...prev.computedStyles,
422
+ ...revertedStyles,
423
+ },
424
+ };
425
+ });
426
+ }
427
+ syncUndoRedoState();
428
+ return;
429
+ }
430
+ const clipboardPasteUndo =
431
+ clipboardPasteUndoStackRef.current[
432
+ clipboardPasteUndoStackRef.current.length - 1
433
+ ];
434
+ if (clipboardPasteUndo) {
435
+ const currentContent =
436
+ latestClipboardMutationContentRef.current.get(clipboardPasteUndo.fileId)
437
+ ?.content ??
438
+ pendingLocalFileContentsRef.current.get(clipboardPasteUndo.fileId)
439
+ ?.content ??
440
+ (clipboardPasteUndo.fileId === activeFile?.id
441
+ ? getFreshActiveContent()
442
+ : (getScreenContent(clipboardPasteUndo.fileId) ?? ""));
443
+ // Only claim the command when this paste is still the top document
444
+ // state. If another edit followed it, the ordinary chronological
445
+ // history below gets first chance; once that edit is undone back to
446
+ // `after`, the next Cmd+Z reaches this immutable paste entry.
447
+ if (currentContent === clipboardPasteUndo.after) {
448
+ const clipboardMutation = publishAuthoritativeClipboardMutation({
449
+ fileId: clipboardPasteUndo.fileId,
450
+ baseContent: clipboardPasteUndo.after,
451
+ nextContent: clipboardPasteUndo.before,
452
+ origin: "clipboard-undo",
453
+ });
454
+ if (!clipboardMutation) return;
455
+ clipboardPasteUndoStackRef.current =
456
+ clipboardPasteUndoStackRef.current.slice(0, -1);
457
+ clipboardPasteRedoStackRef.current = [
458
+ ...clipboardPasteRedoStackRef.current.slice(
459
+ -(MAX_DESIGN_UNDO_STACK - 1),
460
+ ),
461
+ clipboardPasteUndo,
462
+ ];
463
+ if (clipboardPasteUndo.fileId === activeFile?.id) {
464
+ applyLocalContentUpdate(clipboardPasteUndo.before, {
465
+ recordHistory: false,
466
+ forcePreviewFullDocument: true,
467
+ immediateSave: true,
468
+ clipboardMutation,
469
+ });
470
+ } else {
471
+ applyFileContentUpdate(
472
+ clipboardPasteUndo.fileId,
473
+ clipboardPasteUndo.before,
474
+ {
475
+ recordHistory: false,
476
+ forcePreviewFullDocument: true,
477
+ clipboardMutation,
478
+ },
479
+ );
480
+ }
481
+ setSelectedElement((previous) =>
482
+ previous
483
+ ? refreshElementInfoFromContent(clipboardPasteUndo.before, previous)
484
+ : previous,
485
+ );
486
+ setSelectedLayerIdsState((previous) =>
487
+ refreshSelectedLayerIdsFromContent(clipboardPasteUndo.before, previous),
488
+ );
489
+ syncUndoRedoState();
490
+ return;
491
+ }
492
+ }
493
+ const um = undoManagerRef.current;
494
+ const canUseOverviewHistory = viewModeRef.current === "overview";
495
+ let prunedUndoHistory = 0;
496
+ const undoContent = (scope: "any" | "local" | "global" = "any") => {
497
+ if (scope !== "global" && um?.canUndo()) {
498
+ um.undo();
499
+ if (ydoc && activeFile) {
500
+ const next = ydoc.getText("content").toString();
501
+ markPendingLocalFileContent(activeFile.id, next, activeFile.updatedAt);
502
+ lastLocalContentRef.current = next;
503
+ queueFileContentSave(activeFile.id, next, {
504
+ syncCollab: !(ydoc && isSynced),
505
+ });
506
+ // Holistic flash pipeline: only fall back to a full srcdoc rebuild
507
+ // (real iframe reload) when the live in-place patch genuinely
508
+ // failed — replaceRuntimeDocument's forceFullDocument branch already
509
+ // swaps content inside the SAME live iframe (no navigation), so
510
+ // bumping contentRenderRevision unconditionally right after a
511
+ // successful in-place replace was a redundant second reload and the
512
+ // dominant cause of "undo/redo flashes heavily".
513
+ if (
514
+ previewContentReplaceNeedsRenderFallback(
515
+ replacePreviewContent(next, null, {
516
+ forceFullDocument: true,
517
+ }),
518
+ )
519
+ ) {
520
+ setContentRenderRevision((revision) => revision + 1);
521
+ }
522
+ // Clear stale selection if the undo removed the selected element.
523
+ setSelectedElement((prev) => {
524
+ if (!prev) return prev;
525
+ return refreshElementInfoFromContent(next, prev);
526
+ });
527
+ setHoveredElement((prev) => {
528
+ if (!prev) return prev;
529
+ return refreshElementInfoFromContent(next, prev);
530
+ });
531
+ // U18: keep the layers-panel highlight in sync too.
532
+ setSelectedLayerIdsState((prev) =>
533
+ refreshSelectedLayerIdsFromContent(next, prev),
534
+ );
535
+ }
536
+ // Drop the matching local fallback mirror (see U3) so it can't be
537
+ // replayed a second time via the fallthrough path below once the Yjs
538
+ // UndoManager for this file is later torn down.
539
+ const mirroredIndex = findLastContentHistoryChangeIndex(
540
+ localContentUndoStackRef.current,
541
+ activeFile?.id,
542
+ );
543
+ if (mirroredIndex !== -1) {
544
+ localContentUndoStackRef.current =
545
+ localContentUndoStackRef.current.filter(
546
+ (_, index) => index !== mirroredIndex,
547
+ );
548
+ }
549
+ redoOrderRef.current = [
550
+ ...redoOrderRef.current.slice(-(MAX_DESIGN_UNDO_STACK - 1)),
551
+ "content",
552
+ ];
553
+ return true;
554
+ }
555
+
556
+ if (!canUseOverviewHistory && scope !== "global" && activeFile?.id) {
557
+ const localIndex = findLastContentHistoryChangeIndex(
558
+ localContentUndoStackRef.current,
559
+ activeFile.id,
560
+ );
561
+ if (localIndex !== -1) {
562
+ const [entry] = localContentUndoStackRef.current.splice(localIndex, 1);
563
+ if (entry) {
564
+ localContentRedoStackRef.current = [
565
+ ...localContentRedoStackRef.current.slice(
566
+ -(MAX_DESIGN_UNDO_STACK - 1),
567
+ ),
568
+ entry,
569
+ ];
570
+ // U20: route a live-snapshot screen's replay through
571
+ // updateLiveScreenSnapshotContent — see the matching note above.
572
+ if (liveScreenSnapshotsById[entry.fileId]) {
573
+ updateLiveScreenSnapshotContent(entry.fileId, entry.before, {
574
+ recordHistory: false,
575
+ });
576
+ syncLiveScreenSnapshotPreview(entry.fileId, entry.before);
577
+ } else {
578
+ applyLocalContentUpdate(entry.before, {
579
+ refreshPreview: false,
580
+ forcePreviewFullDocument: true,
581
+ immediateSave: true,
582
+ recordHistory: false,
583
+ });
584
+ }
585
+ setSelectedElement((prev) => {
586
+ if (!prev) return prev;
587
+ return refreshElementInfoFromContent(entry.before, prev);
588
+ });
589
+ setHoveredElement((prev) => {
590
+ if (!prev) return prev;
591
+ return refreshElementInfoFromContent(entry.before, prev);
592
+ });
593
+ // U18: keep the layers-panel highlight in sync too.
594
+ setSelectedLayerIdsState((prev) =>
595
+ refreshSelectedLayerIdsFromContent(entry.before, prev),
596
+ );
597
+ return true;
598
+ }
599
+ }
600
+ }
601
+
602
+ if (scope === "local") return false;
603
+ if (!canUseOverviewHistory) return false;
604
+ const entry =
605
+ contentUndoStackRef.current[contentUndoStackRef.current.length - 1];
606
+ if (!entry) return false;
607
+ const entrySelection =
608
+ contentUndoSelectionStackRef.current[
609
+ contentUndoSelectionStackRef.current.length - 1
610
+ ];
611
+ const changes = getAvailableContentHistoryChanges(
612
+ entry,
613
+ files.map((file) => file.id),
614
+ activeFile?.id,
615
+ );
616
+ if (changes.length === 0) {
617
+ contentUndoStackRef.current.pop();
618
+ contentUndoSelectionStackRef.current.pop();
619
+ prunedUndoHistory += 1;
620
+ return false;
621
+ }
622
+ contentUndoStackRef.current.pop();
623
+ contentUndoSelectionStackRef.current.pop();
624
+ contentRedoStackRef.current = [
625
+ ...contentRedoStackRef.current.slice(-(MAX_DESIGN_UNDO_STACK - 1)),
626
+ entry,
627
+ ];
628
+ contentRedoSelectionStackRef.current = [
629
+ ...contentRedoSelectionStackRef.current.slice(
630
+ -(MAX_DESIGN_UNDO_STACK - 1),
631
+ ),
632
+ entrySelection,
633
+ ];
634
+ redoOrderRef.current = [
635
+ ...redoOrderRef.current.slice(-(MAX_DESIGN_UNDO_STACK - 1)),
636
+ "file-content",
637
+ ];
638
+ suppressContentHistoryRef.current = true;
639
+ try {
640
+ for (const change of changes) {
641
+ // U20: a live-snapshot (URL-backed/localhost) screen's visible
642
+ // content lives in liveScreenSnapshotsById, not DesignFile.content
643
+ // — route replay there instead of the regular content path, which
644
+ // that screen's edits never actually write to.
645
+ if (liveScreenSnapshotsById[change.fileId]) {
646
+ updateLiveScreenSnapshotContent(change.fileId, change.before, {
647
+ recordHistory: false,
648
+ });
649
+ syncLiveScreenSnapshotPreview(change.fileId, change.before);
650
+ } else if (change.fileId === activeFile?.id) {
651
+ applyLocalContentUpdate(change.before, {
652
+ refreshPreview: false,
653
+ forcePreviewFullDocument: true,
654
+ immediateSave: true,
655
+ recordHistory: false,
656
+ });
657
+ } else {
658
+ applyFileContentUpdate(change.fileId, change.before, {
659
+ recordHistory: false,
660
+ refreshPreview: false,
661
+ });
662
+ }
663
+ }
664
+ } finally {
665
+ suppressContentHistoryRef.current = false;
666
+ }
667
+ const activeChange = changes.find(
668
+ (change) => change.fileId === activeFile?.id,
669
+ );
670
+ if (activeChange) {
671
+ setSelectedElement((prev) => {
672
+ if (!prev) return prev;
673
+ return refreshElementInfoFromContent(activeChange.before, prev);
674
+ });
675
+ setHoveredElement((prev) => {
676
+ if (!prev) return prev;
677
+ return refreshElementInfoFromContent(activeChange.before, prev);
678
+ });
679
+ // U18: keep the layers-panel highlight in sync too.
680
+ setSelectedLayerIdsState((prev) =>
681
+ refreshSelectedLayerIdsFromContent(activeChange.before, prev),
682
+ );
683
+ }
684
+ // Figma-parity undo/redo selection restore: overrides the
685
+ // refreshSelectedLayerIdsFromContent heuristic just above with the
686
+ // actual captured selection, when one was recorded for this entry.
687
+ restoreSelectionSnapshot(entrySelection);
688
+ return true;
689
+ };
690
+ const undoGeometry = () => {
691
+ if (!canUseOverviewHistory) return false;
692
+ const entry = geometryUndoStackRef.current.pop();
693
+ if (!entry) return false;
694
+ // Freshness guard: this entry last wrote `entry.after`. If a peer/agent
695
+ // has since moved any of the frames it touched, replaying `entry.before`
696
+ // would silently clobber their change — drop this entry instead. The pop
697
+ // above already removed it, so undo skips forward to the next entry.
698
+ const stale = staleGeometryFrameIds(
699
+ entry,
700
+ liveFrameGeometryRef.current,
701
+ entry.after,
702
+ );
703
+ if (stale.length > 0) {
704
+ console.debug(
705
+ "[design] skipping stale geometry undo; frames changed since capture:",
706
+ stale,
707
+ );
708
+ toast.info(t("designEditor.toasts.undoSkippedConcurrentEdit"));
709
+ // Try the next undo entry rather than swallowing the whole gesture.
710
+ return undoGeometry();
711
+ }
712
+ geometryRedoStackRef.current = [
713
+ ...geometryRedoStackRef.current.slice(-(MAX_DESIGN_UNDO_STACK - 1)),
714
+ entry,
715
+ ];
716
+ redoOrderRef.current = [
717
+ ...redoOrderRef.current.slice(-(MAX_DESIGN_UNDO_STACK - 1)),
718
+ "geometry",
719
+ ];
720
+ // U11: merge only this entry's per-frame diff onto the CURRENT live
721
+ // map (read fresh from the ref) instead of replacing the whole board
722
+ // with the entry's stale whole-board snapshot — otherwise a frame
723
+ // created after this entry was recorded has no key in entry.before
724
+ // and would be wiped out by a full-map replace.
725
+ writeFrameGeometrySnapshot(
726
+ applyGeometryHistoryDiff(
727
+ getCanvasFrameGeometry(designDataJsonRef.current),
728
+ entry,
729
+ "undo",
730
+ ),
731
+ {
732
+ syncViewportFrameIds: viewportChangedFrameIds(
733
+ entry.after,
734
+ entry.before,
735
+ ),
736
+ },
737
+ );
738
+ // Figma parity: undo re-selects whatever was selected when this
739
+ // gesture's change was originally made.
740
+ restoreSelectionSnapshot(entry.selectionBefore);
741
+ return true;
742
+ };
743
+ // U12: undo a screen create/duplicate by soft-deleting the file it
744
+ // created (performDeleteFiles already prunes any content/geometry undo
745
+ // entries for that file, mirroring U2's screen-deletion cleanup).
746
+ // Resolved by filename at undo time (filenames are unique) since the
747
+ // entry itself doesn't carry the id assigned by the create mutation.
748
+ const undoFileCreation = () => {
749
+ if (!canUseOverviewHistory) return false;
750
+ const entry = fileCreationUndoStackRef.current.pop();
751
+ if (!entry) return false;
752
+ const createdFile = files.find((file) => file.filename === entry.filename);
753
+ if (!createdFile) return false;
754
+ fileCreationRedoStackRef.current = [
755
+ ...fileCreationRedoStackRef.current.slice(-(MAX_DESIGN_UNDO_STACK - 1)),
756
+ entry,
757
+ ];
758
+ redoOrderRef.current = [
759
+ ...redoOrderRef.current.slice(-(MAX_DESIGN_UNDO_STACK - 1)),
760
+ "file-created",
761
+ ];
762
+ // skipFileCreationRedoPrune: the entry was just pushed onto the redo
763
+ // stack above for this exact filename — without this flag
764
+ // performDeleteFiles' filename-keyed redo prune would immediately pop
765
+ // it back off, leaving redo permanently empty after this undo.
766
+ performDeleteFiles([createdFile], { skipFileCreationRedoPrune: true });
767
+ return true;
768
+ };
769
+ const undoFileDeletion = () => {
770
+ if (!canUseOverviewHistory || !id) return false;
771
+ const entry = fileDeletionUndoStackRef.current.pop();
772
+ if (!entry) return false;
773
+
774
+ fileHistoryMutationPendingRef.current = true;
775
+ syncUndoRedoState();
776
+ void (async () => {
777
+ const recreatedIds: string[] = [];
778
+ try {
779
+ for (const file of entry.files) {
780
+ const result = (await createFileMutation.mutateAsync({
781
+ designId: id,
782
+ filename: file.filename,
783
+ content: file.content,
784
+ fileType: file.fileType,
785
+ } as any)) as { id?: string };
786
+ if (!result.id) {
787
+ throw new Error(`Failed to restore "${file.filename}"`);
788
+ }
789
+ recreatedIds.push(result.id);
790
+ }
791
+
792
+ const recreatedEntry = remapFileDeletionHistoryEntryIds(
793
+ entry,
794
+ recreatedIds,
795
+ );
796
+ if (recreatedEntry.files.length !== entry.files.length) {
797
+ throw new Error("Failed to restore every deleted screen");
798
+ }
799
+ fileDeletionRedoStackRef.current = [
800
+ ...fileDeletionRedoStackRef.current.slice(
801
+ -(MAX_DESIGN_UNDO_STACK - 1),
802
+ ),
803
+ recreatedEntry,
804
+ ];
805
+ redoOrderRef.current = [
806
+ ...redoOrderRef.current.slice(-(MAX_DESIGN_UNDO_STACK - 1)),
807
+ "file-deleted",
808
+ ];
809
+
810
+ const nextGeometry = {
811
+ ...getCanvasFrameGeometry(designDataJsonRef.current),
812
+ };
813
+ recreatedEntry.files.forEach((file, index) => {
814
+ const geometry = entry.files[index]?.geometry;
815
+ if (geometry) nextGeometry[file.id] = geometry;
816
+ });
817
+ writeFrameGeometrySnapshot(nextGeometry);
818
+ queryClient.invalidateQueries({
819
+ queryKey: ["action", "get-design"],
820
+ });
821
+
822
+ const firstRestoredId = recreatedEntry.files[0]?.id;
823
+ if (firstRestoredId) {
824
+ setActiveFileId(firstRestoredId);
825
+ setOverviewSelectedScreenIds(
826
+ recreatedEntry.files.map((file) => file.id),
827
+ );
828
+ setSelectedLayerIdsState(recreatedEntry.files.map((file) => file.id));
829
+ }
830
+ } catch (error) {
831
+ await Promise.allSettled(
832
+ recreatedIds.map((fileId) =>
833
+ deleteFileMutation.mutateAsync({ id: fileId } as any),
834
+ ),
835
+ );
836
+ fileDeletionUndoStackRef.current = [
837
+ ...fileDeletionUndoStackRef.current.slice(
838
+ -(MAX_DESIGN_UNDO_STACK - 1),
839
+ ),
840
+ entry,
841
+ ];
842
+ historyOrderRef.current = [
843
+ ...historyOrderRef.current.slice(-(MAX_DESIGN_UNDO_STACK - 1)),
844
+ "file-deleted",
845
+ ];
846
+ queryClient.invalidateQueries({
847
+ queryKey: ["action", "get-design"],
848
+ });
849
+ toast.error(
850
+ error instanceof Error ? error.message : t("common.genericError"),
851
+ );
852
+ } finally {
853
+ fileHistoryMutationPendingRef.current = false;
854
+ syncUndoRedoState();
855
+ }
856
+ })();
857
+ return true;
858
+ };
859
+
860
+ const undoByOrder = (preferred?: UndoRedoOrderKind) => {
861
+ if (preferred === "file-deleted") {
862
+ return (
863
+ undoFileDeletion() ||
864
+ undoFileCreation() ||
865
+ undoContent() ||
866
+ undoGeometry()
867
+ );
868
+ }
869
+ if (preferred === "file-created")
870
+ return (
871
+ undoFileCreation() ||
872
+ undoFileDeletion() ||
873
+ undoContent() ||
874
+ undoGeometry()
875
+ );
876
+ if (preferred === "geometry") return undoGeometry() || undoContent();
877
+ if (preferred === "file-content") {
878
+ const prunedBefore = prunedUndoHistory;
879
+ if (undoContent("global")) return true;
880
+ if (prunedUndoHistory > prunedBefore) return false;
881
+ return undoGeometry();
882
+ }
883
+ if (preferred === "content") {
884
+ const prunedBefore = prunedUndoHistory;
885
+ return (
886
+ undoContent("local") ||
887
+ undoContent("global") ||
888
+ (prunedUndoHistory > prunedBefore ? false : undoGeometry())
889
+ );
890
+ }
891
+ return undoFileDeletion() || undoContent() || undoGeometry();
892
+ };
893
+ let didUndo = false;
894
+ if (canUseOverviewHistory) {
895
+ while (!didUndo) {
896
+ const preferred = historyOrderRef.current.pop();
897
+ didUndo = undoByOrder(preferred);
898
+ if (didUndo || preferred === undefined) break;
899
+ }
900
+ } else {
901
+ didUndo = undoContent("local");
902
+ }
903
+ if (didUndo || prunedUndoHistory > 0) {
904
+ syncUndoRedoState();
905
+ }
906
+ }