@agent-native/core 0.134.1 → 0.135.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (463) hide show
  1. package/corpus/README.md +3 -3
  2. package/corpus/core/CHANGELOG.md +113 -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/access.ts +44 -2
  48. package/corpus/core/src/sharing/index.ts +5 -0
  49. package/corpus/core/src/sharing/recipients.ts +67 -0
  50. package/corpus/core/src/triggers/actions/list-automations.ts +27 -8
  51. package/corpus/core/src/triggers/actions/manage-automation.ts +17 -6
  52. package/corpus/core/src/triggers/actions.ts +16 -4
  53. package/corpus/core/src/triggers/dispatcher.ts +44 -30
  54. package/corpus/core/src/triggers/routes.ts +19 -6
  55. package/corpus/core/src/vitest-config.ts +68 -0
  56. package/corpus/core/vitest.config.ts +5 -0
  57. package/corpus/templates/analytics/app/pages/Settings.tsx +37 -53
  58. package/corpus/templates/analytics/changelog/2026-08-01-new-error-alerts-now-reach-you-by-email-and-a-scheduled-dash.md +6 -0
  59. package/corpus/templates/analytics/server/jobs/dashboard-report.ts +43 -0
  60. package/corpus/templates/analytics/server/lib/error-capture.ts +5 -1
  61. package/corpus/templates/analytics/vitest.config.ts +16 -12
  62. package/corpus/templates/assets/actions/refresh-generation-run.ts +3 -0
  63. package/corpus/templates/assets/app/components/ui/switch.tsx +1 -0
  64. package/corpus/templates/assets/app/hooks/use-assets-prefs.ts +55 -0
  65. package/corpus/templates/assets/app/i18n-data.ts +30 -0
  66. package/corpus/templates/assets/app/routes/settings.tsx +58 -55
  67. package/corpus/templates/assets/changelog/2026-08-01-get-an-email-when-a-generation-you-started-finishes-or-fails.md +6 -0
  68. package/corpus/templates/assets/server/lib/generation-run-notifications.ts +89 -0
  69. package/corpus/templates/assets/server/lib/video-runs.ts +8 -1
  70. package/corpus/templates/assets/server/routes/_agent-native/assets/user-prefs.get.ts +17 -0
  71. package/corpus/templates/assets/server/routes/_agent-native/assets/user-prefs.put.ts +36 -0
  72. package/corpus/templates/assets/shared/assets-user-prefs.ts +10 -0
  73. package/corpus/templates/assets/vitest.config.ts +16 -12
  74. package/corpus/templates/brain/app/routes/settings.tsx +15 -15
  75. package/corpus/templates/brain/vitest.config.ts +16 -12
  76. package/corpus/templates/calendar/app/pages/Settings.tsx +32 -40
  77. package/corpus/templates/calendar/changelog/2026-08-01-language-and-appearance-now-sit-together-in-one-preferences-.md +6 -0
  78. package/corpus/templates/calendar/vitest.config.ts +16 -12
  79. package/corpus/templates/chat/app/routes/settings.tsx +14 -22
  80. package/corpus/templates/chat/vitest.config.ts +9 -0
  81. package/corpus/templates/clips/.agents/skills/video-editing/SKILL.md +31 -0
  82. package/corpus/templates/clips/AGENTS.md +1 -0
  83. package/corpus/templates/clips/actions/add-comment.ts +12 -1
  84. package/corpus/templates/clips/actions/create-meeting.ts +2 -2
  85. package/corpus/templates/clips/actions/create-recording.ts +2 -2
  86. package/corpus/templates/clips/actions/finalize-recording.ts +42 -6
  87. package/corpus/templates/clips/actions/generate-filmstrip.ts +154 -0
  88. package/corpus/templates/clips/actions/get-recording-player-data.ts +6 -0
  89. package/corpus/templates/clips/actions/import-loom-recording.ts +2 -2
  90. package/corpus/templates/clips/actions/lib/ensure-recording-filmstrip.ts +258 -0
  91. package/corpus/templates/clips/actions/lib/ensure-seekable-video.ts +11 -3
  92. package/corpus/templates/clips/actions/react-to-comment.ts +13 -0
  93. package/corpus/templates/clips/actions/react-to-recording.ts +10 -1
  94. package/corpus/templates/clips/actions/reply-to-comment.ts +12 -1
  95. package/corpus/templates/clips/actions/stitch-recordings.ts +2 -2
  96. package/corpus/templates/clips/app/components/editor/editor-layout.tsx +152 -23
  97. package/corpus/templates/clips/app/components/editor/waveform.tsx +237 -75
  98. package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +23 -1
  99. package/corpus/templates/clips/app/components/settings/ai-setup-section.tsx +441 -0
  100. package/corpus/templates/clips/app/components/settings/slack-section.tsx +329 -0
  101. package/corpus/templates/clips/app/components/settings/types.ts +14 -0
  102. package/corpus/templates/clips/app/components/settings/video-storage-section.tsx +443 -0
  103. package/corpus/templates/clips/app/hooks/use-secret-status.ts +69 -0
  104. package/corpus/templates/clips/app/i18n/en-US.ts +9 -1
  105. package/corpus/templates/clips/app/lib/video-filmstrip.ts +262 -0
  106. package/corpus/templates/clips/app/routes/_app.settings._index.tsx +165 -1351
  107. package/corpus/templates/clips/changelog/2026-07-27-saved-clips-now-resume-from-the-last-uploaded-byte-after-an-.md +6 -0
  108. package/corpus/templates/clips/changelog/2026-07-31-editor-timeline-now-shows-video-frames-behind-the-waveform-g.md +6 -0
  109. package/corpus/templates/clips/changelog/2026-07-31-email-notifications-now-go-out-when-someone-comments-on-or-r.md +6 -0
  110. package/corpus/templates/clips/changelog/2026-07-31-you-can-now-set-a-default-visibility-for-new-recordings-in-s.md +6 -0
  111. package/corpus/templates/clips/changelog/2026-08-01-email-notifications-for-comments-and-reactions-now-actually-.md +6 -0
  112. package/corpus/templates/clips/changelog/2026-08-03-the-desktop-app-now-opens-directly-beneath-its-tray-icon-on-.md +6 -0
  113. package/corpus/templates/clips/chrome-extension/package.json +1 -0
  114. package/corpus/templates/clips/chrome-extension/vitest.config.ts +9 -0
  115. package/corpus/templates/clips/desktop/package.json +1 -0
  116. package/corpus/templates/clips/desktop/src/app.tsx +17 -1
  117. package/corpus/templates/clips/desktop/src/lib/recorder.ts +331 -36
  118. package/corpus/templates/clips/desktop/src/lib/upload-recovery.ts +129 -0
  119. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +815 -15
  120. package/corpus/templates/clips/desktop/src-tauri/src/tray.rs +61 -11
  121. package/corpus/templates/clips/desktop/vitest.config.ts +9 -0
  122. package/corpus/templates/clips/server/db/schema.ts +17 -0
  123. package/corpus/templates/clips/server/jobs/media-verification.ts +4 -0
  124. package/corpus/templates/clips/server/lib/activity-notifications.ts +185 -0
  125. package/corpus/templates/clips/server/lib/recording-media-cleanup.ts +2 -0
  126. package/corpus/templates/clips/server/lib/recording-upload-state.ts +46 -9
  127. package/corpus/templates/clips/server/lib/recordings.ts +24 -0
  128. package/corpus/templates/clips/server/lib/resumable-session.ts +10 -4
  129. package/corpus/templates/clips/server/lib/transactional-email-templates.ts +109 -0
  130. package/corpus/templates/clips/server/lib/upload-interruption.ts +8 -0
  131. package/corpus/templates/clips/server/lib/upload-lease.ts +50 -4
  132. package/corpus/templates/clips/server/lib/video-filmstrip-sprite.ts +207 -0
  133. package/corpus/templates/clips/server/lib/video-remux.ts +13 -4
  134. package/corpus/templates/clips/server/plugins/db.ts +16 -0
  135. package/corpus/templates/clips/server/routes/api/_agent-native-background/post-finalize-worker.post.ts +4 -0
  136. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/abort.post.ts +123 -36
  137. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +159 -29
  138. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/interrupt.post.ts +197 -0
  139. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/reset-chunks.post.ts +193 -39
  140. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/resume.get.ts +191 -12
  141. package/corpus/templates/clips/shared/clips-ai-prefs.ts +6 -0
  142. package/corpus/templates/clips/shared/feature-flags.ts +12 -0
  143. package/corpus/templates/clips/shared/recording-core.ts +4 -0
  144. package/corpus/templates/clips/vitest.config.ts +23 -19
  145. package/corpus/templates/content/actions/_database-utils.ts +113 -4
  146. package/corpus/templates/content/actions/add-comment.ts +15 -1
  147. package/corpus/templates/content/actions/get-document.ts +84 -3
  148. package/corpus/templates/content/actions/list-documents.ts +1 -0
  149. package/corpus/templates/content/app/components/editor/BuilderBodySyncingNotice.tsx +9 -2
  150. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +34 -15
  151. package/corpus/templates/content/app/components/editor/DocumentToolbar.tsx +55 -13
  152. package/corpus/templates/content/app/components/editor/body-hydration.ts +12 -6
  153. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +2 -3
  154. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +5 -12
  155. package/corpus/templates/content/app/components/ui/switch.tsx +1 -0
  156. package/corpus/templates/content/app/hooks/use-content-database.ts +16 -27
  157. package/corpus/templates/content/app/hooks/use-content-prefs.ts +55 -0
  158. package/corpus/templates/content/app/hooks/use-create-page.ts +3 -6
  159. package/corpus/templates/content/app/hooks/use-document-properties.ts +9 -16
  160. package/corpus/templates/content/app/hooks/use-document-versions.ts +3 -3
  161. package/corpus/templates/content/app/hooks/use-documents.ts +85 -29
  162. package/corpus/templates/content/app/hooks/use-notion.ts +15 -13
  163. package/corpus/templates/content/app/i18n-data.ts +72 -1
  164. package/corpus/templates/content/app/lib/document-query.ts +36 -0
  165. package/corpus/templates/content/app/routes/_app.settings.tsx +47 -18
  166. package/corpus/templates/content/changelog/2026-07-30-breadcrumb-menus-now-hide-internal-system-pages.md +6 -0
  167. package/corpus/templates/content/changelog/2026-08-01-get-an-email-when-someone-comments-on-replies-in-or-mentions.md +6 -0
  168. package/corpus/templates/content/changelog/2026-08-02-pages-opened-from-a-database-now-keep-that-database-s-fields.md +6 -0
  169. package/corpus/templates/content/server/lib/comment-notifications.ts +146 -0
  170. package/corpus/templates/content/server/lib/document-context.ts +11 -6
  171. package/corpus/templates/content/server/routes/_agent-native/content/user-prefs.get.ts +17 -0
  172. package/corpus/templates/content/server/routes/_agent-native/content/user-prefs.put.ts +36 -0
  173. package/corpus/templates/content/shared/api.ts +8 -0
  174. package/corpus/templates/content/shared/content-user-prefs.ts +11 -0
  175. package/corpus/templates/content/vitest.config.ts +20 -16
  176. package/corpus/templates/crm/app/routes/settings.tsx +14 -22
  177. package/corpus/templates/crm/vitest.config.ts +8 -4
  178. package/corpus/templates/design/app/routes/settings.tsx +14 -22
  179. package/corpus/templates/design/changelog/2026-08-01-review-comments-replies-and-mentions-now-send-an-email-to-th.md +6 -0
  180. package/corpus/templates/design/vitest.config.ts +18 -12
  181. package/corpus/templates/dispatch/app/routes/settings.tsx +26 -40
  182. package/corpus/templates/dispatch/vitest.config.ts +11 -7
  183. package/corpus/templates/factory/app/routes/settings.tsx +14 -22
  184. package/corpus/templates/factory/vitest.config.ts +9 -0
  185. package/corpus/templates/forms/app/routes/_app.settings.tsx +14 -22
  186. package/corpus/templates/forms/vitest.config.ts +27 -23
  187. package/corpus/templates/macros/app/routes/settings.tsx +14 -22
  188. package/corpus/templates/macros/vitest.config.ts +16 -12
  189. package/corpus/templates/mail/app/pages/SettingsPage.tsx +14 -16
  190. package/corpus/templates/mail/vitest.config.ts +16 -12
  191. package/corpus/templates/plan/app/routes/settings.tsx +30 -44
  192. package/corpus/templates/plan/vitest.config.ts +24 -20
  193. package/corpus/templates/slides/actions/add-slide-comment.ts +12 -1
  194. package/corpus/templates/slides/app/components/ui/switch.tsx +1 -0
  195. package/corpus/templates/slides/app/hooks/use-slides-prefs.ts +55 -0
  196. package/corpus/templates/slides/app/i18n/en-US.ts +4 -0
  197. package/corpus/templates/slides/app/routes/settings.tsx +45 -22
  198. package/corpus/templates/slides/changelog/2026-08-01-get-an-email-when-someone-comments-on-or-replies-in-your-dec.md +6 -0
  199. package/corpus/templates/slides/server/lib/comment-notifications.ts +182 -0
  200. package/corpus/templates/slides/server/routes/_agent-native/slides/user-prefs.get.ts +16 -0
  201. package/corpus/templates/slides/server/routes/_agent-native/slides/user-prefs.put.ts +36 -0
  202. package/corpus/templates/slides/shared/slides-user-prefs.ts +11 -0
  203. package/corpus/templates/slides/vitest.config.ts +17 -13
  204. package/corpus/templates/tasks/app/components/layout/Sidebar.tsx +2 -0
  205. package/corpus/templates/tasks/app/routes/settings.tsx +14 -22
  206. package/corpus/templates/tasks/vitest.config.ts +17 -13
  207. package/corpus/toolkit/CHANGELOG.md +9 -0
  208. package/corpus/toolkit/package.json +1 -1
  209. package/corpus/toolkit/vitest.config.ts +3 -0
  210. package/dist/automations/service.d.ts +2 -0
  211. package/dist/automations/service.d.ts.map +1 -1
  212. package/dist/automations/service.js +29 -4
  213. package/dist/automations/service.js.map +1 -1
  214. package/dist/cli/mcp-config-writers.d.ts.map +1 -1
  215. package/dist/cli/mcp-config-writers.js +24 -23
  216. package/dist/cli/mcp-config-writers.js.map +1 -1
  217. package/dist/client/agent-page/AgentJobsTab.d.ts.map +1 -1
  218. package/dist/client/agent-page/AgentJobsTab.js +86 -29
  219. package/dist/client/agent-page/AgentJobsTab.js.map +1 -1
  220. package/dist/client/agent-page/AutomationDetailsDialog.d.ts +21 -0
  221. package/dist/client/agent-page/AutomationDetailsDialog.d.ts.map +1 -0
  222. package/dist/client/agent-page/AutomationDetailsDialog.js +33 -0
  223. package/dist/client/agent-page/AutomationDetailsDialog.js.map +1 -0
  224. package/dist/client/agent-page/AutomationScheduleDialog.d.ts +15 -0
  225. package/dist/client/agent-page/AutomationScheduleDialog.d.ts.map +1 -0
  226. package/dist/client/agent-page/AutomationScheduleDialog.js +46 -0
  227. package/dist/client/agent-page/AutomationScheduleDialog.js.map +1 -0
  228. package/dist/client/agent-page/TimezoneSelect.d.ts +17 -0
  229. package/dist/client/agent-page/TimezoneSelect.d.ts.map +1 -0
  230. package/dist/client/agent-page/TimezoneSelect.js +107 -0
  231. package/dist/client/agent-page/TimezoneSelect.js.map +1 -0
  232. package/dist/client/agent-page/use-jobs.d.ts +20 -7
  233. package/dist/client/agent-page/use-jobs.d.ts.map +1 -1
  234. package/dist/client/agent-page/use-jobs.js +24 -4
  235. package/dist/client/agent-page/use-jobs.js.map +1 -1
  236. package/dist/client/org/OrgSwitcher.d.ts +8 -1
  237. package/dist/client/org/OrgSwitcher.d.ts.map +1 -1
  238. package/dist/client/org/OrgSwitcher.js +8 -2
  239. package/dist/client/org/OrgSwitcher.js.map +1 -1
  240. package/dist/client/settings/AccountSettingsCard.d.ts.map +1 -1
  241. package/dist/client/settings/AccountSettingsCard.js +10 -9
  242. package/dist/client/settings/AccountSettingsCard.js.map +1 -1
  243. package/dist/client/settings/SchedulingTimezoneField.d.ts +10 -0
  244. package/dist/client/settings/SchedulingTimezoneField.d.ts.map +1 -0
  245. package/dist/client/settings/SchedulingTimezoneField.js +30 -0
  246. package/dist/client/settings/SchedulingTimezoneField.js.map +1 -0
  247. package/dist/client/settings/SettingsRow.d.ts +36 -0
  248. package/dist/client/settings/SettingsRow.d.ts.map +1 -0
  249. package/dist/client/settings/SettingsRow.js +15 -0
  250. package/dist/client/settings/SettingsRow.js.map +1 -0
  251. package/dist/client/settings/agent-settings-search.d.ts.map +1 -1
  252. package/dist/client/settings/agent-settings-search.js +1 -1
  253. package/dist/client/settings/agent-settings-search.js.map +1 -1
  254. package/dist/client/settings/index.d.ts +1 -0
  255. package/dist/client/settings/index.d.ts.map +1 -1
  256. package/dist/client/settings/index.js +1 -0
  257. package/dist/client/settings/index.js.map +1 -1
  258. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  259. package/dist/integrations/identity.d.ts.map +1 -1
  260. package/dist/integrations/identity.js +2 -11
  261. package/dist/integrations/identity.js.map +1 -1
  262. package/dist/jobs/actions/list-automation-runs.d.ts +8 -0
  263. package/dist/jobs/actions/list-automation-runs.d.ts.map +1 -0
  264. package/dist/jobs/actions/list-automation-runs.js +29 -0
  265. package/dist/jobs/actions/list-automation-runs.js.map +1 -0
  266. package/dist/jobs/actions/list-recurring-jobs.d.ts +2 -0
  267. package/dist/jobs/actions/list-recurring-jobs.d.ts.map +1 -1
  268. package/dist/jobs/actions/list-recurring-jobs.js +20 -7
  269. package/dist/jobs/actions/list-recurring-jobs.js.map +1 -1
  270. package/dist/jobs/actions/manage-recurring-job.d.ts +2 -0
  271. package/dist/jobs/actions/manage-recurring-job.js +30 -9
  272. package/dist/jobs/actions/manage-recurring-job.js.map +1 -1
  273. package/dist/jobs/background-automation-runner.d.ts.map +1 -1
  274. package/dist/jobs/background-automation-runner.js +60 -0
  275. package/dist/jobs/background-automation-runner.js.map +1 -1
  276. package/dist/jobs/cron.d.ts +16 -2
  277. package/dist/jobs/cron.d.ts.map +1 -1
  278. package/dist/jobs/cron.js +43 -6
  279. package/dist/jobs/cron.js.map +1 -1
  280. package/dist/jobs/frontmatter.d.ts +12 -0
  281. package/dist/jobs/frontmatter.d.ts.map +1 -1
  282. package/dist/jobs/frontmatter.js +8 -0
  283. package/dist/jobs/frontmatter.js.map +1 -1
  284. package/dist/jobs/run-history.d.ts +56 -0
  285. package/dist/jobs/run-history.d.ts.map +1 -0
  286. package/dist/jobs/run-history.js +169 -0
  287. package/dist/jobs/run-history.js.map +1 -0
  288. package/dist/jobs/scheduler.d.ts.map +1 -1
  289. package/dist/jobs/scheduler.js +51 -18
  290. package/dist/jobs/scheduler.js.map +1 -1
  291. package/dist/jobs/tools.d.ts.map +1 -1
  292. package/dist/jobs/tools.js +37 -6
  293. package/dist/jobs/tools.js.map +1 -1
  294. package/dist/localization/actions/get-localization-preference.d.ts +1 -2
  295. package/dist/localization/actions/get-localization-preference.d.ts.map +1 -1
  296. package/dist/localization/actions/get-localization-preference.js.map +1 -1
  297. package/dist/localization/actions/set-localization-preference.d.ts +3 -3
  298. package/dist/localization/actions/set-localization-preference.d.ts.map +1 -1
  299. package/dist/localization/actions/set-localization-preference.js +33 -8
  300. package/dist/localization/actions/set-localization-preference.js.map +1 -1
  301. package/dist/localization/default-messages.d.ts +25 -0
  302. package/dist/localization/default-messages.d.ts.map +1 -1
  303. package/dist/localization/default-messages.js +25 -0
  304. package/dist/localization/default-messages.js.map +1 -1
  305. package/dist/localization/shared.d.ts +16 -1
  306. package/dist/localization/shared.d.ts.map +1 -1
  307. package/dist/localization/shared.js +22 -4
  308. package/dist/localization/shared.js.map +1 -1
  309. package/dist/localization/user-timezone.d.ts +11 -0
  310. package/dist/localization/user-timezone.d.ts.map +1 -0
  311. package/dist/localization/user-timezone.js +31 -0
  312. package/dist/localization/user-timezone.js.map +1 -0
  313. package/dist/notifications/routes.d.ts +4 -4
  314. package/dist/observability/routes.d.ts +3 -3
  315. package/dist/org/app-roles-handlers.d.ts.map +1 -1
  316. package/dist/org/app-roles-handlers.js +1 -8
  317. package/dist/org/app-roles-handlers.js.map +1 -1
  318. package/dist/org/index.d.ts +1 -0
  319. package/dist/org/index.d.ts.map +1 -1
  320. package/dist/org/index.js +1 -0
  321. package/dist/org/index.js.map +1 -1
  322. package/dist/org/membership.d.ts +6 -0
  323. package/dist/org/membership.d.ts.map +1 -0
  324. package/dist/org/membership.js +16 -0
  325. package/dist/org/membership.js.map +1 -0
  326. package/dist/progress/routes.d.ts +1 -1
  327. package/dist/provider-api/actions/custom-provider-registration.d.ts +13 -13
  328. package/dist/provider-api/actions/provider-api.d.ts +8 -8
  329. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  330. package/dist/review/actions/create-review-comment.d.ts +32 -1
  331. package/dist/review/actions/create-review-comment.d.ts.map +1 -1
  332. package/dist/review/actions/create-review-comment.js +3 -1
  333. package/dist/review/actions/create-review-comment.js.map +1 -1
  334. package/dist/review/actions/reply-review-comment.d.ts +32 -1
  335. package/dist/review/actions/reply-review-comment.d.ts.map +1 -1
  336. package/dist/review/actions/reply-review-comment.js +3 -1
  337. package/dist/review/actions/reply-review-comment.js.map +1 -1
  338. package/dist/review/index.d.ts +1 -0
  339. package/dist/review/index.d.ts.map +1 -1
  340. package/dist/review/index.js +1 -0
  341. package/dist/review/index.js.map +1 -1
  342. package/dist/review/notifications.d.ts +24 -0
  343. package/dist/review/notifications.d.ts.map +1 -0
  344. package/dist/review/notifications.js +123 -0
  345. package/dist/review/notifications.js.map +1 -0
  346. package/dist/review/types.d.ts +5 -0
  347. package/dist/review/types.d.ts.map +1 -1
  348. package/dist/review/types.js.map +1 -1
  349. package/dist/server/action-discovery.d.ts.map +1 -1
  350. package/dist/server/action-discovery.js +4 -0
  351. package/dist/server/action-discovery.js.map +1 -1
  352. package/dist/server/activity-notifications.d.ts +61 -0
  353. package/dist/server/activity-notifications.d.ts.map +1 -0
  354. package/dist/server/activity-notifications.js +103 -0
  355. package/dist/server/activity-notifications.js.map +1 -0
  356. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  357. package/dist/server/app-name.d.ts +18 -0
  358. package/dist/server/app-name.d.ts.map +1 -1
  359. package/dist/server/app-name.js +31 -0
  360. package/dist/server/app-name.js.map +1 -1
  361. package/dist/server/better-auth-instance.js +4 -4
  362. package/dist/server/better-auth-instance.js.map +1 -1
  363. package/dist/server/email-templates.d.ts +12 -0
  364. package/dist/server/email-templates.d.ts.map +1 -1
  365. package/dist/server/email-templates.js +57 -14
  366. package/dist/server/email-templates.js.map +1 -1
  367. package/dist/server/email.d.ts +12 -0
  368. package/dist/server/email.d.ts.map +1 -1
  369. package/dist/server/email.js +52 -5
  370. package/dist/server/email.js.map +1 -1
  371. package/dist/server/index.d.ts +1 -0
  372. package/dist/server/index.d.ts.map +1 -1
  373. package/dist/server/index.js +1 -0
  374. package/dist/server/index.js.map +1 -1
  375. package/dist/server/transcribe-voice.d.ts +1 -1
  376. package/dist/sharing/access.d.ts.map +1 -1
  377. package/dist/sharing/access.js +32 -2
  378. package/dist/sharing/access.js.map +1 -1
  379. package/dist/sharing/index.d.ts +1 -0
  380. package/dist/sharing/index.d.ts.map +1 -1
  381. package/dist/sharing/index.js +1 -0
  382. package/dist/sharing/index.js.map +1 -1
  383. package/dist/sharing/recipients.d.ts +31 -0
  384. package/dist/sharing/recipients.d.ts.map +1 -0
  385. package/dist/sharing/recipients.js +40 -0
  386. package/dist/sharing/recipients.js.map +1 -0
  387. package/dist/templates/chat/app/routes/settings.tsx +14 -22
  388. package/dist/templates/chat/vitest.config.ts +9 -0
  389. package/dist/triggers/actions/list-automations.d.ts +2 -0
  390. package/dist/triggers/actions/list-automations.d.ts.map +1 -1
  391. package/dist/triggers/actions/list-automations.js +19 -8
  392. package/dist/triggers/actions/list-automations.js.map +1 -1
  393. package/dist/triggers/actions/manage-automation.d.ts +6 -0
  394. package/dist/triggers/actions/manage-automation.js +12 -6
  395. package/dist/triggers/actions/manage-automation.js.map +1 -1
  396. package/dist/triggers/actions.d.ts.map +1 -1
  397. package/dist/triggers/actions.js +15 -4
  398. package/dist/triggers/actions.js.map +1 -1
  399. package/dist/triggers/dispatcher.d.ts.map +1 -1
  400. package/dist/triggers/dispatcher.js +19 -17
  401. package/dist/triggers/dispatcher.js.map +1 -1
  402. package/dist/triggers/routes.d.ts.map +1 -1
  403. package/dist/triggers/routes.js +6 -6
  404. package/dist/triggers/routes.js.map +1 -1
  405. package/dist/vitest-config.d.ts +10 -0
  406. package/dist/vitest-config.d.ts.map +1 -0
  407. package/dist/vitest-config.js +55 -0
  408. package/dist/vitest-config.js.map +1 -0
  409. package/package.json +5 -4
  410. package/src/automations/service.ts +41 -4
  411. package/src/cli/mcp-config-writers.ts +27 -21
  412. package/src/client/agent-page/AgentJobsTab.tsx +187 -85
  413. package/src/client/agent-page/AutomationDetailsDialog.tsx +208 -0
  414. package/src/client/agent-page/AutomationScheduleDialog.tsx +180 -0
  415. package/src/client/agent-page/TimezoneSelect.tsx +241 -0
  416. package/src/client/agent-page/use-jobs.ts +50 -10
  417. package/src/client/org/OrgSwitcher.tsx +26 -0
  418. package/src/client/settings/AccountSettingsCard.tsx +35 -31
  419. package/src/client/settings/SchedulingTimezoneField.tsx +71 -0
  420. package/src/client/settings/SettingsRow.tsx +113 -0
  421. package/src/client/settings/agent-settings-search.ts +2 -1
  422. package/src/client/settings/index.ts +6 -0
  423. package/src/integrations/identity.ts +2 -12
  424. package/src/jobs/actions/list-automation-runs.ts +33 -0
  425. package/src/jobs/actions/list-recurring-jobs.ts +27 -6
  426. package/src/jobs/actions/manage-recurring-job.ts +41 -9
  427. package/src/jobs/background-automation-runner.ts +88 -0
  428. package/src/jobs/cron.ts +52 -6
  429. package/src/jobs/frontmatter.ts +20 -0
  430. package/src/jobs/run-history.ts +242 -0
  431. package/src/jobs/scheduler.ts +73 -18
  432. package/src/jobs/tools.ts +54 -6
  433. package/src/localization/actions/get-localization-preference.ts +2 -2
  434. package/src/localization/actions/set-localization-preference.ts +49 -11
  435. package/src/localization/default-messages.ts +27 -0
  436. package/src/localization/shared.ts +39 -6
  437. package/src/localization/user-timezone.ts +36 -0
  438. package/src/org/app-roles-handlers.ts +1 -9
  439. package/src/org/index.ts +2 -0
  440. package/src/org/membership.ts +18 -0
  441. package/src/review/actions/create-review-comment.ts +4 -1
  442. package/src/review/actions/reply-review-comment.ts +4 -1
  443. package/src/review/index.ts +5 -0
  444. package/src/review/notifications.ts +163 -0
  445. package/src/review/types.ts +7 -0
  446. package/src/server/action-discovery.ts +4 -0
  447. package/src/server/activity-notifications.ts +174 -0
  448. package/src/server/app-name.ts +31 -0
  449. package/src/server/better-auth-instance.ts +4 -4
  450. package/src/server/email-templates.ts +70 -14
  451. package/src/server/email.ts +66 -3
  452. package/src/server/index.ts +10 -0
  453. package/src/sharing/access.ts +44 -2
  454. package/src/sharing/index.ts +5 -0
  455. package/src/sharing/recipients.ts +67 -0
  456. package/src/templates/chat/app/routes/settings.tsx +14 -22
  457. package/src/templates/chat/vitest.config.ts +9 -0
  458. package/src/triggers/actions/list-automations.ts +27 -8
  459. package/src/triggers/actions/manage-automation.ts +17 -6
  460. package/src/triggers/actions.ts +16 -4
  461. package/src/triggers/dispatcher.ts +44 -30
  462. package/src/triggers/routes.ts +19 -6
  463. package/src/vitest-config.ts +68 -0
