@agent-native/core 0.88.0 → 0.90.2

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 (1866) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +96 -2
  3. package/corpus/core/docs/content/agent-native-toolkit.mdx +87 -146
  4. package/corpus/core/docs/content/cloneable-saas.mdx +19 -19
  5. package/corpus/core/docs/content/components.mdx +16 -4
  6. package/corpus/core/docs/content/data-programs.mdx +329 -0
  7. package/corpus/core/docs/content/deployment.mdx +9 -9
  8. package/corpus/core/docs/content/extensions.mdx +8 -7
  9. package/corpus/core/docs/content/generative-ui.mdx +5 -3
  10. package/corpus/core/docs/content/key-concepts.mdx +6 -9
  11. package/corpus/core/docs/content/locales/ar-SA/agent-native-toolkit.mdx +100 -70
  12. package/corpus/core/docs/content/locales/ar-SA/cloneable-saas.mdx +19 -19
  13. package/corpus/core/docs/content/locales/ar-SA/components.mdx +12 -2
  14. package/corpus/core/docs/content/locales/ar-SA/data-programs.mdx +329 -0
  15. package/corpus/core/docs/content/locales/ar-SA/deployment.mdx +8 -9
  16. package/corpus/core/docs/content/locales/ar-SA/extensions.mdx +6 -4
  17. package/corpus/core/docs/content/locales/ar-SA/key-concepts.mdx +7 -9
  18. package/corpus/core/docs/content/locales/ar-SA/real-time-collaboration.mdx +9 -10
  19. package/corpus/core/docs/content/locales/de-DE/agent-native-toolkit.mdx +100 -74
  20. package/corpus/core/docs/content/locales/de-DE/cloneable-saas.mdx +19 -19
  21. package/corpus/core/docs/content/locales/de-DE/components.mdx +12 -2
  22. package/corpus/core/docs/content/locales/de-DE/data-programs.mdx +329 -0
  23. package/corpus/core/docs/content/locales/de-DE/deployment.mdx +8 -9
  24. package/corpus/core/docs/content/locales/de-DE/extensions.mdx +6 -4
  25. package/corpus/core/docs/content/locales/de-DE/key-concepts.mdx +8 -10
  26. package/corpus/core/docs/content/locales/de-DE/real-time-collaboration.mdx +9 -10
  27. package/corpus/core/docs/content/locales/es-ES/agent-native-toolkit.mdx +100 -74
  28. package/corpus/core/docs/content/locales/es-ES/cloneable-saas.mdx +19 -19
  29. package/corpus/core/docs/content/locales/es-ES/components.mdx +12 -2
  30. package/corpus/core/docs/content/locales/es-ES/data-programs.mdx +329 -0
  31. package/corpus/core/docs/content/locales/es-ES/deployment.mdx +8 -9
  32. package/corpus/core/docs/content/locales/es-ES/extensions.mdx +6 -4
  33. package/corpus/core/docs/content/locales/es-ES/key-concepts.mdx +8 -10
  34. package/corpus/core/docs/content/locales/es-ES/real-time-collaboration.mdx +9 -10
  35. package/corpus/core/docs/content/locales/fr-FR/agent-native-toolkit.mdx +100 -76
  36. package/corpus/core/docs/content/locales/fr-FR/cloneable-saas.mdx +19 -19
  37. package/corpus/core/docs/content/locales/fr-FR/components.mdx +12 -2
  38. package/corpus/core/docs/content/locales/fr-FR/data-programs.mdx +329 -0
  39. package/corpus/core/docs/content/locales/fr-FR/deployment.mdx +8 -9
  40. package/corpus/core/docs/content/locales/fr-FR/extensions.mdx +6 -4
  41. package/corpus/core/docs/content/locales/fr-FR/key-concepts.mdx +8 -10
  42. package/corpus/core/docs/content/locales/fr-FR/real-time-collaboration.mdx +9 -10
  43. package/corpus/core/docs/content/locales/hi-IN/agent-native-toolkit.mdx +100 -71
  44. package/corpus/core/docs/content/locales/hi-IN/cloneable-saas.mdx +19 -19
  45. package/corpus/core/docs/content/locales/hi-IN/components.mdx +20 -10
  46. package/corpus/core/docs/content/locales/hi-IN/data-programs.mdx +329 -0
  47. package/corpus/core/docs/content/locales/hi-IN/deployment.mdx +8 -9
  48. package/corpus/core/docs/content/locales/hi-IN/extensions.mdx +6 -4
  49. package/corpus/core/docs/content/locales/hi-IN/key-concepts.mdx +8 -10
  50. package/corpus/core/docs/content/locales/hi-IN/real-time-collaboration.mdx +9 -10
  51. package/corpus/core/docs/content/locales/ja-JP/agent-native-toolkit.mdx +100 -64
  52. package/corpus/core/docs/content/locales/ja-JP/cloneable-saas.mdx +19 -19
  53. package/corpus/core/docs/content/locales/ja-JP/components.mdx +12 -2
  54. package/corpus/core/docs/content/locales/ja-JP/data-programs.mdx +329 -0
  55. package/corpus/core/docs/content/locales/ja-JP/deployment.mdx +8 -9
  56. package/corpus/core/docs/content/locales/ja-JP/extensions.mdx +6 -4
  57. package/corpus/core/docs/content/locales/ja-JP/key-concepts.mdx +9 -11
  58. package/corpus/core/docs/content/locales/ja-JP/real-time-collaboration.mdx +9 -10
  59. package/corpus/core/docs/content/locales/ko-KR/agent-native-toolkit.mdx +100 -60
  60. package/corpus/core/docs/content/locales/ko-KR/cloneable-saas.mdx +19 -19
  61. package/corpus/core/docs/content/locales/ko-KR/components.mdx +20 -10
  62. package/corpus/core/docs/content/locales/ko-KR/data-programs.mdx +329 -0
  63. package/corpus/core/docs/content/locales/ko-KR/deployment.mdx +8 -9
  64. package/corpus/core/docs/content/locales/ko-KR/extensions.mdx +16 -14
  65. package/corpus/core/docs/content/locales/ko-KR/key-concepts.mdx +9 -11
  66. package/corpus/core/docs/content/locales/ko-KR/real-time-collaboration.mdx +9 -10
  67. package/corpus/core/docs/content/locales/pt-BR/agent-native-toolkit.mdx +100 -74
  68. package/corpus/core/docs/content/locales/pt-BR/cloneable-saas.mdx +19 -19
  69. package/corpus/core/docs/content/locales/pt-BR/components.mdx +12 -2
  70. package/corpus/core/docs/content/locales/pt-BR/data-programs.mdx +329 -0
  71. package/corpus/core/docs/content/locales/pt-BR/deployment.mdx +8 -9
  72. package/corpus/core/docs/content/locales/pt-BR/extensions.mdx +6 -4
  73. package/corpus/core/docs/content/locales/pt-BR/key-concepts.mdx +8 -11
  74. package/corpus/core/docs/content/locales/pt-BR/real-time-collaboration.mdx +9 -10
  75. package/corpus/core/docs/content/locales/zh-CN/agent-native-toolkit.mdx +100 -58
  76. package/corpus/core/docs/content/locales/zh-CN/cloneable-saas.mdx +19 -19
  77. package/corpus/core/docs/content/locales/zh-CN/components.mdx +20 -10
  78. package/corpus/core/docs/content/locales/zh-CN/data-programs.mdx +329 -0
  79. package/corpus/core/docs/content/locales/zh-CN/deployment.mdx +7 -9
  80. package/corpus/core/docs/content/locales/zh-CN/extensions.mdx +16 -14
  81. package/corpus/core/docs/content/locales/zh-CN/key-concepts.mdx +7 -11
  82. package/corpus/core/docs/content/locales/zh-CN/real-time-collaboration.mdx +10 -11
  83. package/corpus/core/docs/content/locales/zh-TW/agent-native-toolkit.mdx +100 -58
  84. package/corpus/core/docs/content/locales/zh-TW/cloneable-saas.mdx +19 -19
  85. package/corpus/core/docs/content/locales/zh-TW/components.mdx +20 -10
  86. package/corpus/core/docs/content/locales/zh-TW/data-programs.mdx +329 -0
  87. package/corpus/core/docs/content/locales/zh-TW/deployment.mdx +7 -9
  88. package/corpus/core/docs/content/locales/zh-TW/extensions.mdx +16 -14
  89. package/corpus/core/docs/content/locales/zh-TW/key-concepts.mdx +7 -11
  90. package/corpus/core/docs/content/locales/zh-TW/real-time-collaboration.mdx +10 -11
  91. package/corpus/core/docs/content/real-time-collaboration.mdx +10 -11
  92. package/corpus/core/docs/content/toolkit-app-adapters.mdx +46 -0
  93. package/corpus/core/docs/content/toolkit-collaboration-ui.mdx +48 -0
  94. package/corpus/core/docs/content/toolkit-sharing-ui.mdx +33 -0
  95. package/corpus/core/docs/content/toolkit-shell-hooks.mdx +27 -0
  96. package/corpus/core/docs/content/toolkit-ui.mdx +69 -0
  97. package/corpus/core/package.json +5 -1
  98. package/corpus/core/src/agent/durable-background.ts +53 -0
  99. package/corpus/core/src/agent/engine/ai-sdk-engine.ts +7 -1
  100. package/corpus/core/src/agent/engine/anthropic-engine.ts +29 -1
  101. package/corpus/core/src/agent/engine/builder-engine.ts +1 -0
  102. package/corpus/core/src/agent/engine/output-tokens.ts +26 -13
  103. package/corpus/core/src/agent/engine/registry.ts +45 -26
  104. package/corpus/core/src/agent/engine/translate-ai-sdk.ts +56 -7
  105. package/corpus/core/src/agent/engine/translate-anthropic.ts +43 -1
  106. package/corpus/core/src/agent/engine/types.ts +16 -0
  107. package/corpus/core/src/agent/model-config.ts +100 -0
  108. package/corpus/core/src/agent/production-agent.ts +492 -241
  109. package/corpus/core/src/agent/runtime-context.ts +50 -12
  110. package/corpus/core/src/agent/tool-result-images.ts +178 -0
  111. package/corpus/core/src/brand-kit/fig/index.ts +8 -4
  112. package/corpus/core/src/client/AgentPanel.tsx +89 -32
  113. package/corpus/core/src/client/AssistantChat.tsx +95 -46
  114. package/corpus/core/src/client/KeepTabOpenNotice.tsx +176 -0
  115. package/corpus/core/src/client/MultiTabAssistantChat.tsx +7 -0
  116. package/corpus/core/src/client/RunStuckBanner.tsx +13 -1
  117. package/corpus/core/src/client/chat/run-recovery.tsx +7 -1
  118. package/corpus/core/src/client/clipboard.ts +17 -13
  119. package/corpus/core/src/client/components/AgentPresenceChip.tsx +4 -69
  120. package/corpus/core/src/client/components/LiveCursorOverlay.tsx +5 -240
  121. package/corpus/core/src/client/components/PresenceBar.tsx +4 -346
  122. package/corpus/core/src/client/components/RecentEditHighlights.tsx +4 -219
  123. package/corpus/core/src/client/components/RemoteSelectionRings.tsx +5 -220
  124. package/corpus/core/src/client/composer/TiptapComposer.tsx +94 -38
  125. package/corpus/core/src/client/db-admin/SqlEditor.tsx +13 -3
  126. package/corpus/core/src/client/dynamic-suggestions.ts +6 -1
  127. package/corpus/core/src/client/extensions/ExtensionViewer.tsx +0 -14
  128. package/corpus/core/src/client/extensions/ExtensionsListPage.tsx +29 -30
  129. package/corpus/core/src/client/extensions/ExtensionsSidebarSection.tsx +2 -1
  130. package/corpus/core/src/client/host-bridge.ts +29 -8
  131. package/corpus/core/src/client/i18n.tsx +7 -1
  132. package/corpus/core/src/client/index.ts +5 -0
  133. package/corpus/core/src/client/resources/McpIntegrationDialog.tsx +34 -69
  134. package/corpus/core/src/client/rich-markdown-editor/SharedRichEditor.tsx +4 -0
  135. package/corpus/core/src/client/rich-markdown-editor/useCollabReconcile.ts +18 -7
  136. package/corpus/core/src/client/settings/SettingsPanel.tsx +409 -230
  137. package/corpus/core/src/client/settings/SettingsTabsPage.tsx +5 -7
  138. package/corpus/core/src/client/settings/index.ts +5 -1
  139. package/corpus/core/src/client/sharing/VisibilityBadge.tsx +4 -37
  140. package/corpus/core/src/client/use-action.ts +39 -2
  141. package/corpus/core/src/client/use-db-sync.ts +44 -18
  142. package/corpus/core/src/client/use-run-stuck-detection.ts +10 -1
  143. package/corpus/core/src/coding-tools/run-code.ts +473 -74
  144. package/corpus/core/src/coding-tools/sandbox/background.ts +406 -0
  145. package/corpus/core/src/coding-tools/sandbox/executions-store.ts +517 -0
  146. package/corpus/core/src/coding-tools/sandbox/index.ts +53 -13
  147. package/corpus/core/src/coding-tools/sandbox/local-child-process-adapter.ts +61 -4
  148. package/corpus/core/src/collab/client.ts +870 -562
  149. package/corpus/core/src/collab/presence.ts +12 -23
  150. package/corpus/core/src/collab/recent-edits.ts +13 -32
  151. package/corpus/core/src/data-programs/actions.ts +335 -0
  152. package/corpus/core/src/data-programs/contract.ts +284 -0
  153. package/corpus/core/src/data-programs/execute.ts +693 -0
  154. package/corpus/core/src/data-programs/index.ts +72 -0
  155. package/corpus/core/src/data-programs/schema.ts +148 -0
  156. package/corpus/core/src/data-programs/store.ts +755 -0
  157. package/corpus/core/src/db/create-get-db.ts +32 -2
  158. package/corpus/core/src/deploy/build.ts +34 -0
  159. package/corpus/core/src/extensions/actions.ts +2 -2
  160. package/corpus/core/src/file-upload/builder.ts +11 -0
  161. package/corpus/core/src/integrations/webhook-handler.ts +27 -8
  162. package/corpus/core/src/local-artifacts/index.ts +1 -1
  163. package/corpus/core/src/localization/default-messages.ts +1 -2
  164. package/corpus/core/src/mcp-client/index.ts +29 -0
  165. package/corpus/core/src/onboarding/default-steps.ts +5 -2
  166. package/corpus/core/src/resources/store.ts +34 -3
  167. package/corpus/core/src/scripts/agent-engines/list-agent-engines.ts +4 -8
  168. package/corpus/core/src/scripts/agent-engines/test-agent-engine.ts +9 -2
  169. package/corpus/core/src/scripts/db/tool-mode.ts +2 -2
  170. package/corpus/core/src/scripts/resources/migrate-learnings.ts +9 -13
  171. package/corpus/core/src/server/agent-chat-plugin.ts +154 -62
  172. package/corpus/core/src/server/agent-discovery.ts +67 -7
  173. package/corpus/core/src/server/auth.ts +90 -3
  174. package/corpus/core/src/server/builder-design-systems.ts +2 -2
  175. package/corpus/core/src/server/core-routes-plugin.ts +119 -22
  176. package/corpus/core/src/server/credential-provider.ts +28 -8
  177. package/corpus/core/src/server/csrf.ts +3 -0
  178. package/corpus/core/src/server/framework-request-handler.ts +52 -4
  179. package/corpus/core/src/server/oauth-helpers.ts +11 -1
  180. package/corpus/core/src/server/onboarding-html.ts +80 -48
  181. package/corpus/core/src/server/prompts/shared-rules.ts +2 -2
  182. package/corpus/core/src/server/transcribe-voice.ts +93 -9
  183. package/corpus/core/src/styles/agent-native.css +57 -45
  184. package/corpus/core/src/templates/default/.agents/skills/actions/SKILL.md +55 -0
  185. package/corpus/core/src/templates/default/AGENTS.md +11 -14
  186. package/corpus/core/src/templates/default/actions/navigate.ts +1 -1
  187. package/corpus/core/src/templates/default/app/i18n/ar-SA.ts +0 -1
  188. package/corpus/core/src/templates/default/app/i18n/de-DE.ts +0 -1
  189. package/corpus/core/src/templates/default/app/i18n/en-US.ts +0 -1
  190. package/corpus/core/src/templates/default/app/i18n/es-ES.ts +0 -1
  191. package/corpus/core/src/templates/default/app/i18n/fr-FR.ts +0 -1
  192. package/corpus/core/src/templates/default/app/i18n/hi-IN.ts +0 -1
  193. package/corpus/core/src/templates/default/app/i18n/ja-JP.ts +0 -1
  194. package/corpus/core/src/templates/default/app/i18n/ko-KR.ts +0 -1
  195. package/corpus/core/src/templates/default/app/i18n/pt-BR.ts +0 -1
  196. package/corpus/core/src/templates/default/app/i18n/zh-CN.ts +0 -1
  197. package/corpus/core/src/templates/default/app/i18n/zh-TW.ts +0 -1
  198. package/corpus/core/src/templates/default/app/routes/settings.tsx +22 -33
  199. package/corpus/core/src/templates/headless/.agents/skills/agent-native-docs/SKILL.md +20 -7
  200. package/corpus/core/src/templates/headless/AGENTS.md +9 -6
  201. package/corpus/core/src/templates/workspace-core/.agents/skills/actions/SKILL.md +55 -0
  202. package/corpus/core/src/templates/workspace-core/.agents/skills/data-programs/SKILL.md +128 -0
  203. package/corpus/core/src/templates/workspace-core/.agents/skills/harness-agents/SKILL.md +10 -4
  204. package/corpus/core/src/vite/agents-bundle-plugin.ts +26 -1
  205. package/corpus/core/src/vite/client.ts +12 -0
  206. package/corpus/templates/analytics/.agents/skills/actions/SKILL.md +55 -0
  207. package/corpus/templates/analytics/.agents/skills/analysis-workspace/SKILL.md +8 -0
  208. package/corpus/templates/analytics/.agents/skills/data-programs/SKILL.md +348 -0
  209. package/corpus/templates/analytics/.agents/skills/provider-api/SKILL.md +10 -0
  210. package/corpus/templates/analytics/AGENTS.md +31 -32
  211. package/corpus/templates/analytics/actions/dashboard-mutation-api.ts +1 -1
  212. package/corpus/templates/analytics/actions/delete-data-source-credentials.ts +32 -0
  213. package/corpus/templates/analytics/actions/ensure-risk-meeting-dashboard.ts +247 -0
  214. package/corpus/templates/analytics/actions/update-data-source-credentials.ts +123 -0
  215. package/corpus/templates/analytics/app/components/InlineMetric.tsx +2 -2
  216. package/corpus/templates/analytics/app/components/dashboard/CumulativeNetChart.tsx +2 -7
  217. package/corpus/templates/analytics/app/components/dashboard/DataTable.tsx +11 -16
  218. package/corpus/templates/analytics/app/components/dashboard/DateRangePicker.tsx +1 -2
  219. package/corpus/templates/analytics/app/components/dashboard/MetricCard.tsx +2 -7
  220. package/corpus/templates/analytics/app/components/dashboard/RecentActivity.tsx +3 -6
  221. package/corpus/templates/analytics/app/components/dashboard/RevenueChart.tsx +1 -6
  222. package/corpus/templates/analytics/app/components/dashboard/RevenueComparisonChart.tsx +2 -7
  223. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +11 -15
  224. package/corpus/templates/analytics/app/components/dashboard/StatsCard.tsx +1 -7
  225. package/corpus/templates/analytics/app/components/dashboard/TimeSeriesChart.tsx +2 -7
  226. package/corpus/templates/analytics/app/components/layout/CommandPalette.tsx +16 -16
  227. package/corpus/templates/analytics/app/components/layout/Header.tsx +2 -2
  228. package/corpus/templates/analytics/app/components/layout/HeaderActions.tsx +2 -1
  229. package/corpus/templates/analytics/app/components/layout/Layout.tsx +2 -2
  230. package/corpus/templates/analytics/app/components/layout/MobileNav.tsx +33 -17
  231. package/corpus/templates/analytics/app/components/layout/NewAnalysisDialog.tsx +6 -6
  232. package/corpus/templates/analytics/app/components/layout/NewDashboardDialog.tsx +4 -3
  233. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +119 -68
  234. package/corpus/templates/analytics/app/components/ui/alert-dialog.tsx +1 -0
  235. package/corpus/templates/analytics/app/components/ui/alert.tsx +1 -0
  236. package/corpus/templates/analytics/app/components/ui/avatar.tsx +1 -0
  237. package/corpus/templates/analytics/app/components/ui/badge.tsx +1 -0
  238. package/corpus/templates/analytics/app/components/ui/button.tsx +1 -0
  239. package/corpus/templates/analytics/app/components/ui/calendar.tsx +1 -1
  240. package/corpus/templates/analytics/app/components/ui/card.tsx +1 -0
  241. package/corpus/templates/analytics/app/components/ui/checkbox.tsx +1 -0
  242. package/corpus/templates/analytics/app/components/ui/collapsible.tsx +1 -0
  243. package/corpus/templates/analytics/app/components/ui/command.tsx +1 -0
  244. package/corpus/templates/analytics/app/components/ui/date-picker.tsx +1 -0
  245. package/corpus/templates/analytics/app/components/ui/dialog.tsx +1 -0
  246. package/corpus/templates/analytics/app/components/ui/dropdown-menu.tsx +1 -0
  247. package/corpus/templates/analytics/app/components/ui/input.tsx +1 -0
  248. package/corpus/templates/analytics/app/components/ui/label.tsx +1 -0
  249. package/corpus/templates/analytics/app/components/ui/popover.tsx +1 -0
  250. package/corpus/templates/analytics/app/components/ui/progress.tsx +1 -0
  251. package/corpus/templates/analytics/app/components/ui/select.tsx +1 -0
  252. package/corpus/templates/analytics/app/components/ui/sheet.tsx +1 -0
  253. package/corpus/templates/analytics/app/components/ui/skeleton.tsx +1 -0
  254. package/corpus/templates/analytics/app/components/ui/sonner.tsx +1 -0
  255. package/corpus/templates/analytics/app/components/ui/spinner.tsx +1 -0
  256. package/corpus/templates/analytics/app/components/ui/switch.tsx +1 -0
  257. package/corpus/templates/analytics/app/components/ui/table.tsx +1 -0
  258. package/corpus/templates/analytics/app/components/ui/tabs.tsx +1 -0
  259. package/corpus/templates/analytics/app/components/ui/textarea.tsx +1 -0
  260. package/corpus/templates/analytics/app/components/ui/toaster.tsx +1 -0
  261. package/corpus/templates/analytics/app/components/ui/toggle-group.tsx +1 -0
  262. package/corpus/templates/analytics/app/components/ui/toolkit-provider.tsx +12 -0
  263. package/corpus/templates/analytics/app/components/ui/tooltip.tsx +1 -0
  264. package/corpus/templates/analytics/app/global.css +26 -3
  265. package/corpus/templates/analytics/app/i18n/zh-TW.ts +27 -4
  266. package/corpus/templates/analytics/app/i18n-data.ts +277 -42
  267. package/corpus/templates/analytics/app/pages/Agents.tsx +19 -19
  268. package/corpus/templates/analytics/app/pages/DataDictionary.tsx +21 -21
  269. package/corpus/templates/analytics/app/pages/DataSources.tsx +31 -54
  270. package/corpus/templates/analytics/app/pages/NotFound.tsx +2 -1
  271. package/corpus/templates/analytics/app/pages/Placeholder.tsx +2 -1
  272. package/corpus/templates/analytics/app/pages/Settings.tsx +19 -37
  273. package/corpus/templates/analytics/app/pages/adhoc/DashboardSkeleton.tsx +3 -2
  274. package/corpus/templates/analytics/app/pages/adhoc/_shared/KpiChart.tsx +8 -13
  275. package/corpus/templates/analytics/app/pages/adhoc/_shared/components/DateRangeInput.tsx +1 -2
  276. package/corpus/templates/analytics/app/pages/adhoc/explorer/components/ChartTypePicker.tsx +2 -4
  277. package/corpus/templates/analytics/app/pages/adhoc/explorer/components/DateRangePicker.tsx +1 -2
  278. package/corpus/templates/analytics/app/pages/adhoc/explorer/components/EventCombobox.tsx +7 -7
  279. package/corpus/templates/analytics/app/pages/adhoc/explorer/components/EventPanel.tsx +2 -1
  280. package/corpus/templates/analytics/app/pages/adhoc/explorer/components/EventRow.tsx +2 -1
  281. package/corpus/templates/analytics/app/pages/adhoc/explorer/components/ExplorerChart.tsx +3 -8
  282. package/corpus/templates/analytics/app/pages/adhoc/explorer/components/FilterBuilder.tsx +5 -4
  283. package/corpus/templates/analytics/app/pages/adhoc/explorer/components/GroupByPicker.tsx +2 -1
  284. package/corpus/templates/analytics/app/pages/adhoc/explorer/components/PropertyCombobox.tsx +7 -7
  285. package/corpus/templates/analytics/app/pages/adhoc/explorer/components/PropertyValueCombobox.tsx +7 -7
  286. package/corpus/templates/analytics/app/pages/adhoc/explorer/components/SqlPreview.tsx +1 -1
  287. package/corpus/templates/analytics/app/pages/adhoc/explorer/index.tsx +16 -16
  288. package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/ChartCard.tsx +16 -21
  289. package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/index.tsx +32 -32
  290. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/DashboardFilterBar.tsx +14 -14
  291. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/EmailReportDialog.tsx +14 -13
  292. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/PanelEditorDialog.tsx +200 -52
  293. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +23 -28
  294. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/ViewSqlPopover.tsx +13 -12
  295. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/ViewsMenu.tsx +15 -15
  296. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +32 -36
  297. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts +2 -1
  298. package/corpus/templates/analytics/app/pages/analyses/AnalysesList.tsx +8 -7
  299. package/corpus/templates/analytics/app/pages/analyses/AnalysisDetail.tsx +19 -19
  300. package/corpus/templates/analytics/app/pages/analyses/LegacyFusionAnalysis.tsx +11 -15
  301. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +14 -14
  302. package/corpus/templates/analytics/app/pages/sessions/SessionDevToolsPanel.tsx +7 -12
  303. package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +12 -12
  304. package/corpus/templates/analytics/app/pages/settings/AlertRulesSettingsCard.tsx +25 -25
  305. package/corpus/templates/analytics/app/root.tsx +22 -19
  306. package/corpus/templates/analytics/app/routes/catalog.tsx +8 -8
  307. package/corpus/templates/analytics/changelog/2026-07-06-dashboard-lists-and-headers-stay-compact.md +6 -0
  308. package/corpus/templates/analytics/changelog/2026-07-06-dashboards-can-now-chart-data-programs-saved-server-side-scr.md +6 -0
  309. package/corpus/templates/analytics/changelog/2026-07-06-extension-pages-and-mobile-headers-now-share-one-background-shade.md +6 -0
  310. package/corpus/templates/analytics/changelog/2026-07-06-mobile-chat-header-is-more-compact-and-the-menu-button-opens-navigation.md +5 -0
  311. package/corpus/templates/analytics/learnings.defaults.md +36 -0
  312. package/corpus/templates/analytics/seeds/data-programs/risk-meeting-cohort.js +254 -0
  313. package/corpus/templates/analytics/seeds/data-programs/risk-meeting-pylon-early-warning.js +184 -0
  314. package/corpus/templates/analytics/server/handlers/sql-query.ts +1 -1
  315. package/corpus/templates/analytics/server/lib/dashboard-panel-query.ts +109 -1
  316. package/corpus/templates/assets/.agents/skills/actions/SKILL.md +55 -0
  317. package/corpus/templates/assets/.agents/skills/asset-generation/SKILL.md +5 -0
  318. package/corpus/templates/assets/.agents/skills/logo-composite/SKILL.md +34 -6
  319. package/corpus/templates/assets/AGENTS.md +23 -4
  320. package/corpus/templates/assets/actions/_generation-preset-settings.ts +48 -0
  321. package/corpus/templates/assets/actions/_helpers.ts +8 -1
  322. package/corpus/templates/assets/actions/_preset-skeleton-validation.ts +64 -0
  323. package/corpus/templates/assets/actions/create-generation-preset.ts +11 -3
  324. package/corpus/templates/assets/actions/generate-image-batch.ts +7 -2
  325. package/corpus/templates/assets/actions/generate-image.ts +342 -25
  326. package/corpus/templates/assets/actions/get-library.ts +4 -3
  327. package/corpus/templates/assets/actions/navigate.ts +2 -1
  328. package/corpus/templates/assets/actions/refresh-generation-run.ts +5 -1
  329. package/corpus/templates/assets/actions/update-asset.ts +1 -0
  330. package/corpus/templates/assets/actions/update-generation-preset.ts +8 -1
  331. package/corpus/templates/assets/actions/view-screen.ts +8 -1
  332. package/corpus/templates/assets/app/components/create/RecentDraftsSection.tsx +3 -2
  333. package/corpus/templates/assets/app/components/generation/GenerationResults.tsx +25 -11
  334. package/corpus/templates/assets/app/components/layout/Header.tsx +1 -1
  335. package/corpus/templates/assets/app/components/layout/PageShell.tsx +4 -4
  336. package/corpus/templates/assets/app/components/layout/Sidebar.tsx +12 -12
  337. package/corpus/templates/assets/app/components/library/CreateLibraryDialog.tsx +8 -8
  338. package/corpus/templates/assets/app/components/library/EditLibraryDialog.tsx +7 -7
  339. package/corpus/templates/assets/app/components/library/LibraryCard.tsx +8 -8
  340. package/corpus/templates/assets/app/components/library/LibraryPresetGrid.tsx +3 -2
  341. package/corpus/templates/assets/app/components/ui/alert-dialog.tsx +1 -0
  342. package/corpus/templates/assets/app/components/ui/badge.tsx +1 -0
  343. package/corpus/templates/assets/app/components/ui/button.tsx +1 -0
  344. package/corpus/templates/assets/app/components/ui/card.tsx +1 -0
  345. package/corpus/templates/assets/app/components/ui/checkbox.tsx +1 -0
  346. package/corpus/templates/assets/app/components/ui/dialog.tsx +1 -0
  347. package/corpus/templates/assets/app/components/ui/dropdown-menu.tsx +1 -0
  348. package/corpus/templates/assets/app/components/ui/input.tsx +1 -0
  349. package/corpus/templates/assets/app/components/ui/label.tsx +1 -0
  350. package/corpus/templates/assets/app/components/ui/popover.tsx +1 -0
  351. package/corpus/templates/assets/app/components/ui/select.tsx +1 -0
  352. package/corpus/templates/assets/app/components/ui/separator.tsx +1 -0
  353. package/corpus/templates/assets/app/components/ui/sheet.tsx +1 -0
  354. package/corpus/templates/assets/app/components/ui/skeleton.tsx +1 -0
  355. package/corpus/templates/assets/app/components/ui/sonner.tsx +1 -0
  356. package/corpus/templates/assets/app/components/ui/spinner.tsx +1 -0
  357. package/corpus/templates/assets/app/components/ui/tabs.tsx +1 -0
  358. package/corpus/templates/assets/app/components/ui/toolkit-provider.tsx +12 -0
  359. package/corpus/templates/assets/app/components/ui/tooltip.tsx +1 -0
  360. package/corpus/templates/assets/app/global.css +1 -0
  361. package/corpus/templates/assets/app/hooks/use-image-model-menu.ts +1 -0
  362. package/corpus/templates/assets/app/hooks/use-navigation-state.ts +11 -0
  363. package/corpus/templates/assets/app/i18n/zh-TW.ts +64 -0
  364. package/corpus/templates/assets/app/i18n-data.ts +650 -0
  365. package/corpus/templates/assets/app/root.tsx +12 -9
  366. package/corpus/templates/assets/app/routes/asset.$id.tsx +21 -20
  367. package/corpus/templates/assets/app/routes/audit.tsx +15 -14
  368. package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +93 -87
  369. package/corpus/templates/assets/app/routes/brand-kits.$id_.presets.$presetId.tsx +1474 -0
  370. package/corpus/templates/assets/app/routes/library.tsx +74 -32
  371. package/corpus/templates/assets/app/routes/settings.tsx +21 -35
  372. package/corpus/templates/assets/changelog/2026-07-06-mobile-layouts-no-longer-stack-two-top-headers.md +5 -0
  373. package/corpus/templates/assets/server/lib/generation.ts +366 -44
  374. package/corpus/templates/assets/server/lib/image-processing.ts +252 -0
  375. package/corpus/templates/assets/server/lib/preset-skeleton.ts +137 -0
  376. package/corpus/templates/assets/shared/api.ts +47 -0
  377. package/corpus/templates/brain/.agents/skills/actions/SKILL.md +55 -0
  378. package/corpus/templates/brain/.agents/skills/ask-across-everything/SKILL.md +6 -1
  379. package/corpus/templates/brain/.agents/skills/brain/SKILL.md +63 -5
  380. package/corpus/templates/brain/.agents/skills/ingestion-and-connectors/SKILL.md +112 -0
  381. package/corpus/templates/brain/AGENTS.md +93 -8
  382. package/corpus/templates/brain/app/components/brain/CanonicalPreviewSheet.tsx +6 -5
  383. package/corpus/templates/brain/app/components/brain/Surface.tsx +3 -8
  384. package/corpus/templates/brain/app/components/layout/Layout.tsx +7 -7
  385. package/corpus/templates/brain/app/components/layout/Sidebar.tsx +6 -6
  386. package/corpus/templates/brain/app/components/ui/badge.tsx +1 -0
  387. package/corpus/templates/brain/app/components/ui/button.tsx +1 -0
  388. package/corpus/templates/brain/app/components/ui/card.tsx +1 -0
  389. package/corpus/templates/brain/app/components/ui/input.tsx +1 -0
  390. package/corpus/templates/brain/app/components/ui/label.tsx +1 -0
  391. package/corpus/templates/brain/app/components/ui/progress.tsx +1 -0
  392. package/corpus/templates/brain/app/components/ui/select.tsx +1 -0
  393. package/corpus/templates/brain/app/components/ui/separator.tsx +1 -0
  394. package/corpus/templates/brain/app/components/ui/sheet.tsx +1 -0
  395. package/corpus/templates/brain/app/components/ui/skeleton.tsx +1 -0
  396. package/corpus/templates/brain/app/components/ui/switch.tsx +1 -0
  397. package/corpus/templates/brain/app/components/ui/table.tsx +1 -0
  398. package/corpus/templates/brain/app/components/ui/textarea.tsx +1 -0
  399. package/corpus/templates/brain/app/components/ui/toolkit-provider.tsx +12 -0
  400. package/corpus/templates/brain/app/components/ui/tooltip.tsx +1 -0
  401. package/corpus/templates/brain/app/global.css +1 -0
  402. package/corpus/templates/brain/app/hooks/use-distillation-bridge.ts +9 -4
  403. package/corpus/templates/brain/app/root.tsx +11 -8
  404. package/corpus/templates/brain/app/routes/knowledge.tsx +19 -19
  405. package/corpus/templates/brain/app/routes/ops.tsx +18 -18
  406. package/corpus/templates/brain/app/routes/review.tsx +22 -27
  407. package/corpus/templates/brain/app/routes/search.tsx +20 -20
  408. package/corpus/templates/brain/app/routes/settings.tsx +25 -40
  409. package/corpus/templates/brain/app/routes/sources.tsx +23 -23
  410. package/corpus/templates/calendar/.agents/skills/actions/SKILL.md +55 -0
  411. package/corpus/templates/calendar/actions/find-a-time.ts +29 -16
  412. package/corpus/templates/calendar/actions/list-bookings.ts +33 -2
  413. package/corpus/templates/calendar/actions/list-events.ts +34 -8
  414. package/corpus/templates/calendar/actions/update-calendar-visual-preferences.ts +105 -34
  415. package/corpus/templates/calendar/app/components/ThemeToggle.tsx +5 -5
  416. package/corpus/templates/calendar/app/components/TimezoneCombobox.tsx +6 -6
  417. package/corpus/templates/calendar/app/components/booking/BookingConfirmation.tsx +2 -1
  418. package/corpus/templates/calendar/app/components/booking/BookingForm.tsx +9 -8
  419. package/corpus/templates/calendar/app/components/booking/DatePicker.tsx +2 -2
  420. package/corpus/templates/calendar/app/components/booking/TimeSlotPicker.tsx +3 -2
  421. package/corpus/templates/calendar/app/components/calendar/AddCalendarDialog.tsx +10 -15
  422. package/corpus/templates/calendar/app/components/calendar/AttendeeAutocomplete.tsx +6 -6
  423. package/corpus/templates/calendar/app/components/calendar/CommandPalette.tsx +73 -3
  424. package/corpus/templates/calendar/app/components/calendar/CreateEventDialog.tsx +27 -27
  425. package/corpus/templates/calendar/app/components/calendar/DayView.tsx +553 -267
  426. package/corpus/templates/calendar/app/components/calendar/DeleteEventDialog.tsx +9 -12
  427. package/corpus/templates/calendar/app/components/calendar/EventAttendeesSection.tsx +10 -13
  428. package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +7 -7
  429. package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +21 -21
  430. package/corpus/templates/calendar/app/components/calendar/EventDialog.tsx +11 -11
  431. package/corpus/templates/calendar/app/components/calendar/EventOptionControls.tsx +9 -9
  432. package/corpus/templates/calendar/app/components/calendar/FindTimePanel.tsx +9 -9
  433. package/corpus/templates/calendar/app/components/calendar/GoogleConnectBanner.tsx +6 -6
  434. package/corpus/templates/calendar/app/components/calendar/GoogleSetupWizard.tsx +3 -3
  435. package/corpus/templates/calendar/app/components/calendar/GoogleSyncButton.tsx +1 -1
  436. package/corpus/templates/calendar/app/components/calendar/GuestNotificationDialog.tsx +8 -10
  437. package/corpus/templates/calendar/app/components/calendar/IntegrationsSidebar.tsx +12 -12
  438. package/corpus/templates/calendar/app/components/calendar/KeyboardShortcutsHelp.tsx +2 -2
  439. package/corpus/templates/calendar/app/components/calendar/MonthView.tsx +93 -41
  440. package/corpus/templates/calendar/app/components/calendar/PeopleSearchDialog.tsx +6 -6
  441. package/corpus/templates/calendar/app/components/calendar/WeekView.tsx +530 -299
  442. package/corpus/templates/calendar/app/components/layout/AppLayout.tsx +229 -134
  443. package/corpus/templates/calendar/app/components/layout/Sidebar.tsx +135 -108
  444. package/corpus/templates/calendar/app/components/ui/alert-dialog.tsx +1 -0
  445. package/corpus/templates/calendar/app/components/ui/badge.tsx +1 -0
  446. package/corpus/templates/calendar/app/components/ui/button.tsx +1 -0
  447. package/corpus/templates/calendar/app/components/ui/card.tsx +1 -0
  448. package/corpus/templates/calendar/app/components/ui/checkbox.tsx +1 -0
  449. package/corpus/templates/calendar/app/components/ui/collapsible.tsx +1 -0
  450. package/corpus/templates/calendar/app/components/ui/command.tsx +1 -0
  451. package/corpus/templates/calendar/app/components/ui/dialog.tsx +1 -0
  452. package/corpus/templates/calendar/app/components/ui/dropdown-menu.tsx +1 -0
  453. package/corpus/templates/calendar/app/components/ui/input.tsx +1 -0
  454. package/corpus/templates/calendar/app/components/ui/label.tsx +1 -0
  455. package/corpus/templates/calendar/app/components/ui/popover.tsx +1 -0
  456. package/corpus/templates/calendar/app/components/ui/radio-group.tsx +1 -0
  457. package/corpus/templates/calendar/app/components/ui/select.tsx +1 -0
  458. package/corpus/templates/calendar/app/components/ui/separator.tsx +1 -0
  459. package/corpus/templates/calendar/app/components/ui/skeleton.tsx +1 -0
  460. package/corpus/templates/calendar/app/components/ui/spinner.tsx +1 -0
  461. package/corpus/templates/calendar/app/components/ui/switch.tsx +1 -0
  462. package/corpus/templates/calendar/app/components/ui/table.tsx +1 -0
  463. package/corpus/templates/calendar/app/components/ui/tabs.tsx +1 -0
  464. package/corpus/templates/calendar/app/components/ui/textarea.tsx +1 -0
  465. package/corpus/templates/calendar/app/components/ui/toolkit-provider.tsx +12 -0
  466. package/corpus/templates/calendar/app/components/ui/tooltip.tsx +1 -0
  467. package/corpus/templates/calendar/app/global.css +1 -0
  468. package/corpus/templates/calendar/app/hooks/use-event-drag.ts +63 -9
  469. package/corpus/templates/calendar/app/hooks/use-google-auth.ts +15 -27
  470. package/corpus/templates/calendar/app/hooks/use-grid-create-drag.ts +254 -0
  471. package/corpus/templates/calendar/app/hooks/use-mobile.ts +1 -0
  472. package/corpus/templates/calendar/app/hooks/use-view-preferences.ts +287 -21
  473. package/corpus/templates/calendar/app/hooks/use-zoom-auth.ts +56 -19
  474. package/corpus/templates/calendar/app/i18n/zh-TW.ts +3 -0
  475. package/corpus/templates/calendar/app/i18n-data.ts +31 -0
  476. package/corpus/templates/calendar/app/lib/event-colors.ts +29 -8
  477. package/corpus/templates/calendar/app/pages/AvailabilitySettings.tsx +11 -11
  478. package/corpus/templates/calendar/app/pages/BookingLinksPage.tsx +45 -50
  479. package/corpus/templates/calendar/app/pages/BookingPage.tsx +2 -2
  480. package/corpus/templates/calendar/app/pages/BookingsList.tsx +12 -12
  481. package/corpus/templates/calendar/app/pages/CalendarView.tsx +438 -271
  482. package/corpus/templates/calendar/app/pages/ManageBookingPage.tsx +13 -13
  483. package/corpus/templates/calendar/app/pages/Settings.tsx +15 -29
  484. package/corpus/templates/calendar/app/root.tsx +19 -15
  485. package/corpus/templates/calendar/app/routes/event.tsx +2 -2
  486. package/corpus/templates/calendar/changelog/2026-07-06-calendar-now-loads-view-preferences-much-more-efficiently.md +6 -0
  487. package/corpus/templates/calendar/changelog/2026-07-06-connected-calendars-keep-their-own-color-when-you-customize-.md +6 -0
  488. package/corpus/templates/calendar/changelog/2026-07-06-create-events-from-the-command-menu-with-natural-phrases-lik.md +6 -0
  489. package/corpus/templates/calendar/changelog/2026-07-06-drag-across-empty-week-or-day-slots-to-create-an-event-with-.md +6 -0
  490. package/corpus/templates/calendar/changelog/2026-07-06-dragging-and-resizing-events-is-much-smoother-especially-on-.md +6 -0
  491. package/corpus/templates/calendar/changelog/2026-07-06-each-connected-google-account-can-now-have-its-own-calendar-.md +6 -0
  492. package/corpus/templates/calendar/changelog/2026-07-06-events-spanning-multiple-days-now-show-on-every-day-they-cov.md +6 -0
  493. package/corpus/templates/calendar/changelog/2026-07-06-google-calendar-now-finishes-connecting-correctly-after-sign.md +6 -0
  494. package/corpus/templates/calendar/changelog/2026-07-06-public-booking-pages-respond-faster.md +6 -0
  495. package/corpus/templates/calendar/changelog/2026-07-06-the-app-header-no-longer-shows-the-global-notifications-bell.md +6 -0
  496. package/corpus/templates/calendar/package.json +1 -0
  497. package/corpus/templates/calendar/server/handlers/bookings.ts +46 -52
  498. package/corpus/templates/calendar/server/handlers/google-auth.ts +21 -5
  499. package/corpus/templates/calendar/server/lib/google-calendar.ts +32 -3
  500. package/corpus/templates/calendar/shared/calendar-view-preferences.ts +66 -2
  501. package/corpus/templates/chat/.agents/skills/actions/SKILL.md +55 -0
  502. package/corpus/templates/chat/app/components/layout/Layout.tsx +6 -6
  503. package/corpus/templates/chat/app/components/layout/Sidebar.tsx +12 -12
  504. package/corpus/templates/chat/app/components/ui/button.tsx +1 -0
  505. package/corpus/templates/chat/app/components/ui/card.tsx +1 -0
  506. package/corpus/templates/chat/app/components/ui/dropdown-menu.tsx +1 -0
  507. package/corpus/templates/chat/app/components/ui/input.tsx +1 -0
  508. package/corpus/templates/chat/app/components/ui/label.tsx +1 -0
  509. package/corpus/templates/chat/app/components/ui/sheet.tsx +1 -0
  510. package/corpus/templates/chat/app/components/ui/toolkit-provider.tsx +12 -0
  511. package/corpus/templates/chat/app/components/ui/tooltip.tsx +1 -0
  512. package/corpus/templates/chat/app/global.css +1 -0
  513. package/corpus/templates/chat/app/root.tsx +7 -4
  514. package/corpus/templates/chat/app/routes/settings.tsx +6 -21
  515. package/corpus/templates/clips/.agents/skills/actions/SKILL.md +55 -0
  516. package/corpus/templates/clips/.agents/skills/video-sharing/SKILL.md +18 -4
  517. package/corpus/templates/clips/.env.example +8 -0
  518. package/corpus/templates/clips/actions/accept-invite.ts +3 -0
  519. package/corpus/templates/clips/actions/add-recording-to-space.ts +55 -27
  520. package/corpus/templates/clips/actions/add-vocabulary-term.ts +3 -1
  521. package/corpus/templates/clips/actions/archive-recording.ts +5 -12
  522. package/corpus/templates/clips/actions/cleanup-transcript.ts +5 -4
  523. package/corpus/templates/clips/actions/clear-edits.ts +42 -29
  524. package/corpus/templates/clips/actions/create-meeting.ts +87 -40
  525. package/corpus/templates/clips/actions/create-recording.ts +6 -3
  526. package/corpus/templates/clips/actions/delete-meeting.ts +25 -0
  527. package/corpus/templates/clips/actions/delete-recording-permanent.ts +3 -0
  528. package/corpus/templates/clips/actions/disconnect-calendar.ts +10 -4
  529. package/corpus/templates/clips/actions/export-insights-csv.ts +28 -8
  530. package/corpus/templates/clips/actions/finalize-meeting.ts +106 -33
  531. package/corpus/templates/clips/actions/finalize-recording.ts +201 -3
  532. package/corpus/templates/clips/actions/get-organization-insights.ts +51 -7
  533. package/corpus/templates/clips/actions/import-loom-recording.ts +17 -0
  534. package/corpus/templates/clips/actions/lib/audio-only-transcription.ts +18 -1
  535. package/corpus/templates/clips/actions/lib/create-recording-schema.ts +7 -1
  536. package/corpus/templates/clips/actions/lib/ensure-seekable-video.ts +16 -0
  537. package/corpus/templates/clips/actions/list-clip-views.ts +60 -0
  538. package/corpus/templates/clips/actions/regenerate-summary.ts +10 -0
  539. package/corpus/templates/clips/actions/remove-vocabulary-term.ts +59 -0
  540. package/corpus/templates/clips/actions/request-transcript.ts +170 -25
  541. package/corpus/templates/clips/actions/restore-recording.ts +5 -12
  542. package/corpus/templates/clips/actions/save-browser-transcript.ts +13 -2
  543. package/corpus/templates/clips/actions/set-chapters.ts +6 -15
  544. package/corpus/templates/clips/actions/set-thumbnail.ts +77 -36
  545. package/corpus/templates/clips/actions/split-recording.ts +49 -35
  546. package/corpus/templates/clips/actions/stitch-recordings.ts +7 -4
  547. package/corpus/templates/clips/actions/stop-meeting-recording.ts +15 -1
  548. package/corpus/templates/clips/actions/sync-calendars.ts +10 -108
  549. package/corpus/templates/clips/actions/trash-recording.ts +43 -14
  550. package/corpus/templates/clips/actions/trim-recording.ts +48 -27
  551. package/corpus/templates/clips/actions/undo-edit.ts +49 -37
  552. package/corpus/templates/clips/app/components/capture-install-options.tsx +6 -6
  553. package/corpus/templates/clips/app/components/dictate/vocabulary-section.tsx +208 -0
  554. package/corpus/templates/clips/app/components/editable-recording-title.tsx +2 -2
  555. package/corpus/templates/clips/app/components/editor/chapters-editor.tsx +7 -7
  556. package/corpus/templates/clips/app/components/editor/editor-toolbar.tsx +26 -26
  557. package/corpus/templates/clips/app/components/editor/split-button.tsx +5 -4
  558. package/corpus/templates/clips/app/components/editor/stitch-manager.tsx +9 -9
  559. package/corpus/templates/clips/app/components/editor/thumbnail-picker.tsx +11 -16
  560. package/corpus/templates/clips/app/components/editor/timeline.tsx +3 -3
  561. package/corpus/templates/clips/app/components/editor/transcript-editor.tsx +5 -5
  562. package/corpus/templates/clips/app/components/library/bulk-action-toolbar.tsx +4 -4
  563. package/corpus/templates/clips/app/components/library/create-space-dialog.tsx +8 -8
  564. package/corpus/templates/clips/app/components/library/empty-state.tsx +2 -1
  565. package/corpus/templates/clips/app/components/library/folder-tree.tsx +14 -14
  566. package/corpus/templates/clips/app/components/library/library-grid.tsx +24 -98
  567. package/corpus/templates/clips/app/components/library/library-layout.tsx +25 -21
  568. package/corpus/templates/clips/app/components/library/organization-switcher.tsx +11 -11
  569. package/corpus/templates/clips/app/components/library/recording-card.tsx +44 -50
  570. package/corpus/templates/clips/app/components/library/search-bar.tsx +5 -5
  571. package/corpus/templates/clips/app/components/library/sort-menu.tsx +5 -4
  572. package/corpus/templates/clips/app/components/library/tag-input.tsx +4 -4
  573. package/corpus/templates/clips/app/components/meetings/attendee-stack.tsx +2 -6
  574. package/corpus/templates/clips/app/components/meetings/auto-record-prompt.tsx +1 -1
  575. package/corpus/templates/clips/app/components/meetings/bullet-link.tsx +4 -4
  576. package/corpus/templates/clips/app/components/meetings/canvas-editor.tsx +5 -5
  577. package/corpus/templates/clips/app/components/meetings/meeting-card.tsx +3 -3
  578. package/corpus/templates/clips/app/components/meetings/quick-ask-sidebar.tsx +17 -9
  579. package/corpus/templates/clips/app/components/meetings/share-meeting-dialog.tsx +6 -11
  580. package/corpus/templates/clips/app/components/meetings/transcript-bubbles.tsx +215 -21
  581. package/corpus/templates/clips/app/components/player/access-password-prompt.tsx +3 -2
  582. package/corpus/templates/clips/app/components/player/comments-panel.tsx +14 -14
  583. package/corpus/templates/clips/app/components/player/delete-recording-menu.tsx +7 -6
  584. package/corpus/templates/clips/app/components/player/insights-panel.tsx +18 -6
  585. package/corpus/templates/clips/app/components/player/player-controls.tsx +42 -36
  586. package/corpus/templates/clips/app/components/player/reactions-tray.tsx +4 -4
  587. package/corpus/templates/clips/app/components/player/settings-panel.tsx +13 -12
  588. package/corpus/templates/clips/app/components/player/share-dialog.tsx +80 -49
  589. package/corpus/templates/clips/app/components/player/sign-in-prompt-dialog.tsx +3 -2
  590. package/corpus/templates/clips/app/components/player/timestamped-comment-button.tsx +2 -2
  591. package/corpus/templates/clips/app/components/player/transcript-panel.tsx +7 -7
  592. package/corpus/templates/clips/app/components/player/video-player.tsx +182 -15
  593. package/corpus/templates/clips/app/components/recorder/camera-visualizer.tsx +1 -1
  594. package/corpus/templates/clips/app/components/recorder/microphone-visualizer.tsx +1 -1
  595. package/corpus/templates/clips/app/components/recorder/pre-record-panel.tsx +49 -36
  596. package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +399 -57
  597. package/corpus/templates/clips/app/components/recorder/recording-toolbar.tsx +6 -5
  598. package/corpus/templates/clips/app/components/recorder/storage-setup-card.tsx +7 -6
  599. package/corpus/templates/clips/app/components/sharing/share-ui.tsx +10 -10
  600. package/corpus/templates/clips/app/components/sharing/viewed-by-popover.tsx +132 -0
  601. package/corpus/templates/clips/app/components/ui/alert-dialog.tsx +1 -0
  602. package/corpus/templates/clips/app/components/ui/avatar.tsx +1 -0
  603. package/corpus/templates/clips/app/components/ui/badge.tsx +1 -0
  604. package/corpus/templates/clips/app/components/ui/button.tsx +1 -0
  605. package/corpus/templates/clips/app/components/ui/card.tsx +1 -0
  606. package/corpus/templates/clips/app/components/ui/checkbox.tsx +1 -0
  607. package/corpus/templates/clips/app/components/ui/collapsible.tsx +1 -0
  608. package/corpus/templates/clips/app/components/ui/context-menu.tsx +1 -0
  609. package/corpus/templates/clips/app/components/ui/dialog.tsx +1 -0
  610. package/corpus/templates/clips/app/components/ui/dropdown-menu.tsx +1 -0
  611. package/corpus/templates/clips/app/components/ui/input.tsx +1 -0
  612. package/corpus/templates/clips/app/components/ui/label.tsx +1 -0
  613. package/corpus/templates/clips/app/components/ui/popover.tsx +1 -0
  614. package/corpus/templates/clips/app/components/ui/scroll-area.tsx +1 -0
  615. package/corpus/templates/clips/app/components/ui/select.tsx +1 -0
  616. package/corpus/templates/clips/app/components/ui/separator.tsx +1 -0
  617. package/corpus/templates/clips/app/components/ui/sheet.tsx +1 -0
  618. package/corpus/templates/clips/app/components/ui/skeleton.tsx +1 -0
  619. package/corpus/templates/clips/app/components/ui/slider.tsx +1 -0
  620. package/corpus/templates/clips/app/components/ui/sonner.tsx +1 -0
  621. package/corpus/templates/clips/app/components/ui/spinner.tsx +1 -0
  622. package/corpus/templates/clips/app/components/ui/switch.tsx +1 -0
  623. package/corpus/templates/clips/app/components/ui/table.tsx +1 -0
  624. package/corpus/templates/clips/app/components/ui/tabs.tsx +1 -0
  625. package/corpus/templates/clips/app/components/ui/textarea.tsx +1 -0
  626. package/corpus/templates/clips/app/components/ui/toolkit-provider.tsx +12 -0
  627. package/corpus/templates/clips/app/components/ui/tooltip.tsx +1 -0
  628. package/corpus/templates/clips/app/components/workspace/branding-editor.tsx +5 -9
  629. package/corpus/templates/clips/app/components/workspace/insights-hub.tsx +9 -19
  630. package/corpus/templates/clips/app/components/workspace/invite-dialog.tsx +10 -9
  631. package/corpus/templates/clips/app/components/workspace/members-list.tsx +11 -10
  632. package/corpus/templates/clips/app/components/workspace/notifications-list.tsx +2 -1
  633. package/corpus/templates/clips/app/components/workspace/top-creators-table.tsx +3 -2
  634. package/corpus/templates/clips/app/components/workspace/top-videos-table.tsx +3 -2
  635. package/corpus/templates/clips/app/global.css +1 -0
  636. package/corpus/templates/clips/app/hooks/use-desktop-promo.ts +2 -1
  637. package/corpus/templates/clips/app/hooks/use-mobile.ts +1 -0
  638. package/corpus/templates/clips/app/hooks/use-player-shortcuts.ts +14 -10
  639. package/corpus/templates/clips/app/hooks/use-view-tracking.ts +14 -0
  640. package/corpus/templates/clips/app/i18n/ar-SA.ts +56 -0
  641. package/corpus/templates/clips/app/i18n/de-DE.ts +53 -0
  642. package/corpus/templates/clips/app/i18n/en-US.ts +52 -0
  643. package/corpus/templates/clips/app/i18n/es-ES.ts +53 -0
  644. package/corpus/templates/clips/app/i18n/fr-FR.ts +53 -0
  645. package/corpus/templates/clips/app/i18n/hi-IN.ts +52 -0
  646. package/corpus/templates/clips/app/i18n/ja-JP.ts +53 -0
  647. package/corpus/templates/clips/app/i18n/ko-KR.ts +52 -0
  648. package/corpus/templates/clips/app/i18n/pt-BR.ts +53 -0
  649. package/corpus/templates/clips/app/i18n/zh-CN.ts +51 -0
  650. package/corpus/templates/clips/app/i18n/zh-TW.ts +51 -0
  651. package/corpus/templates/clips/app/lib/recorder-preferences.ts +4 -0
  652. package/corpus/templates/clips/app/lib/timestamp-mapping.ts +5 -0
  653. package/corpus/templates/clips/app/root.tsx +26 -23
  654. package/corpus/templates/clips/app/routes/_app.dictate.tsx +39 -23
  655. package/corpus/templates/clips/app/routes/_app.library._index.tsx +27 -1
  656. package/corpus/templates/clips/app/routes/_app.meetings.$meetingId.tsx +134 -39
  657. package/corpus/templates/clips/app/routes/_app.meetings._index.tsx +20 -20
  658. package/corpus/templates/clips/app/routes/_app.notifications.tsx +3 -3
  659. package/corpus/templates/clips/app/routes/_app.settings._index.tsx +92 -60
  660. package/corpus/templates/clips/app/routes/_app.settings.organization.tsx +8 -13
  661. package/corpus/templates/clips/app/routes/_app.spaces._index.tsx +1 -1
  662. package/corpus/templates/clips/app/routes/_app.trash.tsx +96 -31
  663. package/corpus/templates/clips/app/routes/bug-report.done.tsx +1 -1
  664. package/corpus/templates/clips/app/routes/bug-report.tsx +11 -11
  665. package/corpus/templates/clips/app/routes/download.tsx +2 -2
  666. package/corpus/templates/clips/app/routes/embed.$shareId.tsx +2 -1
  667. package/corpus/templates/clips/app/routes/invite.$token.tsx +8 -13
  668. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +263 -251
  669. package/corpus/templates/clips/app/routes/record.tsx +185 -78
  670. package/corpus/templates/clips/app/routes/share.$shareId.tsx +51 -29
  671. package/corpus/templates/clips/app/routes/share.meeting.$meetingId.tsx +71 -16
  672. package/corpus/templates/clips/changelog/2026-07-05-mobile-clip-pages-use-a-tighter-viewer-layout-with-inline-ac.md +6 -0
  673. package/corpus/templates/clips/changelog/2026-07-06-ai-setup-now-switches-to-your-saved-provider-key-and-uses-fa.md +6 -0
  674. package/corpus/templates/clips/changelog/2026-07-06-browsers-that-can-t-play-a-clip-s-video-format-now-see-a-cle.md +6 -0
  675. package/corpus/templates/clips/changelog/2026-07-06-calendar-connections-no-longer-ask-you-to-reconnect-after-te.md +6 -0
  676. package/corpus/templates/clips/changelog/2026-07-06-chrome-extension-recordings-keep-your-selected-microphone.md +6 -0
  677. package/corpus/templates/clips/changelog/2026-07-06-clip-recordings-now-use-streaming-uploads-by-default.md +6 -0
  678. package/corpus/templates/clips/changelog/2026-07-06-clip-titles-in-the-library-grid-now-open-the-clip-instead-of.md +6 -0
  679. package/corpus/templates/clips/changelog/2026-07-06-desktop-countdown-now-dims-the-screen-before-recording.md +5 -0
  680. package/corpus/templates/clips/changelog/2026-07-06-desktop-popover-no-longer-drops-bluetooth-headphones-into-call-quality-mode.md +6 -0
  681. package/corpus/templates/clips/changelog/2026-07-06-desktop-recorder-now-refreshes-cameras-and-mics-when-the-pop.md +6 -0
  682. package/corpus/templates/clips/changelog/2026-07-06-desktop-recordings-keep-microphone-volume-clear-while-recor.md +5 -0
  683. package/corpus/templates/clips/changelog/2026-07-06-desktop-recordings-no-longer-get-stuck-optimizing-when-scree.md +6 -0
  684. package/corpus/templates/clips/changelog/2026-07-06-dictation-history-from-the-desktop-app-now-appears-in-the-di.md +6 -0
  685. package/corpus/templates/clips/changelog/2026-07-06-dictation-no-longer-overwrites-your-clipboard-and-messaging-.md +6 -0
  686. package/corpus/templates/clips/changelog/2026-07-06-double-tap-your-dictation-key-for-hands-free-dictation-tap-a.md +6 -0
  687. package/corpus/templates/clips/changelog/2026-07-06-fixed-desktop-recordings-sometimes-uploading-without-audio-a.md +6 -0
  688. package/corpus/templates/clips/changelog/2026-07-06-fixed-desktop-recordings-that-could-get-stuck-uploading-and-.md +6 -0
  689. package/corpus/templates/clips/changelog/2026-07-06-fixed-recordings-sometimes-capturing-from-the-wrong-micropho.md +6 -0
  690. package/corpus/templates/clips/changelog/2026-07-06-hands-free-dictation-and-meeting-cleanup-recover-more-reliab.md +6 -0
  691. package/corpus/templates/clips/changelog/2026-07-06-live-meetings-show-time-remaining-and-you-can-end-a-meeting-.md +6 -0
  692. package/corpus/templates/clips/changelog/2026-07-06-manage-your-personal-dictation-dictionary-from-the-dictate-t.md +6 -0
  693. package/corpus/templates/clips/changelog/2026-07-06-meetings-interrupted-by-quitting-or-crashing-now-close-out-c.md +6 -0
  694. package/corpus/templates/clips/changelog/2026-07-06-mic-only-desktop-recordings-now-use-the-selected-microphone-.md +6 -0
  695. package/corpus/templates/clips/changelog/2026-07-06-paste-your-last-dictation-anytime-with-cmd-ctrl-v-or-from-th.md +6 -0
  696. package/corpus/templates/clips/changelog/2026-07-06-press-esc-to-cancel-a-dictation-in-progress.md +6 -0
  697. package/corpus/templates/clips/changelog/2026-07-06-recordings-whose-video-file-contains-no-audio-track-no-longe.md +6 -0
  698. package/corpus/templates/clips/changelog/2026-07-06-search-inside-meeting-transcripts.md +6 -0
  699. package/corpus/templates/clips/changelog/2026-07-06-see-who-viewed-your-shared-clips-and-when-click-the-view-cou.md +6 -0
  700. package/corpus/templates/clips/changelog/2026-07-06-share-links-for-agents-now-appear-as-soon-as-the-share-menu-.md +6 -0
  701. package/corpus/templates/clips/changelog/2026-07-06-share-popovers-now-stay-above-video-playback-controls.md +6 -0
  702. package/corpus/templates/clips/changelog/2026-07-06-shared-clip-pages-now-recover-automatically-from-transient-v.md +6 -0
  703. package/corpus/templates/clips/changelog/2026-07-06-the-desktop-app-now-walks-you-through-permissions-on-first-l.md +6 -0
  704. package/corpus/templates/clips/changelog/2026-07-06-transcript-cleanup-preferences-now-save-per-user.md +6 -0
  705. package/corpus/templates/clips/changelog/2026-07-06-transcripts-now-retry-automatically-after-transient-failures.md +6 -0
  706. package/corpus/templates/clips/changelog/2026-07-06-video-compression-now-runs-in-the-background-without-blocking.md +6 -0
  707. package/corpus/templates/clips/changelog/2026-07-07-native-microphone-recordings-now-reduce-steady-background-noise.md +6 -0
  708. package/corpus/templates/clips/changelog/2026-07-07-recording-transcripts-now-line-up-with-the-video-start-afte.md +6 -0
  709. package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -1
  710. package/corpus/templates/clips/chrome-extension/src/background.ts +141 -48
  711. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +383 -59
  712. package/corpus/templates/clips/chrome-extension/src/overlay.ts +51 -10
  713. package/corpus/templates/clips/chrome-extension/src/popup.ts +57 -6
  714. package/corpus/templates/clips/desktop/package.json +1 -1
  715. package/corpus/templates/clips/desktop/scripts/sign-macos-local.ts +67 -0
  716. package/corpus/templates/clips/desktop/src/app.tsx +38 -13
  717. package/corpus/templates/clips/desktop/src/components/SourceRow.tsx +1 -0
  718. package/corpus/templates/clips/desktop/src/hooks/useMediaDevices.ts +88 -28
  719. package/corpus/templates/clips/desktop/src/hooks/useMeetingTranscription.ts +165 -55
  720. package/corpus/templates/clips/desktop/src/hooks/useMicMeter.ts +35 -10
  721. package/corpus/templates/clips/desktop/src/lib/backtrack.ts +12 -0
  722. package/corpus/templates/clips/desktop/src/lib/media-capture-constraints.ts +129 -34
  723. package/corpus/templates/clips/desktop/src/lib/media-device-selection.ts +157 -0
  724. package/corpus/templates/clips/desktop/src/lib/permissions.ts +2 -0
  725. package/corpus/templates/clips/desktop/src/lib/recorder.ts +109 -47
  726. package/corpus/templates/clips/desktop/src/lib/transcription-capture.ts +22 -2
  727. package/corpus/templates/clips/desktop/src/lib/transcription-engine.ts +29 -1
  728. package/corpus/templates/clips/desktop/src/lib/updater.ts +6 -0
  729. package/corpus/templates/clips/desktop/src/lib/voice-dictation.ts +372 -31
  730. package/corpus/templates/clips/desktop/src/overlays/finalizing.tsx +90 -0
  731. package/corpus/templates/clips/desktop/src/overlays/flow-bar.tsx +4 -6
  732. package/corpus/templates/clips/desktop/src/overlays/onboarding.tsx +214 -9
  733. package/corpus/templates/clips/desktop/src/overlays/recording-pill.tsx +37 -52
  734. package/corpus/templates/clips/desktop/src/styles.css +1 -1
  735. package/corpus/templates/clips/desktop/src-tauri/Cargo.toml +7 -0
  736. package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +371 -43
  737. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +79 -0
  738. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +1418 -260
  739. package/corpus/templates/clips/desktop/src-tauri/src/native_speech.rs +188 -44
  740. package/corpus/templates/clips/desktop/src-tauri/src/shortcuts.rs +197 -8
  741. package/corpus/templates/clips/desktop/src-tauri/src/silence_detector.rs +64 -8
  742. package/corpus/templates/clips/desktop/src-tauri/src/state.rs +3 -2
  743. package/corpus/templates/clips/desktop/src-tauri/src/system_audio.rs +10 -0
  744. package/corpus/templates/clips/desktop/src-tauri/src/tray.rs +29 -8
  745. package/corpus/templates/clips/desktop/src-tauri/src/whisper_speech.rs +136 -13
  746. package/corpus/templates/clips/desktop/vite.config.ts +3 -0
  747. package/corpus/templates/clips/package.json +1 -1
  748. package/corpus/templates/clips/server/db/schema.ts +26 -0
  749. package/corpus/templates/clips/server/jobs/builder-media-compression.ts +41 -0
  750. package/corpus/templates/clips/server/jobs/stale-meeting-sweeper.ts +294 -0
  751. package/corpus/templates/clips/server/lib/builder-media-compression.ts +976 -0
  752. package/corpus/templates/clips/server/lib/calendar-event-meetings.ts +107 -56
  753. package/corpus/templates/clips/server/lib/env-flags.ts +4 -0
  754. package/corpus/templates/clips/server/lib/google-calendar-client.ts +8 -1
  755. package/corpus/templates/clips/server/lib/media-worker.ts +153 -0
  756. package/corpus/templates/clips/server/lib/s3-upload-provider.ts +58 -17
  757. package/corpus/templates/clips/server/lib/streaming-upload-mode.ts +17 -5
  758. package/corpus/templates/clips/server/lib/video-remux.ts +90 -0
  759. package/corpus/templates/clips/server/plugins/calendar-jobs.ts +4 -0
  760. package/corpus/templates/clips/server/plugins/db.ts +196 -0
  761. package/corpus/templates/clips/server/register-secrets.ts +78 -0
  762. package/corpus/templates/clips/server/routes/_agent-native/clips/user-prefs.get.ts +15 -0
  763. package/corpus/templates/clips/server/routes/_agent-native/clips/user-prefs.put.ts +27 -0
  764. package/corpus/templates/clips/server/routes/api/media-worker/callback.post.ts +88 -0
  765. package/corpus/templates/clips/server/routes/api/public-recording.get.ts +87 -7
  766. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/abort.post.ts +11 -4
  767. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +55 -13
  768. package/corpus/templates/clips/server/routes/api/video/[recordingId].get.ts +24 -42
  769. package/corpus/templates/clips/server/routes/api/view-event.post.ts +29 -1
  770. package/corpus/templates/clips/shared/finalize-recovery.ts +23 -3
  771. package/corpus/templates/clips/shared/media-device-selection.ts +157 -0
  772. package/corpus/templates/clips/shared/media-worker-contract.ts +146 -0
  773. package/corpus/templates/content/.agents/skills/actions/SKILL.md +55 -0
  774. package/corpus/templates/content/.agents/skills/notion-integration/SKILL.md +67 -2
  775. package/corpus/templates/content/AGENTS.md +73 -65
  776. package/corpus/templates/content/actions/_builder-cms-read-client.ts +188 -8
  777. package/corpus/templates/content/actions/_builder-docs-client.ts +41 -3
  778. package/corpus/templates/content/actions/_database-source-utils.ts +998 -160
  779. package/corpus/templates/content/actions/_database-utils.ts +44 -4
  780. package/corpus/templates/content/actions/_local-file-documents.ts +90 -1
  781. package/corpus/templates/content/actions/_notion-action-utils.ts +12 -4
  782. package/corpus/templates/content/actions/add-content-database-source-field-property.ts +23 -1
  783. package/corpus/templates/content/actions/attach-content-database-source.ts +85 -12
  784. package/corpus/templates/content/actions/change-content-database-source-role.ts +9 -1
  785. package/corpus/templates/content/actions/delete-content-database.ts +1 -0
  786. package/corpus/templates/content/actions/delete-document.ts +8 -0
  787. package/corpus/templates/content/actions/get-document.ts +11 -2
  788. package/corpus/templates/content/actions/list-content-databases.ts +57 -9
  789. package/corpus/templates/content/actions/prepare-builder-source-review.ts +23 -6
  790. package/corpus/templates/content/actions/refresh-content-database-source.ts +26 -3
  791. package/corpus/templates/content/actions/remove-local-file-source.ts +100 -0
  792. package/corpus/templates/content/actions/restore-content-database.ts +1 -0
  793. package/corpus/templates/content/actions/sync-notion-comments.ts +174 -20
  794. package/corpus/templates/content/actions/update-document.ts +134 -5
  795. package/corpus/templates/content/app/blocks/SourceComponentBlock.tsx +31 -3
  796. package/corpus/templates/content/app/blocks/contentBlockRegistry.tsx +4 -4
  797. package/corpus/templates/content/app/components/EmptyState.tsx +1 -1
  798. package/corpus/templates/content/app/components/ThemeToggle.tsx +6 -6
  799. package/corpus/templates/content/app/components/editor/BubbleToolbar.tsx +5 -5
  800. package/corpus/templates/content/app/components/editor/BuilderBodySyncingNotice.tsx +21 -0
  801. package/corpus/templates/content/app/components/editor/CommentsSidebar.tsx +5 -5
  802. package/corpus/templates/content/app/components/editor/ContentReferencePreview.tsx +1 -1
  803. package/corpus/templates/content/app/components/editor/DocumentDatabase.tsx +9 -14630
  804. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +142 -79
  805. package/corpus/templates/content/app/components/editor/DocumentEditorSkeleton.tsx +36 -0
  806. package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +98 -46
  807. package/corpus/templates/content/app/components/editor/DocumentToolbar.tsx +29 -44
  808. package/corpus/templates/content/app/components/editor/EmojiPicker.tsx +5 -4
  809. package/corpus/templates/content/app/components/editor/LinkHoverPreview.tsx +5 -4
  810. package/corpus/templates/content/app/components/editor/NotionConflictBanner.tsx +1 -1
  811. package/corpus/templates/content/app/components/editor/NotionSyncBar.tsx +2 -2
  812. package/corpus/templates/content/app/components/editor/SlashCommandMenu.tsx +5 -5
  813. package/corpus/templates/content/app/components/editor/TableHoverControls.tsx +13 -13
  814. package/corpus/templates/content/app/components/editor/VersionHistoryPanel.tsx +10 -10
  815. package/corpus/templates/content/app/components/editor/VisualEditor.tsx +28 -11
  816. package/corpus/templates/content/app/components/editor/body-hydration.ts +112 -0
  817. package/corpus/templates/content/app/components/editor/builder-body-hydration-pump.ts +52 -0
  818. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +736 -69
  819. package/corpus/templates/content/app/components/editor/database/navigation-state.ts +36 -1
  820. package/corpus/templates/content/app/components/editor/database/settings.tsx +3224 -0
  821. package/corpus/templates/content/app/components/editor/database/shared.tsx +19 -19
  822. package/corpus/templates/content/app/components/editor/database/text.ts +23 -0
  823. package/corpus/templates/content/app/components/editor/database-sources/BuilderSourceReviewDialog.tsx +50 -9
  824. package/corpus/templates/content/app/components/editor/extensions/AudioBlock.tsx +19 -18
  825. package/corpus/templates/content/app/components/editor/extensions/ImageBlock.tsx +57 -31
  826. package/corpus/templates/content/app/components/editor/extensions/LocalMdxComponentNode.tsx +8 -8
  827. package/corpus/templates/content/app/components/editor/extensions/NotionExtensions.tsx +9 -0
  828. package/corpus/templates/content/app/components/editor/extensions/VideoBlock.tsx +19 -18
  829. package/corpus/templates/content/app/components/editor/previewDocumentSaveController.ts +21 -1
  830. package/corpus/templates/content/app/components/layout/Header.tsx +1 -5
  831. package/corpus/templates/content/app/components/layout/Layout.tsx +25 -8
  832. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +214 -56
  833. package/corpus/templates/content/app/components/sidebar/DocumentTreeItem.tsx +33 -30
  834. package/corpus/templates/content/app/components/sidebar/NotionButton.tsx +11 -11
  835. package/corpus/templates/content/app/components/sidebar/document-sidebar-sections.ts +109 -1
  836. package/corpus/templates/content/app/components/ui/alert-dialog.tsx +1 -0
  837. package/corpus/templates/content/app/components/ui/button.tsx +1 -0
  838. package/corpus/templates/content/app/components/ui/dialog.tsx +1 -0
  839. package/corpus/templates/content/app/components/ui/dropdown-menu.tsx +1 -0
  840. package/corpus/templates/content/app/components/ui/input.tsx +1 -0
  841. package/corpus/templates/content/app/components/ui/label.tsx +1 -0
  842. package/corpus/templates/content/app/components/ui/popover.tsx +1 -0
  843. package/corpus/templates/content/app/components/ui/separator.tsx +1 -0
  844. package/corpus/templates/content/app/components/ui/sheet.tsx +1 -0
  845. package/corpus/templates/content/app/components/ui/skeleton.tsx +1 -0
  846. package/corpus/templates/content/app/components/ui/sonner.tsx +1 -0
  847. package/corpus/templates/content/app/components/ui/spinner.tsx +1 -0
  848. package/corpus/templates/content/app/components/ui/table.tsx +1 -0
  849. package/corpus/templates/content/app/components/ui/toaster.tsx +1 -0
  850. package/corpus/templates/content/app/components/ui/toolkit-provider.tsx +12 -0
  851. package/corpus/templates/content/app/components/ui/tooltip.tsx +1 -0
  852. package/corpus/templates/content/app/global.css +42 -0
  853. package/corpus/templates/content/app/hooks/use-content-database.ts +346 -42
  854. package/corpus/templates/content/app/hooks/use-db-sync.ts +9 -0
  855. package/corpus/templates/content/app/hooks/use-documents.ts +136 -37
  856. package/corpus/templates/content/app/hooks/use-mobile.ts +1 -0
  857. package/corpus/templates/content/app/hooks/use-notion.ts +85 -15
  858. package/corpus/templates/content/app/i18n/zh-TW.ts +49 -0
  859. package/corpus/templates/content/app/i18n-data.ts +508 -0
  860. package/corpus/templates/content/app/root.tsx +57 -29
  861. package/corpus/templates/content/app/routes/_app._index.tsx +1 -1
  862. package/corpus/templates/content/app/routes/_app.local-files.tsx +268 -81
  863. package/corpus/templates/content/app/routes/_app.settings.tsx +4 -21
  864. package/corpus/templates/content/changelog/2026-07-01-builder-database-sources-keep-loading-rows-in-the-background.md +6 -0
  865. package/corpus/templates/content/changelog/2026-07-01-builder-source-attach-starts-with-usable-rows.md +6 -0
  866. package/corpus/templates/content/changelog/2026-07-01-builder-source-attachment-now-stays-connected-after-large.md +6 -0
  867. package/corpus/templates/content/changelog/2026-07-01-builder-source-components-show-preservation-and-review-states.md +6 -0
  868. package/corpus/templates/content/changelog/2026-07-01-builder-source-refreshes-now-show-progress-for-large-docs-blog.md +6 -0
  869. package/corpus/templates/content/changelog/2026-07-01-database-pages-now-delete-to-trash-without-route-errors.md +6 -0
  870. package/corpus/templates/content/changelog/2026-07-02-builder-source-refreshes-recover-from-stalled-background-row.md +6 -0
  871. package/corpus/templates/content/changelog/2026-07-02-builder-sourced-rows-open-with-sync-protection.md +6 -0
  872. package/corpus/templates/content/changelog/2026-07-02-database-rows-and-pages-open-faster-from-recently-loaded-dat.md +6 -0
  873. package/corpus/templates/content/changelog/2026-07-02-preview-panels-close-reliably-narrow-sidebar-row-actions-sta.md +6 -0
  874. package/corpus/templates/content/changelog/2026-07-06-builder-sync-progress-no-longer-shows-continue.md +6 -0
  875. package/corpus/templates/content/changelog/2026-07-06-document-headers-and-page-bodies-now-share-one-background-shade.md +6 -0
  876. package/corpus/templates/content/changelog/2026-07-06-export-submenus-stay-visible-outside-the-page-actions-menu.md +6 -0
  877. package/corpus/templates/content/changelog/2026-07-06-fixed-collaborative-documents-reapplying-content-before-save.md +6 -0
  878. package/corpus/templates/content/changelog/2026-07-06-local-files-now-remember-sidebar-collapse-state-and-include-.md +6 -0
  879. package/corpus/templates/content/changelog/2026-07-06-notion-comment-sync-now-preserves-threads-replies-stay-attac.md +6 -0
  880. package/corpus/templates/content/changelog/2026-07-06-notion-sync-is-far-more-reliable-auto-sync-no-longer-overwri.md +6 -0
  881. package/corpus/templates/content/changelog/2026-07-06-opening-pages-now-highlights-the-sidebar-item-and-shows-the-.md +6 -0
  882. package/corpus/templates/content/changelog/2026-07-06-presence-markers-in-the-editor-are-now-compact-instead-of-co.md +6 -0
  883. package/corpus/templates/content/changelog/2026-07-06-sidebar-collapse-now-keeps-the-same-background-shade-as-the-.md +6 -0
  884. package/corpus/templates/content/changelog/2026-07-06-the-content-header-no-longer-shows-the-global-notifications-.md +6 -0
  885. package/corpus/templates/content/parity/matrix.md +24 -24
  886. package/corpus/templates/content/parity/matrix.ts +7 -3
  887. package/corpus/templates/content/server/db/schema.ts +15 -0
  888. package/corpus/templates/content/server/lib/notion-sync.ts +481 -39
  889. package/corpus/templates/content/server/lib/notion.ts +212 -48
  890. package/corpus/templates/content/server/plugins/db.ts +20 -0
  891. package/corpus/templates/content/server/routes/api/documents/[id]/notion/resolve.post.ts +14 -2
  892. package/corpus/templates/content/server/routes/api/notion/callback.get.ts +54 -6
  893. package/corpus/templates/content/shared/api.ts +13 -0
  894. package/corpus/templates/content/shared/builder-mdx.ts +84 -15
  895. package/corpus/templates/content/shared/builder-source-component-registry.ts +177 -0
  896. package/corpus/templates/content/shared/nfm.ts +459 -66
  897. package/corpus/templates/content/shared/source-component-block.ts +72 -6
  898. package/corpus/templates/content/vite.config.ts +17 -1
  899. package/corpus/templates/design/.agents/skills/actions/SKILL.md +55 -0
  900. package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +44 -25
  901. package/corpus/templates/design/.agents/skills/responsive-breakpoints/SKILL.md +98 -0
  902. package/corpus/templates/design/.agents/skills/shader-fills/SKILL.md +162 -0
  903. package/corpus/templates/design/AGENTS.md +12 -4
  904. package/corpus/templates/design/actions/add-breakpoint.ts +6 -3
  905. package/corpus/templates/design/actions/apply-a11y-fix.ts +57 -37
  906. package/corpus/templates/design/actions/apply-component-prop-edit.ts +41 -20
  907. package/corpus/templates/design/actions/apply-motion-edit.ts +251 -38
  908. package/corpus/templates/design/actions/apply-shader-fill.ts +90 -66
  909. package/corpus/templates/design/actions/apply-visual-edit.ts +282 -55
  910. package/corpus/templates/design/actions/create-component.ts +29 -60
  911. package/corpus/templates/design/actions/create-file.ts +9 -2
  912. package/corpus/templates/design/actions/duplicate-design.ts +31 -2
  913. package/corpus/templates/design/actions/edit-design.ts +36 -38
  914. package/corpus/templates/design/actions/generate-design-title.ts +87 -0
  915. package/corpus/templates/design/actions/generate-design.ts +53 -18
  916. package/corpus/templates/design/actions/generate-screens.ts +108 -4
  917. package/corpus/templates/design/actions/get-motion-timeline.ts +35 -7
  918. package/corpus/templates/design/actions/import-design-source.ts +8 -58
  919. package/corpus/templates/design/actions/insert-asset.ts +34 -29
  920. package/corpus/templates/design/actions/insert-design-native-asset.ts +129 -32
  921. package/corpus/templates/design/actions/insert-figma-library-asset.ts +35 -30
  922. package/corpus/templates/design/actions/present-design-variants.ts +7 -2
  923. package/corpus/templates/design/actions/remove-motion-timeline.ts +31 -36
  924. package/corpus/templates/design/actions/run-design-audit.ts +127 -1
  925. package/corpus/templates/design/actions/take-design-screenshot.ts +673 -0
  926. package/corpus/templates/design/actions/update-file.ts +190 -75
  927. package/corpus/templates/design/app/components/ThemeToggle.tsx +6 -6
  928. package/corpus/templates/design/app/components/design/BreakpointBar.tsx +403 -0
  929. package/corpus/templates/design/app/components/design/CanvasContextMenu.tsx +480 -233
  930. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +708 -53
  931. package/corpus/templates/design/app/components/design/DesignEditorSkeleton.tsx +2 -2
  932. package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +99 -24
  933. package/corpus/templates/design/app/components/design/DesignImportPanel.tsx +6 -97
  934. package/corpus/templates/design/app/components/design/DrawOverlay.tsx +7 -7
  935. package/corpus/templates/design/app/components/design/EditPanel.tsx +1120 -6977
  936. package/corpus/templates/design/app/components/design/FusionAppBanner.tsx +9 -8
  937. package/corpus/templates/design/app/components/design/LayersPanel.tsx +433 -99
  938. package/corpus/templates/design/app/components/design/LocalhostWriteConsentDialog.tsx +6 -5
  939. package/corpus/templates/design/app/components/design/MotionDock.tsx +1138 -117
  940. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +2610 -1398
  941. package/corpus/templates/design/app/components/design/QuestionFlow.tsx +3 -3
  942. package/corpus/templates/design/app/components/design/ReadOnlyDesignBanner.tsx +22 -0
  943. package/corpus/templates/design/app/components/design/ReviewPanel.tsx +3 -3
  944. package/corpus/templates/design/app/components/design/StatesPanel.tsx +13 -13
  945. package/corpus/templates/design/app/components/design/TokensPanel.tsx +14 -14
  946. package/corpus/templates/design/app/components/design/TweaksPanel.tsx +6 -6
  947. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +2221 -244
  948. package/corpus/templates/design/app/components/design/bridge/motion-preview.bridge.ts +187 -11
  949. package/corpus/templates/design/app/components/design/bridge/shader-fill-preview.bridge.ts +376 -1
  950. package/corpus/templates/design/app/components/design/bridge/shader-runtime.bridge.ts +746 -0
  951. package/corpus/templates/design/app/components/design/canvas-primitive-style.ts +17 -0
  952. package/corpus/templates/design/app/components/design/code-workbench/ActivityBar.tsx +3 -3
  953. package/corpus/templates/design/app/components/design/code-workbench/SideBar.tsx +5 -8
  954. package/corpus/templates/design/app/components/design/code-workbench/editor/EditorTabs.tsx +6 -6
  955. package/corpus/templates/design/app/components/design/code-workbench/editor/MonacoHost.tsx +2 -1
  956. package/corpus/templates/design/app/components/design/code-workbench/explorer/FileTree.tsx +18 -20
  957. package/corpus/templates/design/app/components/design/code-workbench/quickinput/QuickInput.tsx +1 -1
  958. package/corpus/templates/design/app/components/design/code-workbench/search/SearchView.tsx +6 -6
  959. package/corpus/templates/design/app/components/design/edit-panel/appearance-properties.tsx +518 -0
  960. package/corpus/templates/design/app/components/design/edit-panel/code-inspect-helpers.tsx +562 -0
  961. package/corpus/templates/design/app/components/design/edit-panel/component-section.tsx +763 -0
  962. package/corpus/templates/design/app/components/design/edit-panel/document-colors.ts +94 -0
  963. package/corpus/templates/design/app/components/design/edit-panel/effects-properties.tsx +780 -0
  964. package/corpus/templates/design/app/components/design/edit-panel/element-classification.ts +446 -0
  965. package/corpus/templates/design/app/components/design/edit-panel/element-identity.ts +79 -0
  966. package/corpus/templates/design/app/components/design/edit-panel/field-primitives.tsx +315 -0
  967. package/corpus/templates/design/app/components/design/edit-panel/fill-gradient-helpers.ts +344 -0
  968. package/corpus/templates/design/app/components/design/edit-panel/fill-properties.tsx +594 -0
  969. package/corpus/templates/design/app/components/design/edit-panel/frame-presets-panel.tsx +119 -0
  970. package/corpus/templates/design/app/components/design/edit-panel/inspector-controls.tsx +248 -0
  971. package/corpus/templates/design/app/components/design/edit-panel/interaction-state-helpers.ts +45 -0
  972. package/corpus/templates/design/app/components/design/edit-panel/layout-properties.tsx +693 -0
  973. package/corpus/templates/design/app/components/design/edit-panel/panel-primitives.tsx +724 -0
  974. package/corpus/templates/design/app/components/design/edit-panel/position-helpers.ts +207 -0
  975. package/corpus/templates/design/app/components/design/edit-panel/position-layout-properties.tsx +628 -0
  976. package/corpus/templates/design/app/components/design/edit-panel/selection-helpers.ts +91 -0
  977. package/corpus/templates/design/app/components/design/edit-panel/stroke-properties.tsx +649 -0
  978. package/corpus/templates/design/app/components/design/edit-panel/style-change-types.ts +118 -0
  979. package/corpus/templates/design/app/components/design/edit-panel/style-options.ts +88 -0
  980. package/corpus/templates/design/app/components/design/edit-panel/transform-helpers.ts +100 -0
  981. package/corpus/templates/design/app/components/design/edit-panel/typography-helpers.ts +117 -0
  982. package/corpus/templates/design/app/components/design/edit-panel/typography-properties.tsx +501 -0
  983. package/corpus/templates/design/app/components/design/inspector/AlignmentMatrix.tsx +3 -3
  984. package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +98 -47
  985. package/corpus/templates/design/app/components/design/inspector/BreakpointOverrideIndicator.tsx +82 -0
  986. package/corpus/templates/design/app/components/design/inspector/ConstraintsWidget.tsx +3 -3
  987. package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +207 -105
  988. package/corpus/templates/design/app/components/design/inspector/ExportSettingsPanel.tsx +6 -6
  989. package/corpus/templates/design/app/components/design/inspector/GlslShaderPanel.tsx +1097 -0
  990. package/corpus/templates/design/app/components/design/inspector/GradientEditor.tsx +44 -0
  991. package/corpus/templates/design/app/components/design/inspector/ImageFillControls.tsx +6 -6
  992. package/corpus/templates/design/app/components/design/inspector/InteractionStatePanel.tsx +258 -0
  993. package/corpus/templates/design/app/components/design/inspector/MotionKeyframeDiamond.tsx +144 -0
  994. package/corpus/templates/design/app/components/design/inspector/ScrubInput.tsx +106 -16
  995. package/corpus/templates/design/app/components/design/inspector/ShaderControls.tsx +14 -14
  996. package/corpus/templates/design/app/components/design/inspector/ShaderEffectRow.tsx +9 -9
  997. package/corpus/templates/design/app/components/design/inspector/ShaderFillsPanel.tsx +6 -6
  998. package/corpus/templates/design/app/components/design/inspector/frame-size-presets.ts +116 -0
  999. package/corpus/templates/design/app/components/design/inspector/index.ts +26 -0
  1000. package/corpus/templates/design/app/components/design/inspector/scrub-input-utils.ts +29 -0
  1001. package/corpus/templates/design/app/components/design/multi-screen/alt-hover-measurement.ts +171 -0
  1002. package/corpus/templates/design/app/components/design/multi-screen/board-surface-html.ts +220 -0
  1003. package/corpus/templates/design/app/components/design/multi-screen/chrome-transitions.ts +35 -0
  1004. package/corpus/templates/design/app/components/design/multi-screen/cross-screen-drop.ts +134 -0
  1005. package/corpus/templates/design/app/components/design/multi-screen/culling.ts +185 -0
  1006. package/corpus/templates/design/app/components/design/multi-screen/gradient-overlay-geometry.ts +120 -0
  1007. package/corpus/templates/design/app/components/design/multi-screen/types.ts +892 -0
  1008. package/corpus/templates/design/app/components/design/multi-screen/vector-edit-geometry.ts +21 -0
  1009. package/corpus/templates/design/app/components/design/types.ts +15 -0
  1010. package/corpus/templates/design/app/components/design-system/DesignSystemCard.tsx +3 -2
  1011. package/corpus/templates/design/app/components/editor/PromptDialog.tsx +124 -23
  1012. package/corpus/templates/design/app/components/layout/Header.tsx +2 -2
  1013. package/corpus/templates/design/app/components/layout/Sidebar.tsx +5 -5
  1014. package/corpus/templates/design/app/components/ui/alert-dialog.tsx +1 -0
  1015. package/corpus/templates/design/app/components/ui/avatar.tsx +1 -0
  1016. package/corpus/templates/design/app/components/ui/badge.tsx +1 -0
  1017. package/corpus/templates/design/app/components/ui/button.tsx +1 -0
  1018. package/corpus/templates/design/app/components/ui/card.tsx +1 -0
  1019. package/corpus/templates/design/app/components/ui/checkbox.tsx +1 -0
  1020. package/corpus/templates/design/app/components/ui/collapsible.tsx +1 -0
  1021. package/corpus/templates/design/app/components/ui/context-menu.tsx +1 -0
  1022. package/corpus/templates/design/app/components/ui/dialog.tsx +1 -0
  1023. package/corpus/templates/design/app/components/ui/dropdown-menu.tsx +1 -0
  1024. package/corpus/templates/design/app/components/ui/input.tsx +1 -0
  1025. package/corpus/templates/design/app/components/ui/label.tsx +1 -0
  1026. package/corpus/templates/design/app/components/ui/popover.tsx +1 -0
  1027. package/corpus/templates/design/app/components/ui/select.tsx +1 -0
  1028. package/corpus/templates/design/app/components/ui/separator.tsx +1 -0
  1029. package/corpus/templates/design/app/components/ui/sheet.tsx +1 -0
  1030. package/corpus/templates/design/app/components/ui/skeleton.tsx +1 -0
  1031. package/corpus/templates/design/app/components/ui/slider.tsx +1 -0
  1032. package/corpus/templates/design/app/components/ui/sonner.tsx +1 -0
  1033. package/corpus/templates/design/app/components/ui/spinner.tsx +1 -0
  1034. package/corpus/templates/design/app/components/ui/switch.tsx +1 -0
  1035. package/corpus/templates/design/app/components/ui/tabs.tsx +1 -0
  1036. package/corpus/templates/design/app/components/ui/textarea.tsx +1 -0
  1037. package/corpus/templates/design/app/components/ui/toolkit-provider.tsx +12 -0
  1038. package/corpus/templates/design/app/components/ui/tooltip.tsx +1 -0
  1039. package/corpus/templates/design/app/components/visual-editor/CanvasCommentPins.tsx +7 -7
  1040. package/corpus/templates/design/app/components/visual-editor/DrawOverlay.tsx +7 -7
  1041. package/corpus/templates/design/app/components/visual-editor/SaveStatusIndicator.tsx +3 -3
  1042. package/corpus/templates/design/app/global.css +9 -8
  1043. package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +259 -2
  1044. package/corpus/templates/design/app/i18n/zh-TW.ts +166 -13
  1045. package/corpus/templates/design/app/i18n-breakpoints.ts +205 -0
  1046. package/corpus/templates/design/app/i18n-data.ts +1702 -187
  1047. package/corpus/templates/design/app/i18n-motion.ts +563 -0
  1048. package/corpus/templates/design/app/lib/design-import.ts +73 -0
  1049. package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +6 -6
  1050. package/corpus/templates/design/app/pages/DesignSystems.tsx +31 -31
  1051. package/corpus/templates/design/app/pages/Index.tsx +66 -51
  1052. package/corpus/templates/design/app/pages/NotFound.tsx +2 -1
  1053. package/corpus/templates/design/app/pages/Present.tsx +2 -1
  1054. package/corpus/templates/design/app/pages/VisualEdit.tsx +2 -1
  1055. package/corpus/templates/design/app/root.tsx +7 -4
  1056. package/corpus/templates/design/app/routes/settings.tsx +22 -49
  1057. package/corpus/templates/design/changelog/2026-06-29-design-inspector-controls-now-match-figmas-softer-dark-chrome.md +1 -1
  1058. package/corpus/templates/design/changelog/2026-06-30-design-now-has-a-figma-style-left-rail-for-file-agent-assets.md +1 -1
  1059. package/corpus/templates/design/changelog/2026-06-30-design-tools-now-use-a-quieter-figma-style-list-layout.md +1 -1
  1060. package/corpus/templates/design/changelog/2026-06-30-multi-select-outlines-now-stay-consistent-across-all-screens.md +1 -1
  1061. package/corpus/templates/design/changelog/2026-07-03-canvas-interactions-now-match-figma-rotation-aware-resizing-.md +1 -1
  1062. package/corpus/templates/design/changelog/2026-07-03-layers-panel-matches-figma-top-layer-on-top-drag-with-auto-s.md +1 -1
  1063. package/corpus/templates/design/changelog/2026-07-05-code-sidebar-tabs-now-use-a-quieter-active-highlight-that-ma.md +6 -0
  1064. package/corpus/templates/design/changelog/2026-07-05-designs-get-ai-generated-names-from-your-prompt.md +6 -0
  1065. package/corpus/templates/design/changelog/2026-07-05-local-code-screens-keep-their-connection-when-duplicated-and.md +6 -0
  1066. package/corpus/templates/design/changelog/2026-07-05-press-a-to-draw-screens-on-the-canvas-inside-a-screen-it-dr.md +6 -0
  1067. package/corpus/templates/design/changelog/2026-07-06-3d-rotation-and-perspective-controls-on-every-element.md +6 -0
  1068. package/corpus/templates/design/changelog/2026-07-06-ai-generated-and-duplicated-screens-now-get-their-editor-sel.md +6 -0
  1069. package/corpus/templates/design/changelog/2026-07-06-applying-shaders-and-fills-in-quick-succession-can-no-longer.md +6 -0
  1070. package/corpus/templates/design/changelog/2026-07-06-breakpoint-frames-in-the-overview-now-support-click-to-selec.md +6 -0
  1071. package/corpus/templates/design/changelog/2026-07-06-breakpoint-segments-in-the-inspector-now-show-a-device-icon-.md +6 -0
  1072. package/corpus/templates/design/changelog/2026-07-06-breakpoint-targeting-now-lives-in-one-compact-control-in-the.md +6 -0
  1073. package/corpus/templates/design/changelog/2026-07-06-canvas-edits-can-no-longer-be-overwritten-by-stale-collabora.md +6 -0
  1074. package/corpus/templates/design/changelog/2026-07-06-canvas-edits-inside-screens-save-reliably-again.md +6 -0
  1075. package/corpus/templates/design/changelog/2026-07-06-cleaner-inspector-removed-the-ask-ai-box-from-the-design-tab.md +6 -0
  1076. package/corpus/templates/design/changelog/2026-07-06-clicking-empty-canvas-space-now-deselects-an-element-inside-.md +6 -0
  1077. package/corpus/templates/design/changelog/2026-07-06-dragging-an-element-onto-a-rectangle-now-nests-it-inside-wit.md +6 -0
  1078. package/corpus/templates/design/changelog/2026-07-06-dragging-screens-and-objects-on-the-canvas-is-now-much-smoot.md +6 -0
  1079. package/corpus/templates/design/changelog/2026-07-06-drawing-a-shape-or-text-on-the-canvas-no-longer-flashes-away.md +6 -0
  1080. package/corpus/templates/design/changelog/2026-07-06-dropping-an-element-between-two-others-inside-a-screen-now-s.md +6 -0
  1081. package/corpus/templates/design/changelog/2026-07-06-duplicating-a-screen-now-places-the-copy-exactly-where-you-d.md +6 -0
  1082. package/corpus/templates/design/changelog/2026-07-06-edit-gradients-directly-on-elements-inside-screens-scale-str.md +6 -0
  1083. package/corpus/templates/design/changelog/2026-07-06-edits-from-collaborators-and-the-agent-now-update-the-canvas.md +6 -0
  1084. package/corpus/templates/design/changelog/2026-07-06-fig-files-now-stay-on-the-builder-indexing-path-through-desi.md +6 -0
  1085. package/corpus/templates/design/changelog/2026-07-06-fixed-a-bug-where-most-style-position-and-text-edits-briefly.md +6 -0
  1086. package/corpus/templates/design/changelog/2026-07-06-fixed-a-confusing-raw-error-message-when-a-file-upload-fails.md +6 -0
  1087. package/corpus/templates/design/changelog/2026-07-06-fixed-a-rare-case-where-an-edit-could-be-silently-overwritte.md +6 -0
  1088. package/corpus/templates/design/changelog/2026-07-06-fixed-inserted-assets-sometimes-disappearing-reappearing-or-.md +6 -0
  1089. package/corpus/templates/design/changelog/2026-07-06-fixed-inspector-value-scrubbing-producing-erratic-numbers-an.md +6 -0
  1090. package/corpus/templates/design/changelog/2026-07-06-fixed-padding-gap-and-min-max-size-scrubbing-not-updating-th.md +6 -0
  1091. package/corpus/templates/design/changelog/2026-07-06-fixed-text-stroke-color-turning-black-and-glyphs-disappearin.md +6 -0
  1092. package/corpus/templates/design/changelog/2026-07-06-fixed-the-collapsed-effects-and-stroke-inspector-sections-sh.md +6 -0
  1093. package/corpus/templates/design/changelog/2026-07-06-fixed-the-new-design-dialog-closing-unexpectedly-when-using-.md +6 -0
  1094. package/corpus/templates/design/changelog/2026-07-06-fixed-the-typography-section-missing-when-selecting-text-nes.md +6 -0
  1095. package/corpus/templates/design/changelog/2026-07-06-full-view-no-longer-flashes-back-to-the-overview-on-entry-no.md +6 -0
  1096. package/corpus/templates/design/changelog/2026-07-06-keyframe-animation-timeline-per-property-keyframes-easing-cu.md +6 -0
  1097. package/corpus/templates/design/changelog/2026-07-06-mobile-layouts-no-longer-stack-two-top-headers.md +5 -0
  1098. package/corpus/templates/design/changelog/2026-07-06-moving-restyling-and-scrubbing-values-on-elements-in-ai-gene.md +6 -0
  1099. package/corpus/templates/design/changelog/2026-07-06-padding-and-corner-radius-value-scrubbing-now-snaps-to-whole.md +6 -0
  1100. package/corpus/templates/design/changelog/2026-07-06-padding-handles-now-only-resize-on-the-handle-line-itself-sh.md +6 -0
  1101. package/corpus/templates/design/changelog/2026-07-06-preview-and-edit-hover-focus-and-pressed-states-with-real-cs.md +6 -0
  1102. package/corpus/templates/design/changelog/2026-07-06-promoting-an-element-to-a-component-now-works-correctly-for-.md +6 -0
  1103. package/corpus/templates/design/changelog/2026-07-06-reduced-url-history-churn-and-selection-overlay-flicker-whil.md +6 -0
  1104. package/corpus/templates/design/changelog/2026-07-06-removed-the-decorative-dot-before-screen-names-in-the-overvi.md +6 -0
  1105. package/corpus/templates/design/changelog/2026-07-06-responsive-breakpoints-edit-at-any-width-with-cascading-over.md +6 -0
  1106. package/corpus/templates/design/changelog/2026-07-06-screen-labels-no-longer-turn-blue-when-you-hover-inside-the-.md +6 -0
  1107. package/corpus/templates/design/changelog/2026-07-06-scrolling-a-screen-with-an-element-selected-stays-smooth-on-.md +6 -0
  1108. package/corpus/templates/design/changelog/2026-07-06-selecting-multiple-elements-and-dragging-now-moves-the-whole.md +6 -0
  1109. package/corpus/templates/design/changelog/2026-07-06-selection-borders-handles-and-value-readouts-now-stay-the-sa.md +6 -0
  1110. package/corpus/templates/design/changelog/2026-07-06-selection-handles-and-editing-chrome-now-stay-a-constant-siz.md +6 -0
  1111. package/corpus/templates/design/changelog/2026-07-06-shader-fills-and-effects-ai-authored-or-preset-glsl-shaders-.md +6 -0
  1112. package/corpus/templates/design/changelog/2026-07-06-text-drawn-on-the-canvas-now-defaults-to-white-and-inter-in-.md +6 -0
  1113. package/corpus/templates/design/changelog/2026-07-06-text-dropped-into-a-shape-now-stays-readable-instead-of-disa.md +6 -0
  1114. package/corpus/templates/design/changelog/2026-07-06-the-add-tweak-controls-prompt-no-longer-jumps-to-the-corner-.md +6 -0
  1115. package/corpus/templates/design/changelog/2026-07-06-the-design-agent-now-visually-reviews-rendered-screenshots-o.md +6 -0
  1116. package/corpus/templates/design/changelog/2026-07-06-the-padding-value-readout-now-stays-visible-while-you-hover-.md +6 -0
  1117. package/corpus/templates/design/changelog/2026-07-06-the-selected-element-attachment-now-clears-from-the-chat-com.md +6 -0
  1118. package/corpus/templates/design/changelog/2026-07-06-the-zoom-menu-is-more-compact-with-tight-rows-and-right-alig.md +6 -0
  1119. package/corpus/templates/design/changelog/2026-07-06-undo-and-redo-no-longer-flash-the-screen-changes-now-apply-i.md +6 -0
  1120. package/corpus/templates/design/changelog/2026-07-06-zoom-presets-like-zoom-to-50-now-stay-on-the-focused-screen-.md +6 -0
  1121. package/corpus/templates/design/server/handlers/import-design-file.ts +4 -43
  1122. package/corpus/templates/design/server/handlers/index-design-system-with-builder.ts +2 -2
  1123. package/corpus/templates/design/server/lib/import-design-files.ts +11 -2
  1124. package/corpus/templates/design/server/lib/visible-clipboard-html.ts +32 -0
  1125. package/corpus/templates/design/server/source-workspace.ts +139 -59
  1126. package/corpus/templates/design/shared/board-file.ts +11 -2
  1127. package/corpus/templates/design/shared/breakpoint-media.ts +543 -0
  1128. package/corpus/templates/design/shared/canvas-math.ts +354 -35
  1129. package/corpus/templates/design/shared/code-layer.ts +277 -8
  1130. package/corpus/templates/design/shared/design-review.ts +1 -0
  1131. package/corpus/templates/design/shared/design-state.ts +11 -4
  1132. package/corpus/templates/design/shared/interaction-states.ts +548 -0
  1133. package/corpus/templates/design/shared/motion-compiler.ts +186 -23
  1134. package/corpus/templates/design/shared/motion-easing.ts +294 -0
  1135. package/corpus/templates/design/shared/motion-timeline.ts +400 -34
  1136. package/corpus/templates/design/shared/prompt-title.ts +95 -0
  1137. package/corpus/templates/design/shared/responsive-classes.ts +448 -10
  1138. package/corpus/templates/design/shared/screen-annotation.ts +46 -0
  1139. package/corpus/templates/design/shared/shader-fills.ts +1009 -0
  1140. package/corpus/templates/design/shared/shader-presets.ts +852 -4
  1141. package/corpus/templates/dispatch/.agents/skills/actions/SKILL.md +55 -0
  1142. package/corpus/templates/dispatch/actions/list-staged-datasets.ts +3 -0
  1143. package/corpus/templates/dispatch/app/routes/settings.tsx +3 -17
  1144. package/corpus/templates/forms/.agents/skills/actions/SKILL.md +55 -0
  1145. package/corpus/templates/forms/app/components/CloudUpgrade.tsx +10 -10
  1146. package/corpus/templates/forms/app/components/ThemeToggle.tsx +5 -5
  1147. package/corpus/templates/forms/app/components/builder/FieldPropertiesPanel.tsx +11 -10
  1148. package/corpus/templates/forms/app/components/builder/FieldRenderer.tsx +10 -13
  1149. package/corpus/templates/forms/app/components/layout/Sidebar.tsx +17 -17
  1150. package/corpus/templates/forms/app/components/ui/alert-dialog.tsx +1 -0
  1151. package/corpus/templates/forms/app/components/ui/badge.tsx +1 -0
  1152. package/corpus/templates/forms/app/components/ui/button.tsx +1 -0
  1153. package/corpus/templates/forms/app/components/ui/calendar.tsx +1 -1
  1154. package/corpus/templates/forms/app/components/ui/card.tsx +1 -0
  1155. package/corpus/templates/forms/app/components/ui/checkbox.tsx +1 -0
  1156. package/corpus/templates/forms/app/components/ui/command.tsx +1 -1
  1157. package/corpus/templates/forms/app/components/ui/dialog.tsx +1 -0
  1158. package/corpus/templates/forms/app/components/ui/dropdown-menu.tsx +1 -0
  1159. package/corpus/templates/forms/app/components/ui/input.tsx +1 -0
  1160. package/corpus/templates/forms/app/components/ui/label.tsx +1 -0
  1161. package/corpus/templates/forms/app/components/ui/popover.tsx +1 -0
  1162. package/corpus/templates/forms/app/components/ui/radio-group.tsx +1 -0
  1163. package/corpus/templates/forms/app/components/ui/scroll-area.tsx +1 -0
  1164. package/corpus/templates/forms/app/components/ui/select.tsx +1 -0
  1165. package/corpus/templates/forms/app/components/ui/separator.tsx +1 -0
  1166. package/corpus/templates/forms/app/components/ui/skeleton.tsx +1 -0
  1167. package/corpus/templates/forms/app/components/ui/slider.tsx +1 -0
  1168. package/corpus/templates/forms/app/components/ui/switch.tsx +1 -0
  1169. package/corpus/templates/forms/app/components/ui/table.tsx +1 -0
  1170. package/corpus/templates/forms/app/components/ui/tabs.tsx +1 -0
  1171. package/corpus/templates/forms/app/components/ui/textarea.tsx +1 -0
  1172. package/corpus/templates/forms/app/components/ui/toolkit-provider.tsx +12 -0
  1173. package/corpus/templates/forms/app/components/ui/tooltip.tsx +1 -0
  1174. package/corpus/templates/forms/app/global.css +1 -0
  1175. package/corpus/templates/forms/app/hooks/use-mobile.ts +1 -0
  1176. package/corpus/templates/forms/app/hooks/use-navigation-state.ts +0 -1
  1177. package/corpus/templates/forms/app/pages/FormBuilderPage.tsx +24 -26
  1178. package/corpus/templates/forms/app/pages/FormFillPage.tsx +2 -2
  1179. package/corpus/templates/forms/app/pages/FormsListPage.tsx +21 -21
  1180. package/corpus/templates/forms/app/pages/ResponseInsightsPage.tsx +11 -11
  1181. package/corpus/templates/forms/app/pages/ResponsesPage.tsx +4 -4
  1182. package/corpus/templates/forms/app/root.tsx +11 -8
  1183. package/corpus/templates/forms/app/routes/_app.settings.tsx +6 -21
  1184. package/corpus/templates/forms/app/routes/form-preview.tsx +3 -3
  1185. package/corpus/templates/forms/changelog/2026-07-06-the-form-builder-header-no-longer-shows-the-global-notificat.md +6 -0
  1186. package/corpus/templates/macros/app/components/AddExerciseDialog.tsx +1 -1
  1187. package/corpus/templates/macros/app/components/AddMealDialog.tsx +1 -1
  1188. package/corpus/templates/macros/app/components/AddWeightDialog.tsx +1 -1
  1189. package/corpus/templates/macros/app/components/DailyProgress.tsx +7 -7
  1190. package/corpus/templates/macros/app/components/WeeklyCaloriesChart.tsx +2 -1
  1191. package/corpus/templates/macros/app/components/WeightCard.tsx +10 -10
  1192. package/corpus/templates/macros/app/components/WeightTracker.tsx +1 -1
  1193. package/corpus/templates/macros/app/components/layout/AppLayout.tsx +6 -10
  1194. package/corpus/templates/macros/app/components/ui/alert-dialog.tsx +1 -0
  1195. package/corpus/templates/macros/app/components/ui/label.tsx +1 -0
  1196. package/corpus/templates/macros/app/components/ui/select.tsx +1 -0
  1197. package/corpus/templates/macros/app/components/ui/sheet.tsx +1 -0
  1198. package/corpus/templates/macros/app/components/ui/skeleton.tsx +1 -0
  1199. package/corpus/templates/macros/app/components/ui/sonner.tsx +1 -0
  1200. package/corpus/templates/macros/app/components/ui/toolkit-provider.tsx +12 -0
  1201. package/corpus/templates/macros/app/components/ui/tooltip.tsx +1 -0
  1202. package/corpus/templates/macros/app/global.css +1 -0
  1203. package/corpus/templates/macros/app/root.tsx +17 -14
  1204. package/corpus/templates/macros/app/routes/_index.tsx +1 -1
  1205. package/corpus/templates/macros/app/routes/analytics.tsx +8 -8
  1206. package/corpus/templates/macros/app/routes/settings.tsx +4 -19
  1207. package/corpus/templates/mail/.agents/skills/actions/SKILL.md +55 -0
  1208. package/corpus/templates/mail/.agents/skills/contacts-and-crm/SKILL.md +67 -0
  1209. package/corpus/templates/mail/.agents/skills/draft-queue/SKILL.md +35 -1
  1210. package/corpus/templates/mail/.agents/skills/email-drafts/SKILL.md +74 -33
  1211. package/corpus/templates/mail/AGENTS.md +102 -16
  1212. package/corpus/templates/mail/actions/archive-email.ts +67 -11
  1213. package/corpus/templates/mail/actions/manage-snippets.ts +106 -0
  1214. package/corpus/templates/mail/actions/mark-read.ts +45 -11
  1215. package/corpus/templates/mail/actions/send-queued-drafts.ts +77 -42
  1216. package/corpus/templates/mail/actions/star-email.ts +55 -10
  1217. package/corpus/templates/mail/actions/trash-email.ts +48 -9
  1218. package/corpus/templates/mail/app/components/GoogleConnectBanner.tsx +1 -1
  1219. package/corpus/templates/mail/app/components/ThemeToggle.tsx +6 -6
  1220. package/corpus/templates/mail/app/components/agent-engine-picker.tsx +10 -10
  1221. package/corpus/templates/mail/app/components/email/CodeBlockLangPicker.tsx +4 -3
  1222. package/corpus/templates/mail/app/components/email/ComposeBubbleToolbar.tsx +5 -5
  1223. package/corpus/templates/mail/app/components/email/ComposeEditor.tsx +109 -10
  1224. package/corpus/templates/mail/app/components/email/ComposeModal.tsx +44 -19
  1225. package/corpus/templates/mail/app/components/email/ComposeSlashMenu.tsx +139 -17
  1226. package/corpus/templates/mail/app/components/email/EmailList.tsx +522 -314
  1227. package/corpus/templates/mail/app/components/email/EmailListItem.tsx +130 -45
  1228. package/corpus/templates/mail/app/components/email/EmailThread.tsx +9 -9
  1229. package/corpus/templates/mail/app/components/email/InlineReplyComposer.tsx +35 -13
  1230. package/corpus/templates/mail/app/components/email/IntegrationsSidebar.tsx +13 -13
  1231. package/corpus/templates/mail/app/components/email/MobileActionBar.tsx +13 -13
  1232. package/corpus/templates/mail/app/components/email/RecipientInput.tsx +53 -42
  1233. package/corpus/templates/mail/app/components/email/SendLaterButton.tsx +5 -4
  1234. package/corpus/templates/mail/app/components/email/SnoozeModal.tsx +1 -1
  1235. package/corpus/templates/mail/app/components/email/SnoozePopover.tsx +6 -6
  1236. package/corpus/templates/mail/app/components/layout/AppLayout.tsx +19 -23
  1237. package/corpus/templates/mail/app/components/layout/HeaderActions.tsx +12 -4
  1238. package/corpus/templates/mail/app/components/layout/SearchBar.tsx +138 -18
  1239. package/corpus/templates/mail/app/components/settings/GmailFiltersSection.tsx +23 -23
  1240. package/corpus/templates/mail/app/components/settings/SnippetsSection.tsx +303 -0
  1241. package/corpus/templates/mail/app/components/ui/alert-dialog.tsx +1 -0
  1242. package/corpus/templates/mail/app/components/ui/badge.tsx +1 -0
  1243. package/corpus/templates/mail/app/components/ui/button.tsx +1 -0
  1244. package/corpus/templates/mail/app/components/ui/calendar.tsx +1 -1
  1245. package/corpus/templates/mail/app/components/ui/command.tsx +1 -1
  1246. package/corpus/templates/mail/app/components/ui/dialog.tsx +1 -0
  1247. package/corpus/templates/mail/app/components/ui/drawer.tsx +1 -0
  1248. package/corpus/templates/mail/app/components/ui/dropdown-menu.tsx +1 -0
  1249. package/corpus/templates/mail/app/components/ui/popover.tsx +1 -0
  1250. package/corpus/templates/mail/app/components/ui/select.tsx +1 -0
  1251. package/corpus/templates/mail/app/components/ui/skeleton.tsx +1 -0
  1252. package/corpus/templates/mail/app/components/ui/spinner.tsx +1 -0
  1253. package/corpus/templates/mail/app/components/ui/switch.tsx +1 -0
  1254. package/corpus/templates/mail/app/components/ui/toolkit-provider.tsx +12 -0
  1255. package/corpus/templates/mail/app/components/ui/tooltip.tsx +1 -0
  1256. package/corpus/templates/mail/app/global.css +20 -3
  1257. package/corpus/templates/mail/app/hooks/use-emails.ts +187 -0
  1258. package/corpus/templates/mail/app/hooks/use-snippets.ts +103 -0
  1259. package/corpus/templates/mail/app/i18n/ar-SA.ts +17 -0
  1260. package/corpus/templates/mail/app/i18n/de-DE.ts +18 -0
  1261. package/corpus/templates/mail/app/i18n/en-US.ts +17 -0
  1262. package/corpus/templates/mail/app/i18n/es-ES.ts +17 -0
  1263. package/corpus/templates/mail/app/i18n/fr-FR.ts +18 -0
  1264. package/corpus/templates/mail/app/i18n/hi-IN.ts +17 -0
  1265. package/corpus/templates/mail/app/i18n/ja-JP.ts +18 -0
  1266. package/corpus/templates/mail/app/i18n/ko-KR.ts +17 -0
  1267. package/corpus/templates/mail/app/i18n/pt-BR.ts +17 -0
  1268. package/corpus/templates/mail/app/i18n/zh-CN.ts +16 -0
  1269. package/corpus/templates/mail/app/i18n/zh-TW.ts +16 -0
  1270. package/corpus/templates/mail/app/pages/DraftQueuePage.tsx +14 -14
  1271. package/corpus/templates/mail/app/pages/InboxPage.tsx +3 -3
  1272. package/corpus/templates/mail/app/pages/NotFound.tsx +2 -1
  1273. package/corpus/templates/mail/app/pages/SettingsPage.tsx +64 -66
  1274. package/corpus/templates/mail/app/root.tsx +21 -18
  1275. package/corpus/templates/mail/app/routes/_index.tsx +2 -1
  1276. package/corpus/templates/mail/app/routes/email.tsx +2 -2
  1277. package/corpus/templates/mail/changelog/2026-07-06-a-queued-draft-can-no-longer-be-sent-twice-by-simultaneous-s.md +6 -0
  1278. package/corpus/templates/mail/changelog/2026-07-06-bulk-archive-trash-star-and-mark-read-now-complete-in-one-fa.md +6 -0
  1279. package/corpus/templates/mail/changelog/2026-07-06-dark-mode-sidebars-and-notifications-now-use-the-softer-gray.md +6 -0
  1280. package/corpus/templates/mail/changelog/2026-07-06-inbox-automations-pick-up-new-mail-faster.md +6 -0
  1281. package/corpus/templates/mail/changelog/2026-07-06-inbox-lists-stay-fast-as-scheduled-and-snoozed-mail-accumula.md +6 -0
  1282. package/corpus/templates/mail/changelog/2026-07-06-long-inboxes-scroll-smoothly-and-stay-fast-as-more-mail-load.md +6 -0
  1283. package/corpus/templates/mail/changelog/2026-07-06-paste-or-drop-images-straight-into-the-composer-to-send-them.md +6 -0
  1284. package/corpus/templates/mail/changelog/2026-07-06-save-reusable-snippets-and-insert-them-from-the-compose-slas.md +6 -0
  1285. package/corpus/templates/mail/changelog/2026-07-06-scrolling-older-mail-keeps-loading-correctly-when-no-gmail-a.md +6 -0
  1286. package/corpus/templates/mail/changelog/2026-07-06-search-shows-instant-matches-from-already-loaded-mail-while-.md +6 -0
  1287. package/corpus/templates/mail/changelog/2026-07-06-sends-now-fail-with-a-clear-error-instead-of-quietly-missing.md +6 -0
  1288. package/corpus/templates/mail/changelog/2026-07-06-the-mail-header-no-longer-shows-the-global-notifications-bel.md +6 -0
  1289. package/corpus/templates/mail/package.json +1 -0
  1290. package/corpus/templates/mail/server/db/schema.ts +11 -0
  1291. package/corpus/templates/mail/server/handlers/emails.ts +20 -1
  1292. package/corpus/templates/mail/server/lib/automation-engine.ts +53 -25
  1293. package/corpus/templates/mail/server/lib/google-auth.ts +140 -9
  1294. package/corpus/templates/mail/server/lib/jobs.ts +22 -6
  1295. package/corpus/templates/mail/server/lib/outgoing-email.ts +11 -2
  1296. package/corpus/templates/mail/server/lib/queued-drafts.ts +145 -4
  1297. package/corpus/templates/mail/server/plugins/db.ts +29 -0
  1298. package/corpus/templates/plan/.agents/skills/actions/SKILL.md +55 -0
  1299. package/corpus/templates/plan/app/components/editor/PlanDocumentEditor.tsx +42 -3
  1300. package/corpus/templates/plan/app/components/layout/Layout.tsx +5 -4
  1301. package/corpus/templates/plan/app/components/layout/Sidebar.tsx +20 -20
  1302. package/corpus/templates/plan/app/components/plan/CanvasArea.tsx +3 -2
  1303. package/corpus/templates/plan/app/components/plan/DocumentArea.tsx +2 -2
  1304. package/corpus/templates/plan/app/components/plan/GuestModeBanner.tsx +2 -1
  1305. package/corpus/templates/plan/app/components/plan/LocalCodebasePicker.tsx +8 -8
  1306. package/corpus/templates/plan/app/components/plan/PlanContentRenderer.tsx +6 -1
  1307. package/corpus/templates/plan/app/components/plan/PlanImageViewer.tsx +8 -12
  1308. package/corpus/templates/plan/app/components/plan/PlanMarkdownEditor.tsx +6 -1
  1309. package/corpus/templates/plan/app/components/plan/PlanVisualSurface.tsx +3 -3
  1310. package/corpus/templates/plan/app/components/plan/planBlocks.tsx +4 -4
  1311. package/corpus/templates/plan/app/components/plan/wireframe/kit/rough.tsx +61 -27
  1312. package/corpus/templates/plan/app/components/ui/alert-dialog.tsx +1 -0
  1313. package/corpus/templates/plan/app/components/ui/avatar.tsx +1 -0
  1314. package/corpus/templates/plan/app/components/ui/badge.tsx +1 -0
  1315. package/corpus/templates/plan/app/components/ui/button.tsx +1 -0
  1316. package/corpus/templates/plan/app/components/ui/card.tsx +1 -0
  1317. package/corpus/templates/plan/app/components/ui/checkbox.tsx +1 -0
  1318. package/corpus/templates/plan/app/components/ui/collapsible.tsx +1 -0
  1319. package/corpus/templates/plan/app/components/ui/dialog.tsx +1 -0
  1320. package/corpus/templates/plan/app/components/ui/dropdown-menu.tsx +1 -0
  1321. package/corpus/templates/plan/app/components/ui/input.tsx +1 -0
  1322. package/corpus/templates/plan/app/components/ui/label.tsx +1 -0
  1323. package/corpus/templates/plan/app/components/ui/popover.tsx +1 -0
  1324. package/corpus/templates/plan/app/components/ui/scroll-area.tsx +1 -0
  1325. package/corpus/templates/plan/app/components/ui/select.tsx +1 -0
  1326. package/corpus/templates/plan/app/components/ui/separator.tsx +1 -0
  1327. package/corpus/templates/plan/app/components/ui/sheet.tsx +1 -0
  1328. package/corpus/templates/plan/app/components/ui/skeleton.tsx +1 -0
  1329. package/corpus/templates/plan/app/components/ui/sonner.tsx +1 -0
  1330. package/corpus/templates/plan/app/components/ui/spinner.tsx +1 -0
  1331. package/corpus/templates/plan/app/components/ui/textarea.tsx +1 -0
  1332. package/corpus/templates/plan/app/components/ui/toggle-group.tsx +1 -0
  1333. package/corpus/templates/plan/app/components/ui/toolkit-provider.tsx +12 -0
  1334. package/corpus/templates/plan/app/components/ui/tooltip.tsx +1 -0
  1335. package/corpus/templates/plan/app/global.css +13 -0
  1336. package/corpus/templates/plan/app/hooks/use-plan-presence.ts +11 -1
  1337. package/corpus/templates/plan/app/lib/plan-access-prompt.ts +74 -0
  1338. package/corpus/templates/plan/app/lib/plan-annotation-runtime.ts +1435 -0
  1339. package/corpus/templates/plan/app/lib/plan-comment-editor-helpers.ts +142 -0
  1340. package/corpus/templates/plan/app/lib/plan-local-bridge.ts +376 -0
  1341. package/corpus/templates/plan/app/lib/plan-native-anchors.ts +682 -0
  1342. package/corpus/templates/plan/app/pages/PlansPage.tsx +216 -2747
  1343. package/corpus/templates/plan/app/root.tsx +13 -10
  1344. package/corpus/templates/plan/app/routes/_index.tsx +1 -2
  1345. package/corpus/templates/plan/app/routes/local-plans.$slug.tsx +1 -1
  1346. package/corpus/templates/plan/app/routes/plans.$id.tsx +1 -2
  1347. package/corpus/templates/plan/app/routes/plans.tsx +1 -2
  1348. package/corpus/templates/plan/app/routes/recaps.$id.tsx +1 -2
  1349. package/corpus/templates/plan/app/routes/recaps.tsx +1 -2
  1350. package/corpus/templates/plan/app/routes/settings.tsx +7 -21
  1351. package/corpus/templates/plan/changelog/2026-07-05-plan-pages-open-at-the-top-unless-a-shared-link-targets-a-sp.md +6 -0
  1352. package/corpus/templates/plan/changelog/2026-07-06-fixed-collaborative-plan-editing-so-content-no-longer-duplic.md +6 -0
  1353. package/corpus/templates/plan/changelog/2026-07-06-fixed-visual-reports-using-excessive-cpu-while-rendering-ske.md +6 -0
  1354. package/corpus/templates/slides/.agents/skills/actions/SKILL.md +55 -0
  1355. package/corpus/templates/slides/app/components/ThemeToggle.tsx +6 -6
  1356. package/corpus/templates/slides/app/components/comments/SlideCommentsPanel.tsx +5 -5
  1357. package/corpus/templates/slides/app/components/deck/DeckCard.tsx +7 -7
  1358. package/corpus/templates/slides/app/components/deck/ExcalidrawSlide.tsx +2 -1
  1359. package/corpus/templates/slides/app/components/deck/SlideRenderer.tsx +1 -1
  1360. package/corpus/templates/slides/app/components/design-system/DesignSystemCard.tsx +3 -2
  1361. package/corpus/templates/slides/app/components/design-system/DesignSystemSetup.tsx +15 -14
  1362. package/corpus/templates/slides/app/components/editor/AnimationsPanel.tsx +7 -7
  1363. package/corpus/templates/slides/app/components/editor/BlockBubbleMenu.tsx +5 -5
  1364. package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +6 -6
  1365. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +29 -16
  1366. package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +10 -9
  1367. package/corpus/templates/slides/app/components/editor/GenerateSlidesDialog.tsx +6 -5
  1368. package/corpus/templates/slides/app/components/editor/HistoryPanel.tsx +12 -12
  1369. package/corpus/templates/slides/app/components/editor/ImageDropPromptPopover.tsx +4 -3
  1370. package/corpus/templates/slides/app/components/editor/ImageSearchPanel.tsx +5 -4
  1371. package/corpus/templates/slides/app/components/editor/ImportButton.tsx +14 -4
  1372. package/corpus/templates/slides/app/components/editor/PromptDialog.tsx +2 -1
  1373. package/corpus/templates/slides/app/components/editor/ShareDialog.tsx +10 -10
  1374. package/corpus/templates/slides/app/components/editor/SlideBubbleMenu.tsx +10 -10
  1375. package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +6 -6
  1376. package/corpus/templates/slides/app/components/editor/SlideStyleInspector.tsx +2 -1
  1377. package/corpus/templates/slides/app/components/editor/TweaksPanel.tsx +1 -1
  1378. package/corpus/templates/slides/app/components/layout/Header.tsx +2 -2
  1379. package/corpus/templates/slides/app/components/layout/Sidebar.tsx +5 -5
  1380. package/corpus/templates/slides/app/components/ui/alert-dialog.tsx +1 -0
  1381. package/corpus/templates/slides/app/components/ui/button.tsx +1 -0
  1382. package/corpus/templates/slides/app/components/ui/card.tsx +1 -0
  1383. package/corpus/templates/slides/app/components/ui/dialog.tsx +1 -0
  1384. package/corpus/templates/slides/app/components/ui/dropdown-menu.tsx +1 -0
  1385. package/corpus/templates/slides/app/components/ui/input.tsx +1 -0
  1386. package/corpus/templates/slides/app/components/ui/label.tsx +1 -0
  1387. package/corpus/templates/slides/app/components/ui/popover.tsx +1 -0
  1388. package/corpus/templates/slides/app/components/ui/scroll-area.tsx +1 -0
  1389. package/corpus/templates/slides/app/components/ui/select.tsx +1 -0
  1390. package/corpus/templates/slides/app/components/ui/separator.tsx +1 -0
  1391. package/corpus/templates/slides/app/components/ui/sheet.tsx +1 -0
  1392. package/corpus/templates/slides/app/components/ui/skeleton.tsx +1 -0
  1393. package/corpus/templates/slides/app/components/ui/spinner.tsx +1 -0
  1394. package/corpus/templates/slides/app/components/ui/textarea.tsx +1 -0
  1395. package/corpus/templates/slides/app/components/ui/toast.tsx +1 -0
  1396. package/corpus/templates/slides/app/components/ui/toaster.tsx +1 -0
  1397. package/corpus/templates/slides/app/components/ui/toggle-group.tsx +1 -0
  1398. package/corpus/templates/slides/app/components/ui/toolkit-provider.tsx +12 -0
  1399. package/corpus/templates/slides/app/components/ui/tooltip.tsx +1 -0
  1400. package/corpus/templates/slides/app/components/visual-editor/CanvasCommentPins.tsx +6 -6
  1401. package/corpus/templates/slides/app/components/visual-editor/DrawOverlay.tsx +7 -7
  1402. package/corpus/templates/slides/app/global.css +1 -0
  1403. package/corpus/templates/slides/app/hooks/use-toast.ts +1 -0
  1404. package/corpus/templates/slides/app/lib/upload-response.ts +71 -0
  1405. package/corpus/templates/slides/app/pages/DeckEditor.tsx +3 -3
  1406. package/corpus/templates/slides/app/pages/DesignSystems.tsx +1 -1
  1407. package/corpus/templates/slides/app/pages/Index.tsx +14 -17
  1408. package/corpus/templates/slides/app/root.tsx +13 -10
  1409. package/corpus/templates/slides/app/routes/settings.tsx +6 -21
  1410. package/corpus/templates/slides/changelog/2026-07-06-mobile-layouts-no-longer-stack-two-top-headers.md +5 -0
  1411. package/corpus/templates/slides/server/handlers/uploads.ts +7 -2
  1412. package/corpus/templates/slides/server/lib/chat-attachments.ts +1 -1
  1413. package/dist/agent/durable-background.d.ts +29 -0
  1414. package/dist/agent/durable-background.d.ts.map +1 -1
  1415. package/dist/agent/durable-background.js +46 -0
  1416. package/dist/agent/durable-background.js.map +1 -1
  1417. package/dist/agent/engine/ai-sdk-engine.d.ts.map +1 -1
  1418. package/dist/agent/engine/ai-sdk-engine.js +7 -2
  1419. package/dist/agent/engine/ai-sdk-engine.js.map +1 -1
  1420. package/dist/agent/engine/anthropic-engine.d.ts.map +1 -1
  1421. package/dist/agent/engine/anthropic-engine.js +28 -2
  1422. package/dist/agent/engine/anthropic-engine.js.map +1 -1
  1423. package/dist/agent/engine/builder-engine.d.ts.map +1 -1
  1424. package/dist/agent/engine/builder-engine.js +1 -1
  1425. package/dist/agent/engine/builder-engine.js.map +1 -1
  1426. package/dist/agent/engine/output-tokens.d.ts +3 -3
  1427. package/dist/agent/engine/output-tokens.d.ts.map +1 -1
  1428. package/dist/agent/engine/output-tokens.js +16 -14
  1429. package/dist/agent/engine/output-tokens.js.map +1 -1
  1430. package/dist/agent/engine/registry.d.ts +3 -3
  1431. package/dist/agent/engine/registry.d.ts.map +1 -1
  1432. package/dist/agent/engine/registry.js +28 -25
  1433. package/dist/agent/engine/registry.js.map +1 -1
  1434. package/dist/agent/engine/translate-ai-sdk.d.ts +14 -2
  1435. package/dist/agent/engine/translate-ai-sdk.d.ts.map +1 -1
  1436. package/dist/agent/engine/translate-ai-sdk.js +33 -18
  1437. package/dist/agent/engine/translate-ai-sdk.js.map +1 -1
  1438. package/dist/agent/engine/translate-anthropic.d.ts.map +1 -1
  1439. package/dist/agent/engine/translate-anthropic.js +42 -1
  1440. package/dist/agent/engine/translate-anthropic.js.map +1 -1
  1441. package/dist/agent/engine/types.d.ts +15 -0
  1442. package/dist/agent/engine/types.d.ts.map +1 -1
  1443. package/dist/agent/engine/types.js.map +1 -1
  1444. package/dist/agent/model-config.d.ts +8 -0
  1445. package/dist/agent/model-config.d.ts.map +1 -1
  1446. package/dist/agent/model-config.js +86 -0
  1447. package/dist/agent/model-config.js.map +1 -1
  1448. package/dist/agent/production-agent.d.ts +91 -5
  1449. package/dist/agent/production-agent.d.ts.map +1 -1
  1450. package/dist/agent/production-agent.js +389 -180
  1451. package/dist/agent/production-agent.js.map +1 -1
  1452. package/dist/agent/runtime-context.d.ts +29 -0
  1453. package/dist/agent/runtime-context.d.ts.map +1 -1
  1454. package/dist/agent/runtime-context.js +40 -11
  1455. package/dist/agent/runtime-context.js.map +1 -1
  1456. package/dist/agent/tool-result-images.d.ts +49 -0
  1457. package/dist/agent/tool-result-images.d.ts.map +1 -0
  1458. package/dist/agent/tool-result-images.js +141 -0
  1459. package/dist/agent/tool-result-images.js.map +1 -0
  1460. package/dist/brand-kit/fig/index.d.ts.map +1 -1
  1461. package/dist/brand-kit/fig/index.js +6 -3
  1462. package/dist/brand-kit/fig/index.js.map +1 -1
  1463. package/dist/client/AgentPanel.d.ts.map +1 -1
  1464. package/dist/client/AgentPanel.js +67 -18
  1465. package/dist/client/AgentPanel.js.map +1 -1
  1466. package/dist/client/AssistantChat.d.ts.map +1 -1
  1467. package/dist/client/AssistantChat.js +72 -40
  1468. package/dist/client/AssistantChat.js.map +1 -1
  1469. package/dist/client/KeepTabOpenNotice.d.ts +55 -0
  1470. package/dist/client/KeepTabOpenNotice.d.ts.map +1 -0
  1471. package/dist/client/KeepTabOpenNotice.js +85 -0
  1472. package/dist/client/KeepTabOpenNotice.js.map +1 -0
  1473. package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
  1474. package/dist/client/MultiTabAssistantChat.js +2 -1
  1475. package/dist/client/MultiTabAssistantChat.js.map +1 -1
  1476. package/dist/client/RunStuckBanner.d.ts +7 -1
  1477. package/dist/client/RunStuckBanner.d.ts.map +1 -1
  1478. package/dist/client/RunStuckBanner.js +7 -2
  1479. package/dist/client/RunStuckBanner.js.map +1 -1
  1480. package/dist/client/chat/run-recovery.d.ts.map +1 -1
  1481. package/dist/client/chat/run-recovery.js +7 -1
  1482. package/dist/client/chat/run-recovery.js.map +1 -1
  1483. package/dist/client/clipboard.d.ts.map +1 -1
  1484. package/dist/client/clipboard.js +5 -6
  1485. package/dist/client/clipboard.js.map +1 -1
  1486. package/dist/client/components/AgentPresenceChip.d.ts +1 -11
  1487. package/dist/client/components/AgentPresenceChip.d.ts.map +1 -1
  1488. package/dist/client/components/AgentPresenceChip.js +1 -41
  1489. package/dist/client/components/AgentPresenceChip.js.map +1 -1
  1490. package/dist/client/components/LiveCursorOverlay.d.ts +1 -44
  1491. package/dist/client/components/LiveCursorOverlay.d.ts.map +1 -1
  1492. package/dist/client/components/LiveCursorOverlay.js +1 -123
  1493. package/dist/client/components/LiveCursorOverlay.js.map +1 -1
  1494. package/dist/client/components/PresenceBar.d.ts +1 -26
  1495. package/dist/client/components/PresenceBar.d.ts.map +1 -1
  1496. package/dist/client/components/PresenceBar.js +1 -164
  1497. package/dist/client/components/PresenceBar.js.map +1 -1
  1498. package/dist/client/components/RecentEditHighlights.d.ts +1 -42
  1499. package/dist/client/components/RecentEditHighlights.d.ts.map +1 -1
  1500. package/dist/client/components/RecentEditHighlights.js +1 -145
  1501. package/dist/client/components/RecentEditHighlights.js.map +1 -1
  1502. package/dist/client/components/RemoteSelectionRings.d.ts +1 -51
  1503. package/dist/client/components/RemoteSelectionRings.d.ts.map +1 -1
  1504. package/dist/client/components/RemoteSelectionRings.js +1 -129
  1505. package/dist/client/components/RemoteSelectionRings.js.map +1 -1
  1506. package/dist/client/composer/TiptapComposer.d.ts +4 -0
  1507. package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
  1508. package/dist/client/composer/TiptapComposer.js +74 -26
  1509. package/dist/client/composer/TiptapComposer.js.map +1 -1
  1510. package/dist/client/db-admin/SqlEditor.d.ts.map +1 -1
  1511. package/dist/client/db-admin/SqlEditor.js +13 -3
  1512. package/dist/client/db-admin/SqlEditor.js.map +1 -1
  1513. package/dist/client/dynamic-suggestions.d.ts.map +1 -1
  1514. package/dist/client/dynamic-suggestions.js +7 -1
  1515. package/dist/client/dynamic-suggestions.js.map +1 -1
  1516. package/dist/client/extensions/ExtensionViewer.d.ts.map +1 -1
  1517. package/dist/client/extensions/ExtensionViewer.js +7 -7
  1518. package/dist/client/extensions/ExtensionViewer.js.map +1 -1
  1519. package/dist/client/extensions/ExtensionsListPage.d.ts.map +1 -1
  1520. package/dist/client/extensions/ExtensionsListPage.js +5 -7
  1521. package/dist/client/extensions/ExtensionsListPage.js.map +1 -1
  1522. package/dist/client/extensions/ExtensionsSidebarSection.d.ts.map +1 -1
  1523. package/dist/client/extensions/ExtensionsSidebarSection.js +1 -1
  1524. package/dist/client/extensions/ExtensionsSidebarSection.js.map +1 -1
  1525. package/dist/client/host-bridge.d.ts +2 -0
  1526. package/dist/client/host-bridge.d.ts.map +1 -1
  1527. package/dist/client/host-bridge.js +26 -10
  1528. package/dist/client/host-bridge.js.map +1 -1
  1529. package/dist/client/i18n.d.ts +1 -0
  1530. package/dist/client/i18n.d.ts.map +1 -1
  1531. package/dist/client/i18n.js +3 -1
  1532. package/dist/client/i18n.js.map +1 -1
  1533. package/dist/client/index.d.ts +2 -1
  1534. package/dist/client/index.d.ts.map +1 -1
  1535. package/dist/client/index.js +2 -1
  1536. package/dist/client/index.js.map +1 -1
  1537. package/dist/client/resources/McpIntegrationDialog.d.ts.map +1 -1
  1538. package/dist/client/resources/McpIntegrationDialog.js +12 -31
  1539. package/dist/client/resources/McpIntegrationDialog.js.map +1 -1
  1540. package/dist/client/rich-markdown-editor/SharedRichEditor.d.ts +3 -1
  1541. package/dist/client/rich-markdown-editor/SharedRichEditor.d.ts.map +1 -1
  1542. package/dist/client/rich-markdown-editor/SharedRichEditor.js +2 -1
  1543. package/dist/client/rich-markdown-editor/SharedRichEditor.js.map +1 -1
  1544. package/dist/client/rich-markdown-editor/useCollabReconcile.d.ts +3 -1
  1545. package/dist/client/rich-markdown-editor/useCollabReconcile.d.ts.map +1 -1
  1546. package/dist/client/rich-markdown-editor/useCollabReconcile.js +17 -8
  1547. package/dist/client/rich-markdown-editor/useCollabReconcile.js.map +1 -1
  1548. package/dist/client/settings/SettingsPanel.d.ts +3 -1
  1549. package/dist/client/settings/SettingsPanel.d.ts.map +1 -1
  1550. package/dist/client/settings/SettingsPanel.js +103 -9
  1551. package/dist/client/settings/SettingsPanel.js.map +1 -1
  1552. package/dist/client/settings/SettingsTabsPage.d.ts.map +1 -1
  1553. package/dist/client/settings/SettingsTabsPage.js +5 -5
  1554. package/dist/client/settings/SettingsTabsPage.js.map +1 -1
  1555. package/dist/client/settings/index.d.ts +1 -1
  1556. package/dist/client/settings/index.d.ts.map +1 -1
  1557. package/dist/client/settings/index.js +1 -1
  1558. package/dist/client/settings/index.js.map +1 -1
  1559. package/dist/client/sharing/VisibilityBadge.d.ts +1 -10
  1560. package/dist/client/sharing/VisibilityBadge.d.ts.map +1 -1
  1561. package/dist/client/sharing/VisibilityBadge.js +1 -18
  1562. package/dist/client/sharing/VisibilityBadge.js.map +1 -1
  1563. package/dist/client/use-action.d.ts +3 -0
  1564. package/dist/client/use-action.d.ts.map +1 -1
  1565. package/dist/client/use-action.js +34 -3
  1566. package/dist/client/use-action.js.map +1 -1
  1567. package/dist/client/use-db-sync.d.ts +4 -0
  1568. package/dist/client/use-db-sync.d.ts.map +1 -1
  1569. package/dist/client/use-db-sync.js +37 -18
  1570. package/dist/client/use-db-sync.js.map +1 -1
  1571. package/dist/client/use-run-stuck-detection.d.ts +8 -1
  1572. package/dist/client/use-run-stuck-detection.d.ts.map +1 -1
  1573. package/dist/client/use-run-stuck-detection.js +3 -2
  1574. package/dist/client/use-run-stuck-detection.js.map +1 -1
  1575. package/dist/coding-tools/run-code.d.ts +36 -0
  1576. package/dist/coding-tools/run-code.d.ts.map +1 -1
  1577. package/dist/coding-tools/run-code.js +333 -62
  1578. package/dist/coding-tools/run-code.js.map +1 -1
  1579. package/dist/coding-tools/sandbox/background.d.ts +141 -0
  1580. package/dist/coding-tools/sandbox/background.d.ts.map +1 -0
  1581. package/dist/coding-tools/sandbox/background.js +279 -0
  1582. package/dist/coding-tools/sandbox/background.js.map +1 -0
  1583. package/dist/coding-tools/sandbox/executions-store.d.ts +138 -0
  1584. package/dist/coding-tools/sandbox/executions-store.d.ts.map +1 -0
  1585. package/dist/coding-tools/sandbox/executions-store.js +380 -0
  1586. package/dist/coding-tools/sandbox/executions-store.js.map +1 -0
  1587. package/dist/coding-tools/sandbox/index.d.ts +22 -13
  1588. package/dist/coding-tools/sandbox/index.d.ts.map +1 -1
  1589. package/dist/coding-tools/sandbox/index.js +31 -13
  1590. package/dist/coding-tools/sandbox/index.js.map +1 -1
  1591. package/dist/coding-tools/sandbox/local-child-process-adapter.d.ts.map +1 -1
  1592. package/dist/coding-tools/sandbox/local-child-process-adapter.js +61 -4
  1593. package/dist/coding-tools/sandbox/local-child-process-adapter.js.map +1 -1
  1594. package/dist/collab/awareness.d.ts +2 -2
  1595. package/dist/collab/awareness.d.ts.map +1 -1
  1596. package/dist/collab/client.d.ts +16 -12
  1597. package/dist/collab/client.d.ts.map +1 -1
  1598. package/dist/collab/client.js +744 -496
  1599. package/dist/collab/client.js.map +1 -1
  1600. package/dist/collab/presence.d.ts +2 -20
  1601. package/dist/collab/presence.d.ts.map +1 -1
  1602. package/dist/collab/presence.js +3 -0
  1603. package/dist/collab/presence.js.map +1 -1
  1604. package/dist/collab/recent-edits.d.ts +2 -37
  1605. package/dist/collab/recent-edits.d.ts.map +1 -1
  1606. package/dist/collab/recent-edits.js +2 -9
  1607. package/dist/collab/recent-edits.js.map +1 -1
  1608. package/dist/collab/struct-routes.d.ts +1 -1
  1609. package/dist/data-programs/actions.d.ts +19 -0
  1610. package/dist/data-programs/actions.d.ts.map +1 -0
  1611. package/dist/data-programs/actions.js +279 -0
  1612. package/dist/data-programs/actions.js.map +1 -0
  1613. package/dist/data-programs/contract.d.ts +70 -0
  1614. package/dist/data-programs/contract.d.ts.map +1 -0
  1615. package/dist/data-programs/contract.js +225 -0
  1616. package/dist/data-programs/contract.js.map +1 -0
  1617. package/dist/data-programs/execute.d.ts +96 -0
  1618. package/dist/data-programs/execute.d.ts.map +1 -0
  1619. package/dist/data-programs/execute.js +454 -0
  1620. package/dist/data-programs/execute.js.map +1 -0
  1621. package/dist/data-programs/index.d.ts +11 -0
  1622. package/dist/data-programs/index.d.ts.map +1 -0
  1623. package/dist/data-programs/index.js +11 -0
  1624. package/dist/data-programs/index.js.map +1 -0
  1625. package/dist/data-programs/schema.d.ts +517 -0
  1626. package/dist/data-programs/schema.d.ts.map +1 -0
  1627. package/dist/data-programs/schema.js +138 -0
  1628. package/dist/data-programs/schema.js.map +1 -0
  1629. package/dist/data-programs/store.d.ts +158 -0
  1630. package/dist/data-programs/store.d.ts.map +1 -0
  1631. package/dist/data-programs/store.js +493 -0
  1632. package/dist/data-programs/store.js.map +1 -0
  1633. package/dist/db/create-get-db.d.ts +25 -0
  1634. package/dist/db/create-get-db.d.ts.map +1 -1
  1635. package/dist/db/create-get-db.js +25 -2
  1636. package/dist/db/create-get-db.js.map +1 -1
  1637. package/dist/deploy/build.d.ts.map +1 -1
  1638. package/dist/deploy/build.js +34 -0
  1639. package/dist/deploy/build.js.map +1 -1
  1640. package/dist/extensions/actions.js +2 -2
  1641. package/dist/extensions/actions.js.map +1 -1
  1642. package/dist/file-upload/builder.d.ts.map +1 -1
  1643. package/dist/file-upload/builder.js +9 -0
  1644. package/dist/file-upload/builder.js.map +1 -1
  1645. package/dist/integrations/webhook-handler.d.ts.map +1 -1
  1646. package/dist/integrations/webhook-handler.js +21 -10
  1647. package/dist/integrations/webhook-handler.js.map +1 -1
  1648. package/dist/local-artifacts/index.js +1 -1
  1649. package/dist/local-artifacts/index.js.map +1 -1
  1650. package/dist/localization/default-messages.d.ts +0 -1
  1651. package/dist/localization/default-messages.d.ts.map +1 -1
  1652. package/dist/localization/default-messages.js +1 -2
  1653. package/dist/localization/default-messages.js.map +1 -1
  1654. package/dist/mcp-client/index.d.ts +9 -0
  1655. package/dist/mcp-client/index.d.ts.map +1 -1
  1656. package/dist/mcp-client/index.js +23 -0
  1657. package/dist/mcp-client/index.js.map +1 -1
  1658. package/dist/observability/routes.d.ts +3 -3
  1659. package/dist/onboarding/default-steps.d.ts.map +1 -1
  1660. package/dist/onboarding/default-steps.js +2 -2
  1661. package/dist/onboarding/default-steps.js.map +1 -1
  1662. package/dist/progress/routes.d.ts +1 -1
  1663. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  1664. package/dist/resources/store.d.ts.map +1 -1
  1665. package/dist/resources/store.js +35 -3
  1666. package/dist/resources/store.js.map +1 -1
  1667. package/dist/scripts/agent-engines/list-agent-engines.d.ts.map +1 -1
  1668. package/dist/scripts/agent-engines/list-agent-engines.js +4 -8
  1669. package/dist/scripts/agent-engines/list-agent-engines.js.map +1 -1
  1670. package/dist/scripts/agent-engines/test-agent-engine.d.ts.map +1 -1
  1671. package/dist/scripts/agent-engines/test-agent-engine.js +7 -2
  1672. package/dist/scripts/agent-engines/test-agent-engine.js.map +1 -1
  1673. package/dist/scripts/db/tool-mode.js +3 -3
  1674. package/dist/scripts/db/tool-mode.js.map +1 -1
  1675. package/dist/scripts/resources/migrate-learnings.d.ts +5 -1
  1676. package/dist/scripts/resources/migrate-learnings.d.ts.map +1 -1
  1677. package/dist/scripts/resources/migrate-learnings.js +8 -10
  1678. package/dist/scripts/resources/migrate-learnings.js.map +1 -1
  1679. package/dist/server/agent-chat-plugin.d.ts +20 -6
  1680. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  1681. package/dist/server/agent-chat-plugin.js +138 -59
  1682. package/dist/server/agent-chat-plugin.js.map +1 -1
  1683. package/dist/server/agent-discovery.d.ts.map +1 -1
  1684. package/dist/server/agent-discovery.js +62 -8
  1685. package/dist/server/agent-discovery.js.map +1 -1
  1686. package/dist/server/auth.d.ts.map +1 -1
  1687. package/dist/server/auth.js +75 -3
  1688. package/dist/server/auth.js.map +1 -1
  1689. package/dist/server/builder-design-systems.js +2 -2
  1690. package/dist/server/builder-design-systems.js.map +1 -1
  1691. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  1692. package/dist/server/core-routes-plugin.js +100 -20
  1693. package/dist/server/core-routes-plugin.js.map +1 -1
  1694. package/dist/server/credential-provider.d.ts +6 -3
  1695. package/dist/server/credential-provider.d.ts.map +1 -1
  1696. package/dist/server/credential-provider.js +25 -8
  1697. package/dist/server/credential-provider.js.map +1 -1
  1698. package/dist/server/csrf.d.ts.map +1 -1
  1699. package/dist/server/csrf.js +3 -0
  1700. package/dist/server/csrf.js.map +1 -1
  1701. package/dist/server/framework-request-handler.d.ts.map +1 -1
  1702. package/dist/server/framework-request-handler.js +37 -1
  1703. package/dist/server/framework-request-handler.js.map +1 -1
  1704. package/dist/server/oauth-helpers.d.ts.map +1 -1
  1705. package/dist/server/oauth-helpers.js +9 -1
  1706. package/dist/server/oauth-helpers.js.map +1 -1
  1707. package/dist/server/onboarding-html.d.ts.map +1 -1
  1708. package/dist/server/onboarding-html.js +80 -48
  1709. package/dist/server/onboarding-html.js.map +1 -1
  1710. package/dist/server/prompts/shared-rules.js +2 -2
  1711. package/dist/server/prompts/shared-rules.js.map +1 -1
  1712. package/dist/server/transcribe-voice.d.ts +1 -1
  1713. package/dist/server/transcribe-voice.d.ts.map +1 -1
  1714. package/dist/server/transcribe-voice.js +77 -15
  1715. package/dist/server/transcribe-voice.js.map +1 -1
  1716. package/dist/styles/agent-native.css +57 -45
  1717. package/dist/templates/default/.agents/skills/actions/SKILL.md +55 -0
  1718. package/dist/templates/default/AGENTS.md +11 -14
  1719. package/dist/templates/default/actions/navigate.ts +1 -1
  1720. package/dist/templates/default/app/i18n/ar-SA.ts +0 -1
  1721. package/dist/templates/default/app/i18n/de-DE.ts +0 -1
  1722. package/dist/templates/default/app/i18n/en-US.ts +0 -1
  1723. package/dist/templates/default/app/i18n/es-ES.ts +0 -1
  1724. package/dist/templates/default/app/i18n/fr-FR.ts +0 -1
  1725. package/dist/templates/default/app/i18n/hi-IN.ts +0 -1
  1726. package/dist/templates/default/app/i18n/ja-JP.ts +0 -1
  1727. package/dist/templates/default/app/i18n/ko-KR.ts +0 -1
  1728. package/dist/templates/default/app/i18n/pt-BR.ts +0 -1
  1729. package/dist/templates/default/app/i18n/zh-CN.ts +0 -1
  1730. package/dist/templates/default/app/i18n/zh-TW.ts +0 -1
  1731. package/dist/templates/default/app/routes/settings.tsx +22 -33
  1732. package/dist/templates/headless/.agents/skills/agent-native-docs/SKILL.md +20 -7
  1733. package/dist/templates/headless/AGENTS.md +9 -6
  1734. package/dist/templates/workspace-core/.agents/skills/actions/SKILL.md +55 -0
  1735. package/dist/templates/workspace-core/.agents/skills/data-programs/SKILL.md +128 -0
  1736. package/dist/templates/workspace-core/.agents/skills/harness-agents/SKILL.md +10 -4
  1737. package/dist/vite/agents-bundle-plugin.d.ts.map +1 -1
  1738. package/dist/vite/agents-bundle-plugin.js +26 -1
  1739. package/dist/vite/agents-bundle-plugin.js.map +1 -1
  1740. package/dist/vite/client.d.ts.map +1 -1
  1741. package/dist/vite/client.js +11 -0
  1742. package/dist/vite/client.js.map +1 -1
  1743. package/docs/content/agent-native-toolkit.mdx +87 -146
  1744. package/docs/content/cloneable-saas.mdx +19 -19
  1745. package/docs/content/components.mdx +16 -4
  1746. package/docs/content/data-programs.mdx +329 -0
  1747. package/docs/content/deployment.mdx +9 -9
  1748. package/docs/content/extensions.mdx +8 -7
  1749. package/docs/content/generative-ui.mdx +5 -3
  1750. package/docs/content/key-concepts.mdx +6 -9
  1751. package/docs/content/locales/ar-SA/agent-native-toolkit.mdx +100 -70
  1752. package/docs/content/locales/ar-SA/cloneable-saas.mdx +19 -19
  1753. package/docs/content/locales/ar-SA/components.mdx +12 -2
  1754. package/docs/content/locales/ar-SA/data-programs.mdx +329 -0
  1755. package/docs/content/locales/ar-SA/deployment.mdx +8 -9
  1756. package/docs/content/locales/ar-SA/extensions.mdx +6 -4
  1757. package/docs/content/locales/ar-SA/key-concepts.mdx +7 -9
  1758. package/docs/content/locales/ar-SA/real-time-collaboration.mdx +9 -10
  1759. package/docs/content/locales/de-DE/agent-native-toolkit.mdx +100 -74
  1760. package/docs/content/locales/de-DE/cloneable-saas.mdx +19 -19
  1761. package/docs/content/locales/de-DE/components.mdx +12 -2
  1762. package/docs/content/locales/de-DE/data-programs.mdx +329 -0
  1763. package/docs/content/locales/de-DE/deployment.mdx +8 -9
  1764. package/docs/content/locales/de-DE/extensions.mdx +6 -4
  1765. package/docs/content/locales/de-DE/key-concepts.mdx +8 -10
  1766. package/docs/content/locales/de-DE/real-time-collaboration.mdx +9 -10
  1767. package/docs/content/locales/es-ES/agent-native-toolkit.mdx +100 -74
  1768. package/docs/content/locales/es-ES/cloneable-saas.mdx +19 -19
  1769. package/docs/content/locales/es-ES/components.mdx +12 -2
  1770. package/docs/content/locales/es-ES/data-programs.mdx +329 -0
  1771. package/docs/content/locales/es-ES/deployment.mdx +8 -9
  1772. package/docs/content/locales/es-ES/extensions.mdx +6 -4
  1773. package/docs/content/locales/es-ES/key-concepts.mdx +8 -10
  1774. package/docs/content/locales/es-ES/real-time-collaboration.mdx +9 -10
  1775. package/docs/content/locales/fr-FR/agent-native-toolkit.mdx +100 -76
  1776. package/docs/content/locales/fr-FR/cloneable-saas.mdx +19 -19
  1777. package/docs/content/locales/fr-FR/components.mdx +12 -2
  1778. package/docs/content/locales/fr-FR/data-programs.mdx +329 -0
  1779. package/docs/content/locales/fr-FR/deployment.mdx +8 -9
  1780. package/docs/content/locales/fr-FR/extensions.mdx +6 -4
  1781. package/docs/content/locales/fr-FR/key-concepts.mdx +8 -10
  1782. package/docs/content/locales/fr-FR/real-time-collaboration.mdx +9 -10
  1783. package/docs/content/locales/hi-IN/agent-native-toolkit.mdx +100 -71
  1784. package/docs/content/locales/hi-IN/cloneable-saas.mdx +19 -19
  1785. package/docs/content/locales/hi-IN/components.mdx +20 -10
  1786. package/docs/content/locales/hi-IN/data-programs.mdx +329 -0
  1787. package/docs/content/locales/hi-IN/deployment.mdx +8 -9
  1788. package/docs/content/locales/hi-IN/extensions.mdx +6 -4
  1789. package/docs/content/locales/hi-IN/key-concepts.mdx +8 -10
  1790. package/docs/content/locales/hi-IN/real-time-collaboration.mdx +9 -10
  1791. package/docs/content/locales/ja-JP/agent-native-toolkit.mdx +100 -64
  1792. package/docs/content/locales/ja-JP/cloneable-saas.mdx +19 -19
  1793. package/docs/content/locales/ja-JP/components.mdx +12 -2
  1794. package/docs/content/locales/ja-JP/data-programs.mdx +329 -0
  1795. package/docs/content/locales/ja-JP/deployment.mdx +8 -9
  1796. package/docs/content/locales/ja-JP/extensions.mdx +6 -4
  1797. package/docs/content/locales/ja-JP/key-concepts.mdx +9 -11
  1798. package/docs/content/locales/ja-JP/real-time-collaboration.mdx +9 -10
  1799. package/docs/content/locales/ko-KR/agent-native-toolkit.mdx +100 -60
  1800. package/docs/content/locales/ko-KR/cloneable-saas.mdx +19 -19
  1801. package/docs/content/locales/ko-KR/components.mdx +20 -10
  1802. package/docs/content/locales/ko-KR/data-programs.mdx +329 -0
  1803. package/docs/content/locales/ko-KR/deployment.mdx +8 -9
  1804. package/docs/content/locales/ko-KR/extensions.mdx +16 -14
  1805. package/docs/content/locales/ko-KR/key-concepts.mdx +9 -11
  1806. package/docs/content/locales/ko-KR/real-time-collaboration.mdx +9 -10
  1807. package/docs/content/locales/pt-BR/agent-native-toolkit.mdx +100 -74
  1808. package/docs/content/locales/pt-BR/cloneable-saas.mdx +19 -19
  1809. package/docs/content/locales/pt-BR/components.mdx +12 -2
  1810. package/docs/content/locales/pt-BR/data-programs.mdx +329 -0
  1811. package/docs/content/locales/pt-BR/deployment.mdx +8 -9
  1812. package/docs/content/locales/pt-BR/extensions.mdx +6 -4
  1813. package/docs/content/locales/pt-BR/key-concepts.mdx +8 -11
  1814. package/docs/content/locales/pt-BR/real-time-collaboration.mdx +9 -10
  1815. package/docs/content/locales/zh-CN/agent-native-toolkit.mdx +100 -58
  1816. package/docs/content/locales/zh-CN/cloneable-saas.mdx +19 -19
  1817. package/docs/content/locales/zh-CN/components.mdx +20 -10
  1818. package/docs/content/locales/zh-CN/data-programs.mdx +329 -0
  1819. package/docs/content/locales/zh-CN/deployment.mdx +7 -9
  1820. package/docs/content/locales/zh-CN/extensions.mdx +16 -14
  1821. package/docs/content/locales/zh-CN/key-concepts.mdx +7 -11
  1822. package/docs/content/locales/zh-CN/real-time-collaboration.mdx +10 -11
  1823. package/docs/content/locales/zh-TW/agent-native-toolkit.mdx +100 -58
  1824. package/docs/content/locales/zh-TW/cloneable-saas.mdx +19 -19
  1825. package/docs/content/locales/zh-TW/components.mdx +20 -10
  1826. package/docs/content/locales/zh-TW/data-programs.mdx +329 -0
  1827. package/docs/content/locales/zh-TW/deployment.mdx +7 -9
  1828. package/docs/content/locales/zh-TW/extensions.mdx +16 -14
  1829. package/docs/content/locales/zh-TW/key-concepts.mdx +7 -11
  1830. package/docs/content/locales/zh-TW/real-time-collaboration.mdx +10 -11
  1831. package/docs/content/real-time-collaboration.mdx +10 -11
  1832. package/docs/content/toolkit-app-adapters.mdx +46 -0
  1833. package/docs/content/toolkit-collaboration-ui.mdx +48 -0
  1834. package/docs/content/toolkit-sharing-ui.mdx +33 -0
  1835. package/docs/content/toolkit-shell-hooks.mdx +27 -0
  1836. package/docs/content/toolkit-ui.mdx +69 -0
  1837. package/package.json +6 -2
  1838. package/src/templates/default/.agents/skills/actions/SKILL.md +55 -0
  1839. package/src/templates/default/AGENTS.md +11 -14
  1840. package/src/templates/default/actions/navigate.spec.ts +53 -0
  1841. package/src/templates/default/actions/navigate.ts +1 -1
  1842. package/src/templates/default/app/i18n/ar-SA.ts +0 -1
  1843. package/src/templates/default/app/i18n/de-DE.ts +0 -1
  1844. package/src/templates/default/app/i18n/en-US.ts +0 -1
  1845. package/src/templates/default/app/i18n/es-ES.ts +0 -1
  1846. package/src/templates/default/app/i18n/fr-FR.ts +0 -1
  1847. package/src/templates/default/app/i18n/hi-IN.ts +0 -1
  1848. package/src/templates/default/app/i18n/ja-JP.ts +0 -1
  1849. package/src/templates/default/app/i18n/ko-KR.ts +0 -1
  1850. package/src/templates/default/app/i18n/pt-BR.ts +0 -1
  1851. package/src/templates/default/app/i18n/zh-CN.ts +0 -1
  1852. package/src/templates/default/app/i18n/zh-TW.ts +0 -1
  1853. package/src/templates/default/app/routes/settings.tsx +22 -33
  1854. package/src/templates/headless/.agents/skills/agent-native-docs/SKILL.md +20 -7
  1855. package/src/templates/headless/AGENTS.md +9 -6
  1856. package/src/templates/workspace-core/.agents/skills/actions/SKILL.md +55 -0
  1857. package/src/templates/workspace-core/.agents/skills/data-programs/SKILL.md +128 -0
  1858. package/src/templates/workspace-core/.agents/skills/harness-agents/SKILL.md +10 -4
  1859. package/corpus/templates/design/app/components/design/inspector/InspectorAiActions.tsx +0 -153
  1860. package/corpus/templates/design/app/hooks/useAgentEditRequest.ts +0 -133
  1861. package/corpus/templates/design/app/pages/DesignEditor.tsx +0 -22928
  1862. package/corpus/templates/design/server/lib/figma-import/clipboard.ts +0 -122
  1863. package/corpus/templates/design/server/lib/figma-import/decode.ts +0 -455
  1864. package/corpus/templates/design/server/lib/figma-import/processor.ts +0 -116
  1865. package/corpus/templates/design/server/lib/figma-import/render-html.ts +0 -397
  1866. package/corpus/templates/design/server/lib/figma-import/types.ts +0 -60
