@agent-native/core 0.79.17 → 0.79.19

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 (355) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +14 -0
  3. package/corpus/core/docs/content/file-uploads.md +11 -4
  4. package/corpus/core/docs/content/locales/ar-SA/multi-app-workspace.md +3 -3
  5. package/corpus/core/docs/content/locales/de-DE/multi-app-workspace.md +3 -3
  6. package/corpus/core/docs/content/locales/es-ES/multi-app-workspace.md +3 -3
  7. package/corpus/core/docs/content/locales/fr-FR/multi-app-workspace.md +3 -3
  8. package/corpus/core/docs/content/locales/hi-IN/multi-app-workspace.md +3 -3
  9. package/corpus/core/docs/content/locales/ja-JP/multi-app-workspace.md +3 -3
  10. package/corpus/core/docs/content/locales/ko-KR/multi-app-workspace.md +3 -3
  11. package/corpus/core/docs/content/locales/pt-BR/multi-app-workspace.md +3 -3
  12. package/corpus/core/docs/content/locales/zh-CN/multi-app-workspace.md +3 -3
  13. package/corpus/core/docs/content/locales/zh-TW/multi-app-workspace.md +3 -3
  14. package/corpus/core/docs/content/multi-app-workspace.md +3 -3
  15. package/corpus/core/docs/content/template-clips.md +14 -5
  16. package/corpus/core/package.json +1 -1
  17. package/corpus/core/src/agent/engine/builder-engine.ts +8 -3
  18. package/corpus/core/src/cli/mcp.ts +11 -6
  19. package/corpus/core/src/client/AssistantChat.tsx +6 -2
  20. package/corpus/core/src/client/chat/markdown-renderer.tsx +1 -0
  21. package/corpus/core/src/client/i18n.tsx +77 -65
  22. package/corpus/core/src/client/index.ts +7 -1
  23. package/corpus/core/src/client/org/OrgSwitcher.tsx +5 -5
  24. package/corpus/core/src/client/org/TeamPage.tsx +11 -3
  25. package/corpus/core/src/client/settings/SettingsTabsPage.tsx +216 -0
  26. package/corpus/core/src/client/settings/index.ts +5 -0
  27. package/corpus/core/src/file-upload/index.ts +1 -0
  28. package/corpus/core/src/file-upload/registry.ts +22 -6
  29. package/corpus/core/src/file-upload/types.ts +6 -1
  30. package/corpus/core/src/integrations/adapters/email.ts +26 -18
  31. package/corpus/core/src/integrations/adapters/slack.ts +8 -7
  32. package/corpus/core/src/integrations/adapters/telegram.ts +9 -11
  33. package/corpus/core/src/integrations/adapters/whatsapp.ts +12 -9
  34. package/corpus/core/src/integrations/plugin.ts +79 -13
  35. package/corpus/core/src/onboarding/default-steps.ts +6 -3
  36. package/corpus/core/src/onboarding/types.ts +1 -1
  37. package/corpus/core/src/org/handlers.ts +1 -1
  38. package/corpus/core/src/server/agent-chat-plugin.ts +4 -7
  39. package/corpus/core/src/server/better-auth-instance.ts +19 -62
  40. package/corpus/core/src/server/core-routes-plugin.ts +93 -180
  41. package/corpus/core/src/server/create-server.ts +55 -116
  42. package/corpus/core/src/server/email-actions.ts +3 -6
  43. package/corpus/core/src/server/email.ts +52 -17
  44. package/corpus/core/src/server/index.ts +2 -1
  45. package/corpus/core/src/server/scoped-key-storage.ts +223 -0
  46. package/corpus/core/src/shared/agent-env.ts +3 -3
  47. package/corpus/core/src/sharing/actions/share-resource.ts +1 -1
  48. package/corpus/core/src/styles/agent-native.css +47 -3
  49. package/corpus/core/src/templates/workspace-root/AGENTS.md +3 -2
  50. package/corpus/core/src/templates/workspace-root/README.md +1 -1
  51. package/corpus/core/src/templates/workspace-root/scripts/repair-workspace-org.ts +29 -90
  52. package/corpus/core/src/terminal/pty-server.ts +3 -38
  53. package/corpus/core/src/triggers/dispatcher.ts +1 -2
  54. package/corpus/templates/.retired/issues/package.json +7 -0
  55. package/corpus/templates/.retired/meeting-notes/package.json +7 -0
  56. package/corpus/templates/.retired/recruiting/package.json +7 -0
  57. package/corpus/templates/.retired/scheduling/package.json +7 -0
  58. package/corpus/templates/.retired/voice/package.json +7 -0
  59. package/corpus/templates/analytics/app/components/layout/Header.tsx +0 -1
  60. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +1 -18
  61. package/corpus/templates/analytics/app/global.css +15 -0
  62. package/corpus/templates/analytics/app/i18n/zh-TW.ts +7 -1
  63. package/corpus/templates/analytics/app/i18n-data.ts +71 -2
  64. package/corpus/templates/analytics/app/pages/Ask.tsx +1 -1
  65. package/corpus/templates/analytics/app/pages/Settings.tsx +120 -73
  66. package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +0 -11
  67. package/corpus/templates/analytics/app/routes/catalog.tsx +33 -176
  68. package/corpus/templates/analytics/app/routes/team.tsx +3 -2
  69. package/corpus/templates/analytics/changelog/2026-06-26-dashboard-templates-are-tucked-into-settings-and-show-simple.md +6 -0
  70. package/corpus/templates/analytics/changelog/2026-06-26-the-ask-tab-uses-a-softer-dark-gray-canvas-while-sidebar-cha.md +6 -0
  71. package/corpus/templates/analytics/changelog/2026-06-26-the-sessions-page-shows-a-single-page-title-instead-of-repea.md +6 -0
  72. package/corpus/templates/analytics/server/lib/dashboard-catalog.ts +37 -28
  73. package/corpus/templates/assets/app/routes/settings.tsx +65 -43
  74. package/corpus/templates/assets/app/routes/team.tsx +2 -10
  75. package/corpus/templates/brain/app/routes/settings.tsx +390 -358
  76. package/corpus/templates/brain/app/routes/team.tsx +2 -8
  77. package/corpus/templates/calendar/actions/db-connect.ts +5 -29
  78. package/corpus/templates/calendar/app/components/CloudUpgrade.tsx +8 -51
  79. package/corpus/templates/calendar/app/components/calendar/GoogleConnectBanner.tsx +2 -1
  80. package/corpus/templates/calendar/app/components/calendar/GoogleSetupWizard.tsx +4 -2
  81. package/corpus/templates/calendar/app/components/layout/Sidebar.tsx +0 -2
  82. package/corpus/templates/calendar/app/pages/Settings.tsx +304 -271
  83. package/corpus/templates/calendar/app/routes/_app.team.tsx +2 -17
  84. package/corpus/templates/calendar/server/handlers/google-auth.ts +22 -9
  85. package/corpus/templates/calendar/server/lib/booking-emails.ts +1 -1
  86. package/corpus/templates/calendar/server/lib/event-guest-notifications.ts +1 -1
  87. package/corpus/templates/calendar/server/lib/google-calendar.ts +48 -28
  88. package/corpus/templates/chat/app/components/layout/Header.tsx +0 -1
  89. package/corpus/templates/chat/app/hooks/use-navigation-state.ts +4 -2
  90. package/corpus/templates/chat/app/routes/settings.tsx +54 -54
  91. package/corpus/templates/chat/app/routes/team.tsx +2 -10
  92. package/corpus/templates/clips/.agents/skills/ai-video-tools/SKILL.md +7 -2
  93. package/corpus/templates/clips/.agents/skills/recording/SKILL.md +1 -1
  94. package/corpus/templates/clips/.agents/skills/video-sharing/SKILL.md +7 -0
  95. package/corpus/templates/clips/AGENTS.md +8 -0
  96. package/corpus/templates/clips/actions/invite-member.ts +1 -1
  97. package/corpus/templates/clips/actions/request-transcript.ts +1 -1
  98. package/corpus/templates/clips/app/components/library/library-layout.tsx +325 -333
  99. package/corpus/templates/clips/app/components/player/delete-recording-menu.tsx +80 -40
  100. package/corpus/templates/clips/app/components/player/transcript-panel.tsx +2 -0
  101. package/corpus/templates/clips/app/routes/_app.settings._index.tsx +629 -643
  102. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +263 -158
  103. package/corpus/templates/clips/app/routes/share.$shareId.tsx +10 -2
  104. package/corpus/templates/clips/changelog/2026-06-26-long-desktop-recordings-are-checkpointed-so-macos-finalizati.md +6 -0
  105. package/corpus/templates/clips/changelog/2026-06-26-non-mac-desktop-recordings-wait-for-upload-transcription.md +6 -0
  106. package/corpus/templates/clips/changelog/2026-06-26-recording-details-button-stays-at-the-far-right-on-small-screen.md +6 -0
  107. package/corpus/templates/clips/changelog/2026-06-26-s3-compatible-storage-keys-now-save-securely-from-clips-sett.md +6 -0
  108. package/corpus/templates/clips/changelog/2026-06-26-shared-clip-links-guide-agents-through-processing-transcripts.md +6 -0
  109. package/corpus/templates/clips/changelog/2026-06-26-the-agent-sidebar-now-slides-in-smoothly-without-duplicate-e.md +6 -0
  110. package/corpus/templates/clips/changelog/2026-06-26-the-clip-details-sidebar-now-collapses-into-a-slide-out-pane.md +6 -0
  111. package/corpus/templates/clips/desktop/src/lib/recorder.ts +55 -3
  112. package/corpus/templates/clips/desktop/src/lib/transcription-capture.ts +217 -5
  113. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +1 -0
  114. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +176 -5
  115. package/corpus/templates/clips/server/lib/public-agent-context.ts +31 -0
  116. package/corpus/templates/clips/server/lib/s3-upload-provider.ts +60 -14
  117. package/corpus/templates/clips/server/lib/video-storage.ts +37 -9
  118. package/corpus/templates/clips/server/routes/api/agent-transcript.json.get.ts +5 -0
  119. package/corpus/templates/content/app/components/layout/Header.tsx +0 -1
  120. package/corpus/templates/content/app/components/sidebar/NotionButton.tsx +1 -0
  121. package/corpus/templates/content/app/routes/_app.settings.tsx +54 -55
  122. package/corpus/templates/content/app/routes/_app.team.tsx +2 -28
  123. package/corpus/templates/content/server/lib/notion.ts +43 -11
  124. package/corpus/templates/content/server/routes/api/notion/auth-url.get.ts +2 -2
  125. package/corpus/templates/content/server/routes/api/notion/status.get.ts +3 -3
  126. package/corpus/templates/design/app/routes/settings.tsx +60 -38
  127. package/corpus/templates/design/app/routes/team.tsx +2 -10
  128. package/corpus/templates/dispatch/app/hooks/use-navigation-state.ts +2 -2
  129. package/corpus/templates/dispatch/app/routes/settings.tsx +70 -55
  130. package/corpus/templates/dispatch/app/routes/team.tsx +5 -1
  131. package/corpus/templates/forms/actions/db-connect.ts +7 -36
  132. package/corpus/templates/forms/actions/navigate.ts +2 -2
  133. package/corpus/templates/forms/app/components/CloudUpgrade.tsx +8 -47
  134. package/corpus/templates/forms/app/components/layout/Header.tsx +0 -1
  135. package/corpus/templates/forms/app/components/layout/Layout.tsx +42 -25
  136. package/corpus/templates/forms/app/components/layout/Sidebar.tsx +35 -76
  137. package/corpus/templates/forms/app/global.css +9 -25
  138. package/corpus/templates/forms/app/hooks/use-navigation-state.ts +4 -4
  139. package/corpus/templates/forms/app/i18n/ar-SA.ts +1 -0
  140. package/corpus/templates/forms/app/i18n/de-DE.ts +1 -0
  141. package/corpus/templates/forms/app/i18n/en-US.ts +1 -0
  142. package/corpus/templates/forms/app/i18n/es-ES.ts +1 -0
  143. package/corpus/templates/forms/app/i18n/fr-FR.ts +1 -0
  144. package/corpus/templates/forms/app/i18n/hi-IN.ts +1 -0
  145. package/corpus/templates/forms/app/i18n/ja-JP.ts +1 -0
  146. package/corpus/templates/forms/app/i18n/ko-KR.ts +1 -0
  147. package/corpus/templates/forms/app/i18n/pt-BR.ts +1 -0
  148. package/corpus/templates/forms/app/i18n/zh-CN.ts +1 -0
  149. package/corpus/templates/forms/app/i18n/zh-TW.ts +1 -0
  150. package/corpus/templates/forms/app/lib/route-prewarm.ts +9 -8
  151. package/corpus/templates/forms/app/pages/AskPage.tsx +70 -0
  152. package/corpus/templates/forms/app/pages/FormsListPage.tsx +74 -81
  153. package/corpus/templates/forms/app/root.tsx +1 -1
  154. package/corpus/templates/forms/app/routes/_app.ask.tsx +14 -0
  155. package/corpus/templates/forms/app/routes/_app.settings.tsx +54 -54
  156. package/corpus/templates/forms/app/routes/_app.team.tsx +2 -9
  157. package/corpus/templates/forms/app/routes/_index.tsx +21 -130
  158. package/corpus/templates/forms/changelog/2026-06-26-ask-forms-opens-as-a-standard-app-tab-with-a-simpler-sidebar.md +6 -0
  159. package/corpus/templates/forms/changelog/2026-06-26-forms-are-shown-in-a-tighter-row-list-with-a-denser-sidebar.md +6 -0
  160. package/corpus/templates/forms/shared/navigation.ts +4 -2
  161. package/corpus/templates/macros/app/routes/settings.tsx +54 -54
  162. package/corpus/templates/macros/app/routes/team.tsx +2 -10
  163. package/corpus/templates/mail/actions/helpers.ts +2 -2
  164. package/corpus/templates/mail/app/components/GoogleConnectBanner.tsx +2 -1
  165. package/corpus/templates/mail/app/pages/SettingsPage.tsx +5 -5
  166. package/corpus/templates/mail/app/routes/team.tsx +3 -2
  167. package/corpus/templates/mail/server/handlers/emails.ts +3 -2
  168. package/corpus/templates/mail/server/handlers/google-auth.ts +19 -12
  169. package/corpus/templates/mail/server/lib/automation-engine.ts +3 -2
  170. package/corpus/templates/mail/server/lib/email-state.ts +2 -3
  171. package/corpus/templates/mail/server/lib/google-auth.ts +28 -11
  172. package/corpus/templates/mail/server/lib/jobs.ts +3 -2
  173. package/corpus/templates/mail/server/onboarding.ts +9 -6
  174. package/corpus/templates/mail/server/routes/api/attachments.get.ts +3 -3
  175. package/corpus/templates/plan/actions/request-plan-access.ts +1 -1
  176. package/corpus/templates/plan/app/components/layout/Header.tsx +0 -1
  177. package/corpus/templates/plan/app/components/plan/CanvasArea.tsx +56 -4
  178. package/corpus/templates/plan/app/global.css +8 -3
  179. package/corpus/templates/plan/app/hooks/use-navigation-state.ts +4 -2
  180. package/corpus/templates/plan/app/lib/route-prewarm.ts +0 -4
  181. package/corpus/templates/plan/app/pages/PlansPage.tsx +1 -1
  182. package/corpus/templates/plan/app/routes/settings.tsx +75 -73
  183. package/corpus/templates/plan/app/routes/team.tsx +2 -8
  184. package/corpus/templates/plan/changelog/2026-06-26-canvas-grid-panning-now-stays-smooth-and-fills-the-workspace.md +6 -0
  185. package/corpus/templates/plan/server/lib/comment-notifications.ts +1 -1
  186. package/corpus/templates/slides/actions/check-image-gen.ts +2 -1
  187. package/corpus/templates/slides/actions/db-connect.ts +7 -45
  188. package/corpus/templates/slides/actions/edit-image.ts +6 -3
  189. package/corpus/templates/slides/actions/generate-image-api.ts +1 -1
  190. package/corpus/templates/slides/actions/generate-image.ts +2 -2
  191. package/corpus/templates/slides/actions/generate-slides-ai.ts +5 -3
  192. package/corpus/templates/slides/actions/image-gen-status.ts +4 -3
  193. package/corpus/templates/slides/actions/image-search.ts +6 -3
  194. package/corpus/templates/slides/actions/search-images.ts +6 -3
  195. package/corpus/templates/slides/app/components/CloudUpgrade.tsx +8 -51
  196. package/corpus/templates/slides/app/components/layout/Header.tsx +0 -1
  197. package/corpus/templates/slides/app/components/layout/Sidebar.tsx +0 -2
  198. package/corpus/templates/slides/app/routes/settings.tsx +54 -54
  199. package/corpus/templates/slides/app/routes/team.tsx +2 -12
  200. package/corpus/templates/slides/server/handlers/google-docs-auth.ts +9 -9
  201. package/corpus/templates/slides/server/handlers/image-gen.ts +5 -1
  202. package/corpus/templates/slides/server/handlers/image-providers/gemini.ts +9 -1
  203. package/corpus/templates/slides/server/handlers/image-providers/index.ts +18 -8
  204. package/corpus/templates/slides/server/handlers/image-providers/openai.ts +10 -4
  205. package/corpus/templates/slides/server/handlers/image-providers/types.ts +1 -0
  206. package/corpus/templates/slides/server/lib/google-docs-oauth.ts +59 -34
  207. package/corpus/templates/slides/server/register-secrets.ts +60 -0
  208. package/corpus/templates/videos/actions/db-connect.ts +7 -45
  209. package/corpus/templates/videos/app/components/CloudUpgrade.tsx +8 -51
  210. package/corpus/templates/videos/app/components/Sidebar.tsx +1 -9
  211. package/corpus/templates/videos/app/components/layout/Header.tsx +0 -1
  212. package/corpus/templates/videos/app/components/layout/NavSidebar.tsx +0 -3
  213. package/corpus/templates/videos/app/routes/settings.tsx +54 -54
  214. package/corpus/templates/videos/app/routes/team.tsx +2 -8
  215. package/dist/agent/engine/builder-engine.d.ts.map +1 -1
  216. package/dist/agent/engine/builder-engine.js +8 -3
  217. package/dist/agent/engine/builder-engine.js.map +1 -1
  218. package/dist/cli/mcp.d.ts.map +1 -1
  219. package/dist/cli/mcp.js +3 -4
  220. package/dist/cli/mcp.js.map +1 -1
  221. package/dist/client/AssistantChat.d.ts.map +1 -1
  222. package/dist/client/AssistantChat.js +6 -2
  223. package/dist/client/AssistantChat.js.map +1 -1
  224. package/dist/client/chat/markdown-renderer.d.ts.map +1 -1
  225. package/dist/client/chat/markdown-renderer.js +1 -0
  226. package/dist/client/chat/markdown-renderer.js.map +1 -1
  227. package/dist/client/i18n.d.ts.map +1 -1
  228. package/dist/client/i18n.js +20 -9
  229. package/dist/client/i18n.js.map +1 -1
  230. package/dist/client/index.d.ts +1 -1
  231. package/dist/client/index.d.ts.map +1 -1
  232. package/dist/client/index.js +1 -1
  233. package/dist/client/index.js.map +1 -1
  234. package/dist/client/org/OrgSwitcher.d.ts +3 -3
  235. package/dist/client/org/OrgSwitcher.js +2 -2
  236. package/dist/client/org/OrgSwitcher.js.map +1 -1
  237. package/dist/client/org/TeamPage.d.ts +6 -1
  238. package/dist/client/org/TeamPage.d.ts.map +1 -1
  239. package/dist/client/org/TeamPage.js +2 -2
  240. package/dist/client/org/TeamPage.js.map +1 -1
  241. package/dist/client/settings/SettingsTabsPage.d.ts +27 -0
  242. package/dist/client/settings/SettingsTabsPage.d.ts.map +1 -0
  243. package/dist/client/settings/SettingsTabsPage.js +108 -0
  244. package/dist/client/settings/SettingsTabsPage.js.map +1 -0
  245. package/dist/client/settings/index.d.ts +1 -0
  246. package/dist/client/settings/index.d.ts.map +1 -1
  247. package/dist/client/settings/index.js +1 -0
  248. package/dist/client/settings/index.js.map +1 -1
  249. package/dist/file-upload/index.d.ts +1 -1
  250. package/dist/file-upload/index.d.ts.map +1 -1
  251. package/dist/file-upload/index.js +1 -1
  252. package/dist/file-upload/index.js.map +1 -1
  253. package/dist/file-upload/registry.d.ts +1 -0
  254. package/dist/file-upload/registry.d.ts.map +1 -1
  255. package/dist/file-upload/registry.js +24 -6
  256. package/dist/file-upload/registry.js.map +1 -1
  257. package/dist/file-upload/types.d.ts +6 -1
  258. package/dist/file-upload/types.d.ts.map +1 -1
  259. package/dist/file-upload/types.js.map +1 -1
  260. package/dist/integrations/adapters/email.d.ts.map +1 -1
  261. package/dist/integrations/adapters/email.js +18 -18
  262. package/dist/integrations/adapters/email.js.map +1 -1
  263. package/dist/integrations/adapters/slack.d.ts.map +1 -1
  264. package/dist/integrations/adapters/slack.js +8 -7
  265. package/dist/integrations/adapters/slack.js.map +1 -1
  266. package/dist/integrations/adapters/telegram.d.ts.map +1 -1
  267. package/dist/integrations/adapters/telegram.js +9 -9
  268. package/dist/integrations/adapters/telegram.js.map +1 -1
  269. package/dist/integrations/adapters/whatsapp.d.ts.map +1 -1
  270. package/dist/integrations/adapters/whatsapp.js +10 -9
  271. package/dist/integrations/adapters/whatsapp.js.map +1 -1
  272. package/dist/integrations/plugin.d.ts.map +1 -1
  273. package/dist/integrations/plugin.js +47 -13
  274. package/dist/integrations/plugin.js.map +1 -1
  275. package/dist/notifications/routes.d.ts +3 -3
  276. package/dist/observability/routes.d.ts +6 -6
  277. package/dist/onboarding/default-steps.d.ts.map +1 -1
  278. package/dist/onboarding/default-steps.js +6 -5
  279. package/dist/onboarding/default-steps.js.map +1 -1
  280. package/dist/onboarding/types.d.ts +1 -1
  281. package/dist/onboarding/types.d.ts.map +1 -1
  282. package/dist/onboarding/types.js.map +1 -1
  283. package/dist/org/handlers.js +1 -1
  284. package/dist/org/handlers.js.map +1 -1
  285. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  286. package/dist/server/agent-chat-plugin.js +4 -6
  287. package/dist/server/agent-chat-plugin.js.map +1 -1
  288. package/dist/server/agent-engine-api-key-route.d.ts +2 -2
  289. package/dist/server/better-auth-instance.d.ts.map +1 -1
  290. package/dist/server/better-auth-instance.js +17 -56
  291. package/dist/server/better-auth-instance.js.map +1 -1
  292. package/dist/server/core-routes-plugin.d.ts +1 -1
  293. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  294. package/dist/server/core-routes-plugin.js +67 -153
  295. package/dist/server/core-routes-plugin.js.map +1 -1
  296. package/dist/server/create-server.d.ts +3 -9
  297. package/dist/server/create-server.d.ts.map +1 -1
  298. package/dist/server/create-server.js +37 -91
  299. package/dist/server/create-server.js.map +1 -1
  300. package/dist/server/email-actions.d.ts +2 -3
  301. package/dist/server/email-actions.d.ts.map +1 -1
  302. package/dist/server/email-actions.js +3 -6
  303. package/dist/server/email-actions.js.map +1 -1
  304. package/dist/server/email.d.ts +3 -3
  305. package/dist/server/email.d.ts.map +1 -1
  306. package/dist/server/email.js +40 -19
  307. package/dist/server/email.js.map +1 -1
  308. package/dist/server/index.d.ts +3 -3
  309. package/dist/server/index.d.ts.map +1 -1
  310. package/dist/server/index.js +3 -3
  311. package/dist/server/index.js.map +1 -1
  312. package/dist/server/scoped-key-storage.d.ts +22 -0
  313. package/dist/server/scoped-key-storage.d.ts.map +1 -0
  314. package/dist/server/scoped-key-storage.js +152 -0
  315. package/dist/server/scoped-key-storage.js.map +1 -0
  316. package/dist/shared/agent-env.d.ts +3 -3
  317. package/dist/shared/agent-env.d.ts.map +1 -1
  318. package/dist/shared/agent-env.js +3 -3
  319. package/dist/shared/agent-env.js.map +1 -1
  320. package/dist/sharing/actions/share-resource.js +1 -1
  321. package/dist/sharing/actions/share-resource.js.map +1 -1
  322. package/dist/styles/agent-native.css +47 -3
  323. package/dist/templates/workspace-root/AGENTS.md +3 -2
  324. package/dist/templates/workspace-root/README.md +1 -1
  325. package/dist/templates/workspace-root/scripts/repair-workspace-org.ts +29 -90
  326. package/dist/terminal/pty-server.d.ts.map +1 -1
  327. package/dist/terminal/pty-server.js +3 -36
  328. package/dist/terminal/pty-server.js.map +1 -1
  329. package/dist/triggers/dispatcher.js +1 -1
  330. package/dist/triggers/dispatcher.js.map +1 -1
  331. package/docs/content/file-uploads.md +11 -4
  332. package/docs/content/locales/ar-SA/multi-app-workspace.md +3 -3
  333. package/docs/content/locales/de-DE/multi-app-workspace.md +3 -3
  334. package/docs/content/locales/es-ES/multi-app-workspace.md +3 -3
  335. package/docs/content/locales/fr-FR/multi-app-workspace.md +3 -3
  336. package/docs/content/locales/hi-IN/multi-app-workspace.md +3 -3
  337. package/docs/content/locales/ja-JP/multi-app-workspace.md +3 -3
  338. package/docs/content/locales/ko-KR/multi-app-workspace.md +3 -3
  339. package/docs/content/locales/pt-BR/multi-app-workspace.md +3 -3
  340. package/docs/content/locales/zh-CN/multi-app-workspace.md +3 -3
  341. package/docs/content/locales/zh-TW/multi-app-workspace.md +3 -3
  342. package/docs/content/multi-app-workspace.md +3 -3
  343. package/docs/content/template-clips.md +14 -5
  344. package/package.json +1 -1
  345. package/src/templates/workspace-root/AGENTS.md +3 -2
  346. package/src/templates/workspace-root/README.md +1 -1
  347. package/src/templates/workspace-root/scripts/repair-workspace-org.ts +29 -90
  348. package/corpus/core/src/server/env-var-writes.ts +0 -16
  349. package/corpus/templates/analytics/app/pages/Team.tsx +0 -9
  350. package/corpus/templates/calendar/app/pages/Team.tsx +0 -12
  351. package/corpus/templates/mail/app/pages/Team.tsx +0 -17
  352. package/dist/server/env-var-writes.d.ts +0 -10
  353. package/dist/server/env-var-writes.d.ts.map +0 -1
  354. package/dist/server/env-var-writes.js +0 -18
  355. package/dist/server/env-var-writes.js.map +0 -1
