@agent-native/core 0.169.1 → 0.171.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (463) hide show
  1. package/agent-native.eject.json +4 -1
  2. package/bin/agent-native.js +23 -5
  3. package/corpus/README.md +1 -1
  4. package/corpus/templates/analytics/.agents/skills/secrets/SKILL.md +11 -4
  5. package/corpus/templates/analytics/actions/query-agent-native-analytics.ts +1 -1
  6. package/corpus/templates/analytics/app/i18n-data.ts +28 -0
  7. package/corpus/templates/analytics/app/pages/Agents.tsx +25 -1
  8. package/corpus/templates/analytics/app/routes/$.tsx +10 -0
  9. package/corpus/templates/analytics/app/routes.ts +3 -1
  10. package/corpus/templates/analytics/changelog/2026-08-22-fixed-the-agent-native-templates-first-party-dashboard-panel.md +6 -0
  11. package/corpus/templates/analytics/changelog/2026-08-22-org-admin-panels-no-longer-report-a-database-error-as-a-per.md +9 -0
  12. package/corpus/templates/analytics/changelog/2026-08-22-slack-requests-report-invalid-inputs.md +6 -0
  13. package/corpus/templates/analytics/changelog/2026-08-22-unknown-analytics-links-now-return-a-real-not-found-status.md +6 -0
  14. package/corpus/templates/analytics/docs/schemas/first-party-analytics.md +2 -1
  15. package/corpus/templates/analytics/docs/schemas/tracked-events.md +2 -0
  16. package/corpus/templates/analytics/scripts/emit-netlify-dashboard-report-cron.ts +29 -7
  17. package/corpus/templates/analytics/server/handlers/slack.ts +59 -5
  18. package/corpus/templates/analytics/server/lib/db-admin-connections.ts +11 -6
  19. package/corpus/templates/analytics/server/lib/first-party-analytics.ts +132 -2
  20. package/corpus/templates/analytics/server/lib/slack.ts +4 -2
  21. package/corpus/templates/analytics/vitest.config.ts +6 -1
  22. package/corpus/templates/assets/.agents/skills/secrets/SKILL.md +11 -4
  23. package/corpus/templates/assets/app/routes/library.tsx +5 -3
  24. package/corpus/templates/assets/app/routes.ts +3 -1
  25. package/corpus/templates/assets/changelog/2026-08-22-library-page-loads-again-instead-of-crashing-on-open.md +6 -0
  26. package/corpus/templates/assets/vitest.config.ts +6 -1
  27. package/corpus/templates/brain/.agents/skills/secrets/SKILL.md +11 -4
  28. package/corpus/templates/brain/app/components/ui/pointer-lock.ts +1 -0
  29. package/corpus/templates/brain/app/routes/sources.tsx +8 -3
  30. package/corpus/templates/brain/changelog/2026-08-22-tuning-or-adding-a-source-no-longer-leaves-the-sources-page.md +6 -0
  31. package/corpus/templates/brain/vitest.config.ts +1 -0
  32. package/corpus/templates/calendar/.agents/skills/secrets/SKILL.md +11 -4
  33. package/corpus/templates/calendar/actions/list-booking-links.ts +83 -4
  34. package/corpus/templates/calendar/actions/update-booking-link.ts +4 -3
  35. package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +11 -3
  36. package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +1 -1
  37. package/corpus/templates/calendar/app/components/calendar/GoogleConnectBanner.tsx +103 -44
  38. package/corpus/templates/calendar/app/components/calendar/OutOfOfficeEvent.tsx +28 -24
  39. package/corpus/templates/calendar/app/components/layout/AppLayout.tsx +9 -2
  40. package/corpus/templates/calendar/app/components/layout/Sidebar.tsx +47 -58
  41. package/corpus/templates/calendar/app/hooks/use-booking-links.ts +5 -1
  42. package/corpus/templates/calendar/app/lib/event-layout.ts +52 -26
  43. package/corpus/templates/calendar/app/pages/BookingLinksPage.tsx +126 -98
  44. package/corpus/templates/calendar/app/pages/CalendarView.tsx +1 -0
  45. package/corpus/templates/calendar/app/pages/Settings.tsx +88 -81
  46. package/corpus/templates/calendar/app/routes.ts +3 -1
  47. package/corpus/templates/calendar/changelog/2026-08-22-booking-link-visibility-respects-shared-access.md +6 -0
  48. package/corpus/templates/calendar/changelog/2026-08-22-clicking-an-out-of-office-event-s-marker-no-longer-creates-a.md +6 -0
  49. package/corpus/templates/calendar/changelog/2026-08-22-event-detail-views-now-show-your-calendar-timezone.md +6 -0
  50. package/corpus/templates/calendar/changelog/2026-08-22-google-calendar-can-now-connect-with-the-shared-google-oauth.md +6 -0
  51. package/corpus/templates/calendar/changelog/2026-08-22-overlapping-events-now-show-each-others-boundaries.md +6 -0
  52. package/corpus/templates/calendar/changelog/2026-08-22-public-booking-pages-now-offer-time-slots-from-the-host-s-we.md +6 -0
  53. package/corpus/templates/calendar/server/handlers/bookings.ts +5 -7
  54. package/corpus/templates/calendar/server/handlers/google-auth.ts +10 -2
  55. package/corpus/templates/calendar/server/lib/google-calendar.ts +139 -14
  56. package/corpus/templates/calendar/server/onboarding.ts +85 -0
  57. package/corpus/templates/calendar/server/plugins/agent-chat.ts +2 -0
  58. package/corpus/templates/calendar/shared/api.ts +9 -1
  59. package/corpus/templates/chat/.agents/skills/secrets/SKILL.md +11 -4
  60. package/corpus/templates/chat/app/routes.ts +3 -1
  61. package/corpus/templates/chat/vitest.config.ts +8 -0
  62. package/corpus/templates/clips/.agents/skills/secrets/SKILL.md +11 -4
  63. package/corpus/templates/clips/actions/get-meeting.ts +9 -4
  64. package/corpus/templates/clips/actions/list-recordings.ts +16 -3
  65. package/corpus/templates/clips/actions/search-meetings.ts +1 -0
  66. package/corpus/templates/clips/actions/view-screen.ts +8 -1
  67. package/corpus/templates/clips/app/components/meetings/meeting-history-row.tsx +32 -1
  68. package/corpus/templates/clips/app/components/player/delete-recording-menu.tsx +1 -1
  69. package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +29 -1
  70. package/corpus/templates/clips/app/i18n/en-US.ts +5 -0
  71. package/corpus/templates/clips/app/routes/_app.meetings.$meetingId.tsx +33 -4
  72. package/corpus/templates/clips/app/routes/_app.meetings._index.tsx +1 -0
  73. package/corpus/templates/clips/app/routes/access-request.approve.tsx +16 -9
  74. package/corpus/templates/clips/app/routes/embed.$shareId.tsx +17 -7
  75. package/corpus/templates/clips/app/routes/share.$shareId.tsx +17 -7
  76. package/corpus/templates/clips/changelog/2026-08-21-camera-bubble-black-circle.md +6 -0
  77. package/corpus/templates/clips/changelog/2026-08-22-screen-recordings-respect-the-microphone-toggle.md +6 -0
  78. package/corpus/templates/clips/changelog/2026-08-22-the-recordings-library-no-longer-fails-right-after-server-start.md +6 -0
  79. package/corpus/templates/clips/changelog/2026-08-23-the-agent-can-see-your-library-right-after-server-start.md +6 -0
  80. package/corpus/templates/clips/chrome-extension/src/background.ts +1 -2
  81. package/corpus/templates/clips/chrome-extension/src/native-transcription.ts +39 -7
  82. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +21 -25
  83. package/corpus/templates/clips/desktop/src/app.tsx +53 -24
  84. package/corpus/templates/clips/desktop/src/lib/bubble-playback.ts +68 -0
  85. package/corpus/templates/clips/desktop/src/lib/recorder.ts +3 -0
  86. package/corpus/templates/clips/desktop/src/lib/transcription-capture.ts +72 -14
  87. package/corpus/templates/clips/desktop/src/lib/updater.ts +8 -3
  88. package/corpus/templates/clips/desktop/src/overlays/bubble.tsx +162 -20
  89. package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +43 -6
  90. package/corpus/templates/clips/desktop/src-tauri/src/native_screen/custom_capture.rs +150 -13
  91. package/corpus/templates/clips/desktop/src-tauri/src/tray.rs +12 -0
  92. package/corpus/templates/clips/server/lib/calendar-event-meetings.ts +28 -3
  93. package/corpus/templates/clips/server/lib/emails.ts +17 -33
  94. package/corpus/templates/content/.agents/skills/secrets/SKILL.md +11 -4
  95. package/corpus/templates/content/app/components/editor/VisualEditor.tsx +140 -15
  96. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +12 -14
  97. package/corpus/templates/content/app/components/ui/pointer-lock.ts +1 -0
  98. package/corpus/templates/content/app/hooks/use-content-database.ts +18 -2
  99. package/corpus/templates/content/app/hooks/use-local-storage.ts +38 -22
  100. package/corpus/templates/content/app/i18n-data.ts +25 -0
  101. package/corpus/templates/content/app/routes.ts +3 -1
  102. package/corpus/templates/content/changelog/2026-08-22-code-blocks-inserted-from-the-slash-menu-no-longer-get-stuck.md +6 -0
  103. package/corpus/templates/content/changelog/2026-08-22-the-sidebar-page-tree-no-longer-flashes-empty-after-creating-a-page.md +6 -0
  104. package/corpus/templates/content/docs/solutions/2026-08-21-content-mermaid-rendering-shape.md +233 -0
  105. package/corpus/templates/content/server/plugins/agent-chat.ts +9 -2
  106. package/corpus/templates/content/vitest.config.ts +7 -1
  107. package/corpus/templates/crm/.agents/skills/secrets/SKILL.md +11 -4
  108. package/corpus/templates/design/.agents/skills/secrets/SKILL.md +11 -4
  109. package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +1383 -88
  110. package/corpus/templates/design/actions/generate-design.ts +98 -47
  111. package/corpus/templates/design/actions/get-design-system.ts +6 -1
  112. package/corpus/templates/design/actions/refresh-design-system-with-builder.ts +146 -0
  113. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +70 -26
  114. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +284 -136
  115. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +2057 -141
  116. package/corpus/templates/design/app/components/design/editor/DesignBottomToolbar.tsx +6 -2
  117. package/corpus/templates/design/app/components/design/editor/DesignWorkspaceRail.tsx +4 -4
  118. package/corpus/templates/design/app/components/design/keyboard-shortcuts.ts +10 -1
  119. package/corpus/templates/design/app/components/design/multi-screen/alt-hover-measurement.ts +19 -2
  120. package/corpus/templates/design/app/components/design/multi-screen/types.ts +9 -2
  121. package/corpus/templates/design/app/components/layout/Header.tsx +15 -0
  122. package/corpus/templates/design/app/components/layout/Layout.tsx +7 -2
  123. package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +11 -4
  124. package/corpus/templates/design/app/i18n-data.ts +1 -0
  125. package/corpus/templates/design/app/i18n-keyboard-shortcuts.ts +12 -0
  126. package/corpus/templates/design/app/lib/design-system-data.ts +68 -0
  127. package/corpus/templates/design/app/pages/DesignEditor.tsx +77 -35
  128. package/corpus/templates/design/app/pages/DesignSystems.tsx +136 -42
  129. package/corpus/templates/design/app/pages/Index.tsx +2 -0
  130. package/corpus/templates/design/app/pages/Templates.tsx +0 -10
  131. package/corpus/templates/design/app/pages/design-editor/code-layer-state.ts +10 -0
  132. package/corpus/templates/design/app/pages/design-editor/collab-sync.ts +50 -0
  133. package/corpus/templates/design/app/pages/design-editor/commands/apply-design-editor-command.ts +1 -1
  134. package/corpus/templates/design/app/pages/design-editor/commands/apply-file-content-update.ts +10 -8
  135. package/corpus/templates/design/app/pages/design-editor/commands/apply-local-content-update.ts +6 -6
  136. package/corpus/templates/design/app/pages/design-editor/commands/apply-pending-visual-styles-with-agent.ts +1 -1
  137. package/corpus/templates/design/app/pages/design-editor/commands/commit-visual-styles.ts +18 -7
  138. package/corpus/templates/design/app/pages/design-editor/commands/escape-hotkey.ts +4 -90
  139. package/corpus/templates/design/app/pages/design-editor/commands/layer-marquee-selection-change.ts +2 -1
  140. package/corpus/templates/design/app/pages/design-editor/commands/layer-selection-change.ts +4 -3
  141. package/corpus/templates/design/app/pages/design-editor/commands/screen-element-select.ts +12 -2
  142. package/corpus/templates/design/app/pages/design-editor/commands/screen-visual-style-change.ts +9 -2
  143. package/corpus/templates/design/app/pages/design-editor/commands/send-runtime-layer-move-semantic-handoff.ts +1 -1
  144. package/corpus/templates/design/app/pages/design-editor/commands/send-runtime-layer-semantic-handoff.ts +1 -1
  145. package/corpus/templates/design/app/pages/design-editor/commands/send-runtime-layer-state-semantic-handoff.ts +1 -1
  146. package/corpus/templates/design/app/pages/design-editor/commands/start-sidebar-resize.ts +1 -1
  147. package/corpus/templates/design/app/pages/design-editor/editor-state.ts +11 -1
  148. package/corpus/templates/design/app/pages/design-editor/effects/adopt-db-file-content.ts +11 -19
  149. package/corpus/templates/design/app/pages/design-editor/effects/observe-collab-text.ts +6 -6
  150. package/corpus/templates/design/app/pages/design-editor/effects/publish-agent-selection-context.ts +1 -1
  151. package/corpus/templates/design/app/pages/design-editor/effects/seed-collab-content.ts +12 -15
  152. package/corpus/templates/design/app/pages/design-editor/selection-state.ts +5 -59
  153. package/corpus/templates/design/app/pages/design-editor/tool-state.ts +8 -0
  154. package/corpus/templates/design/app/pages/design-editor/types.ts +3 -3
  155. package/corpus/templates/design/app/routes.ts +3 -1
  156. package/corpus/templates/design/changelog/2026-08-20-canvas-edits-patch-the-frame-instead-of-rebuilding-it.md +6 -0
  157. package/corpus/templates/design/changelog/2026-08-21-builder-design-system-imports-reconcile-after-indexing.md +6 -0
  158. package/corpus/templates/design/changelog/2026-08-21-draw-mode-needs-shift-y.md +6 -0
  159. package/corpus/templates/design/changelog/2026-08-21-eyedropper-shortcut.md +6 -0
  160. package/corpus/templates/design/changelog/2026-08-21-figma-style-alignment-guides-and-snapping.md +6 -0
  161. package/corpus/templates/design/changelog/2026-08-21-group-transforms-keep-the-selection.md +6 -0
  162. package/corpus/templates/design/changelog/2026-08-21-inspector-fields-keep-focus-on-enter.md +6 -0
  163. package/corpus/templates/design/changelog/2026-08-21-pen-alt-keeps-the-incoming-handle.md +6 -0
  164. package/corpus/templates/design/changelog/2026-08-21-pen-on-the-board-creates-one-vector.md +6 -0
  165. package/corpus/templates/design/changelog/2026-08-21-scale-a-multi-selection-from-the-group-handles.md +6 -0
  166. package/corpus/templates/design/changelog/2026-08-21-scale-resizes-text-with-the-object.md +6 -0
  167. package/corpus/templates/design/changelog/2026-08-21-selecting-another-object-keeps-the-scale-tool-armed.md +6 -0
  168. package/corpus/templates/design/changelog/2026-08-21-the-shape-tool-remembers-the-shape-you-picked.md +6 -0
  169. package/corpus/templates/design/changelog/2026-08-21-vectors-drawn-in-a-frame-land-where-you-drew-them.md +6 -0
  170. package/corpus/templates/design/changelog/2026-08-22-multi-screen-generation-no-longer-loses-a-saved-screens-layo.md +6 -0
  171. package/corpus/templates/design/changelog/2026-08-22-rename-and-search-fields-are-labeled-for-screen-readers.md +6 -0
  172. package/corpus/templates/design/package.json +2 -0
  173. package/corpus/templates/design/server/lib/builder-design-system-proxy.ts +311 -0
  174. package/corpus/templates/design/shared/canvas-math.ts +612 -206
  175. package/corpus/templates/design/shared/pen-path.ts +37 -0
  176. package/corpus/templates/design/vitest.config.ts +7 -1
  177. package/corpus/templates/dispatch/.agents/skills/secrets/SKILL.md +11 -4
  178. package/corpus/templates/dispatch/actions/list-workspace-connections.ts +34 -3
  179. package/corpus/templates/dispatch/app/routes/integrations.tsx +55 -21
  180. package/corpus/templates/dispatch/app/routes.ts +3 -1
  181. package/corpus/templates/factory/.agents/skills/secrets/SKILL.md +11 -4
  182. package/corpus/templates/factory/.env.example +2 -7
  183. package/corpus/templates/factory/AGENTS.md +3 -0
  184. package/corpus/templates/factory/README.md +5 -3
  185. package/corpus/templates/factory/actions/create-factory.ts +16 -3
  186. package/corpus/templates/factory/actions/get-slack-feedback-context.ts +2 -0
  187. package/corpus/templates/factory/actions/get-triage-item.ts +2 -0
  188. package/corpus/templates/factory/actions/list-factory-audit.ts +116 -36
  189. package/corpus/templates/factory/actions/list-factory-automations.ts +2 -1
  190. package/corpus/templates/factory/actions/list-triage-items.ts +22 -19
  191. package/corpus/templates/factory/actions/run-factory-automation.ts +7 -3
  192. package/corpus/templates/factory/actions/save-factory-automation.ts +5 -2
  193. package/corpus/templates/factory/actions/save-triage-config.ts +2 -8
  194. package/corpus/templates/factory/actions/start-builder-for-item.ts +21 -0
  195. package/corpus/templates/factory/app/components/factory/FactoryAuditView.tsx +229 -297
  196. package/corpus/templates/factory/app/components/factory/FactorySettingsView.tsx +397 -247
  197. package/corpus/templates/factory/app/components/factory/FactorySourceSettingsGroup.tsx +33 -0
  198. package/corpus/templates/factory/app/components/factory/NewFactoryForm.tsx +202 -162
  199. package/corpus/templates/factory/app/i18n/en-US.ts +32 -4
  200. package/corpus/templates/factory/app/routes/factory.tsx +84 -11
  201. package/corpus/templates/factory/app/routes.ts +3 -1
  202. package/corpus/templates/factory/changelog/2026-08-21-activity-runs-use-automation-display-names.md +6 -0
  203. package/corpus/templates/factory/changelog/2026-08-21-create-and-settings-source-cards.md +6 -0
  204. package/corpus/templates/factory/changelog/2026-08-21-keep-automations-visible-while-running.md +6 -0
  205. package/corpus/templates/factory/changelog/2026-08-21-nested-factory-automations-can-poll.md +6 -0
  206. package/corpus/templates/factory/changelog/2026-08-21-settings-sticky-unsaved-bar.md +6 -0
  207. package/corpus/templates/factory/server/connectors/credentials.ts +10 -5
  208. package/corpus/templates/factory/server/lib/factory-audit-report.ts +412 -0
  209. package/corpus/templates/factory/server/lib/factory-config-reconcile.ts +321 -0
  210. package/corpus/templates/factory/server/lib/factory-scope.ts +25 -2
  211. package/corpus/templates/factory/server/lib/require-factory-automation.ts +13 -4
  212. package/corpus/templates/factory/server/plugins/factory-migrations.ts +148 -4
  213. package/corpus/templates/factory/server/plugins/factory-scheduler-job.ts +4 -6
  214. package/corpus/templates/factory/vitest.config.ts +17 -2
  215. package/corpus/templates/forms/.agents/skills/secrets/SKILL.md +11 -4
  216. package/corpus/templates/forms/app/routes/_app.settings.tsx +4 -1
  217. package/corpus/templates/forms/changelog/2026-08-22-the-extensions-link-now-opens-the-extensions-tab-instead-o.md +6 -0
  218. package/corpus/templates/forms/server/lib/public-form-ssr.ts +11 -1
  219. package/corpus/templates/forms/vitest.config.ts +1 -0
  220. package/corpus/templates/macros/.agents/skills/secrets/SKILL.md +11 -4
  221. package/corpus/templates/macros/app/routes.ts +3 -1
  222. package/corpus/templates/macros/vitest.config.ts +6 -1
  223. package/corpus/templates/mail/.agents/skills/secrets/SKILL.md +11 -4
  224. package/corpus/templates/mail/actions/manage-gmail-filters.ts +10 -2
  225. package/corpus/templates/mail/app/components/GoogleConnectBanner.tsx +100 -37
  226. package/corpus/templates/mail/app/components/email/IntegrationsSidebar.tsx +14 -2
  227. package/corpus/templates/mail/app/components/layout/AppLayout.tsx +66 -36
  228. package/corpus/templates/mail/app/hooks/use-google-auth.ts +2 -0
  229. package/corpus/templates/mail/app/lib/google-oauth-setup.ts +17 -0
  230. package/corpus/templates/mail/app/pages/InboxPage.tsx +69 -9
  231. package/corpus/templates/mail/app/routes/$.tsx +9 -0
  232. package/corpus/templates/mail/app/routes/$view.$threadId.tsx +23 -1
  233. package/corpus/templates/mail/app/routes/$view.tsx +47 -1
  234. package/corpus/templates/mail/app/routes.ts +3 -1
  235. package/corpus/templates/mail/changelog/2026-08-22-gmail-can-now-connect-with-the-shared-google-oauth.md +6 -0
  236. package/corpus/templates/mail/changelog/2026-08-22-gmail-filters-now-shows-the-real-error.md +6 -0
  237. package/corpus/templates/mail/changelog/2026-08-22-unknown-mail-urls-now-show-the-404-page.md +6 -0
  238. package/corpus/templates/mail/server/handlers/google-auth.ts +11 -2
  239. package/corpus/templates/mail/server/lib/google-auth.ts +92 -12
  240. package/corpus/templates/mail/server/onboarding.ts +66 -18
  241. package/corpus/templates/mail/vitest.config.ts +6 -1
  242. package/corpus/templates/plan/.agents/skills/secrets/SKILL.md +11 -4
  243. package/corpus/templates/plan/app/routes/settings.tsx +4 -1
  244. package/corpus/templates/plan/app/routes.ts +3 -1
  245. package/corpus/templates/plan/changelog/2026-08-22-the-extensions-link-now-opens-the-extensions-tab-instead-o.md +6 -0
  246. package/corpus/templates/plan/netlify.toml +1 -1
  247. package/corpus/templates/slides/.agents/skills/secrets/SKILL.md +11 -4
  248. package/corpus/templates/slides/actions/patch-deck.ts +79 -10
  249. package/corpus/templates/slides/actions/update-slide.ts +11 -1
  250. package/corpus/templates/slides/app/components/deck/SlideRenderer.tsx +14 -4
  251. package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +10 -52
  252. package/corpus/templates/slides/app/components/editor/GoogleDocImportHint.tsx +8 -53
  253. package/corpus/templates/slides/app/components/editor/GoogleDriveConnectionCta.tsx +9 -75
  254. package/corpus/templates/slides/app/components/editor/GoogleSlidesReferenceImport.tsx +8 -60
  255. package/corpus/templates/slides/app/components/editor/SlideContextToolbar.tsx +10 -2
  256. package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +19 -7
  257. package/corpus/templates/slides/app/lib/sanitize-slide-html.ts +49 -36
  258. package/corpus/templates/slides/app/routes/access-request.approve.tsx +16 -9
  259. package/corpus/templates/slides/app/routes.ts +3 -1
  260. package/corpus/templates/slides/changelog/2026-08-22-already-uploaded-chat-images-are-no-longer-dropped.md +6 -0
  261. package/corpus/templates/slides/changelog/2026-08-22-google-drive-and-slides-can-now-connect-with-the-shared-google-oauth.md +6 -0
  262. package/corpus/templates/slides/changelog/2026-08-22-multi-slide-repairs-now-report-slides-that-still-overflow.md +6 -0
  263. package/corpus/templates/slides/changelog/2026-08-22-update-slide-now-reports-which-batched-edit-was-skipped.md +6 -0
  264. package/corpus/templates/slides/server/handlers/google-docs-auth.ts +52 -1
  265. package/corpus/templates/slides/server/lib/chat-attachments.ts +29 -3
  266. package/corpus/templates/slides/server/lib/google-docs-oauth.ts +66 -16
  267. package/corpus/templates/slides/server/lib/slide-content-patch.ts +13 -2
  268. package/corpus/templates/slides/server/plugins/agent-chat.ts +5 -2
  269. package/corpus/templates/slides/tsconfig.json +5 -0
  270. package/corpus/templates/slides/vitest.config.ts +6 -1
  271. package/corpus/templates/tasks/.agents/skills/secrets/SKILL.md +11 -4
  272. package/corpus/templates/tasks/app/routes/settings.tsx +4 -1
  273. package/corpus/templates/tasks/app/routes.ts +3 -1
  274. package/corpus/templates/tasks/changelog/2026-08-22-the-extensions-link-now-opens-the-extensions-tab-instead-o.md +6 -0
  275. package/corpus/templates/tasks/vitest.config.ts +7 -1
  276. package/dist/action.d.ts +91 -0
  277. package/dist/action.js +50 -0
  278. package/dist/agent/abort-reasons.d.ts +38 -0
  279. package/dist/agent/abort-reasons.js +49 -0
  280. package/dist/agent/engine/ai-sdk-engine.js +8 -5
  281. package/dist/agent/engine/anthropic-engine.js +7 -5
  282. package/dist/agent/engine/first-event-timeout.d.ts +42 -17
  283. package/dist/agent/engine/first-event-timeout.js +67 -25
  284. package/dist/agent/production-agent.d.ts +23 -0
  285. package/dist/agent/production-agent.js +107 -102
  286. package/dist/agent/run-loop-with-resume.d.ts +2 -28
  287. package/dist/agent/run-loop-with-resume.js +25 -40
  288. package/dist/agent/run-manager.js +5 -4
  289. package/dist/agent/run-store.d.ts +55 -0
  290. package/dist/agent/run-store.js +124 -1
  291. package/dist/agent/tool-search.js +2 -1
  292. package/dist/agent/types.d.ts +9 -4
  293. package/dist/app-config/auth.d.ts +1 -0
  294. package/dist/app-config/auth.js +7 -0
  295. package/dist/app-config/configuration-error.d.ts +11 -0
  296. package/dist/app-config/configuration-error.js +14 -0
  297. package/dist/app-config/index.d.ts +1 -0
  298. package/dist/app-config/index.js +1 -0
  299. package/dist/app-config/integrations.d.ts +1 -0
  300. package/dist/app-config/integrations.js +10 -0
  301. package/dist/app-config/plugins.d.ts +34 -0
  302. package/dist/app-config/plugins.js +39 -0
  303. package/dist/app-config/run-lifecycle-invariants.d.ts +18 -9
  304. package/dist/app-config/run-lifecycle-invariants.js +18 -25
  305. package/dist/app-config/schema.d.ts +17 -0
  306. package/dist/app-config/schema.js +2 -0
  307. package/dist/chat-threads/schema-migrations.js +20 -0
  308. package/dist/chat-threads/store.js +21 -4
  309. package/dist/cli/native-dependencies.d.ts +4 -0
  310. package/dist/cli/native-dependencies.js +15 -0
  311. package/dist/client/EnvironmentBadge.d.ts +1 -1
  312. package/dist/client/EnvironmentBadge.js +40 -5
  313. package/dist/client/MultiTabAssistantChat.d.ts +1 -1
  314. package/dist/client/MultiTabAssistantChat.js +35 -14
  315. package/dist/client/agent-chat/index.d.ts +1 -1
  316. package/dist/client/agent-chat/index.js +1 -1
  317. package/dist/client/agent-page/use-jobs.d.ts +2 -1
  318. package/dist/client/agent-page/use-jobs.js +3 -5
  319. package/dist/client/agent-sidebar-state.js +3 -1
  320. package/dist/client/blocks/library/code-tabs.js +5 -0
  321. package/dist/client/blocks/library/code.js +1 -0
  322. package/dist/client/chat-first/apps-rail.js +2 -2
  323. package/dist/client/chat-model-groups.d.ts +12 -0
  324. package/dist/client/chat-model-groups.js +3 -2
  325. package/dist/client/integrations/GoogleProductLogo.d.ts +5 -0
  326. package/dist/client/integrations/GoogleProductLogo.js +45 -0
  327. package/dist/client/integrations/index.d.ts +2 -0
  328. package/dist/client/integrations/index.js +2 -0
  329. package/dist/client/integrations/workspace-provider-oauth.d.ts +8 -0
  330. package/dist/client/integrations/workspace-provider-oauth.js +13 -0
  331. package/dist/client/resources/mcp-integration-catalog.js +0 -29
  332. package/dist/client/settings/SettingsTabsPage.js +10 -6
  333. package/dist/client/sharing/ShareButton.js +11 -2
  334. package/dist/client/use-chat-models.d.ts +6 -0
  335. package/dist/client/use-chat-models.js +10 -0
  336. package/dist/client/use-db-sync.js +27 -9
  337. package/dist/collab/routes.d.ts +1 -1
  338. package/dist/collab/struct-routes.d.ts +1 -1
  339. package/dist/connections/catalog.d.ts +111 -9
  340. package/dist/connections/catalog.js +86 -30
  341. package/dist/db/create-get-db.js +18 -0
  342. package/dist/db/ddl-guard.d.ts +0 -4
  343. package/dist/db/ddl-guard.js +46 -0
  344. package/dist/deploy/build.d.ts +19 -0
  345. package/dist/deploy/build.js +190 -16
  346. package/dist/deploy/function-bundle.d.ts +45 -0
  347. package/dist/deploy/function-bundle.js +337 -0
  348. package/dist/deploy/workspace-deploy.js +19 -1
  349. package/dist/eject/workspace-connections.js +4 -0
  350. package/dist/email-catalog/registry.d.ts +4 -0
  351. package/dist/email-catalog/registry.js +79 -14
  352. package/dist/extensions/content-patch.js +14 -2
  353. package/dist/integrations/plugin.d.ts +10 -0
  354. package/dist/integrations/plugin.js +40 -6
  355. package/dist/jobs/actions/run-automation-now.d.ts +2 -1
  356. package/dist/jobs/actions/run-automation-now.js +11 -5
  357. package/dist/jobs/frontmatter.js +53 -2
  358. package/dist/jobs/run-now.d.ts +11 -1
  359. package/dist/jobs/run-now.js +42 -7
  360. package/dist/jobs/scheduler.d.ts +1 -0
  361. package/dist/jobs/scheduler.js +2 -1
  362. package/dist/localization/core-messages/ar-SA.js +3 -0
  363. package/dist/localization/core-messages/de-DE.js +3 -0
  364. package/dist/localization/core-messages/en-US.d.ts +3 -0
  365. package/dist/localization/core-messages/en-US.js +3 -0
  366. package/dist/localization/core-messages/es-ES.js +3 -0
  367. package/dist/localization/core-messages/fr-FR.js +3 -0
  368. package/dist/localization/core-messages/hi-IN.js +3 -0
  369. package/dist/localization/core-messages/ja-JP.js +3 -0
  370. package/dist/localization/core-messages/ko-KR.js +3 -0
  371. package/dist/localization/core-messages/pt-BR.js +3 -0
  372. package/dist/localization/core-messages/zh-CN.js +3 -0
  373. package/dist/localization/core-messages/zh-TW.js +3 -0
  374. package/dist/localization/core-messages.d.ts +3 -0
  375. package/dist/localization/default-messages.d.ts +2 -5
  376. package/dist/localization/default-messages.js +2 -5
  377. package/dist/mcp/build-server.d.ts +9 -0
  378. package/dist/mcp/build-server.js +37 -8
  379. package/dist/mcp-client/oauth-client.d.ts +7 -0
  380. package/dist/mcp-client/oauth-client.js +12 -1
  381. package/dist/oauth-tokens/index.d.ts +1 -1
  382. package/dist/oauth-tokens/index.js +1 -1
  383. package/dist/oauth-tokens/lifecycle.d.ts +11 -0
  384. package/dist/oauth-tokens/lifecycle.js +17 -0
  385. package/dist/oauth-tokens/store.js +7 -0
  386. package/dist/observability/posthog-ai.d.ts +15 -1
  387. package/dist/observability/posthog-ai.js +24 -12
  388. package/dist/observability/traces.js +158 -28
  389. package/dist/onboarding/app-profile.js +2 -2
  390. package/dist/onboarding/plugin.js +10 -2
  391. package/dist/onboarding/types.d.ts +2 -0
  392. package/dist/provider-api/actions/custom-provider-registration.d.ts +1 -1
  393. package/dist/provider-api/actions/provider-api.d.ts +2 -2
  394. package/dist/provider-api/index.js +2 -0
  395. package/dist/resources/handlers.d.ts +2 -2
  396. package/dist/resources/store.js +13 -4
  397. package/dist/scripts/agent-engines/list-agent-engines.js +29 -3
  398. package/dist/secrets/register-framework-secrets.js +1 -1
  399. package/dist/server/action-discovery.js +5 -0
  400. package/dist/server/agent-chat/action-filters-a2a.d.ts +27 -3
  401. package/dist/server/agent-chat/action-filters-a2a.js +48 -9
  402. package/dist/server/agent-chat-plugin.js +36 -9
  403. package/dist/server/agent-discovery.js +43 -20
  404. package/dist/server/auth.js +18 -16
  405. package/dist/server/beta-opt-out-html.js +45 -4
  406. package/dist/server/better-auth-instance.d.ts +2 -1
  407. package/dist/server/better-auth-instance.js +37 -7
  408. package/dist/server/builder-design-systems.d.ts +4 -1
  409. package/dist/server/builder-design-systems.js +87 -6
  410. package/dist/server/builder-oauth.d.ts +1 -7
  411. package/dist/server/builder-oauth.js +80 -163
  412. package/dist/server/core-routes-plugin.d.ts +29 -0
  413. package/dist/server/core-routes-plugin.js +100 -20
  414. package/dist/server/default-plugins.d.ts +18 -0
  415. package/dist/server/default-plugins.js +22 -0
  416. package/dist/server/edge.d.ts +1 -0
  417. package/dist/server/edge.js +1 -0
  418. package/dist/server/framework-request-handler.js +31 -3
  419. package/dist/server/google-oauth.d.ts +3 -0
  420. package/dist/server/google-oauth.js +3 -0
  421. package/dist/server/index.d.ts +4 -4
  422. package/dist/server/index.js +3 -3
  423. package/dist/server/onboarding-html.js +37 -3
  424. package/dist/server/realtime-token.d.ts +1 -1
  425. package/dist/server/workspace-provider-oauth.d.ts +6 -1
  426. package/dist/server/workspace-provider-oauth.js +72 -20
  427. package/dist/shared/agent-sidebar-url.d.ts +1 -0
  428. package/dist/shared/agent-sidebar-url.js +1 -0
  429. package/dist/shared/environment-lanes.d.ts +2 -0
  430. package/dist/shared/environment-lanes.js +3 -1
  431. package/dist/shared/index.d.ts +3 -1
  432. package/dist/shared/index.js +3 -1
  433. package/dist/shared/surface-visibility.d.ts +31 -0
  434. package/dist/shared/surface-visibility.js +57 -0
  435. package/dist/templates/chat/.agents/skills/secrets/SKILL.md +11 -4
  436. package/dist/templates/chat/app/routes.ts +3 -1
  437. package/dist/templates/chat/vitest.config.ts +8 -0
  438. package/dist/templates/default/.agents/skills/secrets/SKILL.md +11 -4
  439. package/dist/templates/default/app/routes.ts +3 -1
  440. package/dist/templates/headless/.agents/skills/secrets/SKILL.md +11 -4
  441. package/dist/templates/workspace-core/.agents/skills/secrets/SKILL.md +11 -4
  442. package/dist/tracking/registry.d.ts +11 -0
  443. package/dist/tracking/registry.js +5 -2
  444. package/dist/triggers/actions.js +10 -3
  445. package/dist/usage/store.js +23 -3
  446. package/dist/user-profile/actions/get-auth-methods.js +18 -5
  447. package/dist/vite/client.js +33 -7
  448. package/docs/content/actions-access-control.mdx +43 -4
  449. package/docs/content/actions-defining.mdx +16 -0
  450. package/docs/content/authentication.mdx +40 -21
  451. package/docs/content/environment-variables.mdx +1 -0
  452. package/docs/content/template-clips-capture-everywhere.mdx +29 -30
  453. package/docs/content/template-clips-developers.mdx +4 -4
  454. package/docs/content/template-clips.mdx +3 -1
  455. package/package.json +2 -2
  456. package/src/eject/workspace-connections.ts +4 -0
  457. package/src/templates/chat/.agents/skills/secrets/SKILL.md +11 -4
  458. package/src/templates/chat/app/routes.ts +3 -1
  459. package/src/templates/chat/vitest.config.ts +8 -0
  460. package/src/templates/default/.agents/skills/secrets/SKILL.md +11 -4
  461. package/src/templates/default/app/routes.ts +3 -1
  462. package/src/templates/headless/.agents/skills/secrets/SKILL.md +11 -4
  463. package/src/templates/workspace-core/.agents/skills/secrets/SKILL.md +11 -4
