@agent-native/core 0.80.10 → 0.81.0

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 (267) 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/agent/production-agent.ts +108 -15
  5. package/corpus/core/src/cli/design-connect.ts +419 -19
  6. package/corpus/core/src/client/AssistantChat.tsx +123 -26
  7. package/corpus/core/src/client/ConnectBuilderCard.tsx +1 -0
  8. package/corpus/core/src/client/chat/index.ts +1 -0
  9. package/corpus/core/src/client/chat/run-recovery.tsx +9 -7
  10. package/corpus/core/src/client/chat-view-transition.ts +33 -2
  11. package/corpus/core/src/client/composer/PromptComposer.tsx +21 -2
  12. package/corpus/core/src/client/index.ts +1 -0
  13. package/corpus/core/src/client/org/OrgSwitcher.tsx +26 -4
  14. package/corpus/core/src/client/sharing/ShareButton.tsx +3 -3
  15. package/corpus/core/src/client/sse-event-processor.ts +9 -0
  16. package/corpus/core/src/client/use-agent-chat-home-handoff.ts +31 -3
  17. package/corpus/core/src/db/client.ts +29 -2
  18. package/corpus/core/src/provider-api/index.ts +29 -0
  19. package/corpus/core/src/server/core-routes-plugin.ts +58 -20
  20. package/corpus/core/src/styles/agent-native.css +54 -0
  21. package/corpus/core/src/usage/store.ts +2 -2
  22. package/corpus/templates/analytics/app/components/layout/Layout.tsx +118 -10
  23. package/corpus/templates/analytics/app/components/layout/layout-route-policy.ts +7 -0
  24. package/corpus/templates/analytics/app/global.css +8 -0
  25. package/corpus/templates/analytics/app/hooks/use-navigation-state.ts +7 -1
  26. package/corpus/templates/analytics/app/lib/chat-handoff.ts +34 -0
  27. package/corpus/templates/analytics/app/pages/Ask.tsx +9 -18
  28. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +2 -2
  29. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +109 -21
  30. package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +4 -6
  31. package/corpus/templates/analytics/changelog/2026-06-30-ask-opens-a-fresh-chat-unless-you-were-just-chatting-and-das.md +6 -0
  32. package/corpus/templates/analytics/changelog/2026-06-30-dashboard-email-reports-are-more-reliable-and-use-cleaner.md +6 -0
  33. package/corpus/templates/analytics/changelog/2026-06-30-dashboard-toolbar-controls-use-a-quieter-share-button-and-ve.md +6 -0
  34. package/corpus/templates/analytics/changelog/2026-06-30-session-replay-rows-show-whole-minute-durations-in-the-playl.md +6 -0
  35. package/corpus/templates/analytics/changelog/2026-06-30-session-replays-now-render-captured-styling-and-keep-the-eve.md +6 -0
  36. package/corpus/templates/analytics/server/jobs/dashboard-report.ts +16 -3
  37. package/corpus/templates/analytics/server/lib/dashboard-report.ts +31 -14
  38. package/corpus/templates/assets/.agents/skills/image-generation/SKILL.md +9 -2
  39. package/corpus/templates/assets/AGENTS.md +4 -0
  40. package/corpus/templates/assets/actions/_tool-activity.ts +46 -0
  41. package/corpus/templates/assets/actions/generate-image-batch.ts +26 -5
  42. package/corpus/templates/assets/actions/generate-image.ts +118 -67
  43. package/corpus/templates/assets/actions/list-draft-assets.ts +50 -0
  44. package/corpus/templates/assets/actions/open-asset-picker.ts +9 -0
  45. package/corpus/templates/assets/actions/rerun-generation-run.ts +52 -31
  46. package/corpus/templates/assets/app/components/create/RecentDraftsSection.tsx +100 -0
  47. package/corpus/templates/assets/app/global.css +15 -0
  48. package/corpus/templates/assets/app/hooks/use-navigation-state.ts +4 -0
  49. package/corpus/templates/assets/app/i18n/zh-TW.ts +7 -0
  50. package/corpus/templates/assets/app/i18n-data.ts +61 -0
  51. package/corpus/templates/assets/app/routes/_index.tsx +4 -0
  52. package/corpus/templates/assets/app/routes/library.tsx +233 -52
  53. package/corpus/templates/assets/changelog/2026-06-29-image-generation-can-now-render-requested-text-and-respect-b.md +6 -0
  54. package/corpus/templates/assets/changelog/2026-06-30-image-generation-no-longer-looks-stuck-while-the-provider-is-still-working.md +6 -0
  55. package/corpus/templates/assets/changelog/2026-06-30-tagged-presets-now-control-the-image-aspect-ratio.md +6 -0
  56. package/corpus/templates/assets/changelog/2026-06-30-the-assets-picker-now-supports-a-vertical-embedded-layout-fo.md +6 -0
  57. package/corpus/templates/assets/server/lib/generation.ts +135 -11
  58. package/corpus/templates/assets/shared/api.ts +4 -0
  59. package/corpus/templates/clips/actions/finalize-recording.ts +1 -12
  60. package/corpus/templates/clips/actions/list-ai-requests.ts +60 -0
  61. package/corpus/templates/clips/app/hooks/use-auto-title.ts +24 -25
  62. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +10 -5
  63. package/corpus/templates/clips/app/routes/share.$shareId.tsx +6 -8
  64. package/corpus/templates/clips/changelog/2026-06-30-extension-save-recovery-avoids-false-upload-failures.md +6 -0
  65. package/corpus/templates/clips/changelog/2026-06-30-longer-chrome-extension-recordings-now-keep-saving-reliably-.md +6 -0
  66. package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -1
  67. package/corpus/templates/clips/chrome-extension/src/background.ts +19 -10
  68. package/corpus/templates/clips/chrome-extension/src/content-script.ts +38 -12
  69. package/corpus/templates/clips/chrome-extension/src/finalize-recovery.ts +224 -0
  70. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +119 -16
  71. package/corpus/templates/clips/chrome-extension/src/overlay.ts +16 -0
  72. package/corpus/templates/clips/server/lib/recording-upload-state.ts +48 -0
  73. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/abort.post.ts +9 -1
  74. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +63 -13
  75. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/status.get.ts +74 -0
  76. package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +32 -0
  77. package/corpus/templates/design/AGENTS.md +27 -0
  78. package/corpus/templates/design/CODE_NATIVE_DESIGN_STUDIO_IMPLEMENTATION_PLAN.md +1 -1
  79. package/corpus/templates/design/DESIGN-STUDIO-PLAN.md +5 -5
  80. package/corpus/templates/design/actions/apply-component-prop-edit.ts +76 -25
  81. package/corpus/templates/design/actions/apply-design-state.ts +17 -4
  82. package/corpus/templates/design/actions/apply-motion-edit.ts +76 -115
  83. package/corpus/templates/design/actions/apply-shader-fill.ts +148 -56
  84. package/corpus/templates/design/actions/capture-design-state.ts +27 -4
  85. package/corpus/templates/design/actions/connect-localhost.ts +59 -22
  86. package/corpus/templates/design/actions/delete-design.ts +20 -0
  87. package/corpus/templates/design/actions/export-zip.ts +23 -8
  88. package/corpus/templates/design/actions/generate-design.ts +53 -1
  89. package/corpus/templates/design/actions/get-component-details.ts +21 -26
  90. package/corpus/templates/design/actions/get-motion-timeline.ts +135 -10
  91. package/corpus/templates/design/actions/grant-localhost-write-consent.ts +116 -0
  92. package/corpus/templates/design/actions/import-design-tokens.ts +521 -0
  93. package/corpus/templates/design/actions/index-design-tokens.ts +18 -5
  94. package/corpus/templates/design/actions/insert-design-native-asset.ts +268 -0
  95. package/corpus/templates/design/actions/insert-figma-library-asset.ts +233 -0
  96. package/corpus/templates/design/actions/list-design-extensions.ts +32 -10
  97. package/corpus/templates/design/actions/list-design-native-assets.ts +105 -0
  98. package/corpus/templates/design/actions/list-design-states.ts +19 -1
  99. package/corpus/templates/design/actions/list-figma-library-assets.ts +292 -0
  100. package/corpus/templates/design/actions/migrate-board-objects-to-file.ts +238 -0
  101. package/corpus/templates/design/actions/navigate.ts +18 -3
  102. package/corpus/templates/design/actions/open-component-source.ts +7 -2
  103. package/corpus/templates/design/actions/preview-component-prop-edit.ts +32 -26
  104. package/corpus/templates/design/actions/preview-shader-fill.ts +9 -11
  105. package/corpus/templates/design/actions/provider-api-catalog.ts +2 -2
  106. package/corpus/templates/design/actions/provider-api-docs.ts +1 -1
  107. package/corpus/templates/design/actions/provider-api-request.ts +3 -3
  108. package/corpus/templates/design/actions/revoke-localhost-write-consent.ts +48 -0
  109. package/corpus/templates/design/actions/run-design-audit.ts +74 -10
  110. package/corpus/templates/design/actions/run-design-extension-action.ts +1 -1
  111. package/corpus/templates/design/actions/view-screen.ts +1 -1
  112. package/corpus/templates/design/actions/write-local-file.ts +213 -0
  113. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +244 -3197
  114. package/corpus/templates/design/app/components/design/DesignEditorSkeleton.tsx +30 -20
  115. package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +795 -289
  116. package/corpus/templates/design/app/components/design/EditPanel.tsx +635 -234
  117. package/corpus/templates/design/app/components/design/LayersPanel.tsx +124 -32
  118. package/corpus/templates/design/app/components/design/LocalhostWriteConsentDialog.tsx +161 -0
  119. package/corpus/templates/design/app/components/design/MotionDock.tsx +244 -266
  120. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +491 -56
  121. package/corpus/templates/design/app/components/design/ReviewPanel.tsx +55 -67
  122. package/corpus/templates/design/app/components/design/StatesPanel.tsx +38 -63
  123. package/corpus/templates/design/app/components/design/TokensPanel.tsx +264 -8
  124. package/corpus/templates/design/app/components/design/bridge/codegen.ts +125 -0
  125. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +4037 -0
  126. package/corpus/templates/design/app/components/design/bridge/embedded-wheel.bridge.ts +62 -0
  127. package/corpus/templates/design/app/components/design/bridge/hit-test.bridge.ts +272 -0
  128. package/corpus/templates/design/app/components/design/bridge/nav.bridge.ts +116 -0
  129. package/corpus/templates/design/app/components/design/bridge/sample.bridge.ts +24 -0
  130. package/corpus/templates/design/app/components/design/bridge/shader-fill-preview.bridge.ts +78 -0
  131. package/corpus/templates/design/app/components/design/bridge/tsconfig.json +17 -0
  132. package/corpus/templates/design/app/components/design/bridge/tweak.bridge.ts +28 -0
  133. package/corpus/templates/design/app/components/design/bridge/zoom.bridge.ts +43 -0
  134. package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +45 -16
  135. package/corpus/templates/design/app/components/design/inspector/ImageFillControls.tsx +88 -9
  136. package/corpus/templates/design/app/components/editor/PromptDialog.tsx +94 -9
  137. package/corpus/templates/design/app/components/layout/Layout.tsx +19 -3
  138. package/corpus/templates/design/app/components/layout/Sidebar.tsx +1 -1
  139. package/corpus/templates/design/app/global.css +7 -0
  140. package/corpus/templates/design/app/hooks/use-navigation-state.ts +23 -0
  141. package/corpus/templates/design/app/i18n/zh-TW.ts +22 -0
  142. package/corpus/templates/design/app/i18n-data.ts +369 -0
  143. package/corpus/templates/design/app/lib/pending-generation.ts +1 -0
  144. package/corpus/templates/design/app/pages/DesignEditor.tsx +3024 -616
  145. package/corpus/templates/design/app/pages/Index.tsx +5 -1
  146. package/corpus/templates/design/changelog/2026-06-30-applying-the-one-click-fix-for-a-small-tap-target-accessibil.md +6 -0
  147. package/corpus/templates/design/changelog/2026-06-30-canvas-creation-screen-dragging-undo-redo-and-image-fill-edi.md +6 -0
  148. package/corpus/templates/design/changelog/2026-06-30-component-prop-edits-and-shader-fills-now-preserve-rapid-sam.md +6 -0
  149. package/corpus/templates/design/changelog/2026-06-30-design-can-browse-figma-library-components-and-insert-them-i.md +6 -0
  150. package/corpus/templates/design/changelog/2026-06-30-design-now-has-a-figma-style-left-rail-for-file-agent-assets.md +6 -0
  151. package/corpus/templates/design/changelog/2026-06-30-design-now-has-native-components-embedded-media-and-figma-im.md +6 -0
  152. package/corpus/templates/design/changelog/2026-06-30-design-preview-now-includes-a-publish-app-waitlist-option-fr.md +6 -0
  153. package/corpus/templates/design/changelog/2026-06-30-design-s-left-rail-now-matches-the-sidebar-chrome-and-uses-t.md +6 -0
  154. package/corpus/templates/design/changelog/2026-06-30-design-s-tools-create-menu-now-stays-compact-instead-of-rend.md +6 -0
  155. package/corpus/templates/design/changelog/2026-06-30-design-starter-prompts-now-begin-generating-immediately-and-.md +6 -0
  156. package/corpus/templates/design/changelog/2026-06-30-design-token-edits-now-stay-visible-in-previews-and-token-li.md +6 -0
  157. package/corpus/templates/design/changelog/2026-06-30-design-tools-now-use-a-quieter-figma-style-list-layout.md +6 -0
  158. package/corpus/templates/design/changelog/2026-06-30-drawing-rectangles-on-the-all-screens-canvas-no-longer-flash.md +6 -0
  159. package/corpus/templates/design/changelog/2026-06-30-fixed-code-native-design-studio-edits-so-component-props-mot.md +6 -0
  160. package/corpus/templates/design/changelog/2026-06-30-fixed-visual-editing-hover-overlays-and-spacing-handles-so-s.md +6 -0
  161. package/corpus/templates/design/changelog/2026-06-30-motion-edits-now-save-automatically-without-a-separate-css-a.md +6 -0
  162. package/corpus/templates/design/changelog/2026-06-30-motion-timelines-now-reopen-as-editable-tracks-after-writing.md +6 -0
  163. package/corpus/templates/design/changelog/2026-06-30-new-design-prompts-with-uploaded-screenshots-work-reliably-o.md +6 -0
  164. package/corpus/templates/design/changelog/2026-06-30-screen-previews-in-all-screens-view-now-keep-rounded-outline.md +6 -0
  165. package/corpus/templates/design/changelog/2026-06-30-selection-outlines-now-sit-flush-on-rectangles-drawn-directl.md +6 -0
  166. package/corpus/templates/design/changelog/2026-06-30-share-options-use-a-tighter-design-editor-popover-with-compa.md +6 -0
  167. package/corpus/templates/design/changelog/2026-06-30-the-collapsed-motion-panel-now-stays-tucked-under-the-left-s.md +6 -0
  168. package/corpus/templates/design/changelog/2026-06-30-the-design-inspector-now-shows-states-and-review-in-a-tighte.md +6 -0
  169. package/corpus/templates/design/changelog/2026-06-30-the-sidebar-keeps-the-organization-picker-space-stable-while.md +6 -0
  170. package/corpus/templates/design/changelog/2026-06-30-tokens-can-now-be-imported-from-pasted-notes-code-files-fold.md +6 -0
  171. package/corpus/templates/design/e2e/global-setup.ts +94 -2
  172. package/corpus/templates/design/e2e/helpers.ts +168 -65
  173. package/corpus/templates/design/package.json +3 -1
  174. package/corpus/templates/design/server/db/schema.ts +23 -0
  175. package/corpus/templates/design/server/handlers/uploads.ts +22 -1
  176. package/corpus/templates/design/server/lib/provider-api.ts +7 -4
  177. package/corpus/templates/design/server/lib/verify-write-grant.ts +108 -0
  178. package/corpus/templates/design/server/plugins/db.ts +39 -0
  179. package/corpus/templates/design/server/register-secrets.ts +38 -0
  180. package/corpus/templates/design/shared/board-file.ts +228 -0
  181. package/corpus/templates/design/shared/board-objects.ts +288 -0
  182. package/corpus/templates/design/shared/code-layer.ts +52 -0
  183. package/corpus/templates/design/shared/component-model.ts +35 -0
  184. package/corpus/templates/design/shared/motion-compiler.ts +137 -15
  185. package/corpus/templates/design/shared/motion-timeline.ts +2 -2
  186. package/corpus/templates/design/shared/resolve-tweaks.ts +32 -9
  187. package/corpus/templates/design/tsconfig.json +2 -1
  188. package/corpus/templates/slides/AGENTS.md +9 -0
  189. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +4 -0
  190. package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +4 -24
  191. package/corpus/templates/slides/app/lib/export-pdf-client.ts +29 -20
  192. package/corpus/templates/slides/app/lib/export-pptx-client.ts +365 -0
  193. package/corpus/templates/slides/app/pages/DeckEditor.tsx +12 -9
  194. package/corpus/templates/slides/app/vite-env.d.ts +18 -0
  195. package/corpus/templates/slides/changelog/2026-06-30-powerpoint-downloads-now-preserve-the-rendered-slide-layout-.md +6 -0
  196. package/corpus/templates/slides/changelog/2026-06-30-unavailable-decks-open-in-a-cleaner-full-height-centered-sta.md +6 -0
  197. package/corpus/templates/slides/package.json +1 -0
  198. package/corpus/templates/slides/vite.config.ts +1 -0
  199. package/dist/agent/production-agent.d.ts.map +1 -1
  200. package/dist/agent/production-agent.js +94 -10
  201. package/dist/agent/production-agent.js.map +1 -1
  202. package/dist/cli/design-connect.d.ts +25 -0
  203. package/dist/cli/design-connect.d.ts.map +1 -1
  204. package/dist/cli/design-connect.js +357 -18
  205. package/dist/cli/design-connect.js.map +1 -1
  206. package/dist/client/AssistantChat.d.ts +14 -0
  207. package/dist/client/AssistantChat.d.ts.map +1 -1
  208. package/dist/client/AssistantChat.js +115 -22
  209. package/dist/client/AssistantChat.js.map +1 -1
  210. package/dist/client/ConnectBuilderCard.d.ts.map +1 -1
  211. package/dist/client/ConnectBuilderCard.js +1 -0
  212. package/dist/client/ConnectBuilderCard.js.map +1 -1
  213. package/dist/client/chat/index.d.ts +1 -1
  214. package/dist/client/chat/index.d.ts.map +1 -1
  215. package/dist/client/chat/index.js +1 -1
  216. package/dist/client/chat/index.js.map +1 -1
  217. package/dist/client/chat/run-recovery.d.ts.map +1 -1
  218. package/dist/client/chat/run-recovery.js +5 -5
  219. package/dist/client/chat/run-recovery.js.map +1 -1
  220. package/dist/client/chat-view-transition.d.ts +6 -0
  221. package/dist/client/chat-view-transition.d.ts.map +1 -1
  222. package/dist/client/chat-view-transition.js +23 -2
  223. package/dist/client/chat-view-transition.js.map +1 -1
  224. package/dist/client/composer/PromptComposer.d.ts.map +1 -1
  225. package/dist/client/composer/PromptComposer.js +3 -2
  226. package/dist/client/composer/PromptComposer.js.map +1 -1
  227. package/dist/client/index.d.ts +1 -1
  228. package/dist/client/index.d.ts.map +1 -1
  229. package/dist/client/index.js +1 -1
  230. package/dist/client/index.js.map +1 -1
  231. package/dist/client/org/OrgSwitcher.d.ts.map +1 -1
  232. package/dist/client/org/OrgSwitcher.js +11 -4
  233. package/dist/client/org/OrgSwitcher.js.map +1 -1
  234. package/dist/client/sharing/ShareButton.js +3 -3
  235. package/dist/client/sharing/ShareButton.js.map +1 -1
  236. package/dist/client/sse-event-processor.d.ts.map +1 -1
  237. package/dist/client/sse-event-processor.js +10 -0
  238. package/dist/client/sse-event-processor.js.map +1 -1
  239. package/dist/client/use-agent-chat-home-handoff.d.ts +8 -2
  240. package/dist/client/use-agent-chat-home-handoff.d.ts.map +1 -1
  241. package/dist/client/use-agent-chat-home-handoff.js +20 -6
  242. package/dist/client/use-agent-chat-home-handoff.js.map +1 -1
  243. package/dist/collab/awareness.d.ts +2 -2
  244. package/dist/collab/awareness.d.ts.map +1 -1
  245. package/dist/collab/routes.d.ts +2 -2
  246. package/dist/db/client.d.ts +1 -0
  247. package/dist/db/client.d.ts.map +1 -1
  248. package/dist/db/client.js +28 -2
  249. package/dist/db/client.js.map +1 -1
  250. package/dist/notifications/routes.d.ts +3 -3
  251. package/dist/observability/routes.d.ts +3 -3
  252. package/dist/progress/routes.d.ts +1 -1
  253. package/dist/provider-api/index.d.ts +5 -4
  254. package/dist/provider-api/index.d.ts.map +1 -1
  255. package/dist/provider-api/index.js +28 -0
  256. package/dist/provider-api/index.js.map +1 -1
  257. package/dist/resources/handlers.d.ts +3 -3
  258. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  259. package/dist/server/core-routes-plugin.d.ts +25 -0
  260. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  261. package/dist/server/core-routes-plugin.js +43 -18
  262. package/dist/server/core-routes-plugin.js.map +1 -1
  263. package/dist/server/transcribe-voice.d.ts +1 -1
  264. package/dist/styles/agent-native.css +54 -0
  265. package/dist/usage/store.js +2 -2
  266. package/dist/usage/store.js.map +1 -1
  267. package/package.json +1 -1
