@agent-native/core 0.69.0 → 0.70.1

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 (336) hide show
  1. package/README.md +15 -15
  2. package/corpus/README.md +2 -2
  3. package/corpus/core/CHANGELOG.md +109 -0
  4. package/corpus/core/docs/design/durable-agent-runs.md +217 -0
  5. package/corpus/core/package.json +1 -1
  6. package/corpus/core/src/action.ts +89 -0
  7. package/corpus/core/src/agent/model-config.ts +0 -2
  8. package/corpus/core/src/agent/run-loop-with-resume.ts +48 -0
  9. package/corpus/core/src/changelog/parse.ts +245 -0
  10. package/corpus/core/src/cli/changelog.ts +199 -0
  11. package/corpus/core/src/cli/index.ts +19 -0
  12. package/corpus/core/src/cli/recap.ts +24 -1
  13. package/corpus/core/src/cli/skills.ts +74 -47
  14. package/corpus/core/src/client/AgentPanel.tsx +5 -3
  15. package/corpus/core/src/client/AssistantChat.tsx +8 -0
  16. package/corpus/core/src/client/CommandMenu.tsx +183 -75
  17. package/corpus/core/src/client/agent-chat-adapter.ts +44 -7
  18. package/corpus/core/src/client/analytics.ts +181 -0
  19. package/corpus/core/src/client/changelog/Changelog.tsx +321 -0
  20. package/corpus/core/src/client/client-surface.ts +41 -0
  21. package/corpus/core/src/client/composer/TiptapComposer.tsx +123 -12
  22. package/corpus/core/src/client/feedback-context.ts +4 -0
  23. package/corpus/core/src/client/index.ts +12 -0
  24. package/corpus/core/src/client/mcp-apps/McpAppRenderer.tsx +54 -0
  25. package/corpus/core/src/client/settings/useBuilderStatus.ts +82 -20
  26. package/corpus/core/src/client/sse-event-processor.ts +7 -1
  27. package/corpus/core/src/mcp/build-server.ts +57 -1
  28. package/corpus/core/src/mcp/embed-app.ts +197 -7
  29. package/corpus/core/src/mcp/oauth-route.ts +109 -13
  30. package/corpus/core/src/server/attribution.ts +211 -0
  31. package/corpus/core/src/server/auth.ts +6 -0
  32. package/corpus/core/src/server/better-auth-instance.ts +49 -5
  33. package/corpus/core/src/server/builder-browser.ts +27 -1
  34. package/corpus/core/src/server/core-routes-plugin.ts +136 -0
  35. package/corpus/core/src/server/google-oauth.ts +19 -6
  36. package/corpus/core/src/server/onboarding-html.ts +35 -0
  37. package/corpus/core/src/shared/mcp-embed-headers.ts +1 -1
  38. package/corpus/core/src/styles/agent-conversation.css +21 -0
  39. package/corpus/core/src/templates/workspace-core/.agents/skills/changelog/SKILL.md +111 -0
  40. package/corpus/core/src/templates/workspace-core/.agents/skills/reliable-mutations/SKILL.md +72 -0
  41. package/corpus/core/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +46 -14
  42. package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +22 -0
  43. package/corpus/templates/analytics/AGENTS.md +8 -1
  44. package/corpus/templates/analytics/CHANGELOG.md +10 -0
  45. package/corpus/templates/analytics/actions/install-dashboard-template.ts +99 -4
  46. package/corpus/templates/analytics/actions/open-traffic-dashboard.ts +1 -1
  47. package/corpus/templates/analytics/actions/update-dashboard.ts +39 -8
  48. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +30 -3
  49. package/corpus/templates/analytics/app/components/layout/CommandPalette.tsx +180 -144
  50. package/corpus/templates/analytics/app/components/layout/Header.tsx +2 -3
  51. package/corpus/templates/analytics/app/components/layout/Layout.tsx +7 -5
  52. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +221 -215
  53. package/corpus/templates/analytics/app/hooks/use-navigation-state.ts +6 -6
  54. package/corpus/templates/analytics/app/lib/demo-dashboard-path.ts +1 -1
  55. package/corpus/templates/analytics/app/lib/last-opened.ts +2 -1
  56. package/corpus/templates/analytics/app/pages/Settings.tsx +4 -0
  57. package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/index.tsx +3 -3
  58. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/DashboardFilterBar.tsx +2 -2
  59. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +32 -2
  60. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +101 -99
  61. package/corpus/templates/analytics/app/pages/overview/OverviewPage.tsx +1 -1
  62. package/corpus/templates/analytics/app/routes/adhoc.$id.tsx +18 -5
  63. package/corpus/templates/analytics/app/routes/catalog.tsx +1 -1
  64. package/corpus/templates/analytics/app/routes/dashboards.$id.tsx +9 -0
  65. package/corpus/templates/analytics/app/routes/traffic.tsx +1 -1
  66. package/corpus/templates/analytics/changelog/2026-06-23-long-dashboard-and-analysis-names-in-the-sidebar-now-truncat.md +6 -0
  67. package/corpus/templates/analytics/changelog/2026-06-23-open-any-dashboard-chart-full-screen-in-a-modal-from-the-pan.md +6 -0
  68. package/corpus/templates/analytics/changelog/2026-06-23-the-dashboards-and-analyses-sidebar-settings-buttons-now-sta.md +6 -0
  69. package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +145 -0
  70. package/corpus/templates/analytics/server/db/index.ts +1 -1
  71. package/corpus/templates/analytics/server/lib/dashboard-catalog.ts +9 -2
  72. package/corpus/templates/analytics/server/lib/demo-dashboards.ts +1 -1
  73. package/corpus/templates/analytics/server/plugins/agent-chat.ts +2 -2
  74. package/corpus/templates/analytics/server/plugins/core-routes.ts +6 -6
  75. package/corpus/templates/assets/CHANGELOG.md +10 -0
  76. package/corpus/templates/assets/actions/generate-image.ts +24 -0
  77. package/corpus/templates/assets/app/components/layout/Sidebar.tsx +2 -2
  78. package/corpus/templates/assets/app/global.css +17 -1
  79. package/corpus/templates/assets/app/root.tsx +7 -1
  80. package/corpus/templates/assets/app/routes/_index.tsx +94 -14
  81. package/corpus/templates/assets/app/routes/settings.tsx +6 -0
  82. package/corpus/templates/assets/changelog/2026-06-23-chat-dates-in-the-sidebar-no-longer-wrap-onto-two-lines.md +6 -0
  83. package/corpus/templates/assets/changelog/2026-06-23-clicking-image-video-or-refine-on-the-start-screen-now-drops.md +6 -0
  84. package/corpus/templates/assets/changelog/2026-06-23-pick-your-image-generation-model-right-from-the-chat-model-m.md +6 -0
  85. package/corpus/templates/brain/CHANGELOG.md +10 -0
  86. package/corpus/templates/brain/app/root.tsx +7 -1
  87. package/corpus/templates/brain/app/routes/settings.tsx +8 -1
  88. package/corpus/templates/calendar/CHANGELOG.md +10 -0
  89. package/corpus/templates/calendar/app/pages/Settings.tsx +4 -0
  90. package/corpus/templates/calendar/app/root.tsx +7 -1
  91. package/corpus/templates/chat/CHANGELOG.md +10 -0
  92. package/corpus/templates/chat/app/root.tsx +7 -1
  93. package/corpus/templates/clips/CHANGELOG.md +10 -0
  94. package/corpus/templates/clips/app/components/library/library-grid.tsx +1 -20
  95. package/corpus/templates/clips/app/components/player/share-dialog.tsx +33 -7
  96. package/corpus/templates/clips/app/components/player/sign-in-prompt-dialog.tsx +8 -0
  97. package/corpus/templates/clips/app/components/player/video-player.tsx +150 -20
  98. package/corpus/templates/clips/app/components/recorder/countdown-overlay.tsx +72 -48
  99. package/corpus/templates/clips/app/components/recorder/pre-record-panel.tsx +7 -11
  100. package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +31 -0
  101. package/corpus/templates/clips/app/components/sharing/slack-share-hint.tsx +71 -0
  102. package/corpus/templates/clips/app/lib/countdown-audio-cue.ts +35 -17
  103. package/corpus/templates/clips/app/root.tsx +7 -1
  104. package/corpus/templates/clips/app/routes/_app.settings._index.tsx +15 -1
  105. package/corpus/templates/clips/app/routes/record.tsx +78 -5
  106. package/corpus/templates/clips/app/routes/share.$shareId.tsx +86 -5
  107. package/corpus/templates/clips/changelog/2026-06-23-clips-shared-in-slack-no-longer-show-a-could-not-start-playb.md +6 -0
  108. package/corpus/templates/clips/changelog/2026-06-23-made-the-play-button-on-shared-and-embedded-clips-scale-with.md +6 -0
  109. package/corpus/templates/clips/changelog/2026-06-23-public-clip-share-links-now-show-whether-they-ll-play-inline.md +6 -0
  110. package/corpus/templates/clips/changelog/2026-06-23-removed-select-button-from-library-hover-a-clip-to-select-it.md +6 -0
  111. package/corpus/templates/clips/changelog/2026-06-23-screen-camera-recordings-now-keep-the-presenter-s-face-in-th.md +6 -0
  112. package/corpus/templates/clips/changelog/2026-06-23-shared-video-links-now-play-for-anyone-without-signing-in.md +6 -0
  113. package/corpus/templates/clips/changelog/2026-06-23-skip-or-cancel-the-recording-countdown-with-buttons-beside-t.md +6 -0
  114. package/corpus/templates/clips/changelog/2026-06-23-the-desktop-camera-bubble-now-looks-sharp-the-instant-it-ope.md +6 -0
  115. package/corpus/templates/clips/changelog/2026-06-23-the-desktop-recording-widget-now-grows-downward-only-when-yo.md +6 -0
  116. package/corpus/templates/clips/changelog/2026-06-23-the-recording-start-sound-is-now-a-softer-more-modern-chime.md +6 -0
  117. package/corpus/templates/clips/chrome-extension/public/manifest.json +17 -8
  118. package/corpus/templates/clips/chrome-extension/src/background.ts +769 -180
  119. package/corpus/templates/clips/chrome-extension/src/content-script.ts +177 -0
  120. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +466 -329
  121. package/corpus/templates/clips/chrome-extension/src/overlay.css +377 -0
  122. package/corpus/templates/clips/chrome-extension/src/overlay.html +12 -0
  123. package/corpus/templates/clips/chrome-extension/src/overlay.ts +330 -0
  124. package/corpus/templates/clips/chrome-extension/src/permission.html +227 -0
  125. package/corpus/templates/clips/chrome-extension/src/permission.ts +105 -0
  126. package/corpus/templates/clips/chrome-extension/src/popup.html +3 -0
  127. package/corpus/templates/clips/chrome-extension/src/popup.ts +50 -2
  128. package/corpus/templates/clips/chrome-extension/src/styles.css +50 -0
  129. package/corpus/templates/clips/chrome-extension/vite.config.ts +3 -0
  130. package/corpus/templates/clips/desktop/src/lib/audio-cue.ts +41 -24
  131. package/corpus/templates/clips/desktop/src/lib/bubble-webrtc.ts +119 -2
  132. package/corpus/templates/clips/desktop/src/lib/recorder.ts +20 -0
  133. package/corpus/templates/clips/desktop/src/main.tsx +3 -0
  134. package/corpus/templates/clips/desktop/src/overlays/countdown.tsx +21 -2
  135. package/corpus/templates/clips/desktop/src/overlays/region-record-border.tsx +76 -0
  136. package/corpus/templates/clips/desktop/src/styles.css +119 -2
  137. package/corpus/templates/clips/desktop/src-tauri/capabilities/default.json +2 -1
  138. package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +140 -1
  139. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +2 -0
  140. package/corpus/templates/clips/desktop/src-tauri/src/util.rs +4 -1
  141. package/corpus/templates/clips/server/routes/api/video/[recordingId].get.ts +85 -6
  142. package/corpus/templates/clips/shared/share-attribution.ts +79 -0
  143. package/corpus/templates/content/AGENTS.md +20 -3
  144. package/corpus/templates/content/CHANGELOG.md +10 -0
  145. package/corpus/templates/content/actions/_database-utils.ts +15 -0
  146. package/corpus/templates/content/actions/_property-utils.ts +371 -4
  147. package/corpus/templates/content/actions/configure-document-property.ts +35 -1
  148. package/corpus/templates/content/actions/create-content-database.ts +5 -1
  149. package/corpus/templates/content/actions/delete-document-property.ts +52 -2
  150. package/corpus/templates/content/actions/duplicate-document-property.ts +34 -17
  151. package/corpus/templates/content/actions/reorder-document-property.ts +79 -0
  152. package/corpus/templates/content/actions/set-document-property.ts +39 -0
  153. package/corpus/templates/content/app/components/editor/DocumentBlockFields.tsx +806 -0
  154. package/corpus/templates/content/app/components/editor/DocumentDatabase.tsx +226 -68
  155. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +59 -30
  156. package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +25 -1
  157. package/corpus/templates/content/app/components/editor/blockFieldSaveController.ts +180 -0
  158. package/corpus/templates/content/app/components/editor/blockFieldSaveRegistry.ts +179 -0
  159. package/corpus/templates/content/app/components/editor/previewDocumentSaveController.ts +244 -0
  160. package/corpus/templates/content/app/components/editor/previewDocumentSaveRegistry.ts +132 -0
  161. package/corpus/templates/content/app/global.css +9 -0
  162. package/corpus/templates/content/app/hooks/use-document-properties.ts +21 -0
  163. package/corpus/templates/content/app/root.tsx +7 -1
  164. package/corpus/templates/content/server/db/schema.ts +30 -0
  165. package/corpus/templates/content/server/plugins/db.ts +87 -0
  166. package/corpus/templates/content/shared/api.ts +7 -0
  167. package/corpus/templates/content/shared/properties.ts +109 -0
  168. package/corpus/templates/design/CHANGELOG.md +10 -0
  169. package/corpus/templates/design/actions/generate-design.ts +4 -0
  170. package/corpus/templates/design/app/root.tsx +7 -1
  171. package/corpus/templates/design/app/routes/settings.tsx +10 -2
  172. package/corpus/templates/dispatch/CHANGELOG.md +10 -0
  173. package/corpus/templates/dispatch/app/root.tsx +7 -1
  174. package/corpus/templates/forms/.agents/skills/form-responses/SKILL.md +10 -0
  175. package/corpus/templates/forms/CHANGELOG.md +10 -0
  176. package/corpus/templates/forms/actions/export-responses.ts +6 -0
  177. package/corpus/templates/forms/actions/list-responses.ts +2 -0
  178. package/corpus/templates/forms/actions/response-insights.ts +2 -0
  179. package/corpus/templates/forms/app/pages/ResponsesPage.tsx +138 -1
  180. package/corpus/templates/forms/app/root.tsx +7 -1
  181. package/corpus/templates/forms/changelog/2026-06-23-response-tables-now-show-the-page-each-submission-came-from-.md +6 -0
  182. package/corpus/templates/forms/changelog/2026-06-23-response-tables-now-show-whether-feedback-came-from-the-web-.md +6 -0
  183. package/corpus/templates/forms/server/db/schema.ts +7 -0
  184. package/corpus/templates/forms/server/handlers/submissions.ts +19 -2
  185. package/corpus/templates/forms/server/lib/integrations.ts +76 -2
  186. package/corpus/templates/forms/server/plugins/db.ts +20 -0
  187. package/corpus/templates/forms/shared/types.ts +12 -0
  188. package/corpus/templates/macros/CHANGELOG.md +10 -0
  189. package/corpus/templates/macros/app/root.tsx +7 -1
  190. package/corpus/templates/mail/CHANGELOG.md +10 -0
  191. package/corpus/templates/mail/app/components/layout/CommandPalette.tsx +3 -0
  192. package/corpus/templates/mail/app/pages/SettingsPage.tsx +27 -0
  193. package/corpus/templates/plan/.agents/skills/visual-plan/SKILL.md +31 -17
  194. package/corpus/templates/plan/.agents/skills/visual-plan/references/wireframe.md +7 -4
  195. package/corpus/templates/plan/.agents/skills/visual-recap/SKILL.md +34 -24
  196. package/corpus/templates/plan/.agents/skills/visual-recap/references/wireframe.md +7 -4
  197. package/corpus/templates/plan/AGENTS.md +4 -0
  198. package/corpus/templates/plan/CHANGELOG.md +10 -0
  199. package/corpus/templates/plan/actions/view-screen.ts +5 -0
  200. package/corpus/templates/plan/app/components/layout/Sidebar.tsx +98 -1
  201. package/corpus/templates/plan/app/components/plan/wireframe/html-artboard.css +3 -1
  202. package/corpus/templates/plan/app/pages/PlansPage.tsx +107 -7
  203. package/corpus/templates/plan/app/root.tsx +7 -1
  204. package/corpus/templates/plan/changelog/2026-06-23-large-diagrams-in-a-plan-now-scroll-within-their-block-inste.md +6 -0
  205. package/corpus/templates/plan/shared/share-attribution.ts +72 -0
  206. package/corpus/templates/slides/CHANGELOG.md +10 -0
  207. package/corpus/templates/slides/app/root.tsx +7 -1
  208. package/corpus/templates/videos/CHANGELOG.md +4 -151
  209. package/corpus/templates/videos/app/root.tsx +7 -1
  210. package/dist/action.js +87 -0
  211. package/dist/action.js.map +1 -1
  212. package/dist/agent/engine/builder-engine.d.ts +1 -1
  213. package/dist/agent/engine/builder-engine.d.ts.map +1 -1
  214. package/dist/agent/model-config.d.ts +2 -2
  215. package/dist/agent/model-config.d.ts.map +1 -1
  216. package/dist/agent/model-config.js +0 -2
  217. package/dist/agent/model-config.js.map +1 -1
  218. package/dist/agent/run-loop-with-resume.d.ts +13 -0
  219. package/dist/agent/run-loop-with-resume.d.ts.map +1 -1
  220. package/dist/agent/run-loop-with-resume.js +44 -0
  221. package/dist/agent/run-loop-with-resume.js.map +1 -1
  222. package/dist/changelog/parse.d.ts +70 -0
  223. package/dist/changelog/parse.d.ts.map +1 -0
  224. package/dist/changelog/parse.js +200 -0
  225. package/dist/changelog/parse.js.map +1 -0
  226. package/dist/cli/changelog.d.ts +4 -0
  227. package/dist/cli/changelog.d.ts.map +1 -0
  228. package/dist/cli/changelog.js +166 -0
  229. package/dist/cli/changelog.js.map +1 -0
  230. package/dist/cli/index.js +18 -0
  231. package/dist/cli/index.js.map +1 -1
  232. package/dist/cli/recap.d.ts.map +1 -1
  233. package/dist/cli/recap.js +22 -1
  234. package/dist/cli/recap.js.map +1 -1
  235. package/dist/cli/skills.d.ts +3 -3
  236. package/dist/cli/skills.d.ts.map +1 -1
  237. package/dist/cli/skills.js +74 -47
  238. package/dist/cli/skills.js.map +1 -1
  239. package/dist/client/AgentPanel.d.ts.map +1 -1
  240. package/dist/client/AgentPanel.js +5 -3
  241. package/dist/client/AgentPanel.js.map +1 -1
  242. package/dist/client/AssistantChat.d.ts +6 -1
  243. package/dist/client/AssistantChat.d.ts.map +1 -1
  244. package/dist/client/AssistantChat.js +2 -2
  245. package/dist/client/AssistantChat.js.map +1 -1
  246. package/dist/client/CommandMenu.d.ts +17 -1
  247. package/dist/client/CommandMenu.d.ts.map +1 -1
  248. package/dist/client/CommandMenu.js +49 -13
  249. package/dist/client/CommandMenu.js.map +1 -1
  250. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  251. package/dist/client/agent-chat-adapter.js +43 -6
  252. package/dist/client/agent-chat-adapter.js.map +1 -1
  253. package/dist/client/analytics.d.ts +18 -0
  254. package/dist/client/analytics.d.ts.map +1 -1
  255. package/dist/client/analytics.js +177 -0
  256. package/dist/client/analytics.js.map +1 -1
  257. package/dist/client/changelog/Changelog.d.ts +47 -0
  258. package/dist/client/changelog/Changelog.d.ts.map +1 -0
  259. package/dist/client/changelog/Changelog.js +138 -0
  260. package/dist/client/changelog/Changelog.js.map +1 -0
  261. package/dist/client/client-surface.d.ts +17 -0
  262. package/dist/client/client-surface.d.ts.map +1 -0
  263. package/dist/client/client-surface.js +24 -0
  264. package/dist/client/client-surface.js.map +1 -0
  265. package/dist/client/composer/TiptapComposer.d.ts +26 -1
  266. package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
  267. package/dist/client/composer/TiptapComposer.js +27 -5
  268. package/dist/client/composer/TiptapComposer.js.map +1 -1
  269. package/dist/client/feedback-context.d.ts +3 -0
  270. package/dist/client/feedback-context.d.ts.map +1 -1
  271. package/dist/client/feedback-context.js +2 -0
  272. package/dist/client/feedback-context.js.map +1 -1
  273. package/dist/client/index.d.ts +3 -1
  274. package/dist/client/index.d.ts.map +1 -1
  275. package/dist/client/index.js +3 -1
  276. package/dist/client/index.js.map +1 -1
  277. package/dist/client/mcp-apps/McpAppRenderer.d.ts +1 -0
  278. package/dist/client/mcp-apps/McpAppRenderer.d.ts.map +1 -1
  279. package/dist/client/mcp-apps/McpAppRenderer.js +42 -1
  280. package/dist/client/mcp-apps/McpAppRenderer.js.map +1 -1
  281. package/dist/client/settings/useBuilderStatus.d.ts +2 -0
  282. package/dist/client/settings/useBuilderStatus.d.ts.map +1 -1
  283. package/dist/client/settings/useBuilderStatus.js +58 -10
  284. package/dist/client/settings/useBuilderStatus.js.map +1 -1
  285. package/dist/client/sse-event-processor.d.ts.map +1 -1
  286. package/dist/client/sse-event-processor.js +7 -1
  287. package/dist/client/sse-event-processor.js.map +1 -1
  288. package/dist/mcp/build-server.d.ts +21 -0
  289. package/dist/mcp/build-server.d.ts.map +1 -1
  290. package/dist/mcp/build-server.js +43 -1
  291. package/dist/mcp/build-server.js.map +1 -1
  292. package/dist/mcp/embed-app.d.ts.map +1 -1
  293. package/dist/mcp/embed-app.js +197 -7
  294. package/dist/mcp/embed-app.js.map +1 -1
  295. package/dist/mcp/oauth-route.d.ts.map +1 -1
  296. package/dist/mcp/oauth-route.js +95 -12
  297. package/dist/mcp/oauth-route.js.map +1 -1
  298. package/dist/server/attribution.d.ts +83 -0
  299. package/dist/server/attribution.d.ts.map +1 -0
  300. package/dist/server/attribution.js +178 -0
  301. package/dist/server/attribution.js.map +1 -0
  302. package/dist/server/auth.d.ts.map +1 -1
  303. package/dist/server/auth.js +7 -0
  304. package/dist/server/auth.js.map +1 -1
  305. package/dist/server/better-auth-instance.d.ts +9 -1
  306. package/dist/server/better-auth-instance.d.ts.map +1 -1
  307. package/dist/server/better-auth-instance.js +32 -2
  308. package/dist/server/better-auth-instance.js.map +1 -1
  309. package/dist/server/builder-browser.d.ts +4 -0
  310. package/dist/server/builder-browser.d.ts.map +1 -1
  311. package/dist/server/builder-browser.js +20 -1
  312. package/dist/server/builder-browser.js.map +1 -1
  313. package/dist/server/core-routes-plugin.d.ts +25 -0
  314. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  315. package/dist/server/core-routes-plugin.js +101 -0
  316. package/dist/server/core-routes-plugin.js.map +1 -1
  317. package/dist/server/google-oauth.d.ts.map +1 -1
  318. package/dist/server/google-oauth.js +16 -2
  319. package/dist/server/google-oauth.js.map +1 -1
  320. package/dist/server/onboarding-html.d.ts.map +1 -1
  321. package/dist/server/onboarding-html.js +35 -0
  322. package/dist/server/onboarding-html.js.map +1 -1
  323. package/dist/shared/mcp-embed-headers.js +1 -1
  324. package/dist/shared/mcp-embed-headers.js.map +1 -1
  325. package/dist/styles/agent-conversation.css +21 -0
  326. package/dist/templates/workspace-core/.agents/skills/changelog/SKILL.md +111 -0
  327. package/dist/templates/workspace-core/.agents/skills/reliable-mutations/SKILL.md +72 -0
  328. package/dist/templates/workspace-core/.agents/skills/tracking/SKILL.md +46 -14
  329. package/docs/design/durable-agent-runs.md +217 -0
  330. package/package.json +1 -1
  331. package/src/templates/workspace-core/.agents/skills/changelog/SKILL.md +111 -0
  332. package/src/templates/workspace-core/.agents/skills/reliable-mutations/SKILL.md +72 -0
  333. package/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +46 -14
  334. package/corpus/templates/analytics/app/pages/About.tsx +0 -108
  335. package/corpus/templates/analytics/app/routes/about.tsx +0 -9
  336. /package/corpus/templates/analytics/app/routes/{dashboards.tsx → dashboards._index.tsx} +0 -0
