@agent-native/core 0.90.1 → 0.90.2

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 (295) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +14 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/client/AgentPanel.tsx +1 -1
  5. package/corpus/core/src/client/rich-markdown-editor/useCollabReconcile.ts +11 -6
  6. package/corpus/core/src/client/use-action.ts +39 -2
  7. package/corpus/core/src/client/use-db-sync.ts +44 -18
  8. package/corpus/core/src/db/create-get-db.ts +32 -2
  9. package/corpus/core/src/server/framework-request-handler.ts +52 -4
  10. package/corpus/templates/assets/.agents/skills/asset-generation/SKILL.md +5 -0
  11. package/corpus/templates/assets/.agents/skills/logo-composite/SKILL.md +34 -6
  12. package/corpus/templates/assets/AGENTS.md +23 -4
  13. package/corpus/templates/assets/actions/_generation-preset-settings.ts +48 -0
  14. package/corpus/templates/assets/actions/_helpers.ts +8 -1
  15. package/corpus/templates/assets/actions/_preset-skeleton-validation.ts +64 -0
  16. package/corpus/templates/assets/actions/create-generation-preset.ts +11 -3
  17. package/corpus/templates/assets/actions/generate-image-batch.ts +7 -2
  18. package/corpus/templates/assets/actions/generate-image.ts +342 -25
  19. package/corpus/templates/assets/actions/get-library.ts +4 -3
  20. package/corpus/templates/assets/actions/navigate.ts +2 -1
  21. package/corpus/templates/assets/actions/refresh-generation-run.ts +5 -1
  22. package/corpus/templates/assets/actions/update-asset.ts +1 -0
  23. package/corpus/templates/assets/actions/update-generation-preset.ts +8 -1
  24. package/corpus/templates/assets/actions/view-screen.ts +8 -1
  25. package/corpus/templates/assets/app/components/generation/GenerationResults.tsx +9 -1
  26. package/corpus/templates/assets/app/hooks/use-image-model-menu.ts +1 -0
  27. package/corpus/templates/assets/app/hooks/use-navigation-state.ts +11 -0
  28. package/corpus/templates/assets/app/i18n/zh-TW.ts +64 -0
  29. package/corpus/templates/assets/app/i18n-data.ts +650 -0
  30. package/corpus/templates/assets/app/routes/asset.$id.tsx +2 -1
  31. package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +44 -33
  32. package/corpus/templates/assets/app/routes/brand-kits.$id_.presets.$presetId.tsx +1474 -0
  33. package/corpus/templates/assets/app/routes/library.tsx +43 -1
  34. package/corpus/templates/assets/server/lib/generation.ts +366 -44
  35. package/corpus/templates/assets/server/lib/image-processing.ts +252 -0
  36. package/corpus/templates/assets/server/lib/preset-skeleton.ts +137 -0
  37. package/corpus/templates/assets/shared/api.ts +47 -0
  38. package/corpus/templates/clips/app/components/library/library-layout.tsx +7 -2
  39. package/corpus/templates/clips/app/components/library/sort-menu.tsx +1 -1
  40. package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +114 -33
  41. package/corpus/templates/clips/app/routes/_app.library._index.tsx +27 -1
  42. package/corpus/templates/clips/app/routes/record.tsx +157 -68
  43. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/abort.post.ts +5 -5
  44. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +20 -3
  45. package/corpus/templates/clips/shared/finalize-recovery.ts +23 -3
  46. package/corpus/templates/content/AGENTS.md +52 -52
  47. package/corpus/templates/content/actions/_builder-cms-read-client.ts +188 -8
  48. package/corpus/templates/content/actions/_builder-docs-client.ts +41 -3
  49. package/corpus/templates/content/actions/_database-source-utils.ts +998 -160
  50. package/corpus/templates/content/actions/_database-utils.ts +44 -4
  51. package/corpus/templates/content/actions/_notion-action-utils.ts +6 -2
  52. package/corpus/templates/content/actions/add-content-database-source-field-property.ts +23 -1
  53. package/corpus/templates/content/actions/attach-content-database-source.ts +85 -12
  54. package/corpus/templates/content/actions/change-content-database-source-role.ts +9 -1
  55. package/corpus/templates/content/actions/delete-content-database.ts +1 -0
  56. package/corpus/templates/content/actions/get-document.ts +11 -2
  57. package/corpus/templates/content/actions/list-content-databases.ts +57 -9
  58. package/corpus/templates/content/actions/prepare-builder-source-review.ts +23 -6
  59. package/corpus/templates/content/actions/refresh-content-database-source.ts +26 -3
  60. package/corpus/templates/content/actions/restore-content-database.ts +1 -0
  61. package/corpus/templates/content/actions/update-document.ts +43 -0
  62. package/corpus/templates/content/app/components/editor/BuilderBodySyncingNotice.tsx +21 -0
  63. package/corpus/templates/content/app/components/editor/DocumentDatabase.tsx +0 -1
  64. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +70 -28
  65. package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +69 -17
  66. package/corpus/templates/content/app/components/editor/VisualEditor.tsx +4 -4
  67. package/corpus/templates/content/app/components/editor/body-hydration.ts +112 -0
  68. package/corpus/templates/content/app/components/editor/builder-body-hydration-pump.ts +52 -0
  69. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +9076 -2876
  70. package/corpus/templates/content/app/components/editor/database-sources/BuilderSourceReviewDialog.tsx +41 -1
  71. package/corpus/templates/content/app/components/editor/extensions/ImageBlock.tsx +36 -11
  72. package/corpus/templates/content/app/components/editor/previewDocumentSaveController.ts +21 -1
  73. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +11 -1
  74. package/corpus/templates/content/app/components/sidebar/DocumentTreeItem.tsx +11 -8
  75. package/corpus/templates/content/app/global.css +41 -0
  76. package/corpus/templates/content/app/hooks/use-content-database.ts +346 -42
  77. package/corpus/templates/content/app/hooks/use-db-sync.ts +9 -0
  78. package/corpus/templates/content/app/hooks/use-documents.ts +47 -0
  79. package/corpus/templates/content/app/hooks/use-notion.ts +12 -10
  80. package/corpus/templates/content/app/i18n/zh-TW.ts +35 -0
  81. package/corpus/templates/content/app/i18n-data.ts +355 -0
  82. package/corpus/templates/content/app/root.tsx +22 -0
  83. package/corpus/templates/content/changelog/2026-07-01-builder-database-sources-keep-loading-rows-in-the-background.md +6 -0
  84. package/corpus/templates/content/changelog/2026-07-01-builder-source-attach-starts-with-usable-rows.md +6 -0
  85. package/corpus/templates/content/changelog/2026-07-01-builder-source-attachment-now-stays-connected-after-large.md +6 -0
  86. package/corpus/templates/content/changelog/2026-07-01-builder-source-refreshes-now-show-progress-for-large-docs-blog.md +6 -0
  87. package/corpus/templates/content/changelog/2026-07-01-database-pages-now-delete-to-trash-without-route-errors.md +6 -0
  88. package/corpus/templates/content/changelog/2026-07-02-builder-source-refreshes-recover-from-stalled-background-row.md +6 -0
  89. package/corpus/templates/content/changelog/2026-07-02-builder-sourced-rows-open-with-sync-protection.md +6 -0
  90. package/corpus/templates/content/changelog/2026-07-02-database-rows-and-pages-open-faster-from-recently-loaded-dat.md +6 -0
  91. package/corpus/templates/content/changelog/2026-07-02-preview-panels-close-reliably-narrow-sidebar-row-actions-sta.md +6 -0
  92. package/corpus/templates/content/changelog/2026-07-06-builder-sync-progress-no-longer-shows-continue.md +6 -0
  93. package/corpus/templates/content/shared/api.ts +13 -0
  94. package/corpus/templates/content/shared/builder-mdx.ts +16 -3
  95. package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +23 -13
  96. package/corpus/templates/design/.agents/skills/responsive-breakpoints/SKILL.md +98 -0
  97. package/corpus/templates/design/.agents/skills/shader-fills/SKILL.md +162 -0
  98. package/corpus/templates/design/AGENTS.md +9 -2
  99. package/corpus/templates/design/actions/add-breakpoint.ts +6 -3
  100. package/corpus/templates/design/actions/apply-a11y-fix.ts +57 -37
  101. package/corpus/templates/design/actions/apply-component-prop-edit.ts +41 -20
  102. package/corpus/templates/design/actions/apply-motion-edit.ts +251 -38
  103. package/corpus/templates/design/actions/apply-shader-fill.ts +90 -66
  104. package/corpus/templates/design/actions/apply-visual-edit.ts +282 -55
  105. package/corpus/templates/design/actions/create-component.ts +29 -60
  106. package/corpus/templates/design/actions/create-file.ts +9 -2
  107. package/corpus/templates/design/actions/duplicate-design.ts +31 -2
  108. package/corpus/templates/design/actions/edit-design.ts +36 -38
  109. package/corpus/templates/design/actions/generate-design-title.ts +87 -0
  110. package/corpus/templates/design/actions/generate-design.ts +53 -18
  111. package/corpus/templates/design/actions/generate-screens.ts +108 -4
  112. package/corpus/templates/design/actions/get-motion-timeline.ts +35 -7
  113. package/corpus/templates/design/actions/insert-asset.ts +34 -29
  114. package/corpus/templates/design/actions/insert-design-native-asset.ts +129 -32
  115. package/corpus/templates/design/actions/insert-figma-library-asset.ts +35 -30
  116. package/corpus/templates/design/actions/present-design-variants.ts +7 -2
  117. package/corpus/templates/design/actions/remove-motion-timeline.ts +31 -36
  118. package/corpus/templates/design/actions/update-file.ts +190 -75
  119. package/corpus/templates/design/app/components/design/BreakpointBar.tsx +403 -0
  120. package/corpus/templates/design/app/components/design/CanvasContextMenu.tsx +469 -222
  121. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +708 -53
  122. package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +83 -3
  123. package/corpus/templates/design/app/components/design/EditPanel.tsx +1091 -6948
  124. package/corpus/templates/design/app/components/design/LayersPanel.tsx +419 -85
  125. package/corpus/templates/design/app/components/design/MotionDock.tsx +1124 -103
  126. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +2610 -1398
  127. package/corpus/templates/design/app/components/design/ReadOnlyDesignBanner.tsx +22 -0
  128. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +2182 -235
  129. package/corpus/templates/design/app/components/design/bridge/motion-preview.bridge.ts +187 -11
  130. package/corpus/templates/design/app/components/design/bridge/shader-fill-preview.bridge.ts +376 -1
  131. package/corpus/templates/design/app/components/design/bridge/shader-runtime.bridge.ts +746 -0
  132. package/corpus/templates/design/app/components/design/canvas-primitive-style.ts +17 -0
  133. package/corpus/templates/design/app/components/design/code-workbench/SideBar.tsx +1 -1
  134. package/corpus/templates/design/app/components/design/code-workbench/explorer/FileTree.tsx +1 -3
  135. package/corpus/templates/design/app/components/design/code-workbench/quickinput/QuickInput.tsx +1 -1
  136. package/corpus/templates/design/app/components/design/edit-panel/appearance-properties.tsx +518 -0
  137. package/corpus/templates/design/app/components/design/edit-panel/code-inspect-helpers.tsx +562 -0
  138. package/corpus/templates/design/app/components/design/edit-panel/component-section.tsx +763 -0
  139. package/corpus/templates/design/app/components/design/edit-panel/document-colors.ts +94 -0
  140. package/corpus/templates/design/app/components/design/edit-panel/effects-properties.tsx +780 -0
  141. package/corpus/templates/design/app/components/design/edit-panel/element-classification.ts +446 -0
  142. package/corpus/templates/design/app/components/design/edit-panel/element-identity.ts +79 -0
  143. package/corpus/templates/design/app/components/design/edit-panel/field-primitives.tsx +315 -0
  144. package/corpus/templates/design/app/components/design/edit-panel/fill-gradient-helpers.ts +344 -0
  145. package/corpus/templates/design/app/components/design/edit-panel/fill-properties.tsx +594 -0
  146. package/corpus/templates/design/app/components/design/edit-panel/frame-presets-panel.tsx +119 -0
  147. package/corpus/templates/design/app/components/design/edit-panel/inspector-controls.tsx +248 -0
  148. package/corpus/templates/design/app/components/design/edit-panel/interaction-state-helpers.ts +45 -0
  149. package/corpus/templates/design/app/components/design/edit-panel/layout-properties.tsx +693 -0
  150. package/corpus/templates/design/app/components/design/edit-panel/panel-primitives.tsx +724 -0
  151. package/corpus/templates/design/app/components/design/edit-panel/position-helpers.ts +207 -0
  152. package/corpus/templates/design/app/components/design/edit-panel/position-layout-properties.tsx +628 -0
  153. package/corpus/templates/design/app/components/design/edit-panel/selection-helpers.ts +91 -0
  154. package/corpus/templates/design/app/components/design/edit-panel/stroke-properties.tsx +649 -0
  155. package/corpus/templates/design/app/components/design/edit-panel/style-change-types.ts +118 -0
  156. package/corpus/templates/design/app/components/design/edit-panel/style-options.ts +88 -0
  157. package/corpus/templates/design/app/components/design/edit-panel/transform-helpers.ts +100 -0
  158. package/corpus/templates/design/app/components/design/edit-panel/typography-helpers.ts +117 -0
  159. package/corpus/templates/design/app/components/design/edit-panel/typography-properties.tsx +501 -0
  160. package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +91 -40
  161. package/corpus/templates/design/app/components/design/inspector/BreakpointOverrideIndicator.tsx +82 -0
  162. package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +189 -87
  163. package/corpus/templates/design/app/components/design/inspector/GlslShaderPanel.tsx +1097 -0
  164. package/corpus/templates/design/app/components/design/inspector/GradientEditor.tsx +44 -0
  165. package/corpus/templates/design/app/components/design/inspector/InteractionStatePanel.tsx +258 -0
  166. package/corpus/templates/design/app/components/design/inspector/MotionKeyframeDiamond.tsx +144 -0
  167. package/corpus/templates/design/app/components/design/inspector/ScrubInput.tsx +99 -9
  168. package/corpus/templates/design/app/components/design/inspector/frame-size-presets.ts +116 -0
  169. package/corpus/templates/design/app/components/design/inspector/index.ts +26 -0
  170. package/corpus/templates/design/app/components/design/inspector/scrub-input-utils.ts +29 -0
  171. package/corpus/templates/design/app/components/design/multi-screen/alt-hover-measurement.ts +171 -0
  172. package/corpus/templates/design/app/components/design/multi-screen/board-surface-html.ts +220 -0
  173. package/corpus/templates/design/app/components/design/multi-screen/chrome-transitions.ts +35 -0
  174. package/corpus/templates/design/app/components/design/multi-screen/cross-screen-drop.ts +134 -0
  175. package/corpus/templates/design/app/components/design/multi-screen/culling.ts +185 -0
  176. package/corpus/templates/design/app/components/design/multi-screen/gradient-overlay-geometry.ts +120 -0
  177. package/corpus/templates/design/app/components/design/multi-screen/types.ts +892 -0
  178. package/corpus/templates/design/app/components/design/multi-screen/vector-edit-geometry.ts +21 -0
  179. package/corpus/templates/design/app/components/design/types.ts +15 -0
  180. package/corpus/templates/design/app/components/editor/PromptDialog.tsx +109 -5
  181. package/corpus/templates/design/app/global.css +8 -8
  182. package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +259 -2
  183. package/corpus/templates/design/app/i18n/zh-TW.ts +163 -9
  184. package/corpus/templates/design/app/i18n-breakpoints.ts +205 -0
  185. package/corpus/templates/design/app/i18n-data.ts +1664 -152
  186. package/corpus/templates/design/app/i18n-motion.ts +563 -0
  187. package/corpus/templates/design/app/lib/design-import.ts +73 -0
  188. package/corpus/templates/design/app/pages/Index.tsx +41 -22
  189. package/corpus/templates/design/changelog/2026-06-29-design-inspector-controls-now-match-figmas-softer-dark-chrome.md +1 -1
  190. package/corpus/templates/design/changelog/2026-06-30-design-now-has-a-figma-style-left-rail-for-file-agent-assets.md +1 -1
  191. package/corpus/templates/design/changelog/2026-06-30-design-tools-now-use-a-quieter-figma-style-list-layout.md +1 -1
  192. package/corpus/templates/design/changelog/2026-06-30-multi-select-outlines-now-stay-consistent-across-all-screens.md +1 -1
  193. package/corpus/templates/design/changelog/2026-07-03-canvas-interactions-now-match-figma-rotation-aware-resizing-.md +1 -1
  194. package/corpus/templates/design/changelog/2026-07-03-layers-panel-matches-figma-top-layer-on-top-drag-with-auto-s.md +1 -1
  195. package/corpus/templates/design/changelog/2026-07-05-designs-get-ai-generated-names-from-your-prompt.md +6 -0
  196. package/corpus/templates/design/changelog/2026-07-05-local-code-screens-keep-their-connection-when-duplicated-and.md +6 -0
  197. package/corpus/templates/design/changelog/2026-07-05-press-a-to-draw-screens-on-the-canvas-inside-a-screen-it-dr.md +6 -0
  198. package/corpus/templates/design/changelog/2026-07-06-3d-rotation-and-perspective-controls-on-every-element.md +6 -0
  199. package/corpus/templates/design/changelog/2026-07-06-ai-generated-and-duplicated-screens-now-get-their-editor-sel.md +6 -0
  200. package/corpus/templates/design/changelog/2026-07-06-applying-shaders-and-fills-in-quick-succession-can-no-longer.md +6 -0
  201. package/corpus/templates/design/changelog/2026-07-06-breakpoint-frames-in-the-overview-now-support-click-to-selec.md +6 -0
  202. package/corpus/templates/design/changelog/2026-07-06-breakpoint-segments-in-the-inspector-now-show-a-device-icon-.md +6 -0
  203. package/corpus/templates/design/changelog/2026-07-06-breakpoint-targeting-now-lives-in-one-compact-control-in-the.md +6 -0
  204. package/corpus/templates/design/changelog/2026-07-06-canvas-edits-can-no-longer-be-overwritten-by-stale-collabora.md +6 -0
  205. package/corpus/templates/design/changelog/2026-07-06-canvas-edits-inside-screens-save-reliably-again.md +6 -0
  206. package/corpus/templates/design/changelog/2026-07-06-cleaner-inspector-removed-the-ask-ai-box-from-the-design-tab.md +6 -0
  207. package/corpus/templates/design/changelog/2026-07-06-clicking-empty-canvas-space-now-deselects-an-element-inside-.md +6 -0
  208. package/corpus/templates/design/changelog/2026-07-06-dragging-an-element-onto-a-rectangle-now-nests-it-inside-wit.md +6 -0
  209. package/corpus/templates/design/changelog/2026-07-06-dragging-screens-and-objects-on-the-canvas-is-now-much-smoot.md +6 -0
  210. package/corpus/templates/design/changelog/2026-07-06-drawing-a-shape-or-text-on-the-canvas-no-longer-flashes-away.md +6 -0
  211. package/corpus/templates/design/changelog/2026-07-06-dropping-an-element-between-two-others-inside-a-screen-now-s.md +6 -0
  212. package/corpus/templates/design/changelog/2026-07-06-duplicating-a-screen-now-places-the-copy-exactly-where-you-d.md +6 -0
  213. package/corpus/templates/design/changelog/2026-07-06-edit-gradients-directly-on-elements-inside-screens-scale-str.md +6 -0
  214. package/corpus/templates/design/changelog/2026-07-06-edits-from-collaborators-and-the-agent-now-update-the-canvas.md +6 -0
  215. package/corpus/templates/design/changelog/2026-07-06-fixed-a-bug-where-most-style-position-and-text-edits-briefly.md +6 -0
  216. package/corpus/templates/design/changelog/2026-07-06-fixed-a-confusing-raw-error-message-when-a-file-upload-fails.md +6 -0
  217. package/corpus/templates/design/changelog/2026-07-06-fixed-a-rare-case-where-an-edit-could-be-silently-overwritte.md +6 -0
  218. package/corpus/templates/design/changelog/2026-07-06-fixed-inserted-assets-sometimes-disappearing-reappearing-or-.md +6 -0
  219. package/corpus/templates/design/changelog/2026-07-06-fixed-inspector-value-scrubbing-producing-erratic-numbers-an.md +6 -0
  220. package/corpus/templates/design/changelog/2026-07-06-fixed-padding-gap-and-min-max-size-scrubbing-not-updating-th.md +6 -0
  221. package/corpus/templates/design/changelog/2026-07-06-fixed-text-stroke-color-turning-black-and-glyphs-disappearin.md +6 -0
  222. package/corpus/templates/design/changelog/2026-07-06-fixed-the-collapsed-effects-and-stroke-inspector-sections-sh.md +6 -0
  223. package/corpus/templates/design/changelog/2026-07-06-fixed-the-new-design-dialog-closing-unexpectedly-when-using-.md +6 -0
  224. package/corpus/templates/design/changelog/2026-07-06-fixed-the-typography-section-missing-when-selecting-text-nes.md +6 -0
  225. package/corpus/templates/design/changelog/2026-07-06-full-view-no-longer-flashes-back-to-the-overview-on-entry-no.md +6 -0
  226. package/corpus/templates/design/changelog/2026-07-06-keyframe-animation-timeline-per-property-keyframes-easing-cu.md +6 -0
  227. package/corpus/templates/design/changelog/2026-07-06-moving-restyling-and-scrubbing-values-on-elements-in-ai-gene.md +6 -0
  228. package/corpus/templates/design/changelog/2026-07-06-padding-and-corner-radius-value-scrubbing-now-snaps-to-whole.md +6 -0
  229. package/corpus/templates/design/changelog/2026-07-06-padding-handles-now-only-resize-on-the-handle-line-itself-sh.md +6 -0
  230. package/corpus/templates/design/changelog/2026-07-06-preview-and-edit-hover-focus-and-pressed-states-with-real-cs.md +6 -0
  231. package/corpus/templates/design/changelog/2026-07-06-promoting-an-element-to-a-component-now-works-correctly-for-.md +6 -0
  232. package/corpus/templates/design/changelog/2026-07-06-reduced-url-history-churn-and-selection-overlay-flicker-whil.md +6 -0
  233. package/corpus/templates/design/changelog/2026-07-06-removed-the-decorative-dot-before-screen-names-in-the-overvi.md +6 -0
  234. package/corpus/templates/design/changelog/2026-07-06-responsive-breakpoints-edit-at-any-width-with-cascading-over.md +6 -0
  235. package/corpus/templates/design/changelog/2026-07-06-screen-labels-no-longer-turn-blue-when-you-hover-inside-the-.md +6 -0
  236. package/corpus/templates/design/changelog/2026-07-06-scrolling-a-screen-with-an-element-selected-stays-smooth-on-.md +6 -0
  237. package/corpus/templates/design/changelog/2026-07-06-selecting-multiple-elements-and-dragging-now-moves-the-whole.md +6 -0
  238. package/corpus/templates/design/changelog/2026-07-06-selection-borders-handles-and-value-readouts-now-stay-the-sa.md +6 -0
  239. package/corpus/templates/design/changelog/2026-07-06-selection-handles-and-editing-chrome-now-stay-a-constant-siz.md +6 -0
  240. package/corpus/templates/design/changelog/2026-07-06-shader-fills-and-effects-ai-authored-or-preset-glsl-shaders-.md +6 -0
  241. package/corpus/templates/design/changelog/2026-07-06-text-drawn-on-the-canvas-now-defaults-to-white-and-inter-in-.md +6 -0
  242. package/corpus/templates/design/changelog/2026-07-06-text-dropped-into-a-shape-now-stays-readable-instead-of-disa.md +6 -0
  243. package/corpus/templates/design/changelog/2026-07-06-the-add-tweak-controls-prompt-no-longer-jumps-to-the-corner-.md +6 -0
  244. package/corpus/templates/design/changelog/2026-07-06-the-padding-value-readout-now-stays-visible-while-you-hover-.md +6 -0
  245. package/corpus/templates/design/changelog/2026-07-06-the-selected-element-attachment-now-clears-from-the-chat-com.md +6 -0
  246. package/corpus/templates/design/changelog/2026-07-06-the-zoom-menu-is-more-compact-with-tight-rows-and-right-alig.md +6 -0
  247. package/corpus/templates/design/changelog/2026-07-06-undo-and-redo-no-longer-flash-the-screen-changes-now-apply-i.md +6 -0
  248. package/corpus/templates/design/changelog/2026-07-06-zoom-presets-like-zoom-to-50-now-stay-on-the-focused-screen-.md +6 -0
  249. package/corpus/templates/design/server/lib/import-design-files.ts +11 -2
  250. package/corpus/templates/design/server/source-workspace.ts +139 -59
  251. package/corpus/templates/design/shared/board-file.ts +11 -2
  252. package/corpus/templates/design/shared/breakpoint-media.ts +543 -0
  253. package/corpus/templates/design/shared/canvas-math.ts +354 -35
  254. package/corpus/templates/design/shared/code-layer.ts +277 -8
  255. package/corpus/templates/design/shared/design-state.ts +11 -4
  256. package/corpus/templates/design/shared/interaction-states.ts +548 -0
  257. package/corpus/templates/design/shared/motion-compiler.ts +186 -23
  258. package/corpus/templates/design/shared/motion-easing.ts +294 -0
  259. package/corpus/templates/design/shared/motion-timeline.ts +400 -34
  260. package/corpus/templates/design/shared/prompt-title.ts +95 -0
  261. package/corpus/templates/design/shared/responsive-classes.ts +448 -10
  262. package/corpus/templates/design/shared/screen-annotation.ts +46 -0
  263. package/corpus/templates/design/shared/shader-fills.ts +1009 -0
  264. package/corpus/templates/design/shared/shader-presets.ts +852 -4
  265. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +15 -2
  266. package/corpus/templates/slides/app/components/editor/ImportButton.tsx +10 -1
  267. package/corpus/templates/slides/app/lib/upload-response.ts +71 -0
  268. package/dist/client/AgentPanel.js +1 -1
  269. package/dist/client/AgentPanel.js.map +1 -1
  270. package/dist/client/rich-markdown-editor/useCollabReconcile.d.ts.map +1 -1
  271. package/dist/client/rich-markdown-editor/useCollabReconcile.js +11 -6
  272. package/dist/client/rich-markdown-editor/useCollabReconcile.js.map +1 -1
  273. package/dist/client/use-action.d.ts +3 -0
  274. package/dist/client/use-action.d.ts.map +1 -1
  275. package/dist/client/use-action.js +34 -3
  276. package/dist/client/use-action.js.map +1 -1
  277. package/dist/client/use-db-sync.d.ts +4 -0
  278. package/dist/client/use-db-sync.d.ts.map +1 -1
  279. package/dist/client/use-db-sync.js +37 -18
  280. package/dist/client/use-db-sync.js.map +1 -1
  281. package/dist/collab/routes.d.ts +1 -1
  282. package/dist/collab/struct-routes.d.ts +1 -1
  283. package/dist/db/create-get-db.d.ts +25 -0
  284. package/dist/db/create-get-db.d.ts.map +1 -1
  285. package/dist/db/create-get-db.js +25 -2
  286. package/dist/db/create-get-db.js.map +1 -1
  287. package/dist/observability/routes.d.ts +3 -3
  288. package/dist/secrets/routes.d.ts +9 -9
  289. package/dist/server/framework-request-handler.d.ts.map +1 -1
  290. package/dist/server/framework-request-handler.js +37 -1
  291. package/dist/server/framework-request-handler.js.map +1 -1
  292. package/package.json +1 -1
  293. package/corpus/templates/design/app/components/design/inspector/InspectorAiActions.tsx +0 -153
  294. package/corpus/templates/design/app/hooks/useAgentEditRequest.ts +0 -133
  295. package/corpus/templates/design/app/pages/DesignEditor.tsx +0 -22924
