@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
@@ -73,6 +73,101 @@ import { emit, listen, type UnlistenFn } from "@tauri-apps/api/event";
73
73
 
74
74
  const CONNECT_TIMEOUT_MS = 3000;
75
75
 
76
+ // ---- bitrate tuning for the loopback preview ----------------------------
77
+ //
78
+ // This is a SAME-MACHINE loopback (popover → bubble, both on 127.0.0.1),
79
+ // so there is effectively no bandwidth limit. Left untuned, WebKit's
80
+ // congestion controller still starts the encoder at libwebrtc's cautious
81
+ // ~300 kbps default and ramps up over several seconds, and with the
82
+ // default `balanced` degradation preference it downscales the picture
83
+ // while the bitrate is low. That produced the "bubble is blurry for the
84
+ // first ~10s, then sharpens" regression after the bubble stopped owning a
85
+ // direct local camera stream. We pin a high start/max bitrate and forbid
86
+ // resolution downscaling so the bubble is crisp from the first frame.
87
+ //
88
+ // 720p webcam is comfortable at a few Mbps; these are generous, not silly.
89
+ const BUBBLE_START_BITRATE_KBPS = 2500;
90
+ const BUBBLE_MIN_BITRATE_KBPS = 1200;
91
+ const BUBBLE_MAX_BITRATE_KBPS = 5000;
92
+
93
+ /**
94
+ * Pin the sender to a high, fixed bitrate and forbid resolution
95
+ * downscaling. Called after `setLocalDescription` so `encodings` is
96
+ * populated on every WebKit build. Best-effort — older WebViews may not
97
+ * support every field, so we swallow failures.
98
+ */
99
+ async function configureBubbleSender(
100
+ sender: RTCRtpSender | null,
101
+ ): Promise<void> {
102
+ if (!sender) return;
103
+ try {
104
+ const params = sender.getParameters();
105
+ if (!params.encodings || params.encodings.length === 0) {
106
+ // Some WebKit builds report empty encodings until the first
107
+ // negotiation — seed one so our settings have somewhere to land.
108
+ params.encodings = [{}];
109
+ }
110
+ params.encodings[0].maxBitrate = BUBBLE_MAX_BITRATE_KBPS * 1000;
111
+ params.encodings[0].maxFramerate = 30;
112
+ // `minBitrate` is non-standard but honored by libwebrtc-based stacks
113
+ // (WebKit, Chromium); harmless where unsupported.
114
+ (params.encodings[0] as { minBitrate?: number }).minBitrate =
115
+ BUBBLE_MIN_BITRATE_KBPS * 1000;
116
+ // Never trade resolution for framerate — a soft bubble looks broken;
117
+ // a marginally lower fps does not.
118
+ params.degradationPreference = "maintain-resolution";
119
+ await sender.setParameters(params);
120
+ } catch (err) {
121
+ console.warn("[clips-bubble-webrtc] setParameters failed", err);
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Raise the encoder's START bitrate via SDP so it doesn't begin at
127
+ * libwebrtc's cautious default and slowly climb (the visible quality
128
+ * ramp). WebKit reads these codec params from our own local description
129
+ * and applies them to our encoder. Also stamps a high `b=AS`/`b=TIAS`
130
+ * ceiling on the video m-section.
131
+ */
132
+ function boostBubbleVideoBitrate(sdp: string | undefined): string {
133
+ if (!sdp) return sdp ?? "";
134
+ const eol = sdp.includes("\r\n") ? "\r\n" : "\n";
135
+ const lines = sdp.split(/\r\n|\n/);
136
+ const out: string[] = [];
137
+ let inVideo = false;
138
+ for (const line of lines) {
139
+ if (line.startsWith("m=")) {
140
+ inVideo = line.startsWith("m=video");
141
+ out.push(line);
142
+ continue;
143
+ }
144
+ if (inVideo && line.startsWith("c=")) {
145
+ out.push(line);
146
+ // b=AS is kbps; b=TIAS is bps. Both must follow the c= line.
147
+ out.push(`b=AS:${BUBBLE_MAX_BITRATE_KBPS}`);
148
+ out.push(`b=TIAS:${BUBBLE_MAX_BITRATE_KBPS * 1000}`);
149
+ continue;
150
+ }
151
+ if (
152
+ inVideo &&
153
+ line.startsWith("a=fmtp:") &&
154
+ // Skip RTX/FEC payloads (their fmtp carries `apt=`); only primary
155
+ // codecs honor the x-google-* hints.
156
+ !line.includes("apt=") &&
157
+ !line.includes("x-google-start-bitrate")
158
+ ) {
159
+ out.push(
160
+ `${line};x-google-start-bitrate=${BUBBLE_START_BITRATE_KBPS}` +
161
+ `;x-google-min-bitrate=${BUBBLE_MIN_BITRATE_KBPS}` +
162
+ `;x-google-max-bitrate=${BUBBLE_MAX_BITRATE_KBPS}`,
163
+ );
164
+ continue;
165
+ }
166
+ out.push(line);
167
+ }
168
+ return out.join(eol);
169
+ }
170
+
76
171
  export interface BubbleWebrtcHandle {
77
172
  /** Tear down the peer connection and unsubscribe listeners. */
78
173
  stop(): void;
@@ -243,11 +338,23 @@ export function startBubbleWebrtc(
243
338
  }
244
339
  };
245
340
 
341
+ // Hint the encoder to PRESERVE RESOLUTION. A raw camera track defaults
342
+ // to "motion" semantics — under any bitrate pressure WebKit downscales
343
+ // the picture to protect the frame rate, which on this loopback shows
344
+ // up as a soft/blurry bubble. "detail" flips the default toward keeping
345
+ // pixels, complementing the explicit `maintain-resolution` set below.
346
+ try {
347
+ videoTrack.contentHint = "detail";
348
+ } catch {
349
+ // contentHint is a harmless no-op on older WebViews.
350
+ }
351
+
246
352
  // Add the camera video track. WebRTC will renegotiate if the track
247
353
  // id changes, but we only add once per handshake so this is a
248
- // one-shot.
354
+ // one-shot. Capture the sender so we can pin its bitrate below.
355
+ let sender: RTCRtpSender | null = null;
249
356
  try {
250
- localPc.addTrack(videoTrack, stream);
357
+ sender = localPc.addTrack(videoTrack, stream);
251
358
  } catch (err) {
252
359
  console.warn("[clips-bubble-webrtc] addTrack failed:", err);
253
360
  onFailure("add-track-failed");
@@ -258,6 +365,10 @@ export function startBubbleWebrtc(
258
365
  let offer: RTCSessionDescriptionInit;
259
366
  try {
260
367
  offer = await localPc.createOffer();
368
+ // Raise the encoder's start/max bitrate before we commit the local
369
+ // description so the bubble is sharp immediately instead of ramping
370
+ // up over ~10s. See `boostBubbleVideoBitrate`.
371
+ offer = { ...offer, sdp: boostBubbleVideoBitrate(offer.sdp) };
261
372
  await localPc.setLocalDescription(offer);
262
373
  } catch (err) {
263
374
  console.warn(
@@ -270,6 +381,12 @@ export function startBubbleWebrtc(
270
381
  }
271
382
  if (stopped || myId !== handshakeId) return;
272
383
 
384
+ // Now that the local description is set, `getParameters().encodings`
385
+ // is populated on every WebKit build — pin the bitrate floor/ceiling
386
+ // and forbid resolution downscaling.
387
+ await configureBubbleSender(sender);
388
+ if (stopped || myId !== handshakeId) return;
389
+
273
390
  try {
274
391
  await emit("clips:webrtc-offer", {
275
392
  handshakeId: myId,
@@ -1473,6 +1473,22 @@ async function showRegionGuidesForRecording(wantsScreen: boolean) {
1473
1473
  });
1474
1474
  }
1475
1475
 
1476
+ // Frame the chosen screen region with a live border so the user can see exactly
1477
+ // what's being captured throughout the countdown and recording. The overlay is
1478
+ // capture-excluded and draws its stroke outside the captured pixels, so it never
1479
+ // lands in the video. Torn down with the rest of the recording chrome on stop.
1480
+ async function showRegionRecordBorder(region: RegionCaptureRect | null) {
1481
+ if (!region) return;
1482
+ await invoke("show_region_record_border", {
1483
+ x: region.x,
1484
+ y: region.y,
1485
+ width: region.width,
1486
+ height: region.height,
1487
+ }).catch((err) => {
1488
+ console.warn("[clips-recorder] show_region_record_border failed:", err);
1489
+ });
1490
+ }
1491
+
1476
1492
  async function runRecordingCountdown(wantsScreen: boolean, audioCue: AudioCue) {
1477
1493
  const countdownEvent = waitForCountdownEvent(
1478
1494
  COUNTDOWN_EVENT_TIMEOUT_MS,
@@ -1566,6 +1582,10 @@ async function startNativeFullscreenRecording(
1566
1582
 
1567
1583
  if (params.source === "region") {
1568
1584
  captureRegion = await selectRegionForRecording();
1585
+ // Frame the selected area now so it stays visible through the countdown
1586
+ // and the whole recording. hide_recording_chrome / hide_overlays tear it
1587
+ // down on stop, cancel, and the error paths below.
1588
+ await showRegionRecordBorder(captureRegion);
1569
1589
  }
1570
1590
 
1571
1591
  if (localOnly && localRecordingMode === "separate" && wantsCamera) {
@@ -12,6 +12,7 @@ import { MeetingNub } from "./overlays/meeting-nub";
12
12
  import { FlowBar } from "./overlays/flow-bar";
13
13
  import { RecordingPill } from "./overlays/recording-pill";
14
14
  import { RegionGuideEditor, RegionGuides } from "./overlays/region-guides";
15
+ import { RegionRecordBorder } from "./overlays/region-record-border";
15
16
  import "./styles.css";
16
17
 
17
18
  /**
@@ -55,6 +56,8 @@ function pickRoute(route: string): React.ReactElement {
55
56
  return <RegionGuideEditor />;
56
57
  case "region-capture-selector":
57
58
  return <RegionGuideEditor mode="capture" />;
59
+ case "region-record-border":
60
+ return <RegionRecordBorder />;
58
61
  default:
59
62
  return <App />;
60
63
  }
@@ -1,6 +1,7 @@
1
1
  import { useCallback, useEffect, useRef, useState } from "react";
2
2
  import { emit } from "@tauri-apps/api/event";
3
3
  import { getCurrentWindow } from "@tauri-apps/api/window";
4
+ import { IconPlayerSkipForwardFilled, IconX } from "@tabler/icons-react";
4
5
 
5
6
  const COUNTDOWN_STEP_MS = 1000;
6
7
 
@@ -62,8 +63,26 @@ export function Countdown() {
62
63
 
63
64
  return (
64
65
  <div className="countdown-root">
65
- <div className="countdown-number" key={n} aria-live="polite">
66
- {n > 0 ? n : ""}
66
+ <div className="countdown-controls">
67
+ <button
68
+ type="button"
69
+ className="countdown-control countdown-control-cancel"
70
+ aria-label="Cancel recording"
71
+ onClick={() => closeWithEvent("clips:countdown-cancel")}
72
+ >
73
+ <IconX size={30} stroke={2.4} />
74
+ </button>
75
+ <div className="countdown-number" key={n} aria-live="polite">
76
+ {n > 0 ? n : ""}
77
+ </div>
78
+ <button
79
+ type="button"
80
+ className="countdown-control countdown-control-skip"
81
+ aria-label="Skip countdown and start recording now"
82
+ onClick={() => closeWithEvent("clips:countdown-done")}
83
+ >
84
+ <IconPlayerSkipForwardFilled size={28} />
85
+ </button>
67
86
  </div>
68
87
  <div className="countdown-hint" aria-label="Countdown shortcuts">
69
88
  <span>
@@ -0,0 +1,76 @@
1
+ import { useEffect, useState } from "react";
2
+ import { getCurrentWindow } from "@tauri-apps/api/window";
3
+
4
+ interface BorderRect {
5
+ x: number;
6
+ y: number;
7
+ width: number;
8
+ height: number;
9
+ }
10
+
11
+ function clamp01(value: number): number {
12
+ if (!Number.isFinite(value)) return 0;
13
+ return Math.min(1, Math.max(0, value));
14
+ }
15
+
16
+ /**
17
+ * The live recording region is handed to this overlay window as a normalized
18
+ * rect in the URL query (`?region=x,y,width,height`) so it never has to wait
19
+ * on a Tauri event (which could race the webview load). All four values are
20
+ * fractions [0,1] of the recording monitor — the same space the region-capture
21
+ * selector emitted them in.
22
+ */
23
+ function parseRect(search: string): BorderRect | null {
24
+ const raw = new URLSearchParams(search).get("region");
25
+ if (!raw) return null;
26
+ const parts = raw.split(",").map((value) => Number.parseFloat(value));
27
+ if (parts.length !== 4 || parts.some((value) => !Number.isFinite(value))) {
28
+ return null;
29
+ }
30
+ const [x, y, width, height] = parts;
31
+ if (width <= 0 || height <= 0) return null;
32
+ return {
33
+ x: clamp01(x),
34
+ y: clamp01(y),
35
+ width: clamp01(width),
36
+ height: clamp01(height),
37
+ };
38
+ }
39
+
40
+ /**
41
+ * A thin frame painted around the screen region currently being recorded. The
42
+ * window itself is full-screen, click-through, and capture-excluded (see
43
+ * `show_region_record_border` on the Rust side). The frame is drawn entirely
44
+ * OUTWARD via an outset box-shadow with no fill, so its pixels sit just outside
45
+ * the captured rect and never leak into the recording even on the macOS 15.4+
46
+ * builds where `NSWindowSharingNone` is occasionally bypassed.
47
+ */
48
+ export function RegionRecordBorder() {
49
+ const [rect] = useState<BorderRect | null>(() =>
50
+ typeof window === "undefined" ? null : parseRect(window.location.search),
51
+ );
52
+
53
+ useEffect(() => {
54
+ if (rect) return;
55
+ // No usable region — close the overlay so an empty window never lingers.
56
+ getCurrentWindow()
57
+ .close()
58
+ .catch(() => {});
59
+ }, [rect]);
60
+
61
+ if (!rect) return null;
62
+
63
+ return (
64
+ <div className="region-record-border-layer" aria-hidden>
65
+ <div
66
+ className="region-record-border-frame"
67
+ style={{
68
+ left: `${rect.x * 100}%`,
69
+ top: `${rect.y * 100}%`,
70
+ width: `${rect.width * 100}%`,
71
+ height: `${rect.height * 100}%`,
72
+ }}
73
+ />
74
+ </div>
75
+ );
76
+ }
@@ -2037,6 +2037,70 @@ body[data-clips-route="recording-pill"] #root {
2037
2037
  pointer-events: none;
2038
2038
  }
2039
2039
 
2040
+ /*
2041
+ * Horizontal row of [cancel] [number] [skip]. The two circular buttons are
2042
+ * absolutely centered at exactly ±200px (logical) from the row center so the
2043
+ * native cursor-poll hit-rects in clips/mod.rs (CONTROL_OFFSET_X = 200.0,
2044
+ * CONTROL_DIAMETER = 64.0) line up with the rendered controls. Keep these
2045
+ * numbers in sync with that Rust code.
2046
+ */
2047
+ .countdown-controls {
2048
+ position: relative;
2049
+ display: flex;
2050
+ align-items: center;
2051
+ justify-content: center;
2052
+ }
2053
+
2054
+ .countdown-control {
2055
+ position: absolute;
2056
+ top: 50%;
2057
+ display: inline-flex;
2058
+ align-items: center;
2059
+ justify-content: center;
2060
+ width: 64px;
2061
+ height: 64px;
2062
+ margin: 0;
2063
+ padding: 0;
2064
+ border: 1px solid rgba(255, 255, 255, 0.22);
2065
+ border-radius: 50%;
2066
+ background: rgba(10, 12, 16, 0.42);
2067
+ box-shadow: 0 10px 28px rgba(0, 0, 0, 0.28);
2068
+ color: #ffffff;
2069
+ cursor: pointer;
2070
+ pointer-events: auto;
2071
+ backdrop-filter: blur(8px);
2072
+ transform: translateY(-50%);
2073
+ transition:
2074
+ background-color 0.15s ease,
2075
+ border-color 0.15s ease,
2076
+ transform 0.1s ease;
2077
+ }
2078
+
2079
+ .countdown-control-cancel {
2080
+ /* center at -200px from row center: -200 - (64 / 2) = -232px */
2081
+ left: -232px;
2082
+ }
2083
+
2084
+ .countdown-control-skip {
2085
+ /* center at +200px from row center: 200 - (64 / 2) = 168px */
2086
+ right: -232px;
2087
+ }
2088
+
2089
+ .countdown-control:hover {
2090
+ background: rgba(255, 255, 255, 0.92);
2091
+ border-color: rgba(255, 255, 255, 0.95);
2092
+ color: #0b0d12;
2093
+ }
2094
+
2095
+ .countdown-control:active {
2096
+ transform: translateY(-50%) scale(0.94);
2097
+ }
2098
+
2099
+ .countdown-control:focus-visible {
2100
+ outline: 2px solid rgba(255, 255, 255, 0.9);
2101
+ outline-offset: 2px;
2102
+ }
2103
+
2040
2104
  .countdown-number {
2041
2105
  font-size: 240px;
2042
2106
  font-weight: 800;
@@ -3749,6 +3813,55 @@ body[data-clips-route="recording-pill"] #root {
3749
3813
  backdrop-filter: blur(14px);
3750
3814
  }
3751
3815
 
3816
+ /* ------------------------------------------------------------------------- */
3817
+ /* Overlay: live recording-region border */
3818
+ /* ------------------------------------------------------------------------- */
3819
+
3820
+ .region-record-border-layer {
3821
+ position: fixed;
3822
+ inset: 0;
3823
+ width: 100vw;
3824
+ height: 100vh;
3825
+ overflow: hidden;
3826
+ pointer-events: none;
3827
+ }
3828
+
3829
+ .region-record-border-frame {
3830
+ position: absolute;
3831
+ border-radius: 5px;
3832
+ /* No fill, and the stroke is painted OUTWARD with an outset box-shadow so it
3833
+ lands just outside the captured rect — never inside the recorded video. */
3834
+ box-shadow:
3835
+ 0 0 0 2px rgba(239, 68, 68, 0.95),
3836
+ 0 0 0 4px rgba(239, 68, 68, 0.32),
3837
+ 0 0 26px 6px rgba(239, 68, 68, 0.26);
3838
+ animation: region-record-border-pulse 1.8s ease-in-out infinite;
3839
+ }
3840
+
3841
+ /* Gentle pulse that mirrors the live recording dot, signalling "recording
3842
+ now". Honour reduced-motion by holding the frame steady. */
3843
+ @keyframes region-record-border-pulse {
3844
+ 0%,
3845
+ 100% {
3846
+ box-shadow:
3847
+ 0 0 0 2px rgba(239, 68, 68, 0.9),
3848
+ 0 0 0 4px rgba(239, 68, 68, 0.28),
3849
+ 0 0 22px 5px rgba(239, 68, 68, 0.2);
3850
+ }
3851
+ 50% {
3852
+ box-shadow:
3853
+ 0 0 0 2px rgba(239, 68, 68, 1),
3854
+ 0 0 0 4px rgba(239, 68, 68, 0.44),
3855
+ 0 0 30px 8px rgba(239, 68, 68, 0.34);
3856
+ }
3857
+ }
3858
+
3859
+ @media (prefers-reduced-motion: reduce) {
3860
+ .region-record-border-frame {
3861
+ animation: none;
3862
+ }
3863
+ }
3864
+
3752
3865
  @keyframes flow-bar-in {
3753
3866
  from {
3754
3867
  opacity: 0;
@@ -3799,11 +3912,15 @@ body[data-clips-route="recording-pill"] #root {
3799
3912
  }
3800
3913
 
3801
3914
  /* Collapsed (not expanded). Shrinks to dot + waveform; controls revealed via
3802
- CSS :hover which works at the WebKit level even when the app is unfocused. */
3915
+ CSS :hover which works at the WebKit level even when the app is unfocused.
3916
+ Top-anchored (not centered) inside the headroom-sized window so the hover
3917
+ reveal of the drag grip grows the capsule strictly DOWNWARD — centering it
3918
+ would split the growth and raise the top edge, sliding buttons up under the
3919
+ cursor and causing misclicks. */
3803
3920
  .pill-inner-compact {
3804
3921
  width: fit-content;
3805
3922
  height: fit-content;
3806
- align-self: center;
3923
+ align-self: flex-start;
3807
3924
  border-radius: 18px;
3808
3925
  }
3809
3926
 
@@ -15,7 +15,8 @@
15
15
  "flow-bar",
16
16
  "recording-pill",
17
17
  "region-guides",
18
- "region-guide-editor"
18
+ "region-guide-editor",
19
+ "region-record-border"
19
20
  ],
20
21
  "permissions": [
21
22
  "core:default",
@@ -4,6 +4,7 @@ use std::path::PathBuf;
4
4
  use std::process::Command;
5
5
  #[cfg(target_os = "macos")]
6
6
  use std::process::Stdio;
7
+ use std::sync::atomic::{AtomicBool, Ordering};
7
8
  use std::thread;
8
9
  use std::time::Duration;
9
10
  use tauri::{
@@ -26,11 +27,23 @@ use crate::util::{
26
27
  /// React bundle with a hash route that `main.tsx` uses to pick the component.
27
28
  const COUNTDOWN_LABEL: &str = "countdown";
28
29
  const TOOLBAR_LABEL: &str = "toolbar";
30
+ // Geometry of the two circular cancel/skip buttons that flank the countdown
31
+ // number. These MUST stay in sync with the CSS in
32
+ // `templates/clips/desktop/src/styles.css` (`.countdown-control` is 64px and
33
+ // its center sits ±200px logical from the window center). A few px of slop is
34
+ // added to each hit-rect so edge clicks register.
35
+ const COUNTDOWN_CONTROL_OFFSET_X: f64 = 200.0;
36
+ const COUNTDOWN_CONTROL_DIAMETER: f64 = 64.0;
37
+ const COUNTDOWN_CONTROL_HIT_PAD: f64 = 8.0;
38
+ // Guards the single cursor-poll loop that toggles click-through on the
39
+ // countdown overlay so only the button zones are interactive.
40
+ static COUNTDOWN_CONTROL_TRACKING: AtomicBool = AtomicBool::new(false);
29
41
  const BUBBLE_LABEL: &str = "bubble";
30
42
  const FINALIZING_LABEL: &str = "finalizing";
31
43
  const FLOW_BAR_LABEL: &str = "flow-bar";
32
44
  const REGION_GUIDES_LABEL: &str = "region-guides";
33
45
  const REGION_GUIDE_EDITOR_LABEL: &str = "region-guide-editor";
46
+ const REGION_RECORD_BORDER_LABEL: &str = "region-record-border";
34
47
 
35
48
  /// Physical-pixel bubble sizes. Logical px on retina = physical / 2, so these
36
49
  /// map to ~96 (small) and ~180 (medium) logical px — matching Loom's camera
@@ -303,6 +316,7 @@ pub async fn show_countdown(app: AppHandle) -> Result<(), String> {
303
316
  dlog!("[clips-tray] show_countdown invoked");
304
317
  mark_popover_shown(&app);
305
318
  if let Some(existing) = app.get_webview_window(COUNTDOWN_LABEL) {
319
+ stop_countdown_control_tracking();
306
320
  let _ = app.emit("clips:countdown-shortcuts-active", false);
307
321
  let _ = existing.close();
308
322
  }
@@ -339,10 +353,68 @@ pub async fn show_countdown(app: AppHandle) -> Result<(), String> {
339
353
  configure_overlay_behavior(&win);
340
354
  let _ = win.show();
341
355
  let _ = app.emit("clips:countdown-shortcuts-active", true);
356
+ start_countdown_control_tracking(&app);
342
357
  dlog!("[clips-tray] countdown shown");
343
358
  Ok(())
344
359
  }
345
360
 
361
+ /// True when the global cursor sits inside either circular cancel/skip button
362
+ /// zone of the countdown overlay. The controls row is centered in the window;
363
+ /// each button's center is `COUNTDOWN_CONTROL_OFFSET_X` logical px to the
364
+ /// left/right of the window center, vertically at the window center. Cursor,
365
+ /// window position, and window size all come from Tauri in physical px with a
366
+ /// desktop top-left origin, so we convert the logical button geometry using the
367
+ /// window's scale factor and do a plain point-in-rect test.
368
+ fn cursor_over_countdown_control(window: &WebviewWindow) -> bool {
369
+ let (Ok(c), Ok(p), Ok(s), Ok(scale)) = (
370
+ window.cursor_position(),
371
+ window.outer_position(),
372
+ window.outer_size(),
373
+ window.scale_factor(),
374
+ ) else {
375
+ return false;
376
+ };
377
+ let center_x = p.x as f64 + s.width as f64 / 2.0;
378
+ let center_y = p.y as f64 + s.height as f64 / 2.0;
379
+ let half = (COUNTDOWN_CONTROL_DIAMETER / 2.0 + COUNTDOWN_CONTROL_HIT_PAD) * scale;
380
+ let offset = COUNTDOWN_CONTROL_OFFSET_X * scale;
381
+ let in_button = |bx: f64| -> bool {
382
+ c.x >= bx - half && c.x <= bx + half && c.y >= center_y - half && c.y <= center_y + half
383
+ };
384
+ in_button(center_x - offset) || in_button(center_x + offset)
385
+ }
386
+
387
+ /// Poll the cursor against the two button zones while the countdown overlay is
388
+ /// alive, toggling `set_ignore_cursor_events` so the buttons are clickable only
389
+ /// when the cursor is over them and the rest of the screen stays click-through.
390
+ /// Idempotent; mirrors `start_pill_hover_tracking` in `recording_indicator.rs`.
391
+ fn start_countdown_control_tracking(app: &AppHandle) {
392
+ if COUNTDOWN_CONTROL_TRACKING.swap(true, Ordering::SeqCst) {
393
+ return;
394
+ }
395
+ let app = app.clone();
396
+ tauri::async_runtime::spawn(async move {
397
+ let mut prev_interactive = false;
398
+ while COUNTDOWN_CONTROL_TRACKING.load(Ordering::Relaxed) {
399
+ let Some(win) = app.get_webview_window(COUNTDOWN_LABEL) else {
400
+ break;
401
+ };
402
+ let over = cursor_over_countdown_control(&win);
403
+ if over != prev_interactive {
404
+ prev_interactive = over;
405
+ // ignore_cursor_events(false) => clicks land on the buttons.
406
+ let _ = win.set_ignore_cursor_events(!over);
407
+ }
408
+ tokio::time::sleep(Duration::from_millis(70)).await;
409
+ }
410
+ COUNTDOWN_CONTROL_TRACKING.store(false, Ordering::SeqCst);
411
+ });
412
+ }
413
+
414
+ fn stop_countdown_control_tracking() {
415
+ COUNTDOWN_CONTROL_TRACKING.store(false, Ordering::SeqCst);
416
+ }
417
+
346
418
  /// Full-screen transparent overlay that shows compact bottom-left progress
347
419
  /// while the recorder flushes its final chunks and awaits the server finalize.
348
420
  /// Rendered immediately after the user clicks Stop so they don't stare at a
@@ -448,6 +520,65 @@ pub async fn hide_region_guides(app: AppHandle) -> Result<(), String> {
448
520
  Ok(())
449
521
  }
450
522
 
523
+ /// Live border framing the region currently being recorded. Like the region
524
+ /// guides this is a full-screen, click-through, capture-excluded overlay — but
525
+ /// the rect is ephemeral (it belongs to one recording, not the saved preset),
526
+ /// so it's handed in via the URL query rather than read from config. The React
527
+ /// view paints only an OUTWARD frame so the stroke stays out of the captured
528
+ /// pixels. The recording flow owns this window; it's torn down by
529
+ /// `hide_recording_chrome` / `hide_overlays` when capture stops.
530
+ #[tauri::command]
531
+ pub async fn show_region_record_border(
532
+ app: AppHandle,
533
+ x: f64,
534
+ y: f64,
535
+ width: f64,
536
+ height: f64,
537
+ ) -> Result<(), String> {
538
+ if !(width > 0.0 && height > 0.0) {
539
+ return Ok(());
540
+ }
541
+ if let Some(existing) = app.get_webview_window(REGION_RECORD_BORDER_LABEL) {
542
+ let _ = existing.close();
543
+ }
544
+
545
+ let (mx, my, mw, mh) = tray_monitor_physical_rect(&app);
546
+ let url = WebviewUrl::App(
547
+ format!("index.html?region={x:.6},{y:.6},{width:.6},{height:.6}#region-record-border")
548
+ .into(),
549
+ );
550
+ let win = WebviewWindowBuilder::new(&app, REGION_RECORD_BORDER_LABEL, url)
551
+ .title("Clips Recording Region")
552
+ .decorations(false)
553
+ .transparent(true)
554
+ .always_on_top(true)
555
+ .skip_taskbar(true)
556
+ .resizable(false)
557
+ .shadow(false)
558
+ .visible(false)
559
+ .focused(false)
560
+ .build()
561
+ .map_err(|e| {
562
+ eprintln!("[clips-tray] region record border build failed: {}", e);
563
+ e.to_string()
564
+ })?;
565
+ let _ = win.set_size(tauri::Size::Physical(PhysicalSize::new(mw, mh)));
566
+ let _ = win.set_position(PhysicalPosition::new(mx, my));
567
+ let _ = win.set_ignore_cursor_events(true);
568
+ set_capture_excluded_always(&win);
569
+ configure_overlay_behavior(&win);
570
+ crate::util::show_without_activation(&win);
571
+ Ok(())
572
+ }
573
+
574
+ #[tauri::command]
575
+ pub async fn hide_region_record_border(app: AppHandle) -> Result<(), String> {
576
+ if let Some(w) = app.get_webview_window(REGION_RECORD_BORDER_LABEL) {
577
+ let _ = w.close();
578
+ }
579
+ Ok(())
580
+ }
581
+
451
582
  /// Reconcile the always-on region-guides overlay with the current config.
452
583
  /// Called from config saves and on startup so the guides window matches the
453
584
  /// `always_visible` toggle without needing a recording-flow round trip. When a
@@ -740,6 +871,7 @@ pub async fn set_bubble_capture_excluded(app: AppHandle, excluded: bool) -> Resu
740
871
 
741
872
  #[tauri::command]
742
873
  pub async fn hide_overlays(app: AppHandle) -> Result<(), String> {
874
+ stop_countdown_control_tracking();
743
875
  let _ = app.emit("clips:countdown-shortcuts-active", false);
744
876
  for label in [
745
877
  COUNTDOWN_LABEL,
@@ -748,6 +880,7 @@ pub async fn hide_overlays(app: AppHandle) -> Result<(), String> {
748
880
  FINALIZING_LABEL,
749
881
  FLOW_BAR_LABEL,
750
882
  REGION_GUIDES_LABEL,
883
+ REGION_RECORD_BORDER_LABEL,
751
884
  ] {
752
885
  if let Some(w) = app.get_webview_window(label) {
753
886
  let _ = w.close();
@@ -766,13 +899,16 @@ pub async fn hide_overlays(app: AppHandle) -> Result<(), String> {
766
899
  /// popover-close).
767
900
  #[tauri::command]
768
901
  pub async fn hide_recording_chrome(app: AppHandle) -> Result<(), String> {
902
+ stop_countdown_control_tracking();
769
903
  let _ = app.emit("clips:countdown-shortcuts-active", false);
770
904
  // The countdown + toolbar always tear down on recording stop. The region
771
905
  // guides only tear down when they aren't pinned on-screen via the always-on
772
906
  // toggle — otherwise we'd flicker close→reopen right after stop.
773
907
  let g = crate::config::feature_config(&app).region_guides;
774
908
  let keep_region_guides = g.always_visible && g.enabled && !g.rects.is_empty();
775
- let mut labels: Vec<&str> = vec![COUNTDOWN_LABEL, TOOLBAR_LABEL];
909
+ // The recording-region border belongs to a single recording (never pinned),
910
+ // so it always tears down here alongside the countdown + toolbar.
911
+ let mut labels: Vec<&str> = vec![COUNTDOWN_LABEL, TOOLBAR_LABEL, REGION_RECORD_BORDER_LABEL];
776
912
  if !keep_region_guides {
777
913
  labels.push(REGION_GUIDES_LABEL);
778
914
  }
@@ -1438,6 +1574,9 @@ pub async fn reset_state(app: AppHandle) -> Result<(), String> {
1438
1574
  if let Some(w) = app.get_webview_window(REGION_GUIDES_LABEL) {
1439
1575
  let _ = w.close();
1440
1576
  }
1577
+ if let Some(w) = app.get_webview_window(REGION_RECORD_BORDER_LABEL) {
1578
+ let _ = w.close();
1579
+ }
1441
1580
  if let Some(window) = app.get_webview_window("popover") {
1442
1581
  // Restore normal size in case the window was shrunk to a pinhole
1443
1582
  // during recording — otherwise it would reappear as a 2×2 dot.