@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,335 @@
1
+ import { useActionMutation } from "@agent-native/core/client/hooks";
2
+ import {
3
+ compile as compileMotionTimeline,
4
+ injectManagedMotionCss,
5
+ } from "@shared/motion-compiler";
6
+ import { sortMotionKeyframes } from "@shared/motion-timeline";
7
+ import type { QueryClient } from "@tanstack/react-query";
8
+ import type { Dispatch, RefObject, SetStateAction } from "react";
9
+ import { toast } from "sonner";
10
+
11
+ import type { MotionDockTrack } from "@/components/design/MotionDock";
12
+ import type { ClipboardContentMutationPublication } from "@/lib/clipboard-content-lineage";
13
+ import { MOTION_AUTOSAVE_DELAY_MS } from "@/pages/design-editor/editor-constants";
14
+ import { getFreshActiveFileContent } from "@/pages/design-editor/editor-state";
15
+ import { motionTimelineFingerprint } from "@/pages/design-editor/motion-state";
16
+ import type { DesignFile } from "@/pages/design-editor/types";
17
+
18
+ export interface MotionAutosaveArgs {
19
+ activeContent: string;
20
+ activeFile: DesignFile;
21
+ applyFileContentUpdate: (
22
+ fileId: string,
23
+ nextContent: string,
24
+ options?: {
25
+ refreshPreview?: boolean;
26
+ skipPreview?: boolean;
27
+ forcePreviewFullDocument?: boolean;
28
+ persist?: boolean;
29
+ recordHistory?: boolean;
30
+ updatedAt?: string;
31
+ clipboardMutation?: ClipboardContentMutationPublication;
32
+ },
33
+ ) => void;
34
+ applyMotionEdit: ReturnType<
35
+ typeof useActionMutation<undefined, undefined, "apply-motion-edit">
36
+ >["mutate"];
37
+ clearMotionAutosaveTimer: () => void;
38
+ getScreenContent: (screenId: string) => string;
39
+ id: string | undefined;
40
+ lastLocalContentRef: RefObject<string | null>;
41
+ lastScheduledMotionAutosaveRevisionRef: RefObject<number>;
42
+ latestActiveContentRef: RefObject<string | null>;
43
+ motionAutosaveFailedRevisionRef: RefObject<number | null>;
44
+ motionAutosaveFlushRef: RefObject<(() => void) | null>;
45
+ motionAutosavePending: ReturnType<
46
+ typeof useActionMutation<undefined, undefined, "apply-motion-edit">
47
+ >["isPending"];
48
+ motionAutosaveRevision: number;
49
+ motionAutosaveRevisionRef: RefObject<number>;
50
+ motionAutosaveTimerRef: RefObject<number | null>;
51
+ motionDefaultEase: string;
52
+ motionDurationMs: number;
53
+ motionTimelineId: string | null;
54
+ motionTracks: MotionDockTrack[];
55
+ motionTracksDirty: boolean;
56
+ previousMotionFileIdRef: RefObject<string | null>;
57
+ queryClient: QueryClient;
58
+ removeMotionTimeline: ReturnType<
59
+ typeof useActionMutation<undefined, undefined, "remove-motion-timeline">
60
+ >["mutate"];
61
+ removeMotionTimelineMutation: ReturnType<
62
+ typeof useActionMutation<undefined, undefined, "remove-motion-timeline">
63
+ >;
64
+ setMotionHydrationFingerprint: Dispatch<SetStateAction<string | null>>;
65
+ setMotionTimelineId: Dispatch<SetStateAction<string | null>>;
66
+ setMotionTracksDirty: Dispatch<SetStateAction<boolean>>;
67
+ }
68
+
69
+ export function runMotionAutosave({
70
+ activeContent,
71
+ activeFile,
72
+ applyFileContentUpdate,
73
+ applyMotionEdit,
74
+ clearMotionAutosaveTimer,
75
+ getScreenContent,
76
+ id,
77
+ lastLocalContentRef,
78
+ lastScheduledMotionAutosaveRevisionRef,
79
+ latestActiveContentRef,
80
+ motionAutosaveFailedRevisionRef,
81
+ motionAutosaveFlushRef,
82
+ motionAutosavePending,
83
+ motionAutosaveRevision,
84
+ motionAutosaveRevisionRef,
85
+ motionAutosaveTimerRef,
86
+ motionDefaultEase,
87
+ motionDurationMs,
88
+ motionTimelineId,
89
+ motionTracks,
90
+ motionTracksDirty,
91
+ previousMotionFileIdRef,
92
+ queryClient,
93
+ removeMotionTimeline,
94
+ removeMotionTimelineMutation,
95
+ setMotionHydrationFingerprint,
96
+ setMotionTimelineId,
97
+ setMotionTracksDirty,
98
+ }: MotionAutosaveArgs) {
99
+ if (!id || !activeFile?.id || !motionTracksDirty) return;
100
+ if (motionAutosavePending) return;
101
+ if (motionAutosaveFailedRevisionRef.current === motionAutosaveRevision)
102
+ return;
103
+ if (
104
+ lastScheduledMotionAutosaveRevisionRef.current === motionAutosaveRevision &&
105
+ motionAutosaveTimerRef.current !== null
106
+ ) {
107
+ return;
108
+ }
109
+ // U-motion-empty: the user deleted every track (or the last keyframe of
110
+ // the last track, which MotionDock collapses into removing the track).
111
+ // apply-motion-edit's schema rejects an empty tracks array, so this can't
112
+ // go through the normal autosave — it must go through remove-motion-timeline
113
+ // instead, which deletes the motion_timeline row AND strips the managed
114
+ // <style data-agent-native-motion> block so a reload doesn't restore the
115
+ // old animation. Nothing to remove if there was never a persisted
116
+ // timeline for this file (motionTimelineId is null) — just clear dirty.
117
+ if (motionTracks.length === 0) {
118
+ if (!motionTimelineId) {
119
+ setMotionTracksDirty(false);
120
+ return;
121
+ }
122
+ if (removeMotionTimelineMutation.isPending) return;
123
+ const revisionAtSchedule = motionAutosaveRevision;
124
+ const timelineIdAtSchedule = motionTimelineId;
125
+ removeMotionTimeline(
126
+ { designId: id, timelineId: timelineIdAtSchedule },
127
+ {
128
+ onSuccess: () => {
129
+ if (motionAutosaveRevisionRef.current !== revisionAtSchedule) return;
130
+ setMotionTracksDirty(false);
131
+ setMotionTimelineId(null);
132
+ setMotionHydrationFingerprint(null);
133
+ void queryClient.invalidateQueries({
134
+ queryKey: ["action", "get-motion-timeline"],
135
+ });
136
+ },
137
+ onError: (error: unknown) => {
138
+ if (motionAutosaveRevisionRef.current === revisionAtSchedule) {
139
+ motionAutosaveFailedRevisionRef.current = revisionAtSchedule;
140
+ }
141
+ toast.error(
142
+ error instanceof Error
143
+ ? error.message
144
+ : // i18n-ignore: fallback toast for motion timeline removal failure.
145
+ "Motion changes could not be saved.",
146
+ );
147
+ },
148
+ },
149
+ );
150
+ return;
151
+ }
152
+
153
+ const revisionAtSchedule = motionAutosaveRevision;
154
+ lastScheduledMotionAutosaveRevisionRef.current = revisionAtSchedule;
155
+ clearMotionAutosaveTimer();
156
+ const fileIdAtSchedule = activeFile.id;
157
+ const fileRevisionAtSchedule = activeFile.updatedAt;
158
+ const fire = () => {
159
+ if (motionAutosaveRevisionRef.current !== revisionAtSchedule) return;
160
+ // Drop empty tracks defensively (a 0-keyframe track fails the action
161
+ // schema and would brick every subsequent autosave) and canonicalise
162
+ // keyframe order so the persisted JSON is always time-sorted. Full
163
+ // emptiness (0 tracks) is handled above via remove-motion-timeline
164
+ // before this closure is ever scheduled; this guards the case where
165
+ // every remaining track individually has 0 keyframes (shouldn't
166
+ // normally happen — MotionDock removes a track once its keyframes hit
167
+ // 0 — but stays a defensive no-op rather than sending an invalid
168
+ // payload that would brick the next autosave).
169
+ const tracksForSave = motionTracks
170
+ .filter((track) => track.keyframes.length > 0)
171
+ .map(({ label: _label, ...track }) => ({
172
+ ...track,
173
+ keyframes: sortMotionKeyframes(track.keyframes),
174
+ }));
175
+ if (tracksForSave.length === 0) return;
176
+ // When flushed after a file switch, the schedule-time file is no longer
177
+ // active; resolve its content from the per-screen cache instead of the
178
+ // active-file refs (which may already point at the NEW file).
179
+ const isActiveFileNow =
180
+ previousMotionFileIdRef.current === fileIdAtSchedule;
181
+ const currentContent = isActiveFileNow
182
+ ? getFreshActiveFileContent({
183
+ activeContent,
184
+ latestContent: latestActiveContentRef.current,
185
+ lastLocalContent: lastLocalContentRef.current,
186
+ })
187
+ : getScreenContent(fileIdAtSchedule) || activeContent;
188
+ const localMotionCss = compileMotionTimeline({
189
+ id: motionTimelineId ?? "",
190
+ designId: id,
191
+ sourceRef: fileIdAtSchedule,
192
+ filePath: null,
193
+ tracks: tracksForSave,
194
+ durationMs: motionDurationMs,
195
+ defaultEase: motionDefaultEase,
196
+ compiledHash: null,
197
+ createdAt: "",
198
+ updatedAt: "",
199
+ }).css;
200
+ applyMotionEdit(
201
+ {
202
+ designId: id,
203
+ fileId: fileIdAtSchedule,
204
+ timelineId: motionTimelineId ?? undefined,
205
+ sourceRef: fileIdAtSchedule,
206
+ tracks: tracksForSave,
207
+ durationMs: motionDurationMs,
208
+ defaultEase: motionDefaultEase,
209
+ currentContent,
210
+ revision: fileRevisionAtSchedule,
211
+ includeContent: false,
212
+ },
213
+ {
214
+ onSuccess: (result) => {
215
+ const response = result as {
216
+ fileId?: unknown;
217
+ timelineId?: unknown;
218
+ updatedAt?: unknown;
219
+ compiledHash?: unknown;
220
+ contentPatched?: unknown;
221
+ };
222
+ const isStillActiveFile =
223
+ previousMotionFileIdRef.current === fileIdAtSchedule;
224
+ if (typeof response.timelineId === "string" && isStillActiveFile) {
225
+ setMotionTimelineId(response.timelineId);
226
+ }
227
+ if (
228
+ motionAutosaveRevisionRef.current === revisionAtSchedule &&
229
+ isStillActiveFile
230
+ ) {
231
+ setMotionTracksDirty(false);
232
+ // Seed the hydration fingerprint with the EXPECTED post-save
233
+ // identity instead of null: a null fingerprint lets the
234
+ // still-stale get-motion-timeline cache re-hydrate the
235
+ // pre-save tracks (UI reverts, then jumps back on refetch,
236
+ // and edits made in that window lock in the reverted state).
237
+ const contentPatched = response.contentPatched !== false;
238
+ const expectedHash =
239
+ typeof response.compiledHash === "string"
240
+ ? response.compiledHash
241
+ : null;
242
+ setMotionHydrationFingerprint(
243
+ motionTimelineFingerprint(fileIdAtSchedule, {
244
+ id:
245
+ typeof response.timelineId === "string"
246
+ ? response.timelineId
247
+ : (motionTimelineId ?? null),
248
+ designId: id,
249
+ sourceRef: fileIdAtSchedule,
250
+ filePath: null,
251
+ tracks: tracksForSave,
252
+ durationMs: motionDurationMs,
253
+ defaultEase: motionDefaultEase,
254
+ compiledHash: expectedHash,
255
+ cssHash: contentPatched ? expectedHash : null,
256
+ source: "stored",
257
+ createdAt: null,
258
+ updatedAt:
259
+ typeof response.updatedAt === "string"
260
+ ? response.updatedAt
261
+ : null,
262
+ }),
263
+ );
264
+ lastScheduledMotionAutosaveRevisionRef.current = 0;
265
+ }
266
+ if (
267
+ typeof response.fileId === "string" &&
268
+ response.fileId === fileIdAtSchedule &&
269
+ response.contentPatched !== false
270
+ ) {
271
+ // Re-inject the managed CSS into the FRESHEST content at
272
+ // success time — replaying the pre-flight snapshot would
273
+ // clobber document edits made while the save was in flight.
274
+ const freshContent = isStillActiveFile
275
+ ? getFreshActiveFileContent({
276
+ activeContent: currentContent,
277
+ latestContent: latestActiveContentRef.current,
278
+ lastLocalContent: lastLocalContentRef.current,
279
+ })
280
+ : currentContent;
281
+ const hasConcurrentEdits = freshContent !== currentContent;
282
+ const patchedContent = injectManagedMotionCss(
283
+ freshContent,
284
+ localMotionCss,
285
+ );
286
+ applyFileContentUpdate(response.fileId, patchedContent, {
287
+ refreshPreview: false,
288
+ forcePreviewFullDocument: true,
289
+ recordHistory: false,
290
+ // Without concurrent edits, adopt the server's write as-is
291
+ // (no re-save). With concurrent edits the merged content must
292
+ // flow through the normal save path and stay marked pending.
293
+ ...(hasConcurrentEdits
294
+ ? {}
295
+ : {
296
+ persist: false,
297
+ ...(typeof response.updatedAt === "string"
298
+ ? { updatedAt: response.updatedAt }
299
+ : {}),
300
+ }),
301
+ });
302
+ }
303
+ void queryClient.invalidateQueries({
304
+ queryKey: ["action", "get-motion-timeline"],
305
+ });
306
+ },
307
+ onError: (error) => {
308
+ if (motionAutosaveRevisionRef.current === revisionAtSchedule) {
309
+ motionAutosaveFailedRevisionRef.current = revisionAtSchedule;
310
+ lastScheduledMotionAutosaveRevisionRef.current = 0;
311
+ }
312
+ toast.error(
313
+ error instanceof Error
314
+ ? error.message
315
+ : // i18n-ignore: fallback toast for motion autosave failure.
316
+ "Motion changes could not be saved.",
317
+ );
318
+ },
319
+ },
320
+ );
321
+ };
322
+ motionAutosaveTimerRef.current = window.setTimeout(() => {
323
+ motionAutosaveTimerRef.current = null;
324
+ motionAutosaveFlushRef.current = null;
325
+ fire();
326
+ }, MOTION_AUTOSAVE_DELAY_MS);
327
+ motionAutosaveFlushRef.current = () => {
328
+ if (motionAutosaveTimerRef.current !== null) {
329
+ window.clearTimeout(motionAutosaveTimerRef.current);
330
+ motionAutosaveTimerRef.current = null;
331
+ }
332
+ motionAutosaveFlushRef.current = null;
333
+ fire();
334
+ };
335
+ }
@@ -0,0 +1,183 @@
1
+ import type { Dispatch, RefObject, SetStateAction } from "react";
2
+ import * as Y from "yjs";
3
+
4
+ import type { ElementInfo } from "@/components/design/types";
5
+ import { refreshElementInfoFromContent } from "@/pages/design-editor/code-layer-state";
6
+ import { shouldApplyRemotePreviewContent } from "@/pages/design-editor/collab-sync";
7
+ import {
8
+ LOCAL_EDIT_ORIGIN,
9
+ TAB_ID,
10
+ shouldCheckpointAgentContent,
11
+ } from "@/pages/design-editor/editor-session";
12
+ import type { PreviewContentReplaceResult } from "@/pages/design-editor/editor-state";
13
+ import { previewContentReplaceNeedsRenderFallback } from "@/pages/design-editor/editor-state";
14
+ import type { ContentHistoryChange } from "@/pages/design-editor/history";
15
+
16
+ export interface ObserveCollabTextArgs {
17
+ activeFileId: string | null;
18
+ agentActive: boolean;
19
+ documentFileContentRef: RefObject<string | null>;
20
+ documentFileUpdatedAtRef: RefObject<string | null>;
21
+ isSynced: boolean;
22
+ lastAppliedFileUpdatedAtRef: RefObject<string | null>;
23
+ lastLocalContentRef: RefObject<string | null>;
24
+ latestActiveContentRef: RefObject<string | null>;
25
+ pendingLocalFileContentsRef: RefObject<
26
+ Map<
27
+ string,
28
+ { content: string; startedAt: number; baseUpdatedAt?: string | null }
29
+ >
30
+ >;
31
+ recordExternalContentHistoryCheckpoint: (
32
+ change: ContentHistoryChange,
33
+ ) => void;
34
+ replacePreviewContent: (
35
+ nextContent: string,
36
+ selector?: string | null,
37
+ options?: { forceFullDocument?: boolean },
38
+ ) => PreviewContentReplaceResult;
39
+ setCollabContent: Dispatch<SetStateAction<string | null>>;
40
+ setCollabContentFileId: Dispatch<SetStateAction<string | null>>;
41
+ setContentRenderRevision: Dispatch<SetStateAction<number>>;
42
+ setHoveredElement: Dispatch<SetStateAction<ElementInfo | null>>;
43
+ setSelectedElement: Dispatch<SetStateAction<ElementInfo | null>>;
44
+ undoManagerRef: RefObject<Y.UndoManager | null>;
45
+ ydoc: Y.Doc | null;
46
+ }
47
+
48
+ export function runObserveCollabText({
49
+ activeFileId,
50
+ agentActive,
51
+ documentFileContentRef,
52
+ documentFileUpdatedAtRef,
53
+ isSynced,
54
+ lastAppliedFileUpdatedAtRef,
55
+ lastLocalContentRef,
56
+ latestActiveContentRef,
57
+ pendingLocalFileContentsRef,
58
+ recordExternalContentHistoryCheckpoint,
59
+ replacePreviewContent,
60
+ setCollabContent,
61
+ setCollabContentFileId,
62
+ setContentRenderRevision,
63
+ setHoveredElement,
64
+ setSelectedElement,
65
+ undoManagerRef,
66
+ ydoc,
67
+ }: ObserveCollabTextArgs) {
68
+ if (!ydoc || !isSynced || !activeFileId) return;
69
+ const fileId = activeFileId;
70
+ const ytext = ydoc.getText("content");
71
+ const handler = (_event: unknown, transaction?: { origin?: unknown }) => {
72
+ const next = ytext.toString();
73
+ // Item 5 (edit-flash): capture what the preview already reflects BEFORE
74
+ // this observe fires, so a remote-origin transaction that merely ECHOES
75
+ // content we already rendered (e.g. update-file's own applyText/
76
+ // seedFromText round-tripping our own just-saved commit back through
77
+ // the collab sync channel) can be recognized as a no-op instead of
78
+ // unconditionally forcing a full srcdoc rebuild below. Every commit
79
+ // path already sets latestActiveContentRef.current synchronously
80
+ // before the network round trip lands, so this ref reliably holds the
81
+ // pre-update value at the moment a same-content echo arrives.
82
+ const previousActiveContent = latestActiveContentRef.current;
83
+ // UndoManager fires with itself as the origin; treat those as local too
84
+ // so the reconcile watermark and stale-selection fix are consistent.
85
+ const isLocalEdit =
86
+ transaction?.origin === TAB_ID ||
87
+ transaction?.origin === LOCAL_EDIT_ORIGIN ||
88
+ transaction?.origin === undoManagerRef.current;
89
+ if (
90
+ shouldCheckpointAgentContent({
91
+ agentActive,
92
+ isLocalEdit,
93
+ previousContent: previousActiveContent,
94
+ nextContent: next,
95
+ })
96
+ ) {
97
+ // An agent/chat edit is remote at the CRDT layer but local in the UX:
98
+ // Cmd+Z should restore the attachment/design state from before the run.
99
+ // Record the replacement in the history owned by the current view
100
+ // mode so it remains undoable from either canvas.
101
+ recordExternalContentHistoryCheckpoint({
102
+ fileId,
103
+ before: previousActiveContent!,
104
+ after: next,
105
+ });
106
+ }
107
+ const pendingLocalContent =
108
+ pendingLocalFileContentsRef.current.get(fileId)?.content;
109
+ if (pendingLocalContent && next !== pendingLocalContent && !isLocalEdit) {
110
+ setCollabContent(pendingLocalContent);
111
+ setCollabContentFileId(fileId);
112
+ lastLocalContentRef.current = pendingLocalContent;
113
+ latestActiveContentRef.current = pendingLocalContent;
114
+ if (
115
+ previewContentReplaceNeedsRenderFallback(
116
+ replacePreviewContent(pendingLocalContent, null, {
117
+ forceFullDocument: true,
118
+ }),
119
+ )
120
+ ) {
121
+ setContentRenderRevision((revision) => revision + 1);
122
+ }
123
+ // Untracked full rewrite — see clear() note in the seed effect above.
124
+ undoManagerRef.current?.clear(true, false);
125
+ ydoc.transact(() => {
126
+ ytext.delete(0, ytext.length);
127
+ ytext.insert(0, pendingLocalContent);
128
+ }, TAB_ID);
129
+ return;
130
+ }
131
+ setCollabContent(next);
132
+ setCollabContentFileId(fileId);
133
+ latestActiveContentRef.current = next;
134
+ if (isLocalEdit) {
135
+ lastLocalContentRef.current = next;
136
+ } else if (
137
+ shouldApplyRemotePreviewContent({
138
+ isLocalEdit,
139
+ previousContent: previousActiveContent,
140
+ nextContent: next,
141
+ })
142
+ ) {
143
+ // Holistic flash pipeline: a remote (peer/agent) edit arriving mid-
144
+ // session is exactly the "remote adoption" case that should apply
145
+ // in-place — this is not a file switch or initial mount. Try the
146
+ // bridge's live in-place full-document replace (same live iframe, no
147
+ // navigation) first; only fall back to an actual srcdoc rebuild when
148
+ // the bridge can't apply it (e.g. this screen's iframe isn't mounted/
149
+ // registered right now).
150
+ if (
151
+ previewContentReplaceNeedsRenderFallback(
152
+ replacePreviewContent(next, null, { forceFullDocument: true }),
153
+ )
154
+ ) {
155
+ setContentRenderRevision((revision) => revision + 1);
156
+ }
157
+ }
158
+ // Only advance the DB reconcile watermark when the live CRDT text
159
+ // actually matches the current SQL snapshot. Otherwise an intermediate
160
+ // or malformed Yjs update can shadow valid saved HTML until reload.
161
+ if (next === documentFileContentRef.current) {
162
+ lastAppliedFileUpdatedAtRef.current =
163
+ documentFileUpdatedAtRef.current ?? lastAppliedFileUpdatedAtRef.current;
164
+ }
165
+ // Stale-selection fix: when a remote/agent edit changes the document,
166
+ // verify the selected element still exists in the new DOM. If not, clear
167
+ // selection and hover so the Edit panel doesn't operate on a ghost element.
168
+ if (!isLocalEdit) {
169
+ setSelectedElement((prev) => {
170
+ if (!prev) return prev;
171
+ return refreshElementInfoFromContent(next, prev);
172
+ });
173
+ setHoveredElement((prev) => {
174
+ if (!prev) return prev;
175
+ return refreshElementInfoFromContent(next, prev);
176
+ });
177
+ }
178
+ };
179
+ ytext.observe(handler);
180
+ return () => {
181
+ ytext.unobserve(handler);
182
+ };
183
+ }