@agent-native/core 0.102.1 → 0.103.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (295) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +15 -0
  3. package/corpus/core/docs/content/local-file-mode.mdx +99 -489
  4. package/corpus/core/docs/content/locales/ar-SA/local-file-mode.mdx +35 -490
  5. package/corpus/core/docs/content/locales/ar-SA/template-content.mdx +10 -24
  6. package/corpus/core/docs/content/locales/de-DE/local-file-mode.mdx +37 -491
  7. package/corpus/core/docs/content/locales/de-DE/template-content.mdx +10 -24
  8. package/corpus/core/docs/content/locales/es-ES/local-file-mode.mdx +35 -493
  9. package/corpus/core/docs/content/locales/es-ES/template-content.mdx +10 -24
  10. package/corpus/core/docs/content/locales/fr-FR/local-file-mode.mdx +38 -491
  11. package/corpus/core/docs/content/locales/fr-FR/template-content.mdx +10 -24
  12. package/corpus/core/docs/content/locales/hi-IN/local-file-mode.mdx +33 -492
  13. package/corpus/core/docs/content/locales/hi-IN/template-content.mdx +10 -24
  14. package/corpus/core/docs/content/locales/ja-JP/local-file-mode.mdx +36 -492
  15. package/corpus/core/docs/content/locales/ja-JP/template-content.mdx +10 -24
  16. package/corpus/core/docs/content/locales/ko-KR/local-file-mode.mdx +35 -490
  17. package/corpus/core/docs/content/locales/ko-KR/template-content.mdx +10 -24
  18. package/corpus/core/docs/content/locales/pt-BR/local-file-mode.mdx +35 -493
  19. package/corpus/core/docs/content/locales/pt-BR/template-content.mdx +10 -24
  20. package/corpus/core/docs/content/locales/zh-CN/local-file-mode.mdx +31 -489
  21. package/corpus/core/docs/content/locales/zh-CN/template-content.mdx +9 -24
  22. package/corpus/core/docs/content/locales/zh-TW/local-file-mode.mdx +31 -489
  23. package/corpus/core/docs/content/locales/zh-TW/template-content.mdx +9 -24
  24. package/corpus/core/docs/content/template-content.mdx +26 -42
  25. package/corpus/core/package.json +1 -1
  26. package/corpus/core/src/cli/content-local.ts +86 -18
  27. package/corpus/core/src/cli/index.ts +2 -2
  28. package/corpus/core/src/cli/skills-content/content-skill.ts +36 -25
  29. package/corpus/core/src/cli/skills.ts +47 -13
  30. package/corpus/core/src/client/AgentPanel.tsx +7 -0
  31. package/corpus/core/src/client/AssistantChat.tsx +7 -0
  32. package/corpus/core/src/client/agent-chat-adapter.ts +7 -1
  33. package/corpus/core/src/client/chat/message-components.tsx +17 -3
  34. package/corpus/core/src/client/composer/ComposerPlusMenu.tsx +96 -6
  35. package/corpus/core/src/client/composer/asset-picker-url.ts +80 -0
  36. package/corpus/core/src/client/error-format.ts +24 -0
  37. package/corpus/core/src/client/resources/McpIntegrationDialog.tsx +8 -1
  38. package/corpus/core/src/local-artifacts/index.ts +65 -0
  39. package/corpus/templates/assets/app/components/library/GenerationPresetsPanel.tsx +325 -0
  40. package/corpus/templates/assets/app/hooks/use-navigation-state.ts +12 -0
  41. package/corpus/templates/assets/app/i18n/zh-TW.ts +21 -0
  42. package/corpus/templates/assets/app/i18n-data.ts +215 -0
  43. package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +19 -479
  44. package/corpus/templates/assets/app/routes/brand-kits.$id_.presets.$presetId.tsx +1 -1
  45. package/corpus/templates/assets/app/routes/brand-kits.$id_.settings.tsx +446 -0
  46. package/corpus/templates/assets/app/routes/library.tsx +69 -0
  47. package/corpus/templates/assets/changelog/2026-07-15-fixed-top-level-image-picker-selections-returning-to-the-com.md +6 -0
  48. package/corpus/templates/clips/actions/disconnect-calendar.ts +57 -9
  49. package/corpus/templates/clips/actions/generate-workflow.ts +102 -59
  50. package/corpus/templates/clips/actions/get-recording-player-data.ts +64 -1
  51. package/corpus/templates/clips/actions/regenerate-summary.ts +7 -0
  52. package/corpus/templates/clips/actions/regenerate-title.ts +29 -11
  53. package/corpus/templates/clips/app/components/editor/editor-layout.tsx +34 -13
  54. package/corpus/templates/clips/app/components/editor/thumbnail-picker.tsx +87 -9
  55. package/corpus/templates/clips/app/components/editor/trim-handles.tsx +15 -0
  56. package/corpus/templates/clips/app/components/editor/waveform.tsx +21 -8
  57. package/corpus/templates/clips/app/components/library/folder-tree.tsx +52 -0
  58. package/corpus/templates/clips/app/components/player/settings-panel.tsx +116 -58
  59. package/corpus/templates/clips/app/components/player/share-dialog.tsx +43 -37
  60. package/corpus/templates/clips/app/components/player/video-player.tsx +50 -2
  61. package/corpus/templates/clips/app/components/recorder/microphone-visualizer.tsx +22 -3
  62. package/corpus/templates/clips/app/i18n/ar-SA.ts +5 -2
  63. package/corpus/templates/clips/app/i18n/de-DE.ts +6 -2
  64. package/corpus/templates/clips/app/i18n/en-US.ts +5 -2
  65. package/corpus/templates/clips/app/i18n/es-ES.ts +6 -2
  66. package/corpus/templates/clips/app/i18n/fr-FR.ts +5 -2
  67. package/corpus/templates/clips/app/i18n/hi-IN.ts +5 -2
  68. package/corpus/templates/clips/app/i18n/ja-JP.ts +6 -1
  69. package/corpus/templates/clips/app/i18n/ko-KR.ts +5 -2
  70. package/corpus/templates/clips/app/i18n/pt-BR.ts +6 -2
  71. package/corpus/templates/clips/app/i18n/zh-CN.ts +5 -2
  72. package/corpus/templates/clips/app/i18n/zh-TW.ts +5 -2
  73. package/corpus/templates/clips/app/lib/timestamp-mapping.ts +14 -0
  74. package/corpus/templates/clips/app/routes/_app.library._index.tsx +1 -2
  75. package/corpus/templates/clips/app/routes/_index.tsx +1 -2
  76. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +98 -32
  77. package/corpus/templates/clips/app/routes/share.$shareId.tsx +4 -1
  78. package/corpus/templates/clips/changelog/2026-07-15-ai-workflow-generation-now-stays-visible-avoids-duplicate-re.md +6 -0
  79. package/corpus/templates/clips/changelog/2026-07-15-clip-playback-and-cta-controls-remain-interactive-through-ed.md +6 -0
  80. package/corpus/templates/clips/changelog/2026-07-15-clip-titles-no-longer-describe-the-product-as-a-loom-alterna.md +6 -0
  81. package/corpus/templates/clips/changelog/2026-07-15-description-regeneration-now-opens-the-agent-chat-so-progres.md +6 -0
  82. package/corpus/templates/clips/changelog/2026-07-15-direct-recording-urls-now-respect-share-link-and-password-ac.md +6 -0
  83. package/corpus/templates/clips/changelog/2026-07-15-disconnecting-a-calendar-now-clears-unrecorded-synced-meetin.md +6 -0
  84. package/corpus/templates/clips/changelog/2026-07-15-folders-now-expose-rename-and-delete-actions-directly-in-the.md +6 -0
  85. package/corpus/templates/clips/changelog/2026-07-15-recording-pages-now-explain-sign-in-access-and-avoid-showing.md +6 -0
  86. package/corpus/templates/clips/changelog/2026-07-15-the-clips-extension-quick-actions-toolbar-can-be-moved-out-o.md +6 -0
  87. package/corpus/templates/clips/changelog/2026-07-15-the-editor-now-skips-cut-sections-during-preview-and-keeps-t.md +6 -0
  88. package/corpus/templates/clips/changelog/2026-07-15-thumbnail-capture-now-updates-previews-while-scrubbing-and-l.md +6 -0
  89. package/corpus/templates/clips/changelog/2026-07-15-windows-permission-links-open-the-matching-settings-page.md +6 -0
  90. package/corpus/templates/clips/chrome-extension/src/content-script.ts +117 -2
  91. package/corpus/templates/clips/chrome-extension/src/overlay.ts +27 -0
  92. package/corpus/templates/clips/desktop/src-tauri/tauri.conf.json +1 -1
  93. package/corpus/templates/clips/server/lib/calendar-event-meetings.ts +139 -101
  94. package/corpus/templates/clips/server/lib/recording-page-access.ts +35 -0
  95. package/corpus/templates/clips/server/lib/share-password.ts +6 -3
  96. package/corpus/templates/content/.agents/skills/content/SKILL.md +26 -39
  97. package/corpus/templates/content/AGENTS.md +25 -30
  98. package/corpus/templates/content/actions/_builder-docs-client.ts +23 -0
  99. package/corpus/templates/content/actions/_content-database-source-adapters.ts +21 -3
  100. package/corpus/templates/content/actions/_content-files.ts +338 -0
  101. package/corpus/templates/content/actions/_content-space-access.ts +135 -0
  102. package/corpus/templates/content/actions/_content-spaces.ts +640 -0
  103. package/corpus/templates/content/actions/_database-source-utils.ts +90 -7
  104. package/corpus/templates/content/actions/_database-utils.ts +7 -1
  105. package/corpus/templates/content/actions/_local-file-documents.ts +0 -9
  106. package/corpus/templates/content/actions/_local-folder-source.ts +53 -0
  107. package/corpus/templates/content/actions/_property-utils.ts +9 -4
  108. package/corpus/templates/content/actions/add-database-item.ts +108 -65
  109. package/corpus/templates/content/actions/backfill-content-files.ts +22 -0
  110. package/corpus/templates/content/actions/connect-local-folder-source.ts +221 -0
  111. package/corpus/templates/content/actions/create-content-database.ts +129 -3
  112. package/corpus/templates/content/actions/create-document.ts +65 -55
  113. package/corpus/templates/content/actions/create-inline-content-database.ts +6 -1
  114. package/corpus/templates/content/actions/delete-content-database.ts +3 -0
  115. package/corpus/templates/content/actions/delete-document.ts +7 -11
  116. package/corpus/templates/content/actions/disconnect-local-folder-source.ts +175 -0
  117. package/corpus/templates/content/actions/duplicate-database-item.ts +10 -0
  118. package/corpus/templates/content/actions/duplicate-database-items.ts +13 -0
  119. package/corpus/templates/content/actions/edit-document.ts +2 -28
  120. package/corpus/templates/content/actions/ensure-content-spaces.ts +26 -0
  121. package/corpus/templates/content/actions/export-content-source.ts +47 -9
  122. package/corpus/templates/content/actions/get-document.ts +0 -14
  123. package/corpus/templates/content/actions/import-content-source.ts +43 -3
  124. package/corpus/templates/content/actions/list-content-spaces.ts +103 -0
  125. package/corpus/templates/content/actions/list-documents.ts +12 -9
  126. package/corpus/templates/content/actions/move-document.ts +3 -14
  127. package/corpus/templates/content/actions/pull-document.ts +0 -23
  128. package/corpus/templates/content/actions/remove-local-file-source.ts +50 -29
  129. package/corpus/templates/content/actions/resolve-local-folder-conflict.ts +442 -0
  130. package/corpus/templates/content/actions/reveal-local-source-file.ts +6 -17
  131. package/corpus/templates/content/actions/search-documents.ts +0 -34
  132. package/corpus/templates/content/actions/share-local-file-document.ts +25 -1
  133. package/corpus/templates/content/actions/submit-content-database-form.ts +12 -0
  134. package/corpus/templates/content/actions/sync-local-folder-source.ts +731 -0
  135. package/corpus/templates/content/actions/sync-manifest-local-folder-source.ts +123 -0
  136. package/corpus/templates/content/actions/update-content-database-view.ts +10 -1
  137. package/corpus/templates/content/actions/update-document.ts +0 -15
  138. package/corpus/templates/content/actions/view-screen.ts +2 -50
  139. package/corpus/templates/content/app/components/editor/SlashCommandMenu.tsx +76 -62
  140. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +70 -22
  141. package/corpus/templates/content/app/components/editor/database/grouping.ts +2 -1
  142. package/corpus/templates/content/app/components/editor/database/settings.tsx +23 -20
  143. package/corpus/templates/content/app/components/editor/database/shared.tsx +2 -0
  144. package/corpus/templates/content/app/components/editor/database/sidebar.tsx +330 -0
  145. package/corpus/templates/content/app/components/editor/database/types.ts +1 -0
  146. package/corpus/templates/content/app/components/editor/database/view-config.ts +3 -1
  147. package/corpus/templates/content/app/components/layout/Layout.tsx +25 -1
  148. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +209 -113
  149. package/corpus/templates/content/app/components/sidebar/select-content-space.ts +31 -0
  150. package/corpus/templates/content/app/hooks/use-content-database.ts +16 -0
  151. package/corpus/templates/content/app/hooks/use-content-spaces.ts +39 -0
  152. package/corpus/templates/content/app/i18n/zh-TW.ts +11 -0
  153. package/corpus/templates/content/app/i18n-data.ts +75 -0
  154. package/corpus/templates/content/app/routes/_app.local-files.tsx +174 -65
  155. package/corpus/templates/content/changelog/2026-07-16-content-callouts-can-now-be-reviewed-and-published-to-builder.md +6 -0
  156. package/corpus/templates/content/changelog/2026-07-16-heading-5-and-heading-6-are-available-from-the-editor-slash-.md +6 -0
  157. package/corpus/templates/content/changelog/2026-07-16-organize-every-workspace-through-a-customizable-files-databa.md +6 -0
  158. package/corpus/templates/content/parity/matrix.md +2 -1
  159. package/corpus/templates/content/parity/matrix.ts +37 -1
  160. package/corpus/templates/content/scripts/dev-database.mjs +7 -5
  161. package/corpus/templates/content/server/db/schema.ts +113 -40
  162. package/corpus/templates/content/server/lib/document-context.ts +15 -1
  163. package/corpus/templates/content/server/lib/notion-sync.ts +6 -0
  164. package/corpus/templates/content/server/plugins/db.ts +153 -0
  165. package/corpus/templates/content/shared/api.ts +17 -3
  166. package/corpus/templates/content/shared/builder-mdx.ts +163 -16
  167. package/corpus/templates/design/.agents/skills/design-templates/SKILL.md +59 -0
  168. package/corpus/templates/design/AGENTS.md +6 -1
  169. package/corpus/templates/design/actions/create-design-from-template.ts +19 -7
  170. package/corpus/templates/design/actions/list-design-templates.ts +69 -21
  171. package/corpus/templates/design/actions/navigate.ts +2 -2
  172. package/corpus/templates/design/actions/view-screen.ts +15 -2
  173. package/corpus/templates/design/agent-native.app-skill.json +5 -0
  174. package/corpus/templates/design/app/components/design/design-canvas/external-preview.ts +7 -3
  175. package/corpus/templates/design/app/components/editor/PromptDialog.tsx +281 -56
  176. package/corpus/templates/design/app/components/templates/TemplatePreview.tsx +14 -2
  177. package/corpus/templates/design/app/hooks/use-navigation-state.ts +3 -2
  178. package/corpus/templates/design/app/i18n/zh-TW.ts +3 -3
  179. package/corpus/templates/design/app/i18n-data.ts +21 -2
  180. package/corpus/templates/design/app/i18n-template-feature.ts +218 -10
  181. package/corpus/templates/design/app/pages/Index.tsx +195 -24
  182. package/corpus/templates/design/app/pages/Templates.tsx +144 -55
  183. package/corpus/templates/design/app/pages/design-editor/generation-prompt-directives.ts +12 -1
  184. package/corpus/templates/design/changelog/2026-07-14-templates-now-appear-in-the-new-design-picker-with-previews-.md +6 -0
  185. package/corpus/templates/design/changelog/2026-07-15-prototype-print-and-download-buttons-now-work-in-preview.md +6 -0
  186. package/corpus/templates/design/changelog/2026-07-15-templates-now-open-immediately-after-copying-even-while-the-.md +6 -0
  187. package/corpus/templates/design/server/plugins/agent-chat.ts +1 -1
  188. package/corpus/templates/mail/AGENTS.md +23 -18
  189. package/corpus/templates/mail/actions/bulk-archive.ts +32 -28
  190. package/corpus/templates/mail/actions/mark-read.ts +131 -20
  191. package/corpus/templates/mail/actions/mark-thread-read.ts +1 -1
  192. package/corpus/templates/mail/actions/move-email.ts +30 -24
  193. package/corpus/templates/mail/actions/send-email.ts +73 -68
  194. package/corpus/templates/mail/changelog/2026-07-15-unread-inbox-cleanup-now-completes-reliably.md +6 -0
  195. package/corpus/templates/mail/server/handlers/emails.ts +218 -209
  196. package/corpus/templates/mail/server/lib/bulk-mark-read.ts +30 -0
  197. package/corpus/templates/mail/server/lib/email-state.ts +198 -113
  198. package/corpus/templates/mail/server/lib/google-auth.ts +218 -25
  199. package/corpus/templates/mail/server/lib/jobs.ts +69 -74
  200. package/corpus/templates/mail/server/lib/local-email-store.ts +265 -0
  201. package/corpus/templates/mail/server/plugins/agent-chat.ts +6 -0
  202. package/corpus/templates/slides/actions/extract-pdf.ts +1 -1
  203. package/corpus/templates/slides/actions/import-file.ts +4 -1
  204. package/corpus/templates/slides/app/components/editor/ImageGenPanel.tsx +1 -1
  205. package/corpus/templates/slides/app/components/editor/ImageSearchPanel.tsx +9 -12
  206. package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +29 -21
  207. package/corpus/templates/slides/app/i18n/ar-SA.ts +1 -0
  208. package/corpus/templates/slides/app/i18n/de-DE.ts +1 -0
  209. package/corpus/templates/slides/app/i18n/en-US.ts +1 -0
  210. package/corpus/templates/slides/app/i18n/es-ES.ts +1 -0
  211. package/corpus/templates/slides/app/i18n/fr-FR.ts +1 -0
  212. package/corpus/templates/slides/app/i18n/hi-IN.ts +1 -0
  213. package/corpus/templates/slides/app/i18n/ja-JP.ts +1 -0
  214. package/corpus/templates/slides/app/i18n/ko-KR.ts +1 -0
  215. package/corpus/templates/slides/app/i18n/pt-BR.ts +1 -0
  216. package/corpus/templates/slides/app/i18n/zh-CN.ts +1 -0
  217. package/corpus/templates/slides/app/i18n/zh-TW.ts +1 -0
  218. package/corpus/templates/slides/changelog/2026-07-15-image-search-and-generation-controls-now-use-the-registered-.md +6 -0
  219. package/corpus/templates/slides/changelog/2026-07-15-pdf-deck-references-can-be-imported-for-slide-inspiration-ag.md +6 -0
  220. package/corpus/templates/slides/changelog/2026-07-15-stable-style-inspector-dock.md +6 -0
  221. package/dist/cli/content-local.d.ts +1 -0
  222. package/dist/cli/content-local.d.ts.map +1 -1
  223. package/dist/cli/content-local.js +75 -17
  224. package/dist/cli/content-local.js.map +1 -1
  225. package/dist/cli/index.js +2 -2
  226. package/dist/cli/index.js.map +1 -1
  227. package/dist/cli/skills-content/content-skill.d.ts +1 -1
  228. package/dist/cli/skills-content/content-skill.d.ts.map +1 -1
  229. package/dist/cli/skills-content/content-skill.js +36 -25
  230. package/dist/cli/skills-content/content-skill.js.map +1 -1
  231. package/dist/cli/skills.d.ts.map +1 -1
  232. package/dist/cli/skills.js +47 -14
  233. package/dist/cli/skills.js.map +1 -1
  234. package/dist/client/AgentPanel.d.ts.map +1 -1
  235. package/dist/client/AgentPanel.js +5 -0
  236. package/dist/client/AgentPanel.js.map +1 -1
  237. package/dist/client/AssistantChat.d.ts.map +1 -1
  238. package/dist/client/AssistantChat.js +7 -0
  239. package/dist/client/AssistantChat.js.map +1 -1
  240. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  241. package/dist/client/agent-chat-adapter.js +8 -1
  242. package/dist/client/agent-chat-adapter.js.map +1 -1
  243. package/dist/client/chat/message-components.d.ts +4 -0
  244. package/dist/client/chat/message-components.d.ts.map +1 -1
  245. package/dist/client/chat/message-components.js +10 -3
  246. package/dist/client/chat/message-components.js.map +1 -1
  247. package/dist/client/composer/ComposerPlusMenu.d.ts.map +1 -1
  248. package/dist/client/composer/ComposerPlusMenu.js +58 -6
  249. package/dist/client/composer/ComposerPlusMenu.js.map +1 -1
  250. package/dist/client/composer/asset-picker-url.d.ts +16 -0
  251. package/dist/client/composer/asset-picker-url.d.ts.map +1 -0
  252. package/dist/client/composer/asset-picker-url.js +64 -0
  253. package/dist/client/composer/asset-picker-url.js.map +1 -0
  254. package/dist/client/error-format.d.ts.map +1 -1
  255. package/dist/client/error-format.js +17 -0
  256. package/dist/client/error-format.js.map +1 -1
  257. package/dist/client/resources/McpIntegrationDialog.d.ts.map +1 -1
  258. package/dist/client/resources/McpIntegrationDialog.js +6 -1
  259. package/dist/client/resources/McpIntegrationDialog.js.map +1 -1
  260. package/dist/collab/awareness.d.ts +2 -2
  261. package/dist/collab/awareness.d.ts.map +1 -1
  262. package/dist/collab/struct-routes.d.ts +1 -1
  263. package/dist/local-artifacts/index.d.ts +10 -0
  264. package/dist/local-artifacts/index.d.ts.map +1 -1
  265. package/dist/local-artifacts/index.js +39 -0
  266. package/dist/local-artifacts/index.js.map +1 -1
  267. package/dist/notifications/routes.d.ts +1 -1
  268. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  269. package/dist/resources/handlers.d.ts +3 -3
  270. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  271. package/dist/server/transcribe-voice.d.ts +1 -1
  272. package/docs/content/local-file-mode.mdx +99 -489
  273. package/docs/content/locales/ar-SA/local-file-mode.mdx +35 -490
  274. package/docs/content/locales/ar-SA/template-content.mdx +10 -24
  275. package/docs/content/locales/de-DE/local-file-mode.mdx +37 -491
  276. package/docs/content/locales/de-DE/template-content.mdx +10 -24
  277. package/docs/content/locales/es-ES/local-file-mode.mdx +35 -493
  278. package/docs/content/locales/es-ES/template-content.mdx +10 -24
  279. package/docs/content/locales/fr-FR/local-file-mode.mdx +38 -491
  280. package/docs/content/locales/fr-FR/template-content.mdx +10 -24
  281. package/docs/content/locales/hi-IN/local-file-mode.mdx +33 -492
  282. package/docs/content/locales/hi-IN/template-content.mdx +10 -24
  283. package/docs/content/locales/ja-JP/local-file-mode.mdx +36 -492
  284. package/docs/content/locales/ja-JP/template-content.mdx +10 -24
  285. package/docs/content/locales/ko-KR/local-file-mode.mdx +35 -490
  286. package/docs/content/locales/ko-KR/template-content.mdx +10 -24
  287. package/docs/content/locales/pt-BR/local-file-mode.mdx +35 -493
  288. package/docs/content/locales/pt-BR/template-content.mdx +10 -24
  289. package/docs/content/locales/zh-CN/local-file-mode.mdx +31 -489
  290. package/docs/content/locales/zh-CN/template-content.mdx +9 -24
  291. package/docs/content/locales/zh-TW/local-file-mode.mdx +31 -489
  292. package/docs/content/locales/zh-TW/template-content.mdx +9 -24
  293. package/docs/content/template-content.mdx +26 -42
  294. package/package.json +1 -1
  295. package/corpus/templates/assets/app/components/library/EditLibraryDialog.tsx +0 -126
