@agent-native/core 0.100.2 → 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 (345) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +65 -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/analytics.ts +8 -1
  37. package/corpus/core/src/client/chat/markdown-renderer.tsx +54 -8
  38. package/corpus/core/src/client/chat/message-components.tsx +8 -2
  39. package/corpus/core/src/client/clipboard.ts +28 -1
  40. package/corpus/core/src/client/guided-questions.tsx +4 -0
  41. package/corpus/core/src/client/i18n.tsx +2 -1
  42. package/corpus/core/src/client/index.ts +4 -0
  43. package/corpus/core/src/client/review/ReviewCommentComposer.tsx +108 -0
  44. package/corpus/core/src/client/review/ReviewThreadPanel.tsx +482 -150
  45. package/corpus/core/src/client/review/index.ts +8 -0
  46. package/corpus/core/src/client/review/use-review.ts +35 -1
  47. package/corpus/core/src/client/use-agent-chat-context.ts +3 -2
  48. package/corpus/core/src/review/actions/create-review-comment.ts +2 -1
  49. package/corpus/core/src/review/actions/get-review-feedback.ts +6 -11
  50. package/corpus/core/src/review/actions/list-review-comments.ts +38 -4
  51. package/corpus/core/src/review/actions/reply-review-comment.ts +33 -22
  52. package/corpus/core/src/review/actions/resolve-review-thread.ts +29 -2
  53. package/corpus/core/src/review/actions/send-review-thread-to-agent.ts +62 -0
  54. package/corpus/core/src/review/identity.ts +101 -0
  55. package/corpus/core/src/review/index.ts +14 -0
  56. package/corpus/core/src/review/store.ts +315 -8
  57. package/corpus/core/src/review/types.ts +4 -0
  58. package/corpus/core/src/server/action-discovery.ts +4 -0
  59. package/corpus/core/src/server/action-routes.ts +125 -4
  60. package/corpus/core/src/server/agent-chat/plugin-options.ts +16 -0
  61. package/corpus/core/src/server/agent-chat-plugin.ts +1 -0
  62. package/corpus/core/src/server/index.ts +8 -0
  63. package/corpus/core/src/vite/action-types-plugin.ts +4 -0
  64. package/corpus/templates/analytics/changelog/2026-07-14-daily-dashboard-email-captures-authenticate-with-a-sessi.md +6 -0
  65. package/corpus/templates/analytics/server/lib/dashboard-report.ts +156 -3
  66. package/corpus/templates/assets/app/components/generation/GenerationResults.tsx +402 -142
  67. package/corpus/templates/assets/app/i18n/zh-TW.ts +1 -0
  68. package/corpus/templates/assets/app/i18n-data.ts +1 -0
  69. package/corpus/templates/assets/app/routes/library.tsx +187 -48
  70. package/corpus/templates/assets/changelog/2026-07-13-merged-the-candidates-panel-into-the-library-as-a-drafts-tab.md +6 -0
  71. package/corpus/templates/chat/app/lib/agent-page.tsx +40 -0
  72. package/corpus/templates/chat/app/routes/agent.tsx +5 -3
  73. package/corpus/templates/chat/changelog/2026-07-14-fixed-chat-template-startup-with-older-core-versions.md +6 -0
  74. package/corpus/templates/clips/.agents/skills/ai-video-tools/SKILL.md +5 -0
  75. package/corpus/templates/clips/AGENTS.md +4 -0
  76. package/corpus/templates/clips/actions/get-feature-flags.ts +30 -0
  77. package/corpus/templates/clips/actions/lib/transcript-preview.ts +48 -0
  78. package/corpus/templates/clips/actions/request-transcript.ts +41 -1
  79. package/corpus/templates/clips/actions/view-screen.ts +5 -9
  80. package/corpus/templates/clips/app/components/library/library-layout.tsx +1 -1
  81. package/corpus/templates/clips/app/components/player/video-player.tsx +39 -1
  82. package/corpus/templates/clips/app/hooks/use-desktop-promo.ts +6 -12
  83. package/corpus/templates/clips/app/hooks/use-mse-video-source.ts +146 -0
  84. package/corpus/templates/clips/app/lib/capture-install-options.ts +21 -0
  85. package/corpus/templates/clips/app/lib/fmp4.ts +271 -0
  86. package/corpus/templates/clips/app/lib/mse-video-loader.ts +585 -0
  87. package/corpus/templates/clips/app/routes/_app.dictate.tsx +0 -4
  88. package/corpus/templates/clips/app/routes/_app.meetings.$meetingId.tsx +5 -13
  89. package/corpus/templates/clips/app/routes/_app.meetings._index.tsx +2 -37
  90. package/corpus/templates/clips/app/routes/download.tsx +3 -2
  91. package/corpus/templates/clips/changelog/2026-07-10-shared-clips-now-start-playing-instantly-instead-of-showing-.md +6 -0
  92. package/corpus/templates/clips/changelog/2026-07-14-clips-now-distinguishes-transcript-previews-from-incomplete-.md +6 -0
  93. package/corpus/templates/clips/changelog/2026-07-14-desktop-app-prompts-stay-hidden-after-you-download-the-insta.md +6 -0
  94. package/corpus/templates/clips/desktop/src/lib/recorder.ts +5 -0
  95. package/corpus/templates/clips/desktop/src/overlays/recording-pill.tsx +10 -0
  96. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +4 -0
  97. package/corpus/templates/clips/desktop/src-tauri/src/native_screen/custom_capture.rs +2518 -0
  98. package/corpus/templates/clips/desktop/src-tauri/src/native_screen/live_upload.rs +353 -0
  99. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +558 -67
  100. package/corpus/templates/clips/desktop/src-tauri/src/recording_indicator.rs +11 -1
  101. package/corpus/templates/clips/desktop/src-tauri/src/remote_flags.rs +188 -0
  102. package/corpus/templates/clips/desktop/src-tauri/src/whisper_speech.rs +1 -1
  103. package/corpus/templates/clips/server/lib/post-finalize-dispatch.ts +1 -0
  104. package/corpus/templates/clips/server/plugins/agent-chat.ts +5 -0
  105. package/corpus/templates/clips/server/routes/api/_agent-native-background/post-finalize-worker.post.ts +5 -1
  106. package/corpus/templates/clips/shared/feature-flags.ts +41 -0
  107. package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +3 -0
  108. package/corpus/templates/design/.agents/skills/design-review-feedback/SKILL.md +42 -0
  109. package/corpus/templates/design/AGENTS.md +6 -0
  110. package/corpus/templates/design/README.md +1 -0
  111. package/corpus/templates/design/actions/navigate.ts +4 -4
  112. package/corpus/templates/design/actions/view-screen.ts +116 -2
  113. package/corpus/templates/design/agent-native.app-skill.json +5 -0
  114. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +52 -59
  115. package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +13 -9
  116. package/corpus/templates/design/app/components/design/EditPanel.tsx +79 -11
  117. package/corpus/templates/design/app/components/design/LayersPanel.tsx +78 -65
  118. package/corpus/templates/design/app/components/design/ReviewCommentsPanel.tsx +161 -0
  119. package/corpus/templates/design/app/components/design/ReviewStatusControl.tsx +96 -0
  120. package/corpus/templates/design/app/components/design/bridge/hit-test.bridge.ts +166 -0
  121. package/corpus/templates/design/app/components/layout/Layout.tsx +1 -0
  122. package/corpus/templates/design/app/components/visual-editor/ReviewCanvasPins.tsx +1077 -0
  123. package/corpus/templates/design/app/components/visual-editor/index.ts +1 -0
  124. package/corpus/templates/design/app/components/visual-editor/review-canvas-state.ts +42 -0
  125. package/corpus/templates/design/app/hooks/use-design-systems.ts +2 -2
  126. package/corpus/templates/design/app/hooks/use-navigation-state.ts +9 -6
  127. package/corpus/templates/design/app/hooks/use-question-flow.ts +7 -1
  128. package/corpus/templates/design/app/i18n/ar-SA.ts +56 -1
  129. package/corpus/templates/design/app/i18n/de-DE.ts +57 -1
  130. package/corpus/templates/design/app/i18n/en-US.ts +56 -1
  131. package/corpus/templates/design/app/i18n/es-ES.ts +56 -1
  132. package/corpus/templates/design/app/i18n/fr-FR.ts +57 -1
  133. package/corpus/templates/design/app/i18n/hi-IN.ts +56 -1
  134. package/corpus/templates/design/app/i18n/ja-JP.ts +56 -1
  135. package/corpus/templates/design/app/i18n/ko-KR.ts +56 -1
  136. package/corpus/templates/design/app/i18n/pt-BR.ts +57 -1
  137. package/corpus/templates/design/app/i18n/zh-CN.ts +56 -1
  138. package/corpus/templates/design/app/i18n/zh-TW.ts +51 -0
  139. package/corpus/templates/design/app/pages/Present.tsx +172 -14
  140. package/corpus/templates/design/app/pages/design-editor/overview-camera.ts +23 -0
  141. package/corpus/templates/design/app/pages/design-editor/tool-state.ts +10 -0
  142. package/corpus/templates/design/app/pages/present-review-state.ts +18 -0
  143. package/corpus/templates/design/app/public-routes.ts +8 -0
  144. package/corpus/templates/design/app/root.tsx +7 -6
  145. package/corpus/templates/design/changelog/2026-07-13-dragged-design-assets-now-appear-at-the-visible-drop-locatio.md +6 -0
  146. package/corpus/templates/design/changelog/2026-07-13-reviewers-can-pin-comments-on-shared-designs-and-apply-verif.md +6 -0
  147. package/corpus/templates/design/server/plugins/agent-chat.ts +10 -0
  148. package/corpus/templates/design/server/plugins/auth.ts +7 -5
  149. package/corpus/templates/design/server/plugins/review.ts +9 -0
  150. package/corpus/templates/design/shared/review-anchor.ts +83 -0
  151. package/corpus/templates/design/shared/review-summary.ts +25 -0
  152. package/corpus/templates/forms/.agents/skills/form-publishing/SKILL.md +2 -0
  153. package/corpus/templates/forms/AGENTS.md +4 -0
  154. package/corpus/templates/forms/actions/create-form.ts +2 -1
  155. package/corpus/templates/forms/actions/update-form.ts +5 -2
  156. package/corpus/templates/forms/app/i18n/ar-SA.ts +3 -0
  157. package/corpus/templates/forms/app/i18n/de-DE.ts +3 -0
  158. package/corpus/templates/forms/app/i18n/en-US.ts +3 -0
  159. package/corpus/templates/forms/app/i18n/es-ES.ts +3 -0
  160. package/corpus/templates/forms/app/i18n/fr-FR.ts +3 -0
  161. package/corpus/templates/forms/app/i18n/hi-IN.ts +3 -0
  162. package/corpus/templates/forms/app/i18n/ja-JP.ts +3 -0
  163. package/corpus/templates/forms/app/i18n/ko-KR.ts +3 -0
  164. package/corpus/templates/forms/app/i18n/pt-BR.ts +3 -0
  165. package/corpus/templates/forms/app/i18n/zh-CN.ts +3 -0
  166. package/corpus/templates/forms/app/i18n/zh-TW.ts +3 -0
  167. package/corpus/templates/forms/app/pages/FormBuilderPage.tsx +19 -0
  168. package/corpus/templates/forms/changelog/2026-07-14-fixed-the-ask-forms-loading-input-appearing-above-the-welcom.md +6 -0
  169. package/corpus/templates/forms/changelog/2026-07-14-form-social-previews-now-show-the-form-title-description-and.md +6 -0
  170. package/corpus/templates/forms/changelog/2026-07-14-forms-can-email-the-form-owner-when-new-responses-arrive.md +6 -0
  171. package/corpus/templates/forms/server/handlers/submissions.ts +17 -0
  172. package/corpus/templates/forms/server/lib/form-og-image.ts +64 -6
  173. package/corpus/templates/forms/server/lib/public-form-ssr.ts +6 -2
  174. package/corpus/templates/forms/server/lib/response-email.ts +65 -0
  175. package/corpus/templates/forms/server/plugins/agent-chat.ts +1 -0
  176. package/corpus/templates/forms/server/routes/api/forms/og/[...slug]/og.png.get.ts +184 -0
  177. package/corpus/templates/forms/shared/types.ts +2 -0
  178. package/corpus/templates/plan/.agents/skills/visual-plan/SKILL.md +6 -4
  179. package/corpus/templates/plan/.agents/skills/visual-recap/SKILL.md +7 -1
  180. package/dist/a2a/index.d.ts +2 -1
  181. package/dist/a2a/index.d.ts.map +1 -1
  182. package/dist/a2a/index.js +1 -1
  183. package/dist/a2a/index.js.map +1 -1
  184. package/dist/a2a/server.d.ts +25 -0
  185. package/dist/a2a/server.d.ts.map +1 -1
  186. package/dist/a2a/server.js +30 -6
  187. package/dist/a2a/server.js.map +1 -1
  188. package/dist/agent/production-agent.d.ts +2 -0
  189. package/dist/agent/production-agent.d.ts.map +1 -1
  190. package/dist/agent/production-agent.js +37 -12
  191. package/dist/agent/production-agent.js.map +1 -1
  192. package/dist/cli/create.d.ts +5 -3
  193. package/dist/cli/create.d.ts.map +1 -1
  194. package/dist/cli/create.js +44 -19
  195. package/dist/cli/create.js.map +1 -1
  196. package/dist/cli/pr-visual-recap-workflow.d.ts +2 -2
  197. package/dist/cli/pr-visual-recap-workflow.d.ts.map +1 -1
  198. package/dist/cli/pr-visual-recap-workflow.js +2 -2
  199. package/dist/cli/pr-visual-recap-workflow.js.map +1 -1
  200. package/dist/cli/recap.d.ts +3 -562
  201. package/dist/cli/recap.d.ts.map +1 -1
  202. package/dist/cli/recap.js +3 -3875
  203. package/dist/cli/recap.js.map +1 -1
  204. package/dist/cli/skills-content/visual-plan-skill.d.ts +1 -1
  205. package/dist/cli/skills-content/visual-plan-skill.d.ts.map +1 -1
  206. package/dist/cli/skills-content/visual-plan-skill.js +6 -4
  207. package/dist/cli/skills-content/visual-plan-skill.js.map +1 -1
  208. package/dist/cli/skills-content/visual-recap-skill.d.ts +1 -1
  209. package/dist/cli/skills-content/visual-recap-skill.d.ts.map +1 -1
  210. package/dist/cli/skills-content/visual-recap-skill.js +7 -1
  211. package/dist/cli/skills-content/visual-recap-skill.js.map +1 -1
  212. package/dist/client/AgentPanel.d.ts.map +1 -1
  213. package/dist/client/AgentPanel.js +3 -3
  214. package/dist/client/AgentPanel.js.map +1 -1
  215. package/dist/client/analytics.d.ts +5 -0
  216. package/dist/client/analytics.d.ts.map +1 -1
  217. package/dist/client/analytics.js +3 -1
  218. package/dist/client/analytics.js.map +1 -1
  219. package/dist/client/chat/markdown-renderer.d.ts +1 -0
  220. package/dist/client/chat/markdown-renderer.d.ts.map +1 -1
  221. package/dist/client/chat/markdown-renderer.js +36 -1
  222. package/dist/client/chat/markdown-renderer.js.map +1 -1
  223. package/dist/client/chat/message-components.d.ts.map +1 -1
  224. package/dist/client/chat/message-components.js +5 -2
  225. package/dist/client/chat/message-components.js.map +1 -1
  226. package/dist/client/clipboard.d.ts +3 -1
  227. package/dist/client/clipboard.d.ts.map +1 -1
  228. package/dist/client/clipboard.js +24 -1
  229. package/dist/client/clipboard.js.map +1 -1
  230. package/dist/client/guided-questions.d.ts +3 -1
  231. package/dist/client/guided-questions.d.ts.map +1 -1
  232. package/dist/client/guided-questions.js +2 -1
  233. package/dist/client/guided-questions.js.map +1 -1
  234. package/dist/client/i18n.d.ts.map +1 -1
  235. package/dist/client/i18n.js +3 -1
  236. package/dist/client/i18n.js.map +1 -1
  237. package/dist/client/index.d.ts +1 -1
  238. package/dist/client/index.d.ts.map +1 -1
  239. package/dist/client/index.js +1 -1
  240. package/dist/client/index.js.map +1 -1
  241. package/dist/client/review/ReviewCommentComposer.d.ts +18 -0
  242. package/dist/client/review/ReviewCommentComposer.d.ts.map +1 -0
  243. package/dist/client/review/ReviewCommentComposer.js +30 -0
  244. package/dist/client/review/ReviewCommentComposer.js.map +1 -0
  245. package/dist/client/review/ReviewThreadPanel.d.ts +28 -1
  246. package/dist/client/review/ReviewThreadPanel.d.ts.map +1 -1
  247. package/dist/client/review/ReviewThreadPanel.js +151 -39
  248. package/dist/client/review/ReviewThreadPanel.js.map +1 -1
  249. package/dist/client/review/index.d.ts +3 -2
  250. package/dist/client/review/index.d.ts.map +1 -1
  251. package/dist/client/review/index.js +2 -1
  252. package/dist/client/review/index.js.map +1 -1
  253. package/dist/client/review/use-review.d.ts +23 -0
  254. package/dist/client/review/use-review.d.ts.map +1 -1
  255. package/dist/client/review/use-review.js +3 -0
  256. package/dist/client/review/use-review.js.map +1 -1
  257. package/dist/client/use-agent-chat-context.d.ts +1 -1
  258. package/dist/client/use-agent-chat-context.d.ts.map +1 -1
  259. package/dist/client/use-agent-chat-context.js +4 -2
  260. package/dist/client/use-agent-chat-context.js.map +1 -1
  261. package/dist/collab/awareness.d.ts +2 -2
  262. package/dist/collab/awareness.d.ts.map +1 -1
  263. package/dist/collab/struct-routes.d.ts +1 -1
  264. package/dist/notifications/routes.d.ts +3 -3
  265. package/dist/observability/routes.d.ts +5 -5
  266. package/dist/progress/routes.d.ts +1 -1
  267. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  268. package/dist/resources/handlers.d.ts +2 -2
  269. package/dist/review/actions/create-review-comment.js +2 -1
  270. package/dist/review/actions/create-review-comment.js.map +1 -1
  271. package/dist/review/actions/get-review-feedback.js +6 -11
  272. package/dist/review/actions/get-review-feedback.js.map +1 -1
  273. package/dist/review/actions/list-review-comments.d.ts +1 -0
  274. package/dist/review/actions/list-review-comments.js +25 -4
  275. package/dist/review/actions/list-review-comments.js.map +1 -1
  276. package/dist/review/actions/reply-review-comment.js +12 -5
  277. package/dist/review/actions/reply-review-comment.js.map +1 -1
  278. package/dist/review/actions/resolve-review-thread.d.ts +3 -0
  279. package/dist/review/actions/resolve-review-thread.js +20 -3
  280. package/dist/review/actions/resolve-review-thread.js.map +1 -1
  281. package/dist/review/actions/send-review-thread-to-agent.d.ts +17 -0
  282. package/dist/review/actions/send-review-thread-to-agent.d.ts.map +1 -0
  283. package/dist/review/actions/send-review-thread-to-agent.js +49 -0
  284. package/dist/review/actions/send-review-thread-to-agent.js.map +1 -0
  285. package/dist/review/identity.d.ts +6 -0
  286. package/dist/review/identity.d.ts.map +1 -0
  287. package/dist/review/identity.js +68 -0
  288. package/dist/review/identity.js.map +1 -0
  289. package/dist/review/index.d.ts +3 -1
  290. package/dist/review/index.d.ts.map +1 -1
  291. package/dist/review/index.js +2 -1
  292. package/dist/review/index.js.map +1 -1
  293. package/dist/review/store.d.ts +36 -0
  294. package/dist/review/store.d.ts.map +1 -1
  295. package/dist/review/store.js +223 -8
  296. package/dist/review/store.js.map +1 -1
  297. package/dist/review/types.d.ts +4 -0
  298. package/dist/review/types.d.ts.map +1 -1
  299. package/dist/review/types.js.map +1 -1
  300. package/dist/secrets/routes.d.ts +9 -9
  301. package/dist/server/action-discovery.d.ts.map +1 -1
  302. package/dist/server/action-discovery.js +4 -0
  303. package/dist/server/action-discovery.js.map +1 -1
  304. package/dist/server/action-routes.d.ts +45 -0
  305. package/dist/server/action-routes.d.ts.map +1 -1
  306. package/dist/server/action-routes.js +71 -5
  307. package/dist/server/action-routes.js.map +1 -1
  308. package/dist/server/agent-chat/plugin-options.d.ts +16 -0
  309. package/dist/server/agent-chat/plugin-options.d.ts.map +1 -1
  310. package/dist/server/agent-chat/plugin-options.js.map +1 -1
  311. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  312. package/dist/server/agent-chat-plugin.js +1 -0
  313. package/dist/server/agent-chat-plugin.js.map +1 -1
  314. package/dist/server/index.d.ts +3 -1
  315. package/dist/server/index.d.ts.map +1 -1
  316. package/dist/server/index.js +2 -0
  317. package/dist/server/index.js.map +1 -1
  318. package/dist/vite/action-types-plugin.d.ts.map +1 -1
  319. package/dist/vite/action-types-plugin.js +4 -0
  320. package/dist/vite/action-types-plugin.js.map +1 -1
  321. package/docs/content/locales/ar-SA/pr-visual-recap.mdx +48 -30
  322. package/docs/content/locales/ar-SA/template-plan.mdx +7 -0
  323. package/docs/content/locales/de-DE/pr-visual-recap.mdx +48 -30
  324. package/docs/content/locales/de-DE/template-plan.mdx +7 -0
  325. package/docs/content/locales/es-ES/pr-visual-recap.mdx +48 -30
  326. package/docs/content/locales/es-ES/template-plan.mdx +14 -0
  327. package/docs/content/locales/fr-FR/pr-visual-recap.mdx +48 -30
  328. package/docs/content/locales/fr-FR/template-plan.mdx +7 -0
  329. package/docs/content/locales/hi-IN/pr-visual-recap.mdx +48 -30
  330. package/docs/content/locales/hi-IN/template-plan.mdx +7 -0
  331. package/docs/content/locales/ja-JP/pr-visual-recap.mdx +48 -30
  332. package/docs/content/locales/ja-JP/template-plan.mdx +7 -0
  333. package/docs/content/locales/ko-KR/pr-visual-recap.mdx +48 -30
  334. package/docs/content/locales/ko-KR/template-plan.mdx +7 -0
  335. package/docs/content/locales/pt-BR/pr-visual-recap.mdx +48 -30
  336. package/docs/content/locales/pt-BR/template-plan.mdx +14 -0
  337. package/docs/content/locales/zh-CN/pr-visual-recap.mdx +48 -30
  338. package/docs/content/locales/zh-CN/template-plan.mdx +7 -0
  339. package/docs/content/locales/zh-TW/pr-visual-recap.mdx +48 -30
  340. package/docs/content/locales/zh-TW/template-plan.mdx +7 -0
  341. package/docs/content/pr-visual-recap.mdx +50 -32
  342. package/docs/content/template-plan.mdx +14 -0
  343. package/docs/content/toolkit-comments-review.mdx +16 -2
  344. package/package.json +4 -1
  345. package/corpus/templates/forms/server/routes/api/forms/og/[slug]/og.png.get.ts +0 -45
