@agent-native/core 0.80.11 → 0.81.1

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 (230) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +53 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/cli/design-connect.ts +419 -19
  5. package/corpus/core/src/client/AgentPanel.tsx +33 -1
  6. package/corpus/core/src/client/ConnectBuilderCard.tsx +1 -0
  7. package/corpus/core/src/client/chat/index.ts +1 -0
  8. package/corpus/core/src/client/chat/run-recovery.tsx +9 -7
  9. package/corpus/core/src/client/chat-view-transition.ts +33 -2
  10. package/corpus/core/src/client/composer/PromptComposer.tsx +21 -2
  11. package/corpus/core/src/client/index.ts +1 -0
  12. package/corpus/core/src/client/org/OrgSwitcher.tsx +26 -4
  13. package/corpus/core/src/client/sharing/ShareButton.tsx +3 -3
  14. package/corpus/core/src/client/use-agent-chat-home-handoff.ts +31 -3
  15. package/corpus/core/src/db/client.ts +29 -2
  16. package/corpus/core/src/provider-api/index.ts +29 -0
  17. package/corpus/core/src/server/core-routes-plugin.ts +58 -20
  18. package/corpus/core/src/styles/agent-native.css +54 -0
  19. package/corpus/core/src/usage/store.ts +2 -2
  20. package/corpus/templates/analytics/app/components/layout/Layout.tsx +118 -10
  21. package/corpus/templates/analytics/app/components/layout/layout-route-policy.ts +7 -0
  22. package/corpus/templates/analytics/app/global.css +8 -0
  23. package/corpus/templates/analytics/app/hooks/use-navigation-state.ts +7 -1
  24. package/corpus/templates/analytics/app/lib/chat-handoff.ts +34 -0
  25. package/corpus/templates/analytics/app/pages/Ask.tsx +9 -18
  26. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +2 -2
  27. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +109 -21
  28. package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +4 -6
  29. package/corpus/templates/analytics/changelog/2026-06-30-ask-opens-a-fresh-chat-unless-you-were-just-chatting-and-das.md +6 -0
  30. package/corpus/templates/analytics/changelog/2026-06-30-dashboard-email-reports-are-more-reliable-and-use-cleaner.md +6 -0
  31. package/corpus/templates/analytics/changelog/2026-06-30-dashboard-toolbar-controls-use-a-quieter-share-button-and-ve.md +6 -0
  32. package/corpus/templates/analytics/changelog/2026-06-30-session-replay-rows-show-whole-minute-durations-in-the-playl.md +6 -0
  33. package/corpus/templates/analytics/changelog/2026-06-30-session-replays-now-render-captured-styling-and-keep-the-eve.md +6 -0
  34. package/corpus/templates/analytics/server/jobs/dashboard-report.ts +16 -3
  35. package/corpus/templates/analytics/server/lib/dashboard-report.ts +31 -14
  36. package/corpus/templates/assets/actions/open-asset-picker.ts +9 -0
  37. package/corpus/templates/assets/app/global.css +15 -0
  38. package/corpus/templates/assets/app/hooks/use-navigation-state.ts +4 -0
  39. package/corpus/templates/assets/app/routes/library.tsx +90 -16
  40. package/corpus/templates/assets/changelog/2026-06-30-the-assets-picker-now-supports-a-vertical-embedded-layout-fo.md +6 -0
  41. package/corpus/templates/clips/actions/finalize-recording.ts +1 -12
  42. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +10 -5
  43. package/corpus/templates/clips/app/routes/share.$shareId.tsx +6 -8
  44. package/corpus/templates/clips/changelog/2026-06-30-extension-save-recovery-avoids-false-upload-failures.md +6 -0
  45. package/corpus/templates/clips/changelog/2026-06-30-longer-chrome-extension-recordings-now-keep-saving-reliably-.md +6 -0
  46. package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -1
  47. package/corpus/templates/clips/chrome-extension/src/background.ts +19 -10
  48. package/corpus/templates/clips/chrome-extension/src/content-script.ts +38 -12
  49. package/corpus/templates/clips/chrome-extension/src/finalize-recovery.ts +224 -0
  50. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +119 -16
  51. package/corpus/templates/clips/chrome-extension/src/overlay.ts +16 -0
  52. package/corpus/templates/clips/server/lib/recording-upload-state.ts +48 -0
  53. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/abort.post.ts +9 -1
  54. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +63 -13
  55. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/status.get.ts +74 -0
  56. package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +32 -0
  57. package/corpus/templates/design/AGENTS.md +27 -0
  58. package/corpus/templates/design/CODE_NATIVE_DESIGN_STUDIO_IMPLEMENTATION_PLAN.md +1 -1
  59. package/corpus/templates/design/DESIGN-STUDIO-PLAN.md +5 -5
  60. package/corpus/templates/design/actions/apply-component-prop-edit.ts +63 -20
  61. package/corpus/templates/design/actions/apply-motion-edit.ts +67 -69
  62. package/corpus/templates/design/actions/apply-shader-fill.ts +148 -56
  63. package/corpus/templates/design/actions/capture-design-state.ts +27 -4
  64. package/corpus/templates/design/actions/connect-localhost.ts +59 -22
  65. package/corpus/templates/design/actions/export-zip.ts +23 -8
  66. package/corpus/templates/design/actions/generate-design.ts +53 -1
  67. package/corpus/templates/design/actions/get-component-details.ts +5 -19
  68. package/corpus/templates/design/actions/get-motion-timeline.ts +135 -10
  69. package/corpus/templates/design/actions/grant-localhost-write-consent.ts +116 -0
  70. package/corpus/templates/design/actions/import-design-tokens.ts +521 -0
  71. package/corpus/templates/design/actions/index-design-tokens.ts +10 -1
  72. package/corpus/templates/design/actions/insert-design-native-asset.ts +268 -0
  73. package/corpus/templates/design/actions/insert-figma-library-asset.ts +233 -0
  74. package/corpus/templates/design/actions/list-design-extensions.ts +32 -10
  75. package/corpus/templates/design/actions/list-design-native-assets.ts +105 -0
  76. package/corpus/templates/design/actions/list-figma-library-assets.ts +292 -0
  77. package/corpus/templates/design/actions/migrate-board-objects-to-file.ts +238 -0
  78. package/corpus/templates/design/actions/navigate.ts +18 -3
  79. package/corpus/templates/design/actions/provider-api-catalog.ts +2 -2
  80. package/corpus/templates/design/actions/provider-api-docs.ts +1 -1
  81. package/corpus/templates/design/actions/provider-api-request.ts +3 -3
  82. package/corpus/templates/design/actions/revoke-localhost-write-consent.ts +48 -0
  83. package/corpus/templates/design/actions/run-design-audit.ts +53 -4
  84. package/corpus/templates/design/actions/run-design-extension-action.ts +1 -1
  85. package/corpus/templates/design/actions/view-screen.ts +1 -1
  86. package/corpus/templates/design/actions/write-local-file.ts +213 -0
  87. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +230 -3308
  88. package/corpus/templates/design/app/components/design/DesignEditorSkeleton.tsx +30 -20
  89. package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +776 -287
  90. package/corpus/templates/design/app/components/design/EditPanel.tsx +212 -208
  91. package/corpus/templates/design/app/components/design/LayersPanel.tsx +14 -2
  92. package/corpus/templates/design/app/components/design/LocalhostWriteConsentDialog.tsx +161 -0
  93. package/corpus/templates/design/app/components/design/MotionDock.tsx +235 -266
  94. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +490 -55
  95. package/corpus/templates/design/app/components/design/ReviewPanel.tsx +43 -62
  96. package/corpus/templates/design/app/components/design/StatesPanel.tsx +34 -61
  97. package/corpus/templates/design/app/components/design/TokensPanel.tsx +251 -0
  98. package/corpus/templates/design/app/components/design/bridge/codegen.ts +125 -0
  99. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +4037 -0
  100. package/corpus/templates/design/app/components/design/bridge/embedded-wheel.bridge.ts +62 -0
  101. package/corpus/templates/design/app/components/design/bridge/hit-test.bridge.ts +272 -0
  102. package/corpus/templates/design/app/components/design/bridge/nav.bridge.ts +116 -0
  103. package/corpus/templates/design/app/components/design/bridge/sample.bridge.ts +24 -0
  104. package/corpus/templates/design/app/components/design/bridge/shader-fill-preview.bridge.ts +78 -0
  105. package/corpus/templates/design/app/components/design/bridge/tsconfig.json +17 -0
  106. package/corpus/templates/design/app/components/design/bridge/tweak.bridge.ts +28 -0
  107. package/corpus/templates/design/app/components/design/bridge/zoom.bridge.ts +43 -0
  108. package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +45 -16
  109. package/corpus/templates/design/app/components/design/inspector/ImageFillControls.tsx +88 -9
  110. package/corpus/templates/design/app/components/editor/PromptDialog.tsx +94 -9
  111. package/corpus/templates/design/app/components/layout/Layout.tsx +19 -3
  112. package/corpus/templates/design/app/components/layout/Sidebar.tsx +1 -1
  113. package/corpus/templates/design/app/global.css +7 -0
  114. package/corpus/templates/design/app/hooks/use-navigation-state.ts +23 -0
  115. package/corpus/templates/design/app/i18n/zh-TW.ts +21 -0
  116. package/corpus/templates/design/app/i18n-data.ts +358 -0
  117. package/corpus/templates/design/app/lib/pending-generation.ts +1 -0
  118. package/corpus/templates/design/app/pages/DesignEditor.tsx +2402 -499
  119. package/corpus/templates/design/app/pages/Index.tsx +5 -1
  120. package/corpus/templates/design/changelog/2026-06-30-applying-the-one-click-fix-for-a-small-tap-target-accessibil.md +6 -0
  121. package/corpus/templates/design/changelog/2026-06-30-canvas-creation-screen-dragging-undo-redo-and-image-fill-edi.md +6 -0
  122. package/corpus/templates/design/changelog/2026-06-30-component-prop-edits-and-shader-fills-now-preserve-rapid-sam.md +6 -0
  123. package/corpus/templates/design/changelog/2026-06-30-design-can-browse-figma-library-components-and-insert-them-i.md +6 -0
  124. package/corpus/templates/design/changelog/2026-06-30-design-now-has-a-figma-style-left-rail-for-file-agent-assets.md +6 -0
  125. package/corpus/templates/design/changelog/2026-06-30-design-now-has-native-components-embedded-media-and-figma-im.md +6 -0
  126. package/corpus/templates/design/changelog/2026-06-30-design-preview-now-includes-a-publish-app-waitlist-option-fr.md +6 -0
  127. package/corpus/templates/design/changelog/2026-06-30-design-s-left-rail-now-matches-the-sidebar-chrome-and-uses-t.md +6 -0
  128. package/corpus/templates/design/changelog/2026-06-30-design-s-tools-create-menu-now-stays-compact-instead-of-rend.md +6 -0
  129. package/corpus/templates/design/changelog/2026-06-30-design-starter-prompts-now-begin-generating-immediately-and-.md +6 -0
  130. package/corpus/templates/design/changelog/2026-06-30-design-tools-now-use-a-quieter-figma-style-list-layout.md +6 -0
  131. package/corpus/templates/design/changelog/2026-06-30-drawing-rectangles-on-the-all-screens-canvas-no-longer-flash.md +6 -0
  132. package/corpus/templates/design/changelog/2026-06-30-fixed-code-native-design-studio-edits-so-component-props-mot.md +6 -0
  133. package/corpus/templates/design/changelog/2026-06-30-fixed-visual-editing-hover-overlays-and-spacing-handles-so-s.md +6 -0
  134. package/corpus/templates/design/changelog/2026-06-30-motion-edits-now-save-automatically-without-a-separate-css-a.md +6 -0
  135. package/corpus/templates/design/changelog/2026-06-30-motion-timelines-now-reopen-as-editable-tracks-after-writing.md +6 -0
  136. package/corpus/templates/design/changelog/2026-06-30-new-design-prompts-with-uploaded-screenshots-work-reliably-o.md +6 -0
  137. package/corpus/templates/design/changelog/2026-06-30-screen-previews-in-all-screens-view-now-keep-rounded-outline.md +6 -0
  138. package/corpus/templates/design/changelog/2026-06-30-selection-outlines-now-sit-flush-on-rectangles-drawn-directl.md +6 -0
  139. package/corpus/templates/design/changelog/2026-06-30-share-options-use-a-tighter-design-editor-popover-with-compa.md +6 -0
  140. package/corpus/templates/design/changelog/2026-06-30-the-collapsed-motion-panel-now-stays-tucked-under-the-left-s.md +6 -0
  141. package/corpus/templates/design/changelog/2026-06-30-the-design-inspector-now-shows-states-and-review-in-a-tighte.md +6 -0
  142. package/corpus/templates/design/changelog/2026-06-30-the-sidebar-keeps-the-organization-picker-space-stable-while.md +6 -0
  143. package/corpus/templates/design/changelog/2026-06-30-tokens-can-now-be-imported-from-pasted-notes-code-files-fold.md +6 -0
  144. package/corpus/templates/design/e2e/helpers.ts +168 -65
  145. package/corpus/templates/design/package.json +3 -1
  146. package/corpus/templates/design/server/db/schema.ts +23 -0
  147. package/corpus/templates/design/server/handlers/uploads.ts +22 -1
  148. package/corpus/templates/design/server/lib/provider-api.ts +7 -4
  149. package/corpus/templates/design/server/lib/verify-write-grant.ts +108 -0
  150. package/corpus/templates/design/server/plugins/db.ts +39 -0
  151. package/corpus/templates/design/server/register-secrets.ts +38 -0
  152. package/corpus/templates/design/shared/board-file.ts +228 -0
  153. package/corpus/templates/design/shared/board-objects.ts +288 -0
  154. package/corpus/templates/design/shared/code-layer.ts +52 -0
  155. package/corpus/templates/design/shared/motion-compiler.ts +58 -7
  156. package/corpus/templates/design/shared/motion-timeline.ts +2 -2
  157. package/corpus/templates/design/tsconfig.json +2 -1
  158. package/corpus/templates/slides/AGENTS.md +9 -0
  159. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +4 -0
  160. package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +4 -24
  161. package/corpus/templates/slides/app/lib/export-pdf-client.ts +29 -20
  162. package/corpus/templates/slides/app/lib/export-pptx-client.ts +365 -0
  163. package/corpus/templates/slides/app/pages/DeckEditor.tsx +12 -9
  164. package/corpus/templates/slides/app/vite-env.d.ts +18 -0
  165. package/corpus/templates/slides/changelog/2026-06-30-powerpoint-downloads-now-preserve-the-rendered-slide-layout-.md +6 -0
  166. package/corpus/templates/slides/changelog/2026-06-30-unavailable-decks-open-in-a-cleaner-full-height-centered-sta.md +6 -0
  167. package/corpus/templates/slides/package.json +1 -0
  168. package/corpus/templates/slides/vite.config.ts +1 -0
  169. package/dist/cli/design-connect.d.ts +25 -0
  170. package/dist/cli/design-connect.d.ts.map +1 -1
  171. package/dist/cli/design-connect.js +357 -18
  172. package/dist/cli/design-connect.js.map +1 -1
  173. package/dist/client/AgentPanel.d.ts +1 -1
  174. package/dist/client/AgentPanel.d.ts.map +1 -1
  175. package/dist/client/AgentPanel.js +18 -3
  176. package/dist/client/AgentPanel.js.map +1 -1
  177. package/dist/client/ConnectBuilderCard.d.ts.map +1 -1
  178. package/dist/client/ConnectBuilderCard.js +1 -0
  179. package/dist/client/ConnectBuilderCard.js.map +1 -1
  180. package/dist/client/chat/index.d.ts +1 -1
  181. package/dist/client/chat/index.d.ts.map +1 -1
  182. package/dist/client/chat/index.js +1 -1
  183. package/dist/client/chat/index.js.map +1 -1
  184. package/dist/client/chat/run-recovery.d.ts.map +1 -1
  185. package/dist/client/chat/run-recovery.js +5 -5
  186. package/dist/client/chat/run-recovery.js.map +1 -1
  187. package/dist/client/chat-view-transition.d.ts +6 -0
  188. package/dist/client/chat-view-transition.d.ts.map +1 -1
  189. package/dist/client/chat-view-transition.js +23 -2
  190. package/dist/client/chat-view-transition.js.map +1 -1
  191. package/dist/client/composer/PromptComposer.d.ts.map +1 -1
  192. package/dist/client/composer/PromptComposer.js +3 -2
  193. package/dist/client/composer/PromptComposer.js.map +1 -1
  194. package/dist/client/index.d.ts +1 -1
  195. package/dist/client/index.d.ts.map +1 -1
  196. package/dist/client/index.js +1 -1
  197. package/dist/client/index.js.map +1 -1
  198. package/dist/client/org/OrgSwitcher.d.ts.map +1 -1
  199. package/dist/client/org/OrgSwitcher.js +11 -4
  200. package/dist/client/org/OrgSwitcher.js.map +1 -1
  201. package/dist/client/sharing/ShareButton.js +3 -3
  202. package/dist/client/sharing/ShareButton.js.map +1 -1
  203. package/dist/client/use-agent-chat-home-handoff.d.ts +8 -2
  204. package/dist/client/use-agent-chat-home-handoff.d.ts.map +1 -1
  205. package/dist/client/use-agent-chat-home-handoff.js +20 -6
  206. package/dist/client/use-agent-chat-home-handoff.js.map +1 -1
  207. package/dist/collab/routes.d.ts +1 -1
  208. package/dist/db/client.d.ts +1 -0
  209. package/dist/db/client.d.ts.map +1 -1
  210. package/dist/db/client.js +28 -2
  211. package/dist/db/client.js.map +1 -1
  212. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  213. package/dist/notifications/routes.d.ts +2 -2
  214. package/dist/observability/routes.d.ts +8 -8
  215. package/dist/progress/routes.d.ts +1 -1
  216. package/dist/provider-api/index.d.ts +5 -4
  217. package/dist/provider-api/index.d.ts.map +1 -1
  218. package/dist/provider-api/index.js +28 -0
  219. package/dist/provider-api/index.js.map +1 -1
  220. package/dist/resources/handlers.d.ts +3 -3
  221. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  222. package/dist/server/core-routes-plugin.d.ts +25 -0
  223. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  224. package/dist/server/core-routes-plugin.js +43 -18
  225. package/dist/server/core-routes-plugin.js.map +1 -1
  226. package/dist/server/transcribe-voice.d.ts +1 -1
  227. package/dist/styles/agent-native.css +54 -0
  228. package/dist/usage/store.js +2 -2
  229. package/dist/usage/store.js.map +1 -1
  230. package/package.json +1 -1
