@agent-native/core 0.103.1 → 0.104.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (565) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +30 -0
  3. package/corpus/core/package.json +9 -2
  4. package/corpus/core/src/agent/engine/credential-errors.ts +2 -2
  5. package/corpus/core/src/agent/production-agent.ts +30 -0
  6. package/corpus/core/src/agent/run-store.ts +12 -2
  7. package/corpus/core/src/cli/create.ts +1 -0
  8. package/corpus/core/src/cli/skills-content/visual-plan-skill.ts +30 -11
  9. package/corpus/core/src/cli/templates-meta.ts +4 -3
  10. package/corpus/core/src/client/AgentAskPopover.tsx +89 -0
  11. package/corpus/core/src/client/AgentChatHome.tsx +4 -0
  12. package/corpus/core/src/client/AgentPanel.tsx +321 -223
  13. package/corpus/core/src/client/AssistantChat.tsx +6 -10
  14. package/corpus/core/src/client/MultiTabAssistantChat.tsx +53 -372
  15. package/corpus/core/src/client/agent-chat-adapter.ts +13 -1
  16. package/corpus/core/src/client/agent-page/AgentContextTab.tsx +133 -149
  17. package/corpus/core/src/client/agent-page/AgentEmptyState.tsx +44 -0
  18. package/corpus/core/src/client/agent-page/AgentJobsTab.tsx +153 -82
  19. package/corpus/core/src/client/agent-page/AgentTabFrame.tsx +39 -0
  20. package/corpus/core/src/client/agent-page/AgentTabsPage.tsx +368 -356
  21. package/corpus/core/src/client/agent-page/types.ts +1 -1
  22. package/corpus/core/src/client/analytics.ts +51 -5
  23. package/corpus/core/src/client/chat/run-recovery.tsx +12 -4
  24. package/corpus/core/src/client/composer/PromptComposer.tsx +5 -15
  25. package/corpus/core/src/client/composer/TiptapComposer.tsx +32 -3
  26. package/corpus/core/src/client/dynamic-suggestions.ts +3 -3
  27. package/corpus/core/src/client/index.ts +16 -1
  28. package/corpus/core/src/client/resources/ResourceTree.tsx +249 -38
  29. package/corpus/core/src/client/resources/ResourcesPanel.tsx +307 -140
  30. package/corpus/core/src/client/session-replay.ts +158 -5
  31. package/corpus/core/src/client/settings/AutomationsSection.tsx +30 -10
  32. package/corpus/core/src/client/settings/SettingsPanel.tsx +3 -2
  33. package/corpus/core/src/client/settings/SettingsTabsPage.tsx +2 -0
  34. package/corpus/core/src/client/settings/agent-settings-search.ts +2 -2
  35. package/corpus/core/src/client/use-chat-threads.ts +2 -0
  36. package/corpus/core/src/connections/catalog.ts +49 -7
  37. package/corpus/core/src/db/index.ts +6 -1
  38. package/corpus/core/src/extensions/actions.ts +106 -16
  39. package/corpus/core/src/extensions/content-patch.ts +22 -0
  40. package/corpus/core/src/extensions/routes.ts +18 -0
  41. package/corpus/core/src/extensions/store.ts +8 -0
  42. package/corpus/core/src/index.ts +3 -0
  43. package/corpus/core/src/ingestion/docx.ts +158 -0
  44. package/corpus/core/src/ingestion/figma-node-to-html.ts +1860 -0
  45. package/corpus/core/src/ingestion/figma.ts +548 -0
  46. package/corpus/core/src/ingestion/index.ts +107 -0
  47. package/corpus/core/src/ingestion/media.ts +264 -0
  48. package/corpus/core/src/ingestion/notion.ts +121 -0
  49. package/corpus/core/src/ingestion/office.ts +214 -0
  50. package/corpus/core/src/ingestion/orchestration.ts +160 -0
  51. package/corpus/core/src/ingestion/pptx.ts +333 -0
  52. package/corpus/core/src/ingestion/selection.ts +81 -0
  53. package/corpus/core/src/ingestion/website.ts +151 -0
  54. package/corpus/core/src/integrations/plugin.ts +45 -37
  55. package/corpus/core/src/localization/default-messages.ts +35 -12
  56. package/corpus/core/src/observability/hosted-model-experiment.ts +118 -0
  57. package/corpus/core/src/observability/traces.ts +5 -3
  58. package/corpus/core/src/provider-api/index.ts +452 -8
  59. package/corpus/core/src/scripts/agent-engines/list-agent-engines.ts +18 -1
  60. package/corpus/core/src/search-utils/index.ts +112 -0
  61. package/corpus/core/src/secrets/register-framework-secrets.ts +61 -0
  62. package/corpus/core/src/server/action-discovery.ts +33 -8
  63. package/corpus/core/src/server/agent-chat/framework-prompts.ts +2 -2
  64. package/corpus/core/src/server/agent-chat/prompt-resources.ts +144 -0
  65. package/corpus/core/src/server/agent-chat-plugin.ts +6 -0
  66. package/corpus/core/src/server/core-routes-plugin.ts +12 -0
  67. package/corpus/core/src/server/index.ts +30 -0
  68. package/corpus/core/src/server/workspace-provider-oauth.ts +583 -0
  69. package/corpus/core/src/styles/agent-native.css +9 -0
  70. package/corpus/core/src/templates/default/app/i18n/en-US.ts +3 -3
  71. package/corpus/core/src/templates/workspace-core/.agents/skills/extensions/SKILL.md +14 -5
  72. package/corpus/core/src/templates/workspace-root/.env.example +12 -0
  73. package/corpus/core/src/workspace-connections/index.ts +6 -0
  74. package/corpus/core/src/workspace-connections/lifecycle.ts +43 -0
  75. package/corpus/core/src/workspace-connections/store.ts +16 -0
  76. package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +46 -3
  77. package/corpus/templates/analytics/AGENTS.md +9 -0
  78. package/corpus/templates/analytics/actions/dashboard-mutation-api.ts +120 -6
  79. package/corpus/templates/analytics/actions/mutate-dashboard.ts +92 -30
  80. package/corpus/templates/analytics/actions/update-dashboard.ts +5 -5
  81. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +313 -220
  82. package/corpus/templates/analytics/app/components/layout/CommandPalette.tsx +2 -2
  83. package/corpus/templates/analytics/app/components/layout/Layout.tsx +9 -6
  84. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +90 -64
  85. package/corpus/templates/analytics/app/i18n-data.ts +3 -3
  86. package/corpus/templates/analytics/app/lib/dashboard-list-loading.ts +25 -0
  87. package/corpus/templates/analytics/app/lib/item-popularity.ts +13 -4
  88. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/DashboardFilterBar.tsx +1 -1
  89. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/pivot.ts +8 -3
  90. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts +1 -0
  91. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +15 -3
  92. package/corpus/templates/analytics/changelog/2026-07-16-dashboard-filter-defaults-can-now-be-changed-reliably-withou.md +6 -0
  93. package/corpus/templates/analytics/changelog/2026-07-16-dashboard-lists-load-in-one-stable-order-without-popping-bet.md +6 -0
  94. package/corpus/templates/analytics/changelog/2026-07-16-fixed-dashboard-filter-dropdown-text-alignment.md +6 -0
  95. package/corpus/templates/analytics/changelog/2026-07-16-recurring-signed-in-users-can-now-be-viewed-as-weekly-bars-b.md +6 -0
  96. package/corpus/templates/analytics/changelog/2026-07-16-the-analytics-agent-can-now-duplicate-a-chart-and-place-the-.md +6 -0
  97. package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +41 -1
  98. package/corpus/templates/analytics/server/lib/dashboard-catalog.ts +1 -1
  99. package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +47 -1
  100. package/corpus/templates/analytics/server/lib/session-replay.ts +68 -0
  101. package/corpus/templates/analytics/server/plugins/agent-chat.ts +1 -0
  102. package/corpus/templates/analytics/server/routes/sessions/lookup.get.ts +47 -0
  103. package/corpus/templates/assets/.agents/skills/asset-generation/SKILL.md +16 -9
  104. package/corpus/templates/assets/.agents/skills/creative-context/SKILL.md +86 -0
  105. package/corpus/templates/assets/AGENTS.md +10 -0
  106. package/corpus/templates/assets/actions/create-generation-session.ts +59 -1
  107. package/corpus/templates/assets/actions/edit-image.ts +2 -0
  108. package/corpus/templates/assets/actions/generate-asset.ts +52 -0
  109. package/corpus/templates/assets/actions/generate-image-batch.ts +55 -0
  110. package/corpus/templates/assets/actions/generate-image.ts +219 -1
  111. package/corpus/templates/assets/actions/open-asset-picker.ts +14 -0
  112. package/corpus/templates/assets/actions/refine-image.ts +3 -0
  113. package/corpus/templates/assets/actions/restyle-image.ts +2 -0
  114. package/corpus/templates/assets/agent-native.app-skill.json +4 -0
  115. package/corpus/templates/assets/app/components/layout/Layout.tsx +2 -0
  116. package/corpus/templates/assets/app/i18n/zh-TW.ts +3 -0
  117. package/corpus/templates/assets/app/i18n-data.ts +29 -13
  118. package/corpus/templates/assets/app/routes/agent.tsx +7 -1
  119. package/corpus/templates/assets/app/routes/library.tsx +9 -0
  120. package/corpus/templates/assets/app/routes/settings.tsx +3 -0
  121. package/corpus/templates/assets/changelog/2026-07-16-creative-context-library.md +6 -0
  122. package/corpus/templates/assets/package.json +1 -0
  123. package/corpus/templates/assets/server/lib/image-processing.ts +1 -22
  124. package/corpus/templates/assets/server/plugins/creative-context.ts +205 -0
  125. package/corpus/templates/brain/app/i18n-data.ts +3 -3
  126. package/corpus/templates/brain/app/lib/brain.ts +1 -1
  127. package/corpus/templates/brain/server/lib/search.ts +17 -67
  128. package/corpus/templates/calendar/app/i18n-data.ts +3 -3
  129. package/corpus/templates/calendar/app/pages/BookingLinksPage.tsx +19 -18
  130. package/corpus/templates/chat/app/i18n/en-US.ts +3 -3
  131. package/corpus/templates/chat/app/lib/agent-page.tsx +10 -1
  132. package/corpus/templates/clips/.agents/skills/meetings/SKILL.md +1 -1
  133. package/corpus/templates/clips/AGENTS.md +4 -0
  134. package/corpus/templates/clips/actions/cleanup-transcript.ts +35 -4
  135. package/corpus/templates/clips/actions/create-meeting.ts +6 -2
  136. package/corpus/templates/clips/actions/create-organization.ts +3 -2
  137. package/corpus/templates/clips/actions/create-recording.ts +4 -1
  138. package/corpus/templates/clips/actions/finalize-recording.ts +44 -7
  139. package/corpus/templates/clips/actions/import-loom-recording.ts +4 -1
  140. package/corpus/templates/clips/actions/lib/create-recording-schema.ts +2 -2
  141. package/corpus/templates/clips/actions/list-meetings.ts +23 -5
  142. package/corpus/templates/clips/actions/list-organization-state.ts +1 -1
  143. package/corpus/templates/clips/actions/remove-silences.ts +10 -1
  144. package/corpus/templates/clips/actions/request-transcript.ts +53 -11
  145. package/corpus/templates/clips/actions/search-recordings-utils.ts +7 -0
  146. package/corpus/templates/clips/actions/search-recordings.ts +5 -8
  147. package/corpus/templates/clips/actions/set-organization-branding.ts +2 -2
  148. package/corpus/templates/clips/actions/stitch-recordings.ts +10 -5
  149. package/corpus/templates/clips/actions/update-ai-request-status.ts +38 -0
  150. package/corpus/templates/clips/app/components/editor/editor-layout.tsx +3 -4
  151. package/corpus/templates/clips/app/components/editor/editor-selection.ts +19 -0
  152. package/corpus/templates/clips/app/components/editor/stitch-manager.tsx +10 -10
  153. package/corpus/templates/clips/app/components/library/bulk-action-toolbar.tsx +1 -2
  154. package/corpus/templates/clips/app/components/library/library-grid.tsx +101 -93
  155. package/corpus/templates/clips/app/components/player/video-player.tsx +11 -0
  156. package/corpus/templates/clips/app/components/workspace/branding-editor.tsx +53 -1
  157. package/corpus/templates/clips/app/i18n/ar-SA.ts +7 -1
  158. package/corpus/templates/clips/app/i18n/de-DE.ts +7 -1
  159. package/corpus/templates/clips/app/i18n/en-US.ts +10 -4
  160. package/corpus/templates/clips/app/i18n/es-ES.ts +7 -1
  161. package/corpus/templates/clips/app/i18n/fr-FR.ts +7 -1
  162. package/corpus/templates/clips/app/i18n/hi-IN.ts +7 -1
  163. package/corpus/templates/clips/app/i18n/ja-JP.ts +7 -1
  164. package/corpus/templates/clips/app/i18n/ko-KR.ts +7 -1
  165. package/corpus/templates/clips/app/i18n/pt-BR.ts +7 -1
  166. package/corpus/templates/clips/app/i18n/zh-CN.ts +6 -1
  167. package/corpus/templates/clips/app/i18n/zh-TW.ts +6 -1
  168. package/corpus/templates/clips/app/routes/_app.settings._index.tsx +5 -9
  169. package/corpus/templates/clips/app/routes/_app.settings.organization.tsx +6 -2
  170. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +129 -27
  171. package/corpus/templates/clips/app/routes/record.tsx +23 -2
  172. package/corpus/templates/clips/app/routes/share.$shareId.tsx +6 -2
  173. package/corpus/templates/clips/changelog/2026-07-16-authorized-viewers-can-open-shared-private-clips-without-a-m.md +6 -0
  174. package/corpus/templates/clips/changelog/2026-07-16-camera-bubble-hover-controls-appear-above-the-face.md +6 -0
  175. package/corpus/templates/clips/changelog/2026-07-16-chrome-extension-recovers-stale-recorder-state.md +6 -0
  176. package/corpus/templates/clips/changelog/2026-07-16-clips-agent-chat-no-longer-shows-a-separate-workspace-header.md +6 -0
  177. package/corpus/templates/clips/changelog/2026-07-16-clips-search-now-matches-recording-content-without-requiring.md +6 -0
  178. package/corpus/templates/clips/changelog/2026-07-16-declined-meetings-no-longer-show-desktop-reminders.md +6 -0
  179. package/corpus/templates/clips/changelog/2026-07-16-failed-desktop-saves-now-show-their-local-recovery-copy.md +6 -0
  180. package/corpus/templates/clips/changelog/2026-07-16-long-desktop-clips-now-preserve-complete-playback-synced-aud.md +6 -0
  181. package/corpus/templates/clips/changelog/2026-07-16-long-recording-lists-keep-stitch-and-library-actions-visible.md +6 -0
  182. package/corpus/templates/clips/changelog/2026-07-16-organization-admins-can-choose-new-recording-visibility.md +6 -0
  183. package/corpus/templates/clips/changelog/2026-07-16-remove-silences-now-shows-queued-active-completed-and-failed.md +6 -0
  184. package/corpus/templates/clips/changelog/2026-07-16-settings-no-longer-repeats-the-builder-connection-status-in-.md +6 -0
  185. package/corpus/templates/clips/changelog/2026-07-16-transcripts-now-appear-from-native-browser-speech-sooner-wit.md +6 -0
  186. package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -1
  187. package/corpus/templates/clips/chrome-extension/src/background.ts +73 -1
  188. package/corpus/templates/clips/chrome-extension/src/native-recording-state.ts +32 -0
  189. package/corpus/templates/clips/chrome-extension/src/native-transcription.ts +248 -0
  190. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +106 -0
  191. package/corpus/templates/clips/desktop/src/app.tsx +66 -28
  192. package/corpus/templates/clips/desktop/src/lib/recorder.ts +335 -285
  193. package/corpus/templates/clips/desktop/src/lib/single-flight.ts +7 -0
  194. package/corpus/templates/clips/desktop/src/lib/upload-verification.ts +51 -0
  195. package/corpus/templates/clips/desktop/src/overlays/bubble.tsx +2 -2
  196. package/corpus/templates/clips/desktop/src/overlays/finalizing.tsx +10 -2
  197. package/corpus/templates/clips/desktop/src/styles.css +13 -11
  198. package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +8 -9
  199. package/corpus/templates/clips/desktop/src-tauri/src/meetings_watcher.rs +1 -0
  200. package/corpus/templates/clips/desktop/src-tauri/src/native_screen/custom_capture.rs +88 -13
  201. package/corpus/templates/clips/desktop/src-tauri/src/native_screen/live_upload.rs +13 -2
  202. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +241 -6
  203. package/corpus/templates/clips/server/db/schema.ts +2 -2
  204. package/corpus/templates/clips/server/lib/builder-media-compression.ts +64 -67
  205. package/corpus/templates/clips/server/lib/calendar-event-classification.ts +19 -0
  206. package/corpus/templates/clips/server/lib/google-calendar-client.ts +1 -0
  207. package/corpus/templates/clips/server/lib/recordings.ts +40 -1
  208. package/corpus/templates/clips/server/plugins/agent-chat.ts +1 -0
  209. package/corpus/templates/clips/server/plugins/db.ts +2 -2
  210. package/corpus/templates/clips/server/routes/api/auth/google-calendar.get.ts +1 -1
  211. package/corpus/templates/clips/server/routes/api/public-recording.get.ts +14 -5
  212. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +51 -1
  213. package/corpus/templates/clips/server/routes/api/video/[recordingId].get.ts +1 -64
  214. package/corpus/templates/clips/shared/media-worker-contract.ts +32 -15
  215. package/corpus/templates/content/.agents/skills/content/SKILL.md +16 -0
  216. package/corpus/templates/content/.agents/skills/creative-context/SKILL.md +85 -0
  217. package/corpus/templates/content/.agents/skills/notion-integration/SKILL.md +8 -0
  218. package/corpus/templates/content/AGENTS.md +9 -0
  219. package/corpus/templates/content/actions/create-document.ts +94 -0
  220. package/corpus/templates/content/actions/edit-document.ts +144 -4
  221. package/corpus/templates/content/actions/update-document.ts +138 -0
  222. package/corpus/templates/content/agent-native.app-skill.json +4 -0
  223. package/corpus/templates/content/app/components/layout/Layout.tsx +2 -0
  224. package/corpus/templates/content/app/i18n/zh-TW.ts +3 -0
  225. package/corpus/templates/content/app/i18n-data.ts +13 -4
  226. package/corpus/templates/content/app/routes/_app.agent.tsx +2 -1
  227. package/corpus/templates/content/app/routes/_app.settings.tsx +3 -0
  228. package/corpus/templates/content/changelog/2026-07-16-creative-context-library.md +6 -0
  229. package/corpus/templates/content/package.json +1 -0
  230. package/corpus/templates/content/server/plugins/creative-context.ts +31 -0
  231. package/corpus/templates/design/.agents/skills/creative-context/SKILL.md +116 -0
  232. package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +16 -0
  233. package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +8 -0
  234. package/corpus/templates/design/AGENTS.md +6 -0
  235. package/corpus/templates/design/actions/clone-creative-context-design.ts +176 -0
  236. package/corpus/templates/design/actions/edit-design.ts +128 -0
  237. package/corpus/templates/design/actions/generate-design.ts +278 -14
  238. package/corpus/templates/design/actions/generate-screens.ts +42 -1
  239. package/corpus/templates/design/agent-native.app-skill.json +4 -0
  240. package/corpus/templates/design/app/components/layout/Layout.tsx +7 -3
  241. package/corpus/templates/design/app/i18n/zh-TW.ts +3 -0
  242. package/corpus/templates/design/app/i18n-data.ts +10 -4
  243. package/corpus/templates/design/app/routes/agent.tsx +2 -1
  244. package/corpus/templates/design/app/routes/settings.tsx +3 -0
  245. package/corpus/templates/design/changelog/2026-07-16-creative-context-library.md +6 -0
  246. package/corpus/templates/design/changelog/2026-07-16-design-agent-chat-no-longer-shows-a-separate-workspace-header.md +6 -0
  247. package/corpus/templates/design/package.json +1 -0
  248. package/corpus/templates/design/server/lib/figma-design-context.ts +5 -335
  249. package/corpus/templates/design/server/lib/figma-node-to-html.ts +1 -1859
  250. package/corpus/templates/design/server/lib/import-design-files.ts +6 -8
  251. package/corpus/templates/design/server/plugins/creative-context.ts +31 -0
  252. package/corpus/templates/design/shared/generation-session.ts +7 -0
  253. package/corpus/templates/dispatch/app/i18n-data.ts +3 -3
  254. package/corpus/templates/forms/app/i18n/en-US.ts +3 -3
  255. package/corpus/templates/macros/app/i18n/en-US.ts +3 -3
  256. package/corpus/templates/mail/app/components/email/EmailThread.tsx +35 -5
  257. package/corpus/templates/mail/app/i18n/en-US.ts +3 -3
  258. package/corpus/templates/mail/changelog/2026-07-16-mail-previews-now-expand-to-show-the-full-message-without-an.md +6 -0
  259. package/corpus/templates/plan/.agents/skills/visual-plan/SKILL.md +30 -11
  260. package/corpus/templates/plan/AGENTS.md +21 -1
  261. package/corpus/templates/plan/actions/get-visual-plan.ts +1 -1
  262. package/corpus/templates/plan/actions/patch-visual-plan-source.ts +161 -0
  263. package/corpus/templates/plan/actions/read-visual-plan-source.ts +1 -1
  264. package/corpus/templates/plan/actions/update-visual-plan.ts +126 -10
  265. package/corpus/templates/plan/app/hooks/use-plans.ts +2 -0
  266. package/corpus/templates/plan/app/i18n/en-US.ts +3 -3
  267. package/corpus/templates/plan/app/pages/PlansPage.tsx +65 -62
  268. package/corpus/templates/plan/changelog/2026-07-16-local-plan-previews-now-load-annotations-containing-html-lik.md +6 -0
  269. package/corpus/templates/plan/changelog/2026-07-16-plan-agents-now-prevent-stale-edits-from-overwriting-newer-p.md +6 -0
  270. package/corpus/templates/plan/server/plan-mdx.ts +75 -2
  271. package/corpus/templates/slides/.agents/skills/create-deck/SKILL.md +21 -4
  272. package/corpus/templates/slides/.agents/skills/creative-context/SKILL.md +122 -0
  273. package/corpus/templates/slides/.agents/skills/design-systems/SKILL.md +11 -0
  274. package/corpus/templates/slides/.agents/skills/slide-editing/SKILL.md +12 -3
  275. package/corpus/templates/slides/AGENTS.md +6 -0
  276. package/corpus/templates/slides/actions/add-slide.ts +230 -14
  277. package/corpus/templates/slides/actions/clone-context-slide.ts +302 -0
  278. package/corpus/templates/slides/actions/create-deck.ts +130 -0
  279. package/corpus/templates/slides/actions/extract-pdf.ts +6 -2
  280. package/corpus/templates/slides/actions/import-file.ts +5 -4
  281. package/corpus/templates/slides/actions/patch-deck.ts +200 -10
  282. package/corpus/templates/slides/actions/update-slide.ts +208 -15
  283. package/corpus/templates/slides/app/components/layout/Layout.tsx +2 -0
  284. package/corpus/templates/slides/app/components/visual-editor/CanvasCommentPins.tsx +16 -1
  285. package/corpus/templates/slides/app/i18n/ar-SA.ts +3 -0
  286. package/corpus/templates/slides/app/i18n/de-DE.ts +3 -0
  287. package/corpus/templates/slides/app/i18n/en-US.ts +6 -3
  288. package/corpus/templates/slides/app/i18n/es-ES.ts +3 -0
  289. package/corpus/templates/slides/app/i18n/fr-FR.ts +3 -0
  290. package/corpus/templates/slides/app/i18n/hi-IN.ts +3 -0
  291. package/corpus/templates/slides/app/i18n/ja-JP.ts +3 -0
  292. package/corpus/templates/slides/app/i18n/ko-KR.ts +3 -0
  293. package/corpus/templates/slides/app/i18n/pt-BR.ts +3 -0
  294. package/corpus/templates/slides/app/i18n/zh-CN.ts +3 -0
  295. package/corpus/templates/slides/app/i18n/zh-TW.ts +3 -0
  296. package/corpus/templates/slides/app/lib/validate-native-slide-html.ts +176 -0
  297. package/corpus/templates/slides/app/routes/agent.tsx +7 -1
  298. package/corpus/templates/slides/app/routes/settings.tsx +3 -0
  299. package/corpus/templates/slides/changelog/2026-07-16-creative-context-library.md +6 -0
  300. package/corpus/templates/slides/changelog/2026-07-16-google-slides-native-cloning.md +6 -0
  301. package/corpus/templates/slides/package.json +1 -0
  302. package/corpus/templates/slides/server/handlers/import/docx-parser.ts +4 -85
  303. package/corpus/templates/slides/server/handlers/import/pptx-parser.ts +7 -421
  304. package/corpus/templates/slides/server/plugins/creative-context.ts +166 -0
  305. package/dist/agent/engine/credential-errors.d.ts +1 -1
  306. package/dist/agent/engine/credential-errors.d.ts.map +1 -1
  307. package/dist/agent/engine/credential-errors.js +2 -2
  308. package/dist/agent/engine/credential-errors.js.map +1 -1
  309. package/dist/agent/production-agent.d.ts.map +1 -1
  310. package/dist/agent/production-agent.js +25 -0
  311. package/dist/agent/production-agent.js.map +1 -1
  312. package/dist/agent/run-store.d.ts.map +1 -1
  313. package/dist/agent/run-store.js +12 -2
  314. package/dist/agent/run-store.js.map +1 -1
  315. package/dist/cli/create.d.ts.map +1 -1
  316. package/dist/cli/create.js +1 -0
  317. package/dist/cli/create.js.map +1 -1
  318. package/dist/cli/skills-content/visual-plan-skill.d.ts +1 -1
  319. package/dist/cli/skills-content/visual-plan-skill.d.ts.map +1 -1
  320. package/dist/cli/skills-content/visual-plan-skill.js +30 -11
  321. package/dist/cli/skills-content/visual-plan-skill.js.map +1 -1
  322. package/dist/cli/templates-meta.d.ts.map +1 -1
  323. package/dist/cli/templates-meta.js +4 -3
  324. package/dist/cli/templates-meta.js.map +1 -1
  325. package/dist/client/AgentAskPopover.d.ts +11 -0
  326. package/dist/client/AgentAskPopover.d.ts.map +1 -0
  327. package/dist/client/AgentAskPopover.js +30 -0
  328. package/dist/client/AgentAskPopover.js.map +1 -0
  329. package/dist/client/AgentChatHome.d.ts +1 -1
  330. package/dist/client/AgentChatHome.d.ts.map +1 -1
  331. package/dist/client/AgentChatHome.js +2 -2
  332. package/dist/client/AgentChatHome.js.map +1 -1
  333. package/dist/client/AgentPanel.d.ts +9 -19
  334. package/dist/client/AgentPanel.d.ts.map +1 -1
  335. package/dist/client/AgentPanel.js +119 -80
  336. package/dist/client/AgentPanel.js.map +1 -1
  337. package/dist/client/AssistantChat.d.ts.map +1 -1
  338. package/dist/client/AssistantChat.js +4 -4
  339. package/dist/client/AssistantChat.js.map +1 -1
  340. package/dist/client/MultiTabAssistantChat.d.ts +3 -9
  341. package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
  342. package/dist/client/MultiTabAssistantChat.js +49 -189
  343. package/dist/client/MultiTabAssistantChat.js.map +1 -1
  344. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  345. package/dist/client/agent-chat-adapter.js +12 -1
  346. package/dist/client/agent-chat-adapter.js.map +1 -1
  347. package/dist/client/agent-page/AgentContextTab.d.ts.map +1 -1
  348. package/dist/client/agent-page/AgentContextTab.js +26 -28
  349. package/dist/client/agent-page/AgentContextTab.js.map +1 -1
  350. package/dist/client/agent-page/AgentEmptyState.d.ts +13 -0
  351. package/dist/client/agent-page/AgentEmptyState.d.ts.map +1 -0
  352. package/dist/client/agent-page/AgentEmptyState.js +6 -0
  353. package/dist/client/agent-page/AgentEmptyState.js.map +1 -0
  354. package/dist/client/agent-page/AgentJobsTab.d.ts +1 -1
  355. package/dist/client/agent-page/AgentJobsTab.d.ts.map +1 -1
  356. package/dist/client/agent-page/AgentJobsTab.js +90 -58
  357. package/dist/client/agent-page/AgentJobsTab.js.map +1 -1
  358. package/dist/client/agent-page/AgentTabFrame.d.ts +12 -0
  359. package/dist/client/agent-page/AgentTabFrame.d.ts.map +1 -0
  360. package/dist/client/agent-page/AgentTabFrame.js +7 -0
  361. package/dist/client/agent-page/AgentTabFrame.js.map +1 -0
  362. package/dist/client/agent-page/AgentTabsPage.d.ts +9 -1
  363. package/dist/client/agent-page/AgentTabsPage.d.ts.map +1 -1
  364. package/dist/client/agent-page/AgentTabsPage.js +135 -99
  365. package/dist/client/agent-page/AgentTabsPage.js.map +1 -1
  366. package/dist/client/agent-page/types.d.ts +1 -1
  367. package/dist/client/agent-page/types.d.ts.map +1 -1
  368. package/dist/client/agent-page/types.js.map +1 -1
  369. package/dist/client/analytics.d.ts +10 -3
  370. package/dist/client/analytics.d.ts.map +1 -1
  371. package/dist/client/analytics.js +37 -2
  372. package/dist/client/analytics.js.map +1 -1
  373. package/dist/client/chat/run-recovery.d.ts.map +1 -1
  374. package/dist/client/chat/run-recovery.js +10 -3
  375. package/dist/client/chat/run-recovery.js.map +1 -1
  376. package/dist/client/composer/PromptComposer.d.ts.map +1 -1
  377. package/dist/client/composer/PromptComposer.js +2 -2
  378. package/dist/client/composer/PromptComposer.js.map +1 -1
  379. package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
  380. package/dist/client/composer/TiptapComposer.js +15 -3
  381. package/dist/client/composer/TiptapComposer.js.map +1 -1
  382. package/dist/client/dynamic-suggestions.js +3 -3
  383. package/dist/client/dynamic-suggestions.js.map +1 -1
  384. package/dist/client/index.d.ts +4 -3
  385. package/dist/client/index.d.ts.map +1 -1
  386. package/dist/client/index.js +3 -2
  387. package/dist/client/index.js.map +1 -1
  388. package/dist/client/resources/ResourceTree.d.ts +5 -1
  389. package/dist/client/resources/ResourceTree.d.ts.map +1 -1
  390. package/dist/client/resources/ResourceTree.js +74 -5
  391. package/dist/client/resources/ResourceTree.js.map +1 -1
  392. package/dist/client/resources/ResourcesPanel.d.ts +11 -2
  393. package/dist/client/resources/ResourcesPanel.d.ts.map +1 -1
  394. package/dist/client/resources/ResourcesPanel.js +105 -31
  395. package/dist/client/resources/ResourcesPanel.js.map +1 -1
  396. package/dist/client/session-replay.d.ts +28 -0
  397. package/dist/client/session-replay.d.ts.map +1 -1
  398. package/dist/client/session-replay.js +122 -8
  399. package/dist/client/session-replay.js.map +1 -1
  400. package/dist/client/settings/AutomationsSection.d.ts +3 -1
  401. package/dist/client/settings/AutomationsSection.d.ts.map +1 -1
  402. package/dist/client/settings/AutomationsSection.js +16 -6
  403. package/dist/client/settings/AutomationsSection.js.map +1 -1
  404. package/dist/client/settings/SettingsPanel.d.ts.map +1 -1
  405. package/dist/client/settings/SettingsPanel.js +3 -3
  406. package/dist/client/settings/SettingsPanel.js.map +1 -1
  407. package/dist/client/settings/SettingsTabsPage.d.ts +2 -0
  408. package/dist/client/settings/SettingsTabsPage.d.ts.map +1 -1
  409. package/dist/client/settings/SettingsTabsPage.js.map +1 -1
  410. package/dist/client/settings/agent-settings-search.js +2 -2
  411. package/dist/client/settings/agent-settings-search.js.map +1 -1
  412. package/dist/client/use-chat-threads.d.ts +2 -0
  413. package/dist/client/use-chat-threads.d.ts.map +1 -1
  414. package/dist/client/use-chat-threads.js.map +1 -1
  415. package/dist/collab/routes.d.ts +1 -1
  416. package/dist/connections/catalog.d.ts +41 -7
  417. package/dist/connections/catalog.d.ts.map +1 -1
  418. package/dist/connections/catalog.js +40 -7
  419. package/dist/connections/catalog.js.map +1 -1
  420. package/dist/db/index.d.ts +2 -2
  421. package/dist/db/index.d.ts.map +1 -1
  422. package/dist/db/index.js +2 -2
  423. package/dist/db/index.js.map +1 -1
  424. package/dist/extensions/actions.d.ts.map +1 -1
  425. package/dist/extensions/actions.js +85 -15
  426. package/dist/extensions/actions.js.map +1 -1
  427. package/dist/extensions/content-patch.d.ts +4 -0
  428. package/dist/extensions/content-patch.d.ts.map +1 -1
  429. package/dist/extensions/content-patch.js +18 -0
  430. package/dist/extensions/content-patch.js.map +1 -1
  431. package/dist/extensions/routes.d.ts.map +1 -1
  432. package/dist/extensions/routes.js +14 -0
  433. package/dist/extensions/routes.js.map +1 -1
  434. package/dist/extensions/store.d.ts +1 -0
  435. package/dist/extensions/store.d.ts.map +1 -1
  436. package/dist/extensions/store.js +4 -1
  437. package/dist/extensions/store.js.map +1 -1
  438. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  439. package/dist/index.d.ts +1 -1
  440. package/dist/index.d.ts.map +1 -1
  441. package/dist/index.js +1 -1
  442. package/dist/index.js.map +1 -1
  443. package/dist/ingestion/docx.d.ts +16 -0
  444. package/dist/ingestion/docx.d.ts.map +1 -0
  445. package/dist/ingestion/docx.js +129 -0
  446. package/dist/ingestion/docx.js.map +1 -0
  447. package/dist/ingestion/figma-node-to-html.d.ts +304 -0
  448. package/dist/ingestion/figma-node-to-html.d.ts.map +1 -0
  449. package/dist/ingestion/figma-node-to-html.js +1292 -0
  450. package/dist/ingestion/figma-node-to-html.js.map +1 -0
  451. package/dist/ingestion/figma.d.ts +115 -0
  452. package/dist/ingestion/figma.d.ts.map +1 -0
  453. package/dist/ingestion/figma.js +399 -0
  454. package/dist/ingestion/figma.js.map +1 -0
  455. package/dist/ingestion/index.d.ts +11 -0
  456. package/dist/ingestion/index.d.ts.map +1 -0
  457. package/dist/ingestion/index.js +11 -0
  458. package/dist/ingestion/index.js.map +1 -0
  459. package/dist/ingestion/media.d.ts +36 -0
  460. package/dist/ingestion/media.d.ts.map +1 -0
  461. package/dist/ingestion/media.js +178 -0
  462. package/dist/ingestion/media.js.map +1 -0
  463. package/dist/ingestion/notion.d.ts +12 -0
  464. package/dist/ingestion/notion.d.ts.map +1 -0
  465. package/dist/ingestion/notion.js +104 -0
  466. package/dist/ingestion/notion.js.map +1 -0
  467. package/dist/ingestion/office.d.ts +45 -0
  468. package/dist/ingestion/office.d.ts.map +1 -0
  469. package/dist/ingestion/office.js +141 -0
  470. package/dist/ingestion/office.js.map +1 -0
  471. package/dist/ingestion/orchestration.d.ts +52 -0
  472. package/dist/ingestion/orchestration.d.ts.map +1 -0
  473. package/dist/ingestion/orchestration.js +89 -0
  474. package/dist/ingestion/orchestration.js.map +1 -0
  475. package/dist/ingestion/pptx.d.ts +28 -0
  476. package/dist/ingestion/pptx.d.ts.map +1 -0
  477. package/dist/ingestion/pptx.js +255 -0
  478. package/dist/ingestion/pptx.js.map +1 -0
  479. package/dist/ingestion/selection.d.ts +20 -0
  480. package/dist/ingestion/selection.d.ts.map +1 -0
  481. package/dist/ingestion/selection.js +52 -0
  482. package/dist/ingestion/selection.js.map +1 -0
  483. package/dist/ingestion/website.d.ts +27 -0
  484. package/dist/ingestion/website.d.ts.map +1 -0
  485. package/dist/ingestion/website.js +100 -0
  486. package/dist/ingestion/website.js.map +1 -0
  487. package/dist/integrations/plugin.d.ts.map +1 -1
  488. package/dist/integrations/plugin.js +32 -27
  489. package/dist/integrations/plugin.js.map +1 -1
  490. package/dist/localization/default-messages.d.ts +22 -0
  491. package/dist/localization/default-messages.d.ts.map +1 -1
  492. package/dist/localization/default-messages.js +31 -9
  493. package/dist/localization/default-messages.js.map +1 -1
  494. package/dist/notifications/routes.d.ts +1 -1
  495. package/dist/observability/hosted-model-experiment.d.ts +39 -0
  496. package/dist/observability/hosted-model-experiment.d.ts.map +1 -0
  497. package/dist/observability/hosted-model-experiment.js +90 -0
  498. package/dist/observability/hosted-model-experiment.js.map +1 -0
  499. package/dist/observability/traces.d.ts.map +1 -1
  500. package/dist/observability/traces.js +5 -3
  501. package/dist/observability/traces.js.map +1 -1
  502. package/dist/progress/routes.d.ts +1 -1
  503. package/dist/provider-api/index.d.ts +35 -4
  504. package/dist/provider-api/index.d.ts.map +1 -1
  505. package/dist/provider-api/index.js +339 -8
  506. package/dist/provider-api/index.js.map +1 -1
  507. package/dist/scripts/agent-engines/list-agent-engines.d.ts.map +1 -1
  508. package/dist/scripts/agent-engines/list-agent-engines.js +18 -1
  509. package/dist/scripts/agent-engines/list-agent-engines.js.map +1 -1
  510. package/dist/search-utils/index.d.ts +12 -0
  511. package/dist/search-utils/index.d.ts.map +1 -0
  512. package/dist/search-utils/index.js +95 -0
  513. package/dist/search-utils/index.js.map +1 -0
  514. package/dist/secrets/register-framework-secrets.d.ts.map +1 -1
  515. package/dist/secrets/register-framework-secrets.js +57 -0
  516. package/dist/secrets/register-framework-secrets.js.map +1 -1
  517. package/dist/server/action-discovery.d.ts +8 -0
  518. package/dist/server/action-discovery.d.ts.map +1 -1
  519. package/dist/server/action-discovery.js +26 -9
  520. package/dist/server/action-discovery.js.map +1 -1
  521. package/dist/server/agent-chat/framework-prompts.js +2 -2
  522. package/dist/server/agent-chat/framework-prompts.js.map +1 -1
  523. package/dist/server/agent-chat/prompt-resources.d.ts +19 -0
  524. package/dist/server/agent-chat/prompt-resources.d.ts.map +1 -1
  525. package/dist/server/agent-chat/prompt-resources.js +95 -0
  526. package/dist/server/agent-chat/prompt-resources.js.map +1 -1
  527. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  528. package/dist/server/agent-chat-plugin.js +7 -0
  529. package/dist/server/agent-chat-plugin.js.map +1 -1
  530. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  531. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  532. package/dist/server/core-routes-plugin.js +5 -0
  533. package/dist/server/core-routes-plugin.js.map +1 -1
  534. package/dist/server/index.d.ts +5 -1
  535. package/dist/server/index.d.ts.map +1 -1
  536. package/dist/server/index.js +5 -1
  537. package/dist/server/index.js.map +1 -1
  538. package/dist/server/workspace-provider-oauth.d.ts +48 -0
  539. package/dist/server/workspace-provider-oauth.d.ts.map +1 -0
  540. package/dist/server/workspace-provider-oauth.js +415 -0
  541. package/dist/server/workspace-provider-oauth.js.map +1 -0
  542. package/dist/styles/agent-native.css +9 -0
  543. package/dist/templates/chat/app/i18n/en-US.ts +3 -3
  544. package/dist/templates/chat/app/lib/agent-page.tsx +10 -1
  545. package/dist/templates/default/app/i18n/en-US.ts +3 -3
  546. package/dist/templates/workspace-core/.agents/skills/extensions/SKILL.md +14 -5
  547. package/dist/templates/workspace-root/.env.example +12 -0
  548. package/dist/workspace-connections/index.d.ts +1 -0
  549. package/dist/workspace-connections/index.d.ts.map +1 -1
  550. package/dist/workspace-connections/index.js +1 -0
  551. package/dist/workspace-connections/index.js.map +1 -1
  552. package/dist/workspace-connections/lifecycle.d.ts +16 -0
  553. package/dist/workspace-connections/lifecycle.d.ts.map +1 -0
  554. package/dist/workspace-connections/lifecycle.js +14 -0
  555. package/dist/workspace-connections/lifecycle.js.map +1 -0
  556. package/dist/workspace-connections/store.d.ts.map +1 -1
  557. package/dist/workspace-connections/store.js +16 -0
  558. package/dist/workspace-connections/store.js.map +1 -1
  559. package/package.json +10 -3
  560. package/src/templates/chat/app/i18n/en-US.ts +3 -3
  561. package/src/templates/chat/app/lib/agent-page.spec.tsx +10 -1
  562. package/src/templates/chat/app/lib/agent-page.tsx +10 -1
  563. package/src/templates/default/app/i18n/en-US.ts +3 -3
  564. package/src/templates/workspace-core/.agents/skills/extensions/SKILL.md +14 -5
  565. package/src/templates/workspace-root/.env.example +12 -0
