@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
@@ -232,7 +232,10 @@ export const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
232
232
  const [currentMs, setCurrentMs] = useState(startMs ?? 0);
233
233
  const [loomStartMs, setLoomStartMs] = useState<number | null>(null);
234
234
  const [volume, setVolume] = useState(1);
235
- const [muted, setMuted] = useState(false);
235
+ // Autoplaying players (e.g. the Slack unfurl embed, `?autoplay=1`) must
236
+ // start muted or the browser blocks autoplay with a NotAllowedError. The
237
+ // share page (no autoplay) keeps full sound.
238
+ const [muted, setMuted] = useState(() => !!autoPlay);
236
239
  const [speed, setSpeed] = useState(() =>
237
240
  readPlaybackSpeedPreference(defaultSpeed),
238
241
  );
@@ -354,18 +357,30 @@ export const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
354
357
  setIsPreparing(false);
355
358
  }, []);
356
359
 
357
- const rejectPlayAttempt = useCallback((attemptId: number, err: unknown) => {
358
- if (attemptId !== playAttemptIdRef.current) return;
359
- playAttemptPendingRef.current = false;
360
- setIsPlayPending(false);
361
- setIsBuffering(false);
362
-
363
- const name = err instanceof DOMException ? err.name : "";
364
- if (name === "AbortError") return;
365
-
366
- console.warn("[clips] playback start failed", err);
367
- setPlayError("Could not start playback. Try again.");
368
- }, []);
360
+ const rejectPlayAttempt = useCallback(
361
+ (attemptId: number, err: unknown) => {
362
+ if (attemptId !== playAttemptIdRef.current) return;
363
+ playAttemptPendingRef.current = false;
364
+ setIsPlayPending(false);
365
+ setIsBuffering(false);
366
+
367
+ const name = err instanceof DOMException ? err.name : "";
368
+ // AbortError: a newer load/seek superseded this play() — not a failure.
369
+ // NotAllowedError: the browser blocked autoplay because there was no
370
+ // user gesture (this is what happens inside Slack's cross-origin unfurl
371
+ // iframe). Both are expected — fall back to the click-to-play overlay
372
+ // instead of showing a scary "Could not start playback" message.
373
+ if (name === "AbortError" || name === "NotAllowedError") return;
374
+
375
+ console.warn("[clips] playback start failed", err);
376
+ reportPlaybackIssue("play-start-failed", err, videoRef.current, {
377
+ recordingId,
378
+ autoPlay: !!autoPlay,
379
+ });
380
+ setPlayError("Could not start playback. Try again.");
381
+ },
382
+ [autoPlay, recordingId],
383
+ );
369
384
 