@@ -86,6 +86,7 @@ const NATIVE_FULLSCREEN_RECORDING_FLAG = "clips:native-fullscreen-recording";
86
86
  const DEV_SYNTHETIC_CAPTURE_FLAG = "clips:dev-synthetic-capture";
87
87
  const LEGACY_DEV_REAL_CAPTURE_FLAG = "clips:dev-real-capture";
88
88
  const LIVE_UPLOAD_CHUNK_MS = 1_000;
89
+ const NATIVE_FULLSCREEN_SEGMENT_MS = 5 * 60_000;
89
90
  const CLOUD_CAPTURE_FRAME_RATE = 24;
90
91
  const CLOUD_CAPTURE_MAX_WIDTH = 1920;
91
92
  const CLOUD_CAPTURE_MAX_HEIGHT = 1080;
@@ -122,6 +123,13 @@ export function shouldUseNativeFullscreenRecording(
122
123
  return isMacPlatform();
123
124
  }
124
125
 
126
+ function shouldSaveLocalTranscriptionStartupFailure(): boolean {
127
+ // Local Whisper/SFSpeech capture is macOS-only today. Non-mac desktop builds
128
+ // should wait for upload transcription instead of publishing a misleading
129
+ // native-transcription failure before `request-transcript` runs.
130
+ return isMacPlatform();
131
+ }
132
+
125
133
  function shouldUseDevSyntheticCapture(): boolean {
126
134
  if (!import.meta.env.DEV) return false;
127
135
  if (typeof localStorage === "undefined") return false;
@@ -962,7 +970,7 @@ async function saveRecordingTranscript(
962
970
  recordingId,
963
971
  fullText: text,
964
972
  segments: transcript.segments,
965
- source: "whisper",
973
+ source: transcript.source ?? "whisper",
966
974
  }),
