@agent-native/core 0.91.1 → 0.91.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (209) hide show
  1. package/blueprints/action/crud.md +1 -1
  2. package/blueprints/channel/discord.md +1 -1
  3. package/blueprints/provider/stripe.md +1 -1
  4. package/blueprints/sandbox/docker.md +1 -1
  5. package/corpus/README.md +1 -1
  6. package/corpus/core/CHANGELOG.md +14 -0
  7. package/corpus/core/blueprints/action/crud.md +1 -1
  8. package/corpus/core/blueprints/channel/discord.md +1 -1
  9. package/corpus/core/blueprints/provider/stripe.md +1 -1
  10. package/corpus/core/blueprints/sandbox/docker.md +1 -1
  11. package/corpus/core/package.json +6 -5
  12. package/corpus/core/src/agent/run-manager.ts +23 -2
  13. package/corpus/core/src/agent/run-store.ts +2 -2
  14. package/corpus/core/src/cli/add.ts +1 -1
  15. package/corpus/core/src/cli/create.ts +8 -3
  16. package/corpus/core/src/cli/index.ts +5 -4
  17. package/corpus/core/src/client/extensions/ExtensionSlot.tsx +7 -0
  18. package/corpus/core/src/client/session-replay.ts +97 -3
  19. package/corpus/core/src/jobs/scheduler.ts +30 -1
  20. package/corpus/core/src/localization/default-messages.ts +21 -0
  21. package/corpus/core/src/templates/default/app/i18n/ar-SA.ts +21 -0
  22. package/corpus/core/src/templates/default/app/i18n/de-DE.ts +21 -0
  23. package/corpus/core/src/templates/default/app/i18n/en-US.ts +21 -0
  24. package/corpus/core/src/templates/default/app/i18n/es-ES.ts +21 -0
  25. package/corpus/core/src/templates/default/app/i18n/fr-FR.ts +21 -0
  26. package/corpus/core/src/templates/default/app/i18n/hi-IN.ts +20 -0
  27. package/corpus/core/src/templates/default/app/i18n/ja-JP.ts +21 -0
  28. package/corpus/core/src/templates/default/app/i18n/ko-KR.ts +20 -0
  29. package/corpus/core/src/templates/default/app/i18n/pt-BR.ts +21 -0
  30. package/corpus/core/src/templates/default/app/i18n/zh-CN.ts +20 -0
  31. package/corpus/core/src/templates/default/app/i18n/zh-TW.ts +19 -0
  32. package/corpus/core/src/templates/default/package.json +2 -2
  33. package/corpus/core/src/templates/headless/package.json +3 -3
  34. package/corpus/core/src/templates/workspace-core/package.json +6 -6
  35. package/corpus/core/src/templates/workspace-root/package.json +2 -2
  36. package/corpus/core/src/triggers/dispatcher.ts +30 -2
  37. package/corpus/core/src/vite/client.ts +96 -1
  38. package/corpus/templates/analytics/.agents/skills/session-replay/SKILL.md +21 -2
  39. package/corpus/templates/analytics/app/global.css +26 -10
  40. package/corpus/templates/analytics/app/i18n/zh-TW.ts +2 -0
  41. package/corpus/templates/analytics/app/i18n-data.ts +25 -0
  42. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +548 -237
  43. package/corpus/templates/analytics/app/pages/sessions/SessionDevToolsPanel.tsx +285 -294
  44. package/corpus/templates/analytics/changelog/2026-07-08-console-details-expand-inline-under-each-row-and-the-event-t.md +6 -0
  45. package/corpus/templates/analytics/changelog/2026-07-08-session-replay-clamps-ultra-wide-viewports-while-keeping-the.md +6 -0
  46. package/corpus/templates/analytics/changelog/2026-07-08-session-replay-keeps-a-usable-stage-when-dev-tools-is-open-a.md +6 -0
  47. package/corpus/templates/analytics/changelog/2026-07-08-session-replay-resumes-after-scrubbing-and-expands-console-r.md +6 -0
  48. package/corpus/templates/analytics/changelog/2026-07-08-session-replays-wait-for-full-load-keep-a-normal-viewport-an.md +6 -0
  49. package/corpus/templates/analytics/package.json +2 -2
  50. package/corpus/templates/assets/package.json +2 -2
  51. package/corpus/templates/brain/package.json +2 -2
  52. package/corpus/templates/calendar/AGENTS.md +10 -0
  53. package/corpus/templates/calendar/actions/create-event.ts +5 -1
  54. package/corpus/templates/calendar/actions/event-action-helpers.ts +41 -0
  55. package/corpus/templates/calendar/actions/get-attendee-timezones.ts +28 -0
  56. package/corpus/templates/calendar/actions/set-attendee-timezone.ts +68 -0
  57. package/corpus/templates/calendar/app/components/calendar/EventAttendeesSection.tsx +150 -11
  58. package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +36 -1
  59. package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +32 -0
  60. package/corpus/templates/calendar/app/hooks/use-attendee-timezones.ts +24 -0
  61. package/corpus/templates/calendar/app/i18n/zh-TW.ts +2 -0
  62. package/corpus/templates/calendar/app/i18n-data.ts +20 -0
  63. package/corpus/templates/calendar/app/lib/attendee-local-time.ts +122 -0
  64. package/corpus/templates/calendar/changelog/2026-07-08-event-details-now-support-extension-widgets-and-you-can-see-.md +6 -0
  65. package/corpus/templates/calendar/changelog/2026-07-08-when-you-invite-guests-to-a-new-event-you-now-appear-in.md +6 -0
  66. package/corpus/templates/calendar/package.json +2 -2
  67. package/corpus/templates/calendar/server/handlers/events.ts +5 -1
  68. package/corpus/templates/calendar/shared/api.ts +6 -0
  69. package/corpus/templates/chat/package.json +2 -2
  70. package/corpus/templates/clips/.agents/skills/meetings/SKILL.md +6 -2
  71. package/corpus/templates/clips/actions/create-meeting.ts +8 -3
  72. package/corpus/templates/clips/actions/list-meetings.ts +31 -3
  73. package/corpus/templates/clips/changelog/2026-07-08-after-finishing-a-desktop-recording-reopening-clips-restor.md +6 -0
  74. package/corpus/templates/clips/changelog/2026-07-08-clips-now-pops-up-to-take-notes-when-you-join-an-adhoc-zoom-.md +6 -0
  75. package/corpus/templates/clips/changelog/2026-07-08-desktop-clip-recordings-are-louder-after-capture-mic-only.md +6 -0
  76. package/corpus/templates/clips/changelog/2026-07-08-meeting-reminders-now-appear-1-minute-before-start-stay-unti.md +6 -0
  77. package/corpus/templates/clips/changelog/2026-07-08-the-desktop-meetings-panel-has-room-for-its-drop-shadow-and-.md +6 -0
  78. package/corpus/templates/clips/chrome-extension/package.json +2 -2
  79. package/corpus/templates/clips/desktop/package.json +2 -2
  80. package/corpus/templates/clips/desktop/src/app.tsx +91 -34
  81. package/corpus/templates/clips/desktop/src/hooks/useMeetingTranscription.ts +15 -5
  82. package/corpus/templates/clips/desktop/src/lib/meeting-notification-timing.ts +83 -0
  83. package/corpus/templates/clips/desktop/src/lib/native-recording-warm.ts +21 -0
  84. package/corpus/templates/clips/desktop/src/lib/recorder.ts +98 -20
  85. package/corpus/templates/clips/desktop/src/overlays/meeting-notification.tsx +174 -50
  86. package/corpus/templates/clips/desktop/src/styles.css +128 -53
  87. package/corpus/templates/clips/desktop/src-tauri/src/adhoc_meetings_watcher.rs +364 -0
  88. package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +1 -1
  89. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +5 -0
  90. package/corpus/templates/clips/desktop/src-tauri/src/meetings_watcher.rs +91 -11
  91. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +69 -19
  92. package/corpus/templates/clips/desktop/src-tauri/src/native_speech.rs +36 -1
  93. package/corpus/templates/clips/desktop/src-tauri/src/notifications.rs +62 -10
  94. package/corpus/templates/clips/desktop/src-tauri/src/silence_detector.rs +1 -24
  95. package/corpus/templates/clips/desktop/src-tauri/src/util.rs +36 -1
  96. package/corpus/templates/clips/package.json +2 -2
  97. package/corpus/templates/content/actions/_content-database-personal-view.ts +80 -0
  98. package/corpus/templates/content/actions/get-content-database-personal-view.ts +27 -0
  99. package/corpus/templates/content/actions/update-content-database-personal-view.ts +31 -0
  100. package/corpus/templates/content/actions/update-content-database-view.ts +3 -0
  101. package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +1 -1
  102. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +2584 -546
  103. package/corpus/templates/content/app/components/editor/database/filter-sort.ts +220 -18
  104. package/corpus/templates/content/app/hooks/use-content-database.ts +32 -0
  105. package/corpus/templates/content/app/i18n/zh-TW.ts +16 -0
  106. package/corpus/templates/content/app/i18n-data.ts +154 -0
  107. package/corpus/templates/content/changelog/2026-07-08-database-filters-now-support-notion-style-reset-save-for-eve.md +6 -0
  108. package/corpus/templates/content/changelog/2026-07-08-date-filters-can-now-target-a-between-range.md +6 -0
  109. package/corpus/templates/content/package.json +2 -2
  110. package/corpus/templates/content/parity/matrix.md +1 -1
  111. package/corpus/templates/content/parity/matrix.ts +2 -0
  112. package/corpus/templates/content/shared/api.ts +24 -0
  113. package/corpus/templates/design/package.json +2 -2
  114. package/corpus/templates/dispatch/app/i18n/zh-TW.ts +34 -1
  115. package/corpus/templates/dispatch/app/i18n-data.ts +387 -10
  116. package/corpus/templates/dispatch/app/routes/automations.tsx +1 -0
  117. package/corpus/templates/dispatch/changelog/2026-07-08-added-an-automations-page-to-review-pause-and-create.md +6 -0
  118. package/corpus/templates/dispatch/changelog/2026-07-08-destinations-now-shows-outbound-delivery-queue-health.md +6 -0
  119. package/corpus/templates/dispatch/package.json +2 -2
  120. package/corpus/templates/forms/package.json +2 -2
  121. package/corpus/templates/macros/package.json +2 -2
  122. package/corpus/templates/mail/app/hooks/use-emails.ts +56 -44
  123. package/corpus/templates/mail/app/lib/gmail-mutation-queue.ts +325 -0
  124. package/corpus/templates/mail/changelog/2026-07-08-rapid-archive-and-mark-read-now-batch-gmail-updates-so-the-i.md +6 -0
  125. package/corpus/templates/mail/package.json +2 -2
  126. package/corpus/templates/plan/package.json +2 -2
  127. package/corpus/templates/slides/app/components/editor/ImageDropPromptPopover.tsx +46 -29
  128. package/corpus/templates/slides/app/context/DeckContext.tsx +45 -18
  129. package/corpus/templates/slides/app/i18n/ar-SA.ts +4 -0
  130. package/corpus/templates/slides/app/i18n/de-DE.ts +4 -0
  131. package/corpus/templates/slides/app/i18n/en-US.ts +4 -0
  132. package/corpus/templates/slides/app/i18n/es-ES.ts +4 -0
  133. package/corpus/templates/slides/app/i18n/fr-FR.ts +4 -0
  134. package/corpus/templates/slides/app/i18n/hi-IN.ts +4 -0
  135. package/corpus/templates/slides/app/i18n/ja-JP.ts +4 -0
  136. package/corpus/templates/slides/app/i18n/ko-KR.ts +4 -0
  137. package/corpus/templates/slides/app/i18n/pt-BR.ts +4 -0
  138. package/corpus/templates/slides/app/i18n/zh-CN.ts +4 -0
  139. package/corpus/templates/slides/app/i18n/zh-TW.ts +4 -0
  140. package/corpus/templates/slides/app/lib/image-drop-to-agent.ts +115 -0
  141. package/corpus/templates/slides/app/pages/DeckEditor.tsx +22 -13
  142. package/corpus/templates/slides/changelog/2026-07-08-dropping-an-image-onto-a-slide-and-sending-it-to-the-agent-w.md +6 -0
  143. package/corpus/templates/slides/changelog/2026-07-08-undo-works-after-the-agent-edits-a-slide-so-you-can-reverse.md +6 -0
  144. package/corpus/templates/slides/package.json +2 -2
  145. package/dist/agent/run-manager.d.ts.map +1 -1
  146. package/dist/agent/run-manager.js +23 -2
  147. package/dist/agent/run-manager.js.map +1 -1
  148. package/dist/agent/run-store.js +2 -2
  149. package/dist/agent/run-store.js.map +1 -1
  150. package/dist/catalog.json +2 -2
  151. package/dist/cli/add.js +1 -1
  152. package/dist/cli/add.js.map +1 -1
  153. package/dist/cli/create.d.ts.map +1 -1
  154. package/dist/cli/create.js +8 -3
  155. package/dist/cli/create.js.map +1 -1
  156. package/dist/cli/index.js +5 -4
  157. package/dist/cli/index.js.map +1 -1
  158. package/dist/client/extensions/ExtensionSlot.js +6 -0
  159. package/dist/client/extensions/ExtensionSlot.js.map +1 -1
  160. package/dist/client/session-replay.d.ts +8 -0
  161. package/dist/client/session-replay.d.ts.map +1 -1
  162. package/dist/client/session-replay.js +80 -3
  163. package/dist/client/session-replay.js.map +1 -1
  164. package/dist/collab/struct-routes.d.ts +1 -1
  165. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  166. package/dist/jobs/scheduler.js +26 -1
  167. package/dist/jobs/scheduler.js.map +1 -1
  168. package/dist/localization/default-messages.d.ts +19 -0
  169. package/dist/localization/default-messages.d.ts.map +1 -1
  170. package/dist/localization/default-messages.js +19 -0
  171. package/dist/localization/default-messages.js.map +1 -1
  172. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  173. package/dist/resources/handlers.d.ts +2 -2
  174. package/dist/templates/default/app/i18n/ar-SA.ts +21 -0
  175. package/dist/templates/default/app/i18n/de-DE.ts +21 -0
  176. package/dist/templates/default/app/i18n/en-US.ts +21 -0
  177. package/dist/templates/default/app/i18n/es-ES.ts +21 -0
  178. package/dist/templates/default/app/i18n/fr-FR.ts +21 -0
  179. package/dist/templates/default/app/i18n/hi-IN.ts +20 -0
  180. package/dist/templates/default/app/i18n/ja-JP.ts +21 -0
  181. package/dist/templates/default/app/i18n/ko-KR.ts +20 -0
  182. package/dist/templates/default/app/i18n/pt-BR.ts +21 -0
  183. package/dist/templates/default/app/i18n/zh-CN.ts +20 -0
  184. package/dist/templates/default/app/i18n/zh-TW.ts +19 -0
  185. package/dist/templates/default/package.json +2 -2
  186. package/dist/templates/headless/package.json +3 -3
  187. package/dist/templates/workspace-core/package.json +6 -6
  188. package/dist/templates/workspace-root/package.json +2 -2
  189. package/dist/triggers/dispatcher.js +27 -2
  190. package/dist/triggers/dispatcher.js.map +1 -1
  191. package/dist/vite/client.d.ts.map +1 -1
  192. package/dist/vite/client.js +70 -1
  193. package/dist/vite/client.js.map +1 -1
  194. package/package.json +7 -6
  195. package/src/templates/default/app/i18n/ar-SA.ts +21 -0
  196. package/src/templates/default/app/i18n/de-DE.ts +21 -0
  197. package/src/templates/default/app/i18n/en-US.ts +21 -0
  198. package/src/templates/default/app/i18n/es-ES.ts +21 -0
  199. package/src/templates/default/app/i18n/fr-FR.ts +21 -0
  200. package/src/templates/default/app/i18n/hi-IN.ts +20 -0
  201. package/src/templates/default/app/i18n/ja-JP.ts +21 -0
  202. package/src/templates/default/app/i18n/ko-KR.ts +20 -0
  203. package/src/templates/default/app/i18n/pt-BR.ts +21 -0
  204. package/src/templates/default/app/i18n/zh-CN.ts +20 -0
  205. package/src/templates/default/app/i18n/zh-TW.ts +19 -0
  206. package/src/templates/default/package.json +2 -2
  207. package/src/templates/headless/package.json +3 -3
  208. package/src/templates/workspace-core/package.json +6 -6
  209. package/src/templates/workspace-root/package.json +2 -2