@@ -1,7 +1,7 @@
1
1
  use chrono::Utc;
2
2
  use serde::{Deserialize, Serialize};
3
3
  use std::fs::File;
4
- use std::io::{ErrorKind, Read};
4
+ use std::io::{ErrorKind, Read, Seek, SeekFrom};
5
5
  use std::path::{Path, PathBuf};
6
6
  use std::process::{Child, Command, Stdio};
7
7
  use std::sync::{Arc, Condvar, Mutex, OnceLock};
@@ -24,15 +24,22 @@ use screencapturekit::shareable_content::SCShareableContent;
24
24
  #[cfg(target_os = "macos")]
25
25
  use screencapturekit::stream::{
26
26
  configuration::SCStreamConfiguration, content_filter::SCContentFilter,
27
- output_type::SCStreamOutputType, sc_stream::SCStream,
27
+ output_trait::SCStreamOutputTrait, output_type::SCStreamOutputType, sc_stream::SCStream,
28
28
  };
29
29
  use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
30
30
 
31
31
  pub(crate) const QUICKTIME_RECORDING_MIME_TYPE: &str = "video/quicktime";
32
32
  pub(crate) const MP4_RECORDING_MIME_TYPE: &str = "video/mp4";
33
+ /// Prefix tagging a capture-side finalize/write failure (segment-sink disk
34
+ /// write error, AVAssetWriter finalize failure/timeout) as opposed to a benign
35
+ /// `stop_capture` error. The upload path fails closed on this: the on-disk file
36
+ /// is incomplete even when its init-segment `moov` is present, so uploading
37
+ /// would silently publish a truncated clip.
38
+ const CAPTURE_FINALIZE_INCOMPLETE_PREFIX: &str = "capture finalize incomplete: ";
33
39
  // Keep native chunks comfortably under serverless request/event limits.
