@agent-native/core 0.100.1 → 0.101.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 (387) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +73 -0
  3. package/corpus/core/docs/content/locales/ar-SA/pr-visual-recap.mdx +48 -30
  4. package/corpus/core/docs/content/locales/ar-SA/template-plan.mdx +7 -0
  5. package/corpus/core/docs/content/locales/de-DE/pr-visual-recap.mdx +48 -30
  6. package/corpus/core/docs/content/locales/de-DE/template-plan.mdx +7 -0
  7. package/corpus/core/docs/content/locales/es-ES/pr-visual-recap.mdx +48 -30
  8. package/corpus/core/docs/content/locales/es-ES/template-plan.mdx +14 -0
  9. package/corpus/core/docs/content/locales/fr-FR/pr-visual-recap.mdx +48 -30
  10. package/corpus/core/docs/content/locales/fr-FR/template-plan.mdx +7 -0
  11. package/corpus/core/docs/content/locales/hi-IN/pr-visual-recap.mdx +48 -30
  12. package/corpus/core/docs/content/locales/hi-IN/template-plan.mdx +7 -0
  13. package/corpus/core/docs/content/locales/ja-JP/pr-visual-recap.mdx +48 -30
  14. package/corpus/core/docs/content/locales/ja-JP/template-plan.mdx +7 -0
  15. package/corpus/core/docs/content/locales/ko-KR/pr-visual-recap.mdx +48 -30
  16. package/corpus/core/docs/content/locales/ko-KR/template-plan.mdx +7 -0
  17. package/corpus/core/docs/content/locales/pt-BR/pr-visual-recap.mdx +48 -30
  18. package/corpus/core/docs/content/locales/pt-BR/template-plan.mdx +14 -0
  19. package/corpus/core/docs/content/locales/zh-CN/pr-visual-recap.mdx +48 -30
  20. package/corpus/core/docs/content/locales/zh-CN/template-plan.mdx +7 -0
  21. package/corpus/core/docs/content/locales/zh-TW/pr-visual-recap.mdx +48 -30
  22. package/corpus/core/docs/content/locales/zh-TW/template-plan.mdx +7 -0
  23. package/corpus/core/docs/content/pr-visual-recap.mdx +50 -32
  24. package/corpus/core/docs/content/template-plan.mdx +14 -0
  25. package/corpus/core/docs/content/toolkit-comments-review.mdx +16 -2
  26. package/corpus/core/package.json +4 -1
  27. package/corpus/core/src/a2a/index.ts +2 -1
  28. package/corpus/core/src/a2a/server.ts +34 -9
  29. package/corpus/core/src/agent/production-agent.ts +56 -10
  30. package/corpus/core/src/cli/create.ts +47 -19
  31. package/corpus/core/src/cli/pr-visual-recap-workflow.ts +2 -3
  32. package/corpus/core/src/cli/recap.ts +3 -4932
  33. package/corpus/core/src/cli/skills-content/visual-plan-skill.ts +6 -4
  34. package/corpus/core/src/cli/skills-content/visual-recap-skill.ts +7 -1
  35. package/corpus/core/src/client/AgentPanel.tsx +19 -17
  36. package/corpus/core/src/client/AssistantChat.tsx +4 -0
  37. package/corpus/core/src/client/analytics.ts +8 -1
  38. package/corpus/core/src/client/chat/markdown-renderer.tsx +54 -8
  39. package/corpus/core/src/client/chat/message-components.tsx +8 -2
  40. package/corpus/core/src/client/clipboard.ts +28 -1
  41. package/corpus/core/src/client/composer/PromptComposer.tsx +4 -0
  42. package/corpus/core/src/client/composer/TiptapComposer.tsx +30 -3
  43. package/corpus/core/src/client/guided-questions.tsx +4 -0
  44. package/corpus/core/src/client/i18n.tsx +2 -1
  45. package/corpus/core/src/client/index.ts +4 -0
  46. package/corpus/core/src/client/review/ReviewCommentComposer.tsx +108 -0
  47. package/corpus/core/src/client/review/ReviewThreadPanel.tsx +482 -150
  48. package/corpus/core/src/client/review/index.ts +8 -0
  49. package/corpus/core/src/client/review/use-review.ts +35 -1
  50. package/corpus/core/src/client/use-agent-chat-context.ts +3 -2
  51. package/corpus/core/src/integrations/webhook-handler.ts +4 -2
  52. package/corpus/core/src/review/actions/create-review-comment.ts +2 -1
  53. package/corpus/core/src/review/actions/get-review-feedback.ts +6 -11
  54. package/corpus/core/src/review/actions/list-review-comments.ts +38 -4
  55. package/corpus/core/src/review/actions/reply-review-comment.ts +33 -22
  56. package/corpus/core/src/review/actions/resolve-review-thread.ts +29 -2
  57. package/corpus/core/src/review/actions/send-review-thread-to-agent.ts +62 -0
  58. package/corpus/core/src/review/identity.ts +101 -0
  59. package/corpus/core/src/review/index.ts +14 -0
  60. package/corpus/core/src/review/store.ts +315 -8
  61. package/corpus/core/src/review/types.ts +4 -0
  62. package/corpus/core/src/secrets/crypto.ts +98 -47
  63. package/corpus/core/src/secrets/index.ts +2 -1
  64. package/corpus/core/src/secrets/schema.ts +3 -0
  65. package/corpus/core/src/secrets/storage.ts +164 -31
  66. package/corpus/core/src/server/action-discovery.ts +4 -0
  67. package/corpus/core/src/server/action-routes.ts +125 -4
  68. package/corpus/core/src/server/agent-chat/plugin-options.ts +16 -0
  69. package/corpus/core/src/server/agent-chat-plugin.ts +1 -0
  70. package/corpus/core/src/server/index.ts +8 -0
  71. package/corpus/core/src/vite/action-types-plugin.ts +4 -0
  72. package/corpus/templates/analytics/changelog/2026-07-14-daily-dashboard-email-captures-authenticate-with-a-sessi.md +6 -0
  73. package/corpus/templates/analytics/server/lib/dashboard-report.ts +156 -3
  74. package/corpus/templates/assets/app/components/generation/GenerationResults.tsx +402 -142
  75. package/corpus/templates/assets/app/components/layout/Layout.tsx +4 -0
  76. package/corpus/templates/assets/app/hooks/use-navigation-state.ts +3 -1
  77. package/corpus/templates/assets/app/i18n/zh-TW.ts +1 -0
  78. package/corpus/templates/assets/app/i18n-data.ts +1 -0
  79. package/corpus/templates/assets/app/routes/library.tsx +187 -48
  80. package/corpus/templates/assets/changelog/2026-07-13-assets-keeps-the-right-chat-sidebar-closed-when-the-full-pag.md +6 -0
  81. package/corpus/templates/assets/changelog/2026-07-13-merged-the-candidates-panel-into-the-library-as-a-drafts-tab.md +6 -0
  82. package/corpus/templates/chat/app/lib/agent-page.tsx +40 -0
  83. package/corpus/templates/chat/app/routes/agent.tsx +5 -3
  84. package/corpus/templates/chat/changelog/2026-07-14-fixed-chat-template-startup-with-older-core-versions.md +6 -0
  85. package/corpus/templates/clips/.agents/skills/ai-video-tools/SKILL.md +5 -0
  86. package/corpus/templates/clips/AGENTS.md +4 -0
  87. package/corpus/templates/clips/actions/get-feature-flags.ts +30 -0
  88. package/corpus/templates/clips/actions/lib/transcript-preview.ts +48 -0
  89. package/corpus/templates/clips/actions/request-transcript.ts +41 -1
  90. package/corpus/templates/clips/actions/view-screen.ts +5 -9
  91. package/corpus/templates/clips/app/components/library/library-layout.tsx +1 -1
  92. package/corpus/templates/clips/app/components/player/video-player.tsx +39 -1
  93. package/corpus/templates/clips/app/hooks/use-desktop-promo.ts +6 -12
  94. package/corpus/templates/clips/app/hooks/use-mse-video-source.ts +146 -0
  95. package/corpus/templates/clips/app/lib/capture-install-options.ts +21 -0
  96. package/corpus/templates/clips/app/lib/fmp4.ts +271 -0
  97. package/corpus/templates/clips/app/lib/mse-video-loader.ts +585 -0
  98. package/corpus/templates/clips/app/routes/_app.dictate.tsx +0 -4
  99. package/corpus/templates/clips/app/routes/_app.meetings.$meetingId.tsx +5 -13
  100. package/corpus/templates/clips/app/routes/_app.meetings._index.tsx +2 -37
  101. package/corpus/templates/clips/app/routes/download.tsx +3 -2
  102. package/corpus/templates/clips/changelog/2026-07-10-shared-clips-now-start-playing-instantly-instead-of-showing-.md +6 -0
  103. package/corpus/templates/clips/changelog/2026-07-14-clips-now-distinguishes-transcript-previews-from-incomplete-.md +6 -0
  104. package/corpus/templates/clips/changelog/2026-07-14-desktop-app-prompts-stay-hidden-after-you-download-the-insta.md +6 -0
  105. package/corpus/templates/clips/desktop/src/lib/recorder.ts +5 -0
  106. package/corpus/templates/clips/desktop/src/overlays/recording-pill.tsx +10 -0
  107. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +4 -0
  108. package/corpus/templates/clips/desktop/src-tauri/src/native_screen/custom_capture.rs +2518 -0
  109. package/corpus/templates/clips/desktop/src-tauri/src/native_screen/live_upload.rs +353 -0
  110. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +558 -67
  111. package/corpus/templates/clips/desktop/src-tauri/src/recording_indicator.rs +11 -1
  112. package/corpus/templates/clips/desktop/src-tauri/src/remote_flags.rs +188 -0
  113. package/corpus/templates/clips/desktop/src-tauri/src/whisper_speech.rs +1 -1
  114. package/corpus/templates/clips/server/lib/post-finalize-dispatch.ts +1 -0
  115. package/corpus/templates/clips/server/plugins/agent-chat.ts +5 -0
  116. package/corpus/templates/clips/server/routes/api/_agent-native-background/post-finalize-worker.post.ts +5 -1
  117. package/corpus/templates/clips/shared/feature-flags.ts +41 -0
  118. package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +3 -0
  119. package/corpus/templates/design/.agents/skills/design-review-feedback/SKILL.md +42 -0
  120. package/corpus/templates/design/AGENTS.md +6 -0
  121. package/corpus/templates/design/README.md +1 -0
  122. package/corpus/templates/design/actions/navigate.ts +4 -4
  123. package/corpus/templates/design/actions/view-screen.ts +116 -2
  124. package/corpus/templates/design/agent-native.app-skill.json +5 -0
  125. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +52 -59
  126. package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +13 -9
  127. package/corpus/templates/design/app/components/design/EditPanel.tsx +79 -11
  128. package/corpus/templates/design/app/components/design/LayersPanel.tsx +78 -65
  129. package/corpus/templates/design/app/components/design/ReviewCommentsPanel.tsx +161 -0
  130. package/corpus/templates/design/app/components/design/ReviewStatusControl.tsx +96 -0
  131. package/corpus/templates/design/app/components/design/bridge/hit-test.bridge.ts +166 -0
  132. package/corpus/templates/design/app/components/layout/Layout.tsx +1 -0
  133. package/corpus/templates/design/app/components/visual-editor/ReviewCanvasPins.tsx +1077 -0
  134. package/corpus/templates/design/app/components/visual-editor/index.ts +1 -0
  135. package/corpus/templates/design/app/components/visual-editor/review-canvas-state.ts +42 -0
  136. package/corpus/templates/design/app/hooks/use-design-systems.ts +2 -2
  137. package/corpus/templates/design/app/hooks/use-navigation-state.ts +9 -6
  138. package/corpus/templates/design/app/hooks/use-question-flow.ts +7 -1
  139. package/corpus/templates/design/app/i18n/ar-SA.ts +56 -1
  140. package/corpus/templates/design/app/i18n/de-DE.ts +57 -1
  141. package/corpus/templates/design/app/i18n/en-US.ts +56 -1
  142. package/corpus/templates/design/app/i18n/es-ES.ts +56 -1
  143. package/corpus/templates/design/app/i18n/fr-FR.ts +57 -1
  144. package/corpus/templates/design/app/i18n/hi-IN.ts +56 -1
  145. package/corpus/templates/design/app/i18n/ja-JP.ts +56 -1
  146. package/corpus/templates/design/app/i18n/ko-KR.ts +56 -1
  147. package/corpus/templates/design/app/i18n/pt-BR.ts +57 -1
  148. package/corpus/templates/design/app/i18n/zh-CN.ts +56 -1
  149. package/corpus/templates/design/app/i18n/zh-TW.ts +51 -0
  150. package/corpus/templates/design/app/pages/Present.tsx +172 -14
  151. package/corpus/templates/design/app/pages/design-editor/overview-camera.ts +23 -0
  152. package/corpus/templates/design/app/pages/design-editor/tool-state.ts +10 -0
  153. package/corpus/templates/design/app/pages/present-review-state.ts +18 -0
  154. package/corpus/templates/design/app/public-routes.ts +8 -0
  155. package/corpus/templates/design/app/root.tsx +7 -6
  156. package/corpus/templates/design/changelog/2026-07-13-dragged-design-assets-now-appear-at-the-visible-drop-locatio.md +6 -0
  157. package/corpus/templates/design/changelog/2026-07-13-reviewers-can-pin-comments-on-shared-designs-and-apply-verif.md +6 -0
  158. package/corpus/templates/design/server/plugins/agent-chat.ts +10 -0
  159. package/corpus/templates/design/server/plugins/auth.ts +7 -5
  160. package/corpus/templates/design/server/plugins/review.ts +9 -0
  161. package/corpus/templates/design/shared/review-anchor.ts +83 -0
  162. package/corpus/templates/design/shared/review-summary.ts +25 -0
  163. package/corpus/templates/forms/.agents/skills/form-publishing/SKILL.md +2 -0
  164. package/corpus/templates/forms/AGENTS.md +4 -0
  165. package/corpus/templates/forms/actions/create-form.ts +2 -1
  166. package/corpus/templates/forms/actions/update-form.ts +5 -2
  167. package/corpus/templates/forms/app/i18n/ar-SA.ts +3 -0
  168. package/corpus/templates/forms/app/i18n/de-DE.ts +3 -0
  169. package/corpus/templates/forms/app/i18n/en-US.ts +3 -0
  170. package/corpus/templates/forms/app/i18n/es-ES.ts +3 -0
  171. package/corpus/templates/forms/app/i18n/fr-FR.ts +3 -0
  172. package/corpus/templates/forms/app/i18n/hi-IN.ts +3 -0
  173. package/corpus/templates/forms/app/i18n/ja-JP.ts +3 -0
  174. package/corpus/templates/forms/app/i18n/ko-KR.ts +3 -0
  175. package/corpus/templates/forms/app/i18n/pt-BR.ts +3 -0
  176. package/corpus/templates/forms/app/i18n/zh-CN.ts +3 -0
  177. package/corpus/templates/forms/app/i18n/zh-TW.ts +3 -0
  178. package/corpus/templates/forms/app/pages/FormBuilderPage.tsx +19 -0
  179. package/corpus/templates/forms/changelog/2026-07-14-fixed-the-ask-forms-loading-input-appearing-above-the-welcom.md +6 -0
  180. package/corpus/templates/forms/changelog/2026-07-14-form-social-previews-now-show-the-form-title-description-and.md +6 -0
  181. package/corpus/templates/forms/changelog/2026-07-14-forms-can-email-the-form-owner-when-new-responses-arrive.md +6 -0
  182. package/corpus/templates/forms/server/handlers/submissions.ts +17 -0
  183. package/corpus/templates/forms/server/lib/form-og-image.ts +64 -6
  184. package/corpus/templates/forms/server/lib/public-form-ssr.ts +6 -2
  185. package/corpus/templates/forms/server/lib/response-email.ts +65 -0
  186. package/corpus/templates/forms/server/plugins/agent-chat.ts +1 -0
  187. package/corpus/templates/forms/server/routes/api/forms/og/[...slug]/og.png.get.ts +184 -0
  188. package/corpus/templates/forms/shared/types.ts +2 -0
  189. package/corpus/templates/plan/.agents/skills/visual-plan/SKILL.md +6 -4
  190. package/corpus/templates/plan/.agents/skills/visual-recap/SKILL.md +7 -1
  191. package/dist/a2a/index.d.ts +2 -1
  192. package/dist/a2a/index.d.ts.map +1 -1
  193. package/dist/a2a/index.js +1 -1
  194. package/dist/a2a/index.js.map +1 -1
  195. package/dist/a2a/server.d.ts +25 -0
  196. package/dist/a2a/server.d.ts.map +1 -1
  197. package/dist/a2a/server.js +30 -6
  198. package/dist/a2a/server.js.map +1 -1
  199. package/dist/agent/production-agent.d.ts +2 -0
  200. package/dist/agent/production-agent.d.ts.map +1 -1
  201. package/dist/agent/production-agent.js +37 -12
  202. package/dist/agent/production-agent.js.map +1 -1
  203. package/dist/cli/create.d.ts +5 -3
  204. package/dist/cli/create.d.ts.map +1 -1
  205. package/dist/cli/create.js +44 -19
  206. package/dist/cli/create.js.map +1 -1
  207. package/dist/cli/pr-visual-recap-workflow.d.ts +2 -2
  208. package/dist/cli/pr-visual-recap-workflow.d.ts.map +1 -1
  209. package/dist/cli/pr-visual-recap-workflow.js +2 -2
  210. package/dist/cli/pr-visual-recap-workflow.js.map +1 -1
  211. package/dist/cli/recap.d.ts +3 -562
  212. package/dist/cli/recap.d.ts.map +1 -1
  213. package/dist/cli/recap.js +3 -3875
  214. package/dist/cli/recap.js.map +1 -1
  215. package/dist/cli/skills-content/visual-plan-skill.d.ts +1 -1
  216. package/dist/cli/skills-content/visual-plan-skill.d.ts.map +1 -1
  217. package/dist/cli/skills-content/visual-plan-skill.js +6 -4
  218. package/dist/cli/skills-content/visual-plan-skill.js.map +1 -1
  219. package/dist/cli/skills-content/visual-recap-skill.d.ts +1 -1
  220. package/dist/cli/skills-content/visual-recap-skill.d.ts.map +1 -1
  221. package/dist/cli/skills-content/visual-recap-skill.js +7 -1
  222. package/dist/cli/skills-content/visual-recap-skill.js.map +1 -1
  223. package/dist/client/AgentPanel.d.ts.map +1 -1
  224. package/dist/client/AgentPanel.js +3 -3
  225. package/dist/client/AgentPanel.js.map +1 -1
  226. package/dist/client/AssistantChat.d.ts +2 -0
  227. package/dist/client/AssistantChat.d.ts.map +1 -1
  228. package/dist/client/AssistantChat.js +2 -2
  229. package/dist/client/AssistantChat.js.map +1 -1
  230. package/dist/client/analytics.d.ts +5 -0
  231. package/dist/client/analytics.d.ts.map +1 -1
  232. package/dist/client/analytics.js +3 -1
  233. package/dist/client/analytics.js.map +1 -1
  234. package/dist/client/chat/markdown-renderer.d.ts +1 -0
  235. package/dist/client/chat/markdown-renderer.d.ts.map +1 -1
  236. package/dist/client/chat/markdown-renderer.js +36 -1
  237. package/dist/client/chat/markdown-renderer.js.map +1 -1
  238. package/dist/client/chat/message-components.d.ts.map +1 -1
  239. package/dist/client/chat/message-components.js +5 -2
  240. package/dist/client/chat/message-components.js.map +1 -1
  241. package/dist/client/clipboard.d.ts +3 -1
  242. package/dist/client/clipboard.d.ts.map +1 -1
  243. package/dist/client/clipboard.js +24 -1
  244. package/dist/client/clipboard.js.map +1 -1
  245. package/dist/client/composer/PromptComposer.d.ts +2 -0
  246. package/dist/client/composer/PromptComposer.d.ts.map +1 -1
  247. package/dist/client/composer/PromptComposer.js +2 -2
  248. package/dist/client/composer/PromptComposer.js.map +1 -1
  249. package/dist/client/composer/TiptapComposer.d.ts +3 -1
  250. package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
  251. package/dist/client/composer/TiptapComposer.js +20 -5
  252. package/dist/client/composer/TiptapComposer.js.map +1 -1
  253. package/dist/client/guided-questions.d.ts +3 -1
  254. package/dist/client/guided-questions.d.ts.map +1 -1
  255. package/dist/client/guided-questions.js +2 -1
  256. package/dist/client/guided-questions.js.map +1 -1
  257. package/dist/client/i18n.d.ts.map +1 -1
  258. package/dist/client/i18n.js +3 -1
  259. package/dist/client/i18n.js.map +1 -1
  260. package/dist/client/index.d.ts +1 -1
  261. package/dist/client/index.d.ts.map +1 -1
  262. package/dist/client/index.js +1 -1
  263. package/dist/client/index.js.map +1 -1
  264. package/dist/client/review/ReviewCommentComposer.d.ts +18 -0
  265. package/dist/client/review/ReviewCommentComposer.d.ts.map +1 -0
  266. package/dist/client/review/ReviewCommentComposer.js +30 -0
  267. package/dist/client/review/ReviewCommentComposer.js.map +1 -0
  268. package/dist/client/review/ReviewThreadPanel.d.ts +28 -1
  269. package/dist/client/review/ReviewThreadPanel.d.ts.map +1 -1
  270. package/dist/client/review/ReviewThreadPanel.js +151 -39
  271. package/dist/client/review/ReviewThreadPanel.js.map +1 -1
  272. package/dist/client/review/index.d.ts +3 -2
  273. package/dist/client/review/index.d.ts.map +1 -1
  274. package/dist/client/review/index.js +2 -1
  275. package/dist/client/review/index.js.map +1 -1
  276. package/dist/client/review/use-review.d.ts +23 -0
  277. package/dist/client/review/use-review.d.ts.map +1 -1
  278. package/dist/client/review/use-review.js +3 -0
  279. package/dist/client/review/use-review.js.map +1 -1
  280. package/dist/client/use-agent-chat-context.d.ts +1 -1
  281. package/dist/client/use-agent-chat-context.d.ts.map +1 -1
  282. package/dist/client/use-agent-chat-context.js +4 -2
  283. package/dist/client/use-agent-chat-context.js.map +1 -1
  284. package/dist/collab/awareness.d.ts +2 -2
  285. package/dist/collab/awareness.d.ts.map +1 -1
  286. package/dist/collab/struct-routes.d.ts +1 -1
  287. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  288. package/dist/integrations/webhook-handler.d.ts.map +1 -1
  289. package/dist/integrations/webhook-handler.js +3 -2
  290. package/dist/integrations/webhook-handler.js.map +1 -1
  291. package/dist/notifications/routes.d.ts +3 -3
  292. package/dist/observability/routes.d.ts +5 -5
  293. package/dist/progress/routes.d.ts +1 -1
  294. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  295. package/dist/resources/handlers.d.ts +2 -2
  296. package/dist/review/actions/create-review-comment.js +2 -1
  297. package/dist/review/actions/create-review-comment.js.map +1 -1
  298. package/dist/review/actions/get-review-feedback.js +6 -11
  299. package/dist/review/actions/get-review-feedback.js.map +1 -1
  300. package/dist/review/actions/list-review-comments.d.ts +1 -0
  301. package/dist/review/actions/list-review-comments.js +25 -4
  302. package/dist/review/actions/list-review-comments.js.map +1 -1
  303. package/dist/review/actions/reply-review-comment.js +12 -5
  304. package/dist/review/actions/reply-review-comment.js.map +1 -1
  305. package/dist/review/actions/resolve-review-thread.d.ts +3 -0
  306. package/dist/review/actions/resolve-review-thread.js +20 -3
  307. package/dist/review/actions/resolve-review-thread.js.map +1 -1
  308. package/dist/review/actions/send-review-thread-to-agent.d.ts +17 -0
  309. package/dist/review/actions/send-review-thread-to-agent.d.ts.map +1 -0
  310. package/dist/review/actions/send-review-thread-to-agent.js +49 -0
  311. package/dist/review/actions/send-review-thread-to-agent.js.map +1 -0
  312. package/dist/review/identity.d.ts +6 -0
  313. package/dist/review/identity.d.ts.map +1 -0
  314. package/dist/review/identity.js +68 -0
  315. package/dist/review/identity.js.map +1 -0
  316. package/dist/review/index.d.ts +3 -1
  317. package/dist/review/index.d.ts.map +1 -1
  318. package/dist/review/index.js +2 -1
  319. package/dist/review/index.js.map +1 -1
  320. package/dist/review/store.d.ts +36 -0
  321. package/dist/review/store.d.ts.map +1 -1
  322. package/dist/review/store.js +223 -8
  323. package/dist/review/store.js.map +1 -1
  324. package/dist/review/types.d.ts +4 -0
  325. package/dist/review/types.d.ts.map +1 -1
  326. package/dist/review/types.js.map +1 -1
  327. package/dist/secrets/crypto.d.ts +35 -13
  328. package/dist/secrets/crypto.d.ts.map +1 -1
  329. package/dist/secrets/crypto.js +78 -41
  330. package/dist/secrets/crypto.js.map +1 -1
  331. package/dist/secrets/index.d.ts.map +1 -1
  332. package/dist/secrets/index.js +2 -1
  333. package/dist/secrets/index.js.map +1 -1
  334. package/dist/secrets/routes.d.ts +9 -9
  335. package/dist/secrets/schema.d.ts +20 -1
  336. package/dist/secrets/schema.d.ts.map +1 -1
  337. package/dist/secrets/schema.js +3 -0
  338. package/dist/secrets/schema.js.map +1 -1
  339. package/dist/secrets/storage.d.ts +7 -7
  340. package/dist/secrets/storage.d.ts.map +1 -1
  341. package/dist/secrets/storage.js +121 -30
  342. package/dist/secrets/storage.js.map +1 -1
  343. package/dist/server/action-discovery.d.ts.map +1 -1
  344. package/dist/server/action-discovery.js +4 -0
  345. package/dist/server/action-discovery.js.map +1 -1
  346. package/dist/server/action-routes.d.ts +45 -0
  347. package/dist/server/action-routes.d.ts.map +1 -1
  348. package/dist/server/action-routes.js +71 -5
  349. package/dist/server/action-routes.js.map +1 -1
  350. package/dist/server/agent-chat/plugin-options.d.ts +16 -0
  351. package/dist/server/agent-chat/plugin-options.d.ts.map +1 -1
  352. package/dist/server/agent-chat/plugin-options.js.map +1 -1
  353. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  354. package/dist/server/agent-chat-plugin.js +1 -0
  355. package/dist/server/agent-chat-plugin.js.map +1 -1
  356. package/dist/server/index.d.ts +3 -1
  357. package/dist/server/index.d.ts.map +1 -1
  358. package/dist/server/index.js +2 -0
  359. package/dist/server/index.js.map +1 -1
  360. package/dist/vite/action-types-plugin.d.ts.map +1 -1
  361. package/dist/vite/action-types-plugin.js +4 -0
  362. package/dist/vite/action-types-plugin.js.map +1 -1
  363. package/docs/content/locales/ar-SA/pr-visual-recap.mdx +48 -30
  364. package/docs/content/locales/ar-SA/template-plan.mdx +7 -0
  365. package/docs/content/locales/de-DE/pr-visual-recap.mdx +48 -30
  366. package/docs/content/locales/de-DE/template-plan.mdx +7 -0
  367. package/docs/content/locales/es-ES/pr-visual-recap.mdx +48 -30
  368. package/docs/content/locales/es-ES/template-plan.mdx +14 -0
  369. package/docs/content/locales/fr-FR/pr-visual-recap.mdx +48 -30
  370. package/docs/content/locales/fr-FR/template-plan.mdx +7 -0
  371. package/docs/content/locales/hi-IN/pr-visual-recap.mdx +48 -30
  372. package/docs/content/locales/hi-IN/template-plan.mdx +7 -0
  373. package/docs/content/locales/ja-JP/pr-visual-recap.mdx +48 -30
  374. package/docs/content/locales/ja-JP/template-plan.mdx +7 -0
  375. package/docs/content/locales/ko-KR/pr-visual-recap.mdx +48 -30
  376. package/docs/content/locales/ko-KR/template-plan.mdx +7 -0
  377. package/docs/content/locales/pt-BR/pr-visual-recap.mdx +48 -30
  378. package/docs/content/locales/pt-BR/template-plan.mdx +14 -0
  379. package/docs/content/locales/zh-CN/pr-visual-recap.mdx +48 -30
  380. package/docs/content/locales/zh-CN/template-plan.mdx +7 -0
  381. package/docs/content/locales/zh-TW/pr-visual-recap.mdx +48 -30
  382. package/docs/content/locales/zh-TW/template-plan.mdx +7 -0
  383. package/docs/content/pr-visual-recap.mdx +50 -32
  384. package/docs/content/template-plan.mdx +14 -0
  385. package/docs/content/toolkit-comments-review.mdx +16 -2
  386. package/package.json +4 -1
  387. package/corpus/templates/forms/server/routes/api/forms/og/[slug]/og.png.get.ts +0 -45