@@ -4,7 +4,7 @@ use std::fs::File;
4
4
  use std::io::{ErrorKind, Read};
5
5
  use std::path::{Path, PathBuf};
6
6
  use std::process::{Child, Command, Stdio};
7
- use std::sync::{Arc, Condvar, Mutex};
7
+ use std::sync::{Arc, Condvar, Mutex, OnceLock};
8
8
  use std::time::{Duration, Instant};
9
9
  use tauri::{AppHandle, Emitter, Manager, State};
10
10
 
@@ -26,7 +26,7 @@ use screencapturekit::stream::{
26
26
  configuration::SCStreamConfiguration, content_filter::SCContentFilter,
27
27
  output_type::SCStreamOutputType, sc_stream::SCStream,
28
28
  };
29
- use std::sync::atomic::{AtomicBool, Ordering};
29
+ use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
30
30
 
31
31
  pub(crate) const QUICKTIME_RECORDING_MIME_TYPE: &str = "video/quicktime";
32
32
  pub(crate) const MP4_RECORDING_MIME_TYPE: &str = "video/mp4";
@@ -45,6 +45,8 @@ const TRANSCODE_RATE_LIMIT_OVERHEAD_KBPS: f64 = 64.0;
45
45
  const TRANSCODE_FRAME_RATE_LIMIT: u32 = 30;