@@ -0,0 +1,258 @@
1
+ /**
2
+ * Generate and attach a timeline filmstrip sprite to a stored recording.
3
+ *
4
+ * This is the sibling of `ensure-seekable-video.ts` and follows the same shape:
5
+ * re-fetch the stored provider media, run one ffmpeg pass, upload the result,
6
+ * and point the recording row at it. Nothing here is destructive — a recording
7
+ * without a sprite still renders a filmstrip, because the editor falls back to
8
+ * extracting frames in the browser.
9
+ *
10
+ * Local/dev media (relative `/api/video/...` URLs backed by application_state)
11
+ * is skipped rather than special-cased: there is no absolute URL to fetch
12
+ * server-side, and the browser fallback already covers that environment.
13
+ */
14
+
15
+ import {
16
+ readAppState,
17
+ writeAppState,
18
+ } from "@agent-native/core/application-state";
19
+ import { uploadFile } from "@agent-native/core/file-upload";
20
+ import { and, eq, isNull } from "drizzle-orm";
21
+
22
+ import { getDb, schema } from "../../server/db/index.js";
23
+ import { deleteRecordingMediaObjects } from "../../server/lib/recording-media-cleanup.js";
24
+ import { ownerEmailMatches } from "../../server/lib/recordings.js";
25
+ import {
26
+ DEFAULT_FILMSTRIP_FRAME_COUNT,
27
+ generateFilmstripSprite,
28
+ type FilmstripSpriteGrid,
29
+ } from "../../server/lib/video-filmstrip-sprite.js";
30
+ import { isLoomRecordingSource } from "../../shared/loom.js";
31
+ import {
32
+ fetchProviderBytes,
33
+ isRemoteProviderUrl,
34
+ } from "./ensure-seekable-video.js";
35
+
36
+ export type EnsureFilmstripStatus =
37
+ | "generated"
38
+ | "already-generated"
39
+ | "not-found"
40
+ | "skipped-not-ready"
41
+ | "skipped-no-media"
42
+ | "skipped-local-media"
43
+ | "skipped-no-duration"
44
+ | "skipped-no-ffmpeg"
45
+ | "skipped-fetch-failed"
46
+ | "skipped-too-large"
47
+ | "skipped-upload-failed"
48
+ | "failed-ffmpeg"
49
+ | "failed-empty-output";
50
+
51
+ export interface EnsureFilmstripResult {
52
+ recordingId: string;
53
+ status: EnsureFilmstripStatus;
54
+ changed: boolean;
55
+ filmstripUrl?: string | null;
56
+ grid?: FilmstripSpriteGrid;
57
+ detail?: string;
58
+ }
59
+
60
+ /** application_state key recording which media a sprite was generated from. */
61
+ export function filmstripMarkerKey(recordingId: string): string {
62
+ return `recording-filmstrip-${recordingId}`;
63
+ }
64
+
65
+ async function isAlreadyGenerated(
66
+ recordingId: string,
67
+ videoUrl: string,
68
+ ): Promise<boolean> {
69
+ const marker = await readAppState(filmstripMarkerKey(recordingId)).catch(
70
+ () => null,
71
+ );
72
+ return Boolean(
73
+ marker &&
74
+ typeof marker === "object" &&
75
+ (marker as { videoUrl?: unknown }).videoUrl === videoUrl,
76
+ );
77
+ }
78
+
79
+ /**
80
+ * Ensure one recording has a filmstrip sprite. Owner-scoped: pass the resolved
81
+ * owner email so the lookup can only touch that owner's rows.
82
+ */
83
+ export async function ensureRecordingFilmstrip(params: {
84
+ recordingId: string;
85
+ ownerEmail: string;
86
+ frameCount?: number;
87
+ force?: boolean;
88
+ }): Promise<EnsureFilmstripResult> {
89
+ const {
90
+ recordingId,
91
+ ownerEmail,
92
+ frameCount = DEFAULT_FILMSTRIP_FRAME_COUNT,
93
+ force = false,
94
+ } = params;
95
+ const db = getDb();
96
+
97
+ const [rec] = await db
98
+ .select({
99
+ id: schema.recordings.id,
100
+ status: schema.recordings.status,
101
+ videoUrl: schema.recordings.videoUrl,
102
+ durationMs: schema.recordings.durationMs,
103
+ filmstripUrl: schema.recordings.filmstripUrl,
104
+ sourceAppName: schema.recordings.sourceAppName,
105
+ })
106
+ .from(schema.recordings)
107
+ .where(
108
+ and(
109
+ eq(schema.recordings.id, recordingId),
110
+ ownerEmailMatches(schema.recordings.ownerEmail, ownerEmail),
111
+ ),
112
+ );
113
+
114
+ if (!rec) return { recordingId, status: "not-found", changed: false };
115
+ if (rec.status !== "ready") {
116
+ return { recordingId, status: "skipped-not-ready", changed: false };
117
+ }
118
+ if (isLoomRecordingSource(rec)) {
119
+ return { recordingId, status: "skipped-local-media", changed: false };
120
+ }
121
+ if (!rec.videoUrl) {
122
+ return { recordingId, status: "skipped-no-media", changed: false };
123
+ }
124
+ if (!isRemoteProviderUrl(rec.videoUrl)) {
125
+ return {
126
+ recordingId,
127
+ status: "skipped-local-media",
128
+ changed: false,
129
+ filmstripUrl: rec.filmstripUrl,
130
+ };
131
+ }
132
+ if (rec.durationMs <= 0) {
133
+ return { recordingId, status: "skipped-no-duration", changed: false };
134
+ }
135
+
136
+ if (
137
+ !force &&
138
+ rec.filmstripUrl &&
139
+ (await isAlreadyGenerated(recordingId, rec.videoUrl))
140
+ ) {
141
+ return {
142
+ recordingId,
143
+ status: "already-generated",
144
+ changed: false,
145
+ filmstripUrl: rec.filmstripUrl,
146
+ };
147
+ }
148
+
149
+ const fetched = await fetchProviderBytes(rec.videoUrl);
150
+ if (!fetched.ok) {
151
+ return { recordingId, status: fetched.reason, changed: false };
152
+ }
153
+
154
+ const sprite = await generateFilmstripSprite({
155
+ mediaBytes: fetched.bytes,
156
+ durationMs: rec.durationMs,
157
+ frameCount,
158
+ });
159
+
160
+ if (sprite.status !== "generated" || !sprite.sprite) {
161
+ return {
162
+ recordingId,
163
+ status: sprite.status,
164
+ changed: false,
165
+ filmstripUrl: rec.filmstripUrl,
166
+ detail: sprite.detail,
167
+ };
168
+ }
169
+
170
+ const upload = await uploadFile({
171
+ data: sprite.sprite.bytes,
172
+ filename: `${recordingId}-filmstrip-${Date.now()}.jpg`,
173
+ mimeType: "image/jpeg",
174
+ ownerEmail,
175
+ stableUrl: true,
176
+ recordAsset: false,
177
+ }).catch((err) => {
178
+ console.warn("[ensure-recording-filmstrip] sprite upload failed", {
179
+ recordingId,
180
+ error: err instanceof Error ? err.message : String(err),
181
+ });
182
+ return null;
183
+ });
184
+
185
+ if (!upload?.url) {
186
+ return {
187
+ recordingId,
188
+ status: "skipped-upload-failed",
189
+ changed: false,
190
+ filmstripUrl: rec.filmstripUrl,
191
+ };
192
+ }
193
+
194
+ const { grid } = sprite.sprite;
195
+ const updated = await db
196
+ .update(schema.recordings)
197
+ .set({
198
+ filmstripUrl: upload.url,
199
+ filmstripFrameCount: grid.frameCount,
200
+ filmstripColumns: grid.columns,
201
+ filmstripRows: grid.rows,
202
+ filmstripFrameWidth: grid.frameWidth,
203
+ filmstripFrameHeight: grid.frameHeight,
204
+ updatedAt: new Date().toISOString(),
205
+ })
206
+ .where(
207
+ and(
208
+ eq(schema.recordings.id, recordingId),
209
+ ownerEmailMatches(schema.recordings.ownerEmail, ownerEmail),
210
+ // Don't attach a sprite generated from media the row no longer points at.
211
+ eq(schema.recordings.videoUrl, rec.videoUrl),
212
+ // Ensure no concurrent job modified filmstripUrl in the interim.
213
+ rec.filmstripUrl
214
+ ? eq(schema.recordings.filmstripUrl, rec.filmstripUrl)
215
+ : isNull(schema.recordings.filmstripUrl),
216
+ ),
217
+ )
218
+ .returning({
219
+ id: schema.recordings.id,
220
+ filmstripUrl: schema.recordings.filmstripUrl,
221
+ });
222
+
223
+ if (updated.length !== 1 || updated[0]?.filmstripUrl !== upload.url) {
224
+ await deleteRecordingMediaObjects({
225
+ id: recordingId,
226
+ filmstripUrl: upload.url,
227
+ }).catch(() => {});
228
+ return {
229
+ recordingId,
230
+ status: "skipped-upload-failed",
231
+ changed: false,
232
+ filmstripUrl: rec.filmstripUrl,
233
+ detail: "Recording changed while the filmstrip sprite was uploading.",
234
+ };
235
+ }
236
+
237
+ if (rec.filmstripUrl && rec.filmstripUrl !== upload.url) {
238
+ await deleteRecordingMediaObjects({
239
+ id: recordingId,
240
+ filmstripUrl: rec.filmstripUrl,
241
+ }).catch(() => {});
242
+ }
243
+
244
+ await writeAppState(filmstripMarkerKey(recordingId), {
245
+ recordingId,
246
+ videoUrl: rec.videoUrl,
247
+ at: new Date().toISOString(),
248
+ });
249
+ await writeAppState("refresh-signal", { ts: Date.now() });
250
+
251
+ return {
252
+ recordingId,
253
+ status: "generated",
254
+ changed: true,
255
+ filmstripUrl: upload.url,
256
+ grid,
257
+ };
258
+ }
@@ -95,14 +95,22 @@ async function isAlreadyMarked(
95
95
  );
