@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,1086 @@
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 { getInitialFrameGeometry } from "@/components/design/multi-screen/frame-geometry";
10
+ import type { FrameGeometry } from "@/components/design/multi-screen/types";
11
+ import type {
12
+ ElementInfo,
13
+ RuntimeStructureInsertRequest,
14
+ RuntimeStructureMoveRequest,
15
+ } from "@/components/design/types";
16
+ import type {
17
+ ClipboardContentLineage,
18
+ ClipboardContentMutationOrigin,
19
+ ClipboardContentMutationPublication,
20
+ } from "@/lib/clipboard-content-lineage";
21
+ import {
22
+ refreshElementInfoFromContent,
23
+ refreshSelectedLayerIdsFromContent,
24
+ } from "@/pages/design-editor/code-layer-state";
25
+ import type { LiveScreenSnapshot } from "@/pages/design-editor/command-types";
26
+ import type { OverviewScreen } from "@/pages/design-editor/derive/overview-screens";
27
+ import {
28
+ getCanvasFrameGeometry,
29
+ staleGeometryFrameIds,
30
+ viewportChangedFrameIds,
31
+ } from "@/pages/design-editor/design-data-geometry-utils";
32
+ import type {
33
+ PreviewContentReplaceResult,
34
+ UndoRedoOrderKind,
35
+ } from "@/pages/design-editor/editor-state";
36
+ import { previewContentReplaceNeedsRenderFallback } from "@/pages/design-editor/editor-state";
37
+ import type {
38
+ ContentHistoryChange,
39
+ ContentHistoryEntry,
40
+ FileCreationHistoryEntry,
41
+ FileDeletionHistoryEntry,
42
+ GeometryHistoryEntry,
43
+ GeometryHistorySelection,
44
+ } from "@/pages/design-editor/history";
45
+ import {
46
+ MAX_DESIGN_UNDO_STACK,
47
+ applyGeometryHistoryDiff,
48
+ filterFileDeletionHistoryEntry,
49
+ findLastContentHistoryChangeIndex,
50
+ getAvailableContentHistoryChanges,
51
+ removeRecentUndoRedoOrderKinds,
52
+ } from "@/pages/design-editor/history";
53
+ import type {
54
+ PendingLiveNonStyleEdit,
55
+ PendingLiveNonStyleUndoEntry,
56
+ PendingLiveStructureUndoEntry,
57
+ PendingVisualStyleEdit,
58
+ PendingVisualStyleUndoEntry,
59
+ } from "@/pages/design-editor/pending-edits";
60
+ import {
61
+ buildPendingVisualStyleRevertPatches,
62
+ mergePendingLiveNonStyleEdits,
63
+ mergePendingVisualStyleEdits,
64
+ pendingStructureRedoCommand,
65
+ shouldRedoPendingLiveNonStyleBeforeStyle,
66
+ } from "@/pages/design-editor/pending-edits";
67
+ import { pendingEditTargetsSelectedElement } from "@/pages/design-editor/selection-state";
68
+ import type { DesignFile } from "@/pages/design-editor/types";
69
+
70
+ export interface RedoArgs {
71
+ activeEditorDragRef: RefObject<boolean>;
72
+ activeFile: DesignFile;
73
+ applyFileContentUpdate: (
74
+ fileId: string,
75
+ nextContent: string,
76
+ options?: {
77
+ refreshPreview?: boolean;
78
+ skipPreview?: boolean;
79
+ forcePreviewFullDocument?: boolean;
80
+ persist?: boolean;
81
+ recordHistory?: boolean;
82
+ updatedAt?: string;
83
+ clipboardMutation?: ClipboardContentMutationPublication;
84
+ },
85
+ ) => void;
86
+ applyLocalContentUpdate: (
87
+ nextContent: string,
88
+ options?: {
89
+ refreshPreview?: boolean;
90
+ skipPreview?: boolean;
91
+ forcePreviewFullDocument?: boolean;
92
+ immediateSave?: boolean;
93
+ persist?: boolean;
94
+ recordHistory?: boolean;
95
+ historyBeforeContent?: string;
96
+ updatedAt?: string;
97
+ clipboardMutation?: ClipboardContentMutationPublication;
98
+ },
99
+ ) => void;
100
+ canEditDesign: boolean;
101
+ clipboardPasteRedoStackRef: RefObject<ContentHistoryChange[]>;
102
+ clipboardPasteUndoStackRef: RefObject<ContentHistoryChange[]>;
103
+ contentRedoSelectionStackRef: RefObject<
104
+ (GeometryHistorySelection | undefined)[]
105
+ >;
106
+ contentRedoStackRef: RefObject<ContentHistoryEntry[]>;
107
+ contentUndoSelectionStackRef: RefObject<
108
+ (GeometryHistorySelection | undefined)[]
109
+ >;
110
+ contentUndoStackRef: RefObject<ContentHistoryEntry[]>;
111
+ createFileMutation: ReturnType<
112
+ typeof useActionMutation<undefined, undefined, "create-file">
113
+ >;
114
+ deleteRuntimeElement: (
115
+ selector?: string | null,
116
+ candidates?: readonly string[],
117
+ requestId?: string,
118
+ ) => boolean;
119
+ designDataJsonRef: RefObject<Record<string, unknown>>;
120
+ fileCreationRedoStackRef: RefObject<FileCreationHistoryEntry[]>;
121
+ fileCreationUndoStackRef: RefObject<FileCreationHistoryEntry[]>;
122
+ fileDeletionRedoStackRef: RefObject<FileDeletionHistoryEntry[]>;
123
+ fileDeletionUndoStackRef: RefObject<FileDeletionHistoryEntry[]>;
124
+ fileHistoryMutationPendingRef: RefObject<boolean>;
125
+ files: DesignFile[];
126
+ focusCreatedScreen: (screenId: string, geometry: FrameGeometry) => void;
127
+ geometryRedoStackRef: RefObject<GeometryHistoryEntry[]>;
128
+ geometryUndoStackRef: RefObject<GeometryHistoryEntry[]>;
129
+ getFreshActiveContent: () => string;
130
+ getScreenContent: (screenId: string) => string;
131
+ historyOrderRef: RefObject<UndoRedoOrderKind[]>;
132
+ id: string | undefined;
133
+ isSynced: boolean;
134
+ lastLocalContentRef: RefObject<string | null>;
135
+ latestClipboardMutationContentRef: RefObject<
136
+ Map<string, ClipboardContentLineage>
137
+ >;
138
+ liveFrameGeometryRef: RefObject<CanvasFrameGeometryById>;
139
+ liveScreenSnapshotsById: Record<string, LiveScreenSnapshot>;
140
+ localContentRedoStackRef: RefObject<ContentHistoryChange[]>;
141
+ localContentUndoStackRef: RefObject<ContentHistoryChange[]>;
142
+ markPendingLocalFileContent: (
143
+ fileId: string,
144
+ content: string,
145
+ baseUpdatedAt?: string | null,
146
+ ) => void;
147
+ optimisticallyInsertCreatedFile: (args: {
148
+ fileId: string;
149
+ filename: string;
150
+ fileType: DesignFile["fileType"];
151
+ content: string;
152
+ result?: Record<string, unknown> | null;
153
+ }) => void;
154
+ overviewScreens: OverviewScreen[];
155
+ pendingLiveNonStyleEditsRef: RefObject<PendingLiveNonStyleEdit[]>;
156
+ pendingLiveNonStyleRedoStackRef: RefObject<PendingLiveNonStyleUndoEntry[]>;
157
+ pendingLiveNonStyleUndoStackRef: RefObject<PendingLiveNonStyleUndoEntry[]>;
158
+ pendingLocalFileContentsRef: RefObject<
159
+ Map<
160
+ string,
161
+ { content: string; startedAt: number; baseUpdatedAt?: string | null }
162
+ >
163
+ >;
164
+ pendingStructureRedoReplayRef: RefObject<
165
+ PendingLiveStructureUndoEntry | undefined
166
+ >;
167
+ pendingStructureRedoReplayTimerRef: RefObject<number | undefined>;
168
+ pendingVisualStyleEditsRef: RefObject<PendingVisualStyleEdit[]>;
169
+ pendingVisualStyleRedoStackRef: RefObject<PendingVisualStyleUndoEntry[]>;
170
+ pendingVisualStyleUndoStackRef: RefObject<PendingVisualStyleUndoEntry[]>;
171
+ performDeleteFiles: (
172
+ filesToDelete: DesignFile[],
173
+ options?: {
174
+ skipFileCreationRedoPrune?: boolean;
175
+ recordDeletionHistory?: boolean;
176
+ onMutationSettled?: (
177
+ deletedFiles: DesignFile[],
178
+ failedFiles: DesignFile[],
179
+ ) => void;
180
+ },
181
+ ) => void;
182
+ publishAuthoritativeClipboardMutation: (args: {
183
+ fileId: string;
184
+ baseContent: string;
185
+ nextContent: string;
186
+ origin: ClipboardContentMutationOrigin;
187
+ }) => ClipboardContentMutationPublication | null;
188
+ queryClient: QueryClient;
189
+ queueFileContentSave: (
190
+ fileId: string,
191
+ content: string,
192
+ options?: { syncCollab?: boolean; immediate?: boolean },
193
+ ) => void;
194
+ recordLocalContentHistoryChangeFallback: (
195
+ change: ContentHistoryChange,
196
+ ) => void;
197
+ redoOrderRef: RefObject<UndoRedoOrderKind[]>;
198
+ replacePreviewContent: (
199
+ nextContent: string,
200
+ selector?: string | null,
201
+ options?: { forceFullDocument?: boolean },
202
+ ) => PreviewContentReplaceResult;
203
+ restoreSelectionSnapshot: (
204
+ selection: GeometryHistorySelection | undefined,
205
+ ) => void;
206
+ runtimeStructureInsertRevisionRef: RefObject<number>;
207
+ runtimeStructureMoveRevisionRef: RefObject<number>;
208
+ setContentRenderRevision: Dispatch<SetStateAction<number>>;
209
+ setHoveredElement: Dispatch<SetStateAction<ElementInfo | null>>;
210
+ setPendingLayerStateReplayRequest: Dispatch<
211
+ SetStateAction<{
212
+ requestId: number;
213
+ patches: Array<{
214
+ screenId: string;
215
+ layerId: string;
216
+ state: "hidden" | "locked";
217
+ enabled: boolean;
218
+ }>;
219
+ } | null>
220
+ >;
221
+ setPendingLiveNonStyleEdits: Dispatch<
222
+ SetStateAction<PendingLiveNonStyleEdit[]>
223
+ >;
224
+ setPendingTextRevertRequest: Dispatch<
225
+ SetStateAction<{
226
+ requestId: number;
227
+ patches: Array<{
228
+ screenId: string;
229
+ selector: string;
230
+ sourceId?: string | null;
231
+ value: string;
232
+ html?: string;
233
+ }>;
234
+ } | null>
235
+ >;
236
+ setPendingVisualStyleEdits: Dispatch<
237
+ SetStateAction<PendingVisualStyleEdit[]>
238
+ >;
239
+ setPendingVisualStyleRevertRequest: Dispatch<
240
+ SetStateAction<{
241
+ requestId: number;
242
+ patches: ReturnType<typeof buildPendingVisualStyleRevertPatches>;
243
+ } | null>
244
+ >;
245
+ setRuntimeStructureInsertRequest: Dispatch<
246
+ SetStateAction<
247
+ (RuntimeStructureInsertRequest & { screenId: string }) | null
248
+ >
249
+ >;
250
+ setRuntimeStructureMoveRequest: Dispatch<
251
+ SetStateAction<(RuntimeStructureMoveRequest & { screenId: string }) | null>
252
+ >;
253
+ setSelectedElement: Dispatch<SetStateAction<ElementInfo | null>>;
254
+ setSelectedLayerIdsState: Dispatch<SetStateAction<string[]>>;
255
+ suppressContentHistoryRef: RefObject<boolean>;
256
+ syncLiveScreenSnapshotPreview: (screenId: string, html: string) => void;
257
+ syncUndoRedoState: () => void;
258
+ t: (key: string, options?: Record<string, unknown>) => string;
259
+ undoManagerRef: RefObject<Y.UndoManager | null>;
260
+ updateLiveScreenSnapshotContent: (
261
+ screenId: string,
262
+ html: string,
263
+ options?: { recordHistory?: boolean },
264
+ ) => boolean;
265
+ viewModeRef: RefObject<"single" | "overview">;
266
+ writeFrameGeometrySnapshot: (
267
+ geometryById: CanvasFrameGeometryById,
268
+ options?: { syncViewportFrameIds?: string[]; pinHeightFrameIds?: string[] },
269
+ ) => void;
270
+ ydoc: Y.Doc | null;
271
+ }
272
+
273
+ export function runRedo({
274
+ activeEditorDragRef,
275
+ activeFile,
276
+ applyFileContentUpdate,
277
+ applyLocalContentUpdate,
278
+ canEditDesign,
279
+ clipboardPasteRedoStackRef,
280
+ clipboardPasteUndoStackRef,
281
+ contentRedoSelectionStackRef,
282
+ contentRedoStackRef,
283
+ contentUndoSelectionStackRef,
284
+ contentUndoStackRef,
285
+ createFileMutation,
286
+ deleteRuntimeElement,
287
+ designDataJsonRef,
288
+ fileCreationRedoStackRef,
289
+ fileCreationUndoStackRef,
290
+ fileDeletionRedoStackRef,
291
+ fileDeletionUndoStackRef,
292
+ fileHistoryMutationPendingRef,
293
+ files,
294
+ focusCreatedScreen,
295
+ geometryRedoStackRef,
296
+ geometryUndoStackRef,
297
+ getFreshActiveContent,
298
+ getScreenContent,
299
+ historyOrderRef,
300
+ id,
301
+ isSynced,
302
+ lastLocalContentRef,
303
+ latestClipboardMutationContentRef,
304
+ liveFrameGeometryRef,
305
+ liveScreenSnapshotsById,
306
+ localContentRedoStackRef,
307
+ localContentUndoStackRef,
308
+ markPendingLocalFileContent,
309
+ optimisticallyInsertCreatedFile,
310
+ overviewScreens,
311
+ pendingLiveNonStyleEditsRef,
312
+ pendingLiveNonStyleRedoStackRef,
313
+ pendingLiveNonStyleUndoStackRef,
314
+ pendingLocalFileContentsRef,
315
+ pendingStructureRedoReplayRef,
316
+ pendingStructureRedoReplayTimerRef,
317
+ pendingVisualStyleEditsRef,
318
+ pendingVisualStyleRedoStackRef,
319
+ pendingVisualStyleUndoStackRef,
320
+ performDeleteFiles,
321
+ publishAuthoritativeClipboardMutation,
322
+ queryClient,
323
+ queueFileContentSave,
324
+ recordLocalContentHistoryChangeFallback,
325
+ redoOrderRef,
326
+ replacePreviewContent,
327
+ restoreSelectionSnapshot,
328
+ runtimeStructureInsertRevisionRef,
329
+ runtimeStructureMoveRevisionRef,
330
+ setContentRenderRevision,
331
+ setHoveredElement,
332
+ setPendingLayerStateReplayRequest,
333
+ setPendingLiveNonStyleEdits,
334
+ setPendingTextRevertRequest,
335
+ setPendingVisualStyleEdits,
336
+ setPendingVisualStyleRevertRequest,
337
+ setRuntimeStructureInsertRequest,
338
+ setRuntimeStructureMoveRequest,
339
+ setSelectedElement,
340
+ setSelectedLayerIdsState,
341
+ suppressContentHistoryRef,
342
+ syncLiveScreenSnapshotPreview,
343
+ syncUndoRedoState,
344
+ t,
345
+ undoManagerRef,
346
+ updateLiveScreenSnapshotContent,
347
+ viewModeRef,
348
+ writeFrameGeometrySnapshot,
349
+ ydoc,
350
+ }: RedoArgs) {
351
+ trace("history", "redo", {});
352
+ if (!canEditDesign) return;
353
+ // U10: see the matching guard in handleUndo — don't redo into a document
354
+ // state an in-progress, uncommitted drag is about to overwrite anyway.
355
+ if (activeEditorDragRef.current) return;
356
+ if (fileHistoryMutationPendingRef.current) return;
357
+ const pendingNonStyleRedoStack = pendingLiveNonStyleRedoStackRef.current;
358
+ const pendingNonStyleRedo =
359
+ pendingNonStyleRedoStack[pendingNonStyleRedoStack.length - 1];
360
+ const pendingLiveRedoStack = pendingVisualStyleRedoStackRef.current;
361
+ const pendingLiveRedo = pendingLiveRedoStack[pendingLiveRedoStack.length - 1];
362
+ const redoPendingNonStyleFirst = shouldRedoPendingLiveNonStyleBeforeStyle(
363
+ pendingLiveRedo,
364
+ pendingNonStyleRedo,
365
+ );
366
+ if (redoPendingNonStyleFirst && pendingNonStyleRedo?.kind === "structure") {
367
+ const redoCommand = pendingStructureRedoCommand(pendingNonStyleRedo.edit);
368
+ // A removal has no bridge echo to wait for: re-issuing the delete under
369
+ // the same requestId is the whole replay, so move the entry back onto
370
+ // the undo stack here instead of arming pendingStructureRedoReplayRef
371
+ // for a `visual-structure-change` that will never arrive.
372
+ if (redoCommand.kind === "delete") {
373
+ const redoneEdit = pendingNonStyleRedo.edit;
374
+ if (
375
+ !deleteRuntimeElement(
376
+ redoneEdit.selector,
377
+ [redoneEdit.selector],
378
+ redoneEdit.requestId,
379
+ )
380
+ ) {
381
+ return;
382
+ }
383
+ pendingLiveNonStyleRedoStackRef.current = pendingNonStyleRedoStack.slice(
384
+ 0,
385
+ -1,
386
+ );
387
+ pendingLiveNonStyleUndoStackRef.current = [
388
+ ...pendingLiveNonStyleUndoStackRef.current.slice(
389
+ -(MAX_DESIGN_UNDO_STACK - 1),
390
+ ),
391
+ pendingNonStyleRedo,
392
+ ];
393
+ const nextPending = mergePendingLiveNonStyleEdits(
394
+ pendingLiveNonStyleUndoStackRef.current.map((entry) => entry.edit),
395
+ );
396
+ pendingLiveNonStyleEditsRef.current = nextPending;
397
+ setPendingLiveNonStyleEdits(nextPending);
398
+ syncUndoRedoState();
399
+ return;
400
+ }
401
+ if (pendingStructureRedoReplayRef.current) return;
402
+ pendingStructureRedoReplayRef.current = pendingNonStyleRedo;
403
+ if (redoCommand.kind === "insert") {
404
+ runtimeStructureInsertRevisionRef.current += 1;
405
+ setRuntimeStructureInsertRequest({
406
+ requestId: runtimeStructureInsertRevisionRef.current,
407
+ screenId: pendingNonStyleRedo.edit.screenId,
408
+ html: redoCommand.html,
409
+ replaceAnchor: redoCommand.replaceAnchor,
410
+ anchor: {
411
+ selector: pendingNonStyleRedo.edit.anchorSelector,
412
+ sourceId: pendingNonStyleRedo.edit.anchorSourceId ?? undefined,
413
+ },
414
+ placement: pendingNonStyleRedo.edit.placement,
415
+ });
416
+ if (pendingStructureRedoReplayTimerRef.current !== undefined) {
417
+ window.clearTimeout(pendingStructureRedoReplayTimerRef.current);
418
+ }
419
+ pendingStructureRedoReplayTimerRef.current = window.setTimeout(() => {
420
+ pendingStructureRedoReplayRef.current = undefined;
421
+ pendingStructureRedoReplayTimerRef.current = undefined;
422
+ syncUndoRedoState();
423
+ }, 1_000);
424
+ syncUndoRedoState();
425
+ return;
426
+ }
427
+ runtimeStructureMoveRevisionRef.current += 1;
428
+ setRuntimeStructureMoveRequest({
429
+ requestId: runtimeStructureMoveRevisionRef.current,
430
+ screenId: pendingNonStyleRedo.edit.screenId,
431
+ subject: {
432
+ selector: pendingNonStyleRedo.edit.selector,
433
+ sourceId: pendingNonStyleRedo.edit.sourceId ?? undefined,
434
+ },
435
+ anchor: {
436
+ selector: pendingNonStyleRedo.edit.anchorSelector,
437
+ sourceId: pendingNonStyleRedo.edit.anchorSourceId ?? undefined,
438
+ },
439
+ placement: pendingNonStyleRedo.edit.placement,
440
+ });
441
+ if (pendingStructureRedoReplayTimerRef.current !== undefined) {
442
+ window.clearTimeout(pendingStructureRedoReplayTimerRef.current);
443
+ }
444
+ pendingStructureRedoReplayTimerRef.current = window.setTimeout(() => {
445
+ pendingStructureRedoReplayRef.current = undefined;
446
+ pendingStructureRedoReplayTimerRef.current = undefined;
447
+ syncUndoRedoState();
448
+ }, 1_000);
449
+ syncUndoRedoState();
450
+ return;
451
+ }
452
+ if (redoPendingNonStyleFirst && pendingNonStyleRedo?.kind === "layer-state") {
453
+ pendingLiveNonStyleRedoStackRef.current = pendingNonStyleRedoStack.slice(
454
+ 0,
455
+ -1,
456
+ );
457
+ pendingLiveNonStyleUndoStackRef.current = [
458
+ ...pendingLiveNonStyleUndoStackRef.current.slice(
459
+ -(MAX_DESIGN_UNDO_STACK - 1),
460
+ ),
461
+ pendingNonStyleRedo,
462
+ ];
463
+ const nextPending = mergePendingLiveNonStyleEdits(
464
+ pendingLiveNonStyleUndoStackRef.current.map((entry) => entry.edit),
465
+ );
466
+ pendingLiveNonStyleEditsRef.current = nextPending;
467
+ setPendingLayerStateReplayRequest({
468
+ requestId: Date.now() + Math.random(),
469
+ patches: [
470
+ {
471
+ screenId: pendingNonStyleRedo.edit.screenId,
472
+ layerId: pendingNonStyleRedo.edit.layerId,
473
+ state: pendingNonStyleRedo.edit.state,
474
+ enabled: pendingNonStyleRedo.edit.enabled,
475
+ },
476
+ ],
477
+ });
478
+ setPendingLiveNonStyleEdits(nextPending);
479
+ syncUndoRedoState();
480
+ return;
481
+ }
482
+ if (redoPendingNonStyleFirst && pendingNonStyleRedo?.kind === "text") {
483
+ const pendingTextRedo = pendingNonStyleRedo;
484
+ pendingLiveNonStyleRedoStackRef.current = pendingNonStyleRedoStack.slice(
485
+ 0,
486
+ -1,
487
+ );
488
+ pendingLiveNonStyleUndoStackRef.current = [
489
+ ...pendingLiveNonStyleUndoStackRef.current.slice(
490
+ -(MAX_DESIGN_UNDO_STACK - 1),
491
+ ),
492
+ pendingTextRedo,
493
+ ];
494
+ const nextPending = mergePendingLiveNonStyleEdits(
495
+ pendingLiveNonStyleUndoStackRef.current.map((entry) => entry.edit),
496
+ );
497
+ pendingLiveNonStyleEditsRef.current = nextPending;
498
+ setPendingTextRevertRequest({
499
+ requestId: Date.now() + Math.random(),
500
+ patches: [
501
+ {
502
+ screenId: pendingTextRedo.edit.screenId,
503
+ selector: pendingTextRedo.edit.selector,
504
+ sourceId: pendingTextRedo.edit.sourceId,
505
+ value: pendingTextRedo.edit.value,
506
+ html: pendingTextRedo.edit.html,
507
+ },
508
+ ],
509
+ });
510
+ setPendingLiveNonStyleEdits(nextPending);
511
+ // Bug fix — same stale-inspector-panel issue as handleUndo. Redo
512
+ // reapplies pendingTextRedo.edit.value/html via
513
+ // setPendingTextRevertRequest above, but never resynced
514
+ // selectedElement, so resync with the same values here.
515
+ if (pendingTextRedo.edit.screenId === activeFile?.id) {
516
+ const {
517
+ sourceId: redoneSourceId,
518
+ selector: redoneSelector,
519
+ value: redoneValue,
520
+ html: redoneHtml,
521
+ } = pendingTextRedo.edit;
522
+ setSelectedElement((prev) => {
523
+ if (!prev) return prev;
524
+ if (
525
+ !pendingEditTargetsSelectedElement({
526
+ editSourceId: redoneSourceId,
527
+ editSelector: redoneSelector,
528
+ selectedSourceId: prev.sourceId,
529
+ selectedSelector: prev.selector,
530
+ })
531
+ ) {
532
+ return prev;
533
+ }
534
+ return {
535
+ ...prev,
536
+ textContent: redoneValue,
537
+ htmlContent: redoneHtml ?? prev.htmlContent,
538
+ };
539
+ });
540
+ }
541
+ syncUndoRedoState();
542
+ return;
543
+ }
544
+ if (pendingLiveRedo) {
545
+ const nextRedoStack = pendingLiveRedoStack.slice(0, -1);
546
+ pendingVisualStyleRedoStackRef.current = nextRedoStack;
547
+ pendingVisualStyleUndoStackRef.current = [
548
+ ...pendingVisualStyleUndoStackRef.current.slice(
549
+ -(MAX_DESIGN_UNDO_STACK - 1),
550
+ ),
551
+ pendingLiveRedo,
552
+ ];
553
+ const nextPending = mergePendingVisualStyleEdits(
554
+ pendingVisualStyleUndoStackRef.current.map((entry) => entry.edit),
555
+ );
556
+ pendingVisualStyleEditsRef.current = nextPending;
557
+ setPendingVisualStyleRevertRequest({
558
+ requestId: Date.now() + Math.random(),
559
+ patches: [
560
+ {
561
+ screenId: pendingLiveRedo.edit.screenId,
562
+ selector: pendingLiveRedo.edit.selector,
563
+ sourceId: pendingLiveRedo.edit.sourceId,
564
+ // Redo builds its patch inline rather than through
565
+ // buildPendingVisualStyleRevertPatches, so it needs the runtime
566
+ // pair explicitly or it re-applies into the wrong namespace.
567
+ runtimeSelector: pendingLiveRedo.edit.runtimeSelector,
568
+ runtimeSourceId: pendingLiveRedo.edit.runtimeSourceId,
569
+ styles: pendingLiveRedo.edit.styles,
570
+ interactionState: pendingLiveRedo.edit.interactionState,
571
+ },
572
+ ],
573
+ });
574
+ setPendingVisualStyleEdits(nextPending);
575
+ // Bug fix — same stale-inspector-panel issue as handleUndo's style
576
+ // branch. Merge the redo's own style values (already applied to the
577
+ // DOM via setPendingVisualStyleRevertRequest above) into
578
+ // selectedElement.computedStyles.
579
+ if (pendingLiveRedo.edit.screenId === activeFile?.id) {
580
+ const {
581
+ sourceId: redoneStyleSourceId,
582
+ selector: redoneStyleSelector,
583
+ styles: redoneStyles,
584
+ } = pendingLiveRedo.edit;
585
+ setSelectedElement((prev) => {
586
+ if (!prev) return prev;
587
+ if (
588
+ !pendingEditTargetsSelectedElement({
589
+ editSourceId: redoneStyleSourceId,
590
+ editSelector: redoneStyleSelector,
591
+ selectedSourceId: prev.sourceId,
592
+ selectedSelector: prev.selector,
593
+ })
594
+ ) {
595
+ return prev;
596
+ }
597
+ return {
598
+ ...prev,
599
+ computedStyles: {
600
+ ...prev.computedStyles,
601
+ ...redoneStyles,
602
+ },
603
+ };
604
+ });
605
+ }
606
+ syncUndoRedoState();
607
+ return;
608
+ }
609
+ const clipboardPasteRedo =
610
+ clipboardPasteRedoStackRef.current[
611
+ clipboardPasteRedoStackRef.current.length - 1
612
+ ];
613
+ if (clipboardPasteRedo) {
614
+ const currentContent =
615
+ latestClipboardMutationContentRef.current.get(clipboardPasteRedo.fileId)
616
+ ?.content ??
617
+ pendingLocalFileContentsRef.current.get(clipboardPasteRedo.fileId)
618
+ ?.content ??
619
+ (clipboardPasteRedo.fileId === activeFile?.id
620
+ ? getFreshActiveContent()
621
+ : (getScreenContent(clipboardPasteRedo.fileId) ?? ""));
622
+ if (currentContent === clipboardPasteRedo.before) {
623
+ const clipboardMutation = publishAuthoritativeClipboardMutation({
624
+ fileId: clipboardPasteRedo.fileId,
625
+ baseContent: clipboardPasteRedo.before,
626
+ nextContent: clipboardPasteRedo.after,
627
+ origin: "clipboard-redo",
628
+ });
629
+ if (!clipboardMutation) return;
630
+ clipboardPasteRedoStackRef.current =
631
+ clipboardPasteRedoStackRef.current.slice(0, -1);
632
+ clipboardPasteUndoStackRef.current = [
633
+ ...clipboardPasteUndoStackRef.current.slice(
634
+ -(MAX_DESIGN_UNDO_STACK - 1),
635
+ ),
636
+ clipboardPasteRedo,
637
+ ];
638
+ if (clipboardPasteRedo.fileId === activeFile?.id) {
639
+ applyLocalContentUpdate(clipboardPasteRedo.after, {
640
+ recordHistory: false,
641
+ forcePreviewFullDocument: true,
642
+ immediateSave: true,
643
+ clipboardMutation,
644
+ });
645
+ } else {
646
+ applyFileContentUpdate(
647
+ clipboardPasteRedo.fileId,
648
+ clipboardPasteRedo.after,
649
+ {
650
+ recordHistory: false,
651
+ forcePreviewFullDocument: true,
652
+ clipboardMutation,
653
+ },
654
+ );
655
+ }
656
+ syncUndoRedoState();
657
+ return;
658
+ }
659
+ }
660
+ const um = undoManagerRef.current;
661
+ const canUseOverviewHistory = viewModeRef.current === "overview";
662
+ let prunedRedoHistory = 0;
663
+ const redoContent = (scope: "any" | "local" | "global" = "any") => {
664
+ if (scope !== "global" && um?.canRedo()) {
665
+ const beforeRedoContent = ydoc?.getText("content").toString();
666
+ um.redo();
667
+ if (ydoc && activeFile) {
668
+ const next = ydoc.getText("content").toString();
669
+ markPendingLocalFileContent(activeFile.id, next, activeFile.updatedAt);
670
+ lastLocalContentRef.current = next;
671
+ queueFileContentSave(activeFile.id, next, {
672
+ syncCollab: !(ydoc && isSynced),
673
+ });
674
+ // Holistic flash pipeline: see the matching comment in handleUndo —
675
+ // only fall back to a full srcdoc rebuild when the in-place bridge
676
+ // patch genuinely failed, instead of always reloading the iframe on
677
+ // top of an already-successful in-place replace.
678
+ if (
679
+ previewContentReplaceNeedsRenderFallback(
680
+ replacePreviewContent(next, null, {
681
+ forceFullDocument: true,
682
+ }),
683
+ )
684
+ ) {
685
+ setContentRenderRevision((revision) => revision + 1);
686
+ }
687
+ // Clear stale selection if the redo removed the selected element.
688
+ setSelectedElement((prev) => {
689
+ if (!prev) return prev;
690
+ return refreshElementInfoFromContent(next, prev);
691
+ });
692
+ setHoveredElement((prev) => {
693
+ if (!prev) return prev;
694
+ return refreshElementInfoFromContent(next, prev);
695
+ });
696
+ // U18: keep the layers-panel highlight in sync too.
697
+ setSelectedLayerIdsState((prev) =>
698
+ refreshSelectedLayerIdsFromContent(next, prev),
699
+ );
700
+ // Restore the local fallback mirror (see U3) that undoContent()
701
+ // dropped, so it survives a UndoManager teardown right after redo.
702
+ if (
703
+ typeof beforeRedoContent === "string" &&
704
+ beforeRedoContent !== next
705
+ ) {
706
+ recordLocalContentHistoryChangeFallback({
707
+ fileId: activeFile.id,
708
+ before: beforeRedoContent,
709
+ after: next,
710
+ });
711
+ }
712
+ }
713
+ historyOrderRef.current = [
714
+ ...historyOrderRef.current.slice(-(MAX_DESIGN_UNDO_STACK - 1)),
715
+ "content",
716
+ ];
717
+ return true;
718
+ }
719
+
720
+ if (!canUseOverviewHistory && scope !== "global" && activeFile?.id) {
721
+ const localIndex = findLastContentHistoryChangeIndex(
722
+ localContentRedoStackRef.current,
723
+ activeFile.id,
724
+ );
725
+ if (localIndex !== -1) {
726
+ const [entry] = localContentRedoStackRef.current.splice(localIndex, 1);
727
+ if (entry) {
728
+ localContentUndoStackRef.current = [
729
+ ...localContentUndoStackRef.current.slice(
730
+ -(MAX_DESIGN_UNDO_STACK - 1),
731
+ ),
732
+ entry,
733
+ ];
734
+ // U20: route a live-snapshot screen's replay through
735
+ // updateLiveScreenSnapshotContent — see the matching note above.
736
+ if (liveScreenSnapshotsById[entry.fileId]) {
737
+ updateLiveScreenSnapshotContent(entry.fileId, entry.after, {
738
+ recordHistory: false,
739
+ });
740
+ syncLiveScreenSnapshotPreview(entry.fileId, entry.after);
741
+ } else {
742
+ applyLocalContentUpdate(entry.after, {
743
+ refreshPreview: false,
744
+ forcePreviewFullDocument: true,
745
+ immediateSave: true,
746
+ recordHistory: false,
747
+ });
748
+ }
749
+ setSelectedElement((prev) => {
750
+ if (!prev) return prev;
751
+ return refreshElementInfoFromContent(entry.after, prev);
752
+ });
753
+ setHoveredElement((prev) => {
754
+ if (!prev) return prev;
755
+ return refreshElementInfoFromContent(entry.after, prev);
756
+ });
757
+ // U18: keep the layers-panel highlight in sync too.
758
+ setSelectedLayerIdsState((prev) =>
759
+ refreshSelectedLayerIdsFromContent(entry.after, prev),
760
+ );
761
+ return true;
762
+ }
763
+ }
764
+ }
765
+
766
+ if (scope === "local") return false;
767
+ if (!canUseOverviewHistory) return false;
768
+ const entry =
769
+ contentRedoStackRef.current[contentRedoStackRef.current.length - 1];
770
+ if (!entry) return false;
771
+ const entrySelection =
772
+ contentRedoSelectionStackRef.current[
773
+ contentRedoSelectionStackRef.current.length - 1
774
+ ];
775
+ const changes = getAvailableContentHistoryChanges(
776
+ entry,
777
+ files.map((file) => file.id),
778
+ activeFile?.id,
779
+ );
780
+ if (changes.length === 0) {
781
+ contentRedoStackRef.current.pop();
782
+ contentRedoSelectionStackRef.current.pop();
783
+ prunedRedoHistory += 1;
784
+ return false;
785
+ }
786
+ contentRedoStackRef.current.pop();
787
+ contentRedoSelectionStackRef.current.pop();
788
+ contentUndoStackRef.current = [
789
+ ...contentUndoStackRef.current.slice(-(MAX_DESIGN_UNDO_STACK - 1)),
790
+ entry,
791
+ ];
792
+ contentUndoSelectionStackRef.current = [
793
+ ...contentUndoSelectionStackRef.current.slice(
794
+ -(MAX_DESIGN_UNDO_STACK - 1),
795
+ ),
796
+ entrySelection,
797
+ ];
798
+ historyOrderRef.current = [
799
+ ...historyOrderRef.current.slice(-(MAX_DESIGN_UNDO_STACK - 1)),
800
+ "file-content",
801
+ ];
802
+ suppressContentHistoryRef.current = true;
803
+ try {
804
+ for (const change of changes) {
805
+ // U20: see the matching note in handleUndo — route a live-snapshot
806
+ // screen's replay through updateLiveScreenSnapshotContent instead
807
+ // of the regular content path.
808
+ if (liveScreenSnapshotsById[change.fileId]) {
809
+ updateLiveScreenSnapshotContent(change.fileId, change.after, {
810
+ recordHistory: false,
811
+ });
812
+ syncLiveScreenSnapshotPreview(change.fileId, change.after);
813
+ } else if (change.fileId === activeFile?.id) {
814
+ applyLocalContentUpdate(change.after, {
815
+ refreshPreview: false,
816
+ forcePreviewFullDocument: true,
817
+ immediateSave: true,
818
+ recordHistory: false,
819
+ });
820
+ } else {
821
+ applyFileContentUpdate(change.fileId, change.after, {
822
+ recordHistory: false,
823
+ refreshPreview: false,
824
+ });
825
+ }
826
+ }
827
+ } finally {
828
+ suppressContentHistoryRef.current = false;
829
+ }
830
+ const activeChange = changes.find(
831
+ (change) => change.fileId === activeFile?.id,
832
+ );
833
+ if (activeChange) {
834
+ setSelectedElement((prev) => {
835
+ if (!prev) return prev;
836
+ return refreshElementInfoFromContent(activeChange.after, prev);
837
+ });
838
+ setHoveredElement((prev) => {
839
+ if (!prev) return prev;
840
+ return refreshElementInfoFromContent(activeChange.after, prev);
841
+ });
842
+ // U18: keep the layers-panel highlight in sync too.
843
+ setSelectedLayerIdsState((prev) =>
844
+ refreshSelectedLayerIdsFromContent(activeChange.after, prev),
845
+ );
846
+ }
847
+ // Figma-parity undo/redo selection restore: see the matching note in
848
+ // undoContent above.
849
+ restoreSelectionSnapshot(entrySelection);
850
+ return true;
851
+ };
852
+ const redoGeometry = () => {
853
+ if (!canUseOverviewHistory) return false;
854
+ const entry = geometryRedoStackRef.current.pop();
855
+ if (!entry) return false;
856
+ // Freshness guard: when this entry was undone it wrote `entry.before`. If
857
+ // a peer/agent has since moved any touched frame, replaying `entry.after`
858
+ // would clobber that change — drop this entry and try the next redo.
859
+ const stale = staleGeometryFrameIds(
860
+ entry,
861
+ liveFrameGeometryRef.current,
862
+ entry.before,
863
+ );
864
+ if (stale.length > 0) {
865
+ console.debug(
866
+ "[design] skipping stale geometry redo; frames changed since capture:",
867
+ stale,
868
+ );
869
+ toast.info(t("designEditor.toasts.redoSkippedConcurrentEdit"));
870
+ return redoGeometry();
871
+ }
872
+ geometryUndoStackRef.current = [
873
+ ...geometryUndoStackRef.current.slice(-(MAX_DESIGN_UNDO_STACK - 1)),
874
+ entry,
875
+ ];
876
+ historyOrderRef.current = [
877
+ ...historyOrderRef.current.slice(-(MAX_DESIGN_UNDO_STACK - 1)),
878
+ "geometry",
879
+ ];
880
+ // U11: see the matching undoGeometry note — merge this entry's diff
881
+ // onto the current live map instead of replacing it wholesale.
882
+ writeFrameGeometrySnapshot(
883
+ applyGeometryHistoryDiff(
884
+ getCanvasFrameGeometry(designDataJsonRef.current),
885
+ entry,
886
+ "redo",
887
+ ),
888
+ {
889
+ syncViewportFrameIds: viewportChangedFrameIds(
890
+ entry.before,
891
+ entry.after,
892
+ ),
893
+ },
894
+ );
895
+ // Figma parity: redo re-selects whatever was selected when this
896
+ // gesture's change was originally made (i.e. the selection AFTER the
897
+ // gesture committed, matching what undo just took away).
898
+ restoreSelectionSnapshot(entry.selectionAfter);
899
+ return true;
900
+ };
901
+ // U12: redo a screen create/duplicate by recreating the file with the
902
+ // same filename/content/fileType and restoring its recorded geometry.
903
+ // This is async (createFileMutation), unlike every other redo path here,
904
+ // so it optimistically reports success immediately (mirrors
905
+ // handleAddScreen's own optimistic cache write) and surfaces a toast on
906
+ // failure instead of rolling the redo stacks back.
907
+ const redoFileCreation = () => {
908
+ if (!canUseOverviewHistory) return false;
909
+ const entry = fileCreationRedoStackRef.current.pop();
910
+ if (!entry) return false;
911
+ if (!id) return false;
912
+ fileCreationUndoStackRef.current = [
913
+ ...fileCreationUndoStackRef.current.slice(-(MAX_DESIGN_UNDO_STACK - 1)),
914
+ entry,
915
+ ];
916
+ historyOrderRef.current = [
917
+ ...historyOrderRef.current.slice(-(MAX_DESIGN_UNDO_STACK - 1)),
918
+ "file-created",
919
+ ];
920
+ fileHistoryMutationPendingRef.current = true;
921
+ syncUndoRedoState();
922
+ createFileMutation.mutate(
923
+ {
924
+ designId: id,
925
+ filename: entry.filename,
926
+ content: entry.content,
927
+ fileType: entry.fileType,
928
+ } as any,
929
+ {
930
+ onSuccess: (result: any) => {
931
+ const nextId = typeof result?.id === "string" ? result.id : null;
932
+ if (nextId) {
933
+ const geometry = {
934
+ ...getInitialFrameGeometry(overviewScreens.length, {
935
+ width: 1280,
936
+ height: 2560,
937
+ }),
938
+ ...entry.geometry,
939
+ };
940
+ optimisticallyInsertCreatedFile({
941
+ fileId: nextId,
942
+ filename: entry.filename,
943
+ fileType: entry.fileType,
944
+ content: entry.content,
945
+ result,
946
+ });
947
+ writeFrameGeometrySnapshot({
948
+ ...getCanvasFrameGeometry(designDataJsonRef.current),
949
+ [nextId]: geometry,
950
+ });
951
+ focusCreatedScreen(nextId, geometry);
952
+ }
953
+ fileHistoryMutationPendingRef.current = false;
954
+ syncUndoRedoState();
955
+ queryClient.invalidateQueries({
956
+ queryKey: ["action", "get-design"],
957
+ });
958
+ },
959
+ onError: (error: unknown) => {
960
+ // The optimistic history move happened before the request. Put the
961
+ // entry back exactly where it came from so a failed redo remains
962
+ // retryable and does not leave a phantom undo operation behind.
963
+ if (
964
+ fileCreationUndoStackRef.current[
965
+ fileCreationUndoStackRef.current.length - 1
966
+ ] === entry
967
+ ) {
968
+ fileCreationUndoStackRef.current =
969
+ fileCreationUndoStackRef.current.slice(0, -1);
970
+ }
971
+ historyOrderRef.current = removeRecentUndoRedoOrderKinds(
972
+ historyOrderRef.current,
973
+ "file-created",
974
+ 1,
975
+ );
976
+ fileCreationRedoStackRef.current = [
977
+ ...fileCreationRedoStackRef.current.slice(
978
+ -(MAX_DESIGN_UNDO_STACK - 1),
979
+ ),
980
+ entry,
981
+ ];
982
+ redoOrderRef.current = [
983
+ ...redoOrderRef.current.slice(-(MAX_DESIGN_UNDO_STACK - 1)),
984
+ "file-created",
985
+ ];
986
+ fileHistoryMutationPendingRef.current = false;
987
+ syncUndoRedoState();
988
+ toast.error(
989
+ error instanceof Error
990
+ ? error.message
991
+ : t("designEditor.toasts.screenDuplicateError"),
992
+ );
993
+ },
994
+ },
995
+ );
996
+ return true;
997
+ };
998
+ const redoFileDeletion = () => {
999
+ if (!canUseOverviewHistory) return false;
1000
+ const entry = fileDeletionRedoStackRef.current.pop();
1001
+ if (!entry) return false;
1002
+
1003
+ fileHistoryMutationPendingRef.current = true;
1004
+ syncUndoRedoState();
1005
+ performDeleteFiles(entry.files, {
1006
+ onMutationSettled: (deletedFiles, failedFiles) => {
1007
+ if (deletedFiles.length > 0) {
1008
+ const deletedIds = new Set(deletedFiles.map((file) => file.id));
1009
+ fileDeletionUndoStackRef.current = [
1010
+ ...fileDeletionUndoStackRef.current.slice(
1011
+ -(MAX_DESIGN_UNDO_STACK - 1),
1012
+ ),
1013
+ filterFileDeletionHistoryEntry(entry, deletedIds),
1014
+ ];
1015
+ historyOrderRef.current = [
1016
+ ...historyOrderRef.current.slice(-(MAX_DESIGN_UNDO_STACK - 1)),
1017
+ "file-deleted",
1018
+ ];
1019
+ }
1020
+ if (failedFiles.length > 0) {
1021
+ const failedIds = new Set(failedFiles.map((file) => file.id));
1022
+ fileDeletionRedoStackRef.current = [
1023
+ ...fileDeletionRedoStackRef.current.slice(
1024
+ -(MAX_DESIGN_UNDO_STACK - 1),
1025
+ ),
1026
+ filterFileDeletionHistoryEntry(entry, failedIds),
1027
+ ];
1028
+ redoOrderRef.current = [
1029
+ ...redoOrderRef.current.slice(-(MAX_DESIGN_UNDO_STACK - 1)),
1030
+ "file-deleted",
1031
+ ];
1032
+ }
1033
+ fileHistoryMutationPendingRef.current = false;
1034
+ syncUndoRedoState();
1035
+ },
1036
+ });
1037
+ return true;
1038
+ };
1039
+
1040
+ const redoByOrder = (preferred?: UndoRedoOrderKind) => {
1041
+ if (preferred === "file-deleted") {
1042
+ return (
1043
+ redoFileDeletion() ||
1044
+ redoFileCreation() ||
1045
+ redoContent() ||
1046
+ redoGeometry()
1047
+ );
1048
+ }
1049
+ if (preferred === "file-created")
1050
+ return (
1051
+ redoFileCreation() ||
1052
+ redoFileDeletion() ||
1053
+ redoContent() ||
1054
+ redoGeometry()
1055
+ );
1056
+ if (preferred === "geometry") return redoGeometry() || redoContent();
1057
+ if (preferred === "file-content") {
1058
+ const prunedBefore = prunedRedoHistory;
1059
+ if (redoContent("global")) return true;
1060
+ if (prunedRedoHistory > prunedBefore) return false;
1061
+ return redoGeometry();
1062
+ }
1063
+ if (preferred === "content") {
1064
+ const prunedBefore = prunedRedoHistory;
1065
+ return (
1066
+ redoContent("local") ||
1067
+ redoContent("global") ||
1068
+ (prunedRedoHistory > prunedBefore ? false : redoGeometry())
1069
+ );
1070
+ }
1071
+ return redoFileDeletion() || redoContent() || redoGeometry();
1072
+ };
1073
+ let didRedo = false;
1074
+ if (canUseOverviewHistory) {
1075
+ while (!didRedo) {
1076
+ const preferred = redoOrderRef.current.pop();
1077
+ didRedo = redoByOrder(preferred);
1078
+ if (didRedo || preferred === undefined) break;
1079
+ }
1080
+ } else {
1081
+ didRedo = redoContent("local");
1082
+ }
1083
+ if (didRedo || prunedRedoHistory > 0) {
1084
+ syncUndoRedoState();
1085
+ }
1086
+ }