@@ -15,7 +15,10 @@
15
15
  */
16
16
 
17
17
  import { defineAction } from "@agent-native/core";
18
- import { writeAppState } from "@agent-native/core/application-state";
18
+ import {
19
+ readAppState,
20
+ writeAppState,
21
+ } from "@agent-native/core/application-state";
19
22
  import { assertAccess } from "@agent-native/core/sharing";
20
23
  import { eq } from "drizzle-orm";
21
24
  import { z } from "zod";
@@ -51,6 +54,11 @@ Output markdown.`,
51
54
  Keep it concise, warm, and professional.`,
52
55
  } as const;
53
56
 
57
+ // Actions for the same recording can arrive concurrently when the player and
58
+ // agent both retry a request. Keep the read-check-write/enqueue sequence
59
+ // single-flight within this runtime so only one request can claim generation.
60
+ const workflowGenerationLocks = new Set<string>();
61
+
54
62
  export default defineAction({
55
63
  description:
56
64
  "Ask the agent to generate a structured workflow doc (pr/sop/ticket/email) from this recording's transcript (and the full video when Include full video is enabled). The agent writes the result to clips-workflow-<recordingId> in application_state.",
@@ -61,71 +69,106 @@ export default defineAction({
61
69
  run: async (args) => {
62
70
  await assertAccess("recording", args.recordingId, "viewer");
63
71
 
64
- const db = getDb();
65
- const [rec] = await db
66
- .select()
67
- .from(schema.recordings)
68
- .where(eq(schema.recordings.id, args.recordingId))
69
- .limit(1);
70
- if (!rec) throw new Error(`Recording not found: ${args.recordingId}`);
72
+ const stateKey = `clips-workflow-${args.recordingId}`;
73
+ if (workflowGenerationLocks.has(stateKey)) {
74
+ return {
75
+ queued: false,
76
+ duplicate: true,
77
+ recordingId: args.recordingId,
78
+ kind: args.kind,
79
+ stateKey,
80
+ };
81
+ }
82
+ workflowGenerationLocks.add(stateKey);
71
83
 
72
- const [transcript] = await db
73
- .select()
74
- .from(schema.recordingTranscripts)
75
- .where(eq(schema.recordingTranscripts.recordingId, args.recordingId))
76
- .limit(1);
84
+ try {
85
+ const db = getDb();
86
+ const [rec] = await db
87
+ .select()
88
+ .from(schema.recordings)
89
+ .where(eq(schema.recordings.id, args.recordingId))
90
+ .limit(1);
91
+ if (!rec) throw new Error(`Recording not found: ${args.recordingId}`);
77
92
 
78
- const stateKey = `clips-workflow-${args.recordingId}`;
79
- const includeFullVideoInAi = await readIncludeFullVideoInAi();
93
+ const [transcript] = await db
94
+ .select()
95
+ .from(schema.recordingTranscripts)
96
+ .where(eq(schema.recordingTranscripts.recordingId, args.recordingId))
97
+ .limit(1);
80
98
 
81
- // Seed the output state with a "generating" placeholder so the UI can show
82
- // a loading state immediately.
83
- await writeAppState(stateKey, {
84
- kind: args.kind,
85
- status: "generating",
86
- recordingId: args.recordingId,
87
- requestedAt: new Date().toISOString(),
88
- } as any);
99
+ const includeFullVideoInAi = await readIncludeFullVideoInAi();
89
100
 
90
- const baseMessage =
91
- `Generate a ${args.kind.toUpperCase()} workflow document from recording ${args.recordingId} ` +
92
- `(title: "${rec.title}"). Read the transcript from this request's context. ` +
93
- `${KIND_PROMPTS[args.kind]} ` +
94
- `Then write the final markdown to application_state key "${stateKey}" as ` +
95
- `\`{ kind: "${args.kind}", status: "ready", content: "...", recordingId: "${args.recordingId}" }\`. ` +
96
- `Finish by replying in chat with the same generated markdown so the user can read it immediately.`;
101
+ const existing = await readAppState(stateKey).catch(() => null);
102
+ if (existing?.status === "generating") {
103
+ const requestedAt = Date.parse(String(existing.requestedAt ?? ""));
104
+ const isRecent =
105
+ !Number.isFinite(requestedAt) ||
106
+ Date.now() - requestedAt < 10 * 60_000;
107
+ if (isRecent) {
108
+ return {
109
+ queued: false,
110
+ duplicate: true,
111
+ recordingId: args.recordingId,
112
+ kind: existing.kind ?? args.kind,
113
+ stateKey,
114
+ };
115
+ }
116
+ }
97
117
 
98
- const request = {
99
- kind: "generate-workflow" as const,
100
- workflowKind: args.kind,
101
- recordingId: args.recordingId,
102
- requestedAt: new Date().toISOString(),
103
- recordingTitle: rec.title,
104
- recordingDescription: rec.description,
105
- transcriptStatus: transcript?.status ?? "pending",
106
- transcriptText: transcript?.fullText ?? "",
107
- stateKey,
108
- instructions: KIND_PROMPTS[args.kind],
109
- includeFullVideoInAi,
110
- message: withFullVideoAiInstructions(
111
- baseMessage,
112
- args.recordingId,
118
+ // Seed the output state with a "generating" placeholder so the UI can show
119
+ // a loading state immediately.
120
+ await writeAppState(stateKey, {
121
+ kind: args.kind,
122
+ status: "generating",
123
+ recordingId: args.recordingId,
124
+ requestedAt: new Date().toISOString(),
125
+ } as any);
126
+
127
+ const baseMessage =
128
+ `Generate a ${args.kind.toUpperCase()} workflow document from recording ${args.recordingId} ` +
129
+ `(title: "${rec.title}"). Read the transcript from this request's context. ` +
130
+ `${KIND_PROMPTS[args.kind]} ` +
131
+ `Then write the final markdown to application_state key "${stateKey}" as ` +
132
+ `\`{ kind: "${args.kind}", status: "ready", content: "...", recordingId: "${args.recordingId}" }\`. ` +
133
+ `Finish by replying in chat with the same generated markdown so the user can read it immediately.`;
134
+
135
+ const request = {
136
+ kind: "generate-workflow" as const,
137
+ workflowKind: args.kind,
138
+ recordingId: args.recordingId,
139
+ requestedAt: new Date().toISOString(),
140
+ recordingTitle: rec.title,
141
+ recordingDescription: rec.description,
142
+ transcriptStatus: transcript?.status ?? "pending",
143
+ transcriptText: transcript?.fullText ?? "",
144
+ stateKey,
145
+ instructions: KIND_PROMPTS[args.kind],
113
146
  includeFullVideoInAi,
114
- ),
115
- };
147
+ message: withFullVideoAiInstructions(
148
+ baseMessage,
149
+ args.recordingId,
150
+ includeFullVideoInAi,
151
+ ),
152
+ };
116
153
 
117
- await writeAppState(`clips-ai-request-${args.recordingId}`, request as any);
118
- await writeAppState("refresh-signal", { ts: Date.now() });
154
+ await writeAppState(
155
+ `clips-ai-request-${args.recordingId}`,
156
+ request as any,
157
+ );
158
+ await writeAppState("refresh-signal", { ts: Date.now() });
119
159
 
120
- console.log(
121
- `Delegation queued: generate-workflow (${args.kind}) for ${args.recordingId}`,
122
- );
123
- return {
124
- queued: true,
125
- recordingId: args.recordingId,
126
- kind: args.kind,
127
- stateKey,
128
- includeFullVideoInAi,
129
- };
160
+ console.log(
161
+ `Delegation queued: generate-workflow (${args.kind}) for ${args.recordingId}`,
162
+ );
163
+ return {
164
+ queued: true,
165
+ recordingId: args.recordingId,
166
+ kind: args.kind,
167
+ stateKey,
168
+ includeFullVideoInAi,
169
+ };
170
+ } finally {
171
+ workflowGenerationLocks.delete(stateKey);
172
+ }
130
173
  },
131
174
  });
@@ -20,12 +20,20 @@
20
20
  import { defineAction, embedApp } from "@agent-native/core";
21
21
  import { readAppState } from "@agent-native/core/application-state";
22
22
  import { buildDeepLink } from "@agent-native/core/server";
23
+ import {
24
+ getRequestOrgId,
25
+ getRequestUserEmail,
26
+ } from "@agent-native/core/server/request-context";
23
27
  import { resolveAccess, ForbiddenError } from "@agent-native/core/sharing";
24
- import { asc, eq } from "drizzle-orm";
28
+ import { and, asc, eq, or, sql } from "drizzle-orm";
25
29
  import { z } from "zod";
26
30
 
27
31
  import { getDb, schema } from "../server/db/index.js";
28
32
  import { resolvePlayerVideoUrl } from "../server/lib/player-video-url.js";
33
+ import {
34
+ canOpenDirectRecordingPage,
35
+ isRecordingExpired,
36
+ } from "../server/lib/recording-page-access.js";
29
37
  import { parseSpaceIds } from "../server/lib/recordings.js";
30
38
  import { parseBrowserDiagnosticsRow } from "../shared/browser-diagnostics.js";
31
39
  import {
@@ -106,6 +114,61 @@ export default defineAction({
106
114
  const db = getDb();
107
115
  const rec: any = access.resource;
108
116
 
117
+ if (isRecordingExpired(rec.expiresAt)) {
118
+ throw new ForbiddenError("Recording has expired");
119
+ }
120
+
121
+ let hasExplicitShare = access.role === "owner";
122
+ if (rec.visibility === "public" && access.role !== "owner") {
123
+ const userEmail = getRequestUserEmail()?.trim().toLowerCase();
124
+ const orgId = getRequestOrgId();
125
+ const principals = [];
126
+ if (userEmail) {
127
+ principals.push(
128
+ and(
129
+ eq(schema.recordingShares.principalType, "user"),
130
+ sql`lower(${schema.recordingShares.principalId}) = ${userEmail}`,
131
+ ),
132
+ );
133
+ }
134
+ if (orgId) {
135
+ principals.push(
136
+ and(
137
+ eq(schema.recordingShares.principalType, "org"),
138
+ eq(schema.recordingShares.principalId, orgId),
139
+ ),
140
+ );
141
+ }
142
+ if (principals.length > 0) {
143
+ const [share] = await db
144
+ .select({ id: schema.recordingShares.id })
145
+ .from(schema.recordingShares)
146
+ .where(
147
+ and(
148
+ eq(schema.recordingShares.resourceId, args.recordingId),
149
+ or(...principals),
150
+ ),
151
+ )
152
+ .limit(1);
153
+ hasExplicitShare = Boolean(share);
154
+ } else {
155
+ hasExplicitShare = false;
156
+ }
157
+ }
158
+
159
+ if (
160
+ !canOpenDirectRecordingPage({
161
+ role: access.role,
162
+ visibility: rec.visibility,
163
+ hasPassword: Boolean(rec.password),
164
+ hasExplicitShare,
165
+ })
166
+ ) {
167
+ throw new ForbiddenError(
168
+ "Open this recording from its share link instead of the direct recording URL",
169
+ );
170
+ }
171
+
109
172
  const [transcript] = await db
110
173
  .select()
111
174
  .from(schema.recordingTranscripts)
@@ -22,6 +22,12 @@ export default defineAction({
22
22
  "Ask the agent to regenerate this recording's description/summary based on its transcript (and the full video when Include full video is enabled).",
23
23
  schema: z.object({
24
24
  recordingId: z.string().describe("Recording ID"),
25
+ openInChat: z
26
+ .boolean()
27
+ .optional()
28
+ .describe(
29
+ "When true, focus the queued generation request in the agent chat instead of keeping it hidden.",
30
+ ),
25
31
  }),
26
32
  run: async (args) => {
27
33
  await assertAccess("recording", args.recordingId, "editor");
@@ -74,6 +80,7 @@ export default defineAction({
74
80
  transcriptStatus: transcript?.status ?? "pending",
75
81
  transcriptText: transcript?.fullText ?? "",
76
82
  includeFullVideoInAi,
83
+ openInChat: args.openInChat === true,
77
84
  message: withFullVideoAiInstructions(
78
85
  baseMessage,
79
86
  args.recordingId,
@@ -59,7 +59,9 @@ function buildTitleContext({
59
59
  }): string | undefined {
60
60
  const parts: string[] = [];
61
61
  if (currentTitle && !isDefaultTitle(currentTitle)) {
62
- parts.push(`Current title: ${currentTitle}`);
62
+ parts.push(
63
+ `Current title: ${currentTitle}\nWhen regenerating, do not return this title verbatim; choose a more specific alternative when the transcript supports one.`,
64
+ );
63
65
  }
64
66
  if (agentsContext) parts.push(agentsContext);
65
67
  return parts.length > 0 ? parts.join("\n\n") : undefined;
@@ -237,9 +239,14 @@ export default defineAction({
237
239
 
238
240
  if (!fresh) throw new Error(`Recording not found: ${args.recordingId}`);
239
241
 
242
+ const titleMatchesCurrent =
243
+ fresh.title?.trim().toLocaleLowerCase() ===
244
+ generatedTitle.trim().toLocaleLowerCase();
245
+
240
246
  if (
241
- isAutoTitleReplaceable(fresh.title, fresh.titleSource) ||
242
- fresh.title === rec.title
247
+ !titleMatchesCurrent &&
248
+ (isAutoTitleReplaceable(fresh.title, fresh.titleSource) ||
249
+ fresh.title === rec.title)
243
250
  ) {
244
251
  await db
245
252
  .update(schema.recordings)
@@ -272,12 +279,18 @@ export default defineAction({
272
279
  };
273
280
  }
274
281
 
275
- return {
276
- updated: false,
277
- skipped: true,
278
- reason: "Recording title changed before generation completed",
279
- recordingId: args.recordingId,
280
- };
282
+ if (!titleMatchesCurrent) {
283
+ return {
284
+ updated: false,
285
+ skipped: true,
286
+ reason: "Recording title changed before generation completed",
287
+ recordingId: args.recordingId,
288
+ };
289
+ }
290
+
291
+ console.warn(
292
+ `[clips] AI title regeneration returned the existing title for ${args.recordingId}; queueing a refinement instead`,
293
+ );
281
294
  }
282
295
  } catch (err) {
283
296
  builderCreditsPaused = isBuilderCreditsExhaustedMessage(
@@ -302,9 +315,14 @@ export default defineAction({
302
315
 
303
316
  if (!fresh) throw new Error(`Recording not found: ${args.recordingId}`);
304
317
 
318
+ const titleMatchesCurrent =
319
+ fresh.title?.trim().toLocaleLowerCase() ===
320
+ fallbackTitle.trim().toLocaleLowerCase();
321
+
305
322
  if (
306
- isAutoTitleReplaceable(fresh.title, fresh.titleSource) ||
307
- fresh.title === rec.title
323
+ !titleMatchesCurrent &&
324
+ (isAutoTitleReplaceable(fresh.title, fresh.titleSource) ||
325
+ fresh.title === rec.title)
308
326
  ) {
309
327
  await db
310
328
  .update(schema.recordings)
@@ -63,6 +63,7 @@ import {
63
63
  parseEdits,
64
64
  getExcludedRanges,
65
65
  formatMs,
66
+ skipExcludedRange,
66
67
  type EditsJson,
67
68
  } from "@/lib/timestamp-mapping";
68
69
  import { cn } from "@/lib/utils";
@@ -414,13 +415,20 @@ export function EditorLayout({ recordingId, className }: EditorLayoutProps) {
414
415
  if (!recording?.id) return;
415
416
  const next = readPlaybackSpeedPreference(defaultPreviewSpeed);
416
417
  setPlaybackSpeed(next);
417
- if (videoRef.current) videoRef.current.playbackRate = next;
418
+ if (videoRef.current) {
419
+ videoRef.current.defaultPlaybackRate = next;
420
+ videoRef.current.playbackRate = next;
421
+ }
418
422
  }, [defaultPreviewSpeed, recording?.id]);
419
423
 
420
424
  // Keep the editor preview speed visible and in sync with the media element.
425
+ // `defaultPlaybackRate` is set too so a `videoUrl` source swap that resets
426
+ // `playbackRate` (some browsers do this on load) falls back to the chosen
427
+ // speed instead of 1x.
421
428
  useEffect(() => {
422
429
  const v = videoRef.current;
423
430
  if (!v) return;
431
+ v.defaultPlaybackRate = playbackSpeed;
424
432
  v.playbackRate = playbackSpeed;
425
433
  }, [playbackSpeed, videoUrl]);
426
434
 
@@ -428,17 +436,25 @@ export function EditorLayout({ recordingId, className }: EditorLayoutProps) {
428
436
  const next = parsePlaybackSpeed(rate) ?? 1.2;
429
437
  setPlaybackSpeed(next);
430
438
  savePlaybackSpeedPreference(next);
431
- if (videoRef.current) videoRef.current.playbackRate = next;
439
+ if (videoRef.current) {
440
+ videoRef.current.defaultPlaybackRate = next;
441
+ videoRef.current.playbackRate = next;
442
+ }
432
443
  }, []);
433
444
 
434
445
  // Keep the playheadMs in sync with the element's currentTime.
435
446
  useEffect(() => {
436
447
  const v = videoRef.current;
437
448
  if (!v) return;
438
- const onTime = () => setPlayheadMs(v.currentTime * 1000);
449
+ const onTime = () => {
450
+ const rawMs = v.currentTime * 1000;
451
+ const visibleMs = skipExcludedRange(rawMs, excludedRanges, durationMs);
452
+ if (visibleMs !== rawMs) v.currentTime = visibleMs / 1000;
453
+ setPlayheadMs(visibleMs);
454
+ };
439
455
  v.addEventListener("timeupdate", onTime);
440
456
  return () => v.removeEventListener("timeupdate", onTime);
441
- }, [videoUrl]);
457
+ }, [durationMs, excludedRanges, videoUrl]);
442
458
 
443
459
  // Expose the in-editor state so the agent can read "the user is editing and scrubbed to X".
444
460
  useEffect(() => {
@@ -510,11 +526,15 @@ export function EditorLayout({ recordingId, className }: EditorLayoutProps) {
510
526
  [recordingId, trim],
511
527
  );
512
528
 
513
- const seek = useCallback((ms: number) => {
514
- const v = videoRef.current;
515
- if (v) v.currentTime = ms / 1000;
516
- setPlayheadMs(ms);
517
- }, []);
529
+ const seek = useCallback(
530
+ (ms: number) => {
531
+ const visibleMs = skipExcludedRange(ms, excludedRanges, durationMs);
532
+ const v = videoRef.current;
533
+ if (v) v.currentTime = visibleMs / 1000;
534
+ setPlayheadMs(visibleMs);
535
+ },
536
+ [durationMs, excludedRanges],
537
+ );
518
538
 
519
539
  // --- keyboard shortcuts -------------------------------------------------
520
540
  useEffect(() => {
@@ -585,8 +605,8 @@ export function EditorLayout({ recordingId, className }: EditorLayoutProps) {
585
605
 
586
606
  // Default selection window so the TrimHandles have something to render.
587
607
  const effectiveSelection = selectionRange ?? {
588
- startMs: Math.max(0, playheadMs - 1000),
589
- endMs: Math.min(durationMs || 1_000, playheadMs + 1000),
608
+ startMs: 0,
609
+ endMs: durationMs || 1_000,
590
610
  };
591
611
 
592
612
  if (playerDataQuery.isLoading) {
@@ -684,7 +704,8 @@ export function EditorLayout({ recordingId, className }: EditorLayoutProps) {
684
704
  playheadMs={playheadMs}
685
705
  durationMs={durationMs}
686
706
  excludedRanges={excludedRanges}
687
- selectionRange={selectionRange}
707
+ selectionRange={effectiveSelection}
708
+ splitPoints={splitPoints}
688
709
  activityRanges={transcriptSegments}
689
710
  onSeek={seek}
690
711
  scrollLeft={scrollLeft}
@@ -707,6 +728,7 @@ export function EditorLayout({ recordingId, className }: EditorLayoutProps) {
707
728
  value={effectiveSelection}
708
729
  onChange={setSelectionRange}
709
730
  durationMs={durationMs}
731
+ splitPoints={splitPoints}
710
732
  />
711
733
  </div>
712
734
  </div>
@@ -727,7 +749,6 @@ export function EditorLayout({ recordingId, className }: EditorLayoutProps) {
727
749
  durationMs={durationMs}
728
750
  playheadMs={playheadMs}
729
751
  chapters={chapters}
730
- excludedRanges={excludedRanges}
731
752
  splitPoints={splitPoints}
732
753
  onSeek={seek}
733
754
  onClickChapter={(c) => seek(c.startMs)}
@@ -4,6 +4,7 @@ import {
4
4
  IconPhotoEdit,
5
5
  IconUpload,
6
6
  IconLoader2,
7
+ IconX,
7
8
  } from "@tabler/icons-react";
8
9
  import { useEffect, useRef, useState } from "react";
9
10
  import { toast } from "sonner";
@@ -55,6 +56,10 @@ export function ThumbnailPicker({
55
56
  const [gifDataUrl, setGifDataUrl] = useState<string | null>(null);
56
57
 
57
58
  const videoRef = useRef<HTMLVideoElement | null>(null);
59
+ const framePreviewRef = useRef<HTMLVideoElement | null>(null);
60
+ const gifVideoRef = useRef<HTMLVideoElement | null>(null);
61
+ const gifPreviewRef = useRef<HTMLVideoElement | null>(null);
62
+ const uploadInputRef = useRef<HTMLInputElement | null>(null);
58
63
 
59
64
  const mutation = useActionMutation("set-thumbnail");
60
65
 
@@ -65,6 +70,7 @@ export function ThumbnailPicker({
65
70
  setFrameDataUrl(null);
66
71
  setGifDataUrl(null);
67
72
  setGifProgress(null);
73
+ if (uploadInputRef.current) uploadInputRef.current.value = "";
68
74
  }
69
75
  }, [open]);
70
76
 
@@ -98,6 +104,18 @@ export function ThumbnailPicker({
98
104
  }
99
105
  };
100
106
 
107
+ const seekPreview = (video: HTMLVideoElement | null, timeMs: number) => {
108
+ if (!video || !Number.isFinite(timeMs)) return;
109
+ const apply = () => {
110
+ video.currentTime = Math.max(0, timeMs) / 1000;
111
+ };
112
+ if (video.readyState === HTMLMediaElement.HAVE_NOTHING) {
113
+ video.addEventListener("loadedmetadata", apply, { once: true });
114
+ } else {
115
+ apply();
116
+ }
117
+ };
118
+
101
119
  const handleGifGenerate = async () => {
102
120
  if (!videoUrl) return;
103
121
  try {
@@ -187,6 +205,7 @@ export function ThumbnailPicker({
187
205
  <TabsContent value="upload" className="py-4 space-y-3">
188
206
  <Label>{t("thumbnailPicker.uploadImage")}</Label>
189
207
  <input
208
+ ref={uploadInputRef}
190
209
  type="file"
191
210
  accept="image/*"
192
211
  onChange={(e) => {
@@ -196,14 +215,39 @@ export function ThumbnailPicker({
196
215
  reader.onload = () => setUploadDataUrl(reader.result as string);
197
216
  reader.readAsDataURL(file);
198
217
  }}
199
- className="text-sm"
218
+ className="sr-only"
200
219
  />
220
+ <Button
221
+ type="button"
222
+ variant="secondary"
223
+ size="sm"
224
+ onClick={() => uploadInputRef.current?.click()}
225
+ >
226
+ <IconUpload className="mr-1 h-4 w-4" />
227
+ {t("shareDialog.chooseFile")}
228
+ </Button>
201
229
  {uploadDataUrl ? (
202
- <img
203
- src={uploadDataUrl}
204
- alt={t("thumbnailPicker.uploadedPreview")}
205
- className="max-h-60 rounded border border-border"
206
- />
230
+ <div className="relative w-fit">
231
+ <img
232
+ src={uploadDataUrl}
233
+ alt={t("thumbnailPicker.uploadedPreview")}
234
+ className="max-h-60 rounded border border-border"
235
+ />
236
+ <Button
237
+ type="button"
238
+ variant="secondary"
239
+ size="icon"
240
+ className="absolute right-1 top-1 h-7 w-7"
241
+ aria-label={t("shareDialog.remove")}
242
+ onClick={() => {
243
+ setUploadDataUrl(null);
244
+ if (uploadInputRef.current)
245
+ uploadInputRef.current.value = "";
246
+ }}
247
+ >
248
+ <IconX className="h-4 w-4" />
249
+ </Button>
250
+ </div>
207
251
  ) : currentThumbnailUrl ? (
208
252
  <img
209
253
  src={currentThumbnailUrl}
@@ -235,7 +279,13 @@ export function ThumbnailPicker({
235
279
  max={Math.max(1000, durationMs)}
236
280
  step={100}
237
281
  value={[frameTime]}
238
- onValueChange={([v]) => setFrameTime(v)}
282
+ onValueChange={([v]) => {
283
+ const next = v ?? 0;
284
+ setFrameTime(next);
285
+ setFrameDataUrl(null);
286
+ seekPreview(videoRef.current, next);
287
+ seekPreview(framePreviewRef.current, next);
288
+ }}
239
289
  />
240
290
  <Button
241
291
  size="sm"
@@ -256,6 +306,15 @@ export function ThumbnailPicker({
256
306
  alt={t("thumbnailPicker.capturedFrame")}
257
307
  className="w-full rounded border border-border mt-1"
258
308
  />
309
+ ) : videoUrl ? (
310
+ <video
311
+ ref={framePreviewRef}
312
+ src={videoUrl}
313
+ className="w-full rounded border border-border bg-black mt-1"
314
+ crossOrigin="anonymous"
315
+ preload="auto"
316
+ muted
317
+ />
259
318
  ) : (
260
319
  <div className="w-full aspect-video rounded border border-dashed border-border flex items-center justify-center text-xs text-muted-foreground mt-1">
261
320
  {t("thumbnailPicker.capturePreview")}
@@ -269,6 +328,7 @@ export function ThumbnailPicker({
269
328
  <div className="grid grid-cols-2 gap-4">
270
329
  <div>
271
330
  <video
331
+ ref={gifVideoRef}
272
332
  src={videoUrl ?? undefined}
273
333
  className="w-full rounded border border-border bg-black"
274
334
  crossOrigin="anonymous"
@@ -287,7 +347,13 @@ export function ThumbnailPicker({
287
347
  max={Math.max(0, durationMs - gifDuration)}
288
348
  step={100}
289
349
  value={[gifStart]}
290
- onValueChange={([v]) => setGifStart(v)}
350
+ onValueChange={([v]) => {
351
+ const next = v ?? 0;
352
+ setGifStart(next);
353
+ setGifDataUrl(null);
354
+ seekPreview(gifVideoRef.current, next);
355
+ seekPreview(gifPreviewRef.current, next);
356
+ }}
291
357
  />
292
358
  </div>
293
359
  <div>
@@ -301,7 +367,10 @@ export function ThumbnailPicker({
301
367
  max={10000}
302
368
  step={100}
303
369
  value={[gifDuration]}
304
- onValueChange={([v]) => setGifDuration(v)}
370
+ onValueChange={([v]) => {
371
+ setGifDuration(v ?? 500);
372
+ setGifDataUrl(null);
373
+ }}
305
374
  />
306
375
  </div>
307
376
  <Button
@@ -331,6 +400,15 @@ export function ThumbnailPicker({
331
400
  alt={t("thumbnailPicker.animatedPreview")}
332
401
  className="w-full rounded border border-border mt-1"
333
402
  />
403
+ ) : videoUrl ? (
404
+ <video
405
+ ref={gifPreviewRef}
406
+ src={videoUrl}
407
+ className="w-full rounded border border-border bg-black mt-1"
408
+ crossOrigin="anonymous"
409
+ preload="auto"
410
+ muted
411
+ />
334
412
  ) : (
335
413
  <div className="w-full aspect-video rounded border border-dashed border-border flex items-center justify-center text-xs text-muted-foreground mt-1">
336
414
  {t("thumbnailPicker.generateGifPreview")}