@@ -76,11 +76,16 @@ import {
76
76
  type PauseTransitionQueue,
77
77
  } from "./pause-transition";
78
78
  import { buildCaptureTitle, type CaptureTitleResult } from "./recording-title";
79
+ import { singleFlight } from "./single-flight";
79
80
  import {
80
81
  startTranscriptionCapture,
81
82
  type CapturedTranscript,
82
83
  type TranscriptionCapture,
83
84
  } from "./transcription-capture";
85
+ import {
86
+ verifyFinalizeReceipt,
87
+ type FinalizeReceipt,
88
+ } from "./upload-verification";
84
89
  import { shouldResampleVideoForUpload } from "./upload-video-stream";
85
90
 
86
91
  export type { LocalExportedFile } from "./local-export";
@@ -102,6 +107,8 @@ const LEGACY_DEV_REAL_CAPTURE_FLAG = "clips:dev-real-capture";
102
107
  const LIVE_UPLOAD_CHUNK_MS = 2_000;
103
108
  const NATIVE_FULLSCREEN_SEGMENT_MS = 5 * 60_000;
104
109
  const NATIVE_FULLSCREEN_MIME_TYPE = "video/mp4";
110
+ const MEDIA_RECORDER_STOP_TIMEOUT_MS = 15_000;
111
+ const THUMBNAIL_UPLOAD_TIMEOUT_MS = 8_000;
105
112
  // GCS resumable uploads require every non-final chunk to be a multiple of
