@agent-native/core 0.134.1 → 0.135.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (442) hide show
  1. package/corpus/README.md +3 -3
  2. package/corpus/core/CHANGELOG.md +107 -0
  3. package/corpus/core/package.json +3 -2
  4. package/corpus/core/src/automations/service.ts +41 -4
  5. package/corpus/core/src/cli/mcp-config-writers.ts +27 -21
  6. package/corpus/core/src/client/agent-page/AgentJobsTab.tsx +187 -85
  7. package/corpus/core/src/client/agent-page/AutomationDetailsDialog.tsx +208 -0
  8. package/corpus/core/src/client/agent-page/AutomationScheduleDialog.tsx +180 -0
  9. package/corpus/core/src/client/agent-page/TimezoneSelect.tsx +241 -0
  10. package/corpus/core/src/client/agent-page/use-jobs.ts +50 -10
  11. package/corpus/core/src/client/org/OrgSwitcher.tsx +26 -0
  12. package/corpus/core/src/client/settings/AccountSettingsCard.tsx +35 -31
  13. package/corpus/core/src/client/settings/SchedulingTimezoneField.tsx +71 -0
  14. package/corpus/core/src/client/settings/SettingsRow.tsx +113 -0
  15. package/corpus/core/src/client/settings/agent-settings-search.ts +2 -1
  16. package/corpus/core/src/client/settings/index.ts +6 -0
  17. package/corpus/core/src/integrations/identity.ts +2 -12
  18. package/corpus/core/src/jobs/actions/list-automation-runs.ts +33 -0
  19. package/corpus/core/src/jobs/actions/list-recurring-jobs.ts +27 -6
  20. package/corpus/core/src/jobs/actions/manage-recurring-job.ts +41 -9
  21. package/corpus/core/src/jobs/background-automation-runner.ts +88 -0
  22. package/corpus/core/src/jobs/cron.ts +52 -6
  23. package/corpus/core/src/jobs/frontmatter.ts +20 -0
  24. package/corpus/core/src/jobs/run-history.ts +242 -0
  25. package/corpus/core/src/jobs/scheduler.ts +73 -18
  26. package/corpus/core/src/jobs/tools.ts +54 -6
  27. package/corpus/core/src/localization/actions/get-localization-preference.ts +2 -2
  28. package/corpus/core/src/localization/actions/set-localization-preference.ts +49 -11
  29. package/corpus/core/src/localization/default-messages.ts +27 -0
  30. package/corpus/core/src/localization/shared.ts +39 -6
  31. package/corpus/core/src/localization/user-timezone.ts +36 -0
  32. package/corpus/core/src/org/app-roles-handlers.ts +1 -9
  33. package/corpus/core/src/org/index.ts +2 -0
  34. package/corpus/core/src/org/membership.ts +18 -0
  35. package/corpus/core/src/review/actions/create-review-comment.ts +4 -1
  36. package/corpus/core/src/review/actions/reply-review-comment.ts +4 -1
  37. package/corpus/core/src/review/index.ts +5 -0
  38. package/corpus/core/src/review/notifications.ts +163 -0
  39. package/corpus/core/src/review/types.ts +7 -0
  40. package/corpus/core/src/server/action-discovery.ts +4 -0
  41. package/corpus/core/src/server/activity-notifications.ts +174 -0
  42. package/corpus/core/src/server/app-name.ts +31 -0
  43. package/corpus/core/src/server/better-auth-instance.ts +4 -4
  44. package/corpus/core/src/server/email-templates.ts +70 -14
  45. package/corpus/core/src/server/email.ts +66 -3
  46. package/corpus/core/src/server/index.ts +10 -0
  47. package/corpus/core/src/sharing/index.ts +5 -0
  48. package/corpus/core/src/sharing/recipients.ts +67 -0
  49. package/corpus/core/src/triggers/actions/list-automations.ts +27 -8
  50. package/corpus/core/src/triggers/actions/manage-automation.ts +17 -6
  51. package/corpus/core/src/triggers/actions.ts +16 -4
  52. package/corpus/core/src/triggers/dispatcher.ts +44 -30
  53. package/corpus/core/src/triggers/routes.ts +19 -6
  54. package/corpus/core/src/vitest-config.ts +68 -0
  55. package/corpus/core/vitest.config.ts +5 -0
  56. package/corpus/templates/analytics/app/pages/Settings.tsx +37 -53
  57. package/corpus/templates/analytics/changelog/2026-08-01-new-error-alerts-now-reach-you-by-email-and-a-scheduled-dash.md +6 -0
  58. package/corpus/templates/analytics/server/jobs/dashboard-report.ts +43 -0
  59. package/corpus/templates/analytics/server/lib/error-capture.ts +5 -1
  60. package/corpus/templates/analytics/vitest.config.ts +16 -12
  61. package/corpus/templates/assets/actions/refresh-generation-run.ts +3 -0
  62. package/corpus/templates/assets/app/components/ui/switch.tsx +1 -0
  63. package/corpus/templates/assets/app/hooks/use-assets-prefs.ts +55 -0
  64. package/corpus/templates/assets/app/i18n-data.ts +30 -0
  65. package/corpus/templates/assets/app/routes/settings.tsx +58 -55
  66. package/corpus/templates/assets/changelog/2026-08-01-get-an-email-when-a-generation-you-started-finishes-or-fails.md +6 -0
  67. package/corpus/templates/assets/server/lib/generation-run-notifications.ts +89 -0
  68. package/corpus/templates/assets/server/lib/video-runs.ts +8 -1
  69. package/corpus/templates/assets/server/routes/_agent-native/assets/user-prefs.get.ts +17 -0
  70. package/corpus/templates/assets/server/routes/_agent-native/assets/user-prefs.put.ts +36 -0
  71. package/corpus/templates/assets/shared/assets-user-prefs.ts +10 -0
  72. package/corpus/templates/assets/vitest.config.ts +16 -12
  73. package/corpus/templates/brain/app/routes/settings.tsx +15 -15
  74. package/corpus/templates/brain/vitest.config.ts +16 -12
  75. package/corpus/templates/calendar/app/pages/Settings.tsx +32 -40
  76. package/corpus/templates/calendar/changelog/2026-08-01-language-and-appearance-now-sit-together-in-one-preferences-.md +6 -0
  77. package/corpus/templates/calendar/vitest.config.ts +16 -12
  78. package/corpus/templates/chat/app/routes/settings.tsx +14 -22
  79. package/corpus/templates/chat/vitest.config.ts +9 -0
  80. package/corpus/templates/clips/.agents/skills/video-editing/SKILL.md +31 -0
  81. package/corpus/templates/clips/AGENTS.md +1 -0
  82. package/corpus/templates/clips/actions/add-comment.ts +12 -1
  83. package/corpus/templates/clips/actions/create-meeting.ts +2 -2
  84. package/corpus/templates/clips/actions/create-recording.ts +2 -2
  85. package/corpus/templates/clips/actions/finalize-recording.ts +42 -6
  86. package/corpus/templates/clips/actions/generate-filmstrip.ts +154 -0
  87. package/corpus/templates/clips/actions/get-recording-player-data.ts +6 -0
  88. package/corpus/templates/clips/actions/import-loom-recording.ts +2 -2
  89. package/corpus/templates/clips/actions/lib/ensure-recording-filmstrip.ts +258 -0
  90. package/corpus/templates/clips/actions/lib/ensure-seekable-video.ts +11 -3
  91. package/corpus/templates/clips/actions/react-to-comment.ts +13 -0
  92. package/corpus/templates/clips/actions/react-to-recording.ts +10 -1
  93. package/corpus/templates/clips/actions/reply-to-comment.ts +12 -1
  94. package/corpus/templates/clips/actions/stitch-recordings.ts +2 -2
  95. package/corpus/templates/clips/app/components/editor/editor-layout.tsx +152 -23
  96. package/corpus/templates/clips/app/components/editor/waveform.tsx +237 -75
  97. package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +23 -1
  98. package/corpus/templates/clips/app/components/settings/ai-setup-section.tsx +441 -0
  99. package/corpus/templates/clips/app/components/settings/slack-section.tsx +329 -0
  100. package/corpus/templates/clips/app/components/settings/types.ts +14 -0
  101. package/corpus/templates/clips/app/components/settings/video-storage-section.tsx +443 -0
  102. package/corpus/templates/clips/app/hooks/use-secret-status.ts +69 -0
  103. package/corpus/templates/clips/app/i18n/en-US.ts +9 -1
  104. package/corpus/templates/clips/app/lib/video-filmstrip.ts +262 -0
  105. package/corpus/templates/clips/app/routes/_app.settings._index.tsx +165 -1351
  106. package/corpus/templates/clips/changelog/2026-07-27-saved-clips-now-resume-from-the-last-uploaded-byte-after-an-.md +6 -0
  107. package/corpus/templates/clips/changelog/2026-07-31-editor-timeline-now-shows-video-frames-behind-the-waveform-g.md +6 -0
  108. package/corpus/templates/clips/changelog/2026-07-31-email-notifications-now-go-out-when-someone-comments-on-or-r.md +6 -0
  109. package/corpus/templates/clips/changelog/2026-07-31-you-can-now-set-a-default-visibility-for-new-recordings-in-s.md +6 -0
  110. package/corpus/templates/clips/changelog/2026-08-01-email-notifications-for-comments-and-reactions-now-actually-.md +6 -0
  111. package/corpus/templates/clips/changelog/2026-08-03-the-desktop-app-now-opens-directly-beneath-its-tray-icon-on-.md +6 -0
  112. package/corpus/templates/clips/chrome-extension/package.json +1 -0
  113. package/corpus/templates/clips/chrome-extension/vitest.config.ts +9 -0
  114. package/corpus/templates/clips/desktop/package.json +1 -0
  115. package/corpus/templates/clips/desktop/src/app.tsx +17 -1
  116. package/corpus/templates/clips/desktop/src/lib/recorder.ts +331 -36
  117. package/corpus/templates/clips/desktop/src/lib/upload-recovery.ts +129 -0
  118. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +815 -15
  119. package/corpus/templates/clips/desktop/src-tauri/src/tray.rs +61 -11
  120. package/corpus/templates/clips/desktop/vitest.config.ts +9 -0
  121. package/corpus/templates/clips/server/db/schema.ts +17 -0
  122. package/corpus/templates/clips/server/jobs/media-verification.ts +4 -0
  123. package/corpus/templates/clips/server/lib/activity-notifications.ts +185 -0
  124. package/corpus/templates/clips/server/lib/recording-media-cleanup.ts +2 -0
  125. package/corpus/templates/clips/server/lib/recording-upload-state.ts +46 -9
  126. package/corpus/templates/clips/server/lib/recordings.ts +24 -0
  127. package/corpus/templates/clips/server/lib/resumable-session.ts +10 -4
  128. package/corpus/templates/clips/server/lib/transactional-email-templates.ts +109 -0
  129. package/corpus/templates/clips/server/lib/upload-interruption.ts +8 -0
  130. package/corpus/templates/clips/server/lib/upload-lease.ts +50 -4
  131. package/corpus/templates/clips/server/lib/video-filmstrip-sprite.ts +207 -0
  132. package/corpus/templates/clips/server/lib/video-remux.ts +13 -4
  133. package/corpus/templates/clips/server/plugins/db.ts +16 -0
  134. package/corpus/templates/clips/server/routes/api/_agent-native-background/post-finalize-worker.post.ts +4 -0
  135. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/abort.post.ts +123 -36
  136. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +159 -29
  137. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/interrupt.post.ts +197 -0
  138. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/reset-chunks.post.ts +193 -39
  139. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/resume.get.ts +191 -12
  140. package/corpus/templates/clips/shared/clips-ai-prefs.ts +6 -0
  141. package/corpus/templates/clips/shared/feature-flags.ts +12 -0
  142. package/corpus/templates/clips/shared/recording-core.ts +4 -0
  143. package/corpus/templates/clips/vitest.config.ts +23 -19
  144. package/corpus/templates/content/actions/add-comment.ts +15 -1
  145. package/corpus/templates/content/actions/list-documents.ts +1 -0
  146. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +5 -2
  147. package/corpus/templates/content/app/components/editor/DocumentToolbar.tsx +50 -1
  148. package/corpus/templates/content/app/components/ui/switch.tsx +1 -0
  149. package/corpus/templates/content/app/hooks/use-content-prefs.ts +55 -0
  150. package/corpus/templates/content/app/i18n-data.ts +40 -0
  151. package/corpus/templates/content/app/routes/_app.settings.tsx +47 -18
  152. package/corpus/templates/content/changelog/2026-07-30-breadcrumb-menus-now-hide-internal-system-pages.md +6 -0
  153. package/corpus/templates/content/changelog/2026-08-01-get-an-email-when-someone-comments-on-replies-in-or-mentions.md +6 -0
  154. package/corpus/templates/content/server/lib/comment-notifications.ts +146 -0
  155. package/corpus/templates/content/server/routes/_agent-native/content/user-prefs.get.ts +17 -0
  156. package/corpus/templates/content/server/routes/_agent-native/content/user-prefs.put.ts +36 -0
  157. package/corpus/templates/content/shared/content-user-prefs.ts +11 -0
  158. package/corpus/templates/content/vitest.config.ts +20 -16
  159. package/corpus/templates/crm/app/routes/settings.tsx +14 -22
  160. package/corpus/templates/crm/vitest.config.ts +8 -4
  161. package/corpus/templates/design/app/routes/settings.tsx +14 -22
  162. package/corpus/templates/design/changelog/2026-08-01-review-comments-replies-and-mentions-now-send-an-email-to-th.md +6 -0
  163. package/corpus/templates/design/vitest.config.ts +18 -12
  164. package/corpus/templates/dispatch/app/routes/settings.tsx +26 -40
  165. package/corpus/templates/dispatch/vitest.config.ts +11 -7
  166. package/corpus/templates/factory/app/routes/settings.tsx +14 -22
  167. package/corpus/templates/factory/vitest.config.ts +9 -0
  168. package/corpus/templates/forms/app/routes/_app.settings.tsx +14 -22
  169. package/corpus/templates/forms/vitest.config.ts +27 -23
  170. package/corpus/templates/macros/app/routes/settings.tsx +14 -22
  171. package/corpus/templates/macros/vitest.config.ts +16 -12
  172. package/corpus/templates/mail/app/pages/SettingsPage.tsx +14 -16
  173. package/corpus/templates/mail/vitest.config.ts +16 -12
  174. package/corpus/templates/plan/app/routes/settings.tsx +30 -44
  175. package/corpus/templates/plan/vitest.config.ts +24 -20
  176. package/corpus/templates/slides/actions/add-slide-comment.ts +12 -1
  177. package/corpus/templates/slides/app/components/ui/switch.tsx +1 -0
  178. package/corpus/templates/slides/app/hooks/use-slides-prefs.ts +55 -0
  179. package/corpus/templates/slides/app/i18n/en-US.ts +4 -0
  180. package/corpus/templates/slides/app/routes/settings.tsx +45 -22
  181. package/corpus/templates/slides/changelog/2026-08-01-get-an-email-when-someone-comments-on-or-replies-in-your-dec.md +6 -0
  182. package/corpus/templates/slides/server/lib/comment-notifications.ts +182 -0
  183. package/corpus/templates/slides/server/routes/_agent-native/slides/user-prefs.get.ts +16 -0
  184. package/corpus/templates/slides/server/routes/_agent-native/slides/user-prefs.put.ts +36 -0
  185. package/corpus/templates/slides/shared/slides-user-prefs.ts +11 -0
  186. package/corpus/templates/slides/vitest.config.ts +17 -13
  187. package/corpus/templates/tasks/app/components/layout/Sidebar.tsx +2 -0
  188. package/corpus/templates/tasks/app/routes/settings.tsx +14 -22
  189. package/corpus/templates/tasks/vitest.config.ts +17 -13
  190. package/corpus/toolkit/CHANGELOG.md +9 -0
  191. package/corpus/toolkit/package.json +1 -1
  192. package/corpus/toolkit/vitest.config.ts +3 -0
  193. package/dist/automations/service.d.ts +2 -0
  194. package/dist/automations/service.d.ts.map +1 -1
  195. package/dist/automations/service.js +29 -4
  196. package/dist/automations/service.js.map +1 -1
  197. package/dist/cli/mcp-config-writers.d.ts.map +1 -1
  198. package/dist/cli/mcp-config-writers.js +24 -23
  199. package/dist/cli/mcp-config-writers.js.map +1 -1
  200. package/dist/client/agent-page/AgentJobsTab.d.ts.map +1 -1
  201. package/dist/client/agent-page/AgentJobsTab.js +86 -29
  202. package/dist/client/agent-page/AgentJobsTab.js.map +1 -1
  203. package/dist/client/agent-page/AutomationDetailsDialog.d.ts +21 -0
  204. package/dist/client/agent-page/AutomationDetailsDialog.d.ts.map +1 -0
  205. package/dist/client/agent-page/AutomationDetailsDialog.js +33 -0
  206. package/dist/client/agent-page/AutomationDetailsDialog.js.map +1 -0
  207. package/dist/client/agent-page/AutomationScheduleDialog.d.ts +15 -0
  208. package/dist/client/agent-page/AutomationScheduleDialog.d.ts.map +1 -0
  209. package/dist/client/agent-page/AutomationScheduleDialog.js +46 -0
  210. package/dist/client/agent-page/AutomationScheduleDialog.js.map +1 -0
  211. package/dist/client/agent-page/TimezoneSelect.d.ts +17 -0
  212. package/dist/client/agent-page/TimezoneSelect.d.ts.map +1 -0
  213. package/dist/client/agent-page/TimezoneSelect.js +107 -0
  214. package/dist/client/agent-page/TimezoneSelect.js.map +1 -0
  215. package/dist/client/agent-page/use-jobs.d.ts +20 -7
  216. package/dist/client/agent-page/use-jobs.d.ts.map +1 -1
  217. package/dist/client/agent-page/use-jobs.js +24 -4
  218. package/dist/client/agent-page/use-jobs.js.map +1 -1
  219. package/dist/client/org/OrgSwitcher.d.ts +8 -1
  220. package/dist/client/org/OrgSwitcher.d.ts.map +1 -1
  221. package/dist/client/org/OrgSwitcher.js +8 -2
  222. package/dist/client/org/OrgSwitcher.js.map +1 -1
  223. package/dist/client/settings/AccountSettingsCard.d.ts.map +1 -1
  224. package/dist/client/settings/AccountSettingsCard.js +10 -9
  225. package/dist/client/settings/AccountSettingsCard.js.map +1 -1
  226. package/dist/client/settings/SchedulingTimezoneField.d.ts +10 -0
  227. package/dist/client/settings/SchedulingTimezoneField.d.ts.map +1 -0
  228. package/dist/client/settings/SchedulingTimezoneField.js +30 -0
  229. package/dist/client/settings/SchedulingTimezoneField.js.map +1 -0
  230. package/dist/client/settings/SettingsRow.d.ts +36 -0
  231. package/dist/client/settings/SettingsRow.d.ts.map +1 -0
  232. package/dist/client/settings/SettingsRow.js +15 -0
  233. package/dist/client/settings/SettingsRow.js.map +1 -0
  234. package/dist/client/settings/agent-settings-search.d.ts.map +1 -1
  235. package/dist/client/settings/agent-settings-search.js +1 -1
  236. package/dist/client/settings/agent-settings-search.js.map +1 -1
  237. package/dist/client/settings/index.d.ts +1 -0
  238. package/dist/client/settings/index.d.ts.map +1 -1
  239. package/dist/client/settings/index.js +1 -0
  240. package/dist/client/settings/index.js.map +1 -1
  241. package/dist/collab/struct-routes.d.ts +1 -1
  242. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  243. package/dist/integrations/identity.d.ts.map +1 -1
  244. package/dist/integrations/identity.js +2 -11
  245. package/dist/integrations/identity.js.map +1 -1
  246. package/dist/jobs/actions/list-automation-runs.d.ts +8 -0
  247. package/dist/jobs/actions/list-automation-runs.d.ts.map +1 -0
  248. package/dist/jobs/actions/list-automation-runs.js +29 -0
  249. package/dist/jobs/actions/list-automation-runs.js.map +1 -0
  250. package/dist/jobs/actions/list-recurring-jobs.d.ts +2 -0
  251. package/dist/jobs/actions/list-recurring-jobs.d.ts.map +1 -1
  252. package/dist/jobs/actions/list-recurring-jobs.js +20 -7
  253. package/dist/jobs/actions/list-recurring-jobs.js.map +1 -1
  254. package/dist/jobs/actions/manage-recurring-job.d.ts +2 -0
  255. package/dist/jobs/actions/manage-recurring-job.js +30 -9
  256. package/dist/jobs/actions/manage-recurring-job.js.map +1 -1
  257. package/dist/jobs/background-automation-runner.d.ts.map +1 -1
  258. package/dist/jobs/background-automation-runner.js +60 -0
  259. package/dist/jobs/background-automation-runner.js.map +1 -1
  260. package/dist/jobs/cron.d.ts +16 -2
  261. package/dist/jobs/cron.d.ts.map +1 -1
  262. package/dist/jobs/cron.js +43 -6
  263. package/dist/jobs/cron.js.map +1 -1
  264. package/dist/jobs/frontmatter.d.ts +12 -0
  265. package/dist/jobs/frontmatter.d.ts.map +1 -1
  266. package/dist/jobs/frontmatter.js +8 -0
  267. package/dist/jobs/frontmatter.js.map +1 -1
  268. package/dist/jobs/run-history.d.ts +56 -0
  269. package/dist/jobs/run-history.d.ts.map +1 -0
  270. package/dist/jobs/run-history.js +169 -0
  271. package/dist/jobs/run-history.js.map +1 -0
  272. package/dist/jobs/scheduler.d.ts.map +1 -1
  273. package/dist/jobs/scheduler.js +51 -18
  274. package/dist/jobs/scheduler.js.map +1 -1
  275. package/dist/jobs/tools.d.ts.map +1 -1
  276. package/dist/jobs/tools.js +37 -6
  277. package/dist/jobs/tools.js.map +1 -1
  278. package/dist/localization/actions/get-localization-preference.d.ts +1 -2
  279. package/dist/localization/actions/get-localization-preference.d.ts.map +1 -1
  280. package/dist/localization/actions/get-localization-preference.js.map +1 -1
  281. package/dist/localization/actions/set-localization-preference.d.ts +3 -3
  282. package/dist/localization/actions/set-localization-preference.d.ts.map +1 -1
  283. package/dist/localization/actions/set-localization-preference.js +33 -8
  284. package/dist/localization/actions/set-localization-preference.js.map +1 -1
  285. package/dist/localization/default-messages.d.ts +25 -0
  286. package/dist/localization/default-messages.d.ts.map +1 -1
  287. package/dist/localization/default-messages.js +25 -0
  288. package/dist/localization/default-messages.js.map +1 -1
  289. package/dist/localization/shared.d.ts +16 -1
  290. package/dist/localization/shared.d.ts.map +1 -1
  291. package/dist/localization/shared.js +22 -4
  292. package/dist/localization/shared.js.map +1 -1
  293. package/dist/localization/user-timezone.d.ts +11 -0
  294. package/dist/localization/user-timezone.d.ts.map +1 -0
  295. package/dist/localization/user-timezone.js +31 -0
  296. package/dist/localization/user-timezone.js.map +1 -0
  297. package/dist/notifications/routes.d.ts +1 -1
  298. package/dist/observability/routes.d.ts +1 -1
  299. package/dist/org/app-roles-handlers.d.ts.map +1 -1
  300. package/dist/org/app-roles-handlers.js +1 -8
  301. package/dist/org/app-roles-handlers.js.map +1 -1
  302. package/dist/org/index.d.ts +1 -0
  303. package/dist/org/index.d.ts.map +1 -1
  304. package/dist/org/index.js +1 -0
  305. package/dist/org/index.js.map +1 -1
  306. package/dist/org/membership.d.ts +6 -0
  307. package/dist/org/membership.d.ts.map +1 -0
  308. package/dist/org/membership.js +16 -0
  309. package/dist/org/membership.js.map +1 -0
  310. package/dist/provider-api/actions/custom-provider-registration.d.ts +10 -10
  311. package/dist/provider-api/actions/provider-api.d.ts +8 -8
  312. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  313. package/dist/resources/handlers.d.ts +1 -1
  314. package/dist/review/actions/create-review-comment.d.ts +32 -1
  315. package/dist/review/actions/create-review-comment.d.ts.map +1 -1
  316. package/dist/review/actions/create-review-comment.js +3 -1
  317. package/dist/review/actions/create-review-comment.js.map +1 -1
  318. package/dist/review/actions/reply-review-comment.d.ts +32 -1
  319. package/dist/review/actions/reply-review-comment.d.ts.map +1 -1
  320. package/dist/review/actions/reply-review-comment.js +3 -1
  321. package/dist/review/actions/reply-review-comment.js.map +1 -1
  322. package/dist/review/index.d.ts +1 -0
  323. package/dist/review/index.d.ts.map +1 -1
  324. package/dist/review/index.js +1 -0
  325. package/dist/review/index.js.map +1 -1
  326. package/dist/review/notifications.d.ts +24 -0
  327. package/dist/review/notifications.d.ts.map +1 -0
  328. package/dist/review/notifications.js +123 -0
  329. package/dist/review/notifications.js.map +1 -0
  330. package/dist/review/types.d.ts +5 -0
  331. package/dist/review/types.d.ts.map +1 -1
  332. package/dist/review/types.js.map +1 -1
  333. package/dist/server/action-discovery.d.ts.map +1 -1
  334. package/dist/server/action-discovery.js +4 -0
  335. package/dist/server/action-discovery.js.map +1 -1
  336. package/dist/server/activity-notifications.d.ts +61 -0
  337. package/dist/server/activity-notifications.d.ts.map +1 -0
  338. package/dist/server/activity-notifications.js +103 -0
  339. package/dist/server/activity-notifications.js.map +1 -0
  340. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  341. package/dist/server/app-name.d.ts +18 -0
  342. package/dist/server/app-name.d.ts.map +1 -1
  343. package/dist/server/app-name.js +31 -0
  344. package/dist/server/app-name.js.map +1 -1
  345. package/dist/server/better-auth-instance.js +4 -4
  346. package/dist/server/better-auth-instance.js.map +1 -1
  347. package/dist/server/email-templates.d.ts +12 -0
  348. package/dist/server/email-templates.d.ts.map +1 -1
  349. package/dist/server/email-templates.js +57 -14
  350. package/dist/server/email-templates.js.map +1 -1
  351. package/dist/server/email.d.ts +12 -0
  352. package/dist/server/email.d.ts.map +1 -1
  353. package/dist/server/email.js +52 -5
  354. package/dist/server/email.js.map +1 -1
  355. package/dist/server/index.d.ts +1 -0
  356. package/dist/server/index.d.ts.map +1 -1
  357. package/dist/server/index.js +1 -0
  358. package/dist/server/index.js.map +1 -1
  359. package/dist/sharing/index.d.ts +1 -0
  360. package/dist/sharing/index.d.ts.map +1 -1
  361. package/dist/sharing/index.js +1 -0
  362. package/dist/sharing/index.js.map +1 -1
  363. package/dist/sharing/recipients.d.ts +31 -0
  364. package/dist/sharing/recipients.d.ts.map +1 -0
  365. package/dist/sharing/recipients.js +40 -0
  366. package/dist/sharing/recipients.js.map +1 -0
  367. package/dist/templates/chat/app/routes/settings.tsx +14 -22
  368. package/dist/templates/chat/vitest.config.ts +9 -0
  369. package/dist/triggers/actions/list-automations.d.ts +2 -0
  370. package/dist/triggers/actions/list-automations.d.ts.map +1 -1
  371. package/dist/triggers/actions/list-automations.js +19 -8
  372. package/dist/triggers/actions/list-automations.js.map +1 -1
  373. package/dist/triggers/actions/manage-automation.d.ts +6 -0
  374. package/dist/triggers/actions/manage-automation.js +12 -6
  375. package/dist/triggers/actions/manage-automation.js.map +1 -1
  376. package/dist/triggers/actions.d.ts.map +1 -1
  377. package/dist/triggers/actions.js +15 -4
  378. package/dist/triggers/actions.js.map +1 -1
  379. package/dist/triggers/dispatcher.d.ts.map +1 -1
  380. package/dist/triggers/dispatcher.js +19 -17
  381. package/dist/triggers/dispatcher.js.map +1 -1
  382. package/dist/triggers/routes.d.ts.map +1 -1
  383. package/dist/triggers/routes.js +6 -6
  384. package/dist/triggers/routes.js.map +1 -1
  385. package/dist/vitest-config.d.ts +10 -0
  386. package/dist/vitest-config.d.ts.map +1 -0
  387. package/dist/vitest-config.js +55 -0
  388. package/dist/vitest-config.js.map +1 -0
  389. package/package.json +5 -4
  390. package/src/automations/service.ts +41 -4
  391. package/src/cli/mcp-config-writers.ts +27 -21
  392. package/src/client/agent-page/AgentJobsTab.tsx +187 -85
  393. package/src/client/agent-page/AutomationDetailsDialog.tsx +208 -0
  394. package/src/client/agent-page/AutomationScheduleDialog.tsx +180 -0
  395. package/src/client/agent-page/TimezoneSelect.tsx +241 -0
  396. package/src/client/agent-page/use-jobs.ts +50 -10
  397. package/src/client/org/OrgSwitcher.tsx +26 -0
  398. package/src/client/settings/AccountSettingsCard.tsx +35 -31
  399. package/src/client/settings/SchedulingTimezoneField.tsx +71 -0
  400. package/src/client/settings/SettingsRow.tsx +113 -0
  401. package/src/client/settings/agent-settings-search.ts +2 -1
  402. package/src/client/settings/index.ts +6 -0
  403. package/src/integrations/identity.ts +2 -12
  404. package/src/jobs/actions/list-automation-runs.ts +33 -0
  405. package/src/jobs/actions/list-recurring-jobs.ts +27 -6
  406. package/src/jobs/actions/manage-recurring-job.ts +41 -9
  407. package/src/jobs/background-automation-runner.ts +88 -0
  408. package/src/jobs/cron.ts +52 -6
  409. package/src/jobs/frontmatter.ts +20 -0
  410. package/src/jobs/run-history.ts +242 -0
  411. package/src/jobs/scheduler.ts +73 -18
  412. package/src/jobs/tools.ts +54 -6
  413. package/src/localization/actions/get-localization-preference.ts +2 -2
  414. package/src/localization/actions/set-localization-preference.ts +49 -11
  415. package/src/localization/default-messages.ts +27 -0
  416. package/src/localization/shared.ts +39 -6
  417. package/src/localization/user-timezone.ts +36 -0
  418. package/src/org/app-roles-handlers.ts +1 -9
  419. package/src/org/index.ts +2 -0
  420. package/src/org/membership.ts +18 -0
  421. package/src/review/actions/create-review-comment.ts +4 -1
  422. package/src/review/actions/reply-review-comment.ts +4 -1
  423. package/src/review/index.ts +5 -0
  424. package/src/review/notifications.ts +163 -0
  425. package/src/review/types.ts +7 -0
  426. package/src/server/action-discovery.ts +4 -0
  427. package/src/server/activity-notifications.ts +174 -0
  428. package/src/server/app-name.ts +31 -0
  429. package/src/server/better-auth-instance.ts +4 -4
  430. package/src/server/email-templates.ts +70 -14
  431. package/src/server/email.ts +66 -3
  432. package/src/server/index.ts +10 -0
  433. package/src/sharing/index.ts +5 -0
  434. package/src/sharing/recipients.ts +67 -0
  435. package/src/templates/chat/app/routes/settings.tsx +14 -22
  436. package/src/templates/chat/vitest.config.ts +9 -0
  437. package/src/triggers/actions/list-automations.ts +27 -8
  438. package/src/triggers/actions/manage-automation.ts +17 -6
  439. package/src/triggers/actions.ts +16 -4
  440. package/src/triggers/dispatcher.ts +44 -30
  441. package/src/triggers/routes.ts +19 -6
  442. package/src/vitest-config.ts +68 -0