@@ -0,0 +1,68 @@
1
+ /**
2
+ * When the camera bubble may declare its WebRTC video dead.
3
+ *
4
+ * WKWebView refuses to start a MediaStream-backed `<video>` that has had no
5
+ * user gesture in its page, and pauses one again whenever the owning window
6
+ * loses on-screen area. Neither shows up as an ICE failure, so the bubble's
7
+ * transport can be perfectly connected while the user stares at WebKit's
8
+ * start-playback overlay — a black circle with a white triangle. The bubble
9
+ * retries `play()` on a heartbeat, and uses the predicates here to decide when
10
+ * retrying has stopped being plausible and the popover should be asked for the
11
+ * canvas pump instead.
12
+ */
13
+
14
+ /** Re-play cadence for the WKWebView pause/block trap — matches `bubble-pump.ts`. */
15
+ export const BUBBLE_PLAY_HEARTBEAT_MS = 2000;
16
+
17
+ /**
18
+ * How long a WebRTC track may sit without producing frames before we give up
19
+ * on it. Long enough to cover a slow first decode, short enough that the user
20
+ * is not left wondering whether the camera is broken.
21
+ */
22
+ export const BUBBLE_RENDER_GRACE_MS = 2500;
23
+
24
+ export type BubblePlaybackProbe = {
25
+ /** When the current track arrived, or `null` when there is no track yet. */
26
+ trackArrivedAt: number | null;
27
+ now: number;
28
+ paused: boolean;
29
+ /** `0` until WebKit has decoded a frame, so it is the honest render signal. */
30
+ videoWidth: number;
31
+ alreadyReported: boolean;
32
+ };
33
+
34
+ /**
35
+ * Whether WebRTC video is actually on screen. `videoWidth > 0` is the part
36
+ * that matters: a `<video>` reports `paused === false` the moment `play()`
37
+ * resolves, well before any frame is decoded, so "playing" alone is not proof
38
+ * the user can see anything.
39
+ */
40
+ export function isRenderingWebrtc(
41
+ probe: Pick<BubblePlaybackProbe, "paused" | "videoWidth">,
42
+ ): boolean {
43
+ return !probe.paused && probe.videoWidth > 0;
44
+ }
45
+
46
+ /**
47
+ * Whether a `playing` event may give the WebRTC `<video>` the visible surface.
48
+ *
49
+ * Once the bubble has asked for the canvas pump the popover stops sending, so
50
+ * a later `playing` is the tail of a dead stream. Taking the surface there
51
+ * would park a still frame over the pump's live output. Only a fresh track
52
+ * clears `fallbackRequested` and re-arms this.
53
+ */
54
+ export function shouldClaimWebrtcPath(input: {
55
+ fallbackRequested: boolean;
56
+ videoWidth: number;
57
+ }): boolean {
58
+ if (input.fallbackRequested) return false;
59
+ return input.videoWidth > 0;
60
+ }
61
+
62
+ /** Whether the bubble should ask the popover to fall back to the canvas pump. */
63
+ export function shouldReportUnrendered(probe: BubblePlaybackProbe): boolean {
64
+ if (probe.alreadyReported) return false;
65
+ if (probe.trackArrivedAt == null) return false;
66
+ if (isRenderingWebrtc(probe)) return false;
67
+ return probe.now - probe.trackArrivedAt >= BUBBLE_RENDER_GRACE_MS;
68
+ }
@@ -1672,6 +1672,9 @@ async function saveRecordingTranscript(
1672
1672
  fullText: text,
1673
1673
  segments: transcript.segments,
1674
1674
  source: transcript.source ?? "whisper",
1675
+ ...(transcript.failureReason
1676
+ ? { failureReason: transcript.failureReason }
1677
+ : {}),
1675
1678
  }),