34
- const UPLOAD_CHUNK_BYTES: usize = 3 * 1024 * 1024;
35
- // Master switch for native transcoding/compression.
40
+ const GCS_CHUNK_ALIGN_BYTES: usize = 256 * 1024;
41
+ const UPLOAD_CHUNK_BYTES: usize = 15 * GCS_CHUNK_ALIGN_BYTES; // 3.75 MiB
42
+ // Master switch for native transcoding/compression.
36
43
  const COMPRESSION_ENABLED: bool = true;
37
44
  const TRANSCODE_THRESHOLD_BYTES: u64 = 24 * 1024 * 1024;
38
45
  const TARGET_UPLOAD_BYTES: u64 = 18 * 1024 * 1024;
@@ -100,6 +107,22 @@ fn audio_filter_chain(downmix: bool, denoise: bool, mic_pregain: bool) -> String
100
107
  }
101
108
  const NATIVE_CAPTURE_MAX_LONG_EDGE: u32 = 1280;
102
109
  const NATIVE_CAPTURE_FPS: u32 = 24;
110
+
111
+ // Custom ScreenCaptureKit capture engine: AVAssetWriter fragmented-MP4
112
+ // writer, live audio mixer, and the AVFoundation FFI glue live in a child
113
+ // module; this file keeps session orchestration, upload, and recovery.
114
+ #[cfg(target_os = "macos")]
115
+ mod custom_capture;
116
+ #[cfg(target_os = "macos")]
117
+ use custom_capture::{
118
+ start_custom_screencapturekit_backend_at, CustomCaptureResume, CustomScreenCaptureWriter,
119
+ };
120
+ // Live chunk uploader: tails the fragmented MP4 and streams it during
121
+ // recording; attached/finalized/abandoned from the session logic here.
122
+ #[cfg(target_os = "macos")]
123
+ mod live_upload;
124
+ #[cfg(target_os = "macos")]
125
+ use live_upload::{attach_live_uploader_to_session, LiveUpload};
103
126
  const AVCONVERT_PATH: &str = "/usr/bin/avconvert";
