@agent-native/core 0.79.17 → 0.79.18

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 (344) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +8 -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/index.ts +7 -1
  22. package/corpus/core/src/client/org/OrgSwitcher.tsx +5 -5
  23. package/corpus/core/src/client/org/TeamPage.tsx +11 -3
  24. package/corpus/core/src/client/settings/SettingsTabsPage.tsx +216 -0
  25. package/corpus/core/src/client/settings/index.ts +5 -0
  26. package/corpus/core/src/file-upload/index.ts +1 -0
  27. package/corpus/core/src/file-upload/registry.ts +22 -6
  28. package/corpus/core/src/file-upload/types.ts +6 -1
  29. package/corpus/core/src/integrations/adapters/email.ts +26 -18
  30. package/corpus/core/src/integrations/adapters/slack.ts +8 -7
  31. package/corpus/core/src/integrations/adapters/telegram.ts +9 -11
  32. package/corpus/core/src/integrations/adapters/whatsapp.ts +12 -9
  33. package/corpus/core/src/integrations/plugin.ts +79 -13
  34. package/corpus/core/src/onboarding/default-steps.ts +6 -3
  35. package/corpus/core/src/onboarding/types.ts +1 -1
  36. package/corpus/core/src/org/handlers.ts +1 -1
  37. package/corpus/core/src/server/agent-chat-plugin.ts +4 -7
  38. package/corpus/core/src/server/better-auth-instance.ts +19 -62
  39. package/corpus/core/src/server/core-routes-plugin.ts +93 -180
  40. package/corpus/core/src/server/create-server.ts +55 -116
  41. package/corpus/core/src/server/email-actions.ts +3 -6
  42. package/corpus/core/src/server/email.ts +52 -17
  43. package/corpus/core/src/server/index.ts +2 -1
  44. package/corpus/core/src/server/scoped-key-storage.ts +223 -0
  45. package/corpus/core/src/shared/agent-env.ts +3 -3
  46. package/corpus/core/src/sharing/actions/share-resource.ts +1 -1
  47. package/corpus/core/src/styles/agent-native.css +47 -3
  48. package/corpus/core/src/templates/workspace-root/AGENTS.md +3 -2
  49. package/corpus/core/src/templates/workspace-root/README.md +1 -1
  50. package/corpus/core/src/templates/workspace-root/scripts/repair-workspace-org.ts +29 -90
  51. package/corpus/core/src/terminal/pty-server.ts +3 -38
  52. package/corpus/core/src/triggers/dispatcher.ts +1 -2
  53. package/corpus/templates/.retired/issues/package.json +7 -0
  54. package/corpus/templates/.retired/meeting-notes/package.json +7 -0
  55. package/corpus/templates/.retired/recruiting/package.json +7 -0
  56. package/corpus/templates/.retired/scheduling/package.json +7 -0
  57. package/corpus/templates/.retired/voice/package.json +7 -0
  58. package/corpus/templates/analytics/app/components/layout/Header.tsx +0 -1
  59. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +0 -17
  60. package/corpus/templates/analytics/app/i18n/zh-TW.ts +7 -1
  61. package/corpus/templates/analytics/app/i18n-data.ts +71 -2
  62. package/corpus/templates/analytics/app/pages/Settings.tsx +120 -73
  63. package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +0 -11
  64. package/corpus/templates/analytics/app/routes/catalog.tsx +33 -176
  65. package/corpus/templates/analytics/app/routes/team.tsx +3 -2
  66. package/corpus/templates/analytics/changelog/2026-06-26-dashboard-templates-are-tucked-into-settings-and-show-simple.md +6 -0
  67. package/corpus/templates/analytics/changelog/2026-06-26-the-sessions-page-shows-a-single-page-title-instead-of-repea.md +6 -0
  68. package/corpus/templates/analytics/server/lib/dashboard-catalog.ts +37 -28
  69. package/corpus/templates/assets/app/routes/settings.tsx +65 -43
  70. package/corpus/templates/assets/app/routes/team.tsx +2 -10
  71. package/corpus/templates/brain/app/routes/settings.tsx +390 -358
  72. package/corpus/templates/brain/app/routes/team.tsx +2 -8
  73. package/corpus/templates/calendar/actions/db-connect.ts +5 -29
  74. package/corpus/templates/calendar/app/components/CloudUpgrade.tsx +8 -51
  75. package/corpus/templates/calendar/app/components/calendar/GoogleConnectBanner.tsx +2 -1
  76. package/corpus/templates/calendar/app/components/calendar/GoogleSetupWizard.tsx +4 -2
  77. package/corpus/templates/calendar/app/components/layout/Sidebar.tsx +0 -2
  78. package/corpus/templates/calendar/app/pages/Settings.tsx +304 -271
  79. package/corpus/templates/calendar/app/routes/_app.team.tsx +2 -17
  80. package/corpus/templates/calendar/server/handlers/google-auth.ts +22 -9
  81. package/corpus/templates/calendar/server/lib/booking-emails.ts +1 -1
  82. package/corpus/templates/calendar/server/lib/event-guest-notifications.ts +1 -1
  83. package/corpus/templates/calendar/server/lib/google-calendar.ts +48 -28
  84. package/corpus/templates/chat/app/components/layout/Header.tsx +0 -1
  85. package/corpus/templates/chat/app/hooks/use-navigation-state.ts +4 -2
  86. package/corpus/templates/chat/app/routes/settings.tsx +54 -54
  87. package/corpus/templates/chat/app/routes/team.tsx +2 -10
  88. package/corpus/templates/clips/.agents/skills/ai-video-tools/SKILL.md +7 -2
  89. package/corpus/templates/clips/.agents/skills/recording/SKILL.md +1 -1
  90. package/corpus/templates/clips/.agents/skills/video-sharing/SKILL.md +7 -0
  91. package/corpus/templates/clips/AGENTS.md +8 -0
  92. package/corpus/templates/clips/actions/invite-member.ts +1 -1
  93. package/corpus/templates/clips/actions/request-transcript.ts +1 -1
  94. package/corpus/templates/clips/app/components/player/transcript-panel.tsx +2 -0
  95. package/corpus/templates/clips/app/routes/_app.settings._index.tsx +629 -643
  96. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +219 -155
  97. package/corpus/templates/clips/changelog/2026-06-26-long-desktop-recordings-are-checkpointed-so-macos-finalizati.md +6 -0
  98. package/corpus/templates/clips/changelog/2026-06-26-non-mac-desktop-recordings-wait-for-upload-transcription.md +6 -0
  99. package/corpus/templates/clips/changelog/2026-06-26-s3-compatible-storage-keys-now-save-securely-from-clips-sett.md +6 -0
  100. package/corpus/templates/clips/changelog/2026-06-26-shared-clip-links-guide-agents-through-processing-transcripts.md +6 -0
  101. package/corpus/templates/clips/changelog/2026-06-26-the-clip-details-sidebar-now-collapses-into-a-slide-out-pane.md +6 -0
  102. package/corpus/templates/clips/desktop/src/lib/recorder.ts +55 -3
  103. package/corpus/templates/clips/desktop/src/lib/transcription-capture.ts +217 -5
  104. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +1 -0
  105. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +176 -5
  106. package/corpus/templates/clips/server/lib/public-agent-context.ts +31 -0
  107. package/corpus/templates/clips/server/lib/s3-upload-provider.ts +60 -14
  108. package/corpus/templates/clips/server/lib/video-storage.ts +37 -9
  109. package/corpus/templates/clips/server/routes/api/agent-transcript.json.get.ts +5 -0
  110. package/corpus/templates/content/app/components/layout/Header.tsx +0 -1
  111. package/corpus/templates/content/app/components/sidebar/NotionButton.tsx +1 -0
  112. package/corpus/templates/content/app/routes/_app.settings.tsx +54 -55
  113. package/corpus/templates/content/app/routes/_app.team.tsx +2 -28
  114. package/corpus/templates/content/server/lib/notion.ts +43 -11
  115. package/corpus/templates/content/server/routes/api/notion/auth-url.get.ts +2 -2
  116. package/corpus/templates/content/server/routes/api/notion/status.get.ts +3 -3
  117. package/corpus/templates/design/app/routes/settings.tsx +60 -38
  118. package/corpus/templates/design/app/routes/team.tsx +2 -10
  119. package/corpus/templates/dispatch/app/hooks/use-navigation-state.ts +2 -2
  120. package/corpus/templates/dispatch/app/routes/settings.tsx +70 -55
  121. package/corpus/templates/dispatch/app/routes/team.tsx +5 -1
  122. package/corpus/templates/forms/actions/db-connect.ts +7 -36
  123. package/corpus/templates/forms/actions/navigate.ts +2 -2
  124. package/corpus/templates/forms/app/components/CloudUpgrade.tsx +8 -47
  125. package/corpus/templates/forms/app/components/layout/Header.tsx +0 -1
  126. package/corpus/templates/forms/app/components/layout/Layout.tsx +42 -25
  127. package/corpus/templates/forms/app/components/layout/Sidebar.tsx +35 -76
  128. package/corpus/templates/forms/app/global.css +9 -25
  129. package/corpus/templates/forms/app/hooks/use-navigation-state.ts +4 -4
  130. package/corpus/templates/forms/app/i18n/ar-SA.ts +1 -0
  131. package/corpus/templates/forms/app/i18n/de-DE.ts +1 -0
  132. package/corpus/templates/forms/app/i18n/en-US.ts +1 -0
  133. package/corpus/templates/forms/app/i18n/es-ES.ts +1 -0
  134. package/corpus/templates/forms/app/i18n/fr-FR.ts +1 -0
  135. package/corpus/templates/forms/app/i18n/hi-IN.ts +1 -0
  136. package/corpus/templates/forms/app/i18n/ja-JP.ts +1 -0
  137. package/corpus/templates/forms/app/i18n/ko-KR.ts +1 -0
  138. package/corpus/templates/forms/app/i18n/pt-BR.ts +1 -0
  139. package/corpus/templates/forms/app/i18n/zh-CN.ts +1 -0
  140. package/corpus/templates/forms/app/i18n/zh-TW.ts +1 -0
  141. package/corpus/templates/forms/app/lib/route-prewarm.ts +9 -8
  142. package/corpus/templates/forms/app/pages/AskPage.tsx +70 -0
  143. package/corpus/templates/forms/app/pages/FormsListPage.tsx +74 -81
  144. package/corpus/templates/forms/app/root.tsx +1 -1
  145. package/corpus/templates/forms/app/routes/_app.ask.tsx +14 -0
  146. package/corpus/templates/forms/app/routes/_app.settings.tsx +54 -54
  147. package/corpus/templates/forms/app/routes/_app.team.tsx +2 -9
  148. package/corpus/templates/forms/app/routes/_index.tsx +21 -130
  149. package/corpus/templates/forms/changelog/2026-06-26-ask-forms-opens-as-a-standard-app-tab-with-a-simpler-sidebar.md +6 -0
  150. package/corpus/templates/forms/changelog/2026-06-26-forms-are-shown-in-a-tighter-row-list-with-a-denser-sidebar.md +6 -0
  151. package/corpus/templates/forms/shared/navigation.ts +4 -2
  152. package/corpus/templates/macros/app/routes/settings.tsx +54 -54
  153. package/corpus/templates/macros/app/routes/team.tsx +2 -10
  154. package/corpus/templates/mail/actions/helpers.ts +2 -2
  155. package/corpus/templates/mail/app/components/GoogleConnectBanner.tsx +2 -1
  156. package/corpus/templates/mail/app/pages/SettingsPage.tsx +5 -5
  157. package/corpus/templates/mail/app/routes/team.tsx +3 -2
  158. package/corpus/templates/mail/server/handlers/emails.ts +3 -2
  159. package/corpus/templates/mail/server/handlers/google-auth.ts +19 -12
  160. package/corpus/templates/mail/server/lib/automation-engine.ts +3 -2
  161. package/corpus/templates/mail/server/lib/email-state.ts +2 -3
  162. package/corpus/templates/mail/server/lib/google-auth.ts +28 -11
  163. package/corpus/templates/mail/server/lib/jobs.ts +3 -2
  164. package/corpus/templates/mail/server/onboarding.ts +9 -6
  165. package/corpus/templates/mail/server/routes/api/attachments.get.ts +3 -3
  166. package/corpus/templates/plan/actions/request-plan-access.ts +1 -1
  167. package/corpus/templates/plan/app/components/layout/Header.tsx +0 -1
  168. package/corpus/templates/plan/app/hooks/use-navigation-state.ts +4 -2
  169. package/corpus/templates/plan/app/lib/route-prewarm.ts +0 -4
  170. package/corpus/templates/plan/app/routes/settings.tsx +75 -73
  171. package/corpus/templates/plan/app/routes/team.tsx +2 -8
  172. package/corpus/templates/plan/server/lib/comment-notifications.ts +1 -1
  173. package/corpus/templates/slides/actions/check-image-gen.ts +2 -1
  174. package/corpus/templates/slides/actions/db-connect.ts +7 -45
  175. package/corpus/templates/slides/actions/edit-image.ts +6 -3
  176. package/corpus/templates/slides/actions/generate-image-api.ts +1 -1
  177. package/corpus/templates/slides/actions/generate-image.ts +2 -2
  178. package/corpus/templates/slides/actions/generate-slides-ai.ts +5 -3
  179. package/corpus/templates/slides/actions/image-gen-status.ts +4 -3
  180. package/corpus/templates/slides/actions/image-search.ts +6 -3
  181. package/corpus/templates/slides/actions/search-images.ts +6 -3
  182. package/corpus/templates/slides/app/components/CloudUpgrade.tsx +8 -51
  183. package/corpus/templates/slides/app/components/layout/Header.tsx +0 -1
  184. package/corpus/templates/slides/app/components/layout/Sidebar.tsx +0 -2
  185. package/corpus/templates/slides/app/routes/settings.tsx +54 -54
  186. package/corpus/templates/slides/app/routes/team.tsx +2 -12
  187. package/corpus/templates/slides/server/handlers/google-docs-auth.ts +9 -9
  188. package/corpus/templates/slides/server/handlers/image-gen.ts +5 -1
  189. package/corpus/templates/slides/server/handlers/image-providers/gemini.ts +9 -1
  190. package/corpus/templates/slides/server/handlers/image-providers/index.ts +18 -8
  191. package/corpus/templates/slides/server/handlers/image-providers/openai.ts +10 -4
  192. package/corpus/templates/slides/server/handlers/image-providers/types.ts +1 -0
  193. package/corpus/templates/slides/server/lib/google-docs-oauth.ts +59 -34
  194. package/corpus/templates/slides/server/register-secrets.ts +60 -0
  195. package/corpus/templates/videos/actions/db-connect.ts +7 -45
  196. package/corpus/templates/videos/app/components/CloudUpgrade.tsx +8 -51
  197. package/corpus/templates/videos/app/components/Sidebar.tsx +1 -9
  198. package/corpus/templates/videos/app/components/layout/Header.tsx +0 -1
  199. package/corpus/templates/videos/app/components/layout/NavSidebar.tsx +0 -3
  200. package/corpus/templates/videos/app/routes/settings.tsx +54 -54
  201. package/corpus/templates/videos/app/routes/team.tsx +2 -8
  202. package/dist/agent/engine/builder-engine.d.ts.map +1 -1
  203. package/dist/agent/engine/builder-engine.js +8 -3
  204. package/dist/agent/engine/builder-engine.js.map +1 -1
  205. package/dist/cli/mcp.d.ts.map +1 -1
  206. package/dist/cli/mcp.js +3 -4
  207. package/dist/cli/mcp.js.map +1 -1
  208. package/dist/client/AssistantChat.d.ts.map +1 -1
  209. package/dist/client/AssistantChat.js +6 -2
  210. package/dist/client/AssistantChat.js.map +1 -1
  211. package/dist/client/chat/markdown-renderer.d.ts.map +1 -1
  212. package/dist/client/chat/markdown-renderer.js +1 -0
  213. package/dist/client/chat/markdown-renderer.js.map +1 -1
  214. package/dist/client/index.d.ts +1 -1
  215. package/dist/client/index.d.ts.map +1 -1
  216. package/dist/client/index.js +1 -1
  217. package/dist/client/index.js.map +1 -1
  218. package/dist/client/org/OrgSwitcher.d.ts +3 -3
  219. package/dist/client/org/OrgSwitcher.js +2 -2
  220. package/dist/client/org/OrgSwitcher.js.map +1 -1
  221. package/dist/client/org/TeamPage.d.ts +6 -1
  222. package/dist/client/org/TeamPage.d.ts.map +1 -1
  223. package/dist/client/org/TeamPage.js +2 -2
  224. package/dist/client/org/TeamPage.js.map +1 -1
  225. package/dist/client/settings/SettingsTabsPage.d.ts +27 -0
  226. package/dist/client/settings/SettingsTabsPage.d.ts.map +1 -0
  227. package/dist/client/settings/SettingsTabsPage.js +108 -0
  228. package/dist/client/settings/SettingsTabsPage.js.map +1 -0
  229. package/dist/client/settings/index.d.ts +1 -0
  230. package/dist/client/settings/index.d.ts.map +1 -1
  231. package/dist/client/settings/index.js +1 -0
  232. package/dist/client/settings/index.js.map +1 -1
  233. package/dist/collab/routes.d.ts +1 -1
  234. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  235. package/dist/file-upload/index.d.ts +1 -1
  236. package/dist/file-upload/index.d.ts.map +1 -1
  237. package/dist/file-upload/index.js +1 -1
  238. package/dist/file-upload/index.js.map +1 -1
  239. package/dist/file-upload/registry.d.ts +1 -0
  240. package/dist/file-upload/registry.d.ts.map +1 -1
  241. package/dist/file-upload/registry.js +24 -6
  242. package/dist/file-upload/registry.js.map +1 -1
  243. package/dist/file-upload/types.d.ts +6 -1
  244. package/dist/file-upload/types.d.ts.map +1 -1
  245. package/dist/file-upload/types.js.map +1 -1
  246. package/dist/integrations/adapters/email.d.ts.map +1 -1
  247. package/dist/integrations/adapters/email.js +18 -18
  248. package/dist/integrations/adapters/email.js.map +1 -1
  249. package/dist/integrations/adapters/slack.d.ts.map +1 -1
  250. package/dist/integrations/adapters/slack.js +8 -7
  251. package/dist/integrations/adapters/slack.js.map +1 -1
  252. package/dist/integrations/adapters/telegram.d.ts.map +1 -1
  253. package/dist/integrations/adapters/telegram.js +9 -9
  254. package/dist/integrations/adapters/telegram.js.map +1 -1
  255. package/dist/integrations/adapters/whatsapp.d.ts.map +1 -1
  256. package/dist/integrations/adapters/whatsapp.js +10 -9
  257. package/dist/integrations/adapters/whatsapp.js.map +1 -1
  258. package/dist/integrations/plugin.d.ts.map +1 -1
  259. package/dist/integrations/plugin.js +47 -13
  260. package/dist/integrations/plugin.js.map +1 -1
  261. package/dist/notifications/routes.d.ts +1 -1
  262. package/dist/observability/routes.d.ts +2 -2
  263. package/dist/onboarding/default-steps.d.ts.map +1 -1
  264. package/dist/onboarding/default-steps.js +6 -5
  265. package/dist/onboarding/default-steps.js.map +1 -1
  266. package/dist/onboarding/types.d.ts +1 -1
  267. package/dist/onboarding/types.d.ts.map +1 -1
  268. package/dist/onboarding/types.js.map +1 -1
  269. package/dist/org/handlers.js +1 -1
  270. package/dist/org/handlers.js.map +1 -1
  271. package/dist/progress/routes.d.ts +1 -1
  272. package/dist/resources/handlers.d.ts +3 -3
  273. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  274. package/dist/server/agent-chat-plugin.js +4 -6
  275. package/dist/server/agent-chat-plugin.js.map +1 -1
  276. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  277. package/dist/server/better-auth-instance.d.ts.map +1 -1
  278. package/dist/server/better-auth-instance.js +17 -56
  279. package/dist/server/better-auth-instance.js.map +1 -1
  280. package/dist/server/core-routes-plugin.d.ts +1 -1
  281. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  282. package/dist/server/core-routes-plugin.js +67 -153
  283. package/dist/server/core-routes-plugin.js.map +1 -1
  284. package/dist/server/create-server.d.ts +3 -9
  285. package/dist/server/create-server.d.ts.map +1 -1
  286. package/dist/server/create-server.js +37 -91
  287. package/dist/server/create-server.js.map +1 -1
  288. package/dist/server/email-actions.d.ts +2 -3
  289. package/dist/server/email-actions.d.ts.map +1 -1
  290. package/dist/server/email-actions.js +3 -6
  291. package/dist/server/email-actions.js.map +1 -1
  292. package/dist/server/email.d.ts +3 -3
  293. package/dist/server/email.d.ts.map +1 -1
  294. package/dist/server/email.js +40 -19
  295. package/dist/server/email.js.map +1 -1
  296. package/dist/server/index.d.ts +3 -3
  297. package/dist/server/index.d.ts.map +1 -1
  298. package/dist/server/index.js +3 -3
  299. package/dist/server/index.js.map +1 -1
  300. package/dist/server/scoped-key-storage.d.ts +22 -0
  301. package/dist/server/scoped-key-storage.d.ts.map +1 -0
  302. package/dist/server/scoped-key-storage.js +152 -0
  303. package/dist/server/scoped-key-storage.js.map +1 -0
  304. package/dist/server/transcribe-voice.d.ts +1 -1
  305. package/dist/shared/agent-env.d.ts +3 -3
  306. package/dist/shared/agent-env.d.ts.map +1 -1
  307. package/dist/shared/agent-env.js +3 -3
  308. package/dist/shared/agent-env.js.map +1 -1
  309. package/dist/sharing/actions/share-resource.js +1 -1
  310. package/dist/sharing/actions/share-resource.js.map +1 -1
  311. package/dist/styles/agent-native.css +47 -3
  312. package/dist/templates/workspace-root/AGENTS.md +3 -2
  313. package/dist/templates/workspace-root/README.md +1 -1
  314. package/dist/templates/workspace-root/scripts/repair-workspace-org.ts +29 -90
  315. package/dist/terminal/pty-server.d.ts.map +1 -1
  316. package/dist/terminal/pty-server.js +3 -36
  317. package/dist/terminal/pty-server.js.map +1 -1
  318. package/dist/triggers/dispatcher.js +1 -1
  319. package/dist/triggers/dispatcher.js.map +1 -1
  320. package/docs/content/file-uploads.md +11 -4
  321. package/docs/content/locales/ar-SA/multi-app-workspace.md +3 -3
  322. package/docs/content/locales/de-DE/multi-app-workspace.md +3 -3
  323. package/docs/content/locales/es-ES/multi-app-workspace.md +3 -3
  324. package/docs/content/locales/fr-FR/multi-app-workspace.md +3 -3
  325. package/docs/content/locales/hi-IN/multi-app-workspace.md +3 -3
  326. package/docs/content/locales/ja-JP/multi-app-workspace.md +3 -3
  327. package/docs/content/locales/ko-KR/multi-app-workspace.md +3 -3
  328. package/docs/content/locales/pt-BR/multi-app-workspace.md +3 -3
  329. package/docs/content/locales/zh-CN/multi-app-workspace.md +3 -3
  330. package/docs/content/locales/zh-TW/multi-app-workspace.md +3 -3
  331. package/docs/content/multi-app-workspace.md +3 -3
  332. package/docs/content/template-clips.md +14 -5
  333. package/package.json +1 -1
  334. package/src/templates/workspace-root/AGENTS.md +3 -2
  335. package/src/templates/workspace-root/README.md +1 -1
  336. package/src/templates/workspace-root/scripts/repair-workspace-org.ts +29 -90
  337. package/corpus/core/src/server/env-var-writes.ts +0 -16
  338. package/corpus/templates/analytics/app/pages/Team.tsx +0 -9
  339. package/corpus/templates/calendar/app/pages/Team.tsx +0 -12
  340. package/corpus/templates/mail/app/pages/Team.tsx +0 -17
  341. package/dist/server/env-var-writes.d.ts +0 -10
  342. package/dist/server/env-var-writes.d.ts.map +0 -1
  343. package/dist/server/env-var-writes.js +0 -18
  344. package/dist/server/env-var-writes.js.map +0 -1