@@ -65,18 +65,36 @@ const AUDIO_DENOISE_FILTER: &str = "afftdn=nr=10:nf=-50:tn=1";
65
65
  // explicit output rate the AAC track ends up at 192 kHz and plays back slow.
66
66
  const AUDIO_OUTPUT_SAMPLE_RATE: u32 = 48000;
67
67
 
68
- // Loudness normalization, optionally preceded by the centered-stereo downmix
69
- // that repairs the mic+system L/R split and denoise for native mic captures.
70
- // Pair with `-ar AUDIO_OUTPUT_SAMPLE_RATE` so loudnorm's 192 kHz output is
71
- // resampled back.
72
- fn audio_filter_chain(downmix: bool, denoise: bool) -> String {
68
+ // Pre-gain for mic-only native captures. ScreenCaptureKit records without
69
+ // WebRTC AGC (unlike Loom / the browser path), so speech often lands well
70
+ // below -16 LUFS. 12 dB before loudnorm matches Loom-ish perceptual loudness
71
+ // for MacBook mics; loudnorm still clamps true peaks at TP=-1.5.
72
+ const AUDIO_MIC_PREGAIN_FILTER: &str = "volume=12dB";
73
+ // After the mic+system centered downmix (0.5*L+0.5*R) each source is ~
74
+ // 6 dB quieter. Restore energy before loudnorm so dual-capture clips are not
75
+ // systematically quieter than mic-only. Dual still mixes two sources so peaks
76
+ // may compress more under loudnorm than mic-only — by design when both sides
77
+ // compete for the same LUFS budget.
78
+ const AUDIO_DOWNMIX_MAKEUP_FILTER: &str = "volume=6dB";
79
+
80
+ // Loudness normalization, optionally preceded by the centered-stereo downmix
81
+ // that repairs the mic+system L/R split and denoise for native mic captures.
82
+ // Pair with `-ar AUDIO_OUTPUT_SAMPLE_RATE` so loudnorm's 192 kHz output is
83
+ // resampled back.
84
+ fn audio_filter_chain(downmix: bool, denoise: bool, mic_pregain: bool) -> String {
73
85
  let mut filters = Vec::new();
74
86
  if downmix {
75
87
  filters.push(AUDIO_DOWNMIX_FILTER);
88
+ // Undo pan attenuation; do not also apply mic-only pregain here —
89
+ // system audio would get a double boost.
90
+ filters.push(AUDIO_DOWNMIX_MAKEUP_FILTER);
76
91
  }
77
92
  if denoise {
78
93
  filters.push(AUDIO_DENOISE_FILTER);
79
94
  }
95
+ if mic_pregain {
96
+ filters.push(AUDIO_MIC_PREGAIN_FILTER);
97
+ }
80
98
  filters.push(AUDIO_LOUDNESS_FILTER);
81
99
  filters.join(",")
82
100
  }
@@ -522,11 +540,17 @@ struct SavedNativeRecording {
522
540
  height: Option<u32>,
523
541
  bytes: u64,
524
542
  has_audio: bool,
525
- // Whether the mic was captured. Drives the centered-stereo downmix repair
526
- // for the mic+system L/R split. Defaults to false for recordings queued
527
- // before this field existed so their audio is left untouched.
543
+ // Whether the mic was captured. Drives denoise + (with system audio) the
544
+ // centered-stereo downmix repair for the mic+system L/R split. Defaults to
545
+ // false for recordings queued before this field existed so their audio is
546
+ // left untouched.
528
547
  #[serde(default)]
529
548
  mic_captured: bool,
549
+ // Whether system audio was captured alongside the mic. Needed to decide
550
+ // downmix vs mic-only pregain on retry uploads. Defaults to false for
551
+ // older pending files (safe: skips downmix that would attenuate mic-only).
552
+ #[serde(default)]
553
+ system_audio_captured: bool,
530
554
  has_camera: bool,
531
555
  saved_at: String,
532
556
  last_attempt_at: Option<String>,
@@ -2577,6 +2601,7 @@ fn saved_recording_from_path(
2577
2601
  bytes,
2578
2602
  has_audio,
2579
2603
  mic_captured: session.restart.mic_captured_in_file,
2604
+ system_audio_captured: session.restart.capture_system_audio,
2580
2605
  has_camera,
2581
2606
  saved_at: now_iso(),
2582
2607
  last_attempt_at: None,
@@ -3071,6 +3096,7 @@ async fn upload_recording_file(
3071
3096
  Some(duration_ms),
3072
3097
  has_audio,
3073
3098
  session.restart.mic_captured_in_file,
3099
+ session.restart.capture_system_audio,
3074
3100
  )?;
3075
3101
  let upload_result = upload_prepared_recording_file(
3076
3102
  app,
@@ -3152,6 +3178,7 @@ fn prepare_saved_recording_file(
3152
3178
  Some(saved.duration_ms),
3153
3179
  saved.has_audio,
3154
3180
  saved.mic_captured,
3181
+ saved.system_audio_captured,
3155
3182
  )?;
3156
3183
  Ok((prepared, retry_combined_path))
3157
3184
  }
@@ -3878,7 +3905,14 @@ fn prepare_recording_file(
3878
3905
  duration_ms: Option<u128>,
3879
3906
  has_audio: bool,
3880
3907
  mic_captured_audio: bool,
3908
+ system_audio_captured: bool,
3881
3909
  ) -> Result<PreparedRecordingFile, String> {
3910
+ // Downmix only repairs mic+system L/R split. Applying it to mic-only
3911
+ // capture halves speech energy (~6 dB) when SCK puts the mic on one channel.
3912
+ let downmix_audio = mic_captured_audio && system_audio_captured;
3913
+ let denoise_audio = mic_captured_audio;
3914
+ // Pregain only for mic-only (no system audio compete) — SCK has no AGC.
3915
+ let mic_pregain = mic_captured_audio && !system_audio_captured;
3882
3916
  let metadata = std::fs::metadata(path).map_err(|e| {
3883
3917
  let diag = describe_recording_path(path);
3884
3918
  eprintln!("[clips-tray] native recording file missing at prepare: {e}; {diag}");
@@ -3935,8 +3969,9 @@ fn prepare_recording_file(
3935
3969
  ffmpeg_path,
3936
3970
  path,
3937
3971
  &normalized_path,
3938
- mic_captured_audio,
3939
- mic_captured_audio,
3972
+ downmix_audio,
3973
+ denoise_audio,
3974
+ mic_pregain,
3940
3975
  ) {
3941
3976
  Ok(()) => {
3942
3977
  let normalized_bytes = std::fs::metadata(&normalized_path)
@@ -4022,8 +4057,9 @@ fn prepare_recording_file(
4022
4057
  height,
4023
4058
  duration_ms,
4024
4059
  has_audio,
4025
- mic_captured_audio,
4026
- mic_captured_audio,
4060
+ downmix_audio,
4061
+ denoise_audio,
4062
+ mic_pregain,
4027
4063
  ) {
4028
4064
  Ok(()) => {
4029
4065
  let compressed_bytes = std::fs::metadata(&compressed_path)
@@ -4421,6 +4457,7 @@ fn normalize_audio_with_ffmpeg(
4421
4457
  output: &Path,
4422
4458
  downmix_audio: bool,
4423
4459
  denoise_audio: bool,
4460
+ mic_pregain: bool,
4424
4461
  ) -> Result<(), String> {
4425
4462
  let audio_bitrate = format!("{NORMALIZED_AUDIO_BITRATE_KBPS}k");
4426
4463
  let mut command = Command::new(ffmpeg_path);
@@ -4442,7 +4479,7 @@ fn normalize_audio_with_ffmpeg(
4442
4479
  .arg("-b:a")
4443
4480
  .arg(audio_bitrate)
4444
4481
  .arg("-af")
4445
- .arg(audio_filter_chain(downmix_audio, denoise_audio))
4482
+ .arg(audio_filter_chain(downmix_audio, denoise_audio, mic_pregain))
4446
4483
  .arg("-ac")
4447
4484
  .arg("2")
4448
4485
  .arg("-ar")
@@ -4507,6 +4544,7 @@ fn transcode_with_ffmpeg(
4507
4544
  normalize_audio: bool,
4508
4545
  downmix_audio: bool,
4509
4546
  denoise_audio: bool,
4547
+ mic_pregain: bool,
4510
4548
  ) -> Result<(), String> {
4511
4549
  let mut command = Command::new(ffmpeg_path);
4512
4550
  command
@@ -4532,7 +4570,7 @@ fn transcode_with_ffmpeg(
4532
4570
  if normalize_audio {
4533
4571
  command
4534
4572
  .arg("-af")
4535
- .arg(audio_filter_chain(downmix_audio, denoise_audio));
4573
+ .arg(audio_filter_chain(downmix_audio, denoise_audio, mic_pregain));
4536
4574
  }
4537
4575
 
4538
4576
  let duration_rate_limit =
@@ -4965,7 +5003,8 @@ fn concat_mp4_segments(segments: &[PathBuf], output: &Path) -> Result<(), String
4965
5003
  mod audio_track_probe_tests {
4966
5004
  use super::{
4967
5005
  audio_filter_chain, mp4_has_audio_track, parse_ffmpeg_volume_db, AudioSignalProbe,
4968
- AUDIO_DENOISE_FILTER, AUDIO_DOWNMIX_FILTER, AUDIO_LOUDNESS_FILTER,
5006
+ AUDIO_DENOISE_FILTER, AUDIO_DOWNMIX_FILTER, AUDIO_DOWNMIX_MAKEUP_FILTER,
5007
+ AUDIO_LOUDNESS_FILTER, AUDIO_MIC_PREGAIN_FILTER,
4969
5008
  };
4970
5009
  use std::io::Write;
4971
5010
 
@@ -5006,14 +5045,25 @@ mod audio_track_probe_tests {
5006
5045
 
5007
5046
  #[test]
5008
5047
  fn builds_native_audio_filter_chain_for_mic_noise_reduction() {
5009
- assert_eq!(audio_filter_chain(false, false), AUDIO_LOUDNESS_FILTER);
5010
5048
  assert_eq!(
5011
- audio_filter_chain(false, true),
5049
+ audio_filter_chain(false, false, false),
5050
+ AUDIO_LOUDNESS_FILTER
5051
+ );
5052
+ assert_eq!(
5053
+ audio_filter_chain(false, true, false),
5012
5054
  format!("{AUDIO_DENOISE_FILTER},{AUDIO_LOUDNESS_FILTER}")
5013
5055
  );
5014
5056
  assert_eq!(
5015
- audio_filter_chain(true, true),
5016
- format!("{AUDIO_DOWNMIX_FILTER},{AUDIO_DENOISE_FILTER},{AUDIO_LOUDNESS_FILTER}")
5057
+ audio_filter_chain(false, true, true),
5058
+ format!(
5059
+ "{AUDIO_DENOISE_FILTER},{AUDIO_MIC_PREGAIN_FILTER},{AUDIO_LOUDNESS_FILTER}"
5060
+ )
5061
+ );
5062
+ assert_eq!(
5063
+ audio_filter_chain(true, true, false),
5064
+ format!(
5065
+ "{AUDIO_DOWNMIX_FILTER},{AUDIO_DOWNMIX_MAKEUP_FILTER},{AUDIO_DENOISE_FILTER},{AUDIO_LOUDNESS_FILTER}"
5066
+ )
5017
5067
  );
5018
5068
  }
5019
5069
 
@@ -134,7 +134,8 @@ pub(crate) mod macos {
134
134
  kAudioOutputUnitProperty_CurrentDevice, kAudioUnitScope_Global, AudioUnitSetProperty,
135
135
  };
136
136
  use objc2_avf_audio::{
137
- AVAudioEngine, AVAudioInputNode, AVAudioPCMBuffer, AVAudioTime,
137
+ AVAudioEngine, AVAudioInputNode, AVAudioPCMBuffer, AVAudioSession,
138
+ AVAudioSessionCategoryOptions, AVAudioSessionCategoryPlayAndRecord, AVAudioTime,
138
139
  AVAudioVoiceProcessingOtherAudioDuckingConfiguration,
139
140
  AVAudioVoiceProcessingOtherAudioDuckingLevel,
140
141
  };
@@ -652,6 +653,36 @@ pub(crate) mod macos {
652
653
  }
653
654
  }
654
655
 
656
+ /// Keep Zoom/Meet/Teams' own mic uplink alive while Clips opens a parallel
657
+ /// AVAudioEngine tap. Without MixWithOthers, PlayAndRecord takes exclusive
658
+ /// session ownership and other apps' live calls get ducked or starved even
659
+ /// when VoiceProcessingIO is off.
660
+ fn configure_shared_mic_audio_session() {
661
+ unsafe {
662
+ let session = AVAudioSession::sharedInstance();
663
+ let Some(category) = AVAudioSessionCategoryPlayAndRecord else {
664
+ eprintln!("[whisper-mic] PlayAndRecord category missing — skipping MixWithOthers");
665
+ return;
666
+ };
667
+ let options = AVAudioSessionCategoryOptions::MixWithOthers;
668
+ if let Err(err) = session.setCategory_withOptions_error(category, options) {
669
+ eprintln!(
670
+ "[whisper-mic] setCategory MixWithOthers failed: {} — continuing",
671
+ ns_error_message(&err)
672
+ );
673
+ return;
674
+ }
675
+ if let Err(err) = session.setActive_error(true) {
676
+ eprintln!(
677
+ "[whisper-mic] AVAudioSession setActive failed: {} — continuing",
678
+ ns_error_message(&err)
679
+ );
680
+ return;
681
+ }
682
+ eprintln!("[whisper-mic] AVAudioSession PlayAndRecord + MixWithOthers active");
683
+ }
684
+ }
685
+
655
686
  fn take_warmed_raw_mic_engine(device_key: &RawMicDeviceKey) -> Option<WarmedRawMicEngine> {
656
687
  let mut slot = warmed_raw_mic_engine_slot().lock().ok()?;
657
688
  let should_reuse = slot
@@ -758,6 +789,7 @@ pub(crate) mod macos {
758
789
  mic_device_label: Option<&str>,
759
790
  device_key: RawMicDeviceKey,
760
791
  ) -> Result<WarmedRawMicEngine, String> {
792
+ configure_shared_mic_audio_session();
761
793
  let engine: Retained<AVAudioEngine> = unsafe { AVAudioEngine::new() };
762
794
  configure_engine_input_device(&engine, mic_device_id, mic_device_label)?;
763
795
  let input_node: Retained<AVAudioInputNode> = unsafe { engine.inputNode() };
@@ -955,6 +987,7 @@ pub(crate) mod macos {
955
987
  // Spin up the engine and grab its input node + native format.
956
988
  // SAFETY: `AVAudioEngine::new()` returns a retained engine.
957
989
  // `inputNode` is the engine's singleton input — also retained.
990
+ configure_shared_mic_audio_session();
958
991
  let engine: Retained<AVAudioEngine> = unsafe { AVAudioEngine::new() };
959
992
  configure_engine_input_device(
960
993
  &engine,
@@ -1321,6 +1354,8 @@ pub(crate) mod macos {
1321
1354
  }
1322
1355
  clear_warmed_raw_mic_engine();
1323
1356
 
1357
+ configure_shared_mic_audio_session();
1358
+
1324
1359
  let engine: Retained<AVAudioEngine> = unsafe { AVAudioEngine::new() };
1325
1360
  configure_engine_input_device(
1326
1361
  &engine,
@@ -16,8 +16,8 @@ use crate::util::{
16
16
  };
17
17
 
18
18
  const MEETING_NOTIFICATION_LABEL: &str = "meeting-notif";
19
- const NOTIFICATION_W_LOGICAL: u32 = 420;
20
- const NOTIFICATION_H_LOGICAL: u32 = 148;
19
+ const NOTIFICATION_W_LOGICAL: u32 = 440;
20
+ const NOTIFICATION_H_LOGICAL: u32 = 120;
21
21
  const NOTIFICATION_TOP_MARGIN_LOGICAL: u32 = 44;
22
22
  const NOTIFICATION_RIGHT_MARGIN_LOGICAL: u32 = 24;
23
23
 
@@ -109,6 +109,38 @@ pub fn take_pending_meeting_notification(app: AppHandle) -> Result<Option<Value>
109
109
  Ok(pending.take())
110
110
  }
111
111
 
112
+ fn format_time_range_subtitle(
113
+ scheduled_start: Option<&str>,
114
+ scheduled_end: Option<&str>,
115
+ starts_in_secs: i64,
116
+ ) -> String {
117
+ let Some(start_str) = scheduled_start else {
118
+ return fallback_starts_subtitle(starts_in_secs);
119
+ };
120
+ let Ok(start) = chrono::DateTime::parse_from_rfc3339(start_str) else {
121
+ return fallback_starts_subtitle(starts_in_secs);
122
+ };
123
+ let local_start = start.with_timezone(&chrono::Local);
124
+ let start_label = local_start.format("%-I:%M %p").to_string();
125
+ if let Some(end_str) = scheduled_end {
126
+ if let Ok(end) = chrono::DateTime::parse_from_rfc3339(end_str) {
127
+ let end_label = end.with_timezone(&chrono::Local).format("%-I:%M %p");
128
+ return format!("{start_label} - {end_label}");
129
+ }
130
+ }
131
+ start_label
132
+ }
133
+
134
+ fn fallback_starts_subtitle(starts_in_secs: i64) -> String {
135
+ if starts_in_secs <= 0 {
136
+ "Started".to_string()
137
+ } else if starts_in_secs < 90 {
138
+ format!("Starts in {}s", starts_in_secs)
139
+ } else {
140
+ format!("Starts in {} min", (starts_in_secs / 60).max(1))
141
+ }
142
+ }
143
+
112
144
  #[tauri::command]
113
145
  pub async fn notify_meeting_starting(
114
146
  app: AppHandle,
@@ -116,18 +148,30 @@ pub async fn notify_meeting_starting(
116
148
  title: String,
117
149
  starts_in_secs: i64,
118
150
  join_url: Option<String>,
151
+ scheduled_start: Option<String>,
152
+ scheduled_end: Option<String>,
153
+ platform: Option<String>,
119
154
  auto_start: Option<bool>,
155
+ notification_type: Option<String>,
120
156
  ) -> Result<(), String> {
121
- let pretty_in = if starts_in_secs <= 0 {
122
- "now".to_string()
123
- } else if starts_in_secs < 90 {
124
- format!("in {}s", starts_in_secs)
157
+ let kind = notification_type
158
+ .as_deref()
159
+ .map(str::trim)
160
+ .filter(|s| !s.is_empty())
161
+ .unwrap_or("calendar");
162
+ let is_adhoc = kind == "adhoc";
163
+ let body = if is_adhoc {
164
+ "Take notes?".to_string()
125
165
  } else {
126
- format!("in {} min", (starts_in_secs / 60).max(1))
166
+ format_time_range_subtitle(
167
+ scheduled_start.as_deref(),
168
+ scheduled_end.as_deref(),
169
+ starts_in_secs,
170
+ )
127
171
  };
128
- let body = format!("Starts {}", pretty_in);
129
172
  dlog!(
130
- "[clips-tray] notify_meeting_starting id={} title={} body={}",
173
+ "[clips-tray] notify_meeting_starting type={} id={} title={} body={}",
174
+ kind,
131
175
  meeting_id,
132
176
  title,
133
177
  body
@@ -136,15 +180,23 @@ pub async fn notify_meeting_starting(
136
180
  // Keep the latest payload available for cold overlay windows, then emit
137
181
  // for already-mounted listeners.
138
182
  let payload = serde_json::json!({
139
- "type": "calendar",
183
+ "type": kind,
140
184
  "title": title,
141
185
  "subtitle": body,
142
186
  "meetingId": meeting_id,
143
187
  "joinUrl": join_url,
188
+ "platform": platform,
189
+ "scheduledStart": scheduled_start,
190
+ "scheduledEnd": scheduled_end,
144
191
  "autoStart": auto_start.unwrap_or(false),
145
192
  });
146
193
  store_pending_meeting_notification(&app, &payload);
147
194
  let _ = app.emit("meetings:show-notification", payload.clone());
148
195
 
196
+ // Ensure the overlay window exists / is visible for cold starts.
197
+ if let Err(err) = show_meeting_notification_window(&app) {
198
+ eprintln!("[clips-tray] show meeting notification failed: {err}");
199
+ }
200
+
149
201
  Ok(())
150
202
  }
@@ -341,7 +341,7 @@ fn install_call_ended_watcher(app: &AppHandle, threshold_ms: u64) {
341
341
  fired = false;
342
342
  continue;
343
343
  }
344
- let front = frontmost_bundle_id();
344
+ let front = crate::util::frontmost_bundle_id();
345
345
  let is_strong_vc = front
346
346
  .as_ref()
347
347
  .map(|b| strong_vc_bundles.iter().any(|k| k == b))
@@ -409,26 +409,3 @@ fn audio_recently_silent(state: &tauri::State<'_, DetectorState>, threshold_ms:
409
409
  mic_silent && system_silent
410
410
  }
411
411
 
412
- #[cfg(target_os = "macos")]
413
- fn frontmost_bundle_id() -> Option<String> {
414
- // Lightweight shell-out to avoid extra crates: AppleScript via `osascript`
415
- // returns the bundle id of the frontmost app. This avoids pulling in a
416
- // full objc2 dependency just for one read.
417
- use std::process::Command;
418
- let out = Command::new("osascript")
419
- .args([
420
- "-e",
421
- "tell application \"System Events\" to get bundle identifier of (first process whose frontmost is true)",
422
- ])
423
- .output()
424
- .ok()?;
425
- if !out.status.success() {
426
- return None;
427
- }
428
- let s = String::from_utf8(out.stdout).ok()?.trim().to_string();
429
- if s.is_empty() {
430
- None
431
- } else {
432
- Some(s)
433
- }
434
- }
@@ -5,7 +5,7 @@ use crate::state::{
5
5
  DictationActive, PopoverShownAt, RecordingActive, TrayAnchor, VoiceWakePopover,
6
6
  };
7
7
 
8
- const POPOVER_SHADOW_GUTTER_LOGICAL: f64 = 12.0;
8
+ const POPOVER_SHADOW_GUTTER_LOGICAL: f64 = 24.0;
9
9
  const POPOVER_DEFAULT_WIDTH_LOGICAL: f64 = 360.0;
10
10
  const POPOVER_DEFAULT_HEIGHT_LOGICAL: f64 = 520.0;
11
11
 
@@ -425,6 +425,41 @@ pub fn is_recording_active(app: &AppHandle) -> bool {
425
425
  .unwrap_or(false)
426
426
  }
427
427
 
428
+ pub fn is_meeting_active(app: &AppHandle) -> bool {
429
+ use crate::state::MeetingActive;
430
+ app.try_state::<MeetingActive>()
431
+ .and_then(|s| s.0.lock().ok().map(|g| *g))
432
+ .unwrap_or(false)
433
+ }
434
+
435
+ /// Bundle id of the frontmost macOS app, or `None` on failure / non-macOS.
436
+ /// Uses a lightweight `osascript` shell-out so callers don't need objc2.
437
+ #[cfg(target_os = "macos")]
438
+ pub fn frontmost_bundle_id() -> Option<String> {
439
+ use std::process::Command;
440
+ let out = Command::new("osascript")
441
+ .args([
442
+ "-e",
443
+ "tell application \"System Events\" to get bundle identifier of (first process whose frontmost is true)",
444
+ ])
445
+ .output()
446
+ .ok()?;
447
+ if !out.status.success() {
448
+ return None;
449
+ }
450
+ let s = String::from_utf8(out.stdout).ok()?.trim().to_string();
451
+ if s.is_empty() {
452
+ None
453
+ } else {
454
+ Some(s)
455
+ }
456
+ }
457
+
458
+ #[cfg(not(target_os = "macos"))]
459
+ pub fn frontmost_bundle_id() -> Option<String> {
460
+ None
461
+ }
462
+
428
463
  pub fn set_dictation_active(app: &AppHandle, active: bool) {
429
464
  if let Some(state) = app.try_state::<DictationActive>() {
430
465
  if let Ok(mut g) = state.0.lock() {
@@ -71,7 +71,6 @@
71
71
  "@types/node": "^24.2.1",
72
72
  "@types/react": "catalog:",
73
73
  "@types/react-dom": "catalog:",
74
- "@typescript/native-preview": "catalog:",
75
74
  "@xterm/addon-fit": "^0.11.0",
76
75
  "@xterm/addon-web-links": "^0.12.0",
77
76
  "@xterm/xterm": "^6.0.0",
@@ -93,7 +92,8 @@
93
92
  "tailwind-merge": "^3.5.0",
94
93
  "tailwindcss": "catalog:",
95
94
  "tsx": "^4.20.3",
96
- "typescript": "^6.0.3",
95
+ "typescript": "catalog:",
96
+ "typescript-7": "catalog:",
97
97
  "vaul": "^1.1.2",
98
98
  "vite": "catalog:",
99
99
  "vitest": "catalog:"
@@ -0,0 +1,80 @@
1
+ import { getUserSetting } from "@agent-native/core/settings";
2
+ import { assertAccess } from "@agent-native/core/sharing";
3
+ import { and, eq, isNull } from "drizzle-orm";
4
+ import { z } from "zod";
5
+
6
+ import { getDb, schema } from "../server/db/index.js";
7
+
8
+ export const PERSONAL_DATABASE_VIEW_OVERRIDES_VERSION = 1;
9
+
10
+ export const personalDatabaseViewSettingKey = (databaseId: string) =>
11
+ `content-database-personal-view:${databaseId}`;
12
+
13
+ export const sortSchema = z.object({
14
+ key: z.string(),
15
+ label: z.string(),
16
+ direction: z.enum(["asc", "desc"]),
17
+ });
18
+
19
+ export const filterSchema = z.object({
20
+ key: z.string(),
21
+ label: z.string(),
22
+ operator: z.enum([
23
+ "contains",
24
+ "equals",
25
+ "does_not_equal",
26
+ "greater_than",
27
+ "less_than",
28
+ "before",
29
+ "after",
30
+ "between",
31
+ "is_checked",
32
+ "is_unchecked",
33
+ "is_empty",
34
+ "is_not_empty",
35
+ ]),
36
+ value: z.string(),
37
+ filterGroupId: z.string().optional(),
38
+ parentFilterGroupId: z.string().optional(),
39
+ });
40
+
41
+ export const personalViewOverridesSchema = z.object({
42
+ version: z.literal(PERSONAL_DATABASE_VIEW_OVERRIDES_VERSION),
43
+ activeViewId: z.string().optional(),
44
+ views: z.array(
45
+ z.object({
46
+ id: z.string(),
47
+ sorts: z.array(sortSchema).default([]),
48
+ filters: z.array(filterSchema).default([]),
49
+ filterMode: z.enum(["and", "or"]).default("and"),
50
+ }),
51
+ ),
52
+ });
53
+
54
+ export async function assertContentDatabaseViewerAccess(databaseId: string) {
55
+ const db = getDb();
56
+ const [database] = await db
57
+ .select()
58
+ .from(schema.contentDatabases)
59
+ .where(
60
+ and(
61
+ eq(schema.contentDatabases.id, databaseId),
62
+ isNull(schema.contentDatabases.deletedAt),
63
+ ),
64
+ );
65
+ if (!database) throw new Error(`Database "${databaseId}" not found`);
66
+
67
+ await assertAccess("document", database.documentId, "viewer");
68
+ }
69
+
70
+ export async function readPersonalDatabaseViewOverrides(
71
+ userEmail: string,
72
+ databaseId: string,
73
+ ) {
74
+ const stored = await getUserSetting(
75
+ userEmail,
76
+ personalDatabaseViewSettingKey(databaseId),
77
+ );
78
+ const parsed = personalViewOverridesSchema.safeParse(stored);
79
+ return parsed.success ? parsed.data : null;
80
+ }
@@ -0,0 +1,27 @@
1
+ import { defineAction } from "@agent-native/core";
2
+ import { z } from "zod";
3
+
4
+ import {
5
+ assertContentDatabaseViewerAccess,
6
+ readPersonalDatabaseViewOverrides,
7
+ } from "./_content-database-personal-view.js";
8
+
9
+ export default defineAction({
10
+ description:
11
+ "Get the current user's personal saved filter, sort, and active view overrides for a content database.",
12
+ schema: z.object({
13
+ databaseId: z.string().describe("Database ID"),
14
+ }),
15
+ http: { method: "GET" },
16
+ run: async ({ databaseId }, ctx) => {
17
+ if (!ctx?.userEmail) throw new Error("Not authenticated.");
18
+ await assertContentDatabaseViewerAccess(databaseId);
19
+ return {
20
+ databaseId,
21
+ overrides: await readPersonalDatabaseViewOverrides(
22
+ ctx.userEmail,
23
+ databaseId,
24
+ ),
25
+ };
26
+ },
27
+ });
@@ -0,0 +1,31 @@
1
+ import { defineAction } from "@agent-native/core";
2
+ import { deleteUserSetting, putUserSetting } from "@agent-native/core/settings";
3
+ import { z } from "zod";
4
+
5
+ import {
6
+ assertContentDatabaseViewerAccess,
7
+ personalDatabaseViewSettingKey,
8
+ personalViewOverridesSchema,
9
+ } from "./_content-database-personal-view.js";
10
+
11
+ export default defineAction({
12
+ description:
13
+ "Update or clear the current user's personal saved filter, sort, and active view overrides for a content database.",
14
+ schema: z.object({
15
+ databaseId: z.string().describe("Database ID"),
16
+ overrides: personalViewOverridesSchema.nullable(),
17
+ }),
18
+ run: async ({ databaseId, overrides }, ctx) => {
19
+ if (!ctx?.userEmail) throw new Error("Not authenticated.");
20
+ await assertContentDatabaseViewerAccess(databaseId);
21
+
22
+ const key = personalDatabaseViewSettingKey(databaseId);
23
+ if (overrides) {
24
+ await putUserSetting(ctx.userEmail, key, overrides);
25
+ } else {
26
+ await deleteUserSetting(ctx.userEmail, key);
27
+ }
28
+
29
+ return { databaseId, overrides };
30
+ },
31
+ });
@@ -25,12 +25,15 @@ const filterSchema = z.object({
25
25
  "less_than",
26
26
  "before",
27
27
  "after",
28
+ "between",
28
29
  "is_checked",
29
30
  "is_unchecked",
30
31
  "is_empty",
31
32
  "is_not_empty",
32
33
  ]),
33
34
  value: z.string(),
35
+ filterGroupId: z.string().optional(),
36
+ parentFilterGroupId: z.string().optional(),
34
37
  });
35
38
 
36
39
  const columnCalculationSchema = z.enum([
@@ -440,7 +440,7 @@ function personInitials(value: string) {
440
440
  );
441
441
  }
442
442
 
443
- function PersonPill({ value }: { value: string }) {
443
+ export function PersonPill({ value }: { value: string }) {
444
444
  return (
445
445
  <span className="inline-flex max-w-full items-center gap-1.5 rounded bg-muted px-1.5 py-0.5 text-xs font-medium text-foreground">
446
446
  <span className="flex size-4 shrink-0 items-center justify-center rounded-full bg-background text-[9px] font-semibold text-muted-foreground">