@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
@@ -1378,6 +1378,23 @@ export class RecorderEngine {
1378
1378
  };
1379
1379
  this.lastFinalizeMeta = finalizeMeta;
1380
1380
 
1381
+ // Stop camera and mic hardware immediately — privacy-sensitive inputs no
1382
+ // longer needed once the final chunk is flushed. The composite and display
1383
+ // streams are intentionally left alive: the caller holds a compositeStream
1384
+ // reference for an unawaited thumbnail capture in screen+camera mode.
1385
+ // Full stream teardown happens in the finally block below.
1386
+ this.cameraLive = false;
1387
+ this.audioMixSources = [];
1388
+ this.audioMixCtx?.close().catch(() => {});
1389
+ this.audioMixCtx = null;
1390
+ for (const s of [this.cameraStream, this.rawCameraStream, this.micStream]) {
1391
+ s?.getTracks().forEach((t) => {
1392
+ try {
1393
+ t.stop();
1394
+ } catch {}
1395
+ });
1396
+ }
1397
+
1381
1398
  // Drain any legacy in-flight chunk uploads before we either compress or
1382
1399
  // upload. New recordings upload after stop(), but keeping this guard makes
1383
1400
  // retry paths resilient while a release rolls out.
@@ -1929,41 +1946,99 @@ export class RecorderEngine {
1929
1946
  // Keep binary uploads comfortably under Netlify's effective function
1930
1947
  // payload limit. This mirrors the local-file upload path in record.tsx.
1931
1948
  const UPLOAD_SLICE_BYTES = 3 * 1024 * 1024;
1949
+ const PARALLELISM = 4;
1932
1950
  const totalSlices = Math.max(1, Math.ceil(blob.size / UPLOAD_SLICE_BYTES));
1933
1951
 
1934
- let lastResult: Record<string, unknown> | undefined;
1935
- for (let i = 0; i < totalSlices; i++) {
1936
- if (signal?.aborted) {
1937
- throw signal.reason instanceof Error
1938
- ? signal.reason
1939
- : new Error("Upload aborted");
1940
- }
1941
-
1952
+ const slices = Array.from({ length: totalSlices }, (_, i) => {
1942
1953
  const start = i * UPLOAD_SLICE_BYTES;
1943
1954
  const end = Math.min(start + UPLOAD_SLICE_BYTES, blob.size);
1944
- const slice = blob.slice(start, end, mimeType);
1945
- const isFinal = i === totalSlices - 1;
1946
- const index = this.chunkIndex++;
1955
+ return {
1956
+ index: this.chunkIndex++,
1957
+ slice: blob.slice(start, end, mimeType),
1958
+ isFinal: i === totalSlices - 1,
1959
+ };
1960
+ });
1961
+ const finalSlice = slices[slices.length - 1];
1962
+ const parallelSlices = slices.slice(0, -1);
1963
+
1964
+ const results = new Array<Record<string, unknown> | undefined>(totalSlices);
1965
+ const queue = parallelSlices.slice();
1966
+ const chunkAbort = new AbortController();
1967
+ if (signal?.aborted) {
1968
+ chunkAbort.abort(signal.reason);
1969
+ } else {
1970
+ signal?.addEventListener("abort", () => chunkAbort.abort(signal.reason), {
1971
+ once: true,
1972
+ });
1973
+ }
1974
+ let uploadError: Error | null = null;
1975
+ let completedCount = 0;
1976
+
1977
+ const worker = async () => {
1978
+ while (queue.length > 0) {
1979
+ if (chunkAbort.signal.aborted) break;
1980
+ const item = queue.shift();
1981
+ if (!item) break;
1982
+ const { index, slice } = item;
1983
+ try {
1984
+ results[index] = await this.uploadChunk(slice, index, {
1985
+ isFinal: false,
1986
+ total: totalSlices,
1987
+ mimeType,
1988
+ signal: chunkAbort.signal,
1989
+ });
1990
+ this.opts.onChunk?.({
1991
+ index: completedCount++,
1992
+ bytes: slice.size,
1993
+ total: totalSlices,
1994
+ });
1995
+ } catch (err) {
1996
+ if (chunkAbort.signal.aborted) return;
1997
+ if (!uploadError) {
1998
+ uploadError = err instanceof Error ? err : new Error(String(err));
1999
+ chunkAbort.abort(uploadError);
2000
+ }
2001
+ return;
2002
+ }
2003
+ }
2004
+ };
2005
+
2006
+ await Promise.all(
2007
+ Array.from(
2008
+ { length: Math.min(PARALLELISM, parallelSlices.length) },
2009
+ worker,
2010
+ ),
2011
+ );
2012
+
2013
+ if (uploadError) throw uploadError;
2014
+ if (signal?.aborted) {
2015
+ throw signal.reason instanceof Error
2016
+ ? signal.reason
2017
+ : new Error("Upload aborted");
2018
+ }
1947
2019
 
1948
- lastResult = await this.uploadChunk(slice, index, {
1949
- isFinal,
2020
+ results[finalSlice.index] = await this.uploadChunk(
2021
+ finalSlice.slice,
2022
+ finalSlice.index,
2023
+ {
2024
+ isFinal: true,
1950
2025
  total: totalSlices,
1951
2026
  mimeType,
1952
- durationMs: isFinal ? meta.durationMs : undefined,
1953
- width: isFinal ? meta.dimensions.width : undefined,
1954
- height: isFinal ? meta.dimensions.height : undefined,
1955
- hasAudio: isFinal ? meta.hasAudio : undefined,
1956
- hasCamera: isFinal ? meta.hasCamera : undefined,
2027
+ durationMs: meta.durationMs,
2028
+ width: meta.dimensions.width,
2029
+ height: meta.dimensions.height,
2030
+ hasAudio: meta.hasAudio,
2031
+ hasCamera: meta.hasCamera,
1957
2032
  signal,
1958
- });
1959
- this.opts.onChunk?.({
1960
- index,
1961
- bytes: slice.size,
1962
- total: totalSlices,
1963
- });
1964
- }
2033
+ },
2034
+ );
2035
+ this.opts.onChunk?.({
2036
+ index: finalSlice.index,
2037
+ bytes: finalSlice.slice.size,
2038
+ total: totalSlices,
2039
+ });
1965
2040
 
1966
- return lastResult;
2041
+ return results[finalSlice.index];
1967
2042
  }