@@ -1,67 +1,26 @@
1
- import {
2
- agentNativePath,
3
- appApiPath,
4
- } from "@agent-native/core/client/api-path";
1
+ import { agentNativePath } from "@agent-native/core/client/api-path";
5
2
  import { ChangelogSettingsCard } from "@agent-native/core/client/changelog";
6
- import { useActionQuery } from "@agent-native/core/client/hooks";
7
3
  import { LanguagePicker, useT } from "@agent-native/core/client/i18n";
8
4
  import { useOrg, useSwitchOrg } from "@agent-native/core/client/org";
9
5
  import {
10
6
  AccountSettingsCard,
11
- useBuilderConnectFlow,
12
- useBuilderStatus,
7
+ SettingsGroup,
8
+ SettingsRow,
13
9
  SettingsTabsPage,
14
10
  useAgentSettingsTabs,
11
+ useBuilderConnectFlow,
12
+ useBuilderStatus,
15
13
  type SettingsSearchEntry,
16
14
  } from "@agent-native/core/client/settings";
17
- import {
18
- BUILDER_CREDITS_UPGRADE_URL,
19
- type BuilderCreditsStatus,
20
- } from "@shared/builder-credits";
21
- import {
22
- IconBrain,
23
- IconBolt,
24
- IconBrandSlack,
25
- IconCheck,
26
- IconChevronDown,
27
- IconCloud,
28
- IconExternalLink,
29
- IconKey,
30
- IconLoader2,
31
- IconServer,
32
- IconTrash,
33
- IconUsersGroup,
34
- } from "@tabler/icons-react";
35
- import { useCallback, useEffect, useMemo, useState } from "react";
15
+ import type { ClipsDefaultVisibility } from "@shared/clips-ai-prefs";
16
+ import { useEffect, useMemo, useState } from "react";
36
17
  import { toast } from "sonner";