46
46
  const NORMALIZED_AUDIO_BITRATE_KBPS: u32 = 160;
47
47
  const AUDIO_LOUDNESS_FILTER: &str = "loudnorm=I=-16:TP=-1.5:LRA=11";
48
+ const AUDIO_SIGNAL_MIN_MEAN_VOLUME_DB: f64 = -60.0;
49
+ const AUDIO_SIGNAL_MIN_MAX_VOLUME_DB: f64 = -30.0;
48
50
  // When the mic is captured alongside system audio, ScreenCaptureKit lays the
49
51
  // two sources out as the left/right channels of a single stereo track, which
50
52
  // plays back stuck on one speaker. Force the input to stereo (mono sources
@@ -54,25 +56,36 @@ const AUDIO_LOUDNESS_FILTER: &str = "loudnorm=I=-16:TP=-1.5:LRA=11";
54
56
  // stereo content that must not be flattened.
55
57
  const AUDIO_DOWNMIX_FILTER: &str =
56
58
  "aformat=channel_layouts=stereo,pan=stereo|FL=0.5*FL+0.5*FR|FR=0.5*FL+0.5*FR";
59
+ // Mild FFT denoise for native mic captures. Browser recordings already request
60
+ // WebRTC noise suppression; ScreenCaptureKit/screencapture mic capture does
61
+ // not, so reduce steady broadband room/mic hiss during the existing optimize
62
+ // step. Keep this conservative to avoid watery speech artifacts.
63
+ const AUDIO_DENOISE_FILTER: &str = "afftdn=nr=10:nf=-50:tn=1";
57
64
  // loudnorm operates internally at 192 kHz and emits at 192 kHz; without an