1676
1679
  signal: AbortSignal.timeout(TRANSCRIPT_SAVE_TIMEOUT_MS),
1677
1680
  });
@@ -28,6 +28,8 @@ import {
28
28
  /** Grace period after stop for whisper to emit any flushed trailing finals. */
29
29
  const WHISPER_STOP_SETTLE_MS = 1500;
30
30
  const WEB_SPEECH_STOP_SETTLE_MS = 1200;
31
+ const WEB_SPEECH_RESTART_RETRY_BASE_MS = 400;
32
+ const WEB_SPEECH_MAX_RESTART_ATTEMPTS = 8;
31
33
 
32
34
  function wait(ms: number): Promise<void> {
33
35
  return new Promise((resolve) => window.setTimeout(resolve, ms));
@@ -42,6 +44,10 @@ export interface CapturedTranscript {
42
44
  segments: SourcedTranscriptSegment[];
43
45
  /** Source stored with `save-browser-transcript`. */
44
46
  source?: "web-speech" | "macos-native" | "whisper";
47
+ /** Set when capture died partway. Text plus a reason is what tells the server
48
+ * this transcript is truncated and has to be re-transcribed in the cloud;
49
+ * omitting it saves a partial capture as a complete one. */
50
+ failureReason?: string;
45
51
  }
46
52
 
47
53
  export interface TranscriptionCapture {
@@ -170,15 +176,54 @@ async function startBrowserTranscriptionCapture(): Promise<TranscriptionCapture
170
176
  const transcriptBuffer = createWebSpeechTranscriptBuffer();
171
177
  let stopResolver: ((value: CapturedTranscript) => void) | null = null;
172
178
  let settleTimer: ReturnType<typeof window.setTimeout> | null = null;
179
+ let restartTimer: ReturnType<typeof window.setTimeout> | null = null;
180
+ let restartFailures = 0;
181
+ let failureReason: string | null = null;
173
182
 
174
183
  const captured = (): CapturedTranscript => ({
175
184
  text: transcriptBuffer.text(),
176
185
  segments: [],
177
186
  source: "web-speech",
187
+ failureReason: failureReason ?? undefined,
178
188
  });
179
189
 
190
+ const clearRestartTimer = () => {
191
+ if (restartTimer === null) return;
192
+ window.clearTimeout(restartTimer);
193
+ restartTimer = null;
194
+ };
195
+
196
+ // Chrome ends the recognizer on its own roughly every minute, and always
197
+ // throws if start() is called synchronously from inside `onend`. Nothing
198
+ // starts, so no further `onend` arrives — the retry has to live here or the
199
+ // first failure ends transcription for the rest of the recording.
200
+ const scheduleRestart = (delayMs: number) => {
201
+ if (restartTimer !== null) return;
202
+ restartTimer = window.setTimeout(() => {
203
+ restartTimer = null;
204
+ if (disposed || stopped || paused) return;
205
+ try {
206
+ recognition.start();
207
+ restartFailures = 0;
208
+ } catch (err) {
209
+ const attempt = ++restartFailures;
210
+ if (attempt >= WEB_SPEECH_MAX_RESTART_ATTEMPTS) {
211
+ failureReason =
212
+ "Web Speech transcription stopped mid-recording and could not be restarted.";
213
+ console.warn(
214
+ "[clips-recorder] Web Speech transcription restart failed:",
215
+ err,
216
+ );
217
+ return;
218
+ }
219
+ scheduleRestart(WEB_SPEECH_RESTART_RETRY_BASE_MS * attempt);
220
+ }
221
+ }, delayMs);
222
+ };
223
+
180
224
  const settleStop = () => {
181
225
  if (!stopResolver) return;
226
+ clearRestartTimer();
182
227
  if (settleTimer) {
183
228
  window.clearTimeout(settleTimer);
184
229
  settleTimer = null;
@@ -200,6 +245,12 @@ async function startBrowserTranscriptionCapture(): Promise<TranscriptionCapture
200
245
 
201
246
  recognition.onerror = (event) => {
202
247
  if (event.error === "no-speech" || event.error === "aborted") return;
248
+ // A non-benign error drops audio until `onend` restarts recognition. That
249
+ // restart usually succeeds, so without recording the gap here the transcript
250
+ // saves as complete and the server never schedules cloud transcription for
251
+ // the missing stretch. Deliberately not cleared by a later successful
252
+ // restart: recovering the engine does not recover the lost speech.
253
+ failureReason ??= `Web Speech transcription dropped audio after a "${event.error}" error.`;
203
254
  console.warn(
204
255
  "[clips-recorder] Web Speech transcription error:",
205
256
  event.error,
@@ -215,14 +266,7 @@ async function startBrowserTranscriptionCapture(): Promise<TranscriptionCapture
215
266
  }
216
267
  // While paused, keep the committed transcript but don't restart the engine.
217
268
  if (paused) return;
218
- try {
219
- recognition.start();
220
- } catch (err) {
221
- console.warn(
222
- "[clips-recorder] Web Speech transcription restart failed:",
223
- err,
224
- );
225
- }
269
+ scheduleRestart(0);
226
270
  };
227
271
 
228
272
  try {
@@ -250,6 +294,7 @@ async function startBrowserTranscriptionCapture(): Promise<TranscriptionCapture
250
294
  async cancel() {
251
295
  disposed = true;
252
296
  stopped = true;
297
+ clearRestartTimer();
253
298
  if (settleTimer) {
254
299
  window.clearTimeout(settleTimer);
255
300
  settleTimer = null;
@@ -273,14 +318,18 @@ async function startBrowserTranscriptionCapture(): Promise<TranscriptionCapture
273
318
  async resume() {
274
319
  if (disposed || stopped || !paused) return;
275
320
  paused = false;
321
+ restartFailures = 0;
276
322
  console.log("[clips-recorder] transcription resumed (web-speech)");
277
323
  try {
278
324
  recognition.start();
279
- } catch (err) {
280
- console.warn(
281
- "[clips-recorder] Web Speech transcription resume failed:",
282
- err,
283
- );
325
+ } catch {
326
+ // `paused` is already false, so the recording is live through the
327
+ // failed start and the retry delay. A later successful retry does not
328
+ // recover that speech, so the gap is recorded durably and the server
329
+ // still schedules cloud transcription.
330
+ failureReason ??=
331
+ "Web Speech transcription dropped audio while resuming after a pause.";
332
+ scheduleRestart(WEB_SPEECH_RESTART_RETRY_BASE_MS);
284
333
  }
285
334
  },
286
335
  async resetTimeline() {
@@ -289,7 +338,10 @@ async function startBrowserTranscriptionCapture(): Promise<TranscriptionCapture
289
338
  };
290
339
  }
291
340
 
292
- export const __test = { createWebSpeechTranscriptBuffer };
341
+ export const __test = {
342
+ createWebSpeechTranscriptBuffer,
343
+ startBrowserTranscriptionCapture,
344
+ };
293
345
 
294
346
  /**
295
347
  * Local transcription opens a microphone capture of its own. System-only
@@ -321,6 +373,7 @@ export async function startTranscriptionCapture(
321
373
  // asynchronously. Track when those are expected to have landed so a stop
322
374
  // soon after a pause waits for them instead of dropping the last words.
323
375
  let pauseFinalsSettleUntil = 0;
376
+ let transitionFailure: string | null = null;
324
377
  const unlistens: UnlistenFn[] = [];
325
378
 
326
379
  const cleanup = () => {
@@ -342,6 +395,7 @@ export async function startTranscriptionCapture(
342
395
  text: transcriptFullText(lines),
343
396
  segments: transcriptSegments(lines),
344
397
  source: engine,
398
+ failureReason: transitionFailure ?? undefined,
345
399
  });
346
400
 
347
401
  let engine: TranscriptionEngine;
@@ -407,6 +461,7 @@ export async function startTranscriptionCapture(
407
461
  // reset it) so the next pause/resume toggle retries and converges, and
408
462
  // return early so we don't busy-loop re-applying a persistently failing
409
463
  // transition. `paused` still reflects the real engine state.
464
+ transitionFailure = `Local transcription ${desiredPaused ? "pause" : "resume"} failed; engine still ${paused ? "paused" : "live"}.`;
410
465
  console.warn(
411
466
  `[clips-recorder] transcription ${desiredPaused ? "pause" : "resume"} failed; engine still ${paused ? "paused" : "live"}:`,
412
467
  err,
@@ -416,6 +471,9 @@ export async function startTranscriptionCapture(
416
471
  } finally {
417
472
  transitioning = false;
418
473
  }
474
+ // Reached only when the transition landed, so a failure the next toggle
475
+ // converged on is no longer a reason to re-transcribe in the cloud.
476
+ transitionFailure = null;
419
477
  // Re-apply in case the desired state changed mid-transition.
420
478
  void applyAudioState();
421
479
  };
@@ -96,8 +96,9 @@ async function runCheck() {
96
96
  // made "Restart to update" land on a binary that was already out of date and
97
97
  // immediately ask to restart again — every check runs against the *running*
98
98
  // version, so it re-offers the staged version until a newer one ships.
99
- const staged =
100
- cachedStatus.state === "downloaded" ? cachedStatus.version : null;
99
+ const stagedStatus =
100
+ cachedStatus.state === "downloaded" ? cachedStatus : null;
101
+ const staged = stagedStatus?.version ?? null;
101
102
 
102
103
  lastCheckStartedAt = Date.now();
103
104
  checkInFlight = (async () => {
@@ -114,7 +115,11 @@ async function runCheck() {
114
115
  }
115
116
  } catch (err) {
116
117
  const message = err instanceof Error ? err.message : String(err);
117
- setStatus({ state: "error", message });
118
+ // A staged bundle is already on disk and `pendingUpdate` still points at
119
+ // it. Reporting the check failure here would replace the Restart banner
120
+ // with an error card that has no way back to it, and would make the next
121
+ // successful check re-download the same version from scratch.
122
+ setStatus(stagedStatus ?? { state: "error", message });
118
123
  } finally {
119
124
  checkInFlight = null;
120
125
  }
@@ -3,6 +3,14 @@ import { invoke } from "@tauri-apps/api/core";
3
3
  import { emit, listen } from "@tauri-apps/api/event";
4
4
  import { getCurrentWindow } from "@tauri-apps/api/window";
5
5
  import { useEffect, useRef, useState } from "react";
6
+
7
+ import {
8
+ BUBBLE_PLAY_HEARTBEAT_MS,
9
+ BUBBLE_RENDER_GRACE_MS,
10
+ shouldClaimWebrtcPath,
11
+ shouldReportUnrendered,
12
+ } from "../lib/bubble-playback";
13
+
6
14
  type BubbleSize = "small" | "medium";
7
15
 
8
16
  /**
@@ -41,10 +49,13 @@ type BubbleSize = "small" | "medium";
41
49
  * feature degrades gracefully if the WebRTC handshake fails.
42
50
  *
43
51
  * The bubble always sets up BOTH receivers on mount. Whichever path
44
- * delivers video first wins — the other stays passive. If the
45
- * popover's WebRTC handshake fails (ICE timeout / failed state), it
46
- * stops trying and the popover starts the canvas pump instead, at
47
- * which point the canvas path takes over seamlessly.
52
+ * delivers video first wins — the other stays passive. The popover
53
+ * starts the canvas pump instead when the WebRTC handshake fails (ICE
54
+ * timeout / failed state) OR when this bubble reports that a connected
55
+ * track never rendered (`clips:bubble-webrtc-unrendered`; see the
56
+ * playback watchdog below). Both signals are needed — ICE reaching
57
+ * `connected` says the transport works, never that WebKit painted a
58
+ * frame.
48
59
  *
49
60
  * # Hover controls (Loom-style)
50
61
  *
@@ -59,16 +70,29 @@ type BubbleSize = "small" | "medium";
59
70
  export function Bubble() {
60
71
  // Dual-path rendering: <video> for WebRTC, <canvas> for the legacy
61
72
  // JPEG stream. CSS stacks them in the same circle — whichever has a
62
- // stream / frames visible fills the same space. Starts with canvas
63
- // hidden via `data-path="webrtc"` on the root; once a WebRTC track
64
- // arrives we set it to "webrtc", and if WebRTC fails and canvas
65
- // frames start arriving we flip to "canvas".
73
+ // stream / frames visible fills the same space. `data-path` on the root
74
+ // hides the loser. It flips to "webrtc" only once the <video> actually
75
+ // renders a frame, and to "canvas" on the first JPEG frame.
66
76
  const videoRef = useRef<HTMLVideoElement | null>(null);
67
77
  const canvasRef = useRef<HTMLCanvasElement | null>(null);
68
78
  const firstFrameAtRef = useRef<number | null>(null);
69
- // Which transport delivered the most recent usable frame. Starts as
70
- // "none" — we flip to "webrtc" on ontrack, or "canvas" on the first
71
- // JPEG frame, whichever lands first.
79
+ const firstTrackAtRef = useRef<number | null>(null);
80
+ // When the current WebRTC track arrived, so the playback watchdog can tell
81
+ // "no track yet" from "track arrived and WebKit still isn't rendering it".
82
+ const trackArrivedAtRef = useRef<number | null>(null);
83
+ // Assigned by the playback watchdog effect; called from `ontrack`, which
84
+ // lives in a different effect and must not own the retry policy.
85
+ const attemptPlayRef = useRef<() => void>(() => {});
86
+ // True once we've told the popover this track is dead and asked for the
87
+ // canvas pump. Shared across both effects: the watchdog sets it, and only a
88
+ // fresh track clears it, so the surface can't be handed back to a stream the
89
+ // popover has already stopped sending.
90
+ const fallbackRequestedRef = useRef(false);
91
+ // Which transport last put pixels on screen. Starts as "none"; the
92
+ // playback watchdog flips it to "webrtc" on a `playing` event with real
93
+ // frame dimensions, and the canvas sink flips it on the first JPEG frame.
94
+ // A track merely arriving is NOT enough — claiming "webrtc" there hid the
95
+ // canvas fallback exactly when WebKit had refused to play the video.
72
96
  const [activePath, setActivePath] = useState<"none" | "webrtc" | "canvas">(
73
97
  "none",
74
98
  );
@@ -153,6 +177,121 @@ export function Bubble() {
153
177
  }
154
178
  };
155
179
 
180
+ // ---- playback watchdog --------------------------------------------------
181
+ // WKWebView will not start a MediaStream-backed `<video>` that has had no
182
+ // user gesture in the page, and pauses one again whenever its window loses
183
+ // on-screen area. One `play()` at `ontrack` time therefore leaves the
184
+ // element parked behind WebKit's start-playback overlay — the black circle
185
+ // with a white triangle users report — and nothing ever retries it. The
186
+ // popover's offscreen pump video already defends itself the same way (see
187
+ // `bubble-pump.ts`); this is the bubble's copy of that defense.
188
+ //
189
+ // It also owns the `activePath` transition: only a `playing` event with real
190
+ // frame dimensions proves WebRTC is on screen. Flipping on `ontrack` instead
191
+ // hid the canvas fallback exactly when it was needed.
192
+ useEffect(() => {
193
+ const video = videoRef.current;
194
+ if (!video) return;
195
+ let stopped = false;
196
+ let lastFailure: string | null = null;
197
+
198
+ const attemptPlay = () => {
199
+ if (stopped) return;
200
+ if (!video.srcObject || !video.paused) return;
201
+ video.play().catch((err: unknown) => {
202
+ // Name and message, not the raw object: `console.warn` on a
203
+ // DOMException logs only its stack, which cannot tell NotAllowedError
204
+ // (playback blocked) from AbortError (load interrupted) — the two
205
+ // point at different bugs. Log each distinct reason once so the retry
206
+ // loop cannot bury the rest of the log.
207
+ const name = (err as { name?: string } | null)?.name ?? "unknown";
208
+ const message =
209
+ (err as { message?: string } | null)?.message ?? String(err);
210
+ const signature = `${name}: ${message}`;
211
+ if (lastFailure === signature) return;
212
+ lastFailure = signature;
213
+ console.warn("[bubble] video.play() rejected —", signature);
214
+ });
215
+ };
216
+ attemptPlayRef.current = attemptPlay;
217
+
218
+ const maybeClaimWebrtcPath = () => {
219
+ const claims = shouldClaimWebrtcPath({
220
+ fallbackRequested: fallbackRequestedRef.current,
221
+ videoWidth: video.videoWidth,
222
+ });
223
+ if (!claims) return;
224
+ setActivePath((current) => {
225
+ if (current !== "webrtc") {
226
+ console.log(
227
+ "[bubble] webrtc playback started %dx%d",
228
+ video.videoWidth,
229
+ video.videoHeight,
230
+ );
231
+ }
232
+ return "webrtc";
233
+ });
234
+ };
235
+
236
+ const onPlaying = () => {
237
+ if (stopped) return;
238
+ lastFailure = null;
239
+ maybeClaimWebrtcPath();
240
+ };
241
+
242
+ video.addEventListener("playing", onPlaying);
243
+ video.addEventListener("loadedmetadata", attemptPlay);
244
+ video.addEventListener("canplay", attemptPlay);
245
+ video.addEventListener("pause", attemptPlay);
246
+ document.addEventListener("visibilitychange", attemptPlay);
247
+
248
+ // Heartbeat: covers the resume cases no media event fires for (window
249
+ // shrunk to no visible area and back, a gesture landing on a control
250
+ // rather than the video). `play()` on an already-playing element is a
251
+ // no-op, so this is cheap. It doubles as the unrendered detector — if a
252
+ // track has been sitting there for BUBBLE_RENDER_GRACE_MS with no frames,
253
+ // tell the popover so it can start the proven canvas pump instead of
254
+ // leaving the user with a dead circle.
255
+ const heartbeat = setInterval(() => {
256
+ if (stopped) return;
257
+ attemptPlay();
258
+ maybeClaimWebrtcPath();
259
+ const unrendered = shouldReportUnrendered({
260
+ trackArrivedAt: trackArrivedAtRef.current,
261
+ now: Date.now(),
262
+ paused: video.paused,
263
+ videoWidth: video.videoWidth,
264
+ alreadyReported: fallbackRequestedRef.current,
265
+ });
266
+ if (!unrendered) return;
267
+ fallbackRequestedRef.current = true;
268
+ console.warn(
269
+ "[bubble] webrtc track is not rendering after %dms (paused=%o readyState=%d videoWidth=%d) — asking popover for the canvas pump",
270
+ BUBBLE_RENDER_GRACE_MS,
271
+ video.paused,
272
+ video.readyState,
273
+ video.videoWidth,
274
+ );
275
+ emit("clips:bubble-webrtc-unrendered", {
276
+ paused: video.paused,
277
+ readyState: video.readyState,
278
+ videoWidth: video.videoWidth,
279
+ lastPlayFailure: lastFailure,
280
+ }).catch(() => {});
281
+ }, BUBBLE_PLAY_HEARTBEAT_MS);
282
+
283
+ return () => {
284
+ stopped = true;
285
+ clearInterval(heartbeat);
286
+ video.removeEventListener("playing", onPlaying);
287
+ video.removeEventListener("loadedmetadata", attemptPlay);
288
+ video.removeEventListener("canplay", attemptPlay);
289
+ video.removeEventListener("pause", attemptPlay);
290
+ document.removeEventListener("visibilitychange", attemptPlay);
291
+ attemptPlayRef.current = () => {};
292
+ };
293
+ }, []);
294
+
156
295
  // ---- WebRTC receiver ----------------------------------------------------
157
296
  // Sets up a fresh RTCPeerConnection on mount, emits `bubble-ready`
158
297
  // so the popover knows to start the handshake, and renegotiates on
@@ -187,6 +326,7 @@ export function Bubble() {
187
326
  };
188
327
 
189
328
  function teardownPeer() {
329
+ trackArrivedAtRef.current = null;
190
330
  if (pc) {
191
331
  // Detach the incoming video <video>.srcObject BEFORE closing the
192
332
  // peer — otherwise WKWebView's media pipeline keeps the decoder
@@ -272,17 +412,19 @@ export function Bubble() {
272
412
  const incomingStream = ev.streams[0];
273
413
  if (!incomingStream) return;
274
414
  videoEl.srcObject = incomingStream;
275
- videoEl.play().catch((err) => {
276
- // Autoplay might be blocked in some WebKit corners —
277
- // `muted` + `playsInline` in JSX should be enough, but log
278
- // if it trips so we know to investigate.
279
- console.warn("[bubble] video.play() rejected", err);
280
- });
281
- if (firstFrameAtRef.current == null) {
282
- firstFrameAtRef.current = Date.now();
415
+ // Do NOT flip `activePath` here. A track arriving proves the
416
+ // transport works, not that WebKit will render it — see the
417
+ // playback watchdog below, which owns that transition and the
418
+ // fallback when playback never starts.
419
+ trackArrivedAtRef.current = Date.now();
420
+ // A fresh track is a fresh chance: re-arm the watchdog so it can both
421
+ // grant this one the surface and report it dead in turn.
422
+ fallbackRequestedRef.current = false;
423
+ attemptPlayRef.current();
424
+ if (firstTrackAtRef.current == null) {
425
+ firstTrackAtRef.current = Date.now();
283
426
  console.log("[bubble] first webrtc track received");
284
427
  }
285
- setActivePath("webrtc");
286
428
  };
287
429
 
288
430
  try {
@@ -1275,11 +1275,13 @@ pub async fn resize_popover(app: AppHandle, height: f64, width: Option<f64>) ->
1275
1275
  return Ok(());
1276
1276
  }
1277
1277
  if let Some(w) = app.get_webview_window("popover") {
1278
- let max_logical_height = w
1278
+ let monitor = w
1279
1279
  .current_monitor()
1280
1280
  .ok()
1281
1281
  .flatten()
1282
- .or_else(|| w.primary_monitor().ok().flatten())
1282
+ .or_else(|| w.primary_monitor().ok().flatten());
1283
+ let max_logical_height = monitor
1284
+ .as_ref()
1283
1285
  .map(|monitor| {
1284
1286
  let scale = monitor.scale_factor().max(1.0);
1285
1287
  ((monitor.size().height as f64) / scale
@@ -1288,16 +1290,39 @@ pub async fn resize_popover(app: AppHandle, height: f64, width: Option<f64>) ->
1288
1290
  .clamp(260.0, 820.0)
1289
1291
  })
1290
1292
  .unwrap_or(820.0);
1293
+ // Same idea as height: a monitor narrower than the requested width
1294
+ // (e.g. settings' 720) must not let the window grow past the screen
1295
+ // edge, since `position_popover`'s x-clamp can only slide a
1296
+ // too-wide window, not shrink it back onto the display.
1297
+ let max_logical_width = monitor
1298
+ .map(|monitor| {
1299
+ let scale = monitor.scale_factor().max(1.0);
1300
+ ((monitor.size().width as f64) / scale - 16.0 - POPOVER_SHADOW_GUTTER_LOGICAL * 2.0)
1301
+ .clamp(320.0, 960.0)
1302
+ })
1303
+ .unwrap_or(960.0);
1291
1304
  let clamped = height.clamp(200.0, max_logical_height);
1292
- let width = width.unwrap_or(320.0).clamp(320.0, 960.0);
1305
+ let width = width
1306
+ .unwrap_or(320.0)
1307
+ .clamp(320.0, max_logical_width.max(320.0));
1293
1308
  let (window_width, window_height) = popover_window_size_logical(width, clamped);
1294
1309
  let _ = w.set_size(tauri::Size::Logical(tauri::LogicalSize::new(
1295
1310
  window_width,
1296
1311
  window_height,
1297
1312
  )));
1298
1313
  // Re-anchor to the tray icon so the window doesn't drift below the
1299
- // bottom of the monitor after a growth.
1300
- position_popover(&app, &w);
1314
+ // bottom of the monitor after a growth. Pass the size we just asked
1315
+ // for rather than letting `position_popover` re-read `outer_size()`:
1316
+ // macOS doesn't always commit `set_size()` synchronously, so a
1317
+ // stale (pre-resize) read here would center/clamp against the old,
1318
+ // narrower width and let the window balloon past the screen edge
1319
+ // once the real resize lands a moment later.
1320
+ let target_scale = w.scale_factor().unwrap_or(1.0).max(1.0);
1321
+ let target_physical = PhysicalSize::new(
1322
+ (window_width * target_scale).round() as u32,
1323
+ (window_height * target_scale).round() as u32,
1324
+ );
1325
+ position_popover_with_size(&app, &w, target_physical);
1301
1326
  }
1302
1327
  Ok(())
1303
1328
  }
@@ -2668,13 +2693,25 @@ pub fn toggle_popover(app: &AppHandle) {
2668
2693
  }
2669
2694
 
2670
2695
  pub fn position_popover(app: &AppHandle, window: &WebviewWindow) {
2696
+ let win_size = window.outer_size().unwrap_or(PhysicalSize::new(360, 440));
2697
+ position_popover_with_size(app, window, win_size);
2698
+ }
2699
+
2700
+ /// Same as `position_popover`, but takes the window's size explicitly instead
2701
+ /// of querying `window.outer_size()`. Callers that just called `set_size()`
2702
+ /// must pass the size they requested — see the comment at that call site in
2703
+ /// `resize_popover` for why re-querying there is unsafe.
2704
+ pub fn position_popover_with_size(
2705
+ app: &AppHandle,
2706
+ window: &WebviewWindow,
2707
+ win_size: PhysicalSize<u32>,
2708
+ ) {
2671
2709
  // If we have a recent tray icon rect, anchor the popover's top edge just
2672
2710
  // below the icon and center it horizontally on the icon — same feel as
2673
2711
  // Loom / Raycast / 1Password.
2674
2712
  let anchor = app.state::<TrayAnchor>();
2675
2713
  let tray_rect = anchor.0.lock().ok().and_then(|g| *g);
2676
2714
 
2677
- let win_size: PhysicalSize<u32> = window.outer_size().unwrap_or(PhysicalSize::new(360, 440));
2678
2715
  // IMPORTANT: `current_monitor()` returns None when the window is offscreen
2679
2716
  // (we park it at 99999,99999 on boot to hide the initial flash). Fall back
2680
2717
  // to the primary monitor so we can still position correctly on first show.