37
18
 
38
19
  import { PageHeader } from "@/components/library/page-header";
39
- import {
40
- AlertDialog,
41
- AlertDialogAction,
42
- AlertDialogCancel,
43
- AlertDialogContent,
44
- AlertDialogDescription,
45
- AlertDialogFooter,
46
- AlertDialogHeader,
47
- AlertDialogTitle,
48
- } from "@/components/ui/alert-dialog";
49
- import { Badge } from "@/components/ui/badge";
20
+ import { AiSetupSection } from "@/components/settings/ai-setup-section";
21
+ import { SlackSection } from "@/components/settings/slack-section";
22
+ import { VideoStorageSection } from "@/components/settings/video-storage-section";
50
23
  import { Button } from "@/components/ui/button";
51
- import {
52
- Card,
53
- CardContent,
54
- CardDescription,
55
- CardHeader,
56
- CardTitle,
57
- } from "@/components/ui/card";
58
- import {
59
- Collapsible,
60
- CollapsibleContent,
61
- CollapsibleTrigger,
62
- } from "@/components/ui/collapsible";
63
- import { Input } from "@/components/ui/input";
64
- import { Label } from "@/components/ui/label";
65
24
  import {
66
25
  Select,
67
26
  SelectContent,
@@ -71,9 +30,9 @@ import {
71
30
  } from "@/components/ui/select";
72
31
  import { Switch } from "@/components/ui/switch";
73
32
  import { OrganizationIdentityCard } from "@/components/workspace/organization-identity-card";
33
+ import { useSecretStatus } from "@/hooks/use-secret-status";
74
34
  import { useVideoStorageStatus } from "@/hooks/use-video-storage-status";
75
35
  import enMessages from "@/i18n/en-US";
76
- import { cn } from "@/lib/utils";
77
36
 
78
37
  export function meta() {
79
38
  return [{ title: enMessages.settings.pageTitle }];
@@ -81,112 +40,12 @@ export function meta() {
81
40
 
82
41
  const SPEEDS = ["1", "1.2", "1.5", "1.75", "2"];
83
42
 
84
- const BUILDER_CREDITS_FEATURE_LABELS = [
85
- "builderCredits.featureBackupTranscription",
86
- "builderCredits.featureCleanup",
87
- "builderCredits.featureSummaries",
88
- "builderCredits.featureTitles",
89
- ] as const;
90
-
91
- const S3_STORAGE_FIELDS = [
92
- {
93
- key: "S3_ENDPOINT",
94
- labelKey: "settings.s3EndpointLabel",
95
- placeholder: "https://s3.us-east-1.amazonaws.com",
96
- required: true,
97
- },
98
- {
99
- key: "S3_BUCKET",
100
- labelKey: "settings.s3BucketLabel",
101
- placeholder: "my-clips-bucket",
102
- required: true,
103
- },
104
- {
105
- key: "S3_ACCESS_KEY_ID",
106
- labelKey: "settings.s3AccessKeyLabel",
107
- placeholder: "AKIA...",
108
- required: true,
109
- },
110
- {
111
- key: "S3_SECRET_ACCESS_KEY",
112
- labelKey: "settings.s3SecretAccessKeyLabel",
113
- placeholder: "••••••••",
114
- required: true,
115
- secret: true,
116
- },
117
- {
118
- key: "S3_REGION",
119
- labelKey: "settings.s3RegionLabel",
120
- placeholder: "us-east-1",
121
- },
122
- {
123
- key: "S3_PUBLIC_BASE_URL",
124
- labelKey: "settings.s3PublicBaseUrlLabel",
125
- placeholder: "https://cdn.example.com",
126
- },
127
- ] as const;
128
-
129
- const AI_PROVIDER_FIELDS = [
130
- {
131
- key: "ANTHROPIC_API_KEY",
132
- label: "Anthropic",
133
- placeholder: "sk-ant-...",
134
- storage: "agent-engine",
135
- engine: "anthropic",
136
- },
137
- {
138
- key: "OPENAI_API_KEY",
139
- label: "OpenAI",
140
- placeholder: "sk-...",
141
- storage: "agent-engine",
142
- engine: "ai-sdk:openai",
143
- },
144
- {
145
- key: "GEMINI_API_KEY",
146
- label: "Gemini",
147
- placeholder: "AI...",
148
- storage: "secret",
149
- },
150
- {
151
- key: "GROQ_API_KEY",
152
- label: "Groq",
153
- placeholder: "gsk_...",
154
- storage: "secret",
155
- engine: "ai-sdk:groq",
156
- },
157
- {
158
- key: "OPENROUTER_API_KEY",
159
- label: "OpenRouter",
160
- placeholder: "sk-or-...",
161
- storage: "agent-engine",
162
- engine: "ai-sdk:openrouter",
163
- },
164
- ] as const;
165
-
166
43
  interface ClipsUserSettings {
167
44
  defaultPlaybackSpeed?: string;
168
45
  emailNotifications?: boolean;
169
46
  transcriptCleanupEnabled?: boolean;
170
47
  includeFullVideoInAi?: boolean;
171
- }
172
-
173
- interface SlackInstallation {
174
- id: string;
175
- teamId: string;
176
- teamName: string | null;
177
- enterpriseName: string | null;
178
- apiAppId: string | null;
179
- ownerEmail: string;
180
- orgId: string | null;
181
- status: string;
182
- updatedAt: string;
183
- }
184
-
185
- interface SlackInstallationsResponse {
186
- oauthConfigured: boolean;
187
- signingConfigured: boolean;
188
- scopes: string[];
189
- installations: SlackInstallation[];
48
+ defaultRecordingVisibility?: ClipsDefaultVisibility;
190
49
  }
191
50
 
192
51
  async function loadSettings(): Promise<ClipsUserSettings> {
@@ -215,182 +74,6 @@ async function saveSettings(value: ClipsUserSettings): Promise<void> {
215
74
  }
216
75
  }
217
76
 
218
- function absoluteAppUrl(url: string): string {
219
- const withBase = url.startsWith("/api/") ? appApiPath(url) : url;
220
- return new URL(withBase, window.location.origin).toString();
221
- }
222
-
223
- async function waitForPopupClose(popup: Window): Promise<void> {
224
- await new Promise<void>((resolve) => {
225
- let settled = false;
226
- const finish = () => {
227
- if (settled) return;
228
- settled = true;
229
- window.clearInterval(interval);
230
- window.clearTimeout(timeout);
231
- window.removeEventListener("focus", onFocus);
232
- resolve();
233
- };
234
- const interval = window.setInterval(() => {
235
- if (popup.closed) finish();
236
- }, 500);
237
- const onFocus = () => {
238
- if (popup.closed) finish();
239
- };
240
- window.addEventListener("focus", onFocus);
241
- const timeout = window.setTimeout(finish, 5 * 60 * 1000);
242
- });
243
- }
244
-
245
- async function startSlackOAuth(): Promise<void> {
246
- const res = await fetch(
247
- agentNativePath("/_agent-native/actions/connect-slack?returnUrl=/settings"),
248
- );
249
- const text = await res.text();
250
- let data: {
251
- url?: string;
252
- error?: string;
253
- result?: { url?: string };
254
- } = {};
255
- try {
256
- data = JSON.parse(text);
257
- } catch {
258
- // Keep the fallback below.
259
- }
260
- if (!res.ok) throw new Error(data.error || `Failed (${res.status})`);
261
- const url = data.result?.url ?? data.url;
262
- if (!url) throw new Error("No Slack OAuth URL returned");
263
- const popup = window.open(
264
- absoluteAppUrl(url),
265
- "clips-slack-oauth",
266
- "width=600,height=760",
267
- );
268
- if (!popup) {
269
- throw new Error(
270
- "Popup blocked — please allow popups for this site and try again.",
271
- );
272
- }
273
- await waitForPopupClose(popup);
274
- }
275
-
276
- async function requestDisconnectSlack(id: string): Promise<void> {
277
- const res = await fetch(
278
- agentNativePath("/_agent-native/actions/disconnect-slack"),
279
- {
280
- method: "POST",
281
- headers: { "Content-Type": "application/json" },
282
- body: JSON.stringify({ id }),
283
- },
284
- );
285
- if (!res.ok) {
286
- const body = (await res.json().catch(() => null)) as {
287
- error?: string;
288
- } | null;
289
- throw new Error(body?.error ?? `Disconnect failed (${res.status})`);
290
- }
291
- }
292
-
293
- async function saveS3StorageSettings(
294
- values: Record<string, string>,
295
- ): Promise<void> {
296
- const vars = S3_STORAGE_FIELDS.map((field) => ({
297
- key: field.key,
298
- value: (values[field.key] ?? "").trim(),
299
- })).filter((entry) => entry.value.length > 0);
300
-
301
- if (vars.length === 0) {
302
- throw new Error("Enter at least one storage value.");
303
- }
304
-
305
- for (const { key, value } of vars) {
306
- const res = await fetch(agentNativePath("/_agent-native/secrets/adhoc"), {
307
- method: "POST",
308
- headers: { "Content-Type": "application/json" },
309
- body: JSON.stringify({
310
- name: key,
311
- value,
312
- scope: "workspace",
313
- description: "Clips S3-compatible storage", // i18n-ignore -- secret metadata description, not visible UI
314
- }),
315
- });
316
-
317
- if (!res.ok) {
318
- const body = (await res.json().catch(() => null)) as {
319
- error?: string;
320
- } | null;
321
- throw new Error(body?.error ?? `Save failed (${res.status})`);
322
- }
323
- }
324
- }
325
-
326
- async function saveAgentEngineApiKey(
327
- key: string,
328
- value: string,
329
- ): Promise<void> {
330
- const res = await fetch(
331
- agentNativePath("/_agent-native/agent-engine/api-key"),
332
- {
333
- method: "POST",
334
- headers: { "Content-Type": "application/json" },
335
- body: JSON.stringify({ key, value, scope: "user" }),
336
- },
337
- );
338
-
339
- if (!res.ok) {
340
- const body = (await res.json().catch(() => null)) as {
341
- error?: string;
342
- } | null;
343
- throw new Error(body?.error ?? `Save failed (${res.status})`);
344
- }
345
- }
346
-
347
- async function applyAgentEngine(engine: string): Promise<void> {
348
- const res = await fetch(
349
- agentNativePath("/_agent-native/actions/manage-agent-engine"),
350
- {
351
- method: "POST",
352
- headers: { "Content-Type": "application/json" },
353
- body: JSON.stringify({ action: "set", engine }),
354
- },
355
- );
356
-
357
- const body = (await res.json().catch(() => null)) as {
358
- error?: string;
359
- result?: unknown;
360
- } | null;
361
- if (!res.ok) {
362
- throw new Error(body?.error ?? `Engine switch failed (${res.status})`);
363
- }
364
- const result = body?.result ?? body;
365
- const text =
366
- typeof result === "string"
367
- ? result.trim()
368
- : result && typeof result === "object"
369
- ? JSON.stringify(result)
370
- : "";
371
- if (/^(Error|Warning):/i.test(text)) {
372
- throw new Error(text);
373
- }
374
- }
375
-
376
- async function saveRegisteredSecret(key: string, value: string): Promise<void> {
377
- const res = await fetch(
378
- agentNativePath(`/_agent-native/secrets/${encodeURIComponent(key)}`),
379
- {
380
- method: "POST",
381
- headers: { "Content-Type": "application/json" },
382
- body: JSON.stringify({ value }),
383
- },
384
- );
385
-
386
- if (!res.ok) {
387
- const body = (await res.json().catch(() => null)) as {
388
- error?: string;
389
- } | null;
390
- throw new Error(body?.error ?? `Save failed (${res.status})`);
391
- }
392
- }
393
-
394
77
  export default function SettingsIndexRoute() {
395
78
  const t = useT();
396
79
  const agentSettingsTabs = useAgentSettingsTabs();
@@ -416,6 +99,7 @@ export default function SettingsIndexRoute() {
416
99
  const { data: org } = useOrg();
417
100
  const switchOrg = useSwitchOrg();
418
101
  const storageStatus = useVideoStorageStatus();
102
+ const secrets = useSecretStatus();
419
103
  const builderStatus = useBuilderStatus();
420
104
  const builderConnect = useBuilderConnectFlow({
421
105
  popupUrl:
@@ -427,83 +111,14 @@ export default function SettingsIndexRoute() {
427
111
  toast.success(t("settings.builderConnectedToast"));
428
112
  },
429
113
  });
430
- const slackStatus = useActionQuery<SlackInstallationsResponse>(
431
- "list-slack-installations",
432
- undefined,
433
- { retry: false },
434
- );
435
- const builderCreditStatus = useActionQuery<BuilderCreditsStatus>(
436
- "get-builder-credit-status",
437
- undefined,
438
- { retry: false },
439
- );
440
114
  const [loading, setLoading] = useState(true);
441
115
  const [saving, setSaving] = useState(false);
442
- const [savingStorage, setSavingStorage] = useState(false);
443
- const [connectingSlack, setConnectingSlack] = useState(false);
444
- const [disconnectingSlack, setDisconnectingSlack] = useState(false);
445
- const [disconnectSlackTarget, setDisconnectSlackTarget] =
446
- useState<SlackInstallation | null>(null);
447
116
  const [defaultSpeed, setDefaultSpeed] = useState("1.2");
448
117
  const [emailNotifications, setEmailNotifications] = useState(true);
118
+ const [defaultVisibility, setDefaultVisibility] =
119
+ useState<ClipsDefaultVisibility>("private");
449
120
  const [transcriptCleanupEnabled, setTranscriptCleanupEnabled] =
450
121
  useState(true);
451
- const [s3Values, setS3Values] = useState<Record<string, string>>({});
452
- const [s3Errors, setS3Errors] = useState<Record<string, string>>({});
453
- const [clearingS3, setClearingS3] = useState(false);
454
- const [s3Expanded, setS3Expanded] = useState(false);
455
- const [apiKeysExpanded, setApiKeysExpanded] = useState(false);
456
- const [apiKeyValues, setApiKeyValues] = useState<Record<string, string>>({});
457
- const [apiKeyStatus, setApiKeyStatus] = useState<Record<string, boolean>>({});
458
- const [secretLast4, setSecretLast4] = useState<Record<string, string>>({});
459
- const [apiKeyStatusLoading, setApiKeyStatusLoading] = useState(true);
460
- const [savingApiKey, setSavingApiKey] = useState<string | null>(null);
461
-
462
- const refreshApiKeyStatus = useCallback(async () => {
463
- setApiKeyStatusLoading(true);
464
- try {
465
- const [envRes, secretsRes, adhocRes] = await Promise.all([
466
- fetch(agentNativePath("/_agent-native/env-status")),
467
- fetch(agentNativePath("/_agent-native/secrets")),
468
- fetch(agentNativePath("/_agent-native/secrets/adhoc")),
469
- ]);
470
- const envData = envRes.ok
471
- ? ((await envRes.json()) as Array<{
472
- key: string;
473
- configured?: boolean;
474
- }>)
475
- : [];
476
- const secretsData = secretsRes.ok
477
- ? ((await secretsRes.json()) as Array<{
478
- key: string;
479
- status?: string;
480
- }>)
481
- : [];
482
- const adhocData = adhocRes.ok
483
- ? ((await adhocRes.json()) as Array<{
484
- name: string;
485
- last4?: string;
486
- }>)
487
- : [];
488
- const next = Object.fromEntries(
489
- envData.map((entry) => [entry.key, Boolean(entry.configured)]),
490
- );
491
- for (const entry of secretsData) {
492
- next[entry.key] = entry.status === "set";
493
- }
494
- const nextLast4: Record<string, string> = {};
495
- for (const entry of adhocData) {
496
- next[entry.name] = true;
497
- if (entry.last4) nextLast4[entry.name] = entry.last4;
498
- }
499
- setSecretLast4(nextLast4);
500
- setApiKeyStatus(next);
501
- } catch {
502
- setApiKeyStatus({});
503
- } finally {
504
- setApiKeyStatusLoading(false);
505
- }
506
- }, []);
507
122
 
508
123
  useEffect(() => {
509
124
  let cancelled = false;
@@ -511,6 +126,7 @@ export default function SettingsIndexRoute() {
511
126
  if (cancelled) return;
512
127
  setDefaultSpeed(v.defaultPlaybackSpeed ?? "1.2");
513
128
  setEmailNotifications(v.emailNotifications ?? true);
129
+ setDefaultVisibility(v.defaultRecordingVisibility ?? "private");
514
130
  setTranscriptCleanupEnabled(v.transcriptCleanupEnabled !== false);
515
131
  setLoading(false);
516
132
  });
@@ -519,12 +135,27 @@ export default function SettingsIndexRoute() {
519
135
  };
520
136
  }, []);
521
137
 
522
- useEffect(() => {
523
- void refreshApiKeyStatus();
524
- }, [refreshApiKeyStatus]);
525
-
526
138
  const orgs = org?.orgs ?? [];
527
139
 
140
+ const builder = useMemo(
141
+ () => ({
142
+ connected: Boolean(
143
+ builderConnect.configured ||
144
+ builderStatus.status?.configured ||
145
+ storageStatus.data?.builderConfigured ||
146
+ storageStatus.data?.activeProvider?.id === "builder",
147
+ ),
148
+ loading:
149
+ storageStatus.isLoading ||
150
+ builderStatus.loading ||
151
+ !builderConnect.hasFetchedStatus,
152
+ connecting: builderConnect.connecting,
153
+ orgName: builderConnect.orgName ?? builderStatus.status?.orgName ?? null,
154
+ start: builderConnect.start,
155
+ }),
156
+ [builderConnect, builderStatus, storageStatus],
157
+ );
158
+
528
159
  async function handleUploadWorkspaceChange(organizationId: string) {
529
160
  if (!organizationId || organizationId === org?.orgId) return;
530
161
  try {
@@ -546,6 +177,7 @@ export default function SettingsIndexRoute() {
546
177
  defaultPlaybackSpeed: defaultSpeed,
547
178
  emailNotifications,
548
179
  transcriptCleanupEnabled,
180
+ defaultRecordingVisibility: defaultVisibility,
549
181
  });
550
182
  toast.success(t("settings.saved"));
551
183
  } catch (err) {
@@ -557,229 +189,10 @@ export default function SettingsIndexRoute() {
557
189
  }
558
190
  }
559
191
 
560
- function validateS3Values(
561
- values: Record<string, string>,
562
- ): Record<string, string> {
563
- const errors: Record<string, string> = {};
564
- const urlFields = ["S3_ENDPOINT", "S3_PUBLIC_BASE_URL"];
565
- for (const key of urlFields) {
566
- const val = (values[key] ?? "").trim();
567
- if (!val) continue;
568
- try {
569
- const parsed = new URL(val);
570
- if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
571
- errors[key] = t("settings.s3UrlInvalid");
572
- }
573
- } catch {
574
- errors[key] = t("settings.s3UrlInvalid");
575
- }
576
- }
577
- const bucket = (values["S3_BUCKET"] ?? "").trim();
578
- if (bucket && !/^[a-z0-9][a-z0-9\-.]{1,61}[a-z0-9]$/.test(bucket)) {
579
- errors["S3_BUCKET"] = t("settings.s3BucketInvalid");
580
- }
581
- return errors;
582
- }
583
-
584
- async function handleSaveS3Storage() {
585
- const validationErrors = validateS3Values(s3Values);
586
- if (Object.keys(validationErrors).length > 0) {
587
- setS3Errors(validationErrors);
588
- return;
589
- }
590
- const s3Configured = storageStatus.data?.activeProvider?.id === "s3";
591
- const missing = s3Configured
592
- ? []
593
- : S3_STORAGE_FIELDS.filter(
594
- (field) =>
595
- "required" in field &&
596
- field.required &&
597
- !(s3Values[field.key] ?? "").trim(),
598
- );
599
- if (missing.length > 0) {
600
- toast.error(t("settings.storageRequired"));
601
- return;
602
- }
603
-
604
- setSavingStorage(true);
605
- try {
606
- await saveS3StorageSettings(s3Values);
607
- setS3Values((current) => ({
608
- ...current,
609
- S3_SECRET_ACCESS_KEY: "",
610
- }));
611
- await Promise.all([storageStatus.refetch(), refreshApiKeyStatus()]);
612
- toast.success(t("settings.storageSaved"));
613
- } catch (err) {
614
- toast.error(
615
- err instanceof Error ? err.message : t("settings.saveFailed"),
616
- );
617
- } finally {
618
- setSavingStorage(false);
619
- }
620
- }
621
-
622
- async function handleClearAllS3() {
623
- setClearingS3(true);
624
- try {
625
- const results = await Promise.all(
626
- S3_STORAGE_FIELDS.filter((field) => apiKeyStatus[field.key]).map(
627
- async (field) => {
628
- const res = await fetch(
629
- agentNativePath(
630
- `/_agent-native/secrets/adhoc/${encodeURIComponent(field.key)}`,
631
- ),
632
- { method: "DELETE" },
633
- );
634
- if (!res.ok) {
635
- const body = (await res.json().catch(() => null)) as {
636
- error?: string;
637
- } | null;
638
- throw new Error(
639
- body?.error ?? `Failed to clear ${field.key} (${res.status})`,
640
- );
641
- }
642
- const body = (await res.json().catch(() => null)) as {
643
- removed?: boolean;
644
- } | null;
645
- return { key: field.key, removed: body?.removed !== false };
646
- },
647
- ),
648
- );
649
- const failed = results.filter((r) => !r.removed).map((r) => r.key);
650
- if (failed.length > 0) {
651
- throw new Error(
652
- `Could not remove: ${failed.join(", ")}. You may not have permission.`,
653
- );
654
- }
655
- setS3Values({});
656
- await Promise.all([refreshApiKeyStatus(), storageStatus.refetch()]);
657
- toast.success(t("settings.keyCleared"));
658
- } catch (err) {
659
- toast.error(
660
- err instanceof Error ? err.message : t("settings.saveFailed"),
661
- );
662
- } finally {
663
- setClearingS3(false);
664
- }
665
- }
666
-
667
- async function handleSaveApiKey(key: string) {
668
- const value = (apiKeyValues[key] ?? "").trim();
669
- if (!value) {
670
- toast.error(t("settings.pasteProviderKey"));
671
- return;
672
- }
673
-
674
- setSavingApiKey(key);
675
- try {
676
- const field = AI_PROVIDER_FIELDS.find((item) => item.key === key);
677
- if (field?.storage === "secret") {
678
- await saveRegisteredSecret(key, value);
679
- } else {
680
- await saveAgentEngineApiKey(key, value);
681
- }
682
- if (field && "engine" in field) {
683
- await applyAgentEngine(field.engine);
684
- }
685
- setApiKeyValues((current) => ({ ...current, [key]: "" }));
686
- setApiKeyStatus((current) => ({ ...current, [key]: true }));
687
- window.dispatchEvent(new CustomEvent("agent-engine:configured-changed"));
688
- await refreshApiKeyStatus();
689
- toast.success(t("settings.apiKeySaved"));
690
- } catch (err) {
691
- toast.error(
692
- err instanceof Error ? err.message : t("settings.apiKeyFailed"),
693
- );
694
- } finally {
695
- setSavingApiKey(null);
696
- }
697
- }
698
-
699
- function openAiProviderSetup() {
700
- setApiKeysExpanded(true);
701
- window.requestAnimationFrame(() => {
702
- const section = document.getElementById("ai-provider-keys");
703
- section?.scrollIntoView({ behavior: "smooth", block: "start" });
704
- const firstEmptyField =
705
- AI_PROVIDER_FIELDS.find((field) => !apiKeyStatus[field.key]) ??
706
- AI_PROVIDER_FIELDS[0];
707
- window.setTimeout(() => {
708
- document.getElementById(firstEmptyField.key)?.focus();
709
- }, 150);
710
- });
711
- }
712
-
713
- async function handleConnectSlack() {
714
- const beforeCount = slackInstallations.length;
715
- setConnectingSlack(true);
716
- try {
717
- await startSlackOAuth();
718
- const refreshed = await slackStatus.refetch();
719
- const afterCount = refreshed.data?.installations?.length ?? beforeCount;
720
- if (afterCount > beforeCount) {
721
- toast.success(t("settings.slackConnectedToast"));
722
- } else {
723
- toast.message(t("settings.slackCheckedToast"));
724
- }
725
- } catch (err) {
726
- toast.error(
727
- err instanceof Error ? err.message : t("settings.slackConnectFailed"),
728
- );
729
- } finally {
730
- setConnectingSlack(false);
731
- }
732
- }
733
-
734
- async function handleDisconnectSlack() {
735
- const target = disconnectSlackTarget;
736
- if (!target) return;
737
- setDisconnectingSlack(true);
738
- try {
739
- await requestDisconnectSlack(target.id);
740
- setDisconnectSlackTarget(null);
741
- await slackStatus.refetch();
742
- toast.success(t("settings.slackDisconnectedToast"));
743
- } catch (err) {
744
- toast.error(
745
- err instanceof Error
746
- ? err.message
747
- : t("settings.slackDisconnectFailed"),
748
- );
749
- } finally {
750
- setDisconnectingSlack(false);
751
- }
752
- }
753
-
754
- const storageConfigured = !!storageStatus.data?.configured;
755
- const activeProviderName = storageStatus.data?.activeProvider?.name ?? null;
756
- const activeProviderId = storageStatus.data?.activeProvider?.id ?? null;
757
- const builderConnected = Boolean(
758
- builderConnect.configured ||
759
- builderStatus.status?.configured ||
760
- storageStatus.data?.builderConfigured ||
761
- activeProviderId === "builder",
762
- );
763
- const builderOrgName =
764
- builderConnect.orgName ?? builderStatus.status?.orgName ?? null;
765
- const builderStatusLoading =
766
- storageStatus.isLoading ||
767
- builderStatus.loading ||
768
- !builderConnect.hasFetchedStatus;
769
- const s3Configured = activeProviderId === "s3";
770
- const s3Collapsed = !s3Expanded;
771
- const configuredApiKeyCount = AI_PROVIDER_FIELDS.filter(
772
- (field) => apiKeyStatus[field.key],
773
- ).length;
774
- const slackInstallations = slackStatus.data?.installations ?? [];
775
- const slackOauthConfigured = slackStatus.data?.oauthConfigured ?? false;
776
- const slackSigningConfigured = slackStatus.data?.signingConfigured ?? false;
777
- const slackConnected = slackInstallations.length > 0;
778
192
  const localizedChangelog = t("settings.changelogMarkdown");
779
- const builderCreditsPaused = builderCreditStatus.data?.exhausted === true;
780
- const builderCreditsUpgradeUrl =
781
- builderCreditStatus.data?.upgradeUrl ?? BUILDER_CREDITS_UPGRADE_URL;
782
193
 
194
+ // Hashes match the row ids below, so a search hit still scrolls to the
195
+ // individual setting now that the one-control cards are rows in a group.
783
196
  const generalSearchEntries = useMemo<SettingsSearchEntry[]>(
784
197
  () => [
785
198
  {
@@ -826,6 +239,12 @@ export default function SettingsIndexRoute() {
826
239
  keywords: "transcript cleanup captions",
827
240
  hash: "transcript",
828
241
  },
242
+ {
243
+ id: "clips-sharing",
244
+ label: t("settings.sharing"),
245
+ keywords: "sharing visibility private public organization default",
246
+ hash: "sharing",
247
+ },
829
248
  {
830
249
  id: "clips-notifications",
831
250
  label: t("settings.notifications"),
@@ -855,646 +274,25 @@ export default function SettingsIndexRoute() {
855
274
  {t("settings.intro")}
856
275
  </p>
857
276
 
858
- <Card id="language" className="scroll-mt-16">
859
- <CardHeader>
860
- <CardTitle className="text-base">
861
- {t("settings.languageTitle")}
862
- </CardTitle>
863
- <CardDescription>
864
- {t("settings.languageDescription")}
865
- </CardDescription>
866
- </CardHeader>
867
- <CardContent className="max-w-xs space-y-1.5">
868
- <Label>{t("settings.languageLabel")}</Label>
869
- <LanguagePicker label={t("settings.languageLabel")} />
870
- </CardContent>
871
- </Card>
872
-
873
- {orgs.length > 0 ? (
874
- <Card id="upload-workspace" className="scroll-mt-16">
875
- <CardHeader>
876
- <CardTitle className="flex items-center gap-2 text-base">
877
- <IconUsersGroup className="size-4 text-primary" />
878
- {t("settings.uploadWorkspaceTitle")}
879
- </CardTitle>
880
- <CardDescription>
881
- {t("settings.uploadWorkspaceDescription")}
882
- </CardDescription>
883
- </CardHeader>
884
- <CardContent className="max-w-md space-y-1.5">
885
- <Label htmlFor="upload-workspace-select">
886
- {t("settings.uploadWorkspaceLabel")}
887
- </Label>
888
- <Select
889
- value={org?.orgId ?? undefined}
890
- onValueChange={handleUploadWorkspaceChange}
891
- disabled={switchOrg.isPending || orgs.length < 2}
892
- >
893
- <SelectTrigger id="upload-workspace-select">
894
- <SelectValue
895
- placeholder={t("settings.uploadWorkspacePlaceholder")}
896
- />
897
- </SelectTrigger>
898
- <SelectContent>
899
- {orgs.map((workspace) => (
900
- <SelectItem
901
- key={workspace.orgId}
902
- value={workspace.orgId}
903
- >
904
- {workspace.orgName}
905
- </SelectItem>
906
- ))}
907
- </SelectContent>
908
- </Select>
909
- <p className="text-xs text-muted-foreground">
910
- {switchOrg.isPending
911
- ? t("settings.uploadWorkspaceSaving")
912
- : t("settings.uploadWorkspaceHint")}
913
- </p>
914
- </CardContent>
915
- </Card>
916
- ) : null}
917
-
918
- <Card id="video-storage" className="scroll-mt-16">
919
- <CardHeader>
920
- <CardTitle className="text-base flex items-center gap-2">
921
- <IconCloud className="size-4 text-primary" />
922
- {t("settings.videoStorage")}
923
- </CardTitle>
924
- <CardDescription>
925
- {t("settings.videoStorageDescription")}
926
- </CardDescription>
927
- </CardHeader>
928
- <CardContent className="space-y-4">
929
- <div
930
- className={cn(
931
- "flex flex-col gap-3 rounded-md border px-3 py-3 sm:flex-row sm:items-center sm:justify-between",
932
- builderConnected
933
- ? "border-primary/35 bg-primary/5"
934
- : "border-border bg-accent/30",
935
- )}
936
- >
937
- <div className="min-w-0">
938
- <div className="flex items-center gap-2 text-sm font-medium">
939
- {builderConnected ? (
940
- <IconCheck className="h-4 w-4 text-primary" />
941
- ) : (
942
- <IconKey className="h-4 w-4 text-muted-foreground" />
943
- )}
944
- {builderStatusLoading
945
- ? t("settings.checkingBuilder")
946
- : builderConnected
947
- ? t("settings.builderConnected")
948
- : t("settings.connectBuilder")}
949
- </div>
950
- <p className="mt-0.5 text-xs text-muted-foreground">
951
- {builderConnected
952
- ? builderOrgName
953
- ? t("settings.builderConnectedFor", {
954
- orgName: builderOrgName,
955
- })
956
- : t("settings.builderConnectedGeneric")
957
- : t("settings.builderIncludes")}
958
- </p>
277
+ <SettingsGroup
278
+ id="preferences"
279
+ title={t("settings.preferencesTitle")}
280
+ >
281
+ <SettingsRow
282
+ id="language"
283
+ label={t("settings.languageLabel")}
284
+ description={t("settings.languageDescription")}
285
+ control={
286
+ <div className="w-56">
287
+ <LanguagePicker label={t("settings.languageLabel")} />
959
288
  </div>
960
- {builderConnected ? (
961
- <Badge variant="secondary" className="shrink-0">
962
- {t("common.connected")}
963
- </Badge>
964
- ) : (
965
- <Button
966
- type="button"
967
- variant="outline"
968
- size="sm"
969
- className="shrink-0"
970
- onClick={() =>
971
- builderConnect.start({
972
- trackingSource: "clips_settings_video_storage",
973
- trackingFlow: "video_storage",
974
- })
975
- }
976
- disabled={
977
- builderConnect.connecting || builderStatusLoading
978
- }
979
- >
980
- {builderConnect.connecting ? (
981
- <IconLoader2 className="h-4 w-4 animate-spin" />
982
- ) : (
983
- <IconExternalLink className="h-4 w-4" />
984
- )}
985
- {t("settings.connectBuilder")}
986
- </Button>
987
- )}
988
- </div>
989
-
990
- <Collapsible
991
- open={!s3Collapsed}
992
- onOpenChange={(open) => setS3Expanded(open)}
993
- >
994
- <div className="rounded-md border border-border">
995
- <div className="flex flex-col gap-3 px-3 py-3 sm:flex-row sm:items-center sm:justify-between">
996
- <div className="min-w-0">
997
- <div className="flex flex-wrap items-center gap-2 text-sm font-medium">
998
- <IconServer className="h-4 w-4 text-muted-foreground" />
999
- {t("settings.s3Title")}
1000
- <Badge variant="outline" className="text-[10px]">
1001
- {t("settings.secondary")}
1002
- </Badge>
1003
- {s3Configured ? (
1004
- <Badge
1005
- variant="secondary"
1006
- className="text-[10px]"
1007
- >
1008
- {t("settings.active")}
1009
- </Badge>
1010
- ) : null}
1011
- </div>
1012
- <p className="mt-0.5 text-xs text-muted-foreground">
1013
- {builderConnected
1014
- ? t("settings.s3BuilderConnectedDescription")
1015
- : storageConfigured && activeProviderName
1016
- ? t("settings.s3CurrentProvider", {
1017
- providerName: activeProviderName,
1018
- })
1019
- : t("settings.s3OwnBucketDescription")}
1020
- </p>
1021
- </div>
1022
- <CollapsibleTrigger asChild>
1023
- <Button
1024
- type="button"
1025
- variant="ghost"
1026
- size="sm"
1027
- className="shrink-0"
1028
- >
1029
- {s3Collapsed
1030
- ? t("settings.configureS3")
1031
- : t("settings.hideS3")}
1032
- <IconChevronDown
1033
- className={cn(
1034
- "h-4 w-4 transition-transform",
1035
- !s3Collapsed && "rotate-180",
1036
- )}
1037
- />
1038
- </Button>
1039
- </CollapsibleTrigger>
1040
- </div>
1041
-
1042
- <CollapsibleContent>
1043
- <div className="space-y-4 border-t border-border px-3 py-4">
1044
- <div className="grid gap-4 sm:grid-cols-2">
1045
- {S3_STORAGE_FIELDS.map((field) => {
1046
- const configured = Boolean(
1047
- apiKeyStatus[field.key],
1048
- );
1049
- const last4 = secretLast4[field.key];
1050
- return (
1051
- <div key={field.key} className="space-y-1.5">
1052
- <div className="flex items-center justify-between gap-2">
1053
- <Label htmlFor={field.key}>
1054
- {t(field.labelKey)}
1055
- </Label>
1056
- {configured ? (
1057
- <span className="flex items-center gap-1 text-[10px] font-medium text-primary">
1058
- <IconCheck className="h-3 w-3" />
1059
- {last4
1060
- ? `••••${last4}`
1061
- : t("settings.keySet")}
1062
- </span>
1063
- ) : null}
1064
- </div>
1065
- <Input
1066
- id={field.key}
1067
- type={
1068
- "secret" in field && field.secret
1069
- ? "password"
1070
- : "text"
1071
- }
1072
- value={s3Values[field.key] ?? ""}
1073
- onChange={(event) => {
1074
- setS3Values((current) => ({
1075
- ...current,
1076
- [field.key]: event.target.value,
1077
- }));
1078
- if (s3Errors[field.key]) {
1079
- setS3Errors((current) => {
1080
- const next = { ...current };
1081
- delete next[field.key];
1082
- return next;
1083
- });
1084
- }
1085
- }}
1086
- placeholder={
1087
- configured
1088
- ? t("settings.replaceKey")
1089
- : field.placeholder
1090
- }
1091
- autoComplete="off"
1092
- disabled={savingStorage}
1093
- className={
1094
- s3Errors[field.key]
1095
- ? "border-destructive"
1096
- : undefined
1097
- }
1098
- />
1099
- {s3Errors[field.key] ? (
1100
- <p className="text-[11px] text-destructive">
1101
- {s3Errors[field.key]}
1102
- </p>
1103
- ) : null}
1104
- </div>
1105
- );
1106
- })}
1107
- </div>
1108
-
1109
- <div className="flex items-center justify-end gap-2">
1110
- {S3_STORAGE_FIELDS.some(
1111
- (field) => apiKeyStatus[field.key],
1112
- ) ? (
1113
- <Button
1114
- type="button"
1115
- variant="ghost"
1116
- size="sm"
1117
- onClick={handleClearAllS3}
1118
- disabled={clearingS3 || savingStorage}
1119
- className="text-muted-foreground hover:text-destructive"
1120
- >
1121
- {clearingS3 ? (
1122
- <IconLoader2 className="h-4 w-4 animate-spin" />
1123
- ) : (
1124
- <IconTrash className="h-4 w-4" />
1125
- )}
1126
- {t("settings.clearAllS3")}
1127
- </Button>
1128
- ) : null}
1129
- <Button
1130
- onClick={handleSaveS3Storage}
1131
- disabled={
1132
- savingStorage || storageStatus.isLoading
1133
- }
1134
- >
1135
- {savingStorage && (
1136
- <IconLoader2 className="h-4 w-4 animate-spin" />
1137
- )}
1138
- {t("settings.saveStorage")}
1139
- </Button>
1140
- </div>
1141
- </div>
1142
- </CollapsibleContent>
1143
- </div>
1144
- </Collapsible>
1145
- </CardContent>
1146
- </Card>
1147
-
1148
- <Card id="slack" className="scroll-mt-16">
1149
- <CardHeader>
1150
- <CardTitle className="text-base flex items-center gap-2">
1151
- <IconBrandSlack className="size-4 text-primary" />
1152
- {t("settings.slackTitle")}
1153
- </CardTitle>
1154
- <CardDescription>
1155
- {t("settings.slackDescription")}
1156
- </CardDescription>
1157
- </CardHeader>
1158
- <CardContent className="space-y-4">
1159
- <div className="flex flex-col gap-3 rounded-md border border-border bg-accent/30 px-3 py-3 sm:flex-row sm:items-center sm:justify-between">
1160
- <div className="min-w-0">
1161
- <div className="flex items-center gap-2 text-sm font-medium">
1162
- <IconBrandSlack className="h-4 w-4 text-muted-foreground" />
1163
- {slackStatus.isLoading
1164
- ? t("settings.checkingSlack")
1165
- : slackConnected
1166
- ? t("settings.slackConnected", {
1167
- count: slackInstallations.length,
1168
- })
1169
- : slackOauthConfigured
1170
- ? t("common.notConnected")
1171
- : t("settings.slackOauthNeeded")}
1172
- </div>
1173
- <p className="mt-0.5 text-xs text-muted-foreground">
1174
- {t("settings.slackPreviewDescription")}
1175
- </p>
1176
- </div>
1177
- <Button
1178
- type="button"
1179
- variant="outline"
1180
- size="sm"
1181
- className="shrink-0"
1182
- onClick={handleConnectSlack}
1183
- disabled={
1184
- connectingSlack ||
1185
- slackStatus.isLoading ||
1186
- !slackOauthConfigured
1187
- }
1188
- >
1189
- {connectingSlack ? (
1190
- <IconLoader2 className="h-4 w-4 animate-spin" />
1191
- ) : (
1192
- <IconExternalLink className="h-4 w-4" />
1193
- )}
1194
- {t("settings.connectSlack")}
1195
- </Button>
1196
- </div>
1197
-
1198
- {!slackOauthConfigured ? (
1199
- <div className="rounded-md border border-border p-3 text-xs text-muted-foreground">
1200
- {t("settings.slackClientMissing")}
1201
- </div>
1202
- ) : null}
1203
-
1204
- {!slackSigningConfigured ? (
1205
- <div className="rounded-md border border-border p-3 text-xs text-muted-foreground">
1206
- {t("settings.slackSigningMissing")}
1207
- </div>
1208
- ) : null}
1209
-
1210
- {slackInstallations.length > 0 ? (
1211
- <div className="space-y-2">
1212
- {slackInstallations.map((installation) => (
1213
- <div
1214
- key={installation.id}
1215
- className="flex items-center justify-between gap-3 rounded-md border border-border px-3 py-2"
1216
- >
1217
- <div className="min-w-0">
1218
- <div className="truncate text-sm font-medium">
1219
- {installation.teamName || installation.teamId}
1220
- </div>
1221
- <div className="mt-0.5 flex flex-wrap items-center gap-x-2 gap-y-1 text-xs text-muted-foreground">
1222
- <span>{installation.status}</span>
1223
- {installation.enterpriseName ? (
1224
- <span>{installation.enterpriseName}</span>
1225
- ) : null}
1226
- <span>
1227
- {t("settings.connectedBy", {
1228
- email: installation.ownerEmail,
1229
- })}
1230
- </span>
1231
- </div>
1232
- </div>
1233
- <Button
1234
- type="button"
1235
- variant="ghost"
1236
- size="icon"
1237
- className="shrink-0"
1238
- aria-label={t("settings.disconnectSlackLabel", {
1239
- team:
1240
- installation.teamName || installation.teamId,
1241
- })}
1242
- onClick={() =>
1243
- setDisconnectSlackTarget(installation)
1244
- }
1245
- >
1246
- <IconTrash className="h-4 w-4" />
1247
- </Button>
1248
- </div>
1249
- ))}
1250
- </div>
1251
- ) : null}
1252
- </CardContent>
1253
- </Card>
1254
-
1255
- <Card id="ai-providers" className="scroll-mt-16">
1256
- <CardHeader>
1257
- <CardTitle className="text-base flex items-center gap-2">
1258
- <IconBrain className="size-4 text-primary" />
1259
- {t("settings.apiSetup")}
1260
- </CardTitle>
1261
- <CardDescription>
1262
- {t("settings.apiSetupDescription")}
1263
- </CardDescription>
1264
- </CardHeader>
1265
- <CardContent className="space-y-4">
1266
- <div
1267
- className={cn(
1268
- "flex flex-col gap-3 rounded-md border px-3 py-3 sm:flex-row sm:items-center sm:justify-between",
1269
- builderConnected
1270
- ? "border-border bg-muted/20"
1271
- : "border-border bg-accent/30",
1272
- )}
1273
- >
1274
- <div className="min-w-0">
1275
- <div className="flex items-center gap-2 text-sm font-medium">
1276
- {builderConnected ? (
1277
- <IconCheck className="h-4 w-4 text-primary" />
1278
- ) : (
1279
- <IconKey className="h-4 w-4 text-muted-foreground" />
1280
- )}
1281
- {builderStatusLoading
1282
- ? t("settings.checkingBuilder")
1283
- : builderConnected
1284
- ? t("settings.builderAiAvailable")
1285
- : t("settings.builderEasySetup")}
1286
- </div>
1287
- <p className="mt-0.5 text-xs text-muted-foreground">
1288
- {builderConnected
1289
- ? t("settings.apiSetupDescription")
1290
- : t("settings.builderAiDescription")}
1291
- </p>
1292
- </div>
1293
- {!builderConnected ? (
1294
- <Button
1295
- type="button"
1296
- variant="outline"
1297
- size="sm"
1298
- className="shrink-0"
1299
- onClick={() =>
1300
- builderConnect.start({
1301
- trackingSource: "clips_settings_ai_setup",
1302
- trackingFlow: "connect_llm",
1303
- })
1304
- }
1305
- disabled={
1306
- builderConnect.connecting || builderStatusLoading
1307
- }
1308
- >
1309
- {builderConnect.connecting ? (
1310
- <IconLoader2 className="h-4 w-4 animate-spin" />
1311
- ) : (
1312
- <IconExternalLink className="h-4 w-4" />
1313
- )}
1314
- {t("settings.connectBuilder")}
1315
- </Button>
1316
- ) : null}
1317
- </div>
1318
-
1319
- {builderCreditsPaused ? (
1320
- <div className="rounded-md border border-amber-300/70 bg-amber-50/80 p-3 text-amber-950 shadow-sm dark:border-amber-400/30 dark:bg-amber-950/25 dark:text-amber-100">
1321
- <div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
1322
- <div className="min-w-0 flex-1">
1323
- <div className="flex items-center gap-2 text-sm font-semibold">
1324
- <span className="rounded-md bg-amber-100 p-1 dark:bg-amber-400/15">
1325
- <IconBolt className="h-4 w-4 text-amber-700 dark:text-amber-200" />
1326
- </span>
1327
- {t("builderCredits.pausedTitle")}
1328
- </div>
1329
- <p className="mt-1.5 text-xs leading-relaxed text-amber-900/80 dark:text-amber-100/80">
1330
- {t("builderCredits.settingsDescription")}
1331
- </p>
1332
- <div className="mt-2 flex flex-wrap gap-1.5">
1333
- {BUILDER_CREDITS_FEATURE_LABELS.map((key) => (
1334
- <span
1335
- key={key}
1336
- className="rounded-full border border-amber-300/70 bg-white/70 px-2 py-0.5 text-[11px] font-medium text-amber-900 dark:border-amber-400/30 dark:bg-amber-950/30 dark:text-amber-100"
1337
- >
1338
- {t(key)}
1339
- </span>
1340
- ))}
1341
- </div>
1342
- </div>
1343
- <div className="flex shrink-0 flex-wrap gap-2">
1344
- <Button asChild size="sm" className="h-8">
1345
- <a
1346
- href={builderCreditsUpgradeUrl}
1347
- target="_blank"
1348
- rel="noopener noreferrer"
1349
- >
1350
- <IconExternalLink className="h-4 w-4" />
1351
- {t("builderCredits.upgrade")}
1352
- </a>
1353
- </Button>
1354
- <Button
1355
- type="button"
1356
- variant="outline"
1357
- size="sm"
1358
- className="h-8 border-amber-300/80 bg-white/70 text-amber-950 hover:bg-amber-100 dark:border-amber-400/40 dark:bg-amber-950/30 dark:text-amber-100 dark:hover:bg-amber-900/40"
1359
- onClick={openAiProviderSetup}
1360
- >
1361
- {t("builderCredits.openAiSetup")}
1362
- </Button>
1363
- </div>
1364
- </div>
1365
- </div>
1366
- ) : null}
1367
-
1368
- <Collapsible
1369
- open={apiKeysExpanded}
1370
- onOpenChange={setApiKeysExpanded}
1371
- >
1372
- <div className="rounded-md border border-border">
1373
- <CollapsibleTrigger asChild>
1374
- <button
1375
- id="ai-provider-keys"
1376
- type="button"
1377
- className="flex w-full items-center justify-between gap-3 px-3 py-3 text-start"
1378
- >
1379
- <div className="min-w-0">
1380
- <div className="flex items-center gap-2 text-sm font-medium">
1381
- <IconKey className="h-4 w-4 text-muted-foreground" />
1382
- {t("settings.providerKeyTitle")}
1383
- {configuredApiKeyCount > 0 ? (
1384
- <Badge
1385
- variant="secondary"
1386
- className="text-[10px]"
1387
- >
1388
- {t("settings.providerKeysSet", {
1389
- count: configuredApiKeyCount,
1390
- })}
1391
- </Badge>
1392
- ) : null}
1393
- </div>
1394
- <p className="mt-0.5 text-xs text-muted-foreground">
1395
- {t("settings.providerKeyDescription")}
1396
- </p>
1397
- </div>
1398
- <IconChevronDown
1399
- className={cn(
1400
- "h-4 w-4 shrink-0 text-muted-foreground transition-transform",
1401
- apiKeysExpanded && "rotate-180",
1402
- )}
1403
- />
1404
- </button>
1405
- </CollapsibleTrigger>
1406
- <CollapsibleContent>
1407
- <div className="space-y-3 border-t border-border px-3 py-4">
1408
- {apiKeyStatusLoading ? (
1409
- <div className="text-xs text-muted-foreground">
1410
- {t("settings.checkingProviderKeys")}
1411
- </div>
1412
- ) : null}
1413
- <div className="grid gap-3 sm:grid-cols-2">
1414
- {AI_PROVIDER_FIELDS.map((field) => {
1415
- const configured = Boolean(
1416
- apiKeyStatus[field.key],
1417
- );
1418
- const savingThisKey = savingApiKey === field.key;
1419
- return (
1420
- <div key={field.key} className="space-y-1.5">
1421
- <div className="flex items-center justify-between gap-2">
1422
- <Label htmlFor={field.key}>
1423
- {field.label}
1424
- </Label>
1425
- {configured ? (
1426
- <span className="flex items-center gap-1 text-[10px] font-medium text-primary">
1427
- <IconCheck className="h-3 w-3" />
1428
- {t("settings.keySet")}
1429
- </span>
1430
- ) : null}
1431
- </div>
1432
- <div className="flex gap-2">
1433
- <Input
1434
- id={field.key}
1435
- type="password"
1436
- value={apiKeyValues[field.key] ?? ""}
1437
- onChange={(event) =>
1438
- setApiKeyValues((current) => ({
1439
- ...current,
1440
- [field.key]: event.target.value,
1441
- }))
1442
- }
1443
- onKeyDown={(event) => {
1444
- if (event.key === "Enter") {
1445
- void handleSaveApiKey(field.key);
1446
- }
1447
- }}
1448
- placeholder={
1449
- configured
1450
- ? t("settings.replaceKey")
1451
- : field.placeholder
1452
- }
1453
- autoComplete="off"
1454
- disabled={savingThisKey}
1455
- />
1456
- <Button
1457
- type="button"
1458
- variant="outline"
1459
- size="sm"
1460
- className="shrink-0"
1461
- onClick={() =>
1462
- handleSaveApiKey(field.key)
1463
- }
1464
- disabled={
1465
- savingThisKey ||
1466
- !(apiKeyValues[field.key] ?? "").trim()
1467
- }
1468
- >
1469
- {savingThisKey ? (
1470
- <IconLoader2 className="h-4 w-4 animate-spin" />
1471
- ) : (
1472
- t("common.save")
1473
- )}
1474
- </Button>
1475
- </div>
1476
- </div>
1477
- );
1478
- })}
1479
- </div>
1480
- </div>
1481
- </CollapsibleContent>
1482
- </div>
1483
- </Collapsible>
1484
- </CardContent>
1485
- </Card>
1486
-
1487
- <Card id="playback" className="scroll-mt-16">
1488
- <CardHeader>
1489
- <CardTitle className="text-base">
1490
- {t("settings.playback")}
1491
- </CardTitle>
1492
- </CardHeader>
1493
- <CardContent className="space-y-4">
1494
- <div className="space-y-1.5">
1495
- <Label htmlFor="speed">
1496
- {t("settings.defaultPlaybackSpeed")}
1497
- </Label>
289
+ }
290
+ />
291
+ <SettingsRow
292
+ id="playback"
293
+ label={t("settings.defaultPlaybackSpeed")}
294
+ description={t("settings.playbackDescription")}
295
+ control={
1498
296
  <Select
1499
297
  value={defaultSpeed}
1500
298
  onValueChange={setDefaultSpeed}
@@ -1511,67 +309,121 @@ export default function SettingsIndexRoute() {
1511
309
  ))}
1512
310
  </SelectContent>
1513
311
  </Select>
1514
- <p className="text-xs text-muted-foreground">
1515
- {t("settings.playbackDescription")}
1516
- </p>
1517
- </div>
1518
- </CardContent>
1519
- </Card>
1520
-
1521
- <Card id="transcript" className="scroll-mt-16">
1522
- <CardHeader>
1523
- <CardTitle className="text-base">
1524
- {t("settings.transcript")}
1525
- </CardTitle>
1526
- </CardHeader>
1527
- <CardContent>
1528
- <div className="flex items-center justify-between gap-4">
1529
- <div>
1530
- <Label
1531
- htmlFor="transcript-cleanup"
1532
- className="cursor-pointer"
1533
- >
1534
- {t("settings.transcriptCleanup")}
1535
- </Label>
1536
- <p className="text-xs text-muted-foreground mt-0.5">
1537
- {t("settings.transcriptCleanupDescription")}
1538
- </p>
1539
- </div>
312
+ }
313
+ />
314
+ <SettingsRow
315
+ id="sharing"
316
+ label={t("settings.defaultVisibility")}
317
+ description={t("settings.defaultVisibilityDescription")}
318
+ control={
319
+ <Select
320
+ value={defaultVisibility}
321
+ onValueChange={(value) =>
322
+ setDefaultVisibility(value as ClipsDefaultVisibility)
323
+ }
324
+ disabled={loading}
325
+ >
326
+ <SelectTrigger id="default-visibility" className="w-56">
327
+ <SelectValue />
328
+ </SelectTrigger>
329
+ <SelectContent>
330
+ <SelectItem value="private">
331
+ {t("settings.visibilityPrivate")}
332
+ </SelectItem>
333
+ <SelectItem value="org">
334
+ {t("settings.visibilityOrg")}
335
+ </SelectItem>
336
+ <SelectItem value="public">
337
+ {t("settings.visibilityPublic")}
338
+ </SelectItem>
339
+ </SelectContent>
340
+ </Select>
341
+ }
342
+ />
343
+ <SettingsRow
344
+ id="transcript"
345
+ label={t("settings.transcriptCleanup")}
346
+ description={t("settings.transcriptCleanupDescription")}
347
+ control={
1540
348
  <Switch
1541
349
  id="transcript-cleanup"
350
+ aria-label={t("settings.transcriptCleanup")}
1542
351
  checked={transcriptCleanupEnabled}
1543
352
  onCheckedChange={setTranscriptCleanupEnabled}
1544
353
  disabled={loading}
1545
354
  />
1546
- </div>
1547
- </CardContent>
1548
- </Card>
1549
-
1550
- <Card id="notifications" className="scroll-mt-16">
1551
- <CardHeader>
1552
- <CardTitle className="text-base">
1553
- {t("settings.notifications")}
1554
- </CardTitle>
1555
- </CardHeader>
1556
- <CardContent>
1557
- <div className="flex items-center justify-between">
1558
- <div>
1559
- <Label htmlFor="email-notif" className="cursor-pointer">
1560
- {t("settings.emailNotifications")}
1561
- </Label>
1562
- <p className="text-xs text-muted-foreground mt-0.5">
1563
- {t("settings.emailNotificationsDescription")}
1564
- </p>
1565
- </div>
355
+ }
356
+ />
357
+ <SettingsRow
358
+ id="notifications"
359
+ label={t("settings.emailNotifications")}
360
+ description={t("settings.emailNotificationsDescription")}
361
+ control={
1566
362
  <Switch
1567
363
  id="email-notif"
364
+ aria-label={t("settings.emailNotifications")}
1568
365
  checked={emailNotifications}
1569
366
  onCheckedChange={setEmailNotifications}
1570
367
  disabled={loading}
1571
368
  />
1572
- </div>
1573
- </CardContent>
1574
- </Card>
369
+ }
370
+ />
371
+ </SettingsGroup>
372
+
373
+ {orgs.length > 0 ? (
374
+ <SettingsGroup
375
+ title={t("settings.uploadWorkspaceTitle")}
376
+ description={t("settings.uploadWorkspaceDescription")}
377
+ >
378
+ <SettingsRow
379
+ id="upload-workspace"
380
+ label={t("settings.uploadWorkspaceLabel")}
381
+ description={
382
+ switchOrg.isPending
383
+ ? t("settings.uploadWorkspaceSaving")
384
+ : t("settings.uploadWorkspaceHint")
385
+ }
386
+ control={
387
+ <Select
388
+ value={org?.orgId ?? undefined}
389
+ onValueChange={handleUploadWorkspaceChange}
390
+ disabled={switchOrg.isPending || orgs.length < 2}
391
+ >
392
+ <SelectTrigger
393
+ id="upload-workspace-select"
394
+ className="w-64"
395
+ >
396
+ <SelectValue
397
+ placeholder={t(
398
+ "settings.uploadWorkspacePlaceholder",
399
+ )}
400
+ />
401
+ </SelectTrigger>
402
+ <SelectContent>
403
+ {orgs.map((workspace) => (
404
+ <SelectItem
405
+ key={workspace.orgId}
406
+ value={workspace.orgId}
407
+ >
408
+ {workspace.orgName}
409
+ </SelectItem>
410
+ ))}
411
+ </SelectContent>
412
+ </Select>
413
+ }
414
+ />
415
+ </SettingsGroup>
416
+ ) : null}
417
+
418
+ <VideoStorageSection
419
+ builder={builder}
420
+ secrets={secrets}
421
+ storageStatus={storageStatus}
422
+ />
423
+
424
+ <AiSetupSection builder={builder} secrets={secrets} />
425
+
426
+ <SlackSection />
1575
427
 
1576
428
  <div className="flex justify-end">
1577
429
  <Button
@@ -1597,44 +449,6 @@ export default function SettingsIndexRoute() {
1597
449
  </div>
1598
450
  }
1599
451
  />
1600
- <AlertDialog
1601
- open={!!disconnectSlackTarget}
1602
- onOpenChange={(open) => {
1603
- if (!open && !disconnectingSlack) setDisconnectSlackTarget(null);
1604
- }}
1605
- >
1606
- <AlertDialogContent>
1607
- <AlertDialogHeader>
1608
- <AlertDialogTitle>
1609
- {t("settings.disconnectSlackTitle")}
1610
- </AlertDialogTitle>
1611
- <AlertDialogDescription>
1612
- {t("settings.disconnectSlackDescription", {
1613
- team:
1614
- disconnectSlackTarget?.teamName ||
1615
- disconnectSlackTarget?.teamId ||
1616
- t("settings.thisWorkspace"),
1617
- })}
1618
- </AlertDialogDescription>
1619
- </AlertDialogHeader>
1620
- <AlertDialogFooter>
1621
- <AlertDialogCancel disabled={disconnectingSlack}>
1622
- {t("common.cancel")}
1623
- </AlertDialogCancel>
1624
- <AlertDialogAction
1625
- onClick={(event) => {
1626
- event.preventDefault();
1627
- void handleDisconnectSlack();
1628
- }}
1629
- disabled={disconnectingSlack}
1630
- >
1631
- {disconnectingSlack
1632
- ? t("common.disconnecting")
1633
- : t("common.disconnect")}
1634
- </AlertDialogAction>
1635
- </AlertDialogFooter>
1636
- </AlertDialogContent>
1637
- </AlertDialog>
1638
452
  </>
1639
453
  );
1640
454
  }