967
975
  });
968
976
  if (!res.ok) {
@@ -1753,6 +1761,8 @@ async function startNativeFullscreenRecording(
1753
1761
  let cancelPromise: Promise<void> | null = null;
1754
1762
  let stateUnlistens: UnlistenFn[] = [];
1755
1763
  let tickHandle: ReturnType<typeof setInterval> | null = null;
1764
+ let segmentRotateHandle: ReturnType<typeof setInterval> | null = null;
1765
+ let segmentRotateInFlight = false;
1756
1766
  // Pause/resume tracking. The Rust side actually stops the SCStream on
1757
1767
  // pause and starts a new segment on resume; on stop it concatenates
1758
1768
  // segments via AVFoundation. We keep the JS-side timer in sync so the
@@ -1760,6 +1770,37 @@ async function startNativeFullscreenRecording(
1760
1770
  let pausedAt: number | null = null;
1761
1771
  let accumulatedPauseMs = 0;
1762
1772
 
1773
+ function clearSegmentRotator() {
1774
+ if (segmentRotateHandle) {
1775
+ clearInterval(segmentRotateHandle);
1776
+ segmentRotateHandle = null;
1777
+ }
1778
+ }
1779
+
1780
+ function startSegmentRotator() {
1781
+ clearSegmentRotator();
1782
+ segmentRotateHandle = setInterval(() => {
1783
+ if (stopped || pausedAt != null || segmentRotateInFlight) return;
1784
+ segmentRotateInFlight = true;
1785
+ invoke("native_fullscreen_recording_rotate_segment")
1786
+ .catch((err) => {
1787
+ const message = err instanceof Error ? err.message : String(err);
1788
+ console.warn("[clips-recorder] native segment rotation failed:", err);
1789
+ if (
1790
+ !stopped &&
1791
+ pausedAt == null &&
1792
+ message.includes("paused recording")
1793
+ ) {
1794
+ pausedAt = Date.now();
1795
+ emitState();
1796
+ }
1797
+ })
1798
+ .finally(() => {
1799
+ segmentRotateInFlight = false;
1800
+ });
1801
+ }, NATIVE_FULLSCREEN_SEGMENT_MS);
1802
+ }
1803
+
1763
1804
  function emitState() {
1764
1805
  const now = Date.now();
1765
1806
  const pausedNowMs = pausedAt != null ? now - pausedAt : 0;
@@ -1781,6 +1822,7 @@ async function startNativeFullscreenRecording(
1781
1822
  stopped = true;
1782
1823
  console.log("[clips-recorder] native full-screen stop requested");
1783
1824
  if (!localOnly) showFinalizingFeedback();
1825
+ clearSegmentRotator();
1784
1826
  if (tickHandle) {
1785
1827
  clearInterval(tickHandle);
1786
1828
  tickHandle = null;
@@ -1940,6 +1982,7 @@ async function startNativeFullscreenRecording(
1940
1982
  if (stopped) return;
1941
1983
  cancelPromise = (async () => {
1942
1984
  stopped = true;
1985
+ clearSegmentRotator();
1943
1986
  if (tickHandle) {
1944
1987
  clearInterval(tickHandle);
1945
1988
  tickHandle = null;
@@ -2020,6 +2063,7 @@ async function startNativeFullscreenRecording(
2020
2063
  ]);
2021
2064
  stateUnlistens = toolbarUnlistens;
2022
2065
  tickHandle = setInterval(emitState, 500);
2066
+ startSegmentRotator();
2023
2067
  emit("clips:toolbar-enabled", true).catch(() => {});
2024
2068
  emitState();
2025
2069
 
@@ -2041,7 +2085,11 @@ async function startNativeFullscreenRecording(
2041
2085
  if (stopped && transcriptionCapture) {
2042
2086
  void transcriptionCapture.cancel().catch(() => {});
2043
2087
  transcriptionCapture = null;
2044
- } else if (wantsAudio && !transcriptionCapture) {
2088
+ } else if (
2089
+ wantsAudio &&
2090
+ !transcriptionCapture &&
2091
+ shouldSaveLocalTranscriptionStartupFailure()
2092
+ ) {
2045
2093
  void saveTranscriptFailure(
2046
2094
  "macOS Speech recognition could not start for this recording. Check Speech Recognition and Microphone permissions, then retry transcription.",
2047
2095
  );
@@ -2863,7 +2911,11 @@ async function startRecordingInner(
2863
2911
  if (stopped && transcriptionCapture) {
2864
2912
  void transcriptionCapture.cancel().catch(() => {});
2865
2913
  transcriptionCapture = null;
2866
- } else if (wantsAudio && !transcriptionCapture) {
2914
+ } else if (
2915
+ wantsAudio &&
2916
+ !transcriptionCapture &&
2917
+ shouldSaveLocalTranscriptionStartupFailure()
2918
+ ) {
2867
2919
  void saveTranscriptFailure(
2868
2920
  "macOS Speech recognition could not start for this recording. Check Speech Recognition and Microphone permissions, then retry transcription.",
2869
2921
  );
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * Live transcription for recordings.
3
3
  *
4
- * Thin wrapper over the shared transcription engine (`transcription-engine.ts`),
5
- * which runs a local whisper.cpp context over BOTH the microphone and system
6
- * audio in parallel. Whisper does not end per-utterance, so the workers run
7
- * continuously until stop, which flushes any trailing speech.
4
+ * Thin wrapper over the shared transcription engines. It tries local
5
+ * whisper.cpp/macOS speech first (mic + optional system audio), then falls
6
+ * back to Web Speech in the desktop webview when the local Rust engine is not
7
+ * available (notably non-mac builds).
8
8
  *
9
9
  * The handle exposes `stop()` (returns the full speaker-labelled transcript,
10
10
  * after a short grace for trailing finals) and `cancel()` (stops + discards).
@@ -23,6 +23,7 @@ import {
23
23
 
24
24
  /** Grace period after stop for whisper to emit any flushed trailing finals. */
25
25
  const WHISPER_STOP_SETTLE_MS = 1500;
26
+ const WEB_SPEECH_STOP_SETTLE_MS = 1200;
26
27
 
27
28
  function wait(ms: number): Promise<void> {
28
29
  return new Promise((resolve) => window.setTimeout(resolve, ms));
@@ -33,6 +34,8 @@ export interface CapturedTranscript {
33
34
  text: string;
34
35
  /** Real whisper segments with verbatim timestamps. */
35
36
  segments: SourcedTranscriptSegment[];
37
+ /** Source stored with `save-browser-transcript`. */
38
+ source?: "web-speech" | "macos-native" | "whisper";
36
39
  }
37
40
 
38
41
  export interface TranscriptionCapture {
@@ -40,6 +43,213 @@ export interface TranscriptionCapture {
40
43
  cancel(): Promise<void>;
41
44
  }
42
45
 
46
+ interface SpeechRecognitionResultLike {
47
+ isFinal: boolean;
48
+ 0?: { transcript?: string };
49
+ }
50
+
51
+ interface SpeechRecognitionEventLike {
52
+ resultIndex: number;
53
+ results: ArrayLike<SpeechRecognitionResultLike>;
54
+ }
55
+
56
+ interface SpeechRecognitionErrorEventLike {
57
+ error: string;
58
+ }
59
+
60
+ interface SpeechRecognitionLike {
61
+ continuous: boolean;
62
+ interimResults: boolean;
63
+ lang: string;
64
+ maxAlternatives: number;
65
+ onresult: ((event: SpeechRecognitionEventLike) => void) | null;
66
+ onerror: ((event: SpeechRecognitionErrorEventLike) => void) | null;
67
+ onend: (() => void) | null;
68
+ start: () => void;
69
+ stop: () => void;
70
+ abort: () => void;
71
+ }
72
+
73
+ type SpeechRecognitionCtor = new () => SpeechRecognitionLike;
74
+
75
+ function getSpeechRecognitionCtor(): SpeechRecognitionCtor | null {
76
+ if (typeof window === "undefined") return null;
77
+ const w = window as unknown as {
78
+ SpeechRecognition?: SpeechRecognitionCtor;
79
+ webkitSpeechRecognition?: SpeechRecognitionCtor;
80
+ };
81
+ return w.SpeechRecognition || w.webkitSpeechRecognition || null;
82
+ }
83
+
84
+ function browserLanguage(): string {
85
+ return navigator.language || "en-US";
86
+ }
87
+
88
+ function shouldUseBrowserTranscriptionFallback(): boolean {
89
+ if (typeof navigator === "undefined") return false;
90
+ const platform =
91
+ (navigator as { userAgentData?: { platform?: string } }).userAgentData
92
+ ?.platform ||
93
+ navigator.platform ||
94
+ navigator.userAgent;
95
+
96
+ // Keep macOS on the existing local Whisper -> SFSpeech path. Web Speech is
97
+ // only a bridge for non-mac desktop builds where the Rust engines are absent.
98
+ return !/mac/i.test(platform);
99
+ }
100
+
101
+ function appendTranscriptText(current: string, next: string): string {
102
+ const cleanCurrent = current.trim();
103
+ const cleanNext = next.trim();
104
+ if (!cleanNext) return cleanCurrent;
105
+ if (!cleanCurrent) return cleanNext;
106
+ return `${cleanCurrent} ${cleanNext}`;
107
+ }
108
+
109
+ function createWebSpeechTranscriptBuffer() {
110
+ let committedFinalText = "";
111
+ let sessionFinalText = "";
112
+ let interimText = "";
113
+
114
+ return {
115
+ update(event: SpeechRecognitionEventLike) {
116
+ let nextFinal = "";
117
+ let nextInterim = "";
118
+ for (let i = 0; i < event.results.length; i++) {
119
+ const result = event.results[i];
120
+ const text = result[0]?.transcript ?? "";
121
+ if (result.isFinal) {
122
+ nextFinal += text;
123
+ } else {
124
+ nextInterim += text;
125
+ }
126
+ }
127
+ sessionFinalText = nextFinal;
128
+ interimText = nextInterim;
129
+ },
130
+ commitSession(opts?: { preserveInterim?: boolean }) {
131
+ committedFinalText = appendTranscriptText(
132
+ committedFinalText,
133
+ sessionFinalText,
134
+ );
135
+ sessionFinalText = "";
136
+ if (!opts?.preserveInterim) {
137
+ interimText = "";
138
+ }
139
+ },
140
+ text() {
141
+ return [committedFinalText, sessionFinalText, interimText]
142
+ .map((part) => part.trim())
143
+ .filter(Boolean)
144
+ .join(" ")
145
+ .trim();
146
+ },
147
+ };
148
+ }
149
+
150
+ async function startBrowserTranscriptionCapture(): Promise<TranscriptionCapture | null> {
151
+ const Ctor = getSpeechRecognitionCtor();
152
+ if (!Ctor) return null;
153
+
154
+ const recognition = new Ctor();
155
+ let disposed = false;
156
+ let stopped = false;
157
+ const transcriptBuffer = createWebSpeechTranscriptBuffer();
158
+ let stopResolver: ((value: CapturedTranscript) => void) | null = null;
159
+ let settleTimer: ReturnType<typeof window.setTimeout> | null = null;
160
+
161
+ const captured = (): CapturedTranscript => ({
162
+ text: transcriptBuffer.text(),
163
+ segments: [],
164
+ source: "web-speech",
165
+ });
166
+
167
+ const settleStop = () => {
168
+ if (!stopResolver) return;
169
+ if (settleTimer) {
170
+ window.clearTimeout(settleTimer);
171
+ settleTimer = null;
172
+ }
173
+ const resolve = stopResolver;
174
+ stopResolver = null;
175
+ disposed = true;
176
+ resolve(captured());
177
+ };
178
+
179
+ recognition.continuous = true;
180
+ recognition.interimResults = true;
181
+ recognition.lang = browserLanguage();
182
+ recognition.maxAlternatives = 1;
183
+
184
+ recognition.onresult = (event) => {
185
+ transcriptBuffer.update(event);
186
+ };
187
+
188
+ recognition.onerror = (event) => {
189
+ if (event.error === "no-speech" || event.error === "aborted") return;
190
+ console.warn(
191
+ "[clips-recorder] Web Speech transcription error:",
192
+ event.error,
193
+ );
194
+ };
195
+
196
+ recognition.onend = () => {
197
+ if (disposed) return;
198
+ transcriptBuffer.commitSession({ preserveInterim: stopped });
199
+ if (stopped) {
200
+ settleStop();
201
+ return;
202
+ }
203
+ try {
204
+ recognition.start();
205
+ } catch (err) {
206
+ console.warn(
207
+ "[clips-recorder] Web Speech transcription restart failed:",
208
+ err,
209
+ );
210
+ }
211
+ };
212
+
213
+ try {
214
+ recognition.start();
215
+ console.log("[clips-recorder] transcription started (web-speech mic)");
216
+ } catch (err) {
217
+ console.warn("[clips-recorder] Web Speech transcription unavailable:", err);
218
+ disposed = true;
219
+ return null;
220
+ }
221
+
222
+ return {
223
+ stop() {
224
+ stopped = true;
225
+ return new Promise<CapturedTranscript>((resolve) => {
226
+ stopResolver = resolve;
227
+ settleTimer = window.setTimeout(settleStop, WEB_SPEECH_STOP_SETTLE_MS);
228
+ try {
229
+ recognition.stop();
230
+ } catch {
231
+ settleStop();
232
+ }
233
+ });
234
+ },
235
+ async cancel() {
236
+ disposed = true;
237
+ stopped = true;
238
+ if (settleTimer) {
239
+ window.clearTimeout(settleTimer);
240
+ settleTimer = null;
241
+ }
242
+ try {
243
+ recognition.abort();
244
+ } catch {
245
+ // ignore
246
+ }
247
+ },
248
+ };
249
+ }
250
+
251
+ export const __test = { createWebSpeechTranscriptBuffer };
252
+
43
253
  export async function startTranscriptionCapture(
44
254
  mic?: {
45
255
  deviceId?: string | null;
@@ -84,7 +294,9 @@ export async function startTranscriptionCapture(
84
294
  } catch (err) {
85
295
  cleanup();
86
296
  console.warn("[clips-recorder] whisper transcript unavailable:", err);
87
- return null;
297
+ return shouldUseBrowserTranscriptionFallback()
298
+ ? startBrowserTranscriptionCapture()
299
+ : null;
88
300
  }
89
301
 
90
302
  return {
@@ -113,6 +113,7 @@ pub fn run() {
113
113
  native_screen::native_fullscreen_recording_cancel,
114
114
  native_screen::native_fullscreen_recording_pause,
115
115
  native_screen::native_fullscreen_recording_resume,
116
+ native_screen::native_fullscreen_recording_rotate_segment,
116
117
  native_screen::native_fullscreen_pending_uploads,
117
118
  native_screen::native_fullscreen_recording_retry_upload,
118
119
  native_screen::native_fullscreen_recording_discard_upload,
@@ -129,6 +129,14 @@ struct NativeFullscreenSession {
129
129
  /// time when reporting `duration_ms`, so the upload metadata matches
130
130
  /// the actual recorded content rather than wall-clock time.
131
131
  paused_total: Duration,
132
+ /// Start time for the segment currently being captured. Used to subtract
133
+ /// unrecoverable segment time when ScreenCaptureKit fails finalization
134
+ /// but earlier segments are still playable.
135
+ current_segment_started_at: Instant,
136
+ /// Recorded time lost because a finalized segment was unusable and had to
137
+ /// be skipped during recovery.
138
+ lost_segment_duration: Duration,
139
+ lost_segment_count: u32,
132
140
  /// When the current pause began, if paused. Folded into `paused_total`
133
141
  /// on resume.
134
142
  paused_at: Option<Instant>,
@@ -723,7 +731,9 @@ pub async fn native_fullscreen_recording_begin(
723
731
  // Rebaseline the duration clock: the warm phase ran during the
724
732
  // countdown, so `started_at` (set at warm time) is several seconds
725
733
  // early. The clip's first written frame is now, so measure from now.
726
- session.started_at = Instant::now();
734
+ let now = Instant::now();
735
+ session.started_at = now;
736
+ session.current_segment_started_at = now;
727
737
  session.pending_recording_output = false;
728
738
 
729
739
  Ok(NativeFullscreenStartInfo {
@@ -1057,6 +1067,7 @@ pub async fn native_fullscreen_recording_resume(
1057
1067
  session.backend = Some(backend);
1058
1068
  session.segments.push(segment_path);
1059
1069
  session.restart.segment_counter = next_counter;
1070
+ session.current_segment_started_at = Instant::now();
1060
1071
  session.paused_total = session
1061
1072
  .paused_total
1062
1073
  .checked_add(paused_at.elapsed())
@@ -1065,6 +1076,106 @@ pub async fn native_fullscreen_recording_resume(
1065
1076
  Ok(())
1066
1077
  }
1067
1078
 
1079
+ /// Best-effort checkpoint for long ScreenCaptureKit recordings. It finalizes
1080
+ /// the current segment and immediately starts a sibling segment, so a later
1081
+ /// ReplayKit finalization failure can only lose the active segment instead of
1082
+ /// the entire recording.
1083
+ #[tauri::command]
1084
+ pub async fn native_fullscreen_recording_rotate_segment(
1085
+ app: AppHandle,
1086
+ state: State<'_, NativeFullscreenRecordingState>,
1087
+ ) -> Result<(), String> {
1088
+ #[cfg(not(target_os = "macos"))]
1089
+ {
1090
+ let _ = (app, state);
1091
+ Ok(())
1092
+ }
1093
+
1094
+ #[cfg(target_os = "macos")]
1095
+ {
1096
+ let mut guard = state.inner.lock().map_err(|e| e.to_string())?;
1097
+ let Some(session) = guard.as_mut() else {
1098
+ return Ok(());
1099
+ };
1100
+ if session.pending_recording_output || session.paused_at.is_some() {
1101
+ return Ok(());
1102
+ }
1103
+ if !matches!(
1104
+ session.backend.as_ref(),
1105
+ Some(NativeFullscreenBackend::ScreenCaptureKit { .. })
1106
+ ) {
1107
+ return Ok(());
1108
+ }
1109
+
1110
+ rotate_screencapturekit_segment(&app, session)
1111
+ }
1112
+ }
1113
+
1114
+ #[cfg(target_os = "macos")]
1115
+ fn rotate_screencapturekit_segment(
1116
+ app: &AppHandle,
1117
+ session: &mut NativeFullscreenSession,
1118
+ ) -> Result<(), String> {
1119
+ let restart = session.restart.clone();
1120
+ let next_counter = restart.segment_counter.saturating_add(1);
1121
+ let extension = native_extension_for_mime_type(session.mime_type);
1122
+ let segment_path = segment_path_for(app, &restart.safe_id, extension, next_counter)?;
1123
+ let _ = std::fs::remove_file(&segment_path);
1124
+
1125
+ if let Some(free) = free_disk_bytes(segment_path.parent().unwrap_or(&segment_path)) {
1126
+ if free < DISK_SPACE_BLOCK_BYTES {
1127
+ eprintln!(
1128
+ "[clips-tray] skipping segment rotation because disk space is low: {} free",
1129
+ format_mb(free)
1130
+ );
1131
+ return Ok(());
1132
+ }
1133
+ }
1134
+
1135
+ eprintln!(
1136
+ "[clips-tray] rotating ScreenCaptureKit recording segment -> {}",
1137
+ segment_path.display()
1138
+ );
1139
+ let rotation_started = Instant::now();
1140
+ let stop_outcome = finalize_active_backend(session, true);
1141
+ if let Err(err) = &stop_outcome {
1142
+ eprintln!("[clips-tray] segment rotation finalize reported an error: {err}");
1143
+ }
1144
+ recover_from_unusable_current_segment(session, "segment rotation", true);
1145
+
1146
+ let start_result = start_screencapturekit_backend_at(
1147
+ &segment_path,
1148
+ restart.include_audio,
1149
+ restart.capture_system_audio,
1150
+ restart.mic_device_id.as_deref(),
1151
+ restart.mic_device_label.as_deref(),
1152
+ restart.target_display_id,
1153
+ restart.capture_region,
1154
+ false,
1155
+ );
1156
+
1157
+ let (backend, _, _) = match start_result {
1158
+ Ok(result) => result,
1159
+ Err(err) => {
1160
+ session.paused_at = Some(rotation_started);
1161
+ return Err(format!(
1162
+ "ScreenCaptureKit segment rotation paused recording after a finalized checkpoint, but the next segment could not start: {err}"
1163
+ ));
1164
+ }
1165
+ };
1166
+
1167
+ session.paused_total = session
1168
+ .paused_total
1169
+ .checked_add(rotation_started.elapsed())
1170
+ .unwrap_or(session.paused_total);
1171
+ session.backend = Some(backend);
1172
+ session.segments.push(segment_path);
1173
+ session.restart.segment_counter = next_counter;
1174
+ session.current_segment_started_at = Instant::now();
1175
+ session.paused_at = None;
1176
+ Ok(())
1177
+ }
1178
+
1068
1179
  /// Outcome of taking the active session out of state and finalizing
1069
1180
  /// every backend / segment it owns. Both the upload and the save-locally
1070
1181
  /// stop commands need exactly this prelude, so it lives in one place.
@@ -1076,9 +1187,10 @@ struct StoppedSession {
1076
1187
  stop_outcome: Result<(), String>,
1077
1188
  /// Result of merging segment files into `session.path`.
1078
1189
  consolidate_outcome: Result<(), String>,
1079
- /// True when more than one segment was captured (i.e. the user
1080
- /// paused at least once). Used to decide whether a consolidation
1081
- /// failure is fatal — single-segment consolidation is just a rename.
1190
+ /// True when more than one segment was captured (manual pause/resume or
1191
+ /// automatic long-recording checkpoints). Used to decide whether a
1192
+ /// consolidation failure is fatal — single-segment consolidation is just a
1193
+ /// rename.
1082
1194
  multi_segment: bool,
1083
1195
  }
1084
1196
 
@@ -1105,6 +1217,7 @@ fn take_and_finalize_active_session(
1105
1217
  // playable. The caller persists recovery metadata so a finalize
1106
1218
  // failure doesn't orphan the file.
1107
1219
  let stop_outcome = finalize_active_backend(&mut session, true);
1220
+ recover_from_unusable_current_segment(&mut session, "final stop", false);
1108
1221
  println!(
1109
1222
  "[clips-tray] finalize backend done (ok={}); {}",
1110
1223
  stop_outcome.is_ok(),
@@ -1124,10 +1237,18 @@ fn take_and_finalize_active_session(
1124
1237
  session.segments.len(),
1125
1238
  describe_recording_path(&session.path)
1126
1239
  );
1240
+ if session.lost_segment_count > 0 {
1241
+ eprintln!(
1242
+ "[clips-tray] recovered recording by dropping {} unusable segment(s), approx {}s lost",
1243
+ session.lost_segment_count,
1244
+ session.lost_segment_duration.as_secs()
1245
+ );
1246
+ }
1127
1247
  let duration_ms = session
1128
1248
  .started_at
1129
1249
  .elapsed()
1130
1250
  .saturating_sub(session.paused_total)
1251
+ .saturating_sub(session.lost_segment_duration)
1131
1252
  .as_millis();
1132
1253
  Ok(StoppedSession {
1133
1254
  session,
@@ -1150,6 +1271,52 @@ fn finalize_active_backend(
1150
1271
  stop_native_recording(&mut backend, wait_for_finalize)
1151
1272
  }
1152
1273
 
1274
+ fn playable_recording_file(path: &Path, mime_type: &str) -> bool {
1275
+ match std::fs::metadata(path) {
1276
+ Ok(metadata) if metadata.len() > 0 => {}
1277
+ _ => return false,
1278
+ }
1279
+ if mime_type == MP4_RECORDING_MIME_TYPE || mime_type == QUICKTIME_RECORDING_MIME_TYPE {
1280
+ return mp4_has_moov(path) != Some(false);
1281
+ }
1282
+ true
1283
+ }
1284
+
1285
+ fn recover_from_unusable_current_segment(
1286
+ session: &mut NativeFullscreenSession,
1287
+ reason: &str,
1288
+ allow_empty: bool,
1289
+ ) -> bool {
1290
+ let Some(current) = session.segments.last().cloned() else {
1291
+ return false;
1292
+ };
1293
+ if playable_recording_file(&current, session.mime_type) {
1294
+ return false;
1295
+ }
1296
+ if !allow_empty && session.segments.len() <= 1 {
1297
+ eprintln!(
1298
+ "[clips-tray] current recording segment is unusable after {reason}, but no earlier segment exists to recover"
1299
+ );
1300
+ return false;
1301
+ }
1302
+
1303
+ let dropped = session.segments.pop();
1304
+ if dropped.as_ref() == Some(&current) {
1305
+ let _ = std::fs::remove_file(&current);
1306
+ session.lost_segment_count = session.lost_segment_count.saturating_add(1);
1307
+ session.lost_segment_duration = session
1308
+ .lost_segment_duration
1309
+ .checked_add(session.current_segment_started_at.elapsed())
1310
+ .unwrap_or(session.lost_segment_duration);
1311
+ eprintln!(
1312
+ "[clips-tray] dropped unusable recording segment after {reason}; recovered {} earlier segment(s)",
1313
+ session.segments.len()
1314
+ );
1315
+ return true;
1316
+ }
1317
+ false
1318
+ }
1319
+
1153
1320
  /// Best-effort cleanup of a session being discarded (cancel, or a stale
1154
1321
  /// session displaced by a new start). Finalizes any active backend and
1155
1322
  /// deletes every on-disk artifact — segment files and the final path.
@@ -2243,15 +2410,19 @@ fn new_fullscreen_session(
2243
2410
  height: Option<u32>,
2244
2411
  restart: RestartInfo,
2245
2412
  ) -> NativeFullscreenSession {
2413
+ let now = Instant::now();
2246
2414
  NativeFullscreenSession {
2247
2415
  backend: Some(backend),
2248
2416
  path: path.clone(),
2249
2417
  mime_type,
2250
- started_at: Instant::now(),
2418
+ started_at: now,
2251
2419
  width,
2252
2420
  height,
2253
2421
  segments: vec![path],
2254
2422
  paused_total: Duration::ZERO,
2423
+ current_segment_started_at: now,
2424
+ lost_segment_duration: Duration::ZERO,
2425
+ lost_segment_count: 0,
2255
2426
  paused_at: None,
2256
2427
  restart,
2257
2428
  pending_recording_output: false,
@@ -455,6 +455,34 @@ function compactBugReport(report: PublicAgentBugReport) {
455
455
  };
456
456
  }
457
457
 
458
+ export function transcriptStatusInstructions(
459
+ transcript: PublicAgentTranscript,
460
+ ) {
461
+ const status = transcript?.status ?? "missing";
462
+ const failureReason = transcript?.failureReason ?? "";
463
+ const lowerReason = failureReason.toLowerCase();
464
+
465
+ if (status === "pending") {
466
+ return [
467
+ "The transcript is still pending. For long recordings this can take several minutes; wait 15-30 seconds and fetch apis.context.url or apis.transcript.url again a few times before falling back to frames or telling the user no transcript exists.",
468
+ ];
469
+ }
470
+
471
+ if (status === "failed" && lowerReason.includes("credits exhausted")) {
472
+ return [
473
+ "Transcription failed because Builder transcription credits are exhausted. Tell the user to upgrade or connect Builder.io credits, or configure a Groq key for backup speech-to-text. Generic OpenAI or Anthropic chat keys do not transcribe Clips recordings.",
474
+ ];
475
+ }
476
+
477
+ if (status === "failed" && failureReason) {
478
+ return [
479
+ `Transcription failed with reason: ${failureReason}. Explain this to the user and suggest retrying transcription or configuring the supported Builder.io/Groq transcription fallback.`,
480
+ ];
481
+ }
482
+
483
+ return [];
484
+ }
485
+
458
486
  export function buildPublicAgentContext({
459
487
  event,
460
488
  access,
@@ -496,6 +524,7 @@ export function buildPublicAgentContext({
496
524
  }));
497
525
  const instructions = [
498
526
  "Use transcript.segments for timestamped spoken context.",
527
+ ...transcriptStatusInstructions(transcript),
499
528
  ...(bugReport
500
529
  ? [
501
530
  "Use bugReport for the submitted product context: source URL, page title, app version, environment, severity, and redacted host metadata.",
@@ -559,6 +588,8 @@ export function buildPublicAgentContext({
559
588
  transcript: {
560
589
  status: transcript?.status ?? "missing",
561
590
  language: transcript?.language ?? null,
591
+ failureReason: transcript?.failureReason ?? null,
592
+ retryAfterSeconds: transcript?.status === "pending" ? 15 : null,
562
593
  fullText: transcript?.fullText ?? "",
563
594
  segments: agentSegments,
564
595
  segmentCount: agentSegments.length,