96
96
  }
97
97
 
98
- function isRemoteProviderUrl(videoUrl: string | null | undefined): boolean {
98
+ export function isRemoteProviderUrl(
99
+ videoUrl: string | null | undefined,
100
+ ): boolean {
99
101
  return typeof videoUrl === "string" && /^https:\/\//i.test(videoUrl.trim());
100
102
  }
101
103
 
102
- async function fetchProviderBytes(
104
+ /**
105
+ * Download stored provider media for server-side processing. Shared with the
106
+ * filmstrip sprite path — both need the same size cap and timeout, and both
107
+ * report the same two failure reasons rather than an empty buffer.
108
+ */
109
+ export async function fetchProviderBytes(
103
110
  videoUrl: string,
104
111
  ): Promise<
105
- { ok: true; bytes: Uint8Array } | { ok: false; reason: EnsureSeekableStatus }
112
+ | { ok: true; bytes: Uint8Array }
113
+ | { ok: false; reason: "skipped-fetch-failed" | "skipped-too-large" }
106
114
  > {
107
115
  const controller = new AbortController();
108
116
  const timeout = setTimeout(
@@ -17,6 +17,7 @@ import { and, eq } from "drizzle-orm";
17
17
  import { z } from "zod";
18
18
 
19
19
  import { getDb, schema } from "../server/db/index.js";
20
+ import { notifyRecordingReaction } from "../server/lib/activity-notifications.js";
20
21
 
21
22
  const MAX_CAS_ATTEMPTS = 3;
22
23
 
@@ -90,12 +91,24 @@ export default defineAction({
90
91
  .returning({ id: schema.recordingComments.id });
91
92
 
92
93
  if (updated.length > 0) {
94
+ // Removing a reaction is not an event worth emailing about.
95
+ const notified = had
96
+ ? null
97
+ : await notifyRecordingReaction({
98
+ recordingId: existing.recordingId,
99
+ emoji: args.emoji,
100
+ viewerEmail,
101
+ videoTimestampMs: existing.videoTimestampMs,
102
+ extraRecipients: [existing.authorEmail],
103
+ });
104
+
93
105
  await writeAppState("refresh-signal", { ts: Date.now() });
94
106
  return {
95
107
  id: args.commentId,
96
108
  emoji: args.emoji,
97
109
  reacted: !had,
98
110
  reactions: next,
111
+ notified,
99
112
  };
100
113
  }
101
114
  }
@@ -12,6 +12,7 @@ import { assertAccess } from "@agent-native/core/sharing";
12
12
  import { z } from "zod";
13
13
 
14
14
  import { getDb, schema } from "../server/db/index.js";
15
+ import { notifyRecordingReaction } from "../server/lib/activity-notifications.js";
15
16
  import { nanoid } from "../server/lib/recordings.js";
16
17
 
17
18
  export default defineAction({
@@ -50,8 +51,16 @@ export default defineAction({
50
51
  createdAt: now,
51
52
  });
52
53
 
54
+ const notified = await notifyRecordingReaction({
55
+ recordingId: args.recordingId,
56
+ emoji: args.emoji,
57
+ viewerEmail,
58
+ viewerName: args.viewerName,
59
+ videoTimestampMs: args.videoTimestampMs,
60
+ });
61
+
53
62
  await writeAppState("refresh-signal", { ts: Date.now() });
54
63
 
55
- return { id };
64
+ return { id, notified };
56
65
  },
57
66
  });
@@ -15,6 +15,7 @@ import { eq } from "drizzle-orm";
15
15
  import { z } from "zod";
16
16
 
17
17
  import { getDb, schema } from "../server/db/index.js";
18
+ import { notifyRecordingComment } from "../server/lib/activity-notifications.js";
18
19
  import { nanoid } from "../server/lib/recordings.js";
19
20
 
20
21
  export default defineAction({
@@ -58,12 +59,22 @@ export default defineAction({
58
59
  updatedAt: now,
59
60
  });
60
61
 
62
+ const notified = await notifyRecordingComment({
63
+ recordingId: parent.recordingId,
64
+ threadId: parent.threadId,
65
+ authorEmail,
66
+ authorName: args.authorName,
67
+ content: args.content,
68
+ videoTimestampMs: parent.videoTimestampMs,
69
+ isReply: true,
70
+ });
71
+
61
72
  await writeAppState("refresh-signal", { ts: Date.now() });
62
73
 
63
74
  console.log(
64
75
  `Replied to comment ${args.commentId} (thread: ${parent.threadId})`,
65
76
  );
66
77
 
67
- return { id, threadId: parent.threadId, parentId: parent.id };
78
+ return { id, threadId: parent.threadId, parentId: parent.id, notified };
68
79
  },
69
80
  });
@@ -36,7 +36,7 @@ import { parseEdits, serializeEdits } from "../app/lib/timestamp-mapping.js";
36
36
  import { getDb, schema } from "../server/db/index.js";
37
37
  import {
38
38
  getCurrentOwnerEmail,
39
- getOrganizationDefaultVisibility,
39
+ getDefaultRecordingVisibility,
40
40
  nanoid,
41
41
  ownerEmailMatches,
42
42
  } from "../server/lib/recordings.js";
@@ -121,7 +121,7 @@ export default defineAction({
121
121
  }
122
122
  const organizationId = ordered[0].organizationId;
123
123
  const defaultVisibility =
124
- await getOrganizationDefaultVisibility(organizationId);
124
+ await getDefaultRecordingVisibility(organizationId);
125
125
 
126
126
  const totalDuration =
127
127
  args.durationMs ??
@@ -58,6 +58,11 @@ import {
58
58
  type EditsJson,
59
59
  } from "@/lib/timestamp-mapping";
60
60
  import { cn } from "@/lib/utils";
61
+ import {
62
+ extractFilmstripThumbnails,
63
+ type FilmstripFrame,
64
+ type FilmstripSprite,
65
+ } from "@/lib/video-filmstrip";
61
66
  import { computePeaks, type WaveformPeaks } from "@/lib/waveform-peaks";
62
67
 
63
68
  import { ChaptersEditor } from "./chapters-editor";
@@ -76,7 +81,7 @@ export interface EditorLayoutProps {
76
81
  className?: string;
77
82
  }
78
83
 
79
- const WAVEFORM_HEIGHT = 120;
84
+ const WAVEFORM_HEIGHT = 100;
80
85
  const MIN_TIMELINE_ZOOM = 1;
81
86
  const MAX_TIMELINE_ZOOM = 50;
82
87
 
@@ -244,10 +249,20 @@ export function EditorLayout({ recordingId, className }: EditorLayoutProps) {
244
249
  return () => ro.disconnect();
245
250
  }, []);
246
251
 
247
- const totalWidth = Math.max(
248
- viewportWidth,
249
- Math.floor(viewportWidth * Math.max(1, zoom)),
250
- );
252
+ const baseTrackWidth = useMemo(() => {
253
+ if (durationMs <= 0 || viewportWidth <= 0) return viewportWidth;
254
+ const durationSec = durationMs / 1000;
255
+ // Scale timeline track width proportionally with duration so long videos scale & scroll (bounded to safe max allocation)
256
+ const durationScaledPx = Math.min(8192, Math.round(durationSec * 14));
257
+ return Math.max(viewportWidth, durationScaledPx);
258
+ }, [durationMs, viewportWidth]);
259
+
260
+ const totalWidth = useMemo(() => {
261
+ return Math.max(
262
+ baseTrackWidth,
263
+ Math.floor(baseTrackWidth * Math.max(1, zoom)),
264
+ );
265
+ }, [baseTrackWidth, zoom]);
251
266
 
252
267
  const calculateAnchoredScrollLeft = useCallback(
253
268
  (
@@ -255,8 +270,8 @@ export function EditorLayout({ recordingId, className }: EditorLayoutProps) {
255
270
  anchor?: { anchorRatio?: number; viewportX?: number },
256
271
  ) => {
257
272
  const nextTotalWidth = Math.max(
258
- viewportWidth,
259
- Math.floor(viewportWidth * Math.max(1, nextZoom)),
273
+ baseTrackWidth,
274
+ Math.floor(baseTrackWidth * Math.max(1, nextZoom)),
260
275
  );
261
276
  const maxScrollLeft = Math.max(0, nextTotalWidth - viewportWidth);
262
277
  const anchorMs = selectionRange
@@ -277,7 +292,7 @@ export function EditorLayout({ recordingId, className }: EditorLayoutProps) {
277
292
  const anchorX = anchorRatio * nextTotalWidth;
278
293
  return Math.max(0, Math.min(maxScrollLeft, anchorX - viewportX));
279
294
  },
280
- [durationMs, playheadMs, selectionRange, viewportWidth],
295
+ [baseTrackWidth, durationMs, playheadMs, selectionRange, viewportWidth],
281
296
  );
282
297
 
283
298
  const setAnchoredZoom = useCallback(
@@ -319,8 +334,8 @@ export function EditorLayout({ recordingId, className }: EditorLayoutProps) {
319
334
  viewportX: number,
320
335
  ) => {
321
336
  const sourceTotalWidth = Math.max(
322
- viewportWidth,
323
- Math.floor(viewportWidth * Math.max(1, sourceZoom)),
337
+ baseTrackWidth,
338
+ Math.floor(baseTrackWidth * Math.max(1, sourceZoom)),
324
339
  );
325
340
  return Math.max(
326
341
  0,
@@ -390,7 +405,7 @@ export function EditorLayout({ recordingId, className }: EditorLayoutProps) {
390
405
  el.removeEventListener("gesturechange", handleGestureChange);
391
406
  el.removeEventListener("gestureend", handleGestureEnd);
392
407
  };
393
- }, [scrollLeft, setAnchoredZoom, viewportWidth, zoom]);
408
+ }, [baseTrackWidth, scrollLeft, setAnchoredZoom, viewportWidth, zoom]);
394
409
 
395
410
  // Sync the <video> to play state.
396
411
  useEffect(() => {
@@ -498,6 +513,96 @@ export function EditorLayout({ recordingId, className }: EditorLayoutProps) {
498
513
  };
499
514
  }, [recordingId, waveformMediaUrl]);
500
515
 
516
+ // Filmstrip drawn behind the waveform. A server-generated sprite is one
517
+ // cached image request and is preferred; browser extraction is the fallback
518
+ // for hosts without ffmpeg and for local/dev media the server can't fetch.
519
+ const filmstripSprite = useMemo<FilmstripSprite | null>(() => {
520
+ const url = recording?.filmstripUrl;
521
+ const frameCount = Number(recording?.filmstripFrameCount ?? 0);
522
+ const columns = Number(recording?.filmstripColumns ?? 0);
523
+ if (!url || frameCount <= 0 || columns <= 0) return null;
524
+ return {
525
+ url,
526
+ frameCount,
527
+ columns,
528
+ rows: Number(recording?.filmstripRows ?? 1) || 1,
529
+ frameWidth: Number(recording?.filmstripFrameWidth ?? 0) || 160,
530
+ frameHeight: Number(recording?.filmstripFrameHeight ?? 0) || 90,
531
+ };
532
+ }, [
533
+ recording?.filmstripUrl,
534
+ recording?.filmstripFrameCount,
535
+ recording?.filmstripColumns,
536
+ recording?.filmstripRows,
537
+ recording?.filmstripFrameWidth,
538
+ recording?.filmstripFrameHeight,
539
+ ]);
540
+
541
+ const [filmstripFrames, setFilmstripFrames] = useState<FilmstripFrame[]>([]);
542
+
543
+ useEffect(() => {
544
+ setFilmstripFrames([]);
545
+ }, [recordingId]);
546
+
547
+ // Cells should read as video frames, so aim for one per `height * aspect` of
548
+ // track. Bucketed so ordinary window resizing does not re-extract, and based
549
+ // on the unzoomed width — a zoomed fallback strip stretches, which is one of
550
+ // the reasons the server sprite is the preferred path.
551
+ const filmstripFrameCount = useMemo(() => {
552
+ const bucketedWidth = Math.max(240, Math.round(viewportWidth / 120) * 120);
553
+ const cellWidth = WAVEFORM_HEIGHT * (16 / 9);
554
+ return Math.min(48, Math.max(6, Math.round(bucketedWidth / cellWidth)));
555
+ }, [viewportWidth]);
556
+
557
+ useEffect(() => {
558
+ // A sprite already covers the whole clip — don't decode the video again.
559
+ if (filmstripSprite) {
560
+ setFilmstripFrames([]);
561
+ return;
562
+ }
563
+ if (!waveformMediaUrl || durationMs <= 0) {
564
+ setFilmstripFrames([]);
565
+ return;
566
+ }
567
+ setFilmstripFrames([]);
568
+ let cancelled = false;
569
+ // `waveformMediaUrl`, not `videoUrl`: reading frames back out of a
570
+ // cross-origin video taints the canvas, so provider media must come
571
+ // through the same-origin proxy first.
572
+ extractFilmstripThumbnails({
573
+ videoUrl: waveformMediaUrl,
574
+ durationMs,
575
+ frameCount: filmstripFrameCount,
576
+ })
577
+ .then((result) => {
578
+ if (cancelled) return;
579
+ if (result.status !== "ok") {
580
+ console.warn("[editor] filmstrip extraction failed", {
581
+ recordingId,
582
+ status: result.status,
583
+ detail: result.detail,
584
+ });
585
+ }
586
+ setFilmstripFrames(result.frames);
587
+ })
588
+ .catch((err) => {
589
+ if (cancelled) return;
590
+ console.warn("[editor] filmstrip extraction threw", {
591
+ recordingId,
592
+ error: err instanceof Error ? err.message : String(err),
593
+ });
594
+ });
595
+ return () => {
596
+ cancelled = true;
597
+ };
598
+ }, [
599
+ recordingId,
600
+ waveformMediaUrl,
601
+ durationMs,
602
+ filmstripFrameCount,
603
+ filmstripSprite,
604
+ ]);
605
+
501
606
  // --- actions ------------------------------------------------------------
502
607
  const trim = useActionMutation("trim-recording");
503
608
  const split = useActionMutation("split-recording");
@@ -550,12 +655,22 @@ export function EditorLayout({ recordingId, className }: EditorLayoutProps) {
550
655
  ) {
551
656
  e.preventDefault();
552
657
  undo.mutate({ recordingId });
553
- } else if (e.key.toLowerCase() === "i") {
658
+ } else if (
659
+ !e.ctrlKey &&
660
+ !e.metaKey &&
661
+ !e.altKey &&
662
+ e.key.toLowerCase() === "i"
663
+ ) {
554
664
  setSelectionRange((r) => ({
555
665
  startMs: playheadMs,
556
666
  endMs: r?.endMs && r.endMs > playheadMs ? r.endMs : playheadMs + 1000,
557
667
  }));
558
- } else if (e.key.toLowerCase() === "o") {
668
+ } else if (
669
+ !e.ctrlKey &&
670
+ !e.metaKey &&
671
+ !e.altKey &&
672
+ e.key.toLowerCase() === "o"
673
+ ) {
559
674
  setSelectionRange((r) => ({
560
675
  startMs:
561
676
  r?.startMs && r.startMs < playheadMs
@@ -563,7 +678,12 @@ export function EditorLayout({ recordingId, className }: EditorLayoutProps) {
563
678
  : Math.max(0, playheadMs - 1000),
564
679
  endMs: playheadMs,
565
680
  }));
566
- } else if (e.key.toLowerCase() === "x") {
681
+ } else if (
682
+ !e.ctrlKey &&
683
+ !e.metaKey &&
684
+ !e.altKey &&
685
+ e.key.toLowerCase() === "x"
686
+ ) {
567
687
  // Cut: trim the current selection range
568
688
  const range = selectionRange;
569
689
  if (range) {
@@ -582,7 +702,12 @@ export function EditorLayout({ recordingId, className }: EditorLayoutProps) {
582
702
  toast.error(err?.message ?? t("editorLayout.cutFailed")),
583
703
  );
584
704
  }
585
- } else if (e.key.toLowerCase() === "s") {
705
+ } else if (
706
+ !e.ctrlKey &&
707
+ !e.metaKey &&
708
+ !e.altKey &&
709
+ e.key.toLowerCase() === "s"
710
+ ) {
586
711
  // Split at playhead
587
712
  e.preventDefault();
588
713
  split
@@ -693,6 +818,8 @@ export function EditorLayout({ recordingId, className }: EditorLayoutProps) {
693
818
  <div className="relative min-w-0 overflow-hidden">
694
819
  <Waveform
695
820
  peaks={peaks}
821
+ sprite={filmstripSprite}
822
+ frames={filmstripFrames}
696
823
  width={viewportWidth}
697
824
  height={WAVEFORM_HEIGHT}
698
825
  zoom={zoom}
@@ -717,14 +844,16 @@ export function EditorLayout({ recordingId, className }: EditorLayoutProps) {
717
844
  transform: `translateX(${-scrollLeft}px)`,
718
845
  }}
719
846
  >
720
- <TrimHandles
721
- width={totalWidth}
722
- height={WAVEFORM_HEIGHT}
723
- value={effectiveSelection}
724
- onChange={setSelectionRange}
725
- durationMs={durationMs}
726
- splitPoints={splitPoints}
727
- />
847
+ {durationMs > 0 && (
848
+ <TrimHandles
849
+ width={totalWidth}
850
+ height={WAVEFORM_HEIGHT}
851
+ value={effectiveSelection}
852
+ onChange={setSelectionRange}
853
+ durationMs={durationMs}
854
+ splitPoints={splitPoints}
855
+ />
856
+ )}
728
857
  </div>
729
858
  </div>
730
859
  </div>