@@ -28,6 +28,7 @@ import {
28
28
  DropdownMenuTrigger,
29
29
  } from "@/components/ui/dropdown-menu";
30
30
  import { Spinner } from "@/components/ui/spinner";
31
+ import { useMseVideoSource } from "@/hooks/use-mse-video-source";
31
32
  import {
32
33
  parsePlaybackSpeed,
33
34
  readPlaybackSpeedPreference,
@@ -365,6 +366,32 @@ export const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
365
366
  [resolvedVideoSrc],
366
367
  );
367
368
 
369
+ // Media Source Extensions path for raw fragmented-MP4 recordings (desktop
370
+ // live-stream uploads). Those files declare no up-front duration, so the
371
+ // native progressive pipeline scans the whole file before it can play from
372
+ // a CDN. When the asset sniffs as fragmented, `mse.mode === "mse"` and we
373
+ // hand the element a MediaSource object URL instead of the raw URL, with the
374
+ // duration supplied from the DB. Everything else (classic MP4, WebM, Loom,
375
+ // browsers without MediaSource) stays on the native `<video src>` path,
376
+ // byte-for-byte unchanged.
377
+ const mse = useMseVideoSource({
378
+ videoRef,
379
+ sourceUrl: resolvedVideoSrc,
380
+ durationMs,
381
+ videoFormat,
382
+ disabled: isLoomEmbed || unsupportedFormat,
383
+ });
384
+ const mseActive = mse.mode === "mse" && Boolean(mse.objectUrl);
385
+ // The URL actually put on the <video> element: the MediaSource object URL
386
+ // while MSE drives playback, nothing while we're still sniffing an eligible
387
+ // asset (so the browser never starts the slow native scan), otherwise the
388
+ // normal resolved/cache-busted source.
389
+ const domVideoSrc = mseActive
390
+ ? mse.objectUrl
391
+ : mse.mode === "pending"
392
+ ? undefined
393
+ : activeVideoSrc;
394
+
368
395
  useEffect(() => {
369
396
  if (!resolvedVideoSrc) {
370
397
  setActiveVideoSrc(undefined);
@@ -1164,7 +1191,7 @@ export const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
1164
1191
  ) : activeVideoSrc ? (
1165
1192
  <video
1166
1193
  ref={videoRef}
1167
- src={activeVideoSrc}
1194
+ src={domVideoSrc}
1168
1195
  poster={resolveLocalUrl(thumbnailUrl)}
1169
1196
  // `crossOrigin` is only needed so the owner's canvas thumbnail
1170
1197
  // capture isn't tainted. For everyone else (viewers, and the Slack
@@ -1328,6 +1355,17 @@ export const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
1328
1355
  playAttemptPendingRef.current = false;
1329
1356
  setIsPlayPending(false);
1330
1357
 
1358
+ // If the MSE pipeline surfaced a media error, tear it down and let
1359
+ // the native <video src> path take over the raw asset URL instead
1360
+ // of running the cache-bust retry against a MediaSource blob URL.
1361
+ if (mseActive) {
1362
+ mse.fallbackToNative();
1363
+ setIsBuffering(false);
1364
+ setIsPreparing(true);
1365
+ setCanPlay(false);
1366
+ return;
1367
+ }
1368
+
1331
1369
  // Most "format not supported" / decode errors reported here are
1332
1370
  // transient — e.g. the share page's video element started
1333
1371
  // fetching a moment before a background seekable-remux pass
@@ -1,8 +1,10 @@
1
1
  import { useCallback, useEffect, useState } from "react";
2
2
 
3
3
  import { useIsMobile } from "@/hooks/use-mobile";
4
-
5
- const DISMISSED_KEY = "clips.desktop-promo.dismissed";
4
+ import {
5
+ hasDownloadedDesktopApp,
6
+ markDesktopAppDownloaded,
7
+ } from "@/lib/capture-install-options";
6
8
 
7
9
  function detectDesktopApp(): boolean {
8
10
  if (typeof navigator === "undefined") return false;
@@ -25,20 +27,12 @@ export function useDesktopPromo() {
25
27
 
26
28
  useEffect(() => {
27
29
  setIsDesktopApp(detectDesktopApp());
28
- setDismissed(
29
- typeof window !== "undefined" &&
30
- window.localStorage?.getItem(DISMISSED_KEY) === "1",
31
- );
30
+ setDismissed(hasDownloadedDesktopApp());
32
31
  }, []);
33
32
 
34
33
  const dismiss = useCallback(() => {
35
34
  setDismissed(true);
36
- try {
37
- window.localStorage?.setItem(DISMISSED_KEY, "1");
38
- } catch {
39
- // localStorage can throw in private browsing — ignore, dismissal
40
- // still holds for the session via React state.
41
- }
35
+ markDesktopAppDownloaded();
42
36
  }, []);
43
37
 
44
38
  return {
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Decides whether a recording should play through Media Source Extensions and,
3
+ * when it should, owns the `MseVideoLoader` lifecycle.
4
+ *
5
+ * Raw fragmented-MP4 recordings (from the desktop live-streaming pipeline)
6
+ * declare no up-front duration, so the browser's progressive `<video src>`
7
+ * pipeline stalls when they're served from a CDN. This hook sniffs the asset,
8
+ * and for fragmented files hands the video element a `MediaSource` object URL
9
+ * driven by range requests with the duration supplied from the DB. Classic MP4,
10
+ * WebM, Loom, and anything the browser can't MSE fall back to the native path
11
+ * unchanged.
12
+ */
13
+
14
+ import {
15
+ type RefObject,
16
+ useCallback,
17
+ useEffect,
18
+ useRef,
19
+ useState,
20
+ } from "react";
21
+
22
+ import { sniffFragmentedMp4 } from "@/lib/fmp4";
23
+ import { MseVideoLoader, isMediaSourceSupported } from "@/lib/mse-video-loader";
24
+
25
+ export type MseSourceMode = "pending" | "native" | "mse";
26
+
27
+ export interface UseMseVideoSourceParams {
28
+ videoRef: RefObject<HTMLVideoElement | null>;
29
+ /** The resolved asset URL (proxied same-origin or provider URL). */
30
+ sourceUrl: string | undefined;
31
+ /** Authoritative duration in ms from the DB. */
32
+ durationMs: number;
33
+ /** Container format hint. WebM is never eligible for this MP4-only loader. */
34
+ videoFormat?: "webm" | "mp4" | null;
35
+ /** True for Loom embeds / formats the browser cannot decode — never MSE. */
36
+ disabled?: boolean;
37
+ }
38
+
39
+ export interface UseMseVideoSourceResult {
40
+ mode: MseSourceMode;
41
+ /** MediaSource object URL to use as `<video src>` when `mode === "mse"`. */
42
+ objectUrl: string | undefined;
43
+ /** Drop MSE and revert to the plain `<video src>` path. */
44
+ fallbackToNative: () => void;
45
+ }
46
+
47
+ export function useMseVideoSource({
48
+ videoRef,
49
+ sourceUrl,
50
+ durationMs,
51
+ videoFormat,
52
+ disabled,
53
+ }: UseMseVideoSourceParams): UseMseVideoSourceResult {
54
+ // WebM recordings use the native Infinity-duration workaround; only MP4 (and
55
+ // unknown-format, which the sniff will confirm) are candidates.
56
+ const eligible =
57
+ !disabled &&
58
+ !!sourceUrl &&
59
+ videoFormat !== "webm" &&
60
+ isMediaSourceSupported();
61
+
62
+ const [mode, setMode] = useState<MseSourceMode>(() =>
63
+ eligible ? "pending" : "native",
64
+ );
65
+ const [objectUrl, setObjectUrl] = useState<string | undefined>(undefined);
66
+ const loaderRef = useRef<MseVideoLoader | null>(null);
67
+
68
+ // Latest duration, kept in a ref so the (deliberately duration-independent)
69
+ // rebuild effect below seeds a freshly created loader with the current value
70
+ // even if it changed during the async sniff. Updating a ref in render is
71
+ // side-effect-free and safe.
72
+ const durationMsRef = useRef(durationMs);
73
+ durationMsRef.current = durationMs;
74
+
75
+ useEffect(() => {
76
+ loaderRef.current?.destroy();
77
+ loaderRef.current = null;
78
+ setObjectUrl(undefined);
79
+
80
+ if (!eligible || !sourceUrl) {
81
+ setMode("native");
82
+ return;
83
+ }
84
+
85
+ setMode("pending");
86
+ let cancelled = false;
87
+
88
+ void sniffFragmentedMp4(sourceUrl)
89
+ .then((isFragmented) => {
90
+ if (cancelled) return;
91
+ const video = videoRef.current;
92
+ if (!isFragmented || !video) {
93
+ setMode("native");
94
+ return;
95
+ }
96
+ try {
97
+ const loader = new MseVideoLoader({
98
+ url: sourceUrl,
99
+ durationMs: durationMsRef.current,
100
+ video,
101
+ onFatal: () => {
102
+ if (cancelled) return;
103
+ loaderRef.current?.destroy();
104
+ loaderRef.current = null;
105
+ setObjectUrl(undefined);
106
+ setMode("native");
107
+ },
108
+ });
109
+ loaderRef.current = loader;
110
+ setObjectUrl(loader.objectUrl);
111
+ setMode("mse");
112
+ } catch {
113
+ setMode("native");
114
+ }
115
+ })
116
+ .catch(() => {
117
+ if (!cancelled) setMode("native");
118
+ });
119
+
120
+ return () => {
121
+ cancelled = true;
122
+ loaderRef.current?.destroy();
123
+ loaderRef.current = null;
124
+ };
125
+ // videoRef is a stable ref object. `durationMs` is intentionally excluded:
126
+ // it only seeds the timeline and is pushed to the live loader by the effect
127
+ // below, so a metadata-poll update must not rebuild the loader (which would
128
+ // revoke the object URL and restart playback from byte zero).
129
+ // eslint-disable-next-line react-hooks/exhaustive-deps
130
+ }, [eligible, sourceUrl]);
131
+
132
+ // Recording metadata polling can raise the DB duration while the same
133
+ // fragmented asset is still playing; apply it to the live loader in place.
134
+ useEffect(() => {
135
+ loaderRef.current?.setDuration(durationMs);
136
+ }, [durationMs]);
137
+
138
+ const fallbackToNative = useCallback(() => {
139
+ loaderRef.current?.destroy();
140
+ loaderRef.current = null;
141
+ setObjectUrl(undefined);
142
+ setMode("native");
143
+ }, []);
144
+
145
+ return { mode, objectUrl, fallbackToNative };
146
+ }
@@ -1,3 +1,24 @@
1
+ const DESKTOP_DOWNLOAD_STORAGE_KEY = "clips.desktop-promo.dismissed";
2
+
3
+ export function hasDownloadedDesktopApp(): boolean {
4
+ try {
5
+ return (
6
+ typeof window !== "undefined" &&
7
+ window.localStorage?.getItem(DESKTOP_DOWNLOAD_STORAGE_KEY) === "1"
8
+ );
9
+ } catch {
10
+ return false;
11
+ }
12
+ }
13
+
14
+ export function markDesktopAppDownloaded(): void {
15
+ try {
16
+ window.localStorage?.setItem(DESKTOP_DOWNLOAD_STORAGE_KEY, "1");
17
+ } catch {
18
+ // Download tracking is best-effort and must not block the installer.
19
+ }
20
+ }
21
+
1
22
  function isFalsy(value: unknown): boolean {
2
23
  if (typeof value !== "string") return false;
3
24
  return ["0", "false", "no", "off"].includes(value.trim().toLowerCase());
@@ -0,0 +1,271 @@
1
+ /**
2
+ * Fragmented-MP4 helpers for the Media Source Extensions player path.
3
+ *
4
+ * The desktop custom recording pipeline live-streams captures as fragmented
5
+ * MP4 (an `ftyp`+`moov` init segment followed by ~1s `moof`/`mdat` fragments,
6
+ * brands `isom iso5 hlsf`). Those files declare no up-front duration
7
+ * (`mvhd duration=0`, no `mehd`), so Chrome's progressive `<video src>`
8
+ * pipeline scans the whole file over the network before firing
9
+ * `loadedmetadata`. We instead drive playback through MSE and supply the
10
+ * duration ourselves. These pure helpers detect that file shape and parse the
11
+ * bits of the init segment MSE needs.
12
+ *
13
+ * Everything here operates on raw bytes so it can be unit-tested without a
14
+ * browser.
15
+ */
16
+
17
+ /** Read a 4-char ASCII box type at `offset`. */
18
+ function readType(bytes: Uint8Array, offset: number): string {
19
+ return String.fromCharCode(
20
+ bytes[offset],
21
+ bytes[offset + 1],
22
+ bytes[offset + 2],
23
+ bytes[offset + 3],
24
+ );
25
+ }
26
+
27
+ /** Read a big-endian uint32 at `offset`. */
28
+ function readU32(bytes: Uint8Array, offset: number): number {
29
+ return (
30
+ ((bytes[offset] << 24) |
31
+ (bytes[offset + 1] << 16) |
32
+ (bytes[offset + 2] << 8) |
33
+ bytes[offset + 3]) >>>
34
+ 0
35
+ );
36
+ }
37
+
38
+ /** Find the first index of an ASCII marker at or after `from`, or -1. */
39
+ export function indexOfAscii(
40
+ bytes: Uint8Array,
41
+ marker: string,
42
+ from = 0,
43
+ ): number {
44
+ const len = marker.length;
45
+ const last = bytes.byteLength - len;
46
+ for (let i = Math.max(0, from); i <= last; i++) {
47
+ let match = true;
48
+ for (let j = 0; j < len; j++) {
49
+ if (bytes[i + j] !== marker.charCodeAt(j)) {
50
+ match = false;
51
+ break;
52
+ }
53
+ }
54
+ if (match) return i;
55
+ }
56
+ return -1;
57
+ }
58
+
59
+ export interface TopLevelBox {
60
+ type: string;
61
+ /** Absolute offset of the box (its size field). */
62
+ start: number;
63
+ /** Total box size in bytes, or 0 when the box runs to the end of input. */
64
+ size: number;
65
+ /** Bytes from `start` to the first byte of box payload. */
66
+ headerSize: number;
67
+ }
68
+
69
+ /**
70
+ * Walk the top-level MP4 boxes contained in `bytes`. Stops when a box header
71
+ * would run past the buffer (i.e. the buffer is a truncated head), returning
72
+ * whatever complete boxes were found.
73
+ */
74
+ export function readTopLevelBoxes(bytes: Uint8Array): TopLevelBox[] {
75
+ const boxes: TopLevelBox[] = [];
76
+ let offset = 0;
77
+ const total = bytes.byteLength;
78
+
79
+ while (offset + 8 <= total) {
80
+ let size = readU32(bytes, offset);
81
+ let headerSize = 8;
82
+ const type = readType(bytes, offset + 4);
83
+
84
+ if (size === 1) {
85
+ // 64-bit largesize. We only need the low 32 bits in practice (recordings
86
+ // never exceed 4GB), and JS bitwise math is 32-bit, so read the low word.
87
+ if (offset + 16 > total) break;
88
+ headerSize = 16;
89
+ size = readU32(bytes, offset + 12);
90
+ } else if (size === 0) {
91
+ // Box extends to end of file.
92
+ boxes.push({ type, start: offset, size: 0, headerSize });
93
+ break;
94
+ }
95
+
96
+ if (size < headerSize) break;
97
+ boxes.push({ type, start: offset, size, headerSize });
98
+ offset += size;
99
+ }
100
+
101
+ return boxes;
102
+ }
103
+
104
+ export interface ParsedInitSegment {
105
+ /** Length in bytes of the init segment (end of the `moov` box). */
106
+ initLength: number;
107
+ /** `codecs` string for `video/mp4; codecs="..."`. */
108
+ codecs: string;
109
+ hasVideo: boolean;
110
+ hasAudio: boolean;
111
+ }
112
+
113
+ /**
114
+ * Parse the init segment from the head of a fragmented MP4. `bytes` must
115
+ * contain the whole `ftyp`+`moov` prefix (a few hundred KB is always enough —
116
+ * the init segment carries no per-fragment data). Returns null when `moov` is
117
+ * incomplete or no usable video codec was found.
118
+ */
119
+ export function parseInitSegment(bytes: Uint8Array): ParsedInitSegment | null {
120
+ const boxes = readTopLevelBoxes(bytes);
121
+ const moov = boxes.find((b) => b.type === "moov");
122
+ if (!moov || moov.size === 0) return null;
123
+ const moovEnd = moov.start + moov.size;
124
+ if (moovEnd > bytes.byteLength) return null; // moov not fully present yet
125
+
126
+ const moovRegion = bytes.subarray(moov.start, moovEnd);
127
+ const videoCodec = parseAvcCodec(moovRegion);
128
+ const hasAudio = indexOfAscii(moovRegion, "mp4a") !== -1;
129
+ const hasVideo = Boolean(videoCodec);
130
+
131
+ if (!hasVideo && !hasAudio) return null;
132
+
133
+ const codecParts: string[] = [];
134
+ if (videoCodec) codecParts.push(videoCodec);
135
+ // Clips fMP4 audio is always AAC-LC 48kHz stereo → mp4a.40.2.
136
+ if (hasAudio) codecParts.push("mp4a.40.2");
137
+
138
+ return {
139
+ initLength: moovEnd,
140
+ codecs: codecParts.join(","),
141
+ hasVideo,
142
+ hasAudio,
143
+ };
144
+ }
145
+
146
+ function hex2(value: number): string {
147
+ return value.toString(16).padStart(2, "0");
148
+ }
149
+
150
+ /**
151
+ * Build the `avc1.PPCCLL` codec string from the `avcC` configuration box within
152
+ * `moovRegion` (PP=profile, CC=profile-compatibility, LL=level). Returns null
153
+ * when no `avcC` box is present.
154
+ */
155
+ export function parseAvcCodec(moovRegion: Uint8Array): string | null {
156
+ const marker = indexOfAscii(moovRegion, "avcC");
157
+ if (marker === -1) return null;
158
+ // avcC payload: [0] configurationVersion, [1] AVCProfileIndication,
159
+ // [2] profile_compatibility, [3] AVCLevelIndication.
160
+ const config = marker + 4;
161
+ if (config + 4 > moovRegion.byteLength) return null;
162
+ const profile = moovRegion[config + 1];
163
+ const compat = moovRegion[config + 2];
164
+ const level = moovRegion[config + 3];
165
+ return `avc1.${hex2(profile)}${hex2(compat)}${hex2(level)}`;
166
+ }
167
+
168
+ /**
169
+ * Find the byte offset (relative to `bytes`) of the first `moof` box start — the
170
+ * fragment boundary MSE must begin an append at. `bytes` is a chunk fetched at
171
+ * an arbitrary byte position, so this scans for the `moof` type marker and
172
+ * backs up 4 bytes to the box size field. Returns -1 when none is found.
173
+ */
174
+ export function findMoofOffset(bytes: Uint8Array): number {
175
+ let from = 0;
176
+ for (;;) {
177
+ const marker = indexOfAscii(bytes, "moof", from);
178
+ if (marker === -1) return -1;
179
+ const boxStart = marker - 4;
180
+ from = marker + 4;
181
+ if (boxStart < 0) continue;
182
+
183
+ const size = readU32(bytes, boxStart);
184
+ // A moof box is small; a bogus size means we hit the ASCII "moof" inside
185
+ // mdat payload rather than a real box header.
186
+ if (size < 16 || size > 16 * 1024 * 1024) continue;
187
+
188
+ // A real moof always begins with an `mfhd` child box (payload starts at
189
+ // boxStart+8, whose first child type sits at boxStart+12).
190
+ if (boxStart + 16 > bytes.byteLength) continue;
191
+ if (readType(bytes, boxStart + 12) !== "mfhd") continue;
192
+
193
+ // ...and is immediately followed by its `mdat`. Require this when the
194
+ // follower is within the buffer; near the tail we accept the validated moof.
195
+ const nextBox = boxStart + size;
196
+ if (
197
+ nextBox + 8 <= bytes.byteLength &&
198
+ readType(bytes, nextBox + 4) !== "mdat"
199
+ ) {
200
+ continue;
201
+ }
202
+
203
+ return boxStart;
204
+ }
205
+ }
206
+
207
+ /**
208
+ * True when the head of an MP4 shows the fragmented shape: the `hlsf` brand in
209
+ * `ftyp`, or an `mvex` box inside `moov` (present only in fragmented files).
210
+ * `headBytes` should be the first few KB of the file.
211
+ */
212
+ export function isFragmentedMp4Head(headBytes: Uint8Array): boolean {
213
+ if (headBytes.byteLength < 8) return false;
214
+ // Must look like an MP4 at all.
215
+ if (indexOfAscii(headBytes, "ftyp") !== 4) return false;
216
+ if (indexOfAscii(headBytes, "hlsf") !== -1) return true;
217
+ if (indexOfAscii(headBytes, "mvex") !== -1) return true;
218
+ return false;
219
+ }
220
+
221
+ /** Cache detection by URL identity so we sniff each asset only once. */
222
+ const detectionCache = new Map<string, Promise<boolean>>();
223
+
224
+ function detectionKey(url: string): string {
225
+ try {
226
+ const base =
227
+ typeof window === "undefined"
228
+ ? "http://clips.local"
229
+ : window.location.href;
230
+ const parsed = new URL(url, base);
231
+ // Strip volatile auth/cache-bust params so the same asset shares one entry.
232
+ return `${parsed.origin}${parsed.pathname}`;
233
+ } catch {
234
+ return url;
235
+ }
236
+ }
237
+
238
+ const SNIFF_BYTES = 4096;
239
+
240
+ /**
241
+ * Range-fetch the first few KB of an asset and report whether it is a raw
242
+ * fragmented MP4 that needs the MSE path. Cached per asset; resolves false on
243
+ * any network/parse error so callers fall back to the native `<video src>`.
244
+ */
245
+ export async function sniffFragmentedMp4(url: string): Promise<boolean> {
246
+ const key = detectionKey(url);
247
+ const cached = detectionCache.get(key);
248
+ if (cached) return cached;
249
+
250
+ const promise = (async () => {
251
+ try {
252
+ const res = await fetch(url, {
253
+ headers: { Range: `bytes=0-${SNIFF_BYTES - 1}` },
254
+ });
255
+ if (!res.ok) return false;
256
+ const head = new Uint8Array(await res.arrayBuffer());
257
+ return isFragmentedMp4Head(head);
258
+ } catch {
259
+ return false;
260
+ }
261
+ })();
262
+
263
+ detectionCache.set(key, promise);
264
+ // Don't cache a rejected/false-by-error result forever if it was a transient
265
+ // network blip: drop the entry when it resolves false so a later retry can
266
+ // re-sniff, but keep positive detections cached.
267
+ void promise.then((isFrag) => {
268
+ if (!isFrag) detectionCache.delete(key);
269
+ });
270
+ return promise;
271
+ }