@@ -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,
@@ -16,6 +16,7 @@
16
16
  */
17
17
 
18
18
  import type { FileUploadProvider } from "@agent-native/core/file-upload";
19
+ import { resolveSecret } from "@agent-native/core/server";
19
20
 
20
21
  interface S3Config {
21
22
  region: string;
@@ -26,27 +27,71 @@ interface S3Config {
26
27
  publicBaseUrl: string | null;
27
28
  }
28
29
 
29
- function readS3Config(): S3Config | null {
30
- const env = process.env;
31
- const bucket = env.S3_BUCKET || env.R2_BUCKET;
32
- const accessKeyId = env.S3_ACCESS_KEY_ID || env.R2_ACCESS_KEY_ID;
33
- const secretAccessKey = env.S3_SECRET_ACCESS_KEY || env.R2_SECRET_ACCESS_KEY;
34
- const endpoint = env.S3_ENDPOINT || env.R2_ENDPOINT;
30
+ function cleanValue(value: string | null | undefined): string | undefined {
31
+ const cleaned = value?.trim();
32
+ return cleaned ? cleaned : undefined;
33
+ }
34
+
35
+ function buildS3Config(values: {
36
+ bucket?: string;
37
+ accessKeyId?: string;
38
+ secretAccessKey?: string;
39
+ endpoint?: string;
40
+ region?: string;
41
+ publicBaseUrl?: string;
42
+ }): S3Config | null {
43
+ const bucket = cleanValue(values.bucket);
44
+ const accessKeyId = cleanValue(values.accessKeyId);
45
+ const secretAccessKey = cleanValue(values.secretAccessKey);
46
+ const endpoint = cleanValue(values.endpoint);
35
47
  if (!bucket || !accessKeyId || !secretAccessKey || !endpoint) return null;
36
48
  return {
37
- region: env.S3_REGION || env.R2_REGION || "auto",
49
+ region: cleanValue(values.region) ?? "auto",
38
50
  bucket,
39
51
  accessKeyId,
40
52
  secretAccessKey,
41
53
  endpoint: endpoint.replace(/\/+$/, ""),
42
54
  publicBaseUrl:
43
- (env.S3_PUBLIC_BASE_URL || env.R2_PUBLIC_BASE_URL || "").replace(
44
- /\/+$/,
45
- "",
46
- ) || null,
55
+ cleanValue(values.publicBaseUrl)?.replace(/\/+$/, "") ?? null,
47
56
  };
48
57
  }
49
58
 
59
+ function readS3EnvConfig(): S3Config | null {
60
+ const env = process.env;
61
+ return buildS3Config({
62
+ bucket: env.S3_BUCKET || env.R2_BUCKET,
63
+ accessKeyId: env.S3_ACCESS_KEY_ID || env.R2_ACCESS_KEY_ID,
64
+ secretAccessKey: env.S3_SECRET_ACCESS_KEY || env.R2_SECRET_ACCESS_KEY,
65
+ endpoint: env.S3_ENDPOINT || env.R2_ENDPOINT,
66
+ region: env.S3_REGION || env.R2_REGION,
67
+ publicBaseUrl: env.S3_PUBLIC_BASE_URL || env.R2_PUBLIC_BASE_URL,
68
+ });
69
+ }
70
+
71
+ async function resolveS3Secret(primary: string, fallback: string) {
72
+ return (
73
+ cleanValue(await resolveSecret(primary).catch(() => null)) ??
74
+ cleanValue(await resolveSecret(fallback).catch(() => null))
75
+ );
76
+ }
77
+
78
+ async function readS3Config(): Promise<S3Config | null> {
79
+ return buildS3Config({
80
+ bucket: await resolveS3Secret("S3_BUCKET", "R2_BUCKET"),
81
+ accessKeyId: await resolveS3Secret("S3_ACCESS_KEY_ID", "R2_ACCESS_KEY_ID"),
82
+ secretAccessKey: await resolveS3Secret(
83
+ "S3_SECRET_ACCESS_KEY",
84
+ "R2_SECRET_ACCESS_KEY",
85
+ ),
86
+ endpoint: await resolveS3Secret("S3_ENDPOINT", "R2_ENDPOINT"),
87
+ region: await resolveS3Secret("S3_REGION", "R2_REGION"),
88
+ publicBaseUrl: await resolveS3Secret(
89
+ "S3_PUBLIC_BASE_URL",
90
+ "R2_PUBLIC_BASE_URL",
91
+ ),
92
+ });
93
+ }
94
+
50
95
  // ── SigV4 helpers (Web Crypto, no SDK) ────────────────────────────────
51
96
 
52
97
  async function hmac(key: ArrayBuffer, msg: string): Promise<ArrayBuffer> {
@@ -187,10 +232,11 @@ async function putObject(
187
232
  export const s3FileUploadProvider: FileUploadProvider = {
188
233
  id: "s3",
189
234
  name: "S3-compatible storage",
190
- isConfigured: () => readS3Config() !== null,
235
+ isConfigured: () => readS3EnvConfig() !== null,
236
+ isConfiguredForRequest: async () => (await readS3Config()) !== null,
191
237
  upload: async ({ data, filename, mimeType }) => {
192
- const cfg = readS3Config();
193
- if (!cfg) throw new Error("S3 env vars not configured");
238
+ const cfg = await readS3Config();
239
+ if (!cfg) throw new Error("S3 credentials are not configured");
194
240
 
195
241
  const ext = filename?.split(".").pop() ?? "bin";
196
242
  const stamp = Date.now();