370
385
  const attachPlayPromise = useCallback(
371
386
  (playPromise: Promise<void> | undefined, attemptId: number) => {
@@ -882,7 +897,10 @@ export const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
882
897
  <div
883
898
  ref={containerRef}
884
899
  className={cn(
885
- "relative bg-black overflow-hidden select-none group",
900
+ // `@container` lets the center play button scale with the player
901
+ // width (see CenterPlaybackOverlay) so it isn't oversized inside
902
+ // small embeds like the Slack unfurl iframe.
903
+ "relative @container bg-black overflow-hidden select-none group",
886
904
  theaterMode ? "fixed inset-0 z-40" : "rounded-xl",
887
905
  className,
888
906
  )}
@@ -910,12 +928,19 @@ export const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
910
928
  ref={videoRef}
911
929
  src={activeVideoSrc}
912
930
  poster={resolveLocalUrl(thumbnailUrl)}
913
- crossOrigin="anonymous"
931
+ // `crossOrigin` is only needed so the owner's canvas thumbnail
932
+ // capture isn't tainted. For everyone else (viewers, and the Slack
933
+ // unfurl embed) it adds nothing — but if the player is ever framed
934
+ // into a sandboxed/opaque-origin context (Slack double-iframes the
935
+ // embed), the resulting CORS check on the media bytes fails and the
936
+ // video won't load. Scope it to owners so embeds load cleanly.
937
+ crossOrigin={role === "owner" ? "anonymous" : undefined}
914
938
  className={cn(
915
939
  "w-full h-full",
916
940
  cover ? "object-cover" : "object-contain",
917
941
  )}
918
942
  autoPlay={autoPlay}
943
+ muted={muted}
919
944
  playsInline
920
945
  onLoadStart={() => {
921
946
  setCanPlay(false);
@@ -1014,12 +1039,26 @@ export const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
1014
1039
  setIsBuffering(false);
1015
1040
  onEnded?.();
1016
1041
  }}
1017
- onError={() => {
1042
+ onError={(e) => {
1018
1043
  playAttemptPendingRef.current = false;
1019
1044
  setIsPlayPending(false);
1020
1045
  setIsBuffering(false);
1021
1046
  setIsPreparing(false);
1022
- setPlayError("Video could not be loaded.");
1047
+ const desc = describeMediaError(e.currentTarget.error);
1048
+ reportPlaybackIssue(
1049
+ "media-load-failed",
1050
+ e.currentTarget.error,
1051
+ e.currentTarget,
1052
+ {
1053
+ recordingId,
1054
+ videoSrc: activeVideoSrc,
1055
+ },
1056
+ );
1057
+ setPlayError(
1058
+ desc
1059
+ ? `Video could not be loaded (${desc.label}).`
1060
+ : "Video could not be loaded.",
1061
+ );
1023
1062
  }}
1024
1063
  onVolumeChange={(e) => {
1025
1064
  setVolume(e.currentTarget.volume);
@@ -1039,7 +1078,17 @@ export const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
1039
1078
  durationMs={resolvedDurationMs}
1040
1079
  speed={speed}
1041
1080
  playError={playError}
1042
- onPlay={requestPlay}
1081
+ onPlay={() => {
1082
+ // An explicit click means the user wants to watch with sound, so
1083
+ // undo the muted-autoplay default (see `muted` state) before we
1084
+ // start playback.
1085
+ const v = videoRef.current;
1086
+ if (v && v.muted) {
1087
+ v.muted = false;
1088
+ setMuted(false);
1089
+ }
1090
+ requestPlay();
1091
+ }}
1043
1092
  onSpeedChange={applySpeed}
1044
1093
  menuPortalContainer={fullscreenMenuContainer}
1045
1094
  />
@@ -1193,9 +1242,9 @@ function CenterPlaybackOverlay({
1193
1242
  e.stopPropagation();
1194
1243
  onPlay();
1195
1244
  }}
1196
- className="pointer-events-auto flex h-24 w-24 items-center justify-center rounded-full bg-white text-black shadow-2xl ring-1 ring-white/35 transition-transform duration-150 hover:scale-105 hover:bg-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-offset-2 focus-visible:ring-offset-black"
1245
+ className="pointer-events-auto flex h-[clamp(3rem,13cqw,6rem)] w-[clamp(3rem,13cqw,6rem)] items-center justify-center rounded-full bg-white text-black shadow-2xl ring-1 ring-white/35 transition-transform duration-150 hover:scale-105 hover:bg-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-offset-2 focus-visible:ring-offset-black"
1197
1246
  >
1198
- <IconPlayerPlay className="ml-1 h-12 w-12 fill-current" />
1247
+ <IconPlayerPlay className="ml-[6%] h-[clamp(1.5rem,6.5cqw,3rem)] w-[clamp(1.5rem,6.5cqw,3rem)] fill-current" />
1199
1248
  </button>
1200
1249
 
1201
1250
  <div
@@ -1302,6 +1351,87 @@ function formatSpeedLabel(rate: number): string {
1302
1351
  return `${Number.isInteger(rate) ? rate : rate.toFixed(1)}x`;
1303
1352
  }
1304
1353
 
1354
+ /** Human-readable label for an HTMLMediaElement `error` (MediaError). */
1355
+ function describeMediaError(
1356
+ err: MediaError | null,
1357
+ ): { code: number; label: string } | null {
1358
+ if (!err) return null;
1359
+ const labels: Record<number, string> = {
1360
+ 1: "load aborted",
1361
+ 2: "network error",
1362
+ 3: "decode error",
1363
+ 4: "format not supported",
1364
+ };
1365
+ return { code: err.code, label: labels[err.code] ?? "unknown error" };
1366
+ }
1367
+
1368
+ /**
1369
+ * Surface a playback failure to the console and Sentry with enough context to
1370
+ * debug it remotely — e.g. when a clip "could not be loaded" inside a Slack
1371
+ * unfurl where there's no visible console. Best-effort; never throws. Expected,
1372
+ * benign cases (AbortError / autoplay-blocked NotAllowedError) are filtered out
1373
+ * by the callers and never reach here.
1374
+ */
1375
+ function reportPlaybackIssue(
1376
+ reason: string,
1377
+ err: unknown,
1378
+ video: HTMLVideoElement | null,
1379
+ extra: Record<string, unknown>,
1380
+ ) {
1381
+ const mediaError =
1382
+ err && typeof err === "object" && "code" in err
1383
+ ? (err as MediaError)
1384
+ : (video?.error ?? null);
1385
+ const name =
1386
+ err instanceof DOMException || err instanceof Error ? err.name : undefined;
1387
+ const message = err instanceof Error ? err.message : undefined;
1388
+
1389
+ let videoHost: string | undefined;
1390
+ try {
1391
+ if (video?.currentSrc) videoHost = new URL(video.currentSrc).host;
1392
+ } catch {
1393
+ // ignore unparseable src
1394
+ }
1395
+ let inIframe = false;
1396
+ try {
1397
+ inIframe = typeof window !== "undefined" && window.self !== window.top;
1398
+ } catch {
1399
+ inIframe = true;
1400
+ }
1401
+
1402
+ const detail = {
1403
+ ...extra,
1404
+ errorName: name,
1405
+ errorMessage: message,
1406
+ mediaErrorCode: mediaError?.code,
1407
+ mediaErrorLabel: describeMediaError(mediaError)?.label,
1408
+ videoHost,
1409
+ inIframe,
1410
+ readyState: video?.readyState,
1411
+ networkState: video?.networkState,
1412
+ };
1413
+ console.warn(`[clips] playback issue: ${reason}`, detail);
1414
+
1415
+ try {
1416
+ const reportable =
1417
+ err instanceof Error
1418
+ ? err
1419
+ : new Error(
1420
+ `clips playback ${reason}: ${message ?? name ?? describeMediaError(mediaError)?.label ?? "unknown"}`,
1421
+ );
1422
+ captureClientException(reportable, {
1423
+ tags: {
1424
+ area: "clips-player",
1425
+ playbackIssue: reason,
1426
+ inIframe: String(inIframe),
1427
+ },
1428
+ extra: detail,
1429
+ });
1430
+ } catch {
1431
+ // Diagnostics must never break playback UI.
1432
+ }
1433
+ }
1434
+
1305
1435
  function formatWatchDuration(ms: number): string {
1306
1436
  if (!Number.isFinite(ms) || ms <= 0) return "0 sec";
1307
1437
  const totalSeconds = Math.max(1, Math.round(ms / 1000));
@@ -1,13 +1,12 @@
1
- import { useEffect, useRef, useState } from "react";
2
- import { IconPlayerPause, IconPlayerPlay, IconX } from "@tabler/icons-react";
3
- import { Button } from "@/components/ui/button";
1
+ import { useCallback, useEffect, useRef, useState } from "react";
2
+ import { IconPlayerSkipForward, IconX } from "@tabler/icons-react";
4
3
 
5
4
  export interface CountdownOverlayProps {
6
5
  /** Total seconds to count down from. Default 3. */
7
6
  seconds?: number;
8
- /** Called when the countdown reaches 1. */
7
+ /** Called when the countdown reaches 1 (drives the recording-start cue). */
9
8
  onOneSecond?: () => void;
10
- /** Called when the countdown reaches 0. */
9
+ /** Called when the countdown reaches 0 (or the user skips). */
11
10
  onComplete: () => void;
12
11
  /** Called when the user cancels before recording begins. */
13
12
  onCancel: () => void;
@@ -20,77 +19,102 @@ export function CountdownOverlay({
20
19
  onCancel,
21
20
  }: CountdownOverlayProps) {
22
21
  const [remaining, setRemaining] = useState(seconds);
23
- const [paused, setPaused] = useState(false);
24
- // Ensure onComplete fires exactly once for the lifetime of the countdown,
25
- // even if its identity changes while `remaining` is already 0 (which would
26
- // otherwise re-run this effect and call it again).
22
+ // Ensure each callback fires exactly once for the lifetime of the countdown,
23
+ // even if identities change or the user skips while the timer is mid-flight.
27
24
  const hasCompletedRef = useRef(false);
28
25
  const hasPlayedOneSecondCueRef = useRef(false);
29
26
 
30
- useEffect(() => {
31
- if (remaining !== 1 || hasPlayedOneSecondCueRef.current) return;
27
+ const playOneSecondCue = useCallback(() => {
28
+ if (hasPlayedOneSecondCueRef.current) return;
32
29
  hasPlayedOneSecondCueRef.current = true;
33
30
  onOneSecond?.();
34
- }, [remaining, onOneSecond]);
31
+ }, [onOneSecond]);
32
+
33
+ const complete = useCallback(() => {
34
+ if (hasCompletedRef.current) return;
35
+ hasCompletedRef.current = true;
36
+ onComplete();
37
+ }, [onComplete]);
38
+
39
+ // Skip the rest of the countdown and start recording immediately. Still play
40
+ // the start cue so the user gets the same audible confirmation they'd get if
41
+ // the countdown had run to zero.
42
+ const handleSkip = useCallback(() => {
43
+ playOneSecondCue();
44
+ complete();
45
+ }, [playOneSecondCue, complete]);
46
+
47
+ useEffect(() => {
48
+ if (remaining === 1) playOneSecondCue();
49
+ }, [remaining, playOneSecondCue]);
35
50
 
36
51
  useEffect(() => {
37
52
  if (remaining <= 0) {
38
- if (!hasCompletedRef.current) {
39
- hasCompletedRef.current = true;
40
- onComplete();
41
- }
53
+ complete();
42
54
  return;
43
55
  }
44
- if (paused) return;
45
56
  const id = window.setTimeout(() => setRemaining((v) => v - 1), 1000);
46
57
  return () => window.clearTimeout(id);
47
- }, [remaining, onComplete, paused]);
58
+ }, [remaining, complete]);
59
+
60
+ // Keyboard parity with the desktop overlay: Enter starts now, Esc cancels.
61
+ useEffect(() => {
62
+ const onKeyDown = (event: KeyboardEvent) => {
63
+ if (event.key === "Enter") {
64
+ event.preventDefault();
65
+ handleSkip();
66
+ } else if (event.key === "Escape") {
67
+ event.preventDefault();
68
+ onCancel();
69
+ }
70
+ };
71
+ window.addEventListener("keydown", onKeyDown);
72
+ return () => window.removeEventListener("keydown", onKeyDown);
73
+ }, [handleSkip, onCancel]);
74
+
75
+ const controlClasses =
76
+ "flex h-16 w-16 items-center justify-center rounded-full border border-white/25 bg-white/5 text-white/90 shadow-lg backdrop-blur transition-colors hover:border-white/60 hover:bg-white/15 hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/70";
48
77
 
49
78
  return (
50
79
  <div
51
- className="fixed inset-0 z-[100] flex items-center justify-center bg-black/70 backdrop-blur-sm"
80
+ className="fixed inset-0 z-[100] flex flex-col items-center justify-center bg-black/70 backdrop-blur-sm"
52
81
  aria-live="polite"
53
82
  aria-label={`Recording starts in ${remaining}`}
54
83
  >
55
- <div className="flex flex-col items-center gap-6">
84
+ <div className="flex items-center gap-10 sm:gap-14">
85
+ <button
86
+ type="button"
87
+ onClick={onCancel}
88
+ aria-label="Cancel recording"
89
+ className={controlClasses}
90
+ >
91
+ <IconX className="h-7 w-7" stroke={1.75} />
92
+ </button>
93
+
56
94
  <div
57
95
  key={remaining}
58
- className="flex h-48 w-48 items-center justify-center rounded-full text-[120px] font-bold text-white shadow-2xl"
96
+ className="flex h-44 w-44 items-center justify-center rounded-full text-[112px] font-bold leading-none text-white shadow-2xl duration-200 animate-in zoom-in-75 fade-in"
59
97
  style={{
60
98
  background:
61
- "radial-gradient(circle at 30% 30%, rgba(255,255,255,0.15), transparent 60%), hsl(var(--primary))",
99
+ "radial-gradient(circle at 30% 30%, rgba(255,255,255,0.18), transparent 60%), hsl(var(--primary))",
62
100
  }}
63
101
  >
64
102
  {remaining > 0 ? remaining : "Go"}
65
103
  </div>
66
104
 
67
- <div className="flex items-center gap-2 rounded-full border border-white/15 bg-black/40 p-1.5 shadow-2xl backdrop-blur">
68
- <Button
69
- type="button"
70
- variant="secondary"
71
- size="sm"
72
- className="rounded-full"
73
- onClick={() => setPaused((value) => !value)}
74
- >
75
- {paused ? (
76
- <IconPlayerPlay className="h-4 w-4" />
77
- ) : (
78
- <IconPlayerPause className="h-4 w-4" />
79
- )}
80
- {paused ? "Resume" : "Pause"}
81
- </Button>
82
- <Button
83
- type="button"
84
- variant="outline"
85
- size="sm"
86
- className="rounded-full border-white/20 bg-white/10 text-white hover:bg-white/20 hover:text-white"
87
- onClick={onCancel}
88
- >
89
- <IconX className="h-4 w-4" />
90
- Cancel
91
- </Button>
92
- </div>
105
+ <button
106
+ type="button"
107
+ onClick={handleSkip}
108
+ aria-label="Skip countdown and start recording now"
109
+ className={controlClasses}
110
+ >
111
+ <IconPlayerSkipForward className="h-7 w-7" stroke={1.75} />
112
+ </button>
93
113
  </div>
114
+
115
+ <p className="absolute bottom-16 left-1/2 -translate-x-1/2 rounded-full bg-black/50 px-3 py-1 text-xs font-medium text-white/70 backdrop-blur">
116
+ Esc to cancel · Enter to start now
117
+ </p>
94
118
  </div>
95
119
  );
96
120
  }
@@ -308,8 +308,9 @@ export function PreRecordPanel({
308
308
  }, [cameraId, cameras]);
309
309
 
310
310
  // Camera-only mode needs a camera — coerce a restored "off" sentinel to
311
- // "default" so Start doesn't forward it as an exact deviceId (covers the
312
- // ?mode=camera deep-link/restore path the mode buttons already handle).
311
+ // "default" so Start doesn't forward it as an exact deviceId. This is the
312
+ // single owner of that coercion: it covers both the mode-button click and the
313
+ // ?mode=camera deep-link/restore path.
313
314
  useEffect(() => {
314
315
  if (mode === "camera" && cameraId === NO_CAMERA_DEVICE_ID) {
315
316
  setCameraId("default");
@@ -583,15 +584,10 @@ export function PreRecordPanel({
583
584
  <button
584
585
  key={opt.value}
585
586
  type="button"
586
- onClick={() => {
587
- chooseMode(opt.value);
588
- if (
589
- opt.value === "camera" &&
590
- cameraId === NO_CAMERA_DEVICE_ID
591
- ) {
592
- chooseCamera("default");
593
- }
594
- }}
587
+ // Camera-only mode needs a camera; the [mode, cameraId] effect
588
+ // below coerces a restored "off" sentinel to "default" for both
589
+ // this click and the ?mode=camera deep-link path.
590
+ onClick={() => chooseMode(opt.value)}
595
591
  className={cn(
596
592
  "flex min-h-20 min-w-0 flex-col justify-between rounded-xl border p-3 text-left transition-colors",
597
593
  active
@@ -420,6 +420,13 @@ export class RecorderEngine {
420
420
  */
421
421
  private cameraDisconnectNotified = false;
422
422
  private micDisconnectNotified = false;
423
+ /**
424
+ * Set when a stale/unavailable `micDeviceId` forces a fallback to the system
425
+ * default mic during `acquire()`. The recording then runs on the default
426
+ * device, so live transcription (which can only ever use the default mic) is
427
+ * safe to start even though a specific mic was originally requested.
428
+ */
429
+ private micFellBackToDefault = false;
423
430
 
424
431
  private state: RecorderState = "idle";
425
432
 
@@ -448,10 +455,30 @@ export class RecorderEngine {
448
455
  return this.cameraStream;
449
456
  }
450
457
 
458
+ /**
459
+ * True when `acquire()` had to fall back from the requested `micDeviceId` to
460
+ * the system default mic. Lets the UI start live transcription for this
461
+ * session even though a specific mic was originally selected.
462
+ */
463
+ didMicFallBackToDefault(): boolean {
464
+ return this.micFellBackToDefault;
465
+ }
466
+
451
467
  getPreviewStream(): MediaStream | null {
452
468
  return this.previewStream;
453
469
  }
454
470
 
471
+ /**
472
+ * The composited screen+camera canvas stream that actually gets recorded
473
+ * (screen with the camera bubble drawn in), or `null` for screen-only /
474
+ * camera-only modes where the visible preview already matches the recording.
475
+ * Used to grab a thumbnail that includes the presenter's camera — the raw
476
+ * preview stream in screen+camera mode is screen-only and has no face.
477
+ */
478
+ getCompositeStream(): MediaStream | null {
479
+ return this.cameraComposite?.stream ?? null;
480
+ }
481
+
455
482
  getElapsedMs(): number {
456
483
  if (this.startedAtMs === null) return 0;
457
484
  const now = performance.now();
@@ -515,6 +542,7 @@ export class RecorderEngine {
515
542
  const wantsCamera =
516
543
  this.opts.mode === "camera" || this.opts.mode === "screen+camera";
517
544
  const wantsMic = this.opts.micDeviceId !== NO_MIC_DEVICE_ID;
545
+ this.micFellBackToDefault = false;
518
546
 
519
547
  try {
520
548
  if (!isBrowserSecureContext()) {
@@ -634,6 +662,9 @@ export class RecorderEngine {
634
662
  audio: voiceFocusedAudioConstraints(),
635
663
  video: false,
636
664
  });
665
+ // Recording is now on the system default mic, so live
666
+ // transcription can run for this session after all.
667
+ this.micFellBackToDefault = true;
637
668
  } catch (retryErr) {
638
669
  throw this.friendlyError(retryErr, "microphone");
639
670
  }
@@ -0,0 +1,71 @@
1
+ import { IconBrandSlack } from "@tabler/icons-react";
2
+ import { appPath, useActionQuery } from "@agent-native/core/client";
3
+
4
+ interface SlackInstallation {
5
+ status: string;
6
+ }
7
+
8
+ interface SlackInstallationsResponse {
9
+ oauthConfigured: boolean;
10
+ installations: SlackInstallation[];
11
+ }
12
+
13
+ /**
14
+ * Contextual nudge shown in the share popover's Link tab for public clips.
15
+ *
16
+ * A public Clips link unfurls into a playable video when pasted in a connected
17
+ * Slack workspace, but that integration is otherwise only discoverable in
18
+ * Settings. When a workspace is connected we confirm the payoff; when none is,
19
+ * clip owners/admins get a one-click link to connect one in Settings.
20
+ *
21
+ * Only rendered for public clips (the caller gates on `isPublic`), so the
22
+ * `list-slack-installations` query never runs for private shares.
23
+ */
24
+ export function SlackShareHint({ canManage }: { canManage: boolean }) {
25
+ const slack = useActionQuery<SlackInstallationsResponse>(
26
+ "list-slack-installations",
27
+ undefined,
28
+ { retry: false },
29
+ );
30
+
31
+ const data = slack.data;
32
+ const connected =
33
+ data?.installations?.some((i) => i.status === "connected") ?? false;
34
+
35
+ if (connected) {
36
+ return (
37
+ <div className="flex items-center gap-2.5 rounded-md border border-border bg-muted/40 px-3 py-2">
38
+ <IconBrandSlack className="h-4 w-4 shrink-0 text-muted-foreground" />
39
+ <div className="min-w-0">
40
+ <div className="text-xs font-medium">Plays inline in Slack</div>
41
+ <p className="text-[11px] text-muted-foreground">
42
+ Paste this link in any connected workspace to play it inline.
43
+ </p>
44
+ </div>
45
+ </div>
46
+ );
47
+ }
48
+
49
+ // No workspace connected yet. Connecting needs the deployment's Slack OAuth
50
+ // credentials, so only point managers at Settings when it's actually set up —
51
+ // otherwise the Connect button there is disabled and the link is a dead end.
52
+ // While the query is loading or errored, `data` is undefined and we render
53
+ // nothing rather than guess at the workspace state.
54
+ if (!data || !canManage || !data.oauthConfigured) return null;
55
+
56
+ return (
57
+ <a
58
+ href={appPath("/settings#slack")}
59
+ className="flex items-center gap-2.5 rounded-md border border-border px-3 py-2 transition-colors hover:bg-muted/50"
60
+ >
61
+ <IconBrandSlack className="h-4 w-4 shrink-0 text-muted-foreground" />
62
+ <div className="min-w-0 flex-1">
63
+ <div className="text-xs font-medium">Make it play inline in Slack</div>
64
+ <p className="text-[11px] text-muted-foreground">
65
+ Connect a workspace so this link unfurls as a video.
66
+ </p>
67
+ </div>
68
+ <span className="shrink-0 text-xs font-medium text-primary">Connect</span>
69
+ </a>
70
+ );
71
+ }
@@ -8,35 +8,53 @@ const noopCountdownAudioCue: CountdownAudioCue = {
8
8
  cleanup() {},
9
9
  };
10
10
 
11
+ /**
12
+ * A soft, modern "ready" chime: a rising two-note (D5 → A5) with a faint high
13
+ * shimmer. Each voice has a fast attack and a smooth exponential tail so it
14
+ * reads as a gentle confirmation rather than a harsh beep. Kept under ~380ms so
15
+ * it lands cleanly just before capture.
16
+ */
11
17
  function scheduleCountdownTone(ctx: AudioContext): Promise<void> {
12
18
  return new Promise<void>((resolve) => {
13
- const startedAt = ctx.currentTime + 0.005;
14
- const oscillator = ctx.createOscillator();
15
- const gain = ctx.createGain();
19
+ const t0 = ctx.currentTime + 0.005;
20
+ const voices = [
21
+ { freq: 587.33, at: 0.0, dur: 0.22, peak: 0.06 }, // D5
22
+ { freq: 880.0, at: 0.06, dur: 0.26, peak: 0.075 }, // A5
23
+ { freq: 1760.0, at: 0.065, dur: 0.14, peak: 0.02 }, // A6 shimmer
24
+ ];
16
25
 
17
- oscillator.type = "sine";
18
- oscillator.frequency.setValueAtTime(880, startedAt);
19
- oscillator.frequency.exponentialRampToValueAtTime(660, startedAt + 0.14);
26
+ let lastStop = t0;
27
+ for (const voice of voices) {
28
+ const startAt = t0 + voice.at;
29
+ const stopAt = startAt + voice.dur;
30
+ lastStop = Math.max(lastStop, stopAt);
20
31
 
21
- gain.gain.setValueAtTime(0.0001, startedAt);
22
- gain.gain.exponentialRampToValueAtTime(0.07, startedAt + 0.018);
23
- gain.gain.exponentialRampToValueAtTime(0.0001, startedAt + 0.18);
32
+ const oscillator = ctx.createOscillator();
33
+ oscillator.type = "sine";
34
+ oscillator.frequency.setValueAtTime(voice.freq, startAt);
24
35
 
25
- oscillator.connect(gain);
26
- gain.connect(ctx.destination);
36
+ const gain = ctx.createGain();
37
+ gain.gain.setValueAtTime(0.0001, startAt);
38
+ gain.gain.exponentialRampToValueAtTime(voice.peak, startAt + 0.012);
39
+ gain.gain.exponentialRampToValueAtTime(0.0001, stopAt);
40
+
41
+ oscillator.connect(gain);
42
+ gain.connect(ctx.destination);
43
+
44
+ oscillator.start(startAt);
45
+ oscillator.stop(stopAt + 0.02);
46
+ }
27
47
 
28
48
  let resolved = false;
29
49
  const finish = () => {
30
50
  if (resolved) return;
31
51
  resolved = true;
32
- window.clearTimeout(timer);
33
52
  resolve();
34
53
  };
35
- const timer = window.setTimeout(finish, 500);
36
- oscillator.addEventListener("ended", finish, { once: true });
37
-
38
- oscillator.start(startedAt);
39
- oscillator.stop(startedAt + 0.2);
54
+ window.setTimeout(
55
+ finish,
56
+ Math.ceil((lastStop - ctx.currentTime) * 1000) + 60,
57
+ );
40
58
  });
41
59
  }
42
60
 
@@ -21,6 +21,7 @@ import {
21
21
  } from "@agent-native/core/client";
22
22
  import { IconCheck, IconSun, IconMoon } from "@tabler/icons-react";
23
23
  import { useTheme } from "next-themes";
24
+ import changelog from "../CHANGELOG.md?raw";
24
25
  import { Toaster } from "@/components/ui/sonner";
25
26
  import { Button } from "@/components/ui/button";
26
27
  import {
@@ -258,7 +259,12 @@ function AppContent() {
258
259
  {standalonePublic ? null : <DbSyncSetup />}
259
260
  {standalonePublic ? null : <ClipsExtensionAuthBridge />}
260
261
  {standalonePublic ? null : (
261
- <CommandMenu open={cmdkOpen} onOpenChange={setCmdkOpen}>
262
+ <CommandMenu
263
+ open={cmdkOpen}
264
+ onOpenChange={setCmdkOpen}
265
+ changelog={changelog}
266
+ changelogKey="clips"
267
+ >
262
268
  <CommandMenu.Group heading="Actions">
263
269
  <CommandMenu.Item onSelect={() => {}}>Search</CommandMenu.Item>
264
270
  </CommandMenu.Group>