@@ -16,6 +16,10 @@
16
16
  */
17
17
 
18
18
  import { defineAction } from "@agent-native/core";
19
+ import {
20
+ agentEnterDocument,
21
+ agentLeaveDocument,
22
+ } from "@agent-native/core/collab";
19
23
  import {
20
24
  getRequestOrgId,
21
25
  getRequestUserEmail,
@@ -27,13 +31,41 @@ import { z } from "zod";
27
31
 
28
32
  import { getDb, schema } from "../server/db/index.js";
29
33
  import "../server/db/index.js"; // ensure registerShareableResource runs
34
+ import {
35
+ readLiveSourceFile,
36
+ writeInlineSourceFile,
37
+ type SourceWorkspaceFile,
38
+ } from "../server/source-workspace.js";
30
39
  import {
31
40
  assertSafeMotionCssProperty,
32
41
  assertSafeMotionCssToken,
33
42
  compile,
34
43
  injectManagedMotionCss,
35
44
  } from "../shared/motion-compiler.js";
36
- import type { MotionTrack } from "../shared/motion-timeline.js";
45
+ import { parseSpringToken } from "../shared/motion-easing.js";
46
+ import type {
47
+ MotionPlaybackMode,
48
+ MotionTrack,
49
+ } from "../shared/motion-timeline.js";
50
+ import {
51
+ readTimelinePlaybackMode,
52
+ withTimelinePlaybackMode,
53
+ } from "../shared/motion-timeline.js";
54
+ import { sourceContentHash } from "../shared/source-workspace.js";
55
+
56
+ // ─── DoS-guard caps ──────────────────────────────────────────────────────────
57
+ //
58
+ // Mirrors the style of other hard resource caps in this template (e.g.
59
+ // MAX_SHADERS_PER_ARTBOARD in shared/shader-safety.ts): a small, exported
60
+ // constant plus a clear thrown Error (never a silent clamp) so callers see
61
+ // exactly which limit was exceeded and by how much.
62
+
63
+ /** Max tracks per timeline/screen — keeps compile() bounded and DOM-safe. */
64
+ export const MAX_MOTION_TRACKS = 64;
65
+ /** Max keyframes per track — keeps @keyframes blocks bounded. */
66
+ export const MAX_MOTION_KEYFRAMES_PER_TRACK = 128;
67
+ /** Max total timeline duration, ms (120s) — guards against runaway timelines. */
68
+ export const MAX_MOTION_DURATION_MS = 120_000;
37
69
 
38
70
  // ─── Zod schemas ─────────────────────────────────────────────────────────────
39
71
 
@@ -42,13 +74,23 @@ const keyframeSchema = z.object({
42
74
  .number()
43
75
  .min(0)
44
76
  .max(1)
45
- .describe("Normalised time in [0, 1] where 0 = 0% and 1 = 100%."),
77
+ .describe(
78
+ "Normalised time in [0, 1] within the track's own span " +
79
+ "(0 = span start, 1 = span end; the span is the whole timeline " +
80
+ "unless the track sets delayMs/durationMs).",
81
+ ),
46
82
  value: z.string().describe("CSS property value at this keyframe."),
47
83
  ease: z
48
84
  .string()
49
85
  .optional()
50
86
  .describe(
51
- 'Per-keyframe easing, e.g. "ease-out" or "cubic-bezier(0.4,0,0.2,1)".',
87
+ "Easing of the SEGMENT leaving this keyframe toward the next one " +
88
+ "(Figma semantics: the easing INTO the following keyframe). " +
89
+ 'Accepts CSS keywords ("linear", "ease-out", "step-start" — the ' +
90
+ '"Hold" preset), "cubic-bezier(x1,y1,x2,y2)", "steps(n, pos)", ' +
91
+ 'CSS "linear(...)" stop lists, and spring physics as ' +
92
+ '"spring(bounce)" or "spring(bounce, settle)" with bounce in [0, 1] ' +
93
+ "(compiled to a CSS linear() approximation).",
52
94
  ),
53
95
  });
54
96
 
@@ -61,11 +103,45 @@ const trackSchema = z.object({
61
103
  ),
62
104
  property: z
63
105
  .string()
64
- .describe('CSS property to animate, e.g. "opacity" or "transform".'),
106
+ .describe(
107
+ "CSS property to animate. Figma-parity mapping: translation → " +
108
+ '"translate", scale → "scale", rotation → "rotate", opacity → ' +
109
+ '"opacity", corner radius → "border-radius", fill → ' +
110
+ '"background-color", stroke paint → "border-color", stroke weight ' +
111
+ '→ "border-width", drop shadow → "box-shadow". translate/scale/' +
112
+ "rotate are individual CSS transform properties, so they compose " +
113
+ "freely on one node as separate tracks.",
114
+ ),
65
115
  keyframes: z
66
116
  .array(keyframeSchema)
67
117
  .min(1)
68
- .describe("At least one keyframe is required per track."),
118
+ .describe(
119
+ "At least one keyframe is required per track " +
120
+ `(max ${MAX_MOTION_KEYFRAMES_PER_TRACK} per track).`,
121
+ ),
122
+ delayMs: z
123
+ .number()
124
+ .min(0)
125
+ .optional()
126
+ .describe(
127
+ "Start-time offset of this track within the timeline, in ms " +
128
+ "(compiled to animation-delay). Use for staggering layers.",
129
+ ),
130
+ durationMs: z
131
+ .number()
132
+ .positive()
133
+ .optional()
134
+ .describe(
135
+ "This track's own animation span in ms (compiled to a per-track " +
136
+ "animation-duration). Omit to span the whole timeline.",
137
+ ),
138
+ timelinePlaybackMode: z
139
+ .enum(["loop", "once", "ping-pong"])
140
+ .optional()
141
+ .describe(
142
+ "Internal: timeline-level playback mode stamp persisted on the first " +
143
+ "track. Prefer the top-level playbackMode parameter.",
144
+ ),
69
145
  });