@@ -6,37 +6,32 @@
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
 
37
28
  import { getDb, schema } from "../server/db/index.js";
38
29
  import "../server/db/index.js"; // ensure registerShareableResource runs
39
- import { compile } from "../shared/motion-compiler.js";
30
+ import {
31
+ assertSafeMotionCssProperty,
32
+ assertSafeMotionCssToken,
33
+ compile,
34
+ } from "../shared/motion-compiler.js";
40
35
  import type { MotionTrack } from "../shared/motion-timeline.js";
41
36
 
42
37
  // ─── Zod schemas ─────────────────────────────────────────────────────────────
@@ -85,47 +80,6 @@ const MOTION_STYLE_CLOSE = "</style>";
85
80
  */
86
81
  const STYLE_CLOSE_RE = /<\s*\/\s*style\b[^>]*>/i;
87
82
 
88
- /**
89
- * Reject `<`, `>`, and any `</style` sequence in a CSS token before it is
90
- * compiled into the managed `<style>` block. Keyframe `value` / `ease` come
91
- * straight from the caller and are interpolated raw into the CSS, so an
92
- * unescaped `</style>` (or angle bracket) would break out of the style block
93
- * and inject arbitrary markup. Returns the original string when safe; throws on
94
- * a breakout attempt so the whole atomic write fails loudly rather than
95
- * persisting a poisoned block.
96
- */
97
- function assertSafeCssToken(value: string, field: string): string {
98
- if (/[<>]/.test(value) || /<\s*\/\s*style/i.test(value)) {
99
- throw new Error(
100
- `Invalid ${field}: "<", ">", and "</style" are not allowed in motion CSS values.`,
101
- );
102
- }
103
- return value;
104
- }
105
-
106
- /**
107
- * Validate that a CSS property name is a safe CSS identifier.
108
- *
109
- * Accepts standard and vendor-prefixed property names (e.g. "opacity",
110
- * "transform", "-webkit-transform") and nothing else. Rejects any string
111
- * containing `:`, `;`, `{`, `}`, `<`, `/`, whitespace, or other characters
112
- * that could break out of a CSS declaration or `<style>` block context when
113
- * the property is interpolated as `${property}: ${value};` inside
114
- * `@keyframes`.
115
- *
116
- * Uses the strict CSS ident regex `^-?[a-zA-Z][a-zA-Z0-9-]*$` which covers
117
- * every real animatable CSS property while blocking injection payloads.
118
- */
119
- function assertSafeCssProperty(property: string, field: string): string {
120
- if (!/^-?[a-zA-Z][a-zA-Z0-9-]*$/.test(property)) {
121
- throw new Error(
122
- `Invalid ${field}: "${property}" is not a valid CSS property identifier. ` +
123
- "Only ASCII letters, digits, hyphens, and an optional leading hyphen are allowed.",
124
- );
125
- }
126
- return property;
127
- }
128
-
129
83
  /**
130
84
  * Inject or replace the managed `<style data-agent-native-motion>` block in
131
85
  * the HTML content. Inserts before `</head>` when not already present.
@@ -158,21 +112,6 @@ function injectMotionStyle(html: string, css: string): string {
158
112
  return block + "\n" + html;
159
113
  }
160
114
 
161
- async function liveFileContent(
162
- fileId: string,
163
- storedContent: string,
164
- ): Promise<string> {
165
- try {
166
- if (await hasCollabState(fileId)) {
167
- const live = await getText(fileId, "content");
168
- if (typeof live === "string") return live;
169
- }
170
- } catch {
171
- // Best-effort; SQL is the fallback.
172
- }
173
- return storedContent;
174
- }
175
-
176
115
  async function persistFileContent(
177
116
  fileId: string,
178
117
  designId: string,
@@ -180,29 +119,21 @@ async function persistFileContent(
180
119
  now: string,
181
120
  ): Promise<void> {
182
121
  const db = getDb();
183
- agentEnterDocument(fileId);
184
- try {
185
- await db
186
- .update(schema.designFiles)
187
- .set({ content, updatedAt: now })
188
- .where(eq(schema.designFiles.id, fileId));
189
-
190
- if (await hasCollabState(fileId)) {
191
- await applyText(fileId, content, "content", "agent");
192
- } else {
193
- await seedFromText(fileId, content);
194
- }
195
-
196
- // guard:allow-unscoped the action's run() asserts editor access via
197
- // assertAccess("design", designId, "editor") before this helper is
198
- // invoked; this only touches the addressed design row's updatedAt.
199
- await db
200
- .update(schema.designs)
201
- .set({ updatedAt: now })
202
- .where(eq(schema.designs.id, designId));
203
- } finally {
204
- agentLeaveDocument(fileId);
205
- }
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));
206
137
  }
207
138
 
208
139
  // ─── Action ───────────────────────────────────────────────────────────────────
@@ -213,7 +144,7 @@ export default defineAction({
213
144
  "Persists the motion_timeline row, compiles tracks to CSS, injects the " +
214
145
  "managed <style data-agent-native-motion> block into the design's HTML, " +
215
146
  "and updates compiledHash — all in one atomic step. " +
216
- "This is the 'Write to CSS' commit path; preview/scrubbing uses the " +
147
+ "This is the durable timeline persist path; preview/scrubbing uses the " +
217
148
  "motion-preview postMessage bridge, NOT this action.",
218
149
  schema: z.object({
219
150
  designId: z.string().describe("Design project ID."),
@@ -268,6 +199,14 @@ export default defineAction({
268
199
  .optional()
269
200
  .default(false)
270
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
+ ),
271
210
  }),
272
211
  run: async ({
273
212
  designId,
@@ -278,6 +217,7 @@ export default defineAction({
278
217
  durationMs,
279
218
  defaultEase,
280
219
  includeContent,
220
+ currentContent: currentContentInput,
281
221
  }) => {
282
222
  await assertAccess("design", designId, "editor");
283
223
 
@@ -319,7 +259,11 @@ export default defineAction({
319
259
  }
320
260
 
321
261
  const fileId = file.id;
322
- const currentContent = await liveFileContent(fileId, file.content ?? "");
262
+ const resolvedSourceRef = sourceRef ?? fileId;
263
+ const currentContent =
264
+ currentContentInput !== undefined
265
+ ? currentContentInput
266
+ : (file.content ?? "");
323
267
 
324
268
  // ── 2. Compile tracks → CSS ─────────────────────────────────────────────
325
269
  const typedTracks = tracks as MotionTrack[];
@@ -328,20 +272,20 @@ export default defineAction({
328
272
  // keyframe values, and easing strings before they are compiled into the
329
273
  // managed <style> block.
330
274
  for (const track of typedTracks) {
331
- assertSafeCssProperty(track.property, "track.property");
275
+ assertSafeMotionCssProperty(track.property, "track.property");
332
276
  for (const kf of track.keyframes) {
333
- assertSafeCssToken(kf.value, "keyframe value");
277
+ assertSafeMotionCssToken(kf.value, "keyframe value");
334
278
  if (kf.ease !== undefined) {
335
- assertSafeCssToken(kf.ease, "keyframe ease");
279
+ assertSafeMotionCssToken(kf.ease, "keyframe ease");
336
280
  }
337
281
  }
338
282
  }
339
- assertSafeCssToken(defaultEase, "defaultEase");
283
+ assertSafeMotionCssToken(defaultEase, "defaultEase");
340
284
 
341
285
  const { css, hash } = compile({
342
286
  id: timelineId ?? "",
343
287
  designId,
344
- sourceRef: sourceRef ?? null,
288
+ sourceRef: resolvedSourceRef,
345
289
  filePath: null,
346
290
  tracks: typedTracks,
347
291
  durationMs,
@@ -360,7 +304,7 @@ export default defineAction({
360
304
  // Resolve everything that can fail (existence + ownership) BEFORE touching
361
305
  // content, so we never persist HTML for a row that can't be written.
362
306
  const tracksJson = JSON.stringify(typedTracks);
363
- const resolvedTimelineId = timelineId ?? nanoid();
307
+ let existingTimelineId = timelineId;
364
308
 
365
309
  let insertOwnerEmail: string | null = null;
366
310
  let insertOrgId: string | null = null;
@@ -384,21 +328,37 @@ export default defineAction({
384
328
  );
385
329
  }
386
330
  } else {
387
- // Insert new row — derive ownership from the request context (same
388
- // pattern as create-design-state and other create actions).
389
- insertOwnerEmail = getRequestUserEmail() ?? null;
390
- if (!insertOwnerEmail) throw new Error("no authenticated user");
391
- 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
+ }
392
352
  }
393
353
 
354
+ const resolvedTimelineId = existingTimelineId ?? nanoid();
355
+
394
356
  // ── 5. Persist the motion_timeline row FIRST (atomic SQL portion) ───────
395
357
  // The timeline row is written before the HTML so that a failure in the
396
- // HTML/collab write step cannot leave the design content mutated without a
397
- // corresponding row. The reverse (HTML first) was a false atomicity
398
- // guarantee: if the row write failed after the HTML write, the managed
399
- // <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.
400
360
  await db.transaction(async (tx) => {
401
- if (timelineId) {
361
+ if (existingTimelineId) {
402
362
  await tx
403
363
  .update(schema.motionTimeline)
404
364
  .set({
@@ -406,15 +366,15 @@ export default defineAction({
406
366
  durationMs,
407
367
  defaultEase,
408
368
  compiledHash: hash,
409
- sourceRef: sourceRef ?? null,
369
+ sourceRef: resolvedSourceRef,
410
370
  updatedAt: now,
411
371
  })
412
- .where(eq(schema.motionTimeline.id, timelineId));
372
+ .where(eq(schema.motionTimeline.id, existingTimelineId));
413
373
  } else {
414
374
  await tx.insert(schema.motionTimeline).values({
415
375
  id: resolvedTimelineId,
416
376
  designId,
417
- sourceRef: sourceRef ?? null,
377
+ sourceRef: resolvedSourceRef,
418
378
  filePath: null,
419
379
  tracks: tracksJson,
420
380
  durationMs,
@@ -428,8 +388,8 @@ export default defineAction({
428
388
  }
429
389
  });
430
390
 
431
- // ── 6. Persist the patched HTML content SECOND (Yjs/collab + SQL) ───────
432
- // 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
433
393
  // accurate (correct tracks + hash) and the stale HTML can be recompiled on
434
394
  // the next apply-motion-edit call via compiledHash drift detection.
435
395
  await persistFileContent(fileId, designId, patchedContent, now);
@@ -438,6 +398,7 @@ export default defineAction({
438
398
  timelineId: resolvedTimelineId,
439
399
  designId,
440
400
  fileId,
401
+ sourceRef: resolvedSourceRef,
441
402
  trackCount: typedTracks.length,
442
403
  compiledHash: hash,
443
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.",