106
113
  // 256 KiB. MediaRecorder emits arbitrary blob sizes, so on the streaming path
107
114
  // we buffer raw blobs and only PUT aligned slices; the unaligned remainder is
@@ -761,9 +768,10 @@ async function recoverReadyRecordingAfterFinalizeError({
761
768
  preferAuthenticated: true,
762
769
  });
763
770
  if (!recovered) return false;
764
- await deleteBrowserRecordingBackup(recordingId).catch((err) => {
765
- console.warn("[clips-recorder] recovered backup cleanup failed:", err);
766
- });
771
+ await markBrowserRecordingBackupError(
772
+ recordingId,
773
+ "Upload completed, but its final receipt could not be verified. The local backup was kept.",
774
+ ).catch(() => {});
767
775
  return true;
768
776
  }
769
777
 
@@ -800,7 +808,7 @@ async function postBackupChunk(
800
808
  url: string,
801
809
  blob: Blob,
802
810
  authToken?: string,
803
- ): Promise<void> {
811
+ ): Promise<FinalizeReceipt | null> {
804
812
  const res = await fetch(url, {
805
813
  method: "POST",
806
814
  headers: buildRetryHeaders(
@@ -810,13 +818,13 @@ async function postBackupChunk(
810
818
  credentials: "include",
811
819
  body: blob,
812
820
  });
821
+ const body = await res.text().catch(() => "");
813
822
  if (!res.ok) {
814
- const body = await res.text().catch(() => "");
815
823
  throw new Error(
816
824
  `Upload retry failed (${res.status}): ${body.slice(0, 200)}`,
817
825
  );
818
826
  }
819
- await res.text().catch(() => {});
827
+ return body ? (JSON.parse(body) as FinalizeReceipt) : null;
820
828
  }
821
829
 
822
830
  async function resetBrowserRecordingBackupUpload(
@@ -855,7 +863,7 @@ async function replayBrowserBackupToResumableSession(
855
863
  meta: BrowserRecordingBackupMeta,
856
864
  chunks: BrowserRecordingBackupChunk[],
857
865
  authToken?: string,
858
- ): Promise<void> {
866
+ ): Promise<FinalizeReceipt | null> {
859
867
  // The backup is stored in raw MediaRecorder blobs, which have arbitrary
860
868
  // boundaries. A resumable provider needs every non-final request aligned,
861
869
  // so replay a logical file rather than reusing those blob boundaries.
@@ -894,7 +902,7 @@ async function replayBrowserBackupToResumableSession(
894
902
  // aligned it is intentionally empty; the route sends the provider's close
895
903
  // request with the bytes committed by the previous chunks.
896
904
  const finalBody = recording.slice(offset, recording.size, meta.mimeType);
897
- await postBackupChunk(
905
+ return postBackupChunk(
898
906
  chunkUrl(meta.serverUrl, meta.recordingId, fullChunks, true, {
899
907
  total: String(totalPosts),
900
908
  mimeType: meta.mimeType,
@@ -938,11 +946,12 @@ export async function retryBrowserRecordingBackup(input: {
938
946
 
939
947
  if (uploadMode === "streaming") {
940
948
  try {
941
- await replayBrowserBackupToResumableSession(
949
+ const receipt = await replayBrowserBackupToResumableSession(
942
950
  meta,
943
951
  validatedChunks,
944
952
  input.authToken,
945
953
  );
954
+ verifyFinalizeReceipt(receipt, meta);
946
955
  } catch (err) {
947
956
  if (
948
957
  await recoverReadyRecordingAfterFinalizeError({
@@ -993,11 +1002,12 @@ export async function retryBrowserRecordingBackup(input: {
993
1002
  },
994
1003
  );
995
1004
  try {
996
- await postBackupChunk(
1005
+ const receipt = await postBackupChunk(
997
1006
  finalChunkUrl,
998
1007
  new Blob([], { type: meta.mimeType }),
999
1008
  input.authToken,
1000
1009
  );
1010
+ verifyFinalizeReceipt(receipt, meta);
1001
1011
  } catch (err) {
1002
1012
  if (
1003
1013
  await recoverReadyRecordingAfterFinalizeError({
@@ -1057,7 +1067,6 @@ async function createServerRecording(
1057
1067
  hasCamera,
1058
1068
  hasAudio,
1059
1069
  spaceIds: [],
1060
- visibility: "public",
1061
1070
  ...(options?.requestStreaming
1062
1071
  ? {
1063
1072
  requestStreaming: true,
@@ -1355,6 +1364,7 @@ async function captureAndUploadRecordingThumbnail(params: {
1355
1364
  headers: buildRetryHeaders(blob.type || "image/jpeg", params.authToken),
1356
1365
  credentials: "include",
1357
1366
  body: blob,
1367
+ signal: AbortSignal.timeout(THUMBNAIL_UPLOAD_TIMEOUT_MS),
1358
1368
  });
1359
1369
  if (!res.ok) {
1360
1370
  const body = await res.text().catch(() => "");
@@ -3317,15 +3327,23 @@ async function startRecordingInner(
3317
3327
  backupMeta = { ...backupMeta, ...patch };
3318
3328
  await putBrowserRecordingBackupMeta(backupMeta);
3319
3329
  };
3320
- persistBackupMeta().catch((err) => {
3321
- console.warn("[clips-recorder] local backup metadata failed:", err);
3322
- });
3330
+ let backupFailure: Error | null = null;
3331
+ const backupWrites = new Set<Promise<void>>();
3332
+ let initialBackupWrite: Promise<void>;
3333
+ initialBackupWrite = persistBackupMeta()
3334
+ .catch((err) => {
3335
+ backupFailure = err instanceof Error ? err : new Error(String(err));
3336
+ console.warn("[clips-recorder] local backup metadata failed:", err);
3337
+ })
3338
+ .finally(() => {
3339
+ backupWrites.delete(initialBackupWrite);
3340
+ });
3341
+ backupWrites.add(initialBackupWrite);
3323
3342
 
3324
3343
  // Every raw MediaRecorder blob is mirrored to IndexedDB on both upload paths.
3325
3344
  // If uploads fail the recording can still be recovered locally — replayed to
3326
3345
  // the server (the retry first resets any resumable session so replay routes
3327
3346
  // through the buffered chunk path) or exported to a local file.
3328
- const backupWrites = new Set<Promise<void>>();
3329
3347
  const backupChunkLocally = (blob: Blob): Promise<void> => {
3330
3348
  const backupIdx = backupChunkCount++;
3331
3349
  backupBytes += blob.size;
@@ -3347,6 +3365,7 @@ async function startRecordingInner(
3347
3365
  mimeType: chunkMimeType,
3348
3366
  });
3349
3367
  } catch (err) {
3368
+ backupFailure = err instanceof Error ? err : new Error(String(err));
3350
3369
  console.warn("[clips-recorder] local chunk backup failed:", err);
3351
3370
  }
3352
3371
  })().finally(() => {
@@ -3565,74 +3584,127 @@ async function startRecordingInner(
3565
3584
  // with cameraOn. The recorder reuses that camera stream for the saved
3566
3585
  // video composite and flips the toolbar from disabled → enabled above.
3567
3586
 
3568
- const handle: RecorderHandle = {
3569
- async stop() {
3570
- if (stopped) return { recordingId: id, viewUrl: `/r/${id}` };
3571
- stopped = true;
3572
- // Stamped right after the recorder fully stops below (see stoppedAt).
3573
- // Duration must measure recorded content through the final flushed
3574
- // chunk but NOT the transcript-finalize + thumbnail + upload awaits that
3575
- // follow, which add ~seconds and would overstate the saved duration.
3576
- let stoppedAt = 0;
3577
- const viewUrl = `/r/${id}`;
3578
- const absoluteViewUrl = `${params.serverUrl.replace(/\/+$/, "")}${viewUrl}`;
3579
- console.log("[clips-recorder] stop requested");
3580
- showFinalizingFeedback();
3581
- if (tickHandle) clearInterval(tickHandle);
3582
- stateUnlistens.forEach((u) => u());
3583
- stateUnlistens = [];
3584
-
3585
- // Flush the in-flight recorder buffer, then wait for it to fully stop
3586
- // so we get the trailing dataavailable event.
3587
- const recorderStopped = new Promise<void>((resolve) => {
3588
- if (recorder.state === "inactive") {
3587
+ const performStop = async (): Promise<RecorderStopResult> => {
3588
+ if (stopped) return { recordingId: id, viewUrl: `/r/${id}` };
3589
+ stopped = true;
3590
+ // Stamped right after the recorder fully stops below (see stoppedAt).
3591
+ // Duration must measure recorded content through the final flushed
3592
+ // chunk but NOT the transcript-finalize + thumbnail + upload awaits that
3593
+ // follow, which add ~seconds and would overstate the saved duration.
3594
+ let stoppedAt = 0;
3595
+ const viewUrl = `/r/${id}`;
3596
+ const absoluteViewUrl = `${params.serverUrl.replace(/\/+$/, "")}${viewUrl}`;
3597
+ console.log("[clips-recorder] stop requested");
3598
+ showFinalizingFeedback();
3599
+ if (tickHandle) clearInterval(tickHandle);
3600
+ stateUnlistens.forEach((u) => u());
3601
+ stateUnlistens = [];
3602
+
3603
+ // Flush the in-flight recorder buffer, then wait for it to fully stop
3604
+ // so we get the trailing dataavailable event.
3605
+ const recorderStopped = new Promise<void>((resolve) => {
3606
+ if (recorder.state === "inactive") {
3607
+ stoppedAt = Date.now();
3608
+ resolve();
3609
+ return;
3610
+ }
3611
+ recorder.addEventListener(
3612
+ "stop",
3613
+ () => {
3589
3614
  stoppedAt = Date.now();
3590
3615
  resolve();
3591
- return;
3616
+ },
3617
+ { once: true },
3618
+ );
3619
+ try {
3620
+ if (recorder.state === "paused") {
3621
+ recorder.resume();
3622
+ if (pausedAt) accumulatedPauseMs += Date.now() - pausedAt;
3623
+ pausedAt = null;
3592
3624
  }
3593
- recorder.addEventListener(
3594
- "stop",
3595
- () => {
3596
- stoppedAt = Date.now();
3597
- resolve();
3598
- },
3599
- { once: true },
3625
+ } catch {
3626
+ // ignore
3627
+ }
3628
+ try {
3629
+ recorder.requestData();
3630
+ } catch {
3631
+ // ignore
3632
+ }
3633
+ try {
3634
+ recorder.stop();
3635
+ } catch {
3636
+ // ignore
3637
+ }
3638
+ });
3639
+ // `recorder.stop()` has already been requested, so recorded duration is
3640
+ // fixed even if launching the browser takes a moment. Open the existing
3641
+ // recording row now and let its page poll while upload/finalize continues.
3642
+ //
3643
+ // This must claim the native upload-open slot before launching. The
3644
+ // Finalizing overlay receives the completion event later and uses the
3645
+ // same claim; without it, browser recordings opened once here and then a
3646
+ // second time when finalization completed.
3647
+ await openNativeUploadUrl(id, absoluteViewUrl);
3648
+ const recorderStopTimedOut = await Promise.race([
3649
+ recorderStopped.then(() => false),
3650
+ wait(MEDIA_RECORDER_STOP_TIMEOUT_MS).then(() => true),
3651
+ ]);
3652
+ if (recorderStopTimedOut) {
3653
+ stoppedAt = Date.now();
3654
+ failed ??= new Error(
3655
+ "The recorder did not finish stopping. Your local backup was kept so you can retry or download it from Clips.",
3656
+ );
3657
+ void transcriptionCapture?.cancel().catch((err) => {
3658
+ console.warn(
3659
+ "[clips-recorder] transcription cancel after stop timeout failed:",
3660
+ err,
3600
3661
  );
3601
- try {
3602
- if (recorder.state === "paused") {
3603
- recorder.resume();
3604
- if (pausedAt) accumulatedPauseMs += Date.now() - pausedAt;
3605
- pausedAt = null;
3606
- }
3607
- } catch {
3608
- // ignore
3609
- }
3610
- try {
3611
- recorder.requestData();
3612
- } catch {
3613
- // ignore
3614
- }
3615
- try {
3616
- recorder.stop();
3617
- } catch {
3618
- // ignore
3619
- }
3620
3662
  });
3621
- // `recorder.stop()` has already been requested, so recorded duration is
3622
- // fixed even if launching the browser takes a moment. Open the existing
3623
- // recording row now and let its page poll while upload/finalize continues.
3624
- //
3625
- // This must claim the native upload-open slot before launching. The
3626
- // Finalizing overlay receives the completion event later and uses the
3627
- // same claim; without it, browser recordings opened once here and then a
3628
- // second time when finalization completed.
3629
- await openNativeUploadUrl(id, absoluteViewUrl);
3630
- await recorderStopped;
3631
- // Recorder has fully stopped and flushed its trailing chunk — this is the
3632
- // true end of recorded content. Everything after (transcript, thumbnail,
3633
- // upload) is post-processing and must not count toward duration.
3634
-
3635
- const thumbnailUploadPromise = captureAndUploadRecordingThumbnail({
3663
+ }
3664
+ // Use the recorder's stop event as the content boundary. If WebKit never
3665
+ // emits it, the watchdog boundary preserves the available chunks and keeps
3666
+ // post-processing time out of the recovery copy's duration.
3667
+
3668
+ const videoSettings = uploadPrimaryVideo.stream
3669
+ .getVideoTracks()[0]
3670
+ ?.getSettings();
3671
+ const displaySettings = displayStream?.getVideoTracks()[0]?.getSettings();
3672
+ const durationMs = Math.max(
3673
+ 0,
3674
+ Math.round(stoppedAt - startedAt - accumulatedPauseMs),
3675
+ );
3676
+ const width =
3677
+ typeof videoSettings?.width === "number"
3678
+ ? videoSettings.width
3679
+ : typeof displaySettings?.width === "number"
3680
+ ? displaySettings.width
3681
+ : null;
3682
+ const height =
3683
+ typeof videoSettings?.height === "number"
3684
+ ? videoSettings.height
3685
+ : typeof displaySettings?.height === "number"
3686
+ ? displaySettings.height
3687
+ : null;
3688
+ const finalMimeType = mimeType || backupMeta.mimeType || "video/webm";
3689
+ try {
3690
+ await persistBackupMeta({
3691
+ durationMs,
3692
+ width,
3693
+ height,
3694
+ bytes: backupBytes,
3695
+ hasAudio: uploadCombined.getAudioTracks().length > 0,
3696
+ hasCamera: wantsCamera,
3697
+ chunkCount: backupChunkCount,
3698
+ mimeType: finalMimeType,
3699
+ lastError: null,
3700
+ });
3701
+ } catch (err) {
3702
+ backupFailure = err instanceof Error ? err : new Error(String(err));
3703
+ console.warn("[clips-recorder] local backup final metadata failed:", err);
3704
+ }
3705
+
3706
+ if (!recorderStopTimedOut) {
3707
+ void captureAndUploadRecordingThumbnail({
3636
3708
  serverUrl: params.serverUrl,
3637
3709
  recordingId: id,
3638
3710
  stream: primaryVideo,
@@ -3659,224 +3731,202 @@ async function startRecordingInner(
3659
3731
  "No speech was captured during this recording. If you spoke or played system audio, check System Audio, Microphone input, Speech Recognition permission, and the selected mic, then retry transcription.",
3660
3732
  );
3661
3733
  }
3662
- await thumbnailUploadPromise;
3663
-
3664
- if (popoverOwnsCamera) {
3665
- console.log("[clips-recorder] releasing popover camera");
3666
- emit("clips:release-camera").catch(() => {});
3667
- }
3668
-
3669
- const videoSettings = uploadPrimaryVideo.stream
3670
- .getVideoTracks()[0]
3671
- ?.getSettings();
3672
- const displaySettings = displayStream?.getVideoTracks()[0]?.getSettings();
3673
- const durationMs = Math.max(
3674
- 0,
3675
- Math.round(stoppedAt - startedAt - accumulatedPauseMs),
3676
- );
3677
- const width =
3678
- typeof videoSettings?.width === "number"
3679
- ? videoSettings.width
3680
- : typeof displaySettings?.width === "number"
3681
- ? displaySettings.width
3682
- : null;
3683
- const height =
3684
- typeof videoSettings?.height === "number"
3685
- ? videoSettings.height
3686
- : typeof displaySettings?.height === "number"
3687
- ? displaySettings.height
3688
- : null;
3689
- const finalMimeType = mimeType || backupMeta.mimeType || "video/webm";
3690
- await persistBackupMeta({
3691
- durationMs,
3692
- width,
3693
- height,
3694
- bytes: backupBytes,
3695
- hasAudio: uploadCombined.getAudioTracks().length > 0,
3696
- hasCamera: wantsCamera,
3697
- chunkCount: backupChunkCount,
3698
- mimeType: finalMimeType,
3699
- lastError: null,
3700
- }).catch((err) => {
3701
- console.warn(
3702
- "[clips-recorder] local backup final metadata failed:",
3703
- err,
3704
- );
3705
- });
3706
-
3707
- // Null the data handler so the final MediaRecorder teardown
3708
- // doesn't keep the closure (which captures `inflight`, the URL
3709
- // builder, and indirectly the MediaStream) reachable after we're
3710
- // done with it. WebKit's MediaRecorder can retain a reference to
3711
- // its event handler for the life of the object if you leave a
3712
- // non-null ondataavailable in place — null it to break the chain.
3713
- recorder.ondataavailable = null;
3714
- // Clear MediaStream track lists — just removing our
3715
- // references to the tracks is enough; the tracks themselves are
3716
- // owned by `displayStream` / `audioStream` / `bubbleCameraStream`
3717
- // and get stopped below.
3718
- try {
3719
- uploadCombined
3720
- .getTracks()
3721
- .forEach((t) => uploadCombined.removeTrack(t));
3722
- combined.getTracks().forEach((t) => combined.removeTrack(t));
3723
- } catch {
3724
- // ignore — best-effort
3725
- }
3734
+ }
3735
+ if (popoverOwnsCamera) {
3736
+ console.log("[clips-recorder] releasing popover camera");
3737
+ emit("clips:release-camera").catch(() => {});
3738
+ }
3726
3739
 
3727
- // Stop the streams WE own so OS permission indicators clear. The
3728
- // camera stream is owned by the popover when reused we leave it
3729
- // alone so the bubble stays live if the popover is still open.
3730
- [displayStream, audioStream].forEach((s) =>
3731
- s?.getTracks().forEach((t) => t.stop()),
3732
- );
3733
- streamCleanups.forEach((cleanup) => cleanup());
3734
- if (!popoverOwnsCamera) {
3735
- bubbleCameraStream?.getTracks().forEach((t) => t.stop());
3736
- }
3740
+ // Null the data handler so the final MediaRecorder teardown
3741
+ // doesn't keep the closure (which captures `inflight`, the URL
3742
+ // builder, and indirectly the MediaStream) reachable after we're
3743
+ // done with it. WebKit's MediaRecorder can retain a reference to
3744
+ // its event handler for the life of the object if you leave a
3745
+ // non-null ondataavailable in place — null it to break the chain.
3746
+ recorder.ondataavailable = null;
3747
+ // Clear MediaStream track lists — just removing our
3748
+ // references to the tracks is enough; the tracks themselves are
3749
+ // owned by `displayStream` / `audioStream` / `bubbleCameraStream`
3750
+ // and get stopped below.
3751
+ try {
3752
+ uploadCombined.getTracks().forEach((t) => uploadCombined.removeTrack(t));
3753
+ combined.getTracks().forEach((t) => combined.removeTrack(t));
3754
+ } catch {
3755
+ // ignore — best-effort
3756
+ }
3737
3757
 
3738
- // Hide the recording-specific overlays (countdown + toolbar). The
3739
- // bubble is managed by the popover's session effectwhen the
3740
- // popover is hidden or the user turns camera off, that effect tears
3741
- // down the bubble. Closing it here would cause a flicker on the
3742
- // cancel path where the popover re-appears with camera still on.
3743
- console.log("[clips-recorder] hiding recording chrome");
3744
- await invoke("hide_recording_chrome").catch((err) =>
3745
- console.error(`[clips-recorder] hide_recording_chrome failed:`, err),
3746
- );
3758
+ // Stop the streams WE own so OS permission indicators clear. The
3759
+ // camera stream is owned by the popover when reusedwe leave it
3760
+ // alone so the bubble stays live if the popover is still open.
3761
+ [displayStream, audioStream].forEach((s) =>
3762
+ s?.getTracks().forEach((t) => t.stop()),
3763
+ );
3764
+ streamCleanups.forEach((cleanup) => cleanup());
3765
+ if (!popoverOwnsCamera) {
3766
+ bubbleCameraStream?.getTracks().forEach((t) => t.stop());
3767
+ }
3747
3768
 
3748
- // Wait for any in-flight chunk uploads to settle before sending the
3749
- // final chunk. Otherwise the server could finalize before the last
3750
- // few bytes land. On the streaming path uploads are serialized through
3751
- // `streamQueue`; on the buffered path they run concurrently and each
3752
- // `.finally` has already removed settled entries from `inflight`.
3753
- const pending = Array.from(inflight);
3754
- if (uploadMode === "streaming") {
3755
- await streamQueue;
3756
- } else {
3757
- await Promise.allSettled(pending);
3758
- }
3759
- inflight.clear();
3760
- if (failed) {
3761
- try {
3762
- // Keep the guard until the final metadata and every trailing chunk
3763
- // backup are durable, even though this upload cannot be finalized.
3764
- await Promise.allSettled([...backupWrites]);
3765
- console.error("[clips-recorder] chunk upload failed:", failed);
3766
- await markBrowserRecordingBackupError(id, failed.message).catch(
3767
- () => {},
3768
- );
3769
- await abortRecordingUpload(params.serverUrl, id, failed.message);
3770
- } finally {
3771
- await clearRecordingState();
3772
- await publishFinalizingResult({
3773
- recordingId: id,
3774
- viewUrl: absoluteViewUrl,
3775
- ok: false,
3776
- error: failed.message,
3777
- });
3778
- }
3779
- throw failed;
3780
- }
3769
+ // Hide the recording-specific overlays (countdown + toolbar). The
3770
+ // bubble is managed by the popover's session effect when the
3771
+ // popover is hidden or the user turns camera off, that effect tears
3772
+ // down the bubble. Closing it here would cause a flicker on the
3773
+ // cancel path where the popover re-appears with camera still on.
3774
+ console.log("[clips-recorder] hiding recording chrome");
3775
+ await invoke("hide_recording_chrome").catch((err) =>
3776
+ console.error(`[clips-recorder] hide_recording_chrome failed:`, err),
3777
+ );
3781
3778
 
3782
- // Streaming: the closing bytes are whatever remains under the 256 KiB
3783
- // alignment boundary send them as the final chunk so the resumable
3784
- // session can complete. Buffered: bytes are already staged server-side,
3785
- // so the final chunk is a 0-byte close sentinel.
3786
- const finalBody =
3787
- uploadMode === "streaming"
3788
- ? new Blob(pendingStreamBlobs, { type: finalMimeType })
3789
- : new Blob([], { type: finalMimeType });
3790
- pendingStreamBlobs = [];
3791
- pendingStreamBytes = 0;
3792
-
3793
- const finalizeUrl = chunkUrl(params.serverUrl, id, chunkIndex, true, {
3794
- mimeType: finalMimeType,
3795
- durationMs: String(durationMs),
3796
- ...(width ? { width: String(width) } : {}),
3797
- ...(height ? { height: String(height) } : {}),
3798
- hasAudio: backupMeta.hasAudio ? "1" : "0",
3799
- hasCamera: wantsCamera ? "1" : "0",
3800
- });
3801
- console.log("[clips-recorder] finalize POST", finalizeUrl, {
3802
- chunksSent: chunkIndex,
3803
- inflightAtFinalize: pending.length,
3804
- finalBodyBytes: finalBody.size,
3805
- uploadMode,
3806
- anyFailed: !!failed,
3807
- });
3779
+ // Wait for any in-flight chunk uploads to settle before sending the
3780
+ // final chunk. Otherwise the server could finalize before the last
3781
+ // few bytes land. On the streaming path uploads are serialized through
3782
+ // `streamQueue`; on the buffered path they run concurrently and each
3783
+ // `.finally` has already removed settled entries from `inflight`.
3784
+ const pending = Array.from(inflight);
3785
+ if (uploadMode === "streaming") {
3786
+ await streamQueue;
3787
+ } else {
3788
+ await Promise.allSettled(pending);
3789
+ }
3790
+ inflight.clear();
3791
+ if (failed) {
3808
3792
  try {
3809
- const result = await finalizeAfterDurableBackup({
3810
- // Opening the browser must not make the desktop look idle until every
3811
- // trailing backup write and the final metadata are durable.
3812
- ensureBackupDurable: async () => {
3813
- await Promise.allSettled([...backupWrites]);
3814
- },
3815
- attemptFinalize: async () => {
3816
- try {
3817
- const finalRes = await fetch(finalizeUrl, {
3818
- method: "POST",
3819
- headers: { "Content-Type": "application/octet-stream" },
3820
- credentials: "include",
3821
- body: finalBody,
3822
- signal: AbortSignal.timeout(FINALIZE_UPLOAD_TIMEOUT_MS),
3823
- });
3824
- const bodyText = await finalRes.text().catch(() => "");
3825
- console.log(
3826
- "[clips-recorder] finalize response:",
3827
- finalRes.status,
3828
- bodyText.slice(0, 500),
3829
- );
3830
- if (!finalRes.ok) {
3831
- throw new Error(
3832
- `Finalize failed (${finalRes.status}): ${bodyText.slice(0, 200)}`,
3833
- );
3834
- }
3835
- } catch (err) {
3836
- console.error("[clips-recorder] finalize fetch failed:", err);
3837
- const error = err instanceof Error ? err : new Error(String(err));
3838
- if (
3839
- await recoverReadyRecordingAfterFinalizeError({
3840
- serverUrl: params.serverUrl,
3841
- recordingId: id,
3842
- authToken: params.authToken,
3843
- })
3844
- ) {
3845
- return { recordingId: id, viewUrl };
3846
- }
3847
- await markBrowserRecordingBackupError(id, error.message).catch(
3848
- () => {},
3849
- );
3850
- await abortRecordingUpload(params.serverUrl, id, error.message);
3851
- throw error;
3852
- }
3853
- await deleteBrowserRecordingBackup(id).catch((err) => {
3854
- console.warn(
3855
- "[clips-recorder] local backup cleanup failed:",
3856
- err,
3857
- );
3858
- });
3859
-
3860
- return { recordingId: id, viewUrl };
3861
- },
3862
- releaseGuard: clearRecordingState,
3863
- });
3864
- await publishFinalizingResult({
3865
- recordingId: id,
3866
- viewUrl: absoluteViewUrl,
3867
- ok: true,
3868
- });
3869
- return result;
3870
- } catch (err) {
3793
+ // Keep the guard until the final metadata and every trailing chunk
3794
+ // backup are durable, even though this upload cannot be finalized.
3795
+ await Promise.allSettled([...backupWrites]);
3796
+ console.error("[clips-recorder] chunk upload failed:", failed);
3797
+ await markBrowserRecordingBackupError(id, failed.message).catch(
3798
+ () => {},
3799
+ );
3800
+ await abortRecordingUpload(params.serverUrl, id, failed.message);
3801
+ } finally {
3802
+ await clearRecordingState();
3871
3803
  await publishFinalizingResult({
3872
3804
  recordingId: id,
3873
3805
  viewUrl: absoluteViewUrl,
3874
3806
  ok: false,
3875
- error: err instanceof Error ? err.message : String(err),
3807
+ error: failed.message,
3876
3808
  });
3877
- throw err;
3878
3809
  }
3879
- },
3810
+ throw failed;
3811
+ }
3812
+
3813
+ // Streaming: the closing bytes are whatever remains under the 256 KiB
3814
+ // alignment boundary — send them as the final chunk so the resumable
3815
+ // session can complete. Buffered: bytes are already staged server-side,
3816
+ // so the final chunk is a 0-byte close sentinel.
3817
+ const finalBody =
3818
+ uploadMode === "streaming"
3819
+ ? new Blob(pendingStreamBlobs, { type: finalMimeType })
3820
+ : new Blob([], { type: finalMimeType });
3821
+ pendingStreamBlobs = [];
3822
+ pendingStreamBytes = 0;
3823
+
3824
+ const finalizeUrl = chunkUrl(params.serverUrl, id, chunkIndex, true, {
3825
+ mimeType: finalMimeType,
3826
+ durationMs: String(durationMs),
3827
+ ...(width ? { width: String(width) } : {}),
3828
+ ...(height ? { height: String(height) } : {}),
3829
+ hasAudio: backupMeta.hasAudio ? "1" : "0",
3830
+ hasCamera: wantsCamera ? "1" : "0",
3831
+ });
3832
+ console.log("[clips-recorder] finalize POST", finalizeUrl, {
3833
+ chunksSent: chunkIndex,
3834
+ inflightAtFinalize: pending.length,
3835
+ finalBodyBytes: finalBody.size,
3836
+ uploadMode,
3837
+ anyFailed: !!failed,
3838
+ });
3839
+ try {
3840
+ const result = await finalizeAfterDurableBackup({
3841
+ // Opening the browser must not make the desktop look idle until every
3842
+ // trailing backup write and the final metadata are durable.
3843
+ ensureBackupDurable: async () => {
3844
+ await Promise.all([...backupWrites]);
3845
+ if (backupFailure) throw backupFailure;
3846
+ await persistBackupMeta({
3847
+ durationMs,
3848
+ width,
3849
+ height,
3850
+ bytes: backupBytes,
3851
+ hasAudio: uploadCombined.getAudioTracks().length > 0,
3852
+ hasCamera: wantsCamera,
3853
+ chunkCount: backupChunkCount,
3854
+ mimeType: finalMimeType,
3855
+ lastError: null,
3856
+ });
3857
+ },
3858
+ attemptFinalize: async () => {
3859
+ try {
3860
+ const finalRes = await fetch(finalizeUrl, {
3861
+ method: "POST",
3862
+ headers: { "Content-Type": "application/octet-stream" },
3863
+ credentials: "include",
3864
+ body: finalBody,
3865
+ signal: AbortSignal.timeout(FINALIZE_UPLOAD_TIMEOUT_MS),
3866
+ });
3867
+ const bodyText = await finalRes.text().catch(() => "");
3868
+ console.log(
3869
+ "[clips-recorder] finalize response:",
3870
+ finalRes.status,
3871
+ bodyText.slice(0, 500),
3872
+ );
3873
+ if (!finalRes.ok) {
3874
+ throw new Error(
3875
+ `Finalize failed (${finalRes.status}): ${bodyText.slice(0, 200)}`,
3876
+ );
3877
+ }
3878
+ const receipt = bodyText
3879
+ ? (JSON.parse(bodyText) as FinalizeReceipt)
3880
+ : null;
3881
+ verifyFinalizeReceipt(receipt, {
3882
+ bytes: backupBytes,
3883
+ durationMs,
3884
+ });
3885
+ } catch (err) {
3886
+ console.error("[clips-recorder] finalize fetch failed:", err);
3887
+ const error = err instanceof Error ? err : new Error(String(err));
3888
+ if (
3889
+ await recoverReadyRecordingAfterFinalizeError({
3890
+ serverUrl: params.serverUrl,
3891
+ recordingId: id,
3892
+ authToken: params.authToken,
3893
+ })
3894
+ ) {
3895
+ return { recordingId: id, viewUrl };
3896
+ }
3897
+ await markBrowserRecordingBackupError(id, error.message).catch(
3898
+ () => {},
3899
+ );
3900
+ await abortRecordingUpload(params.serverUrl, id, error.message);
3901
+ throw error;
3902
+ }
3903
+ await deleteBrowserRecordingBackup(id).catch((err) => {
3904
+ console.warn("[clips-recorder] local backup cleanup failed:", err);
3905
+ });
3906
+
3907
+ return { recordingId: id, viewUrl };
3908
+ },
3909
+ releaseGuard: clearRecordingState,
3910
+ });
3911
+ await publishFinalizingResult({
3912
+ recordingId: id,
3913
+ viewUrl: absoluteViewUrl,
3914
+ ok: true,
3915
+ });
3916
+ return result;
3917
+ } catch (err) {
3918
+ await publishFinalizingResult({
3919
+ recordingId: id,
3920
+ viewUrl: absoluteViewUrl,
3921
+ ok: false,
3922
+ error: err instanceof Error ? err.message : String(err),
3923
+ });
3924
+ throw err;
3925
+ }
3926
+ };
3927
+
3928
+ const handle: RecorderHandle = {
3929
+ stop: singleFlight(performStop),
3880
3930
 
3881
3931
  async cancel() {
3882
3932
  if (stopped) return;