58
65
  // explicit output rate the AAC track ends up at 192 kHz and plays back slow.
59
66
  const AUDIO_OUTPUT_SAMPLE_RATE: u32 = 48000;
60
67
 
61
68
  // Loudness normalization, optionally preceded by the centered-stereo downmix
62
- // that repairs the mic+system L/R split. Pair with `-ar AUDIO_OUTPUT_SAMPLE_RATE`
63
- // so loudnorm's 192 kHz output is resampled back.
64
- fn audio_filter_chain(downmix: bool) -> String {
69
+ // that repairs the mic+system L/R split and denoise for native mic captures.
70
+ // Pair with `-ar AUDIO_OUTPUT_SAMPLE_RATE` so loudnorm's 192 kHz output is
71
+ // resampled back.
72
+ fn audio_filter_chain(downmix: bool, denoise: bool) -> String {
73
+ let mut filters = Vec::new();
65
74
  if downmix {
66
- format!("{AUDIO_DOWNMIX_FILTER},{AUDIO_LOUDNESS_FILTER}")
67
- } else {
68
- AUDIO_LOUDNESS_FILTER.to_string()
75
+ filters.push(AUDIO_DOWNMIX_FILTER);
76
+ }
77
+ if denoise {
78
+ filters.push(AUDIO_DENOISE_FILTER);
69
79
  }
80
+ filters.push(AUDIO_LOUDNESS_FILTER);
81
+ filters.join(",")
70
82
  }
71
83
  const NATIVE_CAPTURE_MAX_LONG_EDGE: u32 = 1280;
72
84
  const NATIVE_CAPTURE_FPS: u32 = 24;
73
85
  const AVCONVERT_PATH: &str = "/usr/bin/avconvert";
74
86
  const AVCONVERT_TIMEOUT: Duration = Duration::from_secs(5 * 60);
75
87
  const FFMPEG_TIMEOUT: Duration = Duration::from_secs(8 * 60);
88
+ const FFMPEG_AUDIO_PROBE_TIMEOUT: Duration = Duration::from_secs(90);
76
89
  const FFMPEG_CANDIDATE_PATHS: &[&str] = &[
77
90
  "ffmpeg",
78
91
  "/opt/homebrew/bin/ffmpeg",
@@ -95,6 +108,28 @@ const DISK_MONITOR_CRITICAL_BYTES: u64 = 250 * 1024 * 1024;
95
108
  // How often the background monitor checks free space.
96
109
  const DISK_MONITOR_INTERVAL_SECS: u64 = 30;
97
110
 
111
+ #[derive(Clone, Copy, Debug, PartialEq, Eq)]
112
+ enum NativeUploadMode {
113
+ Buffered,
114
+ Streaming,
115
+ }
116
+
117
+ impl NativeUploadMode {
118
+ fn from_option(value: Option<String>) -> Self {
119
+ match value.as_deref() {
120
+ Some("streaming") => Self::Streaming,
121
+ _ => Self::Buffered,
122
+ }
123
+ }
124
+
125
+ fn label(self) -> &'static str {
126
+ match self {
127
+ Self::Buffered => "buffered",
128
+ Self::Streaming => "streaming",
129
+ }
130
+ }
131
+ }
132
+
98
133
  #[derive(Default)]
99
134
  pub struct NativeFullscreenRecordingState {
100
135
  inner: Mutex<Option<NativeFullscreenSession>>,
@@ -187,9 +222,31 @@ pub(crate) enum NativeFullscreenBackend {
187
222
  /// silent second while ScreenCaptureKit's mic pipeline spins up.
188
223
  /// `None` when the recording has no microphone input.
189
224
  mic_ready: Option<Arc<AtomicBool>>,
225
+ /// Number of microphone sample buffers observed before/after attach.
226
+ /// This is diagnostic only; it lets the tray log distinguish "SCK
227
+ /// never saw mic samples" from "samples existed but encoded silent".
228
+ mic_sample_count: Option<Arc<AtomicU64>>,
190
229
  },
191
230
  }
192
231
 
232
+ /// Safety net for the `screencapture` fallback: if a session carrying a live
233
+ /// `screencapture` child is ever dropped without going through
234
+ /// `stop_native_recording`/`stop_screencapture` (app quit, crash unwind, or an
235
+ /// error path that discards the session), make sure the child process doesn't
236
+ /// keep recording and writing to disk after we've lost track of it. This is a
237
+ /// best-effort hard kill (no SIGINT grace period) since a `Drop` impl is not
238
+ /// the place to block on graceful finalization.
239
+ impl Drop for NativeFullscreenBackend {
240
+ fn drop(&mut self) {
241
+ if let NativeFullscreenBackend::Screencapture { child } = self {
242
+ if matches!(child.try_wait(), Ok(None)) {
243
+ let _ = child.kill();
244
+ let _ = child.wait();
245
+ }
246
+ }
247
+ }
248
+ }
249
+
193
250
  /// `SCRecordingOutput` finalizes the MP4 *asynchronously*: after
194
251
  /// `remove_recording_output()` / `stop_capture()` it still has to flush its
195
252
  /// last buffered sample fragment and write the `moov` atom, then it calls
@@ -258,11 +315,13 @@ impl SCRecordingOutputDelegate for FinishDelegate {
258
315
  }
259
316
  }