70
146
 
71
147
  // ─── Helpers ──────────────────────────────────────────────────────────────────
@@ -109,30 +185,64 @@ export function motionTrackKey(targetNodeId: string, property: string): string {
109
185
  return `${targetNodeId}${MOTION_TRACK_KEY_SEPARATOR}${property}`;
110
186
  }
111
187
 
188
+ /**
189
+ * Validate a caller-supplied ease token: CSS-injection safety plus
190
+ * spring-token well-formedness. A string that LOOKS like a spring token but
191
+ * cannot be parsed would otherwise pass injection checks and then be emitted
192
+ * verbatim into the stylesheet as an invalid timing function.
193
+ */
194
+ export function assertValidMotionEase(ease: string, field: string): string {
195
+ assertSafeMotionCssToken(ease, field);
196
+ if (/^\s*spring/i.test(ease) && parseSpringToken(ease) === null) {
197
+ throw new Error(
198
+ `Invalid ${field}: "${ease}" is not a valid spring token. ` +
199
+ 'Use "spring(bounce)" or "spring(bounce, settle)" with bounce in [0, 1].',
200
+ );
201
+ }
202
+ return ease;
203
+ }
204
+
205
+ /**
206
+ * Persist the patched HTML through the same collab-aware seam as
207
+ * apply-visual-edit.ts / remove-motion-timeline.ts: `writeInlineSourceFile`
208
+ * re-reads the live (collab-authoritative, else SQL) text immediately before
209
+ * its own write and rejects if it no longer matches `expectedVersionHash` —
210
+ * closing the race window where a concurrent editor's change lands between
211
+ * the base read that produced `content` and this persist call. It also bumps
212
+ * `schema.designs.updatedAt` internally, so no separate designs-row bump is
213
+ * needed here (unlike the old raw-SQL `persistFileContent`).
214
+ */
112
215
  async function persistFileContent(
113
- fileId: string,
114
- designId: string,
216
+ file: {
217
+ id: string;
218
+ designId: string;
219
+ filename: string;
220
+ fileType: string;
221
+ },
115
222
  content: string,
116
- now: string,
223
+ expectedVersionHash: string,
117
224
  ): Promise<string> {
118
- const db = getDb();
119
- await db
120
- .update(schema.designFiles)
121
- .set({ content, updatedAt: now })
122
- .where(eq(schema.designFiles.id, fileId));
123
-
124
- // Keep SQL as the source of truth for this atomic write. The editor adopts
125
- // the returned HTML content without re-saving it; applying the whole document
126
- // through an existing collab text snapshot can merge against stale iframe
127
- // state and duplicate the managed motion stylesheet.
128
- // guard:allow-unscoped — editor access on this design is asserted in run()
129
- // before this helper is invoked; this only bumps the addressed design row.
130
- await db
131
- .update(schema.designs)
132
- .set({ updatedAt: now })
133
- .where(eq(schema.designs.id, designId));
134
-
135
- return now;
225
+ agentEnterDocument(file.id);
226
+ try {
227
+ const workspaceFile: SourceWorkspaceFile = {
228
+ id: file.id,
229
+ designId: file.designId,
230
+ filename: file.filename,
231
+ fileType: file.fileType,
232
+ content,
233
+ createdAt: null,
234
+ updatedAt: null,
235
+ };
236
+ const result = await writeInlineSourceFile({
237
+ designId: file.designId,
238
+ file: workspaceFile,
239
+ content,
240
+ expectedVersionHash,
241
+ });
242
+ return result.updatedAt;
243
+ } finally {
244
+ agentLeaveDocument(file.id);
245
+ }
136
246
  }