@@ -115,6 +115,7 @@ type BrowserDiagnosticsData = {
115
115
 
116
116
  type NativeRecordingStatus =
117
117
  | "recording"
118
+ | "paused"
118
119
  | "stopping"
119
120
  | "uploading"
120
121
  | "complete"
@@ -124,6 +125,7 @@ type NativeRecording = {
124
125
  sessionId: string;
125
126
  recordingId: string;
126
127
  clipsBaseUrl: string;
128
+ uploadUrl: string;
127
129
  targetTabId: number;
128
130
  targetTitle: string | null;
129
131
  targetUrl: string | null;
@@ -196,6 +198,243 @@ const sessions = new Map<string, CaptureSession>();
196
198
  const tabToSession = new Map<number, string>();
197
199
  let activeNativeRecording: NativeRecording | null = null;
198
200
 
201
+ // ----- Loom-style in-page overlay state -------------------------------------
202
+ // The service worker is the single source of truth for the recording phase and
203
+ // the elapsed-time base. The phase decides which overlay "parts" each tab should
204
+ // show; the timer base lets every overlay iframe tick the same clock locally.
205
+
206
+ type CaptureMode = "screen" | "camera";
207
+ type OverlayPhase = "idle" | "countdown" | "recording" | "paused" | "saving";
208
+ type OverlayPart = "bubble" | "countdown" | "toolbar" | "saving";
209
+
210
+ let overlayPhase: OverlayPhase = "idle";
211
+ let overlayBaseElapsedMs = 0;
212
+ let overlayBaseEpochMs = 0;
213
+ let overlayShowsBubble = false;
214
+ let countdownEndsAtMs = 0;
215
+
216
+ function desiredParts(): OverlayPart[] {
217
+ // The on-page controls match the desktop app: a left-edge vertical pill plus
218
+ // the face bubble. (Both are captured in full-screen/window recordings — same
219
+ // tradeoff as the desktop's on-screen controls; that's the intended UX.)
220
+ if (overlayPhase === "countdown") {
221
+ return overlayShowsBubble ? ["bubble", "countdown"] : ["countdown"];
222
+ }
223
+ if (overlayPhase === "recording" || overlayPhase === "paused") {
224
+ return overlayShowsBubble ? ["bubble", "toolbar"] : ["toolbar"];
225
+ }
226
+ if (overlayPhase === "saving") {
227
+ return ["saving"];
228
+ }
229
+ return [];
230
+ }
231
+
232
+ function overlayStateForBroadcast(): {
233
+ phase: OverlayPhase;
234
+ baseElapsedMs: number;
235
+ baseEpochMs: number;
236
+ countdownEndsAtMs: number;
237
+ } {
238
+ return {
239
+ phase: overlayPhase,
240
+ baseElapsedMs: overlayBaseElapsedMs,
241
+ baseEpochMs: overlayBaseEpochMs,
242
+ countdownEndsAtMs,
243
+ };
244
+ }
245
+
246
+ // The countdown clock lives here in the worker, not in the overlay. The overlay
247
+ // only *visualizes* it. This is critical: on chrome:// pages (and any page where
248
+ // the overlay can't be injected) the recorder must still start. Without this,
249
+ // recording would silently never begin on those pages.
250
+ const COUNTDOWN_SECONDS = 3;
251
+
252
+ function clearCountdownTimer(): void {
253
+ countdownEndsAtMs = 0;
254
+ }
255
+
256
+ // Toggle the toolbar popup off while recording so clicking the extension icon
257
+ // fires onClicked (immediate stop & save) instead of opening the popup.
258
+ function setActionPopup(path: string): void {
259
+ try {
260
+ void chrome.action.setPopup({ popup: path });
261
+ } catch {
262
+ /* action API unavailable */
263
+ }
264
+ }
265
+
266
+ // Reaches every extension-origin overlay iframe across all tabs at once.
267
+ function broadcastOverlayState(): void {
268
+ void persistOverlay();
269
+ try {
270
+ chrome.runtime.sendMessage(
271
+ { type: "CLIPS_OVERLAY_STATE", state: overlayStateForBroadcast() },
272
+ () => void chrome.runtime.lastError,
273
+ );
274
+ } catch {
275
+ /* no overlay listening yet */
276
+ }
277
+ }
278
+
279
+ // MV3 can suspend the service worker mid-recording, wiping these module vars.
280
+ // Persist them to session storage (survives suspend/revive within the browser
281
+ // session) and restore on worker startup so the overlay controls keep working.
282
+ function persistOverlay(): Promise<void> {
283
+ return sessionStorageSet({
284
+ overlayRuntime: {
285
+ phase: overlayPhase,
286
+ baseElapsedMs: overlayBaseElapsedMs,
287
+ baseEpochMs: overlayBaseEpochMs,
288
+ showsBubble: overlayShowsBubble,
289
+ countdownEndsAtMs,
290
+ },
291
+ }).catch(() => undefined);
292
+ }
293
+
294
+ async function restoreRuntimeState(): Promise<void> {
295
+ const stored = await sessionStorageGet([
296
+ "activeNativeRecording",
297
+ "overlayRuntime",
298
+ ]);
299
+ const rec = stored.activeNativeRecording as NativeRecording | undefined;
300
+ if (rec && typeof rec.sessionId === "string" && !activeNativeRecording) {
301
+ activeNativeRecording = rec;
302
+ }
303
+ const rt = stored.overlayRuntime as
304
+ | {
305
+ phase?: OverlayPhase;
306
+ baseElapsedMs?: number;
307
+ baseEpochMs?: number;
308
+ showsBubble?: boolean;
309
+ countdownEndsAtMs?: number;
310
+ }
311
+ | undefined;
312
+ if (rt && typeof rt.phase === "string" && overlayPhase === "idle") {
313
+ overlayPhase = rt.phase;
314
+ overlayBaseElapsedMs =
315
+ typeof rt.baseElapsedMs === "number" ? rt.baseElapsedMs : 0;
316
+ overlayBaseEpochMs =
317
+ typeof rt.baseEpochMs === "number" ? rt.baseEpochMs : 0;
318
+ overlayShowsBubble = Boolean(rt.showsBubble);
319
+ countdownEndsAtMs =
320
+ typeof rt.countdownEndsAtMs === "number" ? rt.countdownEndsAtMs : 0;
321
+ }
322
+
323
+ if (overlayPhase !== "idle" && activeNativeRecording) {
324
+ // Recording survived a worker restart. The offscreen document owns the
325
+ // recorder + pre-roll timer, so it kept running; just restore immediate-stop
326
+ // mode. If the pre-roll already elapsed, assume the offscreen started the
327
+ // recorder so the icon click does Stop (not Discard).
328
+ setActionPopup("");
329
+ if (
330
+ overlayPhase === "countdown" &&
331
+ countdownEndsAtMs > 0 &&
332
+ nowMs() >= countdownEndsAtMs
333
+ ) {
334
+ overlayPhase = "recording";
335
+ overlayBaseEpochMs = countdownEndsAtMs;
336
+ countdownEndsAtMs = 0;
337
+ }
338
+ }
339
+ }
340
+
341
+ function sendTabMessage(
342
+ tabId: number,
343
+ message: Record<string, unknown>,
344
+ ): Promise<void> {
345
+ return new Promise((resolve) => {
346
+ try {
347
+ chrome.tabs.sendMessage(tabId, message, () => {
348
+ void chrome.runtime.lastError;
349
+ resolve();
350
+ });
351
+ } catch {
352
+ resolve();
353
+ }
354
+ });
355
+ }
356
+
357
+ async function ensureContentScript(tabId: number): Promise<void> {
358
+ const scripting = (
359
+ chrome as typeof chrome & {
360
+ scripting?: {
361
+ executeScript: (args: {
362
+ target: { tabId: number };
363
+ files: string[];
364
+ }) => Promise<unknown>;
365
+ };
366
+ }
367
+ ).scripting;
368
+ if (!scripting) return;
369
+ await scripting
370
+ .executeScript({ target: { tabId }, files: ["assets/content-script.js"] })
371
+ .catch(() => undefined);
372
+ }
373
+
374
+ async function mountOverlayOnTab(tabId: number): Promise<void> {
375
+ await ensureContentScript(tabId);
376
+ await sendTabMessage(tabId, {
377
+ type: "CLIPS_OVERLAY_MOUNT",
378
+ parts: desiredParts(),
379
+ });
380
+ }
381
+
382
+ function allTabs(): Promise<chrome.tabs.Tab[]> {
383
+ return new Promise((resolve) => {
384
+ chrome.tabs.query({}, (tabs) => resolve(tabs ?? []));
385
+ });
386
+ }
387
+
388
+ async function broadcastMount(): Promise<void> {
389
+ const parts = desiredParts();
390
+ const tabs = await allTabs();
391
+ await Promise.all(
392
+ tabs.map((tab) =>
393
+ typeof tab.id === "number"
394
+ ? sendTabMessage(tab.id, { type: "CLIPS_OVERLAY_MOUNT", parts })
395
+ : Promise.resolve(),
396
+ ),
397
+ );
398
+ }
399
+
400
+ async function broadcastUnmount(): Promise<void> {
401
+ const tabs = await allTabs();
402
+ await Promise.all(
403
+ tabs.map((tab) =>
404
+ typeof tab.id === "number"
405
+ ? sendTabMessage(tab.id, { type: "CLIPS_OVERLAY_UNMOUNT" })
406
+ : Promise.resolve(),
407
+ ),
408
+ );
409
+ }
410
+
411
+ function resetOverlay(): void {
412
+ overlayPhase = "idle";
413
+ overlayBaseElapsedMs = 0;
414
+ overlayBaseEpochMs = 0;
415
+ overlayShowsBubble = false;
416
+ clearCountdownTimer();
417
+ setRecordingFlag(false);
418
+ // Bring back the toolbar popup now that we're idle again.
419
+ setActionPopup("src/popup.html");
420
+ void persistOverlay();
421
+ }
422
+
423
+ // Mirrored into chrome.storage.local so content scripts can cheaply tell whether
424
+ // a recording is in progress without waking the service worker on every page
425
+ // load. Content scripts can read storage.local by default; storage.session
426
+ // cannot, which is why we use local here.
427
+ function setRecordingFlag(active: boolean): void {
428
+ try {
429
+ chrome.storage.local.set(
430
+ { clipsRecordingActive: active },
431
+ () => void chrome.runtime.lastError,
432
+ );
433
+ } catch {
434
+ /* storage unavailable */
435
+ }
436
+ }
437
+
199
438
  function nowIso(): string {
200
439
  return new Date().toISOString();
201
440
  }
@@ -356,14 +595,32 @@ function originOf(raw: string | null | undefined): string | null {
356
595
  }
357
596
  }
358
597
 
598
+ // Auth lives in chrome.storage.local (not .session) so the user stays signed in
599
+ // across extension reloads and browser restarts. It is the user's own session
600
+ // token — treated like a cookie — and is cleared on sign-out or when invalid.
601
+ function localStorageSet(value: Record<string, unknown>): Promise<void> {
602
+ return new Promise((resolve) => {
603
+ chrome.storage.local.set(value, () => {
604
+ void chrome.runtime.lastError;
605
+ resolve();
606
+ });
607
+ });
608
+ }
609
+
610
+ function localStorageGet(keys: string[]): Promise<Record<string, unknown>> {
611
+ return new Promise((resolve) => {
612
+ chrome.storage.local.get(keys, (value) => resolve(value ?? {}));
613
+ });
614
+ }
615
+
359
616
  async function saveAuthSession(auth: StoredAuth): Promise<void> {
360
- await sessionStorageSet({ clipsAuth: auth });
617
+ await localStorageSet({ clipsAuth: auth });
361
618
  }
362
619
 
363
620
  async function readAuthSession(
364
621
  settings: ExtensionSettings,
365
622
  ): Promise<StoredAuth | null> {
366
- const stored = await sessionStorageGet(["clipsAuth"]);
623
+ const stored = await localStorageGet(["clipsAuth"]);
367
624
  const auth = stored.clipsAuth as Partial<StoredAuth> | undefined;
368
625
  if (
369
626
  !auth ||
@@ -506,23 +763,6 @@ async function postAction<T>(
506
763
  return (data ?? {}) as T;
507
764
  }
508
765
 
509
- function getTabMediaStreamId(tabId: number): Promise<string> {
510
- return new Promise((resolve, reject) => {
511
- chrome.tabCapture.getMediaStreamId({ targetTabId: tabId }, (streamId) => {
512
- const error = chromeLastError();
513
- if (error) {
514
- reject(error);
515
- return;
516
- }
517
- if (!streamId) {
518
- reject(new Error("Chrome did not provide a tab capture stream."));
519
- return;
520
- }
521
- resolve(streamId);
522
- });
523
- });
524
- }
525
-
526
766
  async function ensureOffscreenDocument(): Promise<void> {
527
767
  const path = "src/offscreen.html";
528
768
  const offscreenUrl = chrome.runtime.getURL(path);
@@ -568,38 +808,6 @@ function recordingUrl(
568
808
  return `${recording.clipsBaseUrl}/r/${encodeURIComponent(recording.recordingId)}`;
569
809
  }
570
810
 
571
- function buildRecordUrl(
572
- settings: ExtensionSettings,
573
- sessionId: string,
574
- tab: ChromeTab,
575
- ): string {
576
- const recordUrl = new URL(`${settings.clipsBaseUrl}/record`);
577
- const mode =
578
- settings.captureSurface === "camera"
579
- ? "camera"
580
- : settings.includeCamera
581
- ? "screen+camera"
582
- : "screen";
583
- const surface =
584
- settings.captureSurface === "camera" ? "browser" : settings.captureSurface;
585
-
586
- recordUrl.searchParams.set("mode", mode);
587
- recordUrl.searchParams.set("surface", surface);
588
- recordUrl.searchParams.set("clipsExtensionId", chrome.runtime.id);
589
- recordUrl.searchParams.set("clipsCaptureSessionId", sessionId);
590
- recordUrl.searchParams.set(
591
- "developerLogs",
592
- settings.includeDeveloperLogs ? "1" : "0",
593
- );
594
- recordUrl.searchParams.set(
595
- "microphone",
596
- settings.includeMicrophone ? "1" : "0",
597
- );
598
- if (tab.title) recordUrl.searchParams.set("sourceTitle", tab.title);
599
- if (tab.url) recordUrl.searchParams.set("sourceUrl", tab.url);
600
- return recordUrl.toString();
601
- }
602
-
603
811
  function createSession(
604
812
  sessionId: string,
605
813
  tab: ChromeTab,
@@ -650,61 +858,80 @@ async function handlePopupStart(message: PopupStartMessage) {
650
858
  }
651
859
 
652
860
  const sessionId = crypto.randomUUID();
653
- if (
654
- settings.captureSurface === "browser" ||
655
- settings.captureSurface === "camera"
656
- ) {
657
- return startNativeRecording({ sessionId, tab, settings });
658
- }
659
-
660
- createSession(sessionId, tab, settings);
661
- await createTab(buildRecordUrl(settings, sessionId, tab));
662
-
663
- return { ok: true, sessionId };
861
+ return armRecording({ sessionId, tab, settings });
664
862
  }
665
863
 
666
- async function startNativeRecording(args: {
864
+ // Arm a Loom-style in-page recording: show the native picker, create the row,
865
+ // then hand the recorder its pre-roll delay. The MediaRecorder starts inside the
866
+ // offscreen document after that delay, which reports "recording" back here
867
+ // (markRecordingStarted) — reliable even when no overlay can be injected.
868
+ async function armRecording(args: {
667
869
  sessionId: string;
668
870
  tab: ChromeTab;
669
871
  settings: ExtensionSettings;
670
872
  }) {
671
873
  const { sessionId, tab, settings } = args;
672
- const streamId =
673
- settings.captureSurface === "browser"
674
- ? await getTabMediaStreamId(tab.id as number)
675
- : undefined;
874
+ const mode: CaptureMode =
875
+ settings.captureSurface === "camera" ? "camera" : "screen";
876
+ const surface: "browser" | "window" | "monitor" =
877
+ settings.captureSurface === "camera" ? "browser" : settings.captureSurface;
878
+
879
+ // Pre-warmed on popup open, so this is effectively instant and keeps the
880
+ // getDisplayMedia() call close to the user's click.
676
881
  await ensureOffscreenDocument();
677
882
 
883
+ // 1) Native "Choose what to share" picker. Do this before any network round
884
+ // trip so the picker stays tied to the user gesture. Throws if cancelled.
885
+ await sendOffscreenMessage<{ ok: boolean; width: number; height: number }>({
886
+ type: "CLIPS_OFFSCREEN_ACQUIRE",
887
+ sessionId,
888
+ mode,
889
+ surface,
890
+ includeMicrophone: settings.includeMicrophone,
891
+ });
892
+
893
+ // 2) Create the recording row now that we know the user picked a source.
678
894
  type CreatedRecording = {
679
895
  id?: string;
680
896
  uploadChunkUrl?: string;
681
897
  abortUrl?: string;
682
898
  };
683
- const created = await postAction<CreatedRecording>(
684
- settings,
685
- "create-recording",
686
- {
899
+ let created: CreatedRecording;
900
+ try {
901
+ created = await postAction<CreatedRecording>(settings, "create-recording", {
687
902
  title: tab.title || "Untitled recording",
688
903
  titleSource: tab.title ? "context" : "default",
689
904
  sourceAppName: "Chrome",
690
905
  sourceWindowTitle: tab.title ?? null,
691
906
  hasCamera: settings.captureSurface === "camera" || settings.includeCamera,
692
907
  hasAudio:
693
- settings.includeMicrophone || settings.captureSurface === "browser",
908
+ settings.includeMicrophone || settings.captureSurface !== "camera",
694
909
  visibility: "public",
695
- },
696
- );
910
+ });
911
+ } catch (err) {
912
+ await sendOffscreenMessage({
913
+ type: "CLIPS_OFFSCREEN_CANCEL",
914
+ sessionId,
915
+ }).catch(() => undefined);
916
+ throw err;
917
+ }
697
918
  if (!created.id || !created.uploadChunkUrl) {
919
+ await sendOffscreenMessage({
920
+ type: "CLIPS_OFFSCREEN_CANCEL",
921
+ sessionId,
922
+ }).catch(() => undefined);
698
923
  throw new Error("Clips did not create a recording target.");
699
924
  }
700
925
 
701
926
  const uploadUrl = `${settings.clipsBaseUrl}${created.uploadChunkUrl}`;
702
927
  const session = createSession(sessionId, tab, settings);
703
928
  session.recordingId = created.id;
929
+
704
930
  activeNativeRecording = {
705
931
  sessionId,
706
932
  recordingId: created.id,
707
933
  clipsBaseUrl: settings.clipsBaseUrl,
934
+ uploadUrl,
708
935
  targetTabId: tab.id as number,
709
936
  targetTitle: tab.title?.trim() || null,
710
937
  targetUrl: tab.url?.trim() || null,
@@ -718,101 +945,250 @@ async function startNativeRecording(args: {
718
945
  recordingUrl: `${settings.clipsBaseUrl}/r/${encodeURIComponent(created.id)}`,
719
946
  error: null,
720
947
  };
721
- await saveActiveNativeRecording();
722
948
 
949
+ // 3) Enter the countdown phase and show the overlay on the launch tab. The
950
+ // camera bubble shows whenever a camera is involved: as the picture-in-
951
+ // picture for screen+camera, or as a live self-preview for camera-only.
952
+ overlayPhase = "countdown";
953
+ overlayBaseElapsedMs = 0;
954
+ overlayBaseEpochMs = nowMs();
955
+ overlayShowsBubble = mode === "camera" || settings.includeCamera;
956
+ countdownEndsAtMs = nowMs() + COUNTDOWN_SECONDS * 1000;
957
+ setRecordingFlag(true);
958
+ // Clicking the icon now stops & saves immediately instead of opening the popup.
959
+ setActionPopup("");
960
+
961
+ // Hand the recorder its targets plus the pre-roll delay. The offscreen document
962
+ // (a reliable context, unlike the suspendable worker) owns the countdown timer
963
+ // and reports "recording" back when the recorder actually starts — that status
964
+ // flip (markRecordingStarted) advances our phase, so recording begins even on
965
+ // pages where no overlay can be injected (chrome://, the New Tab page, etc.).
966
+ const authToken = (await readAuthSession(settings))?.token;
723
967
  try {
724
- await sendOffscreenMessage<{
725
- ok: boolean;
726
- recordingId: string;
727
- width: number;
728
- height: number;
729
- hasAudio: boolean;
730
- hasCamera: boolean;
731
- }>({
732
- type: "CLIPS_OFFSCREEN_START",
968
+ await sendOffscreenMessage({
969
+ type: "CLIPS_OFFSCREEN_BEGIN",
733
970
  sessionId,
734
971
  recordingId: created.id,
735
972
  uploadUrl,
736
- streamId,
737
- captureSurface: settings.captureSurface,
738
- includeCamera: settings.includeCamera,
739
- includeMicrophone: settings.includeMicrophone,
740
- title: tab.title ?? null,
973
+ hasCamera: mode === "camera" || settings.includeCamera,
974
+ startDelayMs: COUNTDOWN_SECONDS * 1000,
975
+ authToken,
741
976
  });
742
- await attachSession(session);
743
- await chrome.action.setBadgeBackgroundColor({ color: "#e11d48" });
744
- await chrome.action.setBadgeText({ text: "REC" });
745
- return {
746
- ok: true,
747
- sessionId,
748
- recordingId: created.id,
749
- native: true,
750
- };
751
977
  } catch (err) {
752
- await deleteSession(sessionId);
753
- activeNativeRecording = null;
754
- await saveActiveNativeRecording();
755
- await chrome.action.setBadgeText({ text: "" });
756
- await postAction(settings, "trash-recording", { id: created.id }).catch(
757
- () => undefined,
758
- );
978
+ await cancelRecording();
759
979
  throw err;
760
980
  }
981
+ await saveActiveNativeRecording();
982
+
983
+ // Browser-tab diagnostics still attach to the launch tab only.
984
+ if (settings.captureSurface === "browser") {
985
+ await attachSession(session);
986
+ }
987
+
988
+ await mountOverlayOnTab(tab.id as number);
989
+ broadcastOverlayState();
990
+ await chrome.action.setBadgeBackgroundColor({ color: "#e11d48" });
991
+ await chrome.action.setBadgeText({ text: "REC" });
992
+
993
+ return { ok: true, sessionId, recordingId: created.id, native: true };
761
994
  }
762
995
 
763
- async function saveNativeDiagnostics(
764
- recording: NativeRecording,
765
- ): Promise<void> {
766
- if (!recording.includeDeveloperLogs) return;
767
- const session = sessions.get(recording.sessionId);
768
- if (!session) return;
769
- const diagnostics = snapshotSession(session);
770
- await postAction(
771
- {
996
+ // Called when the offscreen recorder reports it actually started (after its
997
+ // pre-roll countdown). Advances phase countdown -> recording and starts the
998
+ // on-page timer. Idempotent.
999
+ async function markRecordingStarted() {
1000
+ if (overlayPhase !== "countdown") return;
1001
+ clearCountdownTimer();
1002
+ overlayPhase = "recording";
1003
+ overlayBaseElapsedMs = 0;
1004
+ overlayBaseEpochMs = nowMs();
1005
+ if (activeNativeRecording) {
1006
+ activeNativeRecording.startedAtMs = overlayBaseEpochMs;
1007
+ activeNativeRecording.startedAt = new Date(
1008
+ overlayBaseEpochMs,
1009
+ ).toISOString();
1010
+ activeNativeRecording.status = "recording";
1011
+ await saveActiveNativeRecording();
1012
+ }
1013
+ await broadcastMount();
1014
+ broadcastOverlayState();
1015
+ }
1016
+
1017
+ // Skip the pre-roll: tell the offscreen recorder to start immediately. It will
1018
+ // report "recording", which flips our phase via markRecordingStarted.
1019
+ async function handleOverlaySkip() {
1020
+ if (overlayPhase !== "countdown" || !activeNativeRecording) {
1021
+ return { ok: true };
1022
+ }
1023
+ await sendOffscreenMessage({
1024
+ type: "CLIPS_OFFSCREEN_START_NOW",
1025
+ sessionId: activeNativeRecording.sessionId,
1026
+ }).catch(() => undefined);
1027
+ return { ok: true };
1028
+ }
1029
+
1030
+ function handleOverlayPause() {
1031
+ if (overlayPhase !== "recording") return { ok: true };
1032
+ overlayBaseElapsedMs += Math.max(0, nowMs() - overlayBaseEpochMs);
1033
+ overlayPhase = "paused";
1034
+ if (activeNativeRecording) {
1035
+ activeNativeRecording.status = "paused";
1036
+ void saveActiveNativeRecording();
1037
+ void sendOffscreenMessage({
1038
+ type: "CLIPS_OFFSCREEN_PAUSE",
1039
+ sessionId: activeNativeRecording.sessionId,
1040
+ }).catch(() => undefined);
1041
+ }
1042
+ broadcastOverlayState();
1043
+ return { ok: true };
1044
+ }
1045
+
1046
+ function handleOverlayResume() {
1047
+ if (overlayPhase !== "paused") return { ok: true };
1048
+ overlayBaseEpochMs = nowMs();
1049
+ overlayPhase = "recording";
1050
+ if (activeNativeRecording) {
1051
+ activeNativeRecording.status = "recording";
1052
+ void saveActiveNativeRecording();
1053
+ void sendOffscreenMessage({
1054
+ type: "CLIPS_OFFSCREEN_RESUME",
1055
+ sessionId: activeNativeRecording.sessionId,
1056
+ }).catch(() => undefined);
1057
+ }
1058
+ broadcastOverlayState();
1059
+ return { ok: true };
1060
+ }
1061
+
1062
+ // Restart: discard the in-progress capture but keep the same screen selection,
1063
+ // reset the server-side chunks, then replay the countdown.
1064
+ async function handleOverlayRestart() {
1065
+ const recording = activeNativeRecording;
1066
+ if (!recording) return { ok: true };
1067
+ try {
1068
+ await sendOffscreenMessage({
1069
+ type: "CLIPS_OFFSCREEN_RESTART",
1070
+ sessionId: recording.sessionId,
1071
+ });
1072
+ } catch (err) {
1073
+ return {
1074
+ ok: false,
1075
+ error: err instanceof Error ? err.message : "Could not restart.",
1076
+ };
1077
+ }
1078
+ // If the previous take's chunks could not be cleared, do NOT re-arm with the
1079
+ // same recordingId — finalize would otherwise assemble stale chunk keys from
1080
+ // the aborted take into the restarted recording. Surface the failure instead.
1081
+ const chunksReset = await resetRecordingChunks(recording);
1082
+ if (!chunksReset) {
1083
+ return {
1084
+ ok: false,
1085
+ error:
1086
+ "Could not clear the previous take before restarting. Stop and start a new recording.",
1087
+ };
1088
+ }
1089
+ overlayPhase = "countdown";
1090
+ overlayBaseElapsedMs = 0;
1091
+ overlayBaseEpochMs = nowMs();
1092
+ countdownEndsAtMs = nowMs() + COUNTDOWN_SECONDS * 1000;
1093
+ recording.status = "recording";
1094
+ const restartAuthToken = (
1095
+ await readAuthSession({
772
1096
  clipsBaseUrl: recording.clipsBaseUrl,
773
1097
  captureSurface: recording.captureSurface,
774
1098
  includeCamera: recording.includeCamera,
775
1099
  includeMicrophone: recording.includeMicrophone,
776
1100
  includeDeveloperLogs: recording.includeDeveloperLogs,
777
- },
778
- "save-browser-diagnostics",
779
- {
780
- recordingId: recording.recordingId,
1101
+ })
1102
+ )?.token;
1103
+ // Re-arm the recorder on the same (re-homed) source streams with a fresh
1104
+ // pre-roll. The offscreen reports "recording" when it restarts.
1105
+ try {
1106
+ await sendOffscreenMessage({
1107
+ type: "CLIPS_OFFSCREEN_BEGIN",
781
1108
  sessionId: recording.sessionId,
782
- source: "extension",
783
- phase: "recording",
784
- pageUrl: diagnostics.pageUrl,
785
- userAgent: diagnostics.userAgent,
786
- startedAt: diagnostics.startedAt,
787
- endedAt: diagnostics.endedAt,
788
- consoleLogs: diagnostics.consoleLogs,
789
- networkRequests: diagnostics.networkRequests,
790
- },
791
- ).catch((err) => {
792
- console.warn("[clips-extension] diagnostics save failed:", err);
793
- });
1109
+ recordingId: recording.recordingId,
1110
+ uploadUrl: recording.uploadUrl,
1111
+ hasCamera:
1112
+ recording.captureSurface === "camera" || recording.includeCamera,
1113
+ startDelayMs: COUNTDOWN_SECONDS * 1000,
1114
+ authToken: restartAuthToken,
1115
+ });
1116
+ } catch (err) {
1117
+ // Re-arming failed: tear the countdown overlay back down and report the
1118
+ // failure instead of leaving the user on a pre-roll with no recorder.
1119
+ recording.status = "error";
1120
+ recording.error =
1121
+ err instanceof Error ? err.message : "Could not restart the recorder.";
1122
+ resetOverlay();
1123
+ await saveActiveNativeRecording();
1124
+ await broadcastUnmount();
1125
+ broadcastOverlayState();
1126
+ return { ok: false, error: recording.error };
1127
+ }
1128
+ await saveActiveNativeRecording();
1129
+ await broadcastMount();
1130
+ broadcastOverlayState();
1131
+ return { ok: true };
794
1132
  }
795
1133
 
796
- async function clearNativeRecording(): Promise<void> {
797
- activeNativeRecording = null;
798
- await saveActiveNativeRecording();
799
- await chrome.action.setBadgeText({ text: "" });
1134
+ async function resetRecordingChunks(
1135
+ recording: NativeRecording,
1136
+ ): Promise<boolean> {
1137
+ const url = `${recording.clipsBaseUrl}/api/uploads/${encodeURIComponent(
1138
+ recording.recordingId,
1139
+ )}/reset-chunks`;
1140
+ const headers = await authHeaders({
1141
+ clipsBaseUrl: recording.clipsBaseUrl,
1142
+ captureSurface: recording.captureSurface,
1143
+ includeCamera: recording.includeCamera,
1144
+ includeMicrophone: recording.includeMicrophone,
1145
+ includeDeveloperLogs: recording.includeDeveloperLogs,
1146
+ });
1147
+ const response = await fetch(url, {
1148
+ method: "POST",
1149
+ headers,
1150
+ credentials: "include",
1151
+ cache: "no-store",
1152
+ }).catch(() => undefined);
1153
+ return Boolean(response?.ok);
800
1154
  }
801
1155
 
802
- async function handlePopupStatus() {
803
- return {
804
- ok: true,
805
- activeRecording: activeNativeRecording,
806
- };
1156
+ // Finalize a successful save: open the clip and tear down the "Saving…" overlay.
1157
+ // Guarded + claims the phase synchronously so it runs exactly once even if both
1158
+ // the STOP response and the offscreen "complete" status race to call it (the
1159
+ // latter is the safety net when the worker was suspended mid-upload).
1160
+ async function finishSaving(
1161
+ recording: NativeRecording,
1162
+ recordingIdFromStatus?: string,
1163
+ ): Promise<boolean> {
1164
+ if (overlayPhase !== "saving") return false;
1165
+ resetOverlay(); // first statement sets overlayPhase = "idle" synchronously
1166
+ recording.status = "complete";
1167
+ recording.error = null;
1168
+ if (recordingIdFromStatus) recording.recordingId = recordingIdFromStatus;
1169
+ recording.recordingUrl = recordingUrl(recording);
1170
+ await saveNativeDiagnostics(recording);
1171
+ await deleteSession(recording.sessionId);
1172
+ await broadcastUnmount();
1173
+ broadcastOverlayState();
1174
+ await clearNativeRecording();
1175
+ await createTab(recording.recordingUrl);
1176
+ return true;
807
1177
  }
808
1178
 
809
- async function handlePopupStop() {
1179
+ async function stopRecording() {
810
1180
  const recording = activeNativeRecording;
811
- if (!recording) {
812
- return { ok: false, error: "No active Clips recording." };
813
- }
1181
+ if (!recording) return { ok: false, error: "No active Clips recording." };
814
1182
  recording.status = "stopping";
1183
+ // Keep an overlay up: swap the recording controls/bubble for a "Saving…" card
1184
+ // so the user has feedback during the upload gap (instead of everything
1185
+ // vanishing while the clip uploads invisibly). Mirrors the desktop Finalizing
1186
+ // overlay. The popup stays disabled so the icon can't interrupt the save.
1187
+ overlayPhase = "saving";
1188
+ countdownEndsAtMs = 0;
815
1189
  await saveActiveNativeRecording();
1190
+ await broadcastMount();
1191
+ broadcastOverlayState();
816
1192
  try {
817
1193
  const response = await sendOffscreenMessage<{
818
1194
  ok?: boolean;
@@ -821,16 +1197,33 @@ async function handlePopupStop() {
821
1197
  type: "CLIPS_OFFSCREEN_STOP",
822
1198
  sessionId: recording.sessionId,
823
1199
  });
824
- recording.status = "complete";
825
- recording.error = null;
826
- if (response.result && typeof response.result.recordingId === "string") {
827
- recording.recordingId = response.result.recordingId;
1200
+ if (response.result && response.result.status === "cancelled") {
1201
+ // Stopped during the pre-roll countdown: no media was captured. Treat it
1202
+ // as an aborted take discard the empty recording instead of opening a
1203
+ // playback tab for a finished-but-empty clip.
1204
+ await deleteSession(recording.sessionId);
1205
+ await postAction(
1206
+ {
1207
+ clipsBaseUrl: recording.clipsBaseUrl,
1208
+ captureSurface: recording.captureSurface,
1209
+ includeCamera: recording.includeCamera,
1210
+ includeMicrophone: recording.includeMicrophone,
1211
+ includeDeveloperLogs: recording.includeDeveloperLogs,
1212
+ },
1213
+ "trash-recording",
1214
+ { id: recording.recordingId },
1215
+ ).catch(() => undefined);
1216
+ resetOverlay();
1217
+ await broadcastUnmount();
1218
+ broadcastOverlayState();
1219
+ await clearNativeRecording();
1220
+ return { ok: true, cancelled: true };
828
1221
  }
829
- recording.recordingUrl = recordingUrl(recording);
830
- await saveNativeDiagnostics(recording);
831
- await deleteSession(recording.sessionId);
832
- await clearNativeRecording();
833
- await createTab(recording.recordingUrl);
1222
+ const recordingId =
1223
+ response.result && typeof response.result.recordingId === "string"
1224
+ ? response.result.recordingId
1225
+ : undefined;
1226
+ await finishSaving(recording, recordingId);
834
1227
  return {
835
1228
  ok: true,
836
1229
  recordingId: recording.recordingId,
@@ -840,14 +1233,24 @@ async function handlePopupStop() {
840
1233
  recording.status = "error";
841
1234
  recording.error =
842
1235
  err instanceof Error ? err.message : "Could not stop recording.";
1236
+ resetOverlay();
1237
+ await broadcastUnmount();
1238
+ broadcastOverlayState();
843
1239
  await saveActiveNativeRecording();
844
1240
  return { ok: false, error: recording.error };
845
1241
  }
846
1242
  }
847
1243
 
848
- async function handlePopupCancel() {
1244
+ async function cancelRecording() {
849
1245
  const recording = activeNativeRecording;
850
- if (!recording) return { ok: true };
1246
+ resetOverlay();
1247
+ await broadcastUnmount();
1248
+ broadcastOverlayState();
1249
+ await chrome.action.setBadgeText({ text: "" });
1250
+ if (!recording) {
1251
+ await clearNativeRecording();
1252
+ return { ok: true };
1253
+ }
851
1254
  await sendOffscreenMessage({
852
1255
  type: "CLIPS_OFFSCREEN_CANCEL",
853
1256
  sessionId: recording.sessionId,
@@ -868,6 +1271,60 @@ async function handlePopupCancel() {
868
1271
  return { ok: true };
869
1272
  }
870
1273
 
1274
+ async function saveNativeDiagnostics(
1275
+ recording: NativeRecording,
1276
+ ): Promise<void> {
1277
+ if (!recording.includeDeveloperLogs) return;
1278
+ const session = sessions.get(recording.sessionId);
1279
+ if (!session) return;
1280
+ const diagnostics = snapshotSession(session);
1281
+ await postAction(
1282
+ {
1283
+ clipsBaseUrl: recording.clipsBaseUrl,
1284
+ captureSurface: recording.captureSurface,
1285
+ includeCamera: recording.includeCamera,
1286
+ includeMicrophone: recording.includeMicrophone,
1287
+ includeDeveloperLogs: recording.includeDeveloperLogs,
1288
+ },
1289
+ "save-browser-diagnostics",
1290
+ {
1291
+ recordingId: recording.recordingId,
1292
+ sessionId: recording.sessionId,
1293
+ source: "extension",
1294
+ phase: "recording",
1295
+ pageUrl: diagnostics.pageUrl,
1296
+ userAgent: diagnostics.userAgent,
1297
+ startedAt: diagnostics.startedAt,
1298
+ endedAt: diagnostics.endedAt,
1299
+ consoleLogs: diagnostics.consoleLogs,
1300
+ networkRequests: diagnostics.networkRequests,
1301
+ },
1302
+ ).catch((err) => {
1303
+ console.warn("[clips-extension] diagnostics save failed:", err);
1304
+ });
1305
+ }
1306
+
1307
+ async function clearNativeRecording(): Promise<void> {
1308
+ activeNativeRecording = null;
1309
+ await saveActiveNativeRecording();
1310
+ await chrome.action.setBadgeText({ text: "" });
1311
+ }
1312
+
1313
+ async function handlePopupStatus() {
1314
+ return {
1315
+ ok: true,
1316
+ activeRecording: activeNativeRecording,
1317
+ };
1318
+ }
1319
+
1320
+ async function handlePopupStop() {
1321
+ return stopRecording();
1322
+ }
1323
+
1324
+ async function handlePopupCancel() {
1325
+ return cancelRecording();
1326
+ }
1327
+
871
1328
  async function handlePopupOpen() {
872
1329
  const recording = activeNativeRecording;
873
1330
  if (!recording) return { ok: false, error: "No active recording." };
@@ -1328,13 +1785,61 @@ async function handleExternalMessage(
1328
1785
  return { ok: false, error: "Unknown message." };
1329
1786
  }
1330
1787
 
1331
- chrome.runtime.onInstalled.addListener(() => {
1788
+ chrome.runtime.onInstalled.addListener((details) => {
1332
1789
  void readSettings().then((settings) => storageSet(settings));
1790
+ // Ask for camera/mic once on install so the grant is ready before the first
1791
+ // recording (the record-time gate in the popup is the fallback).
1792
+ if (details.reason === "install") {
1793
+ void createTab(chrome.runtime.getURL("src/permission.html")).catch(
1794
+ () => undefined,
1795
+ );
1796
+ }
1333
1797
  });
1334
1798
 
1799
+ // Restore recording/overlay state whenever the service worker (re)starts so an
1800
+ // in-progress recording survives MV3 worker suspension. Memoized so every entry
1801
+ // point can `await ensureRestored()` before reading activeNativeRecording — the
1802
+ // worker can be revived by the very event (icon click, status message) that
1803
+ // needs the state, and the module globals start out empty until restore runs.
1804
+ let restorePromise: Promise<void> | null = null;
1805
+ function ensureRestored(): Promise<void> {
1806
+ if (!restorePromise) {
1807
+ restorePromise = restoreRuntimeState().catch(() => undefined);
1808
+ }
1809
+ return restorePromise;
1810
+ }
1811
+ void ensureRestored();
1812
+
1335
1813
  chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
1336
1814
  if (!message || typeof message !== "object") return false;
1337
- if ((message as { type?: unknown }).type === "CLIPS_NATIVE_STATUS") {
1815
+ void (async () => {
1816
+ // Critical: never read activeNativeRecording/overlayPhase before state is
1817
+ // restored, or a freshly-woken worker drops the message (e.g. the offscreen
1818
+ // "recording" status, or an overlay Stop click).
1819
+ await ensureRestored();
1820
+ let response: unknown;
1821
+ try {
1822
+ response = await dispatchRuntimeMessage(message);
1823
+ } catch (err) {
1824
+ response = {
1825
+ ok: false,
1826
+ error: err instanceof Error ? err.message : "Could not use Clips.",
1827
+ };
1828
+ }
1829
+ try {
1830
+ sendResponse(response);
1831
+ } catch {
1832
+ /* message port already closed (fire-and-forget sender) */
1833
+ }
1834
+ })();
1835
+ return true;
1836
+ });
1837
+
1838
+ async function dispatchRuntimeMessage(message: unknown): Promise<unknown> {
1839
+ const type = (message as { type?: unknown }).type;
1840
+
1841
+ // Status updates streamed from the offscreen recorder.
1842
+ if (type === "CLIPS_NATIVE_STATUS") {
1338
1843
  const status = message as OffscreenStatusMessage;
1339
1844
  if (
1340
1845
  activeNativeRecording &&
@@ -1349,35 +1854,119 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
1349
1854
  activeNativeRecording,
1350
1855
  );
1351
1856
  }
1352
- void saveActiveNativeRecording();
1857
+ await saveActiveNativeRecording();
1858
+ // The offscreen recorder finished its pre-roll and actually started —
1859
+ // advance from countdown to recording (the reliable phase flip).
1860
+ if (status.status === "recording" && overlayPhase === "countdown") {
1861
+ await markRecordingStarted();
1862
+ }
1863
+ // Safety net: if the worker was suspended during the upload, stopRecording's
1864
+ // await was lost — finalize here when the offscreen reports completion.
1865
+ if (status.status === "complete" && overlayPhase === "saving") {
1866
+ await finishSaving(
1867
+ activeNativeRecording,
1868
+ typeof status.recordingId === "string"
1869
+ ? status.recordingId
1870
+ : undefined,
1871
+ );
1872
+ }
1873
+ if (status.status === "error") {
1874
+ resetOverlay();
1875
+ await broadcastUnmount();
1876
+ await chrome.action.setBadgeText({ text: "" });
1877
+ }
1878
+ }
1879
+ return { ok: true };
1880
+ }
1881
+
1882
+ // The user stopped sharing from Chrome's native control bar.
1883
+ if (type === "CLIPS_NATIVE_ENDED") {
1884
+ const sessionId = (message as { sessionId?: string }).sessionId;
1885
+ if (
1886
+ activeNativeRecording &&
1887
+ activeNativeRecording.sessionId === sessionId &&
1888
+ overlayPhase !== "idle" &&
1889
+ overlayPhase !== "saving"
1890
+ ) {
1891
+ return stopRecording();
1353
1892
  }
1354
- return false;
1893
+ return { ok: true };
1355
1894
  }
1356
1895
 
1357
- const popupMessage = message as PopupMessage;
1358
- let task: Promise<unknown> | null = null;
1359
- if (popupMessage.type === "CLIPS_POPUP_START") {
1360
- task = handlePopupStart(popupMessage);
1361
- } else if (popupMessage.type === "CLIPS_POPUP_STATUS") {
1362
- task = handlePopupStatus();
1363
- } else if (popupMessage.type === "CLIPS_POPUP_STOP") {
1364
- task = handlePopupStop();
1365
- } else if (popupMessage.type === "CLIPS_POPUP_CANCEL") {
1366
- task = handlePopupCancel();
1367
- } else if (popupMessage.type === "CLIPS_POPUP_OPEN") {
1368
- task = handlePopupOpen();
1369
- } else if (popupMessage.type === "CLIPS_POPUP_SIGN_IN") {
1370
- task = handlePopupSignIn(popupMessage);
1896
+ // Content script asking which overlay parts to show on its page.
1897
+ if (type === "CLIPS_CONTENT_HELLO") {
1898
+ return { ok: true, parts: desiredParts() };
1371
1899
  }
1372
- if (!task) return false;
1373
1900
 
1374
- void task.then(sendResponse).catch((err) => {
1375
- sendResponse({
1376
- ok: false,
1377
- error: err instanceof Error ? err.message : "Could not use Clips.",
1378
- });
1379
- });
1380
- return true;
1901
+ // Overlay iframe handshake — rebroadcast the current timer/phase to it.
1902
+ if (type === "CLIPS_OVERLAY_HELLO") {
1903
+ broadcastOverlayState();
1904
+ return { ok: true, state: overlayStateForBroadcast() };
1905
+ }
1906
+
1907
+ if (type === "CLIPS_PREWARM") {
1908
+ void ensureOffscreenDocument().catch(() => undefined);
1909
+ return { ok: true };
1910
+ }
1911
+
1912
+ switch (type) {
1913
+ case "CLIPS_POPUP_START":
1914
+ return handlePopupStart(message as PopupStartMessage);
1915
+ case "CLIPS_POPUP_STATUS":
1916
+ return handlePopupStatus();
1917
+ case "CLIPS_POPUP_STOP":
1918
+ return handlePopupStop();
1919
+ case "CLIPS_POPUP_CANCEL":
1920
+ return handlePopupCancel();
1921
+ case "CLIPS_POPUP_OPEN":
1922
+ return handlePopupOpen();
1923
+ case "CLIPS_POPUP_SIGN_IN":
1924
+ return handlePopupSignIn(
1925
+ message as { settings?: Partial<ExtensionSettings> },
1926
+ );
1927
+ case "CLIPS_OVERLAY_COUNTDOWN_DONE":
1928
+ // Skip button on the countdown overlay: start the recorder now.
1929
+ return handleOverlaySkip();
1930
+ case "CLIPS_OVERLAY_PAUSE":
1931
+ return handleOverlayPause();
1932
+ case "CLIPS_OVERLAY_RESUME":
1933
+ return handleOverlayResume();
1934
+ case "CLIPS_OVERLAY_STOP":
1935
+ return stopRecording();
1936
+ case "CLIPS_OVERLAY_CANCEL":
1937
+ return cancelRecording();
1938
+ case "CLIPS_OVERLAY_RESTART":
1939
+ return handleOverlayRestart();
1940
+ default:
1941
+ return { ok: false, error: "Unknown message." };
1942
+ }
1943
+ }
1944
+
1945
+ // While a recording is active, keep the overlay following the user as they
1946
+ // switch tabs (programmatic injection covers tabs opened before the extension
1947
+ // loaded; declared content scripts cover navigations).
1948
+ chrome.tabs.onActivated.addListener((info) => {
1949
+ void (async () => {
1950
+ await ensureRestored();
1951
+ if (overlayPhase === "idle" || !activeNativeRecording) return;
1952
+ await mountOverlayOnTab(info.tabId);
1953
+ })();
1954
+ });
1955
+
1956
+ // While recording, the popup is disabled (setActionPopup("")), so clicking the
1957
+ // extension icon lands here: stop & save immediately (or discard if we're still
1958
+ // in the pre-roll countdown). When idle, the popup opens and this never fires.
1959
+ chrome.action.onClicked.addListener(() => {
1960
+ void (async () => {
1961
+ // Await restore — the click may have woken the worker, leaving the module
1962
+ // globals empty until this resolves. Without it, Stop silently does nothing.
1963
+ await ensureRestored();
1964
+ if (!activeNativeRecording) return;
1965
+ if (overlayPhase === "countdown") await cancelRecording();
1966
+ else if (overlayPhase === "recording" || overlayPhase === "paused") {
1967
+ await stopRecording();
1968
+ }
1969
+ })();
1381
1970
  });
1382
1971
 
1383
1972
  chrome.runtime.onMessageExternal.addListener(