260
317
 
318
+ #[derive(Clone)]
261
319
  struct PreparedRecordingFile {
262
320
  path: PathBuf,
263
321
  mime_type: String,
264
322
  bytes: u64,
265
323
  temporary: bool,
324
+ locally_transcoded: bool,
266
325
  }
267
326
 
268
327
  pub(crate) fn format_mb(bytes: u64) -> String {
@@ -370,12 +429,93 @@ fn emit_native_upload_progress(
370
429
  );
371
430
  }
372
431
 
432
+ fn clear_recording_active(app: &AppHandle) {
433
+ let mut changed = false;
434
+ if let Some(state) = app.try_state::<crate::state::RecordingActive>() {
435
+ if let Ok(mut active) = state.0.lock() {
436
+ if *active {
437
+ *active = false;
438
+ changed = true;
439
+ }
440
+ }
441
+ }
442
+ if changed {
443
+ crate::tray::rebuild_tray_menu(app);
444
+ }
445
+ }
446
+
447
+ static LAST_NATIVE_UPLOAD_FINISHED: OnceLock<Mutex<Option<NativeUploadFinishedPayload>>> =
448
+ OnceLock::new();
449
+ static CLAIMED_NATIVE_UPLOAD_OPEN: OnceLock<Mutex<Option<String>>> = OnceLock::new();
450
+
451
+ fn last_native_upload_finished() -> &'static Mutex<Option<NativeUploadFinishedPayload>> {
452
+ LAST_NATIVE_UPLOAD_FINISHED.get_or_init(|| Mutex::new(None))
453
+ }
454
+
455
+ fn claimed_native_upload_open() -> &'static Mutex<Option<String>> {
456
+ CLAIMED_NATIVE_UPLOAD_OPEN.get_or_init(|| Mutex::new(None))
457
+ }
458
+
459
+ fn reset_native_upload_completion_state() {
460
+ if let Ok(mut last) = last_native_upload_finished().lock() {
461
+ *last = None;
462
+ }
463
+ if let Ok(mut claimed) = claimed_native_upload_open().lock() {
464
+ *claimed = None;
465
+ }
466
+ }
467
+
468
+ #[tauri::command]
469
+ pub fn native_fullscreen_take_upload_finished() -> Option<NativeUploadFinishedPayload> {
470
+ last_native_upload_finished()
471
+ .lock()
472
+ .ok()
473
+ .and_then(|mut last| last.take())
474
+ }
475
+
476
+ #[tauri::command]
477
+ pub fn native_fullscreen_claim_upload_open(recording_id: String) -> bool {
478
+ let Ok(mut claimed) = claimed_native_upload_open().lock() else {
479
+ return true;
480
+ };
481
+ if claimed.as_deref() == Some(recording_id.as_str()) {
482
+ return false;
483
+ }
484
+ *claimed = Some(recording_id);
485
+ true
486
+ }
487
+
488
+ fn emit_native_upload_finished(
489
+ app: &AppHandle,
490
+ server_url: &str,
491
+ recording_id: &str,
492
+ ok: bool,
493
+ error: Option<String>,
494
+ local_file_path: Option<&Path>,
495
+ ) {
496
+ clear_recording_active(app);
497
+ let base = server_url.trim_end_matches('/');
498
+ let payload = NativeUploadFinishedPayload {
499
+ recording_id: recording_id.to_string(),
500
+ ok,
501
+ view_url: format!("{base}/r/{recording_id}"),
502
+ error,
503
+ local_file_path: local_file_path.map(|path| path.to_string_lossy().to_string()),
504
+ };
505
+ if let Ok(mut last) = last_native_upload_finished().lock() {
506
+ *last = Some(payload.clone());
507
+ }
508
+ let _ = app.emit("clips:native-upload-finished", payload);
509
+ }
510
+
373
511
  #[derive(Debug, Clone, Serialize, Deserialize)]