137
247
 
138
248
  // ─── Action ───────────────────────────────────────────────────────────────────
@@ -143,6 +253,11 @@ export default defineAction({
143
253
  "Persists the motion_timeline row, compiles tracks to CSS, injects the " +
144
254
  "managed <style data-agent-native-motion> block into the design's HTML, " +
145
255
  "and updates compiledHash — all in one atomic step. " +
256
+ "Supports the full keyframe model: per-property tracks (translate/scale/" +
257
+ "rotate compose as separate tracks on one node), per-segment easing " +
258
+ "(curves, steps, springs via spring(bounce)), playback modes " +
259
+ "(loop/once/ping-pong), and per-track start offsets/durations for " +
260
+ "staggering. " +
146
261
  "This is the durable timeline persist path; preview/scrubbing uses the " +
147
262
  "motion-preview postMessage bridge, NOT this action.",
148
263
  schema: z.object({
@@ -174,23 +289,44 @@ export default defineAction({
174
289
  )
175
290
  .describe(
176
291
  "Animation tracks. Each track targets one DOM element by " +
177
- "data-agent-native-node-id and animates one CSS property.",
292
+ "data-agent-native-node-id and animates one CSS property. " +
293
+ `Max ${MAX_MOTION_TRACKS} tracks per timeline.`,
178
294
  ),
179
295
  durationMs: z
180
296
  .number()
181
297
  .int()
182
298
  .positive()
299
+ .max(
300
+ MAX_MOTION_DURATION_MS,
301
+ `Total animation duration must be at most ${MAX_MOTION_DURATION_MS}ms (${
302
+ MAX_MOTION_DURATION_MS / 1000
303
+ }s).`,
304
+ )
183
305
  // Keep in sync with the MotionDock default (DesignEditor
184
306
  // motionDurationMs) and get-motion-timeline's CSS-recovery fallback so
185
307
  // an omitted duration means the same thing on every surface.
186
308
  .default(1000)
187
- .describe("Total animation duration in milliseconds."),
309
+ .describe(
310
+ "Total animation duration in milliseconds. " +
311
+ `Capped at ${MAX_MOTION_DURATION_MS}ms (${
312
+ MAX_MOTION_DURATION_MS / 1000
313
+ }s) to guard against runaway/DoS timelines.`,
314
+ ),
315
+ playbackMode: z
316
+ .enum(["loop", "once", "ping-pong"])
317
+ .optional()
318
+ .describe(
319
+ 'Timeline playback mode: "loop" repeats, "once" plays a single ' +
320
+ 'time, "ping-pong" alternates forward/backward. Omit to keep the ' +
321
+ 'timeline\'s existing mode (or "once" for new timelines).',
322
+ ),
188
323
  defaultEase: z
189
324
  .string()
190
325
  .default("ease")
191
326
  .describe(
192
327
  "Default easing applied to keyframe intervals that omit ease. " +
193
- 'E.g. "ease", "ease-in-out", "cubic-bezier(0.4,0,0.2,1)".',
328
+ 'E.g. "ease", "ease-in-out", "cubic-bezier(0.4,0,0.2,1)", or ' +
329
+ '"spring(0.25)".',
194
330
  ),
195
331
  label: z
196
332
  .string()
@@ -217,6 +353,7 @@ export default defineAction({
217
353
  sourceRef,
218
354
  tracks,
219
355
  durationMs,
356
+ playbackMode,
220
357
  defaultEase,
221
358
  includeContent,
222
359
  currentContent: currentContentInput,
@@ -239,6 +376,7 @@ export default defineAction({
239
376
  id: schema.designFiles.id,
240
377
  designId: schema.designFiles.designId,
241
378
  filename: schema.designFiles.filename,
379
+ fileType: schema.designFiles.fileType,
242
380
  content: schema.designFiles.content,
243
381
  })
244
382
  .from(schema.designFiles)
@@ -259,13 +397,63 @@ export default defineAction({
259
397
 
260
398
  const fileId = file.id;
261
399
  const resolvedSourceRef = sourceRef ?? fileId;
262
- const currentContent =
263
- currentContentInput !== undefined
264
- ? currentContentInput
265
- : (file.content ?? "");
400
+
401
+ // The EDITABLE base for compiling/injecting CSS: prefer the caller's
402
+ // in-flight editor snapshot when supplied (currentContentInput), else the
403
+ // LIVE collab-authoritative content for this file (not the raw SQL row —
404
+ // a live editor's Y.Text may be ahead of the last SQL snapshot). Either
405
+ // way, `baseVersionHash` is computed from the EXACT string chosen as the
406
+ // base, at this same point in time, so it faithfully represents "what
407
+ // this transform's base was" for the persist's optimistic-concurrency
408
+ // check below — never a fresh re-hash of already-transformed content
409
+ // (that would trivially match itself and prove nothing raced).
410
+ let currentContent: string;
411
+ let baseVersionHash: string;
412
+ if (currentContentInput !== undefined) {
413
+ // Caller supplied their own editor snapshot as the patch base. It may
414
+ // already be stale relative to the live doc — that's fine.
415
+ // writeInlineSourceFile's own guard (via readLiveSourceFile at write
416
+ // time) correctly rejects the write if this base no longer matches
417
+ // live state, exactly as designed for every other caller-supplied-base
418
+ // action (see apply-component-prop-edit.ts / apply-shader-fill.ts).
419
+ currentContent = currentContentInput;
420
+ baseVersionHash = sourceContentHash(currentContent);
421
+ } else {
422
+ const workspaceFileForRead: SourceWorkspaceFile = {
423
+ id: file.id,
424
+ designId: file.designId,
425
+ filename: file.filename,
426
+ fileType: file.fileType,
427
+ content: file.content,
428
+ createdAt: null,
429
+ updatedAt: null,
430
+ };
431
+ const live = await readLiveSourceFile(workspaceFileForRead);
432
+ currentContent = live.content;
433
+ baseVersionHash = live.versionHash;
434
+ }
266
435
 
267
436
  // ── 2. Compile tracks → CSS ─────────────────────────────────────────────
268
- const typedTracks = tracks as MotionTrack[];
437
+ const inputTracks = tracks as MotionTrack[];
438
+
439
+ // DoS guards: reject (never silently clamp) before any compilation work
440
+ // so an over-limit request fails fast with a clear, actionable error.
441
+ if (inputTracks.length > MAX_MOTION_TRACKS) {
442
+ throw new Error(
443
+ `Too many motion tracks: ${inputTracks.length} exceeds the ` +
444
+ `${MAX_MOTION_TRACKS}-track limit per timeline. Split the ` +
445
+ "animation across multiple timelines/screens or remove unused tracks.",
446
+ );
447
+ }
448
+ for (const track of inputTracks) {
449
+ if (track.keyframes.length > MAX_MOTION_KEYFRAMES_PER_TRACK) {
450
+ throw new Error(
451
+ `Too many keyframes on track "${track.targetNodeId}"/"${track.property}": ` +
452
+ `${track.keyframes.length} exceeds the ${MAX_MOTION_KEYFRAMES_PER_TRACK}-keyframe ` +
453
+ "limit per track.",
454
+ );
455
+ }
456
+ }
269
457
 
270
458
  // Reject CSS-injection vectors in caller-supplied track properties,
271
459
  // keyframe values, and easing strings before they are compiled into the
@@ -273,7 +461,7 @@ export default defineAction({
273
461
  // pairs: the compiler derives the animation name from that pair, so a
274
462
  // duplicate would silently overwrite the earlier track's keyframes.
275
463
  const seenTrackKeys = new Set<string>();
276
- for (const track of typedTracks) {
464
+ for (const track of inputTracks) {
277
465
  assertSafeMotionCssProperty(track.property, "track.property");
278
466
  const trackKey = motionTrackKey(track.targetNodeId, track.property);
279
467
  if (seenTrackKeys.has(trackKey)) {
@@ -287,11 +475,21 @@ export default defineAction({
287
475
  for (const kf of track.keyframes) {
288
476
  assertSafeMotionCssToken(kf.value, "keyframe value");
289
477
  if (kf.ease !== undefined) {
290
- assertSafeMotionCssToken(kf.ease, "keyframe ease");
478
+ assertValidMotionEase(kf.ease, "keyframe ease");
291
479
  }
292
480
  }
293
481
  }
294
- assertSafeMotionCssToken(defaultEase, "defaultEase");
482
+ assertValidMotionEase(defaultEase, "defaultEase");
483
+
484
+ // Persist the timeline-level playback mode as a stamp on the first track
485
+ // so the stored tracks JSON stays a plain (schema-compatible) array. An
486
+ // explicit playbackMode wins; otherwise any stamp already present in the
487
+ // incoming tracks is preserved.
488
+ const typedTracks = playbackMode
489
+ ? withTimelinePlaybackMode(inputTracks, playbackMode)
490
+ : inputTracks;
491
+ const resolvedPlaybackMode: MotionPlaybackMode =
492
+ playbackMode ?? readTimelinePlaybackMode(inputTracks) ?? "once";
295
493
 
296
494
  const { css, hash } = compile({
297
495
  id: timelineId ?? "",
@@ -300,6 +498,7 @@ export default defineAction({
300
498
  filePath: null,
301
499
  tracks: typedTracks,
302
500
  durationMs,
501
+ playbackMode: resolvedPlaybackMode,
303
502
  defaultEase,
304
503
  compiledHash: null,
305
504
  createdAt: now,
@@ -413,8 +612,21 @@ export default defineAction({
413
612
  // Written after the row so a SQL failure here leaves the timeline row
414
613
  // accurate (correct tracks + hash) and the stale HTML can be recompiled on
415
614
  // the next apply-motion-edit call via compiledHash drift detection.
615
+ // Goes through the collab-aware writeInlineSourceFile seam, conditioned on
616
+ // baseVersionHash (the hash of the SAME base string used above to compile
617
+ // patchedContent), so a concurrent editor's write between that base read
618
+ // and this persist is rejected loud rather than silently overwritten.
416
619
  const updatedAt = contentPatched
417
- ? await persistFileContent(fileId, designId, patchedContent, now)
620
+ ? await persistFileContent(
621
+ {
622
+ id: fileId,
623
+ designId,
624
+ filename: file.filename,
625
+ fileType: file.fileType,
626
+ },
627
+ patchedContent,
628
+ baseVersionHash,
629
+ )
418
630
  : now;
419
631
 
420
632
  return {
@@ -423,6 +635,7 @@ export default defineAction({
423
635
  fileId,
424
636
  sourceRef: resolvedSourceRef,
425
637
  trackCount: typedTracks.length,
638
+ playbackMode: resolvedPlaybackMode,
426
639
  compiledHash: hash,
427
640
  updatedAt,
428
641
  bytesBefore,
@@ -27,12 +27,21 @@
27
27
  */
28
28
 
29
29
  import { defineAction } from "@agent-native/core";
30
- import { agentUpdateSelection } from "@agent-native/core/collab";
30
+ import {
31
+ agentEnterDocument,
32
+ agentLeaveDocument,
33
+ agentUpdateSelection,
34
+ } from "@agent-native/core/collab";
31
35
  import { accessFilter, assertAccess } from "@agent-native/core/sharing";
32
36
  import { and, eq } from "drizzle-orm";
33
37
  import { z } from "zod";
34
38
 
35
39
  import { getDb, schema } from "../server/db/index.js";
40
+ import {
41
+ readLiveSourceFile,
42
+ writeInlineSourceFile,
43
+ type SourceWorkspaceFile,
44
+ } from "../server/source-workspace.js";
36
45
  import {
37
46
  applyVisualEdit,
38
47
  type CodeLayerSource,
@@ -49,6 +58,7 @@ import {
49
58
  type ShaderPresetName,
50
59
  validateDescriptor,
51
60
  } from "../shared/shader-presets.js";
61
+ import { sourceContentHash } from "../shared/source-workspace.js";
52
62
 
53
63
  // ─── Schema ──────────────────────────────────────────────────────────────────
54
64
 
@@ -141,9 +151,10 @@ interface ResolvedDesignFile {
141
151
  id: string;
142
152
  designId: string;
143
153
  filename: string;
154
+ fileType: string;
144
155
  content: string;
145
- updatedAt: string | null;
146
- expectedUpdatedAt?: string;
156
+ /** versionHash of `content` — the exact base the transform reads from. */
157
+ versionHash: string;
147
158
  codeLayerSource: CodeLayerSource;
148
159
  }
149
160
 
@@ -151,6 +162,15 @@ interface ResolvedDesignFile {
151
162
  * Resolve the target HTML design file with an access-scoped read, then assert
152
163
  * editor access. Mirrors `resolveEditableDesignFile` in apply-visual-edit.ts so
153
164
  * the shader-fill write goes through the exact same ownership gate.
165
+ *
166
+ * The editable base is collab-aware: `readLiveSourceFile` returns the live
167
+ * Y.Text content when a collab doc exists for this file, else the SQL row —
168
+ * never only the raw SQL snapshot. When the caller supplies
169
+ * `source.currentContent`, that in-flight editor snapshot is preferred as the
170
+ * base instead (same precedence as apply-motion-edit.ts / apply-component-
171
+ * prop-edit.ts), and its versionHash is computed directly from that exact
172
+ * string via `sourceContentHash` — not from a fresh live re-read, which would
173
+ * prove nothing about whether this specific base is still current.
154
174
  */
155
175
  async function resolveEditableDesignFile(source: {
156
176
  designId?: string;
@@ -221,6 +241,9 @@ async function resolveEditableDesignFile(source: {
221
241
  "source.revision is required when source.currentContent is provided.",
222
242
  );
223
243
  }
244
+ // Legitimate pre-check, distinct from the CAS write-guard below: reject
245
+ // early when the caller's own revision stamp doesn't match the file's SQL
246
+ // updatedAt, before any transform work happens.
224
247
  if (
225
248
  source.currentContent !== undefined &&
226
249
  source.revision &&
@@ -230,17 +253,33 @@ async function resolveEditableDesignFile(source: {
230
253
  throw new ShaderFillRevisionConflictError();
231
254
  }
232
255
 
256
+ let content: string;
257
+ let versionHash: string;
258
+ if (source.currentContent !== undefined) {
259
+ content = source.currentContent;
260
+ versionHash = sourceContentHash(content);
261
+ } else {
262
+ const workspaceFile: SourceWorkspaceFile = {
263
+ id: file.id,
264
+ designId: file.designId,
265
+ filename: file.filename,
266
+ fileType: file.fileType,
267
+ content: file.content,
268
+ createdAt: null,
269
+ updatedAt: file.updatedAt,
270
+ };
271
+ const live = await readLiveSourceFile(workspaceFile);
272
+ content = live.content;
273
+ versionHash = live.versionHash;
274
+ }
275
+
233
276
  return {
234
277
  id: file.id,
235
278
  designId: file.designId,
236
279
  filename: file.filename,
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 ?? ""),
280
+ fileType: file.fileType,
281
+ content,
282
+ versionHash,
244
283
  codeLayerSource: {
245
284
  kind: "design-file",
246
285
  designId: file.designId,
@@ -251,69 +290,52 @@ async function resolveEditableDesignFile(source: {
251
290
  };
252
291
  }
253
292
 
293
+ /**
294
+ * Persist the patched HTML through the collab-aware seam
295
+ * (`writeInlineSourceFile`), conditioned on the versionHash of the SAME base
296
+ * the `applyVisualEdit` transform used — not a fresh re-read/re-check at
297
+ * persist time. `writeInlineSourceFile` re-reads the live text immediately
298
+ * before its own write and throws "Source file changed since it was read..."
299
+ * if it no longer matches; callers map that rejection to the existing
300
+ * `ShaderFillRevisionConflictError`-shaped response (see run()'s catch block).
301
+ */
254
302
  async function persistDesignFileEdit(file: {
255
303
  id: string;
256
304
  designId: string;
305
+ filename: string;
306
+ fileType: string;
257
307
  content: string;
258
- expectedUpdatedAt?: string;
308
+ expectedVersionHash: string;
259
309
  }): Promise<string> {
260
- await assertAccess("design", file.designId, "editor");
261
-
262
- const db = getDb();
263
- const now = new Date().toISOString();
264
-
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();
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
- );
290
-
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) {
310
+ agentEnterDocument(file.id);
311
+ try {
312
+ const workspaceFile: SourceWorkspaceFile = {
313
+ id: file.id,
314
+ designId: file.designId,
315
+ filename: file.filename,
316
+ fileType: file.fileType,
317
+ content: file.content,
318
+ createdAt: null,
319
+ updatedAt: null,
320
+ };
321
+ const result = await writeInlineSourceFile({
322
+ designId: file.designId,
323
+ file: workspaceFile,
324
+ content: file.content,
325
+ expectedVersionHash: file.expectedVersionHash,
326
+ });
327
+ return result.updatedAt;
328
+ } catch (error) {
329
+ if (
330
+ error instanceof Error &&
331
+ /changed since it was read/.test(error.message)
332
+ ) {
301
333
  throw new ShaderFillRevisionConflictError();
302
334
  }
335
+ throw error;
336
+ } finally {
337
+ agentLeaveDocument(file.id);
303
338
  }
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;
317
339
  }
318
340
 
319
341
  // ─── Action ──────────────────────────────────────────────────────────────────
@@ -448,8 +470,10 @@ snippet (WebGL canvas / JSX component), call apply-shader.
448
470
  updatedAt = await persistDesignFileEdit({
449
471
  id: file.id,
450
472
  designId: file.designId,
473
+ filename: file.filename,
474
+ fileType: file.fileType,
451
475
  content: patch.content,
452
- expectedUpdatedAt: file.expectedUpdatedAt,
476
+ expectedVersionHash: file.versionHash,
453
477
  });
454
478
  } catch (error) {
455
479
  if (error instanceof ShaderFillRevisionConflictError) {