104
127
  const AVCONVERT_TIMEOUT: Duration = Duration::from_secs(5 * 60);
105
128
  const FFMPEG_TIMEOUT: Duration = Duration::from_secs(8 * 60);
@@ -229,6 +252,16 @@ struct NativeFullscreenSession {
229
252
  /// warming up) but the recording output hasn't been attached yet, so
230
253
  /// nothing is written to disk. `begin` attaches it and flips this false.
231
254
  pending_recording_output: bool,
255
+ custom_pipeline: bool,
256
+ /// Active live-upload task: streams the growing fragmented MP4 to the
257
+ /// server in chunks during recording (custom pipeline only). `None` when
258
+ /// live upload is disabled or for local-only recordings.
259
+ #[cfg(target_os = "macos")]
260
+ live_upload: Option<LiveUpload>,
261
+ /// True if a live uploader was ever attached. If the uploader is later
262
+ /// abandoned (e.g. pause makes the recording multi-segment), the stop path
263
+ /// must reset already-uploaded chunks before re-uploading the whole file.
264
+ had_live_upload: bool,
232
265
  /// Stop flag for the background disk-space monitor thread. Set to true
233
266
  /// when the session is finalized or discarded so the thread exits cleanly.
234
267
  disk_monitor_stop: Option<Arc<AtomicBool>>,
@@ -273,6 +306,24 @@ pub(crate) enum NativeFullscreenBackend {
273
306
  /// never saw mic samples" from "samples existed but encoded silent".
274
307
  mic_sample_count: Option<Arc<AtomicU64>>,
275
308
  },
309
+ #[cfg(target_os = "macos")]
310
+ CustomScreenCaptureKit {
311
+ /// Behind `Arc<Mutex>` because the capture watchdog can swap in a
312
+ /// rebuilt stream after an interruption while the stop path also needs
313
+ /// to reach it. Lock is only held for brief `stop_capture`/swap calls.
314
+ stream: Arc<Mutex<SCStream>>,
315
+ writer: CustomScreenCaptureWriter,
316
+ mic_ready: Option<Arc<AtomicBool>>,
317
+ recording_enabled: Arc<AtomicBool>,
318
+ /// Signals the watchdog thread to stop supervising (and never rebuild
319
+ /// the stream again) as the session is being torn down.
320
+ watchdog_shutdown: Arc<AtomicBool>,
321
+ /// Handles for the soft pause/resume path: stop only the capture
322
+ /// source on pause and splice a fresh SCStream onto the same writer on
323
+ /// resume, keeping one continuous append-only file so live upload is
324
+ /// never interrupted.
325
+ resume: CustomCaptureResume,
326
+ },
276
327
  }
277
328
 
278
329
  /// Safety net for the `screencapture` fallback: if a session carrying a live