374
512
  #[serde(rename_all = "camelCase")]
375
513
  struct SavedNativeRecording {
376
514
  recording_id: String,
377
515
  server_url: String,
378
516
  file_path: PathBuf,
517
+ #[serde(default)]
518
+ segment_paths: Vec<PathBuf>,
379
519
  mime_type: String,
380
520
  duration_ms: u128,
381
521
  width: Option<u32>,
@@ -435,6 +575,16 @@ pub struct NativeFullscreenUploadResult {
435
575
  bytes: u64,
436
576
  }
437
577
 
578
+ #[derive(Serialize, Clone)]
579
+ #[serde(rename_all = "camelCase")]
580
+ pub struct NativeUploadFinishedPayload {
581
+ recording_id: String,
582
+ ok: bool,
583
+ view_url: String,
584
+ error: Option<String>,
585
+ local_file_path: Option<String>,
586
+ }
587
+
438
588
  #[derive(Serialize)]
439
589
  #[serde(rename_all = "camelCase")]
440
590
  pub struct NativeLocalRecordingFile {
@@ -521,72 +671,53 @@ fn start_native_session_locked(
521
671
  defer_recording_output: bool,
522
672
  ) -> Result<NativeFullscreenStartInfo, String> {
523
673
  let safe_id = sanitize_recording_id(recording_id);
674
+ reset_native_upload_completion_state();
524
675
  let has_specific_mic = mic_device_id
525
676
  .as_deref()
526
677
  .is_some_and(|v| !v.trim().is_empty())
527
678
  || mic_device_label
528
679
  .as_deref()
529
680
  .is_some_and(|v| !v.trim().is_empty());
530
- let session = if include_audio && !capture_system_audio {
531
- if defer_recording_output {
532
- return Err(
533
- "Skipping microphone-only ScreenCaptureKit warmup; begin will use screencapture."
534
- .to_string(),
535
- );
536
- }
537
- if has_specific_mic {
538
- eprintln!(
539
- "[clips-tray] using screencapture for microphone-only recording; selected mic cannot be pinned by this backend"
540
- );
541
- } else {
681
+ let session = match start_screencapturekit_recording(
682
+ app,
683
+ &safe_id,
684
+ include_audio,
685
+ capture_system_audio,
686
+ mic_device_id.as_deref(),
687
+ mic_device_label.as_deref(),
688
+ capture_region,
689
+ defer_recording_output,
690
+ ) {
691
+ Ok(session) => session,
692
+ Err(sck_err) => {
693
+ if defer_recording_output {
694
+ return Err(sck_err);
695
+ }
696
+ if include_audio {
697
+ let mic_description = if has_specific_mic {
698
+ "the selected microphone"
699
+ } else {
700
+ "the resolved default microphone"
701
+ };
702
+ return Err(format!(
703
+ "ScreenCaptureKit recording failed before it could use {mic_description} ({sck_err}). Clips did not fall back to macOS screencapture because that would ignore the requested input."
704
+ ));
705
+ }
542
706
  eprintln!(
543
- "[clips-tray] using screencapture for microphone-only recording to avoid ScreenCaptureKit microphone finalization failures"
707
+ "[clips-tray] ScreenCaptureKit recording unavailable; falling back to screencapture: {sck_err}"
544
708
  );
545
- }
546
- start_screencapture_recording(
547
- app,
548
- &safe_id,
549
- include_audio,
550
- capture_system_audio,
551
- capture_region,
552
- )?
553
- } else {
554
- match start_screencapturekit_recording(
555
- app,
556
- &safe_id,
557
- include_audio,
558
- capture_system_audio,
559
- mic_device_id.as_deref(),
560
- mic_device_label.as_deref(),
561
- capture_region,
562
- defer_recording_output,
563
- ) {
564
- Ok(session) => session,
565
- Err(sck_err) => {
566
- if defer_recording_output {
567
- return Err(sck_err);
568
- }
569
- if include_audio && !capture_system_audio && has_specific_mic {
570
- return Err(format!(
571
- "ScreenCaptureKit recording failed before it could use the selected microphone ({sck_err}). Clips did not fall back to macOS screencapture because that would ignore your selected input."
572
- ));
573
- }
574
- eprintln!(
575
- "[clips-tray] ScreenCaptureKit recording unavailable; falling back to screencapture: {sck_err}"
576
- );
577
- start_screencapture_recording(
578
- app,
579
- &safe_id,
580
- include_audio,
581
- capture_system_audio,
582
- capture_region,
709
+ start_screencapture_recording(
710
+ app,
711
+ &safe_id,
712
+ include_audio,
713
+ capture_system_audio,
714
+ capture_region,
715
+ )
716
+ .map_err(|fallback_err| {
717
+ format!(
718
+ "ScreenCaptureKit recording failed ({sck_err}); screencapture fallback failed ({fallback_err})"
583
719
  )
584
- .map_err(|fallback_err| {
585
- format!(
586
- "ScreenCaptureKit recording failed ({sck_err}); screencapture fallback failed ({fallback_err})"
587
- )
588
- })?
589
- }
720
+ })?
590
721
  }
591
722
  };
592
723
  let width = session.width;
@@ -726,21 +857,34 @@ pub async fn native_fullscreen_recording_begin(
726
857
  );
727
858
  }
728
859
 
729
- // Grab the mic-ready flag without holding the lock during the wait.
730
- let mic_ready = {
860
+ // Grab the mic-ready diagnostics without holding the lock during the wait.
861
+ let mic_state = {
731
862
  let guard = state.inner.lock().map_err(|e| e.to_string())?;
732
863
  guard.as_ref().and_then(|s| match s.backend.as_ref() {
733
- Some(NativeFullscreenBackend::ScreenCaptureKit { mic_ready, .. }) => {
734
- mic_ready.clone()
735
- }
864
+ Some(NativeFullscreenBackend::ScreenCaptureKit {
865
+ mic_ready,
866
+ mic_sample_count,
867
+ ..
868
+ }) => mic_ready
869
+ .as_ref()
870
+ .map(|ready| (Arc::clone(ready), mic_sample_count.clone())),
736
871
  _ => None,
737
872
  })
738
873
  };
739
- if let Some(ready) = mic_ready {
874
+ let mut mic_ready_before_attach: Option<bool> = None;
875
+ let mut mic_samples_before_attach: Option<u64> = None;
876
+ let mut mic_warm_wait_ms: Option<u128> = None;
877
+ if let Some((ready, sample_count)) = mic_state {
878
+ let wait_started = Instant::now();
740
879
  let deadline = Instant::now() + Duration::from_millis(MIC_WARM_TIMEOUT_MS);
741
880
  while !ready.load(Ordering::Relaxed) && Instant::now() < deadline {
742
- std::thread::sleep(Duration::from_millis(15));
881
+ tokio::time::sleep(Duration::from_millis(15)).await;
743
882
  }
883
+ mic_ready_before_attach = Some(ready.load(Ordering::Relaxed));
884
+ mic_samples_before_attach = sample_count
885
+ .as_ref()
886
+ .map(|samples| samples.load(Ordering::Relaxed));
887
+ mic_warm_wait_ms = Some(wait_started.elapsed().as_millis());
744
888
  }
745
889
 
746
890
  let mut guard = state.inner.lock().map_err(|e| e.to_string())?;
@@ -757,6 +901,20 @@ pub async fn native_fullscreen_recording_begin(
757
901
  .add_recording_output(recording)
758
902
  .map_err(|e| format!("add recording output failed: {e:?}"))?;
759
903
  }
904
+ if mic_ready_before_attach.is_some() || mic_samples_before_attach.is_some() {
905
+ eprintln!(
906
+ "[clips-tray] ScreenCaptureKit recording output attached: mic_ready_before_attach={} mic_samples_before_attach={} mic_warm_wait_ms={}",
907
+ mic_ready_before_attach
908
+ .map(|ready| ready.to_string())
909
+ .unwrap_or_else(|| "n/a".to_string()),
910
+ mic_samples_before_attach
911
+ .map(|samples| samples.to_string())
912
+ .unwrap_or_else(|| "n/a".to_string()),
913
+ mic_warm_wait_ms
914
+ .map(|ms| ms.to_string())
915
+ .unwrap_or_else(|| "n/a".to_string())
916
+ );
917
+ }
760
918
  // Rebaseline the duration clock: the warm phase ran during the
761
919
  // countdown, so `started_at` (set at warm time) is several seconds
762
920
  // early. The clip's first written frame is now, so measure from now.
@@ -781,9 +939,11 @@ pub async fn native_fullscreen_recording_stop_and_upload(
781
939
  recording_id: String,
782
940
  auth_token: Option<String>,
783
941
  cookie: Option<String>,
942
+ upload_mode: Option<String>,
784
943
  has_audio: bool,
785
944
  has_camera: bool,
786
945
  ) -> Result<NativeFullscreenUploadResult, String> {
946
+ let upload_mode = NativeUploadMode::from_option(upload_mode);
787
947
  emit_native_upload_progress(&app, "finalizing", "Optimizing clip", None, None);
788
948
  // The recorder's ScreenCaptureKit stream is now fully stopped and its moov
789
949
  // atom is written (or has definitively failed). Signal the UI so it can tear
@@ -812,6 +972,40 @@ pub async fn native_fullscreen_recording_stop_and_upload(
812
972
  // corner through the (multi-second) finalize + upload phase, reading
813
973
  // as "still recording" while the bottom-left card says "processing".
814
974
  let _ = crate::clips::close_bubble(app.clone()).await;
975
+ clear_recording_active(&app);
976
+
977
+ if multi_segment {
978
+ if let Err(merge_err) = &consolidate_outcome {
979
+ let mut saved = saved_recording_from_segments(
980
+ &session,
981
+ &server_url,
982
+ &recording_id,
983
+ duration_ms,
984
+ has_audio,
985
+ has_camera,
986
+ )?;
987
+ saved.last_error = Some(match &stop_outcome {
988
+ Err(stop_err) => {
989
+ format!("{stop_err}. Segment consolidation failed: {merge_err}")
990
+ }
991
+ Ok(()) => merge_err.clone(),
992
+ });
993
+ write_saved_recording_metadata(&app, &saved)?;
994
+ emit_native_upload_progress(&app, "failed", "Upload paused", None, None);
995
+ let error = format!(
996
+ "{merge_err}. The raw clip segments were saved locally and can be retried from the Clips menu."
997
+ );
998
+ emit_native_upload_finished(
999
+ &app,
1000
+ &server_url,
1001
+ &recording_id,
1002
+ false,
1003
+ Some(error.clone()),
1004
+ Some(&saved.file_path),
1005
+ );
1006
+ return Err(error);
1007
+ }
1008
+ }
815
1009
 
816
1010
  let mut saved = saved_recording_from_session(
817
1011
  &session,
@@ -850,7 +1044,16 @@ pub async fn native_fullscreen_recording_stop_and_upload(
850
1044
  } else {
851
1045
  "The clip was saved locally and can be retried from the Clips menu."
852
1046
  };
853
- return Err(format!("{stop_err}. {suffix}"));
1047
+ let error = format!("{stop_err}. {suffix}");
1048
+ emit_native_upload_finished(
1049
+ &app,
1050
+ &server_url,
1051
+ &recording_id,
1052
+ false,
1053
+ Some(error.clone()),
1054
+ Some(&saved.file_path),
1055
+ );
1056
+ return Err(error);
854
1057
  }
855
1058
  Some(true) => {
856
1059
  eprintln!(
@@ -874,29 +1077,29 @@ pub async fn native_fullscreen_recording_stop_and_upload(
874
1077
  eprintln!("[clips-tray] recording missing moov after Ok stop outcome (likely finalize timeout) — saving as retryable, skipping upload");
875
1078
  write_saved_recording_metadata(&app, &saved)?;
876
1079
  emit_native_upload_progress(&app, "failed", "Upload paused", None, None);
877
- return Err(
878
- "Recorded MP4 is missing playback metadata. Please retry the recording.".to_string(),
1080
+ let error =
1081
+ "Recorded MP4 is missing playback metadata. Please retry the recording.".to_string();
1082
+ emit_native_upload_finished(
1083
+ &app,
1084
+ &server_url,
1085
+ &recording_id,
1086
+ false,
1087
+ Some(error.clone()),
1088
+ Some(&saved.file_path),
879
1089
  );
880
- } else if let Err(merge_err) = &consolidate_outcome {
881
- saved.last_error = Some(merge_err.clone());
1090
+ return Err(error);
882
1091
  }
883
1092
  write_saved_recording_metadata(&app, &saved)?;
884
1093
  emit_native_upload_progress(&app, "preparing", "Optimizing clip", None, None);
885
- if multi_segment {
886
- if let Err(merge_err) = consolidate_outcome {
887
- return Err(format!(
888
- "{merge_err}. The clip segments were saved locally and can be retried from the Clips menu."
889
- ));
890
- }
891
- }
892
1094
 
893
1095
  let result = upload_recording_file(
894
1096
  &app,
895
1097
  &session,
896
- server_url,
897
- recording_id,
1098
+ server_url.clone(),
1099
+ recording_id.clone(),
898
1100
  auth_token.unwrap_or_default(),
899
1101
  cookie.unwrap_or_default(),
1102
+ upload_mode,
900
1103
  duration_ms,
901
1104
  has_audio,
902
1105
  has_camera,
@@ -906,6 +1109,7 @@ pub async fn native_fullscreen_recording_stop_and_upload(
906
1109
  match result {
907
1110
  Ok(result) => {
908
1111
  clear_saved_recording_after_success(&app, &saved);
1112
+ emit_native_upload_finished(&app, &server_url, &recording_id, true, None, None);
909
1113
  Ok(result)
910
1114
  }
911
1115
  Err(err) => {
@@ -917,9 +1121,18 @@ pub async fn native_fullscreen_recording_stop_and_upload(
917
1121
  }
918
1122
  let _ = write_saved_recording_metadata(&app, &saved);
919
1123
  emit_native_upload_progress(&app, "failed", "Upload paused", None, None);
920
- Err(format!(
1124
+ let error = format!(
921
1125
  "{err}. The clip was saved locally and can be retried from the Clips menu."
922
- ))
1126
+ );
1127
+ emit_native_upload_finished(
1128
+ &app,
1129
+ &server_url,
1130
+ &recording_id,
1131
+ false,
1132
+ Some(error.clone()),
1133
+ Some(&saved.file_path),
1134
+ );
1135
+ Err(error)
923
1136
  }
924
1137
  }
925
1138
  }
@@ -1013,6 +1226,26 @@ pub async fn native_fullscreen_capture_thumbnail(
1013
1226
  Ok(())
1014
1227
  }
1015
1228
 
1229
+ /// Called from the app's exit path (tray Quit / Cmd+Q) so a live `screencapture`
1230
+ /// fallback process doesn't survive the app quitting. `app.exit()` triggers
1231
+ /// `std::process::exit` under the hood, which does not run Rust destructors,
1232
+ /// so this must run explicitly before exit rather than relying solely on
1233
+ /// `NativeFullscreenBackend`'s `Drop` impl. Synchronous and best-effort: no
1234
+ /// finalize/upload, just make sure nothing keeps recording after we're gone.
1235
+ pub(crate) fn kill_active_screencapture_child(state: &NativeFullscreenRecordingState) {
1236
+ let Ok(mut guard) = state.inner.lock() else {
1237
+ return;
1238
+ };
1239
+ if let Some(session) = guard.as_mut() {
1240
+ if let Some(NativeFullscreenBackend::Screencapture { child }) = session.backend.as_mut() {
1241
+ if matches!(child.try_wait(), Ok(None)) {
1242
+ let _ = child.kill();
1243
+ let _ = child.wait();
1244
+ }
1245
+ }
1246
+ }
1247
+ }
1248
+
1016
1249
  #[tauri::command]
1017
1250
  pub async fn native_fullscreen_recording_cancel(
1018
1251
  state: State<'_, NativeFullscreenRecordingState>,
@@ -1049,7 +1282,11 @@ pub async fn native_fullscreen_recording_pause(
1049
1282
  session.paused_at = Some(Instant::now());
1050
1283
  return Ok(());
1051
1284
  }
1052
- finalize_active_backend(session, true)?;
1285
+ let stop_outcome = finalize_active_backend(session, true);
1286
+ if let Err(err) = &stop_outcome {
1287
+ eprintln!("[clips-tray] pause finalize reported an error: {err}");
1288
+ }
1289
+ recover_from_unusable_current_segment(session, "pause", true);
1053
1290
  session.paused_at = Some(Instant::now());
1054
1291
  Ok(())
1055
1292
  }
@@ -1435,6 +1672,19 @@ fn start_segment_backend(
1435
1672
  ) {
1436
1673
  Ok((backend, w, h)) => return Ok((backend, w, h)),
1437
1674
  Err(sck_err) => {
1675
+ if include_audio {
1676
+ let mic_description = if mic_device_id
1677
+ .is_some_and(|value| !value.trim().is_empty())
1678
+ || mic_device_label.is_some_and(|value| !value.trim().is_empty())
1679
+ {
1680
+ "the selected microphone"
1681
+ } else {
1682
+ "the resolved default microphone"
1683
+ };
1684
+ return Err(format!(
1685
+ "ScreenCaptureKit resume failed before it could use {mic_description} ({sck_err}). Clips did not fall back to macOS screencapture because that would ignore the requested input."
1686
+ ));
1687
+ }
1438
1688
  eprintln!(
1439
1689
  "[clips-tray] ScreenCaptureKit resume failed; falling back to screencapture: {sck_err}"
1440
1690
  );
@@ -1514,12 +1764,7 @@ pub(crate) fn start_screencapturekit_backend_at(
1514
1764
  } else {
1515
1765
  filter_builder.build()
1516
1766
  };
1517
- let capture_microphone_in_recording = false;
1518
- if include_audio {
1519
- eprintln!(
1520
- "[clips-tray] ScreenCaptureKit microphone recording disabled; microphone audio stays out of SCRecordingOutput to avoid macOS 15 finalize timeouts"
1521
- );
1522
- }
1767
+ let capture_microphone_in_recording = include_audio;
1523
1768
  let selected_mic = if capture_microphone_in_recording {
1524
1769
  resolve_microphone_capture_device(mic_device_id, mic_device_label)?
1525
1770
  } else {
@@ -1533,8 +1778,8 @@ pub(crate) fn start_screencapturekit_backend_at(
1533
1778
  .with_queue_depth(8)
1534
1779
  .with_shows_cursor(true)
1535
1780
  // Mic and system audio are independent toggles. SCK delivers them as
1536
- // separate inputs. We intentionally do not hand microphone buffers to
1537
- // SCRecordingOutput because macOS 15 can fail to finalize those MP4s.
1781
+ // separate inputs so recordings can include both the user's mic and
1782
+ // system audio.
1538
1783
  .with_captures_audio(capture_system_audio)
1539
1784
  .with_captures_microphone(capture_microphone_in_recording)
1540
1785
  .with_excludes_current_process_audio(true)
@@ -1569,20 +1814,23 @@ pub(crate) fn start_screencapturekit_backend_at(
1569
1814
  let mut stream = SCStream::new(&filter, &config);
1570
1815
  // Observe the microphone stream so `begin` can wait for the first sample
1571
1816
  // before attaching the recording output (avoids the silent-mic head).
1572
- let mic_ready = if capture_microphone_in_recording {
1817
+ let (mic_ready, mic_sample_count) = if capture_microphone_in_recording {
1573
1818
  let flag = Arc::new(AtomicBool::new(false));
1819
+ let sample_count = Arc::new(AtomicU64::new(0));
1574
1820
  let flag_cb = Arc::clone(&flag);
1821
+ let sample_count_cb = Arc::clone(&sample_count);
1575
1822
  stream.add_output_handler(
1576
1823
  move |_sample, of_type| {
1577
1824
  if matches!(of_type, SCStreamOutputType::Microphone) {
1825
+ sample_count_cb.fetch_add(1, Ordering::Relaxed);
1578
1826
  flag_cb.store(true, Ordering::Relaxed);
1579
1827
  }
1580
1828
  },
1581
1829
  SCStreamOutputType::Microphone,
1582
1830
  );
1583
- Some(flag)
1831
+ (Some(flag), Some(sample_count))
1584
1832
  } else {
1585
- None
1833
+ (None, None)
1586
1834
  };
1587
1835
  if !defer_recording_output {
1588
1836
  stream
@@ -1603,6 +1851,7 @@ pub(crate) fn start_screencapturekit_backend_at(
1603
1851
  recording,
1604
1852
  finish,
1605
1853
  mic_ready,
1854
+ mic_sample_count,
1606
1855
  },
1607
1856
  Some(width),
1608
1857
  Some(height),
@@ -1756,7 +2005,7 @@ pub async fn native_fullscreen_pending_uploads(
1756
2005
  let Ok(saved) = read_saved_recording_metadata_path(&path) else {
1757
2006
  continue;
1758
2007
  };
1759
- if saved.file_path.exists() {
2008
+ if saved_recording_has_local_artifact(&saved) {
1760
2009
  pending.push(PendingNativeRecording::from(&saved));
1761
2010
  } else {
1762
2011
  let _ = std::fs::remove_file(path);
@@ -1857,6 +2106,10 @@ fn describe_recording_path(path: &Path) -> String {
1857
2106
  )
1858
2107
  }
1859
2108
 
2109
+ fn saved_recording_has_local_artifact(saved: &SavedNativeRecording) -> bool {
2110
+ saved.file_path.exists() || saved.segment_paths.iter().any(|path| path.exists())
2111
+ }
2112
+
1860
2113
  fn pending_uploads_dir(app: &AppHandle) -> Result<PathBuf, String> {
1861
2114
  let dir = app
1862
2115
  .path()
@@ -1938,6 +2191,37 @@ fn names_match(a: &str, b: &str) -> bool {
1938
2191
  short.iter().all(|token| long.contains(token))
1939
2192
  }
1940
2193
 
2194
+ #[cfg(target_os = "macos")]
2195
+ fn is_built_in_input_name(value: &str) -> bool {
2196
+ let value = normalize_audio_device_name(value);
2197
+ value.contains("macbook")
2198
+ || value.contains("built in")
2199
+ || value.contains("builtin")
2200
+ || value.contains("internal microphone")
2201
+ }
2202
+
2203
+ #[cfg(target_os = "macos")]
2204
+ fn is_phone_input_name(value: &str) -> bool {
2205
+ let value = normalize_audio_device_name(value);
2206
+ value.contains("iphone")
2207
+ || value.contains("ipad")
2208
+ || value.contains("continuity")
2209
+ || value.contains("phone microphone")
2210
+ }
2211
+
2212
+ #[cfg(target_os = "macos")]
2213
+ fn preferred_default_microphone_device(devices: &[AudioInputDevice]) -> Option<AudioInputDevice> {
2214
+ devices
2215
+ .iter()
2216
+ .find(|device| is_built_in_input_name(&device.name))
2217
+ .or_else(|| {
2218
+ devices
2219
+ .iter()
2220
+ .find(|device| !is_phone_input_name(&device.name))
2221
+ })
2222
+ .cloned()
2223
+ }
2224
+
1941
2225
  #[cfg(target_os = "macos")]
1942
2226
  fn resolve_microphone_capture_device(
1943
2227
  device_id: Option<&str>,
@@ -1948,11 +2232,6 @@ fn resolve_microphone_capture_device(
1948
2232
  .map(str::trim)
1949
2233
  .filter(|value| !value.is_empty());
1950
2234
 
1951
- if device_id.is_none() && device_label.is_none() {
1952
- eprintln!("[clips-tray] audio input devices not provided");
1953
- return Ok(None);
1954
- }
1955
-
1956
2235
  let devices = AudioInputDevice::list();
1957
2236
  devices.iter().for_each(|device| {
1958
2237
  eprintln!(
@@ -1961,6 +2240,18 @@ fn resolve_microphone_capture_device(
1961
2240
  );
1962
2241
  });
1963
2242
 
2243
+ if device_id.is_none() && device_label.is_none() {
2244
+ let resolved = preferred_default_microphone_device(&devices);
2245
+ eprintln!(
2246
+ "[clips-tray] mic resolve: no explicit input provided -> {}",
2247
+ match &resolved {
2248
+ Some(device) => format!("using {} ({})", device.name, device.id),
2249
+ None => "using macOS default input".to_string(),
2250
+ }
2251
+ );
2252
+ return Ok(resolved);
2253
+ }
2254
+
1964
2255
  let resolved = device_id
1965
2256
  .and_then(|id| devices.iter().find(|device| device.id == id))
1966
2257
  .or_else(|| {
@@ -2187,9 +2478,70 @@ fn saved_recording_from_session(
2187
2478
  has_audio: bool,
2188
2479
  has_camera: bool,
2189
2480
  ) -> Result<SavedNativeRecording, String> {
2190
- let bytes = std::fs::metadata(&session.path)
2481
+ saved_recording_from_path(
2482
+ session,
2483
+ &session.path,
2484
+ Vec::new(),
2485
+ server_url,
2486
+ recording_id,
2487
+ duration_ms,
2488
+ has_audio,
2489
+ has_camera,
2490
+ )
2491
+ }
2492
+
2493
+ fn saved_recording_from_segments(
2494
+ session: &NativeFullscreenSession,
2495
+ server_url: &str,
2496
+ recording_id: &str,
2497
+ duration_ms: u128,
2498
+ has_audio: bool,
2499
+ has_camera: bool,
2500
+ ) -> Result<SavedNativeRecording, String> {
2501
+ let segment_paths: Vec<PathBuf> = session
2502
+ .segments
2503
+ .iter()
2504
+ .filter(|path| path.exists())
2505
+ .cloned()
2506
+ .collect();
2507
+ let fallback_path = segment_paths
2508
+ .iter()
2509
+ .find(|path| playable_recording_file(path, session.mime_type))
2510
+ .or_else(|| {
2511
+ segment_paths.iter().find(|path| {
2512
+ std::fs::metadata(path)
2513
+ .map(|meta| meta.len() > 0)
2514
+ .unwrap_or(false)
2515
+ })
2516
+ })
2517
+ .cloned()
2518
+ .ok_or_else(|| "No local recording segment survived consolidation failure.".to_string())?;
2519
+
2520
+ saved_recording_from_path(
2521
+ session,
2522
+ &fallback_path,
2523
+ segment_paths,
2524
+ server_url,
2525
+ recording_id,
2526
+ duration_ms,
2527
+ has_audio,
2528
+ has_camera,
2529
+ )
2530
+ }
2531
+
2532
+ fn saved_recording_from_path(
2533
+ session: &NativeFullscreenSession,
2534
+ file_path: &Path,
2535
+ segment_paths: Vec<PathBuf>,
2536
+ server_url: &str,
2537
+ recording_id: &str,
2538
+ duration_ms: u128,
2539
+ has_audio: bool,
2540
+ has_camera: bool,
2541
+ ) -> Result<SavedNativeRecording, String> {
2542
+ let bytes = std::fs::metadata(file_path)
2191
2543
  .map_err(|e| {
2192
- let diag = describe_recording_path(&session.path);
2544
+ let diag = describe_recording_path(file_path);
2193
2545
  eprintln!(
2194
2546
  "[clips-tray] native recording file missing at save: {e}; backend={}, segments={}, {diag}",
2195
2547
  session.mime_type,
@@ -2201,7 +2553,7 @@ fn saved_recording_from_session(
2201
2553
  if bytes == 0 {
2202
2554
  eprintln!(
2203
2555
  "[clips-tray] native recording empty at save: {}",
2204
- describe_recording_path(&session.path)
2556
+ describe_recording_path(file_path)
2205
2557
  );
2206
2558
  return Err("Native recording produced an empty file.".into());
2207
2559
  }
@@ -2209,7 +2561,8 @@ fn saved_recording_from_session(
2209
2561
  Ok(SavedNativeRecording {
2210
2562
  recording_id: recording_id.to_string(),
2211
2563
  server_url: server_url.trim_end_matches('/').to_string(),
2212
- file_path: session.path.clone(),
2564
+ file_path: file_path.to_path_buf(),
2565
+ segment_paths,
2213
2566
  mime_type: session.mime_type.to_string(),
2214
2567
  duration_ms,
2215
2568
  width: session.width,
@@ -2261,6 +2614,11 @@ fn remove_saved_file(path: &Path, label: &str) -> Result<(), String> {
2261
2614
 
2262
2615
  fn clear_saved_recording(app: &AppHandle, saved: &SavedNativeRecording) -> Result<(), String> {
2263
2616
  remove_saved_file(&saved.file_path, "pending recording file")?;
2617
+ for segment_path in &saved.segment_paths {
2618
+ if segment_path != &saved.file_path {
2619
+ remove_saved_file(segment_path, "pending recording segment")?;
2620
+ }
2621
+ }
2264
2622
  let path = saved_recording_metadata_path(app, &saved.recording_id)?;
2265
2623
  remove_saved_file(&path, "pending recording metadata")
2266
2624
  }
@@ -2391,7 +2749,7 @@ fn start_screencapturekit_recording(
2391
2749
  safe_id: safe_id.to_string(),
2392
2750
  include_audio,
2393
2751
  capture_system_audio,
2394
- mic_captured_in_file: false,
2752
+ mic_captured_in_file: include_audio,
2395
2753
  mic_device_id: mic_device_id.map(str::to_string),
2396
2754
  mic_device_label: mic_device_label.map(str::to_string),
2397
2755
  segment_counter: 0,
@@ -2589,44 +2947,23 @@ pub(crate) fn stop_native_recording(
2589
2947
  match backend {
2590
2948
  NativeFullscreenBackend::Screencapture { child } => stop_screencapture(child),
2591
2949
  #[cfg(target_os = "macos")]
2592
- NativeFullscreenBackend::ScreenCaptureKit {
2593
- stream,
2594
- recording,
2595
- finish,
2596
- ..
2597
- } => {
2598
- // Removing the recording output is the clean "stop recording" path.
2599
- // It lets SCRecordingOutput finish while the stream is still alive;
2600
- // stopping the stream first can make ReplayKit report -5814 while
2601
- // writing the MP4's final metadata.
2602
- let remove_result = stream
2603
- .remove_recording_output(recording)
2604
- .map_err(|e| format!("ScreenCaptureKit recording finalize failed: {e:?}"));
2605
- let remove_result = match remove_result {
2606
- Ok(v) => Ok(v),
2607
- Err(first_err) => {
2608
- eprintln!("[clips-tray] waiting 200ms for SCK frame buffer to drain before remove_recording_output retry");
2609
- std::thread::sleep(Duration::from_millis(200));
2610
- stream.remove_recording_output(recording).map_err(|e| {
2611
- format!(
2612
- "ScreenCaptureKit recording finalize failed (retry): {e:?}; first attempt: {first_err}"
2613
- )
2614
- })
2615
- }
2616
- };
2617
- // remove_recording_output()/stop_capture() only *trigger* the
2618
- // async finalize; the MP4 isn't complete until the delegate's
2619
- // recording_did_finish fires. Prefer waiting after removal while
2620
- // the stream is alive, then stop the stream after the output has
2621
- // had a chance to flush.
2622
- let remove_ok = remove_result.is_ok();
2950
+ NativeFullscreenBackend::ScreenCaptureKit { stream, finish, .. } => {
2951
+ // `remove_recording_output()` looks like the clean stop path, but
2952
+ // on real machines it can block synchronously forever when the
2953
+ // underlying SCStream connection is interrupted. `stop_capture()`
2954
+ // returns control to us, then the delegate callback is bounded by
2955
+ // `SCK_FINALIZE_TIMEOUT`; the moov/audio guards below decide
2956
+ // whether the resulting file is uploadable or recoverable.
2957
+ let stop_result = stream
2958
+ .stop_capture()
2959
+ .map_err(|e| format!("ScreenCaptureKit stop failed: {e:?}"));
2623
2960
  let mut waited_for_finalize = false;
2624
- let mut finalize_outcome = if wait_for_finalize && remove_ok {
2961
+ let finalize_outcome = if wait_for_finalize {
2625
2962
  waited_for_finalize = true;
2626
2963
  let outcome = finish.wait(SCK_FINALIZE_TIMEOUT);
2627
2964
  if outcome.is_none() {
2628
2965
  eprintln!(
2629
- "[clips-tray] SCRecordingOutput finalize callback did not fire within {}s after remove_recording_output; saving file as-is",
2966
+ "[clips-tray] SCRecordingOutput finalize callback did not fire within {}s after stop_capture; saving file as-is",
2630
2967
  SCK_FINALIZE_TIMEOUT.as_secs()
2631
2968
  );
2632
2969
  }
@@ -2635,21 +2972,6 @@ pub(crate) fn stop_native_recording(
2635
2972
  None
2636
2973
  };
2637
2974
 
2638
- let stop_result = stream
2639
- .stop_capture()
2640
- .map_err(|e| format!("ScreenCaptureKit stop failed: {e:?}"));
2641
-
2642
- if wait_for_finalize && !waited_for_finalize && stop_result.is_ok() {
2643
- waited_for_finalize = true;
2644
- finalize_outcome = finish.wait(SCK_FINALIZE_TIMEOUT);
2645
- if finalize_outcome.is_none() {
2646
- eprintln!(
2647
- "[clips-tray] SCRecordingOutput finalize callback did not fire within {}s after stop_capture; saving file as-is",
2648
- SCK_FINALIZE_TIMEOUT.as_secs()
2649
- );
2650
- }
2651
- }
2652
-
2653
2975
  // Check the delegate outcome BEFORE returning on stop_result. When
2654
2976
  // stop_capture() fails AND recording_did_fail fires, callers must see
2655
2977
  // the "finalization callback failed" prefix to correctly identify
@@ -2670,25 +2992,10 @@ pub(crate) fn stop_native_recording(
2670
2992
  );
2671
2993
  }
2672
2994
 
2673
- let remove_recoverable = matches!(finalize_outcome.as_ref(), Some(Ok(())))
2674
- || (waited_for_finalize && finalize_outcome.is_none());
2675
- match remove_result {
2676
- Ok(()) => {}
2677
- Err(remove_err) => {
2678
- if remove_recoverable {
2679
- eprintln!(
2680
- "[clips-tray] ScreenCaptureKit recording output removal reported an error after finalize completed or timed out; continuing upload: {remove_err}"
2681
- );
2682
- } else {
2683
- return Err(remove_err);
2684
- }
2685
- }
2686
- }
2687
-
2688
2995
  if let Err(stop_err) = stop_result {
2689
- if remove_ok || matches!(finalize_outcome.as_ref(), Some(Ok(()))) {
2996
+ if matches!(finalize_outcome.as_ref(), Some(Ok(()))) {
2690
2997
  eprintln!(
2691
- "[clips-tray] ScreenCaptureKit stop_capture reported an error after recording output teardown; continuing upload: {stop_err}"
2998
+ "[clips-tray] ScreenCaptureKit stop_capture reported an error after finalize completed; continuing upload: {stop_err}"
2692
2999
  );
2693
3000
  } else {
2694
3001
  return Err(stop_err);
@@ -2743,6 +3050,7 @@ async fn upload_recording_file(
2743
3050
  recording_id: String,
2744
3051
  auth_token: String,
2745
3052
  cookie: String,
3053
+ upload_mode: NativeUploadMode,
2746
3054
  duration_ms: u128,
2747
3055
  has_audio: bool,
2748
3056
  has_camera: bool,
@@ -2764,6 +3072,7 @@ async fn upload_recording_file(
2764
3072
  recording_id,
2765
3073
  auth_token,
2766
3074
  cookie,
3075
+ upload_mode,
2767
3076
  duration_ms,
2768
3077
  session.width,
2769
3078
  session.height,
@@ -2784,23 +3093,15 @@ async fn upload_saved_recording_file(
2784
3093
  auth_token: String,
2785
3094
  cookie: String,
2786
3095
  ) -> Result<NativeFullscreenUploadResult, String> {
2787
- let prepared = prepare_recording_file(
2788
- app,
2789
- &saved.file_path,
2790
- &saved.mime_type,
2791
- saved.width,
2792
- saved.height,
2793
- Some(saved.duration_ms),
2794
- saved.has_audio,
2795
- saved.mic_captured,
2796
- )?;
2797
- let upload_result = upload_prepared_recording_file(
3096
+ let (prepared, retry_combined_path) = prepare_saved_recording_file(app, saved)?;
3097
+ let upload_result = upload_prepared_recording_file(
2798
3098
  app,
2799
3099
  &prepared,
2800
3100
  server_url,
2801
3101
  saved.recording_id.clone(),
2802
3102
  auth_token,
2803
3103
  cookie,
3104
+ NativeUploadMode::Buffered,
2804
3105
  saved.duration_ms,
2805
3106
  saved.width,
2806
3107
  saved.height,
@@ -2811,9 +3112,64 @@ async fn upload_saved_recording_file(
2811
3112
  if prepared.temporary {
2812
3113
  let _ = std::fs::remove_file(&prepared.path);
2813
3114
  }
3115
+ if let Some(path) = retry_combined_path {
3116
+ if path != prepared.path || !prepared.temporary {
3117
+ let _ = std::fs::remove_file(path);
3118
+ }
3119
+ }
2814
3120
  upload_result
2815
3121
  }
2816
3122
 
3123
+ fn prepare_saved_recording_file(
3124
+ app: &AppHandle,
3125
+ saved: &SavedNativeRecording,
3126
+ ) -> Result<(PreparedRecordingFile, Option<PathBuf>), String> {
3127
+ let retry_combined_path = if saved.segment_paths.len() > 1 {
3128
+ let output = retry_combined_recording_path(saved);
3129
+ let _ = std::fs::remove_file(&output);
3130
+ concat_saved_recording_segments(&saved.segment_paths, &output)?;
3131
+ Some(output)
3132
+ } else {
3133
+ None
3134
+ };
3135
+ let source_path = retry_combined_path
3136
+ .as_ref()
3137
+ .unwrap_or(&saved.file_path)
3138
+ .to_path_buf();
3139
+ let prepared = prepare_recording_file(
3140
+ app,
3141
+ &source_path,
3142
+ &saved.mime_type,
3143
+ saved.width,
3144
+ saved.height,
3145
+ Some(saved.duration_ms),
3146
+ saved.has_audio,
3147
+ saved.mic_captured,
3148
+ )?;
3149
+ Ok((prepared, retry_combined_path))
3150
+ }
3151
+
3152
+ fn retry_combined_recording_path(saved: &SavedNativeRecording) -> PathBuf {
3153
+ let stem = saved
3154
+ .file_path
3155
+ .file_stem()
3156
+ .and_then(|value| value.to_str())
3157
+ .unwrap_or("recording");
3158
+ saved
3159
+ .file_path
3160
+ .with_file_name(format!("{stem}-retry-combined.mp4"))
3161
+ }
3162
+
3163
+ #[cfg(target_os = "macos")]
3164
+ fn concat_saved_recording_segments(segments: &[PathBuf], output: &Path) -> Result<(), String> {
3165
+ concat_mp4_segments(segments, output)
3166
+ }
3167
+
3168
+ #[cfg(not(target_os = "macos"))]
3169
+ fn concat_saved_recording_segments(_segments: &[PathBuf], _output: &Path) -> Result<(), String> {
3170
+ Err("Segment concat is only available on macOS.".into())
3171
+ }
3172
+
2817
3173
  async fn upload_prepared_recording_file(
2818
3174
  app: &AppHandle,
2819
3175
  prepared: &PreparedRecordingFile,
@@ -2821,6 +3177,7 @@ async fn upload_prepared_recording_file(
2821
3177
  recording_id: String,
2822
3178
  auth_token: String,
2823
3179
  cookie: String,
3180
+ upload_mode: NativeUploadMode,
2824
3181
  duration_ms: u128,
2825
3182
  width: Option<u32>,
2826
3183
  height: Option<u32>,
@@ -2828,9 +3185,21 @@ async fn upload_prepared_recording_file(
2828
3185
  has_camera: bool,
2829
3186
  ) -> Result<NativeFullscreenUploadResult, String> {
2830
3187
  let total_bytes = prepared.bytes;
2831
- let total_chunks = ((total_bytes as usize) + UPLOAD_CHUNK_BYTES - 1) / UPLOAD_CHUNK_BYTES;
2832
- let total_posts = total_chunks + 1;
3188
+ let total_bytes_usize = usize::try_from(total_bytes)
3189
+ .map_err(|_| "Native recording is too large to upload on this system.".to_string())?;
3190
+ let total_chunks = (total_bytes_usize + UPLOAD_CHUNK_BYTES - 1) / UPLOAD_CHUNK_BYTES;
3191
+ let streaming_full_chunks = total_bytes_usize / UPLOAD_CHUNK_BYTES;
3192
+ let streaming_remainder = total_bytes_usize % UPLOAD_CHUNK_BYTES;
3193
+ let total_posts = if upload_mode == NativeUploadMode::Streaming {
3194
+ streaming_full_chunks + 1
3195
+ } else {
3196
+ total_chunks + 1
3197
+ };
2833
3198
  emit_native_upload_progress(app, "uploading", "Uploading clip", None, Some(0.0));
3199
+ eprintln!(
3200
+ "[clips-tray] native upload starting recording={recording_id} mode={} bytes={total_bytes} posts={total_posts}",
3201
+ upload_mode.label()
3202
+ );
2834
3203
  let client = reqwest::Client::builder()
2835
3204
  .timeout(Duration::from_secs(180))
2836
3205
  .build()
@@ -2838,68 +3207,144 @@ async fn upload_prepared_recording_file(
2838
3207
  let mut file =
2839
3208
  File::open(&prepared.path).map_err(|e| format!("native recording open failed: {e}"))?;
2840
3209
 
2841
- for index in 0..total_chunks {
2842
- let mut buffer = vec![0_u8; UPLOAD_CHUNK_BYTES];
2843
- let read = file
2844
- .read(&mut buffer)
2845
- .map_err(|e| format!("native recording read failed: {e}"))?;
2846
- if read == 0 {
2847
- return Err("Native recording ended before all chunks were read.".into());
3210
+ if upload_mode == NativeUploadMode::Streaming {
3211
+ // Resumable providers require every non-final body to be aligned. The
3212
+ // final body may be the unaligned tail; if the file is exactly aligned,
3213
+ // an empty final request closes the session.
3214
+ for index in 0..streaming_full_chunks {
3215
+ let mut buffer = vec![0_u8; UPLOAD_CHUNK_BYTES];
3216
+ file.read_exact(&mut buffer)
3217
+ .map_err(|e| format!("native recording read failed: {e}"))?;
3218
+ send_upload_post(
3219
+ &client,
3220
+ &server_url,
3221
+ &recording_id,
3222
+ &auth_token,
3223
+ &cookie,
3224
+ index,
3225
+ total_posts,
3226
+ false,
3227
+ None,
3228
+ &prepared.mime_type,
3229
+ width,
3230
+ height,
3231
+ has_audio,
3232
+ has_camera,
3233
+ upload_mode,
3234
+ false,
3235
+ buffer,
3236
+ )
3237
+ .await?;
3238
+ emit_native_upload_progress(
3239
+ app,
3240
+ "uploading",
3241
+ "Uploading clip",
3242
+ None,
3243
+ Some((index + 1) as f32 / total_posts as f32),
3244
+ );
3245
+ }
3246
+
3247
+ let mut final_body = vec![0_u8; streaming_remainder];
3248
+ if streaming_remainder > 0 {
3249
+ file.read_exact(&mut final_body)
3250
+ .map_err(|e| format!("native recording read failed: {e}"))?;
2848
3251
  }
2849
- buffer.truncate(read);
3252
+
3253
+ emit_native_upload_progress(
3254
+ app,
3255
+ "processing",
3256
+ "Uploading clip",
3257
+ None,
3258
+ Some(streaming_full_chunks as f32 / total_posts as f32),
3259
+ );
2850
3260
  send_upload_post(
2851
3261
  &client,
2852
3262
  &server_url,
2853
3263
  &recording_id,
2854
3264
  &auth_token,
2855
3265
  &cookie,
2856
- index,
3266
+ streaming_full_chunks,
2857
3267
  total_posts,
2858
- false,
2859
- None,
3268
+ true,
3269
+ Some(duration_ms),
2860
3270
  &prepared.mime_type,
2861
3271
  width,
2862
3272
  height,
2863
3273
  has_audio,
2864
3274
  has_camera,
2865
- buffer,
3275
+ upload_mode,
3276
+ prepared.locally_transcoded,
3277
+ final_body,
2866
3278
  )
2867
3279
  .await?;
3280
+ } else {
3281
+ for index in 0..total_chunks {
3282
+ let mut buffer = vec![0_u8; UPLOAD_CHUNK_BYTES];
3283
+ let read = file
3284
+ .read(&mut buffer)
3285
+ .map_err(|e| format!("native recording read failed: {e}"))?;
3286
+ if read == 0 {
3287
+ return Err("Native recording ended before all chunks were read.".into());
3288
+ }
3289
+ buffer.truncate(read);
3290
+ send_upload_post(
3291
+ &client,
3292
+ &server_url,
3293
+ &recording_id,
3294
+ &auth_token,
3295
+ &cookie,
3296
+ index,
3297
+ total_posts,
3298
+ false,
3299
+ None,
3300
+ &prepared.mime_type,
3301
+ width,
3302
+ height,
3303
+ has_audio,
3304
+ has_camera,
3305
+ upload_mode,
3306
+ false,
3307
+ buffer,
3308
+ )
3309
+ .await?;
3310
+ emit_native_upload_progress(
3311
+ app,
3312
+ "uploading",
3313
+ "Uploading clip",
3314
+ None,
3315
+ Some((index + 1) as f32 / total_posts as f32),
3316
+ );
3317
+ }
3318
+
2868
3319
  emit_native_upload_progress(
2869
3320
  app,
2870
- "uploading",
3321
+ "processing",
2871
3322
  "Uploading clip",
2872
3323
  None,
2873
- Some((index + 1) as f32 / total_posts as f32),
3324
+ Some(total_chunks as f32 / total_posts as f32),
2874
3325
  );
3326
+ send_upload_post(
3327
+ &client,
3328
+ &server_url,
3329
+ &recording_id,
3330
+ &auth_token,
3331
+ &cookie,
3332
+ total_chunks,
3333
+ total_posts,
3334
+ true,
3335
+ Some(duration_ms),
3336
+ &prepared.mime_type,
3337
+ width,
3338
+ height,
3339
+ has_audio,
3340
+ has_camera,
3341
+ upload_mode,
3342
+ prepared.locally_transcoded,
3343
+ Vec::new(),
3344
+ )
3345
+ .await?;
2875
3346
  }
2876
3347
 
2877
- emit_native_upload_progress(
2878
- app,
2879
- "processing",
2880
- "Uploading clip",
2881
- None,
2882
- Some(total_chunks as f32 / total_posts as f32),
2883
- );
2884
- send_upload_post(
2885
- &client,
2886
- &server_url,
2887
- &recording_id,
2888
- &auth_token,
2889
- &cookie,
2890
- total_chunks,
2891
- total_posts,
2892
- true,
2893
- Some(duration_ms),
2894
- &prepared.mime_type,
2895
- width,
2896
- height,
2897
- has_audio,
2898
- has_camera,
2899
- Vec::new(),
2900
- )
2901
- .await?;
2902
-
2903
3348
  emit_native_upload_progress(app, "opening", "Uploading clip", None, Some(1.0));
2904
3349
  Ok(NativeFullscreenUploadResult {
2905
3350
  recording_id,
@@ -3016,8 +3461,11 @@ async fn send_upload_post(
3016
3461
  height: Option<u32>,
3017
3462
  has_audio: bool,
3018
3463
  has_camera: bool,
3464
+ upload_mode: NativeUploadMode,
3465
+ locally_transcoded: bool,
3019
3466
  body: Vec<u8>,
3020
3467
  ) -> Result<(), String> {
3468
+ let body_len = body.len();
3021
3469
  let url = upload_url(
3022
3470
  server_url,
3023
3471
  recording_id,
@@ -3030,7 +3478,12 @@ async fn send_upload_post(
3030
3478
  height,
3031
3479
  has_audio,
3032
3480
  has_camera,
3481
+ locally_transcoded,
3033
3482
  )?;
3483
+ eprintln!(
3484
+ "[clips-tray] native upload post start recording={recording_id} mode={} index={index}/{total} final={is_final} bytes={body_len}",
3485
+ upload_mode.label()
3486
+ );
3034
3487
  let mut request = client
3035
3488
  .post(url)
3036
3489
  .header("Content-Type", mime_type)
@@ -3057,6 +3510,10 @@ async fn send_upload_post(
3057
3510
  body.chars().take(400).collect::<String>()
3058
3511
  ));
3059
3512
  }
3513
+ eprintln!(
3514
+ "[clips-tray] native upload post ok recording={recording_id} mode={} index={index}/{total} final={is_final}",
3515
+ upload_mode.label()
3516
+ );
3060
3517
  Ok(())
3061
3518
  }
3062
3519
 
@@ -3072,6 +3529,7 @@ fn upload_url(
3072
3529
  height: Option<u32>,
3073
3530
  has_audio: bool,
3074
3531
  has_camera: bool,
3532
+ locally_transcoded: bool,
3075
3533
  ) -> Result<String, String> {
3076
3534
  let base = server_url.trim_end_matches('/');
3077
3535
  let mut url = url::Url::parse(&format!("{base}/api/uploads/{recording_id}/chunk"))
@@ -3094,6 +3552,9 @@ fn upload_url(
3094
3552
  if let Some(height) = height {
3095
3553
  query.append_pair("height", &height.to_string());
3096
3554
  }
3555
+ if is_final && locally_transcoded {
3556
+ query.append_pair("locallyTranscoded", "1");
3557
+ }
3097
3558
  }
3098
3559
  Ok(url.to_string())
3099
3560
  }
@@ -3172,6 +3633,235 @@ pub(crate) fn mp4_has_moov(path: &Path) -> Option<bool> {
3172
3633
  }
3173
3634
  }
3174
3635
 
3636
+ /// Scan an MP4/QuickTime file for a `soun` (audio) handler nested under the
3637
+ /// top-level `moov` box (i.e. `moov > trak > mdia > hdlr`). Used to verify
3638
+ /// that ffmpeg/avconvert actually preserved an audio track rather than
3639
+ /// silently dropping it — `-map 0:a?` and similar optional maps exit 0 and
3640
+ /// produce a valid, smaller, video-only MP4 when the source audio stream
3641
+ /// can't be mapped, so a successful exit status alone can't be trusted when
3642
+ /// audio is expected. Returns `Some(true)`/`Some(false)` once `moov` has
3643
+ /// been located and scanned, or `None` when the file could not be read or
3644
+ /// `moov` could not be found/parsed (transient I/O error or unexpected
3645
+ /// structure — callers must not treat this as a definite "no audio").
3646
+ pub(crate) fn mp4_has_audio_track(path: &Path) -> Option<bool> {
3647
+ use std::io::{ErrorKind, Read, Seek, SeekFrom};
3648
+ let mut f = match std::fs::File::open(path) {
3649
+ Ok(f) => f,
3650
+ Err(e) => {
3651
+ eprintln!("[clips-tray] mp4_has_audio_track: could not open file for scan: {e}");
3652
+ return None;
3653
+ }
3654
+ };
3655
+
3656
+ // Walk the top-level boxes (mirrors `mp4_has_moov`) until `moov` is
3657
+ // found, then read its entire body into memory. `moov` holds only
3658
+ // metadata (no sample data, which lives in `mdat`), so even for large
3659
+ // recordings it is at most a few hundred KB — safe to buffer fully.
3660
+ let moov = loop {
3661
+ let mut buf = [0u8; 8];
3662
+ match f.read_exact(&mut buf) {
3663
+ Ok(()) => {}
3664
+ Err(e) if e.kind() == ErrorKind::UnexpectedEof => return Some(false),
3665
+ Err(_) => return None,
3666
+ }
3667
+ let box_size_raw = u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]);
3668
+ let box_type = &buf[4..8];
3669
+ let body_size: u64 = match box_size_raw {
3670
+ 0 => return Some(false), // box extends to EOF — can't be moov and something after it
3671
+ 1 => {
3672
+ let mut ext = [0u8; 8];
3673
+ match f.read_exact(&mut ext) {
3674
+ Ok(()) => {}
3675
+ Err(e) if e.kind() == ErrorKind::UnexpectedEof => return Some(false),
3676
+ Err(_) => return None,
3677
+ }
3678
+ u64::from_be_bytes(ext).saturating_sub(16)
3679
+ }
3680
+ n if n < 8 => return Some(false),
3681
+ n => (n as u64).saturating_sub(8),
3682
+ };
3683
+ if box_type == b"moov" {
3684
+ if body_size > 64 * 1024 * 1024 {
3685
+ // Implausibly large metadata box — bail rather than buffer
3686
+ // tens of MB; treat as unparseable rather than "no audio".
3687
+ eprintln!("[clips-tray] mp4_has_audio_track: moov box implausibly large ({body_size} bytes)");
3688
+ return None;
3689
+ }
3690
+ let mut body = vec![0u8; body_size as usize];
3691
+ if f.read_exact(&mut body).is_err() {
3692
+ return None;
3693
+ }
3694
+ break body;
3695
+ }
3696
+ let offset = match i64::try_from(body_size) {
3697
+ Ok(v) => v,
3698
+ Err(_) => return Some(false),
3699
+ };
3700
+ if f.seek(SeekFrom::Current(offset)).is_err() {
3701
+ return None;
3702
+ }
3703
+ };
3704
+
3705
+ // Linear scan for `hdlr` boxes within the buffered moov body. hdlr
3706
+ // layout: size(4) + type(4) + version(1) + flags(3) + pre_defined(4) +
3707
+ // handler_type(4) — so the 4-byte handler type sits 16 bytes after the
3708
+ // box header starts (8 bytes header + 8 bytes version/flags/pre_defined).
3709
+ let mut i = 0usize;
3710
+ while i + 8 <= moov.len() {
3711
+ let box_type = &moov[i + 4..i + 8];
3712
+ if box_type == b"hdlr" && i + 20 <= moov.len() {
3713
+ if &moov[i + 16..i + 20] == b"soun" {
3714
+ return Some(true);
3715
+ }
3716
+ }
3717
+ // Advance by one byte at a time rather than by parsed box size:
3718
+ // `hdlr`/`mdia`/`trak` box sizes aren't otherwise tracked here, and
3719
+ // scanning byte-by-byte for the 4-byte `hdlr` tag is simple, safe
3720
+ // (can't run past the buffer), and cheap given moov's small size.
3721
+ i += 1;
3722
+ }
3723
+ Some(false)
3724
+ }
3725
+
3726
+ #[derive(Clone, Copy, Debug)]
3727
+ struct AudioSignalProbe {
3728
+ mean_volume_db: Option<f64>,
3729
+ max_volume_db: Option<f64>,
3730
+ }
3731
+
3732
+ impl AudioSignalProbe {
3733
+ fn has_audible_signal(self) -> bool {
3734
+ let peak_ok = self
3735
+ .max_volume_db
3736
+ .map(|value| value.is_finite() && value >= AUDIO_SIGNAL_MIN_MAX_VOLUME_DB)
3737
+ .unwrap_or(false);
3738
+ let mean_ok = self
3739
+ .mean_volume_db
3740
+ .map(|value| value.is_finite() && value >= AUDIO_SIGNAL_MIN_MEAN_VOLUME_DB)
3741
+ .unwrap_or(true);
3742
+ peak_ok && mean_ok
3743
+ }
3744
+
3745
+ fn summary(self) -> String {
3746
+ fn fmt(value: Option<f64>) -> String {
3747
+ match value {
3748
+ Some(value) if value.is_finite() => format!("{value:.1} dB"),
3749
+ Some(value) if value.is_infinite() && value.is_sign_negative() => "-inf dB".into(),
3750
+ Some(_) => "non-finite".into(),
3751
+ None => "unknown".into(),
3752
+ }
3753
+ }
3754
+ format!(
3755
+ "mean_volume={} max_volume={} mean_floor={:.1} dB peak_floor={:.1} dB",
3756
+ fmt(self.mean_volume_db),
3757
+ fmt(self.max_volume_db),
3758
+ AUDIO_SIGNAL_MIN_MEAN_VOLUME_DB,
3759
+ AUDIO_SIGNAL_MIN_MAX_VOLUME_DB
3760
+ )
3761
+ }
3762
+ }
3763
+
3764
+ fn parse_ffmpeg_volume_db(stderr: &str, label: &str) -> Option<f64> {
3765
+ stderr.lines().rev().find_map(|line| {
3766
+ let (_, value) = line.split_once(label)?;
3767
+ let value = value.trim().strip_suffix(" dB").unwrap_or(value.trim());
3768
+ if value == "-inf" {
3769
+ Some(f64::NEG_INFINITY)
3770
+ } else {
3771
+ value.parse::<f64>().ok()
3772
+ }
3773
+ })
3774
+ }
3775
+
3776
+ fn audio_signal_probe_with_ffmpeg(
3777
+ ffmpeg_path: &str,
3778
+ source: &Path,
3779
+ ) -> Result<AudioSignalProbe, String> {
3780
+ let child = Command::new(ffmpeg_path)
3781
+ .arg("-hide_banner")
3782
+ .arg("-nostdin")
3783
+ .arg("-i")
3784
+ .arg(source)
3785
+ .arg("-map")
3786
+ .arg("0:a:0")
3787
+ .arg("-af")
3788
+ .arg("volumedetect")
3789
+ .arg("-f")
3790
+ .arg("null")
3791
+ .arg("-")
3792
+ .stdout(Stdio::null())
3793
+ .stderr(Stdio::piped())
3794
+ .spawn()
3795
+ .map_err(|e| format!("ffmpeg audio probe spawn failed: {e}"))?;
3796
+ let stderr = wait_for_child_collect_stderr(
3797
+ child,
3798
+ FFMPEG_AUDIO_PROBE_TIMEOUT,
3799
+ "ffmpeg",
3800
+ "checking recording audio",
3801
+ )?;
3802
+ Ok(AudioSignalProbe {
3803
+ mean_volume_db: parse_ffmpeg_volume_db(&stderr, "mean_volume:"),
3804
+ max_volume_db: parse_ffmpeg_volume_db(&stderr, "max_volume:"),
3805
+ })
3806
+ }
3807
+
3808
+ fn describe_audio_signal_probe(probe: Result<AudioSignalProbe, String>) -> String {
3809
+ match probe {
3810
+ Ok(probe) => probe.summary(),
3811
+ Err(err) => format!("probe failed: {err}"),
3812
+ }
3813
+ }
3814
+
3815
+ fn verify_prepared_audio_signal(
3816
+ ffmpeg_path: &str,
3817
+ candidate: &Path,
3818
+ original: &PreparedRecordingFile,
3819
+ source_path: &Path,
3820
+ label: &str,
3821
+ ) -> Result<Option<PreparedRecordingFile>, String> {
3822
+ let candidate_probe = match audio_signal_probe_with_ffmpeg(ffmpeg_path, candidate) {
3823
+ Ok(probe) => probe,
3824
+ Err(err) => {
3825
+ eprintln!(
3826
+ "[clips-tray] audio signal probe failed for {label}; accepting candidate: {err}"
3827
+ );
3828
+ return Ok(None);
3829
+ }
3830
+ };
3831
+ if candidate_probe.has_audible_signal() {
3832
+ eprintln!(
3833
+ "[clips-tray] audio signal probe ok for {label}: {}",
3834
+ candidate_probe.summary()
3835
+ );
3836
+ return Ok(None);
3837
+ }
3838
+
3839
+ let source_probe = audio_signal_probe_with_ffmpeg(ffmpeg_path, source_path);
3840
+ let source_has_signal = source_probe
3841
+ .as_ref()
3842
+ .ok()
3843
+ .copied()
3844
+ .map(AudioSignalProbe::has_audible_signal)
3845
+ .unwrap_or(false);
3846
+ let source_summary = describe_audio_signal_probe(source_probe);
3847
+ if source_has_signal {
3848
+ eprintln!(
3849
+ "[clips-tray] AUDIO QUIET AFTER PREPARE: {label} is effectively silent ({}) but original has signal ({source_summary}) — uploading original instead",
3850
+ candidate_probe.summary()
3851
+ );
3852
+ return Ok(Some(original.clone()));
3853
+ }
3854
+
3855
+ eprintln!(
3856
+ "[clips-tray] AUDIO CAPTURE TOO QUIET: prepared {label} has an audio track but no usable signal ({}) and original was not usable either ({source_summary})",
3857
+ candidate_probe.summary()
3858
+ );
3859
+ Err(format!(
3860
+ "Recorded file contains an audio track, but the captured audio is too quiet to publish ({})",
3861
+ candidate_probe.summary()
3862
+ ))
3863
+ }
3864
+
3175
3865
  fn prepare_recording_file(
3176
3866
  app: &AppHandle,
3177
3867
  path: &Path,
@@ -3180,7 +3870,7 @@ fn prepare_recording_file(
3180
3870
  height: Option<u32>,
3181
3871
  duration_ms: Option<u128>,
3182
3872
  has_audio: bool,
3183
- downmix_audio: bool,
3873
+ mic_captured_audio: bool,
3184
3874
  ) -> Result<PreparedRecordingFile, String> {
3185
3875
  let metadata = std::fs::metadata(path).map_err(|e| {
3186
3876
  let diag = describe_recording_path(path);
@@ -3216,10 +3906,18 @@ fn prepare_recording_file(
3216
3906
  mime_type: mime_type.to_string(),
3217
3907
  bytes: source_bytes,
3218
3908
  temporary: false,
3909
+ locally_transcoded: false,
3219
3910
  };
3220
3911
 
3221
3912
  let mut smallest_attempt_bytes: Option<u64> = None;
3222
3913
  let ffmpeg_path = resolve_ffmpeg_path();
3914
+ if has_audio {
3915
+ if let Some(ffmpeg_path) = ffmpeg_path.as_deref() {
3916
+ let raw_summary =
3917
+ describe_audio_signal_probe(audio_signal_probe_with_ffmpeg(ffmpeg_path, path));
3918
+ eprintln!("[clips-tray] raw recording audio signal before prepare: {raw_summary}");
3919
+ }
3920
+ }
3223
3921
 
3224
3922
  if !COMPRESSION_ENABLED || source_bytes < TRANSCODE_THRESHOLD_BYTES {
3225
3923
  if has_audio {
@@ -3230,15 +3928,42 @@ fn prepare_recording_file(
3230
3928
  ffmpeg_path,
3231
3929
  path,
3232
3930
  &normalized_path,
3233
- downmix_audio,
3931
+ mic_captured_audio,
3932
+ mic_captured_audio,
3234
3933
  ) {
3235
3934
  Ok(()) => {
3236
3935
  let normalized_bytes = std::fs::metadata(&normalized_path)
3237
3936
  .map_err(|e| format!("normalized recording file missing: {e}"))?
3238
3937
  .len();
3239
3938
  if normalized_bytes > 0 && normalized_bytes <= max_upload_bytes() {
3939
+ if mp4_has_audio_track(&normalized_path) == Some(false) {
3940
+ let _ = std::fs::remove_file(&normalized_path);
3941
+ eprintln!(
3942
+ "[clips-tray] AUDIO LOST: ffmpeg audio normalization dropped the audio track \
3943
+ (source had audio, normalized output did not) — uploading original instead of \
3944
+ a silent smaller file"
3945
+ );
3946
+ return Ok(original);
3947
+ }
3948
+ match verify_prepared_audio_signal(
3949
+ ffmpeg_path,
3950
+ &normalized_path,
3951
+ &original,
3952
+ path,
3953
+ "audio-normalized recording",
3954
+ ) {
3955
+ Ok(Some(fallback)) => {
3956
+ let _ = std::fs::remove_file(&normalized_path);
3957
+ return Ok(fallback);
3958
+ }
3959
+ Ok(None) => {}
3960
+ Err(err) => {
3961
+ let _ = std::fs::remove_file(&normalized_path);
3962
+ return Err(err);
3963
+ }
3964
+ }
3240
3965
  eprintln!(
3241
- "[clips-tray] native recording audio normalized with ffmpeg: {} -> {} bytes",
3966
+ "[clips-tray] native recording audio optimized with ffmpeg: {} -> {} bytes",
3242
3967
  source_bytes, normalized_bytes
3243
3968
  );
3244
3969
  return Ok(PreparedRecordingFile {
@@ -3246,6 +3971,7 @@ fn prepare_recording_file(
3246
3971
  mime_type: MP4_RECORDING_MIME_TYPE.to_string(),
3247
3972
  bytes: normalized_bytes,
3248
3973
  temporary: true,
3974
+ locally_transcoded: false,
3249
3975
  });
3250
3976
  }
3251
3977
  let _ = std::fs::remove_file(&normalized_path);
@@ -3268,7 +3994,7 @@ fn prepare_recording_file(
3268
3994
  return Ok(original);
3269
3995
  }
3270
3996
 
3271
- if let Some(ffmpeg_path) = ffmpeg_path {
3997
+ if let Some(ffmpeg_path) = ffmpeg_path.as_deref() {
3272
3998
  let presets = ffmpeg_transcode_presets(width, height, source_bytes, duration_ms);
3273
3999
  for (index, preset) in presets.iter().enumerate() {
3274
4000
  emit_native_upload_progress(
@@ -3281,7 +4007,7 @@ fn prepare_recording_file(
3281
4007
  let compressed_path = compressed_recording_path(path);
3282
4008
  let _ = std::fs::remove_file(&compressed_path);
3283
4009
  match transcode_with_ffmpeg(
3284
- &ffmpeg_path,
4010
+ ffmpeg_path,
3285
4011
  path,
3286
4012
  &compressed_path,
3287
4013
  preset,
@@ -3289,7 +4015,8 @@ fn prepare_recording_file(
3289
4015
  height,
3290
4016
  duration_ms,
3291
4017
  has_audio,
3292
- downmix_audio,
4018
+ mic_captured_audio,
4019
+ mic_captured_audio,
3293
4020
  ) {
3294
4021
  Ok(()) => {
3295
4022
  let compressed_bytes = std::fs::metadata(&compressed_path)
@@ -3303,6 +4030,43 @@ fn prepare_recording_file(
3303
4030
  );
3304
4031
  continue;
3305
4032
  }
4033
+ if has_audio && mp4_has_audio_track(&compressed_path) == Some(false) {
4034
+ let _ = std::fs::remove_file(&compressed_path);
4035
+ eprintln!(
4036
+ "[clips-tray] AUDIO LOST: ffmpeg {} dropped the audio track (source had \
4037
+ audio, compressed output did not) — rejecting this preset rather than \
4038
+ uploading a silent smaller file",
4039
+ preset.label
4040
+ );
4041
+ continue;
4042
+ }
4043
+ if has_audio {
4044
+ match verify_prepared_audio_signal(
4045
+ ffmpeg_path,
4046
+ &compressed_path,
4047
+ &original,
4048
+ path,
4049
+ preset.label,
4050
+ ) {
4051
+ Ok(Some(fallback)) => {
4052
+ let _ = std::fs::remove_file(&compressed_path);
4053
+ if fallback.bytes <= max_upload_bytes() {
4054
+ return Ok(fallback);
4055
+ }
4056
+ eprintln!(
4057
+ "[clips-tray] original recording has usable audio but is too large to upload without compression ({}); rejecting silent compressed preset {}",
4058
+ format_mb(fallback.bytes),
4059
+ preset.label
4060
+ );
4061
+ continue;
4062
+ }
4063
+ Ok(None) => {}
4064
+ Err(err) => {
4065
+ let _ = std::fs::remove_file(&compressed_path);
4066
+ return Err(err);
4067
+ }
4068
+ }
4069
+ }
3306
4070
  smallest_attempt_bytes = Some(
3307
4071
  smallest_attempt_bytes
3308
4072
  .map(|smallest| smallest.min(compressed_bytes))
@@ -3348,6 +4112,7 @@ fn prepare_recording_file(
3348
4112
  mime_type: MP4_RECORDING_MIME_TYPE.to_string(),
3349
4113
  bytes: compressed_bytes,
3350
4114
  temporary: true,
4115
+ locally_transcoded: true,
3351
4116
  });
3352
4117
  }
3353
4118
  Err(err) => {
@@ -3385,6 +4150,44 @@ fn prepare_recording_file(
3385
4150
  eprintln!("[clips-tray] avconvert produced an empty file with {preset}");
3386
4151
  continue;
3387
4152
  }
4153
+ if has_audio && mp4_has_audio_track(&compressed_path) == Some(false) {
4154
+ let _ = std::fs::remove_file(&compressed_path);
4155
+ eprintln!(
4156
+ "[clips-tray] AUDIO LOST: avconvert {preset} dropped the audio track \
4157
+ (source had audio, compressed output did not) — rejecting this preset \
4158
+ rather than uploading a silent smaller file"
4159
+ );
4160
+ continue;
4161
+ }
4162
+ if has_audio {
4163
+ if let Some(ffmpeg_probe_path) = ffmpeg_path.as_deref() {
4164
+ match verify_prepared_audio_signal(
4165
+ ffmpeg_probe_path,
4166
+ &compressed_path,
4167
+ &original,
4168
+ path,
4169
+ preset,
4170
+ ) {
4171
+ Ok(Some(fallback)) => {
4172
+ let _ = std::fs::remove_file(&compressed_path);
4173
+ if fallback.bytes <= max_upload_bytes() {
4174
+ return Ok(fallback);
4175
+ }
4176
+ eprintln!(
4177
+ "[clips-tray] original recording has usable audio but is too large to upload without compression ({}); rejecting silent avconvert preset {}",
4178
+ format_mb(fallback.bytes),
4179
+ preset
4180
+ );
4181
+ continue;
4182
+ }
4183
+ Ok(None) => {}
4184
+ Err(err) => {
4185
+ let _ = std::fs::remove_file(&compressed_path);
4186
+ return Err(err);
4187
+ }
4188
+ }
4189
+ }
4190
+ }
3388
4191
  smallest_attempt_bytes = Some(
3389
4192
  smallest_attempt_bytes
3390
4193
  .map(|smallest| smallest.min(compressed_bytes))
@@ -3430,6 +4233,7 @@ fn prepare_recording_file(
3430
4233
  mime_type: MP4_RECORDING_MIME_TYPE.to_string(),
3431
4234
  bytes: compressed_bytes,
3432
4235
  temporary: true,
4236
+ locally_transcoded: true,
3433
4237
  });
3434
4238
  }
3435
4239
  Err(err) => {
@@ -3486,7 +4290,7 @@ struct FfmpegTranscodePreset {
3486
4290
  }
3487
4291
 
3488
4292
  /// Maximum total bytes a recording upload may be. Overridable per-deployment
3489
- /// with the `CLIPS_MAX_UPLOAD_BYTES` env var; falls back to 256 MB.
4293
+ /// with the `CLIPS_MAX_UPLOAD_BYTES` env var; falls back to `DEFAULT_MAX_UPLOAD_BYTES` (2 GB).
3490
4294
  fn max_upload_bytes() -> u64 {
3491
4295
  std::env::var("CLIPS_MAX_UPLOAD_BYTES")
3492
4296
  .ok()
@@ -3609,6 +4413,7 @@ fn normalize_audio_with_ffmpeg(
3609
4413
  source: &Path,
3610
4414
  output: &Path,
3611
4415
  downmix_audio: bool,
4416
+ denoise_audio: bool,
3612
4417
  ) -> Result<(), String> {
3613
4418
  let audio_bitrate = format!("{NORMALIZED_AUDIO_BITRATE_KBPS}k");
3614
4419
  let mut command = Command::new(ffmpeg_path);
@@ -3630,7 +4435,7 @@ fn normalize_audio_with_ffmpeg(
3630
4435
  .arg("-b:a")
3631
4436
  .arg(audio_bitrate)
3632
4437
  .arg("-af")
3633
- .arg(audio_filter_chain(downmix_audio))
4438
+ .arg(audio_filter_chain(downmix_audio, denoise_audio))
3634
4439
  .arg("-ac")
3635
4440
  .arg("2")
3636
4441
  .arg("-ar")
@@ -3694,6 +4499,7 @@ fn transcode_with_ffmpeg(
3694
4499
  duration_ms: Option<u128>,
3695
4500
  normalize_audio: bool,
3696
4501
  downmix_audio: bool,
4502
+ denoise_audio: bool,
3697
4503
  ) -> Result<(), String> {
3698
4504
  let mut command = Command::new(ffmpeg_path);
3699
4505
  command
@@ -3717,7 +4523,9 @@ fn transcode_with_ffmpeg(
3717
4523
  }
3718
4524
 
3719
4525
  if normalize_audio {
3720
- command.arg("-af").arg(audio_filter_chain(downmix_audio));
4526
+ command
4527
+ .arg("-af")
4528
+ .arg(audio_filter_chain(downmix_audio, denoise_audio));
3721
4529
  }
3722
4530
 
3723
4531
  let duration_rate_limit =
@@ -3790,19 +4598,25 @@ fn transcode_with_avconvert(source: &Path, output: &Path, preset: &str) -> Resul
3790
4598
  }
3791
4599
 
3792
4600
  fn wait_for_transcode_child(
3793
- mut child: Child,
4601
+ child: Child,
3794
4602
  timeout: Duration,
3795
4603
  tool_name: &str,
3796
4604
  ) -> Result<(), String> {
4605
+ wait_for_child_collect_stderr(child, timeout, tool_name, "compressing recording").map(|_| ())
4606
+ }
4607
+
4608
+ fn wait_for_child_collect_stderr(
4609
+ mut child: Child,
4610
+ timeout: Duration,
4611
+ tool_name: &str,
4612
+ action: &str,
4613
+ ) -> Result<String, String> {
3797
4614
  let deadline = Instant::now() + timeout;
3798
4615
  loop {
3799
4616
  if let Some(status) = child
3800
4617
  .try_wait()
3801
4618
  .map_err(|e| format!("{tool_name} wait failed: {e}"))?
3802
4619
  {
3803
- if status.success() {
3804
- return Ok(());
3805
- }
3806
4620
  let mut stderr = String::new();
3807
4621
  if let Some(mut pipe) = child.stderr.take() {
3808
4622
  let _ = pipe.read_to_string(&mut stderr);
@@ -3816,12 +4630,16 @@ fn wait_for_transcode_child(
3816
4630
  .rev()
3817
4631
  .collect::<Vec<_>>()
3818
4632
  .join("\n");
3819
- return Err(format!("{tool_name} exited with {status}: {}", tail.trim()));
4633
+ return if status.success() {
4634
+ Ok(stderr)
4635
+ } else {
4636
+ Err(format!("{tool_name} exited with {status}: {}", tail.trim()))
4637
+ };
3820
4638
  }
3821
4639
  if Instant::now() >= deadline {
3822
4640
  let _ = child.kill();
3823
4641
  let _ = child.wait();
3824
- return Err(format!("{tool_name} timed out while compressing recording"));
4642
+ return Err(format!("{tool_name} timed out while {action}"));
3825
4643
  }
3826
4644
  std::thread::sleep(Duration::from_millis(250));
3827
4645
  }
@@ -3834,6 +4652,26 @@ fn wait_for_transcode_child(
3834
4652
  /// re-encoding, so concat is roughly disk-IO bound. Called from
3835
4653
  /// `consolidate_segments_into_path` after every segment has been
3836
4654
  /// finalized by `stop_native_recording(_, wait_for_finalize=true)`.
4655
+ fn validate_recording_segment_file(path: &Path) -> Result<(), String> {
4656
+ match std::fs::metadata(path) {
4657
+ Ok(meta) if meta.len() > 0 => {}
4658
+ Ok(_) => return Err(format!("recording segment is empty: {}", path.display())),
4659
+ Err(err) => {
4660
+ return Err(format!(
4661
+ "recording segment is missing or unreadable: {} ({err})",
4662
+ path.display()
4663
+ ));
4664
+ }
4665
+ }
4666
+ if mp4_has_moov(path) == Some(false) {
4667
+ return Err(format!(
4668
+ "recording segment is missing playback metadata: {}",
4669
+ path.display()
4670
+ ));
4671
+ }
4672
+ Ok(())
4673
+ }
4674
+
3837
4675
  #[cfg(target_os = "macos")]
3838
4676
  fn concat_mp4_segments(segments: &[PathBuf], output: &Path) -> Result<(), String> {
3839
4677
  use std::ffi::CString;
@@ -3995,18 +4833,7 @@ fn concat_mp4_segments(segments: &[PathBuf], output: &Path) -> Result<(), String
3995
4833
  let mut appended_any = false;
3996
4834
 
3997
4835
  for path in segments {
3998
- // Skip segments that vanished or are empty (e.g. a pause
3999
- // that fired before the segment captured a single sample).
4000
- match std::fs::metadata(path) {
4001
- Ok(meta) if meta.len() > 0 => {}
4002
- _ => {
4003
- eprintln!(
4004
- "[clips-tray] concat: skipping empty/missing segment {}",
4005
- path.display()
4006
- );
4007
- continue;
4008
- }
4009
- }
4836
+ validate_recording_segment_file(path)?;
4010
4837
  let url = file_url(path)
4011
4838
  .ok_or_else(|| format!("could not build NSURL for {}", path.display()))?;
4012
4839
  let asset: *mut AnyObject = msg_send![asset_cls, URLAssetWithURL: &*url, options: std::ptr::null::<AnyObject>()];
@@ -4018,11 +4845,10 @@ fn concat_mp4_segments(segments: &[PathBuf], output: &Path) -> Result<(), String
4018
4845
  }
4019
4846
  let duration: CMTime = msg_send![asset, duration];
4020
4847
  if duration.flags & 1 == 0 || duration.timescale == 0 || duration.value <= 0 {
4021
- eprintln!(
4022
- "[clips-tray] concat: skipping segment with invalid duration: {}",
4848
+ return Err(format!(
4849
+ "recording segment has invalid duration: {}",
4023
4850
  path.display()
4024
- );
4025
- continue;
4851
+ ));
4026
4852
  }
4027
4853
  let range = CMTimeRange {
4028
4854
  start: CM_TIME_ZERO,
@@ -4127,3 +4953,335 @@ fn concat_mp4_segments(segments: &[PathBuf], output: &Path) -> Result<(), String
4127
4953
 
4128
4954
  Ok(())
4129
4955
  }
4956
+
4957
+ #[cfg(test)]
4958
+ mod audio_track_probe_tests {
4959
+ use super::{
4960
+ audio_filter_chain, mp4_has_audio_track, parse_ffmpeg_volume_db, AudioSignalProbe,
4961
+ AUDIO_DENOISE_FILTER, AUDIO_DOWNMIX_FILTER, AUDIO_LOUDNESS_FILTER,
4962
+ };
4963
+ use std::io::Write;
4964
+
4965
+ /// Append an ISO BMFF box: 4-byte big-endian size (header + body) then
4966
+ /// the 4-byte type tag, then the raw body bytes.
4967
+ fn push_box(buf: &mut Vec<u8>, box_type: &[u8; 4], body: &[u8]) {
4968
+ let size = (8 + body.len()) as u32;
4969
+ buf.extend_from_slice(&size.to_be_bytes());
4970
+ buf.extend_from_slice(box_type);
4971
+ buf.extend_from_slice(body);
4972
+ }
4973
+
4974
+ /// Build a minimal `hdlr` box body for the given handler type (e.g.
4975
+ /// `soun` or `vide`): version(1) + flags(3) + pre_defined(4) +
4976
+ /// handler_type(4), zero-padded further like a real hdlr's trailing
4977
+ /// name/reserved fields.
4978
+ fn hdlr_body(handler_type: &[u8; 4]) -> Vec<u8> {
4979
+ let mut body = vec![0u8; 8]; // version+flags+pre_defined
4980
+ body.extend_from_slice(handler_type);
4981
+ body.extend_from_slice(&[0u8; 4]); // trailing reserved/name padding
4982
+ body
4983
+ }
4984
+
4985
+ fn write_temp_mp4(bytes: &[u8]) -> std::path::PathBuf {
4986
+ let mut path = std::env::temp_dir();
4987
+ path.push(format!(
4988
+ "clips-audio-probe-test-{}-{}.mp4",
4989
+ std::process::id(),
4990
+ std::time::SystemTime::now()
4991
+ .duration_since(std::time::UNIX_EPOCH)
4992
+ .unwrap()
4993
+ .as_nanos()
4994
+ ));
4995
+ let mut f = std::fs::File::create(&path).unwrap();
4996
+ f.write_all(bytes).unwrap();
4997
+ path
4998
+ }
4999
+
5000
+ #[test]
5001
+ fn builds_native_audio_filter_chain_for_mic_noise_reduction() {
5002
+ assert_eq!(audio_filter_chain(false, false), AUDIO_LOUDNESS_FILTER);
5003
+ assert_eq!(
5004
+ audio_filter_chain(false, true),
5005
+ format!("{AUDIO_DENOISE_FILTER},{AUDIO_LOUDNESS_FILTER}")
5006
+ );
5007
+ assert_eq!(
5008
+ audio_filter_chain(true, true),
5009
+ format!("{AUDIO_DOWNMIX_FILTER},{AUDIO_DENOISE_FILTER},{AUDIO_LOUDNESS_FILTER}")
5010
+ );
5011
+ }
5012
+
5013
+ #[test]
5014
+ fn detects_audio_track_present() {
5015
+ let mut moov_body = Vec::new();
5016
+ // moov > trak > mdia > hdlr(soun)
5017
+ let mut mdia_body = Vec::new();
5018
+ push_box(&mut mdia_body, b"hdlr", &hdlr_body(b"soun"));
5019
+ let mut trak_body = Vec::new();
5020
+ push_box(&mut trak_body, b"mdia", &mdia_body);
5021
+ push_box(&mut moov_body, b"trak", &trak_body);
5022
+
5023
+ let mut file = Vec::new();
5024
+ push_box(&mut file, b"ftyp", b"isommp42");
5025
+ push_box(&mut file, b"moov", &moov_body);
5026
+ file.extend_from_slice(b"mdatSOMEFAKEVIDEODATA");
5027
+
5028
+ let path = write_temp_mp4(&file);
5029
+ assert_eq!(mp4_has_audio_track(&path), Some(true));
5030
+ let _ = std::fs::remove_file(&path);
5031
+ }
5032
+
5033
+ #[test]
5034
+ fn detects_video_only_output_as_missing_audio() {
5035
+ // Simulates exactly the bug: ffmpeg with `-map 0:a?` succeeds but
5036
+ // only writes a video (`vide`) handler track, no `soun` track.
5037
+ let mut moov_body = Vec::new();
5038
+ let mut mdia_body = Vec::new();
5039
+ push_box(&mut mdia_body, b"hdlr", &hdlr_body(b"vide"));
5040
+ let mut trak_body = Vec::new();
5041
+ push_box(&mut trak_body, b"mdia", &mdia_body);
5042
+ push_box(&mut moov_body, b"trak", &trak_body);
5043
+
5044
+ let mut file = Vec::new();
5045
+ push_box(&mut file, b"ftyp", b"isommp42");
5046
+ push_box(&mut file, b"moov", &moov_body);
5047
+ file.extend_from_slice(b"mdatSOMEFAKEVIDEODATA");
5048
+
5049
+ let path = write_temp_mp4(&file);
5050
+ assert_eq!(mp4_has_audio_track(&path), Some(false));
5051
+ let _ = std::fs::remove_file(&path);
5052
+ }
5053
+
5054
+ #[test]
5055
+ fn detects_audio_track_among_multiple_tracks() {
5056
+ // video trak first, then audio trak — order shouldn't matter.
5057
+ let mut video_mdia = Vec::new();
5058
+ push_box(&mut video_mdia, b"hdlr", &hdlr_body(b"vide"));
5059
+ let mut video_trak = Vec::new();
5060
+ push_box(&mut video_trak, b"mdia", &video_mdia);
5061
+
5062
+ let mut audio_mdia = Vec::new();
5063
+ push_box(&mut audio_mdia, b"hdlr", &hdlr_body(b"soun"));
5064
+ let mut audio_trak = Vec::new();
5065
+ push_box(&mut audio_trak, b"mdia", &audio_mdia);
5066
+
5067
+ let mut moov_body = Vec::new();
5068
+ push_box(&mut moov_body, b"trak", &video_trak);
5069
+ push_box(&mut moov_body, b"trak", &audio_trak);
5070
+
5071
+ let mut file = Vec::new();
5072
+ push_box(&mut file, b"ftyp", b"isommp42");
5073
+ push_box(&mut file, b"moov", &moov_body);
5074
+ file.extend_from_slice(b"mdatSOMEFAKEVIDEODATA");
5075
+
5076
+ let path = write_temp_mp4(&file);
5077
+ assert_eq!(mp4_has_audio_track(&path), Some(true));
5078
+ let _ = std::fs::remove_file(&path);
5079
+ }
5080
+
5081
+ #[test]
5082
+ fn missing_file_returns_none() {
5083
+ let path = std::env::temp_dir().join("clips-audio-probe-test-does-not-exist.mp4");
5084
+ assert_eq!(mp4_has_audio_track(&path), None);
5085
+ }
5086
+
5087
+ #[test]
5088
+ fn no_moov_box_returns_false() {
5089
+ let mut file = Vec::new();
5090
+ push_box(&mut file, b"ftyp", b"isommp42");
5091
+ file.extend_from_slice(b"mdatSOMEFAKEVIDEODATA");
5092
+
5093
+ let path = write_temp_mp4(&file);
5094
+ assert_eq!(mp4_has_audio_track(&path), Some(false));
5095
+ let _ = std::fs::remove_file(&path);
5096
+ }
5097
+
5098
+ #[test]
5099
+ fn parses_ffmpeg_volumedetect_output() {
5100
+ let stderr = "\
5101
+ [Parsed_volumedetect_0 @ 0x123] mean_volume: -74.0 dB
5102
+ [Parsed_volumedetect_0 @ 0x123] max_volume: -57.6 dB
5103
+ ";
5104
+ assert_eq!(parse_ffmpeg_volume_db(stderr, "mean_volume:"), Some(-74.0));
5105
+ assert_eq!(parse_ffmpeg_volume_db(stderr, "max_volume:"), Some(-57.6));
5106
+ }
5107
+
5108
+ #[test]
5109
+ fn rejects_effectively_silent_prepared_audio() {
5110
+ let silent = AudioSignalProbe {
5111
+ mean_volume_db: Some(-74.0),
5112
+ max_volume_db: Some(-57.6),
5113
+ };
5114
+ let quiet_noise_peak = AudioSignalProbe {
5115
+ mean_volume_db: Some(-71.1),
5116
+ max_volume_db: Some(-47.4),
5117
+ };
5118
+ let audible = AudioSignalProbe {
5119
+ mean_volume_db: Some(-21.5),
5120
+ max_volume_db: Some(-1.4),
5121
+ };
5122
+ assert!(!silent.has_audible_signal());
5123
+ assert!(!quiet_noise_peak.has_audible_signal());
5124
+ assert!(audible.has_audible_signal());
5125
+ }
5126
+ }
5127
+
5128
+ #[cfg(test)]
5129
+ mod segment_recovery_tests {
5130
+ use super::{
5131
+ recover_from_unusable_current_segment, validate_recording_segment_file,
5132
+ NativeFullscreenSession, RestartInfo, MP4_RECORDING_MIME_TYPE,
5133
+ };
5134
+ use std::io::Write;
5135
+ use std::path::PathBuf;
5136
+ use std::time::{Duration, Instant};
5137
+
5138
+ fn push_box(buf: &mut Vec<u8>, box_type: &[u8; 4], body: &[u8]) {
5139
+ let size = (8 + body.len()) as u32;
5140
+ buf.extend_from_slice(&size.to_be_bytes());
5141
+ buf.extend_from_slice(box_type);
5142
+ buf.extend_from_slice(body);
5143
+ }
5144
+
5145
+ fn temp_path(name: &str) -> PathBuf {
5146
+ let mut path = std::env::temp_dir();
5147
+ path.push(format!(
5148
+ "clips-segment-recovery-{name}-{}-{}.mp4",
5149
+ std::process::id(),
5150
+ std::time::SystemTime::now()
5151
+ .duration_since(std::time::UNIX_EPOCH)
5152
+ .unwrap()
5153
+ .as_nanos()
5154
+ ));
5155
+ path
5156
+ }
5157
+
5158
+ fn write_mp4(path: &PathBuf, has_moov: bool) {
5159
+ let mut file = Vec::new();
5160
+ push_box(&mut file, b"ftyp", b"isommp42");
5161
+ if has_moov {
5162
+ push_box(&mut file, b"moov", &[]);
5163
+ }
5164
+ file.extend_from_slice(b"mdatSOMEFAKEVIDEODATA");
5165
+ let mut f = std::fs::File::create(path).unwrap();
5166
+ f.write_all(&file).unwrap();
5167
+ }
5168
+
5169
+ fn test_session(segments: Vec<PathBuf>) -> NativeFullscreenSession {
5170
+ let now = Instant::now();
5171
+ NativeFullscreenSession {
5172
+ backend: None,
5173
+ path: segments
5174
+ .first()
5175
+ .cloned()
5176
+ .unwrap_or_else(|| temp_path("final")),
5177
+ mime_type: MP4_RECORDING_MIME_TYPE,
5178
+ started_at: now,
5179
+ width: Some(1280),
5180
+ height: Some(720),
5181
+ segments,
5182
+ paused_total: Duration::ZERO,
5183
+ current_segment_started_at: now.checked_sub(Duration::from_millis(250)).unwrap_or(now),
5184
+ lost_segment_duration: Duration::ZERO,
5185
+ lost_segment_count: 0,
5186
+ paused_at: None,
5187
+ restart: RestartInfo {
5188
+ safe_id: "test".to_string(),
5189
+ include_audio: true,
5190
+ capture_system_audio: false,
5191
+ mic_captured_in_file: false,
5192
+ mic_device_id: None,
5193
+ mic_device_label: None,
5194
+ segment_counter: 0,
5195
+ target_display_id: None,
5196
+ capture_region: None,
5197
+ },
5198
+ pending_recording_output: false,
5199
+ disk_monitor_stop: None,
5200
+ }
5201
+ }
5202
+
5203
+ #[test]
5204
+ fn drops_unusable_last_segment_when_empty_recovery_allowed() {
5205
+ let good = temp_path("good");
5206
+ let bad = temp_path("bad");
5207
+ write_mp4(&good, true);
5208
+ write_mp4(&bad, false);
5209
+
5210
+ let mut session = test_session(vec![good.clone(), bad.clone()]);
5211
+ assert!(recover_from_unusable_current_segment(
5212
+ &mut session,
5213
+ "test pause",
5214
+ true,
5215
+ ));
5216
+ assert_eq!(session.segments, vec![good.clone()]);
5217
+ assert_eq!(session.lost_segment_count, 1);
5218
+ assert!(session.lost_segment_duration > Duration::ZERO);
5219
+ assert!(!bad.exists());
5220
+
5221
+ let _ = std::fs::remove_file(good);
5222
+ }
5223
+
5224
+ #[test]
5225
+ fn keeps_only_bad_segment_when_empty_recovery_disallowed() {
5226
+ let bad = temp_path("only-bad");
5227
+ write_mp4(&bad, false);
5228
+
5229
+ let mut session = test_session(vec![bad.clone()]);
5230
+ assert!(!recover_from_unusable_current_segment(
5231
+ &mut session,
5232
+ "final stop",
5233
+ false,
5234
+ ));
5235
+ assert_eq!(session.segments, vec![bad.clone()]);
5236
+ assert_eq!(session.lost_segment_count, 0);
5237
+ assert!(bad.exists());
5238
+
5239
+ let _ = std::fs::remove_file(bad);
5240
+ }
5241
+
5242
+ #[test]
5243
+ fn keeps_playable_last_segment() {
5244
+ let first = temp_path("first");
5245
+ let second = temp_path("second");
5246
+ write_mp4(&first, true);
5247
+ write_mp4(&second, true);
5248
+
5249
+ let mut session = test_session(vec![first.clone(), second.clone()]);
5250
+ assert!(!recover_from_unusable_current_segment(
5251
+ &mut session,
5252
+ "test pause",
5253
+ true,
5254
+ ));
5255
+ assert_eq!(session.segments, vec![first.clone(), second.clone()]);
5256
+ assert_eq!(session.lost_segment_count, 0);
5257
+
5258
+ let _ = std::fs::remove_file(first);
5259
+ let _ = std::fs::remove_file(second);
5260
+ }
5261
+
5262
+ #[test]
5263
+ fn concat_validation_rejects_bad_middle_segment() {
5264
+ let first = temp_path("middle-first");
5265
+ let bad = temp_path("middle-bad");
5266
+ let last = temp_path("middle-last");
5267
+ write_mp4(&first, true);
5268
+ write_mp4(&bad, false);
5269
+ write_mp4(&last, true);
5270
+
5271
+ let mut error = None;
5272
+ for path in [&first, &bad, &last] {
5273
+ if let Err(err) = validate_recording_segment_file(path) {
5274
+ error = Some(err);
5275
+ break;
5276
+ }
5277
+ }
5278
+
5279
+ let err = error.expect("bad middle segment should fail concat validation");
5280
+ assert!(err.contains("missing playback metadata"));
5281
+ assert!(err.contains(bad.to_string_lossy().as_ref()));
5282
+
5283
+ let _ = std::fs::remove_file(first);
5284
+ let _ = std::fs::remove_file(bad);
5285
+ let _ = std::fs::remove_file(last);
5286
+ }
5287
+ }