@@ -6,31 +6,22 @@
6
6
  * 2. Persist the `motion_timeline` row (insert or update).
7
7
  * 3. Compile the tracks into deterministic CSS.
8
8
  * 4. Inject/replace the managed `<style data-agent-native-motion>` block inside
9
- * the design's HTML content, using the same persist path as apply-visual-edit
10
- * (Yjs/collab + SQL, via agentEnterDocument / applyText / seedFromText).
9
+ * the design's durable HTML content.
11
10
  * 5. Update `compiledHash` on the row to guard against drift.
12
11
  * 6. Return a diff summary (bytes before/after, track count, hash).
13
12
  *
14
13
  * Never writes unless all steps succeed. Scrubbing/preview is handled by the
15
14
  * separate `motion-preview` postMessage path on the frontend — this action is
16
- * the deliberate "Write to CSS" commit step.
15
+ * the durable autosave/persist path for edited timelines.
17
16
  */
18
17
 
19
18
  import { defineAction } from "@agent-native/core";
20
- import {
21
- agentEnterDocument,
22
- agentLeaveDocument,
23
- applyText,
24
- getText,
25
- hasCollabState,
26
- seedFromText,
27
- } from "@agent-native/core/collab";
28
19
  import {
29
20
  getRequestOrgId,
30
21
  getRequestUserEmail,
31
22
  } from "@agent-native/core/server/request-context";