@@ -284,11 +335,25 @@ pub(crate) enum NativeFullscreenBackend {
284
335
  /// the place to block on graceful finalization.
285
336
  impl Drop for NativeFullscreenBackend {
286
337
  fn drop(&mut self) {
287
- if let NativeFullscreenBackend::Screencapture { child } = self {
288
- if matches!(child.try_wait(), Ok(None)) {
289
- let _ = child.kill();
290
- let _ = child.wait();
338
+ match self {
339
+ NativeFullscreenBackend::Screencapture { child } => {
340
+ if matches!(child.try_wait(), Ok(None)) {
341
+ let _ = child.kill();
342
+ let _ = child.wait();
343
+ }
291
344
  }
345
+ // Guarantee the capture watchdog stops supervising even if a session
346
+ // is dropped without going through the normal stop path (panic
347
+ // unwind, stale-session displacement). Otherwise it would keep the
348
+ // SCStream alive and rebuild it forever.
349
+ #[cfg(target_os = "macos")]
350
+ NativeFullscreenBackend::CustomScreenCaptureKit {
351
+ watchdog_shutdown, ..
352
+ } => {
353
+ watchdog_shutdown.store(true, Ordering::SeqCst);
354
+ }
355
+ #[cfg(target_os = "macos")]
356
+ NativeFullscreenBackend::ScreenCaptureKit { .. } => {}
292
357
  }
293
358
  }
294
359
  }
@@ -584,6 +649,8 @@ struct SavedNativeRecording {
584
649
  last_attempt_at: Option<String>,
585
650
  last_error: Option<String>,
586
651
  retry_count: u32,
652
+ #[serde(default)]
653
+ custom_pipeline: bool,
587
654
  /// True when the SCK finalization callback reported an error, meaning the
588
655
  /// MP4 is missing its moov atom and cannot be recovered by retrying.
589
656
  #[serde(default)]
@@ -870,6 +937,10 @@ pub async fn native_fullscreen_recording_begin(
870
937
  mic_device_id: Option<String>,
871
938
  mic_device_label: Option<String>,
872
939
  capture_region: Option<NativeCaptureRegion>,
940
+ // Local-only recordings never upload, so their live-upload creds stay
941
+ // unresolved regardless of what the shared session holds.
942
+ local_only: Option<bool>,
943
+ has_camera: Option<bool>,
873
944
  ) -> Result<NativeFullscreenStartInfo, String> {
874
945
  #[cfg(not(target_os = "macos"))]
875
946
  {
@@ -882,12 +953,40 @@ pub async fn native_fullscreen_recording_begin(
882
953
  mic_device_id,
883
954
  mic_device_label,
884
955
  capture_region,
956
+ local_only,
957
+ has_camera,
885
958
  );
886
959
  return Err("Native full-screen recording is currently macOS-only.".into());
887
960
  }
888
961
 
889
962
  #[cfg(target_os = "macos")]
890
963
  {
964
+ // Live-upload credentials (server URL + cookie + auth token) live in the
965
+ // shared session state the renderer keeps warm via
966
+ // `meetings_watcher_set_session`. Read them from there instead of taking
967
+ // them as command args. Local-only recordings never upload, so leave
968
+ // their creds `None` regardless of what the session currently holds.
969
+ let (server_url, auth_token, cookie) = if local_only.unwrap_or(false) {
970
+ (None, None, None)
971
+ } else {
972
+ let snapshot = app
973
+ .try_state::<crate::meetings_watcher::MeetingsWatcherState>()
974
+ .map(|s| s.session_snapshot())
975
+ .unwrap_or_default();
976
+ (
977
+ snapshot.server_url,
978
+ snapshot.auth_token,
979
+ snapshot.session_cookie,
980
+ )
981
+ };
982
+
983
+ // Best-effort, non-blocking: keep the server-controlled feature flag
984
+ // cache warm using the session creds this call already has. Never
985
+ // delays recording start — a stale/default cache is used for THIS
986
+ // recording if the fetch hasn't landed yet; the result applies to
987
+ // the next one.
988
+ crate::remote_flags::spawn_refresh(server_url.clone(), cookie.clone(), auth_token.clone());
989
+
891
990
  let is_warmed = {
892
991
  let guard = state.inner.lock().map_err(|e| e.to_string())?;
893
992
  guard
@@ -896,7 +995,7 @@ pub async fn native_fullscreen_recording_begin(
896
995
  .unwrap_or(false)
897
996
  };
898
997
  if !is_warmed {
899
- return start_native_session_locked(
998
+ let info = start_native_session_locked(
900
999
  &app,
901
1000
  &state,
902
1001
  &recording_id,
@@ -906,7 +1005,23 @@ pub async fn native_fullscreen_recording_begin(
906
1005
  mic_device_label,
907
1006
  capture_region,
908
1007
  false,
909
- );
1008
+ )?;
1009
+ {
1010
+ let mut guard = state.inner.lock().map_err(|e| e.to_string())?;
1011
+ if let Some(session) = guard.as_mut() {
1012
+ attach_live_uploader_to_session(
1013
+ &app,
1014
+ session,
1015
+ &recording_id,
1016
+ server_url.as_deref(),
1017
+ auth_token.as_deref(),
1018
+ cookie.as_deref(),
1019
+ include_audio,
1020
+ has_camera.unwrap_or(false),
1021
+ );
1022
+ }
1023
+ }
1024
+ return Ok(info);
910
1025
  }
911
1026
 
912
1027
  // Grab the mic-ready diagnostics without holding the lock during the wait.
@@ -920,6 +1035,9 @@ pub async fn native_fullscreen_recording_begin(
920
1035
  }) => mic_ready
921
1036
  .as_ref()
922
1037
  .map(|ready| (Arc::clone(ready), mic_sample_count.clone())),
1038
+ Some(NativeFullscreenBackend::CustomScreenCaptureKit { mic_ready, .. }) => {
1039
+ mic_ready.as_ref().map(|ready| (Arc::clone(ready), None))
1040
+ }
923
1041
  _ => None,
924
1042
  })
925
1043
  };
@@ -945,13 +1063,20 @@ pub async fn native_fullscreen_recording_begin(
945
1063
  .ok_or_else(|| "No native full-screen recording is active.".to_string())?;
946
1064
  let width = session.width;
947
1065
  let height = session.height;
948
- if let Some(NativeFullscreenBackend::ScreenCaptureKit {
949
- stream, recording, ..
950
- }) = session.backend.as_ref()
951
- {
952
- stream
953
- .add_recording_output(recording)
954
- .map_err(|e| format!("add recording output failed: {e:?}"))?;
1066
+ match session.backend.as_ref() {
1067
+ Some(NativeFullscreenBackend::ScreenCaptureKit {
1068
+ stream, recording, ..
1069
+ }) => {
1070
+ stream
1071
+ .add_recording_output(recording)
1072
+ .map_err(|e| format!("add recording output failed: {e:?}"))?;
1073
+ }
1074
+ Some(NativeFullscreenBackend::CustomScreenCaptureKit {
1075
+ recording_enabled, ..
1076
+ }) => {
1077
+ recording_enabled.store(true, Ordering::SeqCst);
1078
+ }
1079
+ _ => {}
955
1080
  }
956
1081
  if mic_ready_before_attach.is_some() || mic_samples_before_attach.is_some() {
957
1082
  eprintln!(
@@ -975,6 +1100,17 @@ pub async fn native_fullscreen_recording_begin(
975
1100
  session.current_segment_started_at = now;
976
1101
  session.pending_recording_output = false;
977
1102
 
1103
+ attach_live_uploader_to_session(
1104
+ &app,
1105
+ session,
1106
+ &recording_id,
1107
+ server_url.as_deref(),
1108
+ auth_token.as_deref(),
1109
+ cookie.as_deref(),
1110
+ include_audio,
1111
+ has_camera.unwrap_or(false),
1112
+ );
1113
+
978
1114
  Ok(NativeFullscreenStartInfo {
979
1115
  recording_id,
980
1116
  width,
@@ -1008,7 +1144,7 @@ pub async fn native_fullscreen_recording_stop_and_upload(
1008
1144
  // promptly instead of letting it run through the merge window and push the
1009
1145
  // saved transcript past the clip's real end. See recorder.ts `handle.stop()`.
1010
1146
  let StoppedSession {
1011
- session,
1147
+ mut session,
1012
1148
  duration_ms,
1013
1149
  stop_outcome,
1014
1150
  consolidate_outcome,
@@ -1069,6 +1205,28 @@ pub async fn native_fullscreen_recording_stop_and_upload(
1069
1205
  )?;
1070
1206
  let stop_error = stop_outcome.err();
1071
1207
  if let Some(stop_err) = &stop_error {
1208
+ // Capture-side finalize/write failure: the on-disk file is incomplete
1209
+ // even though its init-segment moov is present (segmented fMP4 always
1210
+ // carries one). Never upload — the live uploader already streamed a
1211
+ // prefix, and finishing here would report success on a truncated clip
1212
+ // and delete the local copy. Fail closed and keep the retry copy.
1213
+ if stop_err.starts_with(CAPTURE_FINALIZE_INCOMPLETE_PREFIX) {
1214
+ saved.last_error = Some(stop_err.clone());
1215
+ write_saved_recording_metadata(&app, &saved)?;
1216
+ emit_native_upload_progress(&app, "failed", "Upload paused", None, None);
1217
+ let error = format!(
1218
+ "{stop_err}. The clip was saved locally and can be retried from the Clips menu."
1219
+ );
1220
+ emit_native_upload_finished(
1221
+ &app,
1222
+ &server_url,
1223
+ &recording_id,
1224
+ false,
1225
+ Some(error.clone()),
1226
+ Some(&saved.file_path),
1227
+ );
1228
+ return Err(error);
1229
+ }
1072
1230
  saved.last_error = Some(stop_err.clone());
1073
1231
  // Only mark corrupt when the SCK delegate explicitly called recording_did_fail
1074
1232
  // (error contains "finalize failed"). Transient stop_capture /
@@ -1144,13 +1302,121 @@ pub async fn native_fullscreen_recording_stop_and_upload(
1144
1302
  write_saved_recording_metadata(&app, &saved)?;
1145
1303
  emit_native_upload_progress(&app, "preparing", "Optimizing clip", None, None);
1146
1304
 
1305
+ #[cfg(target_os = "macos")]
1306
+ eprintln!(
1307
+ "[live-upload] stop_and_upload for {recording_id}: live_upload_active={} had_live_upload={} custom_pipeline={}",
1308
+ session.live_upload.is_some(),
1309
+ session.had_live_upload,
1310
+ session.custom_pipeline
1311
+ );
1312
+
1313
+ // Live-upload path: most of the file already streamed to the server
1314
+ // during recording. The writer produces delegate-fed segments that WE
1315
+ // append to the local file (AVFoundation never rewrites it — see the
1316
+ // "Segmented output" section in custom_capture.rs), so the streamed byte
1317
+ // ranges are stable and the uploader can safely drain the tail + send the
1318
+ // final post instead of re-uploading the whole file.
1319
+ #[cfg(target_os = "macos")]
1320
+ if let Some(live) = session.live_upload.take() {
1321
+ eprintln!(
1322
+ "[live-upload] stop: signalling finalize for {recording_id} (duration_ms={duration_ms})"
1323
+ );
1324
+ emit_native_upload_progress(&app, "uploading", "Uploading clip", None, None);
1325
+ live.ctrl
1326
+ .duration_ms
1327
+ .store(duration_ms as u64, Ordering::SeqCst);
1328
+ live.ctrl.finalize.store(true, Ordering::SeqCst);
1329
+ let result = match live.result_rx.await {
1330
+ Ok(inner) => inner,
1331
+ Err(_) => Err("live upload task ended unexpectedly".to_string()),
1332
+ };
1333
+ eprintln!(
1334
+ "[live-upload] stop: finalize result for {recording_id}: {}",
1335
+ match &result {
1336
+ Ok(bytes) => format!("ok ({bytes} bytes)"),
1337
+ Err(e) => format!("error: {e}"),
1338
+ }
1339
+ );
1340
+ return match result {
1341
+ Ok(bytes) => {
1342
+ clear_saved_recording_after_success(&app, &saved);
1343
+ emit_native_upload_finished(&app, &server_url, &recording_id, true, None, None);
1344
+ Ok(NativeFullscreenUploadResult {
1345
+ recording_id,
1346
+ duration_ms,
1347
+ width: session.width,
1348
+ height: session.height,
1349
+ bytes,
1350
+ })
1351
+ }
1352
+ Err(err) => {
1353
+ saved.last_attempt_at = Some(now_iso());
1354
+ saved.last_error = Some(err.clone());
1355
+ saved.retry_count = saved.retry_count.saturating_add(1);
1356
+ let _ = write_saved_recording_metadata(&app, &saved);
1357
+ emit_native_upload_progress(&app, "failed", "Upload paused", None, None);
1358
+ let error = format!(
1359
+ "{err}. The clip was saved locally and can be retried from the Clips menu."
1360
+ );
1361
+ emit_native_upload_finished(
1362
+ &app,
1363
+ &server_url,
1364
+ &recording_id,
1365
+ false,
1366
+ Some(error.clone()),
1367
+ Some(&saved.file_path),
1368
+ );
1369
+ Err(error)
1370
+ }
1371
+ };
1372
+ }
1373
+
1374
+ // If a live upload was started but later abandoned (e.g. the recording was
1375
+ // paused and became multi-segment), the server holds partial/stale chunks.
1376
+ // Clear them before re-uploading the whole consolidated file. A failed
1377
+ // reset is fatal for this path: uploading from index 0 over leftover
1378
+ // higher-index chunks corrupts the server-side reassembly, so keep the
1379
+ // clip parked locally instead (manual retry resets again first).
1380
+ let auth_token = auth_token.unwrap_or_default();
1381
+ let cookie = cookie.unwrap_or_default();
1382
+ if session.had_live_upload {
1383
+ if let Err(err) = reset_upload_chunks(
1384
+ &server_url,
1385
+ &recording_id,
1386
+ session.mime_type,
1387
+ &auth_token,
1388
+ &cookie,
1389
+ )
1390
+ .await
1391
+ {
1392
+ eprintln!("[live-upload] stop: reset of stale chunks failed for {recording_id}: {err}");
1393
+ saved.last_attempt_at = Some(now_iso());
1394
+ saved.last_error = Some(err.clone());
1395
+ saved.retry_count = saved.retry_count.saturating_add(1);
1396
+ let _ = write_saved_recording_metadata(&app, &saved);
1397
+ emit_native_upload_progress(&app, "failed", "Upload paused", None, None);
1398
+ let error = format!(
1399
+ "{err}. The clip was saved locally and can be retried from the Clips menu."
1400
+ );
1401
+ emit_native_upload_finished(
1402
+ &app,
1403
+ &server_url,
1404
+ &recording_id,
1405
+ false,
1406
+ Some(error.clone()),
1407
+ Some(&saved.file_path),
1408
+ );
1409
+ return Err(error);
1410
+ }
1411
+ }
1412
+
1147
1413
  let result = upload_recording_file(
1148
1414
  &app,
1149
1415
  &session,
1150
1416
  server_url.clone(),
1151
1417
  recording_id.clone(),
1152
- auth_token.unwrap_or_default(),
1153
- cookie.unwrap_or_default(),
1418
+ auth_token,
1419
+ cookie,
1154
1420
  upload_mode,
1155
1421
  duration_ms,
1156
1422
  has_audio,
@@ -1197,12 +1463,17 @@ pub async fn native_fullscreen_recording_stop_and_save(
1197
1463
  file_role: String,
1198
1464
  ) -> Result<NativeFullscreenSaveResult, String> {
1199
1465
  let StoppedSession {
1200
- session,
1466
+ mut session,
1201
1467
  duration_ms,
1202
1468
  stop_outcome,
1203
1469
  consolidate_outcome,
1204
1470
  multi_segment,
1205
1471
  } = take_and_finalize_active_session(&state, |_session| {})?;
1472
+ // Saving locally — stop any in-flight live upload to the server.
1473
+ #[cfg(target_os = "macos")]
1474
+ if let Some(live) = session.live_upload.take() {
1475
+ live.ctrl.cancelled.store(true, Ordering::SeqCst);
1476
+ }
1206
1477
  // Capture is finalized — drop the camera bubble now so the face
1207
1478
  // doesn't linger while the clip saves (mirrors the upload path).
1208
1479
  let _ = crate::clips::close_bubble(app.clone()).await;
@@ -1329,8 +1600,65 @@ pub async fn native_fullscreen_recording_pause(
1329
1600
  if session.paused_at.is_some() {
1330
1601
  return Ok(());
1331
1602
  }
1603
+ eprintln!(
1604
+ "[clips-tray] pause requested for {}: segment #{} ran {}ms",
1605
+ session.restart.safe_id,
1606
+ session.restart.segment_counter,
1607
+ session.current_segment_started_at.elapsed().as_millis()
1608
+ );
1609
+
1610
+ // Custom pipeline in live-upload (segmented) mode does a SOFT pause: stop
1611
+ // only the capture source and keep the writer, file, and live uploader
1612
+ // alive. Resume splices a fresh SCStream onto the same append-only file, so
1613
+ // there are no segments to concatenate and the upload is never interrupted.
1614
+ #[cfg(target_os = "macos")]
1615
+ {
1616
+ let soft_paused =
1617
+ if let Some(NativeFullscreenBackend::CustomScreenCaptureKit {
1618
+ resume, writer, ..
1619
+ }) = session.backend.as_ref()
1620
+ {
1621
+ if writer.segmented() && writer.is_started() {
1622
+ resume.pause();
1623
+ true
1624
+ } else {
1625
+ false
1626
+ }
1627
+ } else {
1628
+ false
1629
+ };
1630
+ if soft_paused {
1631
+ session.paused_at = Some(Instant::now());
1632
+ eprintln!(
1633
+ "[clips-tray] soft-paused {} (writer/file/live upload kept alive)",
1634
+ session.restart.safe_id
1635
+ );
1636
+ return Ok(());
1637
+ }
1638
+ }
1639
+
1640
+ // Fallback hard-pause path for pipelines the soft pause above does NOT
1641
+ // cover: the stock recorder, the custom pipeline with live upload off, and
1642
+ // a pause before the first frame. (When live upload is on, the soft pause
1643
+ // handles it and returns before reaching here, keeping the uploader alive.)
1644
+ //
1645
+ // Live upload assumes a single append-only file — the uploader tails one
1646
+ // growing file by byte offset. In these pipelines, pause finalizes that
1647
+ // file and resume starts a brand-new one (multi-segment), which stop later
1648
+ // stitches together into different bytes, so those offsets would be
1649
+ // meaningless. Abandon the in-flight uploader here; the stop path then
1650
+ // resets the partial chunks and re-uploads the consolidated file whole.
1651
+ #[cfg(target_os = "macos")]
1652
+ if let Some(live) = session.live_upload.take() {
1653
+ eprintln!(
1654
+ "[clips-tray] pause: abandoning live upload for {} (recording becomes multi-segment)",
1655
+ session.restart.safe_id
1656
+ );
1657
+ live.ctrl.cancelled.store(true, Ordering::SeqCst);
1658
+ }
1332
1659
  if session.backend.is_none() {
1333
1660
  // No active backend means we're already paused (or never started).
1661
+ eprintln!("[clips-tray] pause: no active backend; marking paused only");
1334
1662
  session.paused_at = Some(Instant::now());
1335
1663
  return Ok(());
1336
1664
  }
@@ -1340,6 +1668,17 @@ pub async fn native_fullscreen_recording_pause(
1340
1668
  }
1341
1669
  recover_from_unusable_current_segment(session, "pause", true);
1342
1670
  session.paused_at = Some(Instant::now());
1671
+ let current_segment_bytes = session
1672
+ .segments
1673
+ .last()
1674
+ .and_then(|path| std::fs::metadata(path).ok())
1675
+ .map(|meta| meta.len());
1676
+ eprintln!(
1677
+ "[clips-tray] paused {}: {} segment(s) on disk, finalized segment size={:?} bytes",
1678
+ session.restart.safe_id,
1679
+ session.segments.len(),
1680
+ current_segment_bytes
1681
+ );
1343
1682
  Ok(())
1344
1683
  }
1345
1684
 
@@ -1360,11 +1699,53 @@ pub async fn native_fullscreen_recording_resume(
1360
1699
  return Ok(());
1361
1700
  };
1362
1701
 
1702
+ // Soft-resume counterpart to the custom-pipeline soft pause: the backend
1703
+ // was never torn down, so build a fresh SCStream onto the same writer and
1704
+ // rebase its timestamps past the pause gap instead of starting a new
1705
+ // segment file.
1706
+ #[cfg(target_os = "macos")]
1707
+ {
1708
+ let paused_for = paused_at.elapsed();
1709
+ let soft_resumed =
1710
+ if let Some(NativeFullscreenBackend::CustomScreenCaptureKit {
1711
+ resume, writer, ..
1712
+ }) = session.backend.as_ref()
1713
+ {
1714
+ if writer.segmented() && writer.is_started() {
1715
+ resume.resume(paused_for)?;
1716
+ true
1717
+ } else {
1718
+ false
1719
+ }
1720
+ } else {
1721
+ false
1722
+ };
1723
+ if soft_resumed {
1724
+ session.paused_total = session
1725
+ .paused_total
1726
+ .checked_add(paused_for)
1727
+ .unwrap_or(session.paused_total);
1728
+ session.paused_at = None;
1729
+ eprintln!(
1730
+ "[clips-tray] soft-resumed {} after {}ms paused (single continuous file, paused_total={}ms)",
1731
+ session.restart.safe_id,
1732
+ paused_for.as_millis(),
1733
+ session.paused_total.as_millis()
1734
+ );
1735
+ return Ok(());
1736
+ }
1737
+ }
1738
+
1363
1739
  let restart = session.restart.clone();
1364
1740
  let next_counter = restart.segment_counter.saturating_add(1);
1365
1741
  let extension = native_extension_for_mime_type(session.mime_type);
1366
1742
  let segment_path = segment_path_for(&app, &restart.safe_id, extension, next_counter)?;
1367
1743
  let _ = std::fs::remove_file(&segment_path);
1744
+ eprintln!(
1745
+ "[clips-tray] resume requested for {}: starting segment #{next_counter} -> {}",
1746
+ restart.safe_id,
1747
+ segment_path.display()
1748
+ );
1368
1749
 
1369
1750
  // Re-check disk space before starting the new segment. The mid-recording
1370
1751
  // monitor warns but does not block, so space can drop below the hard limit
@@ -1403,6 +1784,13 @@ pub async fn native_fullscreen_recording_resume(
1403
1784
  .checked_add(paused_at.elapsed())
1404
1785
  .unwrap_or(session.paused_total);
1405
1786
  session.paused_at = None;
1787
+ eprintln!(
1788
+ "[clips-tray] resumed {}: segment #{next_counter} live after {}ms paused (segments={}, paused_total={}ms)",
1789
+ restart.safe_id,
1790
+ paused_at.elapsed().as_millis(),
1791
+ session.segments.len(),
1792
+ session.paused_total.as_millis()
1793
+ );
1406
1794
  Ok(())
1407
1795
  }
1408
1796
 
@@ -1577,7 +1965,7 @@ fn take_and_finalize_active_session(
1577
1965
  if let Err(err) = &consolidate_outcome {
1578
1966
  eprintln!("[clips-tray] segment consolidation failed: {err}");
1579
1967
  }
1580
- println!(
1968
+ eprintln!(
1581
1969
  "[clips-tray] consolidate done (ok={}, segments={}, multi={multi_segment}); {}",
1582
1970
  consolidate_outcome.is_ok(),
1583
1971
  session.segments.len(),
@@ -1674,6 +2062,10 @@ fn recover_from_unusable_current_segment(
1674
2062
  /// session displaced by a new start). Finalizes any active backend and
1675
2063
  /// deletes every on-disk artifact — segment files and the final path.
1676
2064
  fn discard_session(session: &mut NativeFullscreenSession) {
2065
+ #[cfg(target_os = "macos")]
2066
+ if let Some(live) = session.live_upload.take() {
2067
+ live.ctrl.cancelled.store(true, Ordering::SeqCst);
2068
+ }
1677
2069
  if let Some(stop) = &session.disk_monitor_stop {
1678
2070
  stop.store(true, Ordering::Relaxed);
1679
2071
  }
@@ -1713,22 +2105,34 @@ fn start_segment_backend(
1713
2105
  ) -> Result<(NativeFullscreenBackend, Option<u32>, Option<u32>), String> {
1714
2106
  #[cfg(target_os = "macos")]
1715
2107
  {
1716
- // app / safe_id aren't needed on macOS — the segment path is
1717
- // pre-computed by the caller and the segment backends don't take
1718
- // them. Consume to silence unused-variable warnings.
1719
- let _ = (app, safe_id);
1720
- match start_screencapturekit_backend_at(
1721
- segment_path,
1722
- include_audio,
1723
- capture_system_audio,
1724
- mic_device_id,
1725
- mic_device_label,
1726
- target_display_id,
1727
- capture_region,
1728
- // Resume records immediately — the mic was already warmed by the
1729
- // initial start, so there's no silent-head to avoid here.
1730
- false,
1731
- ) {
2108
+ // safe_id isn't needed on macOS — the segment path is pre-computed by
2109
+ // the caller. Consume to silence the unused-variable warning.
2110
+ let _ = safe_id;
2111
+ let sck_result = if crate::remote_flags::current().use_custom_sck_pipeline {
2112
+ start_custom_screencapturekit_backend_at(
2113
+ app,
2114
+ segment_path,
2115
+ include_audio,
2116
+ capture_system_audio,
2117
+ mic_device_id,
2118
+ mic_device_label,
2119
+ target_display_id,
2120
+ capture_region,
2121
+ false,
2122
+ )
2123
+ } else {
2124
+ start_screencapturekit_backend_at(
2125
+ segment_path,
2126
+ include_audio,
2127
+ capture_system_audio,
2128
+ mic_device_id,
2129
+ mic_device_label,
2130
+ target_display_id,
2131
+ capture_region,
2132
+ false,
2133
+ )
2134
+ };
2135
+ match sck_result {
1732
2136
  Ok((backend, w, h)) => return Ok((backend, w, h)),
1733
2137
  Err(sck_err) => {
1734
2138
  if include_audio {
@@ -2016,6 +2420,15 @@ fn consolidate_segments_into_path(session: &mut NativeFullscreenSession) -> Resu
2016
2420
  #[cfg(target_os = "macos")]
2017
2421
  {
2018
2422
  let segments = session.segments.clone();
2423
+ for (i, segment) in segments.iter().enumerate() {
2424
+ let size = std::fs::metadata(segment).map(|m| m.len()).unwrap_or(0);
2425
+ eprintln!(
2426
+ "[clips-tray] consolidate input segment {}/{}: {} ({size} bytes)",
2427
+ i + 1,
2428
+ segments.len(),
2429
+ segment.display()
2430
+ );
2431
+ }
2019
2432
  // Concatenate into a temp sibling first so a failure mid-export
2020
2433
  // doesn't leave a half-written file at the real output path.
2021
2434
  let target_stem = session
@@ -2771,6 +3184,7 @@ fn saved_recording_from_path(
2771
3184
  last_attempt_at: None,
2772
3185
  last_error: None,
2773
3186
  retry_count: 0,
3187
+ custom_pipeline: session.custom_pipeline,
2774
3188
  corrupt: false,
2775
3189
  })
2776
3190
  }
@@ -2825,6 +3239,11 @@ fn clear_saved_recording_after_success(app: &AppHandle, saved: &SavedNativeRecor
2825
3239
  "[clips-tray] upload succeeded for {}, but local pending recording cleanup failed: {err}",
2826
3240
  saved.recording_id
2827
3241
  );
3242
+ } else {
3243
+ eprintln!(
3244
+ "[clips-tray] upload succeeded for {}; removed local recording file",
3245
+ saved.recording_id
3246
+ );
2828
3247
  }
2829
3248
  }
2830
3249
 
@@ -2905,8 +3324,10 @@ fn start_screencapturekit_recording(
2905
3324
  let target_display_id = tray_display_id(app);
2906
3325
  let path = pending_recording_path(app, safe_id, "mp4")?;
2907
3326
  let _ = std::fs::remove_file(&path);
3327
+ let use_custom_sck_pipeline = crate::remote_flags::current().use_custom_sck_pipeline;
2908
3328
  eprintln!(
2909
- "[clips-tray] starting ScreenCaptureKit recording -> {}",
3329
+ "[clips-tray] starting ScreenCaptureKit recording. use_custom_sck_pipeline = {}, path -> {}",
3330
+ use_custom_sck_pipeline,
2910
3331
  path.display()
2911
3332
  );
2912
3333
  let check_path = path.parent().unwrap_or(&path);
@@ -2924,16 +3345,30 @@ fn start_screencapturekit_recording(
2924
3345
  );
2925
3346
  }
2926
3347
  }
2927
- let (backend, width, height) = start_screencapturekit_backend_at(
2928
- &path,
2929
- include_audio,
2930
- capture_system_audio,
2931
- mic_device_id,
2932
- mic_device_label,
2933
- target_display_id,
2934
- capture_region,
2935
- defer_recording_output,
2936
- )?;
3348
+ let (backend, width, height) = if use_custom_sck_pipeline {
3349
+ start_custom_screencapturekit_backend_at(
3350
+ app,
3351
+ &path,
3352
+ include_audio,
3353
+ capture_system_audio,
3354
+ mic_device_id,
3355
+ mic_device_label,
3356
+ target_display_id,
3357
+ capture_region,
3358
+ defer_recording_output,
3359
+ )?
3360
+ } else {
3361
+ start_screencapturekit_backend_at(
3362
+ &path,
3363
+ include_audio,
3364
+ capture_system_audio,
3365
+ mic_device_id,
3366
+ mic_device_label,
3367
+ target_display_id,
3368
+ capture_region,
3369
+ defer_recording_output,
3370
+ )?
3371
+ };
2937
3372
  let (fallback_width, fallback_height) = primary_monitor_size(app);
2938
3373
  let mut session = new_fullscreen_session(
2939
3374
  backend,
@@ -3030,6 +3465,13 @@ fn new_fullscreen_session(
3030
3465
  height: Option<u32>,
3031
3466
  restart: RestartInfo,
3032
3467
  ) -> NativeFullscreenSession {
3468
+ #[cfg(target_os = "macos")]
3469
+ let custom_pipeline = matches!(
3470
+ backend,
3471
+ NativeFullscreenBackend::CustomScreenCaptureKit { .. }
3472
+ );
3473
+ #[cfg(not(target_os = "macos"))]
3474
+ let custom_pipeline = false;
3033
3475
  let now = Instant::now();
3034
3476
  NativeFullscreenSession {
3035
3477
  backend: Some(backend),
@@ -3046,6 +3488,10 @@ fn new_fullscreen_session(
3046
3488
  paused_at: None,
3047
3489
  restart,
3048
3490
  pending_recording_output: false,
3491
+ custom_pipeline,
3492
+ #[cfg(target_os = "macos")]
3493
+ live_upload: None,
3494
+ had_live_upload: false,
3049
3495
  disk_monitor_stop: None,
3050
3496
  }
3051
3497
  }
@@ -3143,6 +3589,40 @@ pub(crate) fn stop_native_recording(
3143
3589
  match backend {
3144
3590
  NativeFullscreenBackend::Screencapture { child } => stop_screencapture(child),
3145
3591
  #[cfg(target_os = "macos")]
3592
+ NativeFullscreenBackend::CustomScreenCaptureKit {
3593
+ stream,
3594
+ writer,
3595
+ watchdog_shutdown,
3596
+ ..
3597
+ } => {
3598
+ eprintln!(
3599
+ "[clips-tray] stopping custom capture (wait_for_finalize={wait_for_finalize})"
3600
+ );
3601
+ // Stop the watchdog first so it can't rebuild the stream out from
3602
+ // under us mid-teardown.
3603
+ watchdog_shutdown.store(true, Ordering::SeqCst);
3604
+ let stop_result = stream
3605
+ .lock()
3606
+ .map_err(|e| format!("custom ScreenCaptureKit stop lock poisoned: {e}"))
3607
+ .and_then(|guard| {
3608
+ guard
3609
+ .stop_capture()
3610
+ .map_err(|e| format!("custom ScreenCaptureKit stop failed: {e:?}"))
3611
+ });
3612
+ if let Err(err) = &stop_result {
3613
+ eprintln!("[clips-tray] custom capture stop_capture error: {err}");
3614
+ }
3615
+ let finish_result = writer.finish(wait_for_finalize);
3616
+ // The finalize/segment-write result reflects on-disk file integrity,
3617
+ // so it takes priority over a (possibly benign) stop_capture error
3618
+ // and is tagged so the upload path fails closed instead of
3619
+ // publishing a truncated clip.
3620
+ match finish_result {
3621
+ Ok(()) => stop_result,
3622
+ Err(err) => Err(format!("{CAPTURE_FINALIZE_INCOMPLETE_PREFIX}{err}")),
3623
+ }
3624
+ }
3625
+ #[cfg(target_os = "macos")]
3146
3626
  NativeFullscreenBackend::ScreenCaptureKit { stream, finish, .. } => {
3147
3627
  // `remove_recording_output()` looks like the clean stop path, but
3148
3628
  // on real machines it can block synchronously forever when the
@@ -3261,6 +3741,7 @@ async fn upload_recording_file(
3261
3741
  has_audio,
3262
3742
  session.restart.mic_captured_in_file,
3263
3743
  session.restart.capture_system_audio,
3744
+ session.custom_pipeline,
3264
3745
  )?;
3265
3746
  let upload_result = upload_prepared_recording_file(
3266
3747
  app,
@@ -3323,6 +3804,7 @@ fn prepare_saved_recording_file(
3323
3804
  saved.has_audio,
3324
3805
  saved.mic_captured,
3325
3806
  saved.system_audio_captured,
3807
+ saved.custom_pipeline,
3326
3808
  )?;
3327
3809
  Ok((prepared, retry_combined_path))
3328
3810
  }
@@ -3686,6 +4168,9 @@ async fn send_upload_post(
3686
4168
  .map_err(|e| format!("native recording upload failed: {e}"))?;
3687
4169
  let status = response.status();
3688
4170
  let body = response.text().await.unwrap_or_default();
4171
+ eprintln!(
4172
+ "[live-upload] POST chunk #{index} (final={is_final}) -> {status} for {recording_id}"
4173
+ );
3689
4174
  if !status.is_success() {
3690
4175
  return Err(format!(
3691
4176
  "native recording upload returned {status}: {}",
@@ -4073,10 +4558,14 @@ fn prepare_recording_file(
4073
4558
  has_audio: bool,
4074
4559
  mic_captured_audio: bool,
4075
4560
  system_audio_captured: bool,
4561
+ // The file already contains a single (live-mixed or single-source) audio
4562
+ // track written by the custom pipeline; the L/R downmix repair assumes
4563
+ // mic and system on separate stereo channels and must not run on it.
4564
+ audio_premixed: bool,
4076
4565
  ) -> Result<PreparedRecordingFile, String> {
4077
4566
  // Downmix only repairs mic+system L/R split. Applying it to mic-only
4078
4567
  // capture halves speech energy (~6 dB) when SCK puts the mic on one channel.
4079
- let downmix_audio = mic_captured_audio && system_audio_captured;
4568
+ let downmix_audio = mic_captured_audio && system_audio_captured && !audio_premixed;
4080
4569
  let denoise_audio = mic_captured_audio;
4081
4570
  // Pregain only for mic-only (no system audio compete) — SCK has no AGC.
4082
4571
  let mic_pregain = mic_captured_audio && !system_audio_captured;
@@ -4119,7 +4608,13 @@ fn prepare_recording_file(
4119
4608
 
4120
4609
  let mut smallest_attempt_bytes: Option<u64> = None;
4121
4610
  let ffmpeg_path = resolve_ffmpeg_path();
4122
-
4611
+ if has_audio {
4612
+ if let Some(ffmpeg_path) = ffmpeg_path.as_deref() {
4613
+ let raw_summary =
4614
+ describe_audio_signal_probe(audio_signal_probe_with_ffmpeg(ffmpeg_path, path));
4615
+ eprintln!("[clips-tray] raw recording audio signal before prepare: {raw_summary}");
4616
+ }
4617
+ }
4123
4618
  if !COMPRESSION_ENABLED || source_bytes < TRANSCODE_THRESHOLD_BYTES {
4124
4619
  if has_audio {
4125
4620
  if let Some(ffmpeg_path) = ffmpeg_path.as_deref() {
@@ -4629,9 +5124,11 @@ fn normalize_audio_with_ffmpeg(
4629
5124
  .arg("-i")
4630
5125
  .arg(source)
4631
5126
  .arg("-map")
4632
- .arg("0:v:0")
4633
- .arg("-map")
4634
- .arg("0:a?")
5127
+ .arg("0:v:0");
5128
+
5129
+ command.arg("-map").arg("0:a?");
5130
+
5131
+ command
4635
5132
  .arg("-c:v")
4636
5133
  .arg("copy")
4637
5134
  .arg("-c:a")
@@ -4687,16 +5184,6 @@ fn native_transcode_presets(
4687
5184
  }
4688
5185
  }
4689
5186
 
4690
- fn native_preset_label(preset: &str) -> &'static str {
4691
- match preset {
4692
- "Preset1280x720" | "PresetAppleM4V720pHD" => "720p",
4693
- "Preset960x540" => "540p",
4694
- "Preset640x480" | "PresetAppleM4V480pSD" => "480p",
4695
- "PresetAppleM4VCellular" => "mobile quality",
4696
- _ => "a smaller preset",
4697
- }
4698
- }
4699
-
4700
5187
  fn transcode_with_ffmpeg(
4701
5188
  ffmpeg_path: &str,
4702
5189
  source: &Path,
@@ -4719,9 +5206,9 @@ fn transcode_with_ffmpeg(
4719
5206
  .arg("-i")
4720
5207
  .arg(source)
4721
5208
  .arg("-map")
4722
- .arg("0:v:0")
4723
- .arg("-map")
4724
- .arg("0:a?");
5209
+ .arg("0:v:0");
5210
+
5211
+ command.arg("-map").arg("0:a?");
4725
5212
 
4726
5213
  if let Some((scaled_w, scaled_h)) =
4727
5214
  ffmpeg_scaled_dimensions(width, height, preset.max_long_edge, preset.max_short_edge)
@@ -5439,6 +5926,10 @@ mod segment_recovery_tests {
5439
5926
  capture_region: None,
5440
5927
  },
5441
5928
  pending_recording_output: false,
5929
+ custom_pipeline: false,
5930
+ #[cfg(target_os = "macos")]
5931
+ live_upload: None,
5932
+ had_live_upload: false,
5442
5933
  disk_monitor_stop: None,
5443
5934
  }
5444
5935
  }