1968
2043
 
1969
2044
  private async uploadChunk(
@@ -2021,7 +2096,9 @@ export class RecorderEngine {
2021
2096
  uploadErr.name === "AbortError"
2022
2097
  ) {
2023
2098
  if (extra.isFinal && uploadErr.name !== "AbortError") {
2024
- const recovered = await this.recoverReadyAfterFinalUploadError();
2099
+ const recovered = await this.recoverReadyAfterFinalUploadError(
2100
+ extra.signal,
2101
+ );
2025
2102
  if (recovered) return recovered;
2026
2103
  }
2027
2104
  throw uploadErr;
@@ -2040,7 +2117,9 @@ export class RecorderEngine {
2040
2117
  if (extra.isFinal && res.status === 504) {
2041
2118
  triedFinalUploadRecovery = true;
2042
2119
  await res.text().catch(() => "");
2043
- const recovered = await this.recoverReadyAfterFinalUploadError();
2120
+ const recovered = await this.recoverReadyAfterFinalUploadError(
2121
+ extra.signal,
2122
+ );
2044
2123
  if (recovered) return recovered;
2045
2124
  break;
2046
2125
  }
@@ -2075,7 +2154,9 @@ export class RecorderEngine {
2075
2154
  !triedFinalUploadRecovery &&
2076
2155
  this.isFinalUploadRecoveryCandidate(res.status, err)
2077
2156
  ) {
2078
- const recovered = await this.recoverReadyAfterFinalUploadError();
2157
+ const recovered = await this.recoverReadyAfterFinalUploadError(
2158
+ extra.signal,
2159
+ );
2079
2160
  if (recovered) return recovered;
2080
2161
  }
2081
2162
  trackClipUploadBlockingFailure({
@@ -2153,14 +2234,14 @@ export class RecorderEngine {
2153
2234
  return !/too large|exceeds.*limit|chunk too large/i.test(error.message);
2154
2235
  }
2155
2236
 
2156
- private async recoverReadyAfterFinalUploadError(): Promise<Record<
2157
- string,
2158
- unknown
2159
- > | null> {
2237
+ private async recoverReadyAfterFinalUploadError(
2238
+ signal?: AbortSignal,
2239
+ ): Promise<Record<string, unknown> | null> {
2160
2240
  return waitForReadyRecordingAfterFinalizeError({
2161
2241
  uploadUrl: this.opts.uploadUrl,
2162
2242
  recordingId: this.opts.recordingId,
2163
2243
  preferAuthenticated: true,
2244
+ signal,
2164
2245
  });
2165
2246
  }
2166
2247
 
@@ -1,3 +1,8 @@
1
+ import { useT } from "@agent-native/core/client";
2
+ import { Button } from "@agent-native/toolkit/ui/button";
3
+ import { IconPlayerRecord } from "@tabler/icons-react";
4
+ import { NavLink } from "react-router";
5
+
1
6
  import { LibraryGrid } from "@/components/library/library-grid";
2
7
 
3
8
  const SEO_TITLE =
@@ -18,5 +23,26 @@ export function meta() {
18
23
  }
19
24
 
20
25
  export default function LibraryIndexRoute() {
21
- return <LibraryGrid view="library" folderId={null} title="Library" />;
26
+ const t = useT();
27
+ return (
28
+ <LibraryGrid
29
+ view="library"
30
+ folderId={null}
31
+ title="Library"
32
+ extraActions={
33
+ <Button
34
+ className="gap-1.5 bg-primary text-primary-foreground shadow-sm hover:bg-primary/90"
35
+ size="sm"
36
+ asChild
37
+ >
38
+ <NavLink to="/record" aria-label={t("navigation.newRecording")}>
39
+ <IconPlayerRecord className="h-4 w-4" />
40
+ <span className="hidden sm:inline">
41
+ {t("navigation.newRecording")}
42
+ </span>
43
+ </NavLink>
44
+ </Button>
45
+ }
46
+ />
47
+ );
22
48
  }
@@ -1357,6 +1357,7 @@ export default function RecordRoute() {
1357
1357
  // upload pipeline so finalize-recording handles it identically.
1358
1358
  // -------------------------------------------------------------------------
1359
1359
  const UPLOAD_CHUNK_BYTES = 3 * 1024 * 1024;
1360
+ const UPLOAD_PARALLELISM = 4;
1360
1361
 
1361
1362
  const probeVideoMetadata = useCallback(
1362
1363
  (
@@ -1602,92 +1603,180 @@ export default function RecordRoute() {
1602
1603
  1,
1603
1604
  Math.ceil(uploadBlob.size / UPLOAD_CHUNK_BYTES),
1604
1605
  );
1605
- let finalChunkResult: Record<string, unknown> | null = null;
1606
- for (let i = 0; i < totalChunks; i++) {
1607
- if (isStale()) throw makeAbortError("Upload cancelled");
1606
+
1607
+ const chunkDescs = Array.from({ length: totalChunks }, (_, i) => {
1608
1608
  const start = i * UPLOAD_CHUNK_BYTES;
1609
1609
  const end = Math.min(start + UPLOAD_CHUNK_BYTES, uploadBlob.size);
1610
- const slice = uploadBlob.slice(start, end, uploadMimeType);
1611
1610
  const isFinal = i === totalChunks - 1;
1612
- const chunkUrl = chunkUploadUrl(uploadBase, {
1611
+ return {
1613
1612
  index: i,
1614
- total: totalChunks,
1613
+ slice: uploadBlob.slice(start, end, uploadMimeType),
1615
1614
  isFinal,
1616
- mimeType: uploadMimeType,
1617
- durationMs: isFinal ? meta.durationMs : undefined,
1618
- width: isFinal ? meta.width : undefined,
1619
- height: isFinal ? meta.height : undefined,
1620
- hasAudio: isFinal ? true : undefined,
1621
- hasCamera: isFinal ? false : undefined,
1615
+ url: chunkUploadUrl(uploadBase, {
1616
+ index: i,
1617
+ total: totalChunks,
1618
+ isFinal,
1619
+ mimeType: uploadMimeType,
1620
+ durationMs: isFinal ? meta.durationMs : undefined,
1621
+ width: isFinal ? meta.width : undefined,
1622
+ height: isFinal ? meta.height : undefined,
1623
+ hasAudio: isFinal ? true : undefined,
1624
+ hasCamera: isFinal ? false : undefined,
1625
+ }),
1626
+ };
1627
+ });
1628
+ const finalChunkDesc = chunkDescs[chunkDescs.length - 1];
1629
+ const parallelChunks = chunkDescs.slice(0, -1);
1630
+
1631
+ const chunkAbort = new AbortController();
1632
+ if (abort.signal.aborted) {
1633
+ chunkAbort.abort(abort.signal.reason);
1634
+ } else {
1635
+ abort.signal.addEventListener(
1636
+ "abort",
1637
+ () => chunkAbort.abort(abort.signal.reason),
1638
+ { once: true },
1639
+ );
1640
+ }
1641
+
1642
+ const finalChunk = { result: null as Record<string, unknown> | null };
1643
+ let uploadError: Error | null = null;
1644
+ const queue = parallelChunks.slice();
1645
+
1646
+ const worker = async () => {
1647
+ while (queue.length > 0) {
1648
+ if (isStale() || chunkAbort.signal.aborted) break;
1649
+ const item = queue.shift();
1650
+ if (!item) break;
1651
+ const { index, slice, url } = item;
1652
+
1653
+ let chunkRes: Response;
1654
+ try {
1655
+ chunkRes = await fetch(url, {
1656
+ method: "POST",
1657
+ headers: { "Content-Type": uploadMimeType },
1658
+ body: await slice.arrayBuffer(),
1659
+ signal: chunkAbort.signal,
1660
+ });
1661
+ } catch (err) {
1662
+ if (chunkAbort.signal.aborted) return;
1663
+ if (!uploadError) {
1664
+ uploadError =
1665
+ err instanceof Error ? err : new Error(String(err));
1666
+ chunkAbort.abort(uploadError);
1667
+ }
1668
+ return;
1669
+ }
1670
+
1671
+ if (!chunkRes.ok) {
1672
+ const text = await chunkRes.text().catch(() => "");
1673
+ const error = new Error(
1674
+ t("recordRoute.uploadFailedAtChunk", {
1675
+ chunk: index + 1,
1676
+ total: totalChunks,
1677
+ message: text || chunkRes.statusText,
1678
+ }),
1679
+ );
1680
+ (error as Error & { status?: number }).status = chunkRes.status;
1681
+ if (!uploadError) {
1682
+ uploadError = error;
1683
+ chunkAbort.abort(uploadError);
1684
+ }
1685
+ return;
1686
+ }
1687
+ setUploadProgress((index + 1) / totalChunks);
1688
+ }
1689
+ };
1690
+
1691
+ await Promise.all(
1692
+ Array.from(
1693
+ { length: Math.min(UPLOAD_PARALLELISM, parallelChunks.length) },
1694
+ worker,
1695
+ ),
1696
+ );
1697
+
1698
+ if (uploadError) throw uploadError;
1699
+ if (abort.signal.aborted) {
1700
+ const reason = abort.signal.reason;
1701
+ throw reason instanceof Error
1702
+ ? reason
1703
+ : makeAbortError("Upload cancelled");
1704
+ }
1705
+ if (isStale()) throw makeAbortError("Upload cancelled");
1706
+
1707
+ const { index, slice, url } = finalChunkDesc;
1708
+ let chunkRes: Response | null = null;
1709
+ try {
1710
+ chunkRes = await fetch(url, {
1711
+ method: "POST",
1712
+ headers: { "Content-Type": uploadMimeType },
1713
+ body: await slice.arrayBuffer(),
1714
+ signal: abort.signal,
1622
1715
  });
1623
- let chunkRes: Response;
1624
- try {
1625
- chunkRes = await fetch(chunkUrl, {
1626
- method: "POST",
1627
- headers: { "Content-Type": uploadMimeType },
1628
- body: await slice.arrayBuffer(),
1716
+ } catch (err) {
1717
+ if (
1718
+ createdId &&
1719
+ (err as { name?: string } | null)?.name !== "AbortError"
1720
+ ) {
1721
+ const recovered = await waitForReadyRecordingAfterFinalizeError({
1722
+ uploadUrl: uploadBase,
1723
+ recordingId: createdId,
1724
+ preferAuthenticated: true,
1629
1725
  signal: abort.signal,
1630
1726
  });
1631
- } catch (err) {
1632
- if (
1633
- isFinal &&
1634
- createdId &&
1635
- (err as { name?: string } | null)?.name !== "AbortError"
1636
- ) {
1637
- const recovered = await waitForReadyRecordingAfterFinalizeError({
1638
- uploadUrl: uploadBase,
1639
- recordingId: createdId,
1640
- preferAuthenticated: true,
1641
- });
1642
- if (recovered) {
1643
- finalChunkResult = recovered;
1644
- break;
1645
- }
1727
+ if (recovered) {
1728
+ finalChunk.result = recovered;
1729
+ } else {
1730
+ throw err;
1646
1731
  }
1732
+ } else {
1647
1733
  throw err;
1648
1734
  }
1649
- if (!chunkRes.ok) {
1650
- const text = await chunkRes.text().catch(() => "");
1651
- const error = new Error(
1652
- t("recordRoute.uploadFailedAtChunk", {
1653
- chunk: i + 1,
1654
- total: totalChunks,
1655
- message: text || chunkRes.statusText,
1656
- }),
1657
- );
1658
- (error as Error & { status?: number }).status = chunkRes.status;
1659
- if (
1660
- isFinal &&
1661
- createdId &&
1662
- chunkRes.status !== 413 &&
1663
- !isUploadSizeError(error.message)
1664
- ) {
1665
- const recovered = await waitForReadyRecordingAfterFinalizeError({
1666
- uploadUrl: uploadBase,
1667
- recordingId: createdId,
1668
- preferAuthenticated: true,
1669
- });
1670
- if (recovered) {
1671
- finalChunkResult = recovered;
1672
- break;
1673
- }
1735
+ }
1736
+
1737
+ if (chunkRes && !chunkRes.ok) {
1738
+ const text = await chunkRes.text().catch(() => "");
1739
+ const error = new Error(
1740
+ t("recordRoute.uploadFailedAtChunk", {
1741
+ chunk: index + 1,
1742
+ total: totalChunks,
1743
+ message: text || chunkRes.statusText,
1744
+ }),
1745
+ );
1746
+ (error as Error & { status?: number }).status = chunkRes.status;
1747
+ if (
1748
+ createdId &&
1749
+ chunkRes.status !== 413 &&
1750
+ !isUploadSizeError(error.message)
1751
+ ) {
1752
+ const recovered = await waitForReadyRecordingAfterFinalizeError({
1753
+ uploadUrl: uploadBase,
1754
+ recordingId: createdId,
1755
+ preferAuthenticated: true,
1756
+ signal: abort.signal,
1757
+ });
1758
+ if (recovered) {
1759
+ finalChunk.result = recovered;
1760
+ } else {
1761
+ throw error;
1674
1762
  }
1763
+ } else {
1675
1764
  throw error;
1676
1765
  }
1677
- if (isFinal) {
1678
- finalChunkResult =
1679
- ((await chunkRes.json().catch(() => null)) as Record<
1680
- string,
1681
- unknown
1682
- > | null) ?? null;
1683
- }
1684
- setUploadProgress((i + 1) / totalChunks);
1766
+ }
1767
+
1768
+ if (chunkRes?.ok) {
1769
+ finalChunk.result =
1770
+ ((await chunkRes.json().catch(() => null)) as Record<
1771
+ string,
1772
+ unknown
1773
+ > | null) ?? null;
1685
1774
  }
1686
1775
 
1687
1776
  setUiState("complete");
1688
1777
  const waitingForStorage =
1689
- finalChunkResult?.waitingForStorage === true ||
1690
- finalChunkResult?.status === "waiting_storage";
1778
+ finalChunk.result?.waitingForStorage === true ||
1779
+ finalChunk.result?.status === "waiting_storage";
1691
1780
  if (waitingForStorage) {
1692
1781
  toast.info(t("recordRoute.videoReadyToUpload"), {
1693
1782
  description: t("recordRoute.connectStorageToFinish"),
@@ -75,11 +75,6 @@ export default defineEventHandler(async (event: H3Event) => {
75
75
  return { ok: true, recordingId, alreadyFailed: true, chunksCleared: 0 };
76
76
  }
77
77
 
78
- const cleared = await deleteAppStateByPrefix(
79
- `recording-chunks-${recordingId}-`,
80
- );
81
- await deleteResumableSession(recordingId).catch(() => {});
82
-
83
78
  const now = new Date().toISOString();
84
79
  await db
85
80
  .update(schema.recordings)
@@ -96,6 +91,11 @@ export default defineEventHandler(async (event: H3Event) => {
96
91
  failureReason,
97
92
  updatedAt: now,
98
93
  });
94
+
95
+ const cleared = await deleteAppStateByPrefix(
96
+ `recording-chunks-${recordingId}-`,
97
+ );
98
+ await deleteResumableSession(recordingId).catch(() => {});
99
99
  await writeAppState("refresh-signal", { ts: Date.now() });
100
100
 
101
101
  return { ok: true, recordingId, chunksCleared: cleared };
@@ -375,7 +375,11 @@ export default defineEventHandler(async (event: H3Event) => {
375
375
 
376
376
  // Only persist non-empty chunks. The final sentinel can legitimately be
377
377
  // empty — writing a zero-byte chunk would just clutter application_state.
378
+ // Check for abort/failure before writing so parallel in-flight requests
379
+ // don't recreate scratch chunk rows after /abort already cleared them.
378
380
  if (bytes.byteLength > 0) {
381
+ const failedBeforeWrite = await stopIfUploadFailed();
382
+ if (failedBeforeWrite) return failedBeforeWrite;
379
383
  // Pad index to 6 digits so string-sort order matches numeric order if the
380
384
  // finalize path ever sorts lexically. (finalize also parses back to a number.)
381
385
  const paddedIndex = String(index).padStart(6, "0");
@@ -409,15 +413,25 @@ export default defineEventHandler(async (event: H3Event) => {
409
413
 
410
414
  // Update upload progress (best-effort). If total is unknown we treat it as
411
415
  // indeterminate and keep progress at its last known value.
416
+ // Chunks may arrive out of order when uploaded in parallel, so take the
417
+ // max of the current persisted value and the incoming index to keep
418
+ // progress monotonically non-decreasing.
412
419
  if (total > 0) {
413
420
  const failedResponse = await stopIfUploadFailed();
414
421
  if (failedResponse) return failedResponse;
415
- const progress = Math.min(100, Math.round(((index + 1) / total) * 100));
422
+ const chunksReceived = Math.max(
423
+ stateNumber(uploadState, "chunksReceived") ?? 0,
424
+ index + 1,
425
+ );
426
+ const progress = Math.max(
427
+ stateNumber(uploadState, "progress") ?? 0,
428
+ Math.min(100, Math.round((chunksReceived / total) * 100)),
429
+ );
416
430
  await writeAppState(`recording-upload-${recordingId}`, {
417
431
  recordingId,
418
432
  status: isFinal ? "processing" : "uploading",
419
433
  progress,
420
- chunksReceived: index + 1,
434
+ chunksReceived,
421
435
  totalChunks: total,
422
436
  ...(expectedDataChunks !== undefined
423
437
  ? {
@@ -449,7 +463,10 @@ export default defineEventHandler(async (event: H3Event) => {
449
463
  await writeAppState(`recording-upload-${recordingId}`, {
450
464
  recordingId,
451
465
  status: isFinal ? "processing" : "uploading",
452
- chunksReceived: index + 1,
466
+ chunksReceived: Math.max(
467
+ stateNumber(uploadState, "chunksReceived") ?? 0,
468
+ index + 1,
469
+ ),
453
470
  ...(expectedDataChunks !== undefined
454
471
  ? {
455
472
  expectedDataChunks,
@@ -26,8 +26,26 @@ const DEFAULT_READY_RECOVERY_TIMEOUT_MS = 90_000;
26
26
  const DEFAULT_READY_RECOVERY_INTERVAL_MS = 3_000;
27
27
  const DEFAULT_READY_RECOVERY_FETCH_TIMEOUT_MS = 2_000;
28
28
 
29
- function sleep(ms: number): Promise<void> {
30
- return new Promise((resolve) => setTimeout(resolve, ms));
29
+ function sleep(ms: number, signal?: AbortSignal): Promise<void> {
30
+ return new Promise((resolve, reject) => {
31
+ if (signal?.aborted) {
32
+ reject(
33
+ signal.reason instanceof Error ? signal.reason : new Error("Aborted"),
34
+ );
35
+ return;
36
+ }
37
+ const timer = setTimeout(resolve, ms);
38
+ signal?.addEventListener(
39
+ "abort",
40
+ () => {
41
+ clearTimeout(timer);
42
+ reject(
43
+ signal.reason instanceof Error ? signal.reason : new Error("Aborted"),
44
+ );
45
+ },
46
+ { once: true },
47
+ );
48
+ });
31
49
  }
32
50
 
33
51
  function absoluteUploadUrl(uploadUrl: string): URL {
@@ -157,9 +175,10 @@ export async function waitForReadyRecordingAfterFinalizeError(args: {
157
175
  timeoutMs?: number;
158
176
  intervalMs?: number;
159
177
  fetchTimeoutMs?: number;
178
+ signal?: AbortSignal;
160
179
  }): Promise<RecoveredReadyRecording | null> {
161
180
  const fetchImpl = args.fetchImpl ?? fetch;
162
- const sleepImpl = args.sleepImpl ?? sleep;
181
+ const sleepImpl = args.sleepImpl ?? ((ms) => sleep(ms, args.signal));
163
182
  const timeoutMs = Math.max(
164
183
  1,
165
184
  args.timeoutMs ?? DEFAULT_READY_RECOVERY_TIMEOUT_MS,
@@ -182,6 +201,7 @@ export async function waitForReadyRecordingAfterFinalizeError(args: {
182
201
  }
183
202
 
184
203
  for (let attempt = 0; attempt < attempts; attempt += 1) {
204
+ if (args.signal?.aborted) return null;
185
205
  const urls = authenticatedUrl
186
206
  ? [
187
207
  { url: authenticatedUrl, authenticated: true },