32
23
  import { accessFilter, assertAccess } from "@agent-native/core/sharing";
33
- import { and, eq } from "drizzle-orm";
24
+ import { and, desc, eq } from "drizzle-orm";
34
25
  import { nanoid } from "nanoid";
35
26
  import { z } from "zod";
36
27
 
@@ -121,21 +112,6 @@ function injectMotionStyle(html: string, css: string): string {
121
112
  return block + "\n" + html;
122
113
  }
123
114
 
124
- async function liveFileContent(
125
- fileId: string,
126
- storedContent: string,
127
- ): Promise<string> {
128
- try {
129
- if (await hasCollabState(fileId)) {
130
- const live = await getText(fileId, "content");
131
- if (typeof live === "string") return live;
132
- }
133
- } catch {
134
- // Best-effort; SQL is the fallback.
135
- }
136
- return storedContent;
137
- }
138
-
139
115
  async function persistFileContent(
140
116
  fileId: string,
141
117
  designId: string,
@@ -143,29 +119,21 @@ async function persistFileContent(
143
119
  now: string,
144
120
  ): Promise<void> {
145
121
  const db = getDb();
146
- agentEnterDocument(fileId);
147
- try {
148
- await db
149
- .update(schema.designFiles)
150
- .set({ content, updatedAt: now })
151
- .where(eq(schema.designFiles.id, fileId));
152
-
153
- if (await hasCollabState(fileId)) {
154
- await applyText(fileId, content, "content", "agent");
155
- } else {
156
- await seedFromText(fileId, content);
157
- }
158
-
159
- // guard:allow-unscoped the action's run() asserts editor access via
160
- // assertAccess("design", designId, "editor") before this helper is
161
- // invoked; this only touches the addressed design row's updatedAt.
162
- await db
163
- .update(schema.designs)
164
- .set({ updatedAt: now })
165
- .where(eq(schema.designs.id, designId));
166
- } finally {
167
- agentLeaveDocument(fileId);
168
- }
122
+ await db
123
+ .update(schema.designFiles)
124
+ .set({ content, updatedAt: now })
125
+ .where(eq(schema.designFiles.id, fileId));
126
+
127
+ // Keep SQL as the source of truth for this atomic write. The editor adopts
128
+ // the returned HTML content without re-saving it; applying the whole document
129
+ // through an existing collab text snapshot can merge against stale iframe
130
+ // state and duplicate the managed motion stylesheet.
131
+ // guard:allow-unscoped — editor access on this design is asserted in run()
132
+ // before this helper is invoked; this only bumps the addressed design row.
133
+ await db
134
+ .update(schema.designs)
135
+ .set({ updatedAt: now })
136
+ .where(eq(schema.designs.id, designId));
169
137
  }
170
138
 
171
139
  // ─── Action ───────────────────────────────────────────────────────────────────
@@ -176,7 +144,7 @@ export default defineAction({
176
144
  "Persists the motion_timeline row, compiles tracks to CSS, injects the " +
177
145
  "managed <style data-agent-native-motion> block into the design's HTML, " +
178
146
  "and updates compiledHash — all in one atomic step. " +
179
- "This is the 'Write to CSS' commit path; preview/scrubbing uses the " +
147
+ "This is the durable timeline persist path; preview/scrubbing uses the " +
180
148
  "motion-preview postMessage bridge, NOT this action.",
181
149
  schema: z.object({
182
150
  designId: z.string().describe("Design project ID."),
@@ -231,6 +199,14 @@ export default defineAction({
231
199
  .optional()
232
200
  .default(false)
233
201
  .describe("Include the full patched HTML in the response (large)."),
202
+ currentContent: z
203
+ .string()
204
+ .optional()
205
+ .describe(
206
+ "Current open editor HTML for the target file. When supplied, the " +
207
+ "managed motion CSS is patched into this content instead of the " +
208
+ "last SQL snapshot so in-flight local edits are preserved.",
209
+ ),
234
210
  }),
235
211
  run: async ({
236
212
  designId,
@@ -241,6 +217,7 @@ export default defineAction({
241
217
  durationMs,
242
218
  defaultEase,
243
219
  includeContent,
220
+ currentContent: currentContentInput,
244
221
  }) => {
245
222
  await assertAccess("design", designId, "editor");
246
223
 
@@ -282,7 +259,11 @@ export default defineAction({
282
259
  }
283
260
 
284
261
  const fileId = file.id;
285
- const currentContent = await liveFileContent(fileId, file.content ?? "");
262
+ const resolvedSourceRef = sourceRef ?? fileId;
263
+ const currentContent =
264
+ currentContentInput !== undefined
265
+ ? currentContentInput
266
+ : (file.content ?? "");
286
267
 
287
268
  // ── 2. Compile tracks → CSS ─────────────────────────────────────────────
288
269
  const typedTracks = tracks as MotionTrack[];
@@ -304,7 +285,7 @@ export default defineAction({
304
285
  const { css, hash } = compile({
305
286
  id: timelineId ?? "",
306
287
  designId,
307
- sourceRef: sourceRef ?? null,
288
+ sourceRef: resolvedSourceRef,
308
289
  filePath: null,
309
290
  tracks: typedTracks,
310
291
  durationMs,
@@ -323,7 +304,7 @@ export default defineAction({
323
304
  // Resolve everything that can fail (existence + ownership) BEFORE touching
324
305
  // content, so we never persist HTML for a row that can't be written.
325
306
  const tracksJson = JSON.stringify(typedTracks);
326
- const resolvedTimelineId = timelineId ?? nanoid();
307
+ let existingTimelineId = timelineId;
327
308
 
328
309
  let insertOwnerEmail: string | null = null;
329
310
  let insertOrgId: string | null = null;
@@ -347,21 +328,37 @@ export default defineAction({
347
328
  );
348
329
  }
349
330
  } else {
350
- // Insert new row — derive ownership from the request context (same
351
- // pattern as create-design-state and other create actions).
352
- insertOwnerEmail = getRequestUserEmail() ?? null;
353
- if (!insertOwnerEmail) throw new Error("no authenticated user");
354
- insertOrgId = getRequestOrgId() ?? null;
331
+ const [existingForSource] = await db
332
+ .select({ id: schema.motionTimeline.id })
333
+ .from(schema.motionTimeline)
334
+ .where(
335
+ and(
336
+ eq(schema.motionTimeline.designId, designId),
337
+ eq(schema.motionTimeline.sourceRef, resolvedSourceRef),
338
+ ),
339
+ )
340
+ .orderBy(desc(schema.motionTimeline.updatedAt))
341
+ .limit(1);
342
+
343
+ if (existingForSource) {
344
+ existingTimelineId = existingForSource.id;
345
+ } else {
346
+ // Insert new row — derive ownership from the request context (same
347
+ // pattern as create-design-state and other create actions).
348
+ insertOwnerEmail = getRequestUserEmail() ?? null;
349
+ if (!insertOwnerEmail) throw new Error("no authenticated user");
350
+ insertOrgId = getRequestOrgId() ?? null;
351
+ }
355
352
  }
356
353
 
354
+ const resolvedTimelineId = existingTimelineId ?? nanoid();
355
+
357
356
  // ── 5. Persist the motion_timeline row FIRST (atomic SQL portion) ───────
358
357
  // The timeline row is written before the HTML so that a failure in the
359
- // HTML/collab write step cannot leave the design content mutated without a
360
- // corresponding row. The reverse (HTML first) was a false atomicity
361
- // guarantee: if the row write failed after the HTML write, the managed
362
- // <style> block would be permanently out of sync with the DB state.
358
+ // HTML write step cannot leave the design content mutated without a
359
+ // corresponding row.
363
360
  await db.transaction(async (tx) => {
364
- if (timelineId) {
361
+ if (existingTimelineId) {
365
362
  await tx
366
363
  .update(schema.motionTimeline)
367
364
  .set({
@@ -369,15 +366,15 @@ export default defineAction({
369
366
  durationMs,
370
367
  defaultEase,
371
368
  compiledHash: hash,
372
- sourceRef: sourceRef ?? null,
369
+ sourceRef: resolvedSourceRef,
373
370
  updatedAt: now,
374
371
  })
375
- .where(eq(schema.motionTimeline.id, timelineId));
372
+ .where(eq(schema.motionTimeline.id, existingTimelineId));
376
373
  } else {
377
374
  await tx.insert(schema.motionTimeline).values({
378
375
  id: resolvedTimelineId,
379
376
  designId,
380
- sourceRef: sourceRef ?? null,
377
+ sourceRef: resolvedSourceRef,
381
378
  filePath: null,
382
379
  tracks: tracksJson,
383
380
  durationMs,
@@ -391,8 +388,8 @@ export default defineAction({
391
388
  }
392
389
  });
393
390
 
394
- // ── 6. Persist the patched HTML content SECOND (Yjs/collab + SQL) ───────
395
- // Written after the row so a collab/SQL failure here leaves the timeline row
391
+ // ── 6. Persist the patched HTML content SECOND ─────────────────────────
392
+ // Written after the row so a SQL failure here leaves the timeline row
396
393
  // accurate (correct tracks + hash) and the stale HTML can be recompiled on
397
394
  // the next apply-motion-edit call via compiledHash drift detection.
398
395
  await persistFileContent(fileId, designId, patchedContent, now);
@@ -401,6 +398,7 @@ export default defineAction({
401
398
  timelineId: resolvedTimelineId,
402
399
  designId,
403
400
  fileId,
401
+ sourceRef: resolvedSourceRef,
404
402
  trackCount: typedTracks.length,
405
403
  compiledHash: hash,
406
404
  bytesBefore,
@@ -19,24 +19,15 @@
19
19
  * - The descriptor is validated against the preset manifest first, so callers
20
20
  * get clear errors before any round-trip.
21
21
  *
22
- * The write reuses the canonical persist path (Yjs/collab + SQL via
23
- * agentEnterDocument / applyText / seedFromText), so collaborators and the agent
24
- * stay in sync. The edit is a single inline-style `style.background` change on
25
- * the resolved node — no structural inserts, no new owned rows.
22
+ * The write persists durable SQL content directly. The edit is a single
23
+ * inline-style `style.background` change on the resolved node no structural
24
+ * inserts, no new owned rows.
26
25
  *
27
26
  * Plan reference: DESIGN-STUDIO-PLAN.md §6.7 + §7 (shader fill apply).
28
27
  */
29
28
 
30
29
  import { defineAction } from "@agent-native/core";
31
- import {
32
- agentEnterDocument,
33
- agentLeaveDocument,
34
- agentUpdateSelection,
35
- applyText,
36
- getText,
37
- hasCollabState,
38
- seedFromText,
39
- } from "@agent-native/core/collab";
30
+ import { agentUpdateSelection } from "@agent-native/core/collab";
40
31
  import { accessFilter, assertAccess } from "@agent-native/core/sharing";
41
32
  import { and, eq } from "drizzle-orm";
42
33
  import { z } from "zod";
@@ -65,6 +56,15 @@ const PRESET_NAMES = Object.keys(SHADER_PRESET_MAP) as [
65
56
  ShaderPresetName,
66
57
  ...ShaderPresetName[],
67
58
  ];
59
+ const REVISION_CONFLICT_MESSAGE =
60
+ "This file changed since this shader fill was previewed. Refresh the editor and try again.";
61
+
62
+ class ShaderFillRevisionConflictError extends Error {
63
+ constructor() {
64
+ super(REVISION_CONFLICT_MESSAGE);
65
+ this.name = "ShaderFillRevisionConflictError";
66
+ }
67
+ }
68
68
 
69
69
  const descriptorSchema = z.object({
70
70
  preset: z
@@ -101,6 +101,16 @@ const sourceSchema = z
101
101
  fileId: z.string().optional(),
102
102
  filename: z.string().optional(),
103
103
  revision: z.string().optional(),
104
+ currentContent: z
105
+ .string()
106
+ .optional()
107
+ .describe(
108
+ "Current open editor HTML for the target file. When supplied, the " +
109
+ "shader background is patched into this content instead of the " +
110
+ "last SQL snapshot so in-flight local edits are preserved. " +
111
+ "source.revision must also be supplied as the file updatedAt value " +
112
+ "that currentContent was based on.",
113
+ ),
104
114
  })
105
115
  .describe(
106
116
  "Design source. Only kind=design-file (HTML) is persisted. Provide " +
@@ -132,24 +142,11 @@ interface ResolvedDesignFile {
132
142
  designId: string;
133
143
  filename: string;
134
144
  content: string;
145
+ updatedAt: string | null;
146
+ expectedUpdatedAt?: string;
135
147
  codeLayerSource: CodeLayerSource;
136
148
  }
137
149
 
138
- async function liveContent(
139
- fileId: string,
140
- storedContent: string,
141
- ): Promise<string> {
142
- try {
143
- if (await hasCollabState(fileId)) {
144
- const live = await getText(fileId, "content");
145
- if (typeof live === "string") return live;
146
- }
147
- } catch {
148
- // Collab reads are best-effort; SQL content remains the fallback.
149
- }
150
- return storedContent;
151
- }
152
-
153
150
  /**
154
151
  * Resolve the target HTML design file with an access-scoped read, then assert
155
152
  * editor access. Mirrors `resolveEditableDesignFile` in apply-visual-edit.ts so
@@ -160,6 +157,7 @@ async function resolveEditableDesignFile(source: {
160
157
  fileId?: string;
161
158
  filename?: string;
162
159
  revision?: string;
160
+ currentContent?: string;
163
161
  }): Promise<ResolvedDesignFile> {
164
162
  if (!source.fileId && !source.designId) {
165
163
  throw new Error(
@@ -187,6 +185,7 @@ async function resolveEditableDesignFile(source: {
187
185
  filename: schema.designFiles.filename,
188
186
  fileType: schema.designFiles.fileType,
189
187
  content: schema.designFiles.content,
188
+ updatedAt: schema.designFiles.updatedAt,
190
189
  })
191
190
  .from(schema.designFiles)
192
191
  .innerJoin(
@@ -217,11 +216,31 @@ async function resolveEditableDesignFile(source: {
217
216
 
218
217
  await assertAccess("design", file.designId, "editor");
219
218
 
219
+ if (source.currentContent !== undefined && !source.revision) {
220
+ throw new Error(
221
+ "source.revision is required when source.currentContent is provided.",
222
+ );
223
+ }
224
+ if (
225
+ source.currentContent !== undefined &&
226
+ source.revision &&
227
+ file.updatedAt &&
228
+ source.revision !== file.updatedAt
229
+ ) {
230
+ throw new ShaderFillRevisionConflictError();
231
+ }
232
+
220
233
  return {
221
234
  id: file.id,
222
235
  designId: file.designId,
223
236
  filename: file.filename,
224
- content: await liveContent(file.id, file.content ?? ""),
237
+ updatedAt: file.updatedAt,
238
+ expectedUpdatedAt:
239
+ source.currentContent !== undefined ? source.revision : undefined,
240
+ content:
241
+ source.currentContent !== undefined
242
+ ? source.currentContent
243
+ : (file.content ?? ""),
225
244
  codeLayerSource: {
226
245
  kind: "design-file",
227
246
  designId: file.designId,
@@ -236,35 +255,65 @@ async function persistDesignFileEdit(file: {
236
255
  id: string;
237
256
  designId: string;
238
257
  content: string;
239
- }): Promise<void> {
258
+ expectedUpdatedAt?: string;
259
+ }): Promise<string> {
240
260
  await assertAccess("design", file.designId, "editor");
241
261
 
242
262
  const db = getDb();
243
263
  const now = new Date().toISOString();
244
264
 
245
- agentEnterDocument(file.id);
246
- try {
247
- await db
248
- .update(schema.designFiles)
249
- .set({ content: file.content, updatedAt: now })
250
- .where(eq(schema.designFiles.id, file.id));
251
-
252
- if (await hasCollabState(file.id)) {
253
- await applyText(file.id, file.content, "content", "agent");
254
- } else {
255
- await seedFromText(file.id, file.content);
265
+ if (file.expectedUpdatedAt) {
266
+ const [latest] = await db
267
+ .select({ updatedAt: schema.designFiles.updatedAt })
268
+ .from(schema.designFiles)
269
+ .where(eq(schema.designFiles.id, file.id))
270
+ .limit(1);
271
+ if (!latest) {
272
+ throw new Error("Design HTML file not found.");
273
+ }
274
+ if (latest.updatedAt && latest.updatedAt !== file.expectedUpdatedAt) {
275
+ throw new ShaderFillRevisionConflictError();
256
276
  }
277
+ }
278
+
279
+ await db
280
+ .update(schema.designFiles)
281
+ .set({ content: file.content, updatedAt: now })
282
+ .where(
283
+ file.expectedUpdatedAt
284
+ ? and(
285
+ eq(schema.designFiles.id, file.id),
286
+ eq(schema.designFiles.updatedAt, file.expectedUpdatedAt),
287
+ )
288
+ : eq(schema.designFiles.id, file.id),
289
+ );
257
290
 
258
- // guard:allow-unscoped — editor access on this design is asserted above
259
- // (and again at the top of this helper); this only bumps the addressed
260
- // design row's updatedAt.
261
- await db
262
- .update(schema.designs)
263
- .set({ updatedAt: now })
264
- .where(eq(schema.designs.id, file.designId));
265
- } finally {
266
- agentLeaveDocument(file.id);
291
+ if (file.expectedUpdatedAt) {
292
+ const [saved] = await db
293
+ .select({
294
+ content: schema.designFiles.content,
295
+ updatedAt: schema.designFiles.updatedAt,
296
+ })
297
+ .from(schema.designFiles)
298
+ .where(eq(schema.designFiles.id, file.id))
299
+ .limit(1);
300
+ if (!saved || saved.updatedAt !== now || saved.content !== file.content) {
301
+ throw new ShaderFillRevisionConflictError();
302
+ }
267
303
  }
304
+
305
+ // Keep SQL as the source of truth for this guarded server write. The editor
306
+ // already has the live shader preview applied; feeding a full HTML document
307
+ // back through an existing collab text snapshot can merge against stale
308
+ // iframe state and corrupt the saved source.
309
+ // guard:allow-unscoped — editor access on this design is asserted above
310
+ // before this helper is invoked; this only bumps the addressed design row.
311
+ await db
312
+ .update(schema.designs)
313
+ .set({ updatedAt: now })
314
+ .where(eq(schema.designs.id, file.designId));
315
+
316
+ return now;
268
317
  }
269
318
 
270
319
  // ─── Action ──────────────────────────────────────────────────────────────────
@@ -350,7 +399,25 @@ snippet (WebGL canvas / JSX component), call apply-shader.
350
399
  };
351
400
  }
352
401
 
353
- const file = await resolveEditableDesignFile(source);
402
+ let file: ResolvedDesignFile;
403
+ try {
404
+ file = await resolveEditableDesignFile(source);
405
+ } catch (error) {
406
+ if (error instanceof ShaderFillRevisionConflictError) {
407
+ return {
408
+ ok: false,
409
+ persisted: false,
410
+ conflict: true,
411
+ descriptor,
412
+ background,
413
+ fallbackCss,
414
+ colors,
415
+ error: error.message,
416
+ note: error.message,
417
+ };
418
+ }
419
+ throw error;
420
+ }
354
421
 
355
422
  const intent: StyleEditIntent = {
356
423
  kind: "style",
@@ -374,13 +441,36 @@ snippet (WebGL canvas / JSX component), call apply-shader.
374
441
 
375
442
  const changed =
376
443
  patch.result.status === "applied" && patch.result.changed === true;
444
+ let updatedAt: string | undefined;
377
445
 
378
446
  if (changed) {
379
- await persistDesignFileEdit({
380
- id: file.id,
381
- designId: file.designId,
382
- content: patch.content,
383
- });
447
+ try {
448
+ updatedAt = await persistDesignFileEdit({
449
+ id: file.id,
450
+ designId: file.designId,
451
+ content: patch.content,
452
+ expectedUpdatedAt: file.expectedUpdatedAt,
453
+ });
454
+ } catch (error) {
455
+ if (error instanceof ShaderFillRevisionConflictError) {
456
+ return {
457
+ ok: false,
458
+ persisted: false,
459
+ conflict: true,
460
+ descriptor,
461
+ background,
462
+ fallbackCss,
463
+ colors,
464
+ designId: file.designId,
465
+ fileId: file.id,
466
+ filename: file.filename,
467
+ result: patch.result,
468
+ error: error.message,
469
+ note: error.message,
470
+ };
471
+ }
472
+ throw error;
473
+ }
384
474
  }
385
475
 
386
476
  return {
@@ -393,7 +483,9 @@ snippet (WebGL canvas / JSX component), call apply-shader.
393
483
  designId: file.designId,
394
484
  fileId: file.id,
395
485
  filename: file.filename,
486
+ updatedAt,
396
487
  result: patch.result,
488
+ patchedContent: patch.content,
397
489
  bytesBefore: file.content.length,
398
490
  bytesAfter: patch.content.length,
399
491
  note: changed
@@ -12,6 +12,11 @@ import { getDb, schema } from "../server/db/index.js";
12
12
  import "../server/db/index.js"; // ensure registerShareableResource runs
13
13
  import { hasCapability } from "../shared/design-source-capabilities.js";
14
14
  import type { DesignSourceCapabilities } from "../shared/design-source-capabilities.js";
15
+ import {
16
+ normalizeDesignSourceType,
17
+ resolveDescriptorCapabilities,
18
+ type DesignSourceDescriptor,
19
+ } from "../shared/source-mode.js";
15
20
 
16
21
  /**
17
22
  * Resolve the capabilities for the design's source type.
@@ -19,17 +24,35 @@ import type { DesignSourceCapabilities } from "../shared/design-source-capabilit
19
24
  */
20
25
  function resolveCapabilities(
21
26
  designData: string | null,
22
- ): DesignSourceCapabilities | null {
23
- if (!designData) return null;
27
+ ): DesignSourceCapabilities {
28
+ if (!designData) {
29
+ return resolveDescriptorCapabilities({ sourceType: "inline" });
30
+ }
31
+
24
32
  try {
25
33
  const parsed = JSON.parse(designData) as Record<string, unknown>;
26
34
  if (parsed.capabilities && typeof parsed.capabilities === "object") {
27
35
  return parsed.capabilities as DesignSourceCapabilities;
28
36
  }
37
+ const sourceType = normalizeDesignSourceType(parsed.sourceType) ?? "inline";
38
+ const descriptor: DesignSourceDescriptor =
39
+ sourceType === "fusion"
40
+ ? { sourceType, connected: parsed.connected === true }
41
+ : sourceType === "localhost"
42
+ ? {
43
+ sourceType,
44
+ connectionId:
45
+ typeof parsed.connectionId === "string"
46
+ ? parsed.connectionId
47
+ : "unknown",
48
+ }
49
+ : { sourceType };
50
+ return resolveDescriptorCapabilities(descriptor);
29
51
  } catch {
30
52
  // ignore parse errors
31
53
  }
32
- return null;
54
+
55
+ return resolveDescriptorCapabilities({ sourceType: "inline" });
33
56
  }
34
57
 
35
58
  /**
@@ -167,7 +190,7 @@ export default defineAction({
167
190
  }
168
191
 
169
192
  const caps = resolveCapabilities(design.data);
170
- if (caps !== null && !hasCapability(caps, "captureState")) {
193
+ if (!hasCapability(caps, "captureState")) {
171
194
  throw new Error(
172
195
  "The design's source does not support captureState. " +
173
196
  "Connect Builder or a localhost bridge to enable live captures.",