@agent-native/core 0.157.0 → 0.157.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 (114) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/server/jobs/analytics-bigquery-backfill.ts +19 -4
  3. package/corpus/templates/analytics/server/lib/first-party-analytics-backend.ts +7 -2
  4. package/corpus/templates/assets/app/global.css +4 -0
  5. package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +1 -2
  6. package/corpus/templates/brain/app/routes/sources.tsx +1 -2
  7. package/corpus/templates/calendar/app/hooks/use-bookings.ts +12 -2
  8. package/corpus/templates/calendar/app/pages/BookingLinksPage.tsx +64 -2
  9. package/corpus/templates/calendar/server/handlers/bookings.ts +224 -93
  10. package/corpus/templates/calendar/server/plugins/auth.ts +0 -2
  11. package/corpus/templates/clips/actions/create-folder.ts +7 -1
  12. package/corpus/templates/clips/actions/create-meeting.ts +6 -2
  13. package/corpus/templates/clips/actions/create-recording.ts +5 -3
  14. package/corpus/templates/clips/actions/finalize-recording.ts +97 -65
  15. package/corpus/templates/clips/actions/import-loom-recording.ts +5 -3
  16. package/corpus/templates/clips/actions/request-recording-access.ts +305 -0
  17. package/corpus/templates/clips/actions/stitch-recordings.ts +5 -3
  18. package/corpus/templates/clips/app/components/meetings/share-meeting-dialog.tsx +38 -63
  19. package/corpus/templates/clips/app/components/player/share-dialog.tsx +75 -75
  20. package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +3 -21
  21. package/corpus/templates/clips/app/components/sharing/share-ui.tsx +13 -30
  22. package/corpus/templates/clips/app/i18n/en-US.ts +10 -0
  23. package/corpus/templates/clips/app/routes/_app.meetings.$meetingId.tsx +5 -5
  24. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +9 -16
  25. package/corpus/templates/clips/app/routes/share.$shareId.tsx +135 -18
  26. package/corpus/templates/clips/changelog/2026-08-14-long-resumable-uploads-now-abort-cleanly-and-show-errors.md +6 -0
  27. package/corpus/templates/clips/changelog/2026-08-14-standardized-share-popovers.md +6 -0
  28. package/corpus/templates/clips/chrome-extension/public/manifest.json +11 -3
  29. package/corpus/templates/clips/chrome-extension/src/github-preview-content.ts +37 -8
  30. package/corpus/templates/clips/chrome-extension/src/github-preview.ts +3 -3
  31. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +8 -25
  32. package/corpus/templates/clips/desktop/src/app.tsx +24 -0
  33. package/corpus/templates/clips/desktop/src/lib/media-capture-constraints.ts +42 -13
  34. package/corpus/templates/clips/desktop/src/lib/recorder.ts +11 -18
  35. package/corpus/templates/clips/desktop/src/overlays/onboarding.tsx +1 -0
  36. package/corpus/templates/clips/desktop/src/shared/config.ts +1 -0
  37. package/corpus/templates/clips/desktop/src/styles.css +24 -9
  38. package/corpus/templates/clips/desktop/src-tauri/Cargo.toml +3 -0
  39. package/corpus/templates/clips/desktop/src-tauri/src/config.rs +8 -0
  40. package/corpus/templates/clips/desktop/src-tauri/src/native_screen/custom_capture.rs +154 -176
  41. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +115 -31
  42. package/corpus/templates/clips/desktop/src-tauri/src/rewind_clip.rs +7 -0
  43. package/corpus/templates/clips/server/db/index.ts +1 -0
  44. package/corpus/templates/clips/server/db/schema.ts +1 -0
  45. package/corpus/templates/clips/server/lib/calendar-event-meetings.ts +6 -1
  46. package/corpus/templates/clips/server/lib/emails.ts +23 -0
  47. package/corpus/templates/clips/server/lib/public-agent-context.ts +55 -0
  48. package/corpus/templates/clips/server/lib/recordings.ts +2 -1
  49. package/corpus/templates/clips/server/lib/resumable-upload-cleanup.ts +49 -0
  50. package/corpus/templates/clips/server/lib/s3-upload-provider.ts +34 -7
  51. package/corpus/templates/clips/server/lib/upload-interruption.ts +15 -1
  52. package/corpus/templates/clips/server/lib/upload-lease.ts +94 -9
  53. package/corpus/templates/clips/server/routes/api/agent-context.json.get.ts +82 -7
  54. package/corpus/templates/clips/server/routes/api/public-recording.get.ts +1 -0
  55. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +71 -12
  56. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/interrupt.post.ts +4 -3
  57. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/reset-chunks.post.ts +133 -4
  58. package/corpus/templates/clips/server/routes/api/uploads/reap.post.ts +8 -1
  59. package/corpus/templates/clips/shared/recording-link.ts +4 -0
  60. package/corpus/templates/clips/shared/share-loader-response.ts +2 -1
  61. package/corpus/templates/content/app/components/editor/DocumentToolbar.tsx +7 -16
  62. package/corpus/templates/factory/AGENTS.md +5 -1
  63. package/corpus/templates/factory/README.md +6 -2
  64. package/corpus/templates/factory/actions/view-screen.ts +15 -0
  65. package/corpus/templates/factory/app/components/factory/FactoryAgentsView.tsx +58 -189
  66. package/corpus/templates/factory/app/components/layout/Header.tsx +1 -0
  67. package/corpus/templates/factory/app/components/layout/Layout.tsx +1 -1
  68. package/corpus/templates/factory/app/components/layout/Sidebar.tsx +7 -0
  69. package/corpus/templates/factory/app/hooks/use-navigation-state.ts +3 -0
  70. package/corpus/templates/factory/app/i18n/en-US.ts +3 -1
  71. package/corpus/templates/factory/app/routes/agents.tsx +9 -0
  72. package/corpus/templates/mail/server/plugins/auth.ts +0 -2
  73. package/corpus/templates/plan/app/components/plan/LocalCodebasePicker.tsx +50 -14
  74. package/corpus/templates/plan/app/pages/PlansPage.tsx +9 -19
  75. package/corpus/templates/slides/app/components/editor/HistoryPanel.tsx +7 -1
  76. package/corpus/templates/slides/app/context/DeckContext.tsx +71 -22
  77. package/corpus/templates/slides/app/lib/editor-shortcuts.ts +46 -0
  78. package/corpus/templates/slides/app/pages/DeckEditor.tsx +13 -0
  79. package/dist/client/AgentPanel.d.ts +13 -1
  80. package/dist/client/AgentPanel.js +12 -10
  81. package/dist/client/AssistantChat.js +25 -5
  82. package/dist/client/HighlightedCodeBlock.js +12 -2
  83. package/dist/client/agent-chat/index.d.ts +1 -0
  84. package/dist/client/agent-chat/index.js +1 -0
  85. package/dist/client/chat/markdown-renderer.js +16 -3
  86. package/dist/client/chat-first/apps-rail.js +1 -1
  87. package/dist/client/components/ui/message-scroller.js +1 -1
  88. package/dist/client/settings/AccountSettingsCard.js +48 -19
  89. package/dist/client/sharing/ShareButton.d.ts +1 -2
  90. package/dist/client/sharing/ShareButton.js +5 -41
  91. package/dist/client/sharing/ShareDialog.js +5 -4
  92. package/dist/client/use-chat-threads.js +10 -14
  93. package/dist/collab/awareness.d.ts +2 -2
  94. package/dist/collab/routes.d.ts +1 -1
  95. package/dist/collab/struct-routes.d.ts +1 -1
  96. package/dist/localization/default-messages.d.ts +1 -0
  97. package/dist/localization/default-messages.js +1 -0
  98. package/dist/notifications/routes.d.ts +3 -3
  99. package/dist/provider-api/actions/custom-provider-registration.d.ts +11 -11
  100. package/dist/provider-api/actions/provider-api.d.ts +15 -15
  101. package/dist/server/auth-marketing.d.ts +1 -0
  102. package/dist/server/auth-marketing.js +0 -2
  103. package/dist/server/auth.d.ts +1 -0
  104. package/dist/server/auth.js +18 -0
  105. package/dist/server/onboarding-html.d.ts +1 -0
  106. package/dist/server/onboarding-html.js +2 -88
  107. package/dist/server/realtime-token.d.ts +1 -1
  108. package/dist/shared/markdown-block-split.d.ts +16 -0
  109. package/dist/shared/markdown-block-split.js +81 -5
  110. package/dist/styles/agent-native.css +13 -0
  111. package/dist/voice/voice-cleanup-prompt.js +1 -0
  112. package/docs/content/cloneable-saas.mdx +24 -26
  113. package/package.json +3 -3
  114. package/corpus/templates/clips/shared/mic-audio-cleanup.ts +0 -350
@@ -64,22 +64,11 @@ const AUDIO_SIGNAL_MIN_MAX_VOLUME_DB: f64 = -30.0;
64
64
  // stereo content that must not be flattened.
65
65
  const AUDIO_DOWNMIX_FILTER: &str =
66
66
  "aformat=channel_layouts=stereo,pan=stereo|FL=0.5*FL+0.5*FR|FR=0.5*FL+0.5*FR";
67
- // The legacy ScreenCaptureKit output carries mic + system audio together on
68
- // the two source channels. Remove only the narrow mains tones before the
69
- // centered downmix; do not apply the mic-only high-pass/expander to system
70
- // audio, which may legitimately contain bass.
71
- const AUDIO_MIXED_HUM_FILTER: &str =
72
- "bandreject=f=50:t=q:w=18,bandreject=f=60:t=q:w=18,bandreject=f=100:t=q:w=18,bandreject=f=120:t=q:w=18";
73
67
  // Mild FFT denoise for native mic captures. Browser recordings already request
74
68
  // WebRTC noise suppression; ScreenCaptureKit/screencapture mic capture does
75
69
  // not, so reduce steady broadband room/mic hiss during the existing optimize
76
70
  // step. Keep this conservative to avoid watery speech artifacts.
77
71
  const AUDIO_DENOISE_FILTER: &str = "afftdn=nr=10:nf=-50:tn=1";
78
- // The FFT pass above is intentionally broad and does not reliably remove a
79
- // tonal HVAC hum. Apply this narrower chain only to mic-only captures so
80
- // system audio/music keeps its low end when both sources are recorded.
81
- const AUDIO_MIC_CLEANUP_FILTER: &str =
82
- "highpass=f=65,bandreject=f=50:t=q:w=18,bandreject=f=60:t=q:w=18,bandreject=f=100:t=q:w=18,bandreject=f=120:t=q:w=18,agate=threshold=0.008:ratio=2:range=0.35:attack=10:release=180:knee=4:detection=rms";
83
72
  // loudnorm operates internally at 192 kHz and emits at 192 kHz; without an
84
73
  // explicit output rate the AAC track ends up at 192 kHz and plays back slow.
85
74
  const AUDIO_OUTPUT_SAMPLE_RATE: u32 = 48000;
@@ -97,21 +86,19 @@ const AUDIO_MIC_PREGAIN_FILTER: &str = "volume=12dB";
97
86
  const AUDIO_DOWNMIX_MAKEUP_FILTER: &str = "volume=6dB";
98
87
 
99
88
  // Loudness normalization, optionally preceded by the centered-stereo downmix
100
- // that repairs the mic+system L/R split and denoise for native mic captures.
89
+ // that repairs the legacy mic+system L/R split. Realtime custom captures are
90
+ // denoised before they reach the writer, so this pass stays for legacy files
91
+ // and non-ScreenCaptureKit fallbacks.
101
92
  // Pair with `-ar AUDIO_OUTPUT_SAMPLE_RATE` so loudnorm's 192 kHz output is
102
93
  // resampled back.
103
94
  fn audio_filter_chain(downmix: bool, denoise: bool, mic_pregain: bool) -> String {
104
95
  let mut filters = Vec::new();
105
96
  if downmix {
106
- filters.push(AUDIO_MIXED_HUM_FILTER);
107
97
  filters.push(AUDIO_DOWNMIX_FILTER);
108
98
  // Undo pan attenuation; do not also apply mic-only pregain here —
109
99
  // system audio would get a double boost.
110
100
  filters.push(AUDIO_DOWNMIX_MAKEUP_FILTER);
111
101
  }
112
- if mic_pregain {
113
- filters.push(AUDIO_MIC_CLEANUP_FILTER);
114
- }
115
102
  if denoise {
116
103
  filters.push(AUDIO_DENOISE_FILTER);
117
104
  }
@@ -345,6 +332,9 @@ struct NativeFullscreenSession {
345
332
  /// nothing is written to disk. `begin` attaches it and flips this false.
346
333
  pending_recording_output: bool,
347
334
  custom_pipeline: bool,
335
+ /// True when the native realtime microphone denoiser already touched the
336
+ /// audio in this file. Retry preparation must not denoise it a second time.
337
+ audio_cleanup_applied: bool,
348
338
  /// Active live-upload task: streams the growing fragmented MP4 to the
349
339
  /// server in chunks during recording (custom pipeline only). `None` when
350
340
  /// live upload is disabled or for local-only recordings.
@@ -645,6 +635,7 @@ pub(crate) fn prepare_shared_clip_sink(
645
635
  request.height,
646
636
  request.include_mic,
647
637
  request.include_system_audio,
638
+ crate::config::feature_config(app).voice_cleanup_enabled,
648
639
  ) {
649
640
  Ok(sink) => sink,
650
641
  Err(error) => {
@@ -1047,6 +1038,9 @@ struct SavedNativeRecording {
1047
1038
  retry_attempt_id: Option<String>,
1048
1039
  #[serde(default)]
1049
1040
  custom_pipeline: bool,
1041
+ /// True when native realtime microphone cleanup was already applied.
1042
+ #[serde(default)]
1043
+ audio_cleanup_applied: bool,
1050
1044
  /// True when the SCK finalization callback reported an error, meaning the
1051
1045
  /// MP4 is missing its moov atom and cannot be recovered by retrying.
1052
1046
  #[serde(default)]
@@ -1070,6 +1064,8 @@ struct NativeRecordingIntent {
1070
1064
  system_audio_captured: bool,
1071
1065
  has_camera: bool,
1072
1066
  custom_pipeline: bool,
1067
+ #[serde(default)]
1068
+ audio_cleanup_applied: bool,
1073
1069
  saved_at: String,
1074
1070
  }
1075
1071
 
@@ -1183,6 +1179,7 @@ pub(crate) struct FinalizedNativeArtifact {
1183
1179
  pub system_audio_captured: bool,
1184
1180
  pub has_camera: bool,
1185
1181
  pub custom_pipeline: bool,
1182
+ pub audio_cleanup_applied: bool,
1186
1183
  }
1187
1184
 
1188
1185
  impl FinalizedNativeArtifact {
@@ -1197,6 +1194,7 @@ impl FinalizedNativeArtifact {
1197
1194
  system_audio_captured: session.restart.capture_system_audio,
1198
1195
  has_camera: false,
1199
1196
  custom_pipeline: session.custom_pipeline,
1197
+ audio_cleanup_applied: session.audio_cleanup_applied,
1200
1198
  }
1201
1199
  }
1202
1200
 
@@ -1218,6 +1216,7 @@ impl FinalizedNativeArtifact {
1218
1216
  system_audio_captured,
1219
1217
  has_camera: false,
1220
1218
  custom_pipeline: true,
1219
+ audio_cleanup_applied: false,
1221
1220
  }
1222
1221
  }
1223
1222
  }
@@ -1427,6 +1426,7 @@ fn persist_active_recording_intent(
1427
1426
  session.restart.capture_system_audio,
1428
1427
  has_camera,
1429
1428
  session.custom_pipeline,
1429
+ session.audio_cleanup_applied,
1430
1430
  ) {
1431
1431
  eprintln!("[clips-tray] native recording recovery intent unavailable: {error}");
1432
1432
  }
@@ -1783,14 +1783,20 @@ pub async fn native_fullscreen_recording_stop_and_upload(
1783
1783
 
1784
1784
  if multi_segment {
1785
1785
  if let Err(merge_err) = &consolidate_outcome {
1786
- let mut saved = saved_recording_from_segments(
1786
+ let mut saved = match saved_recording_from_segments(
1787
1787
  &session,
1788
1788
  &server_url,
1789
1789
  &recording_id,
1790
1790
  duration_ms,
1791
1791
  has_audio,
1792
1792
  has_camera,
1793
- )?;
1793
+ ) {
1794
+ Ok(saved) => saved,
1795
+ Err(error) => {
1796
+ remove_empty_recording_artifacts(&session);
1797
+ return Err(error);
1798
+ }
1799
+ };
1794
1800
  saved.last_error = Some(match &stop_outcome {
1795
1801
  Err(stop_err) => {
1796
1802
  format!("{stop_err}. Segment consolidation failed: {merge_err}")
@@ -1814,14 +1820,20 @@ pub async fn native_fullscreen_recording_stop_and_upload(
1814
1820
  }
1815
1821
  }
1816
1822
 
1817
- let mut saved = saved_recording_from_session(
1823
+ let mut saved = match saved_recording_from_session(
1818
1824
  &session,
1819
1825
  &server_url,
1820
1826
  &recording_id,
1821
1827
  duration_ms,
1822
1828
  has_audio,
1823
1829
  has_camera,
1824
- )?;
1830
+ ) {
1831
+ Ok(saved) => saved,
1832
+ Err(error) => {
1833
+ remove_empty_recording_artifacts(&session);
1834
+ return Err(error);
1835
+ }
1836
+ };
1825
1837
  let stop_error = stop_outcome.err();
1826
1838
  if let Some(stop_err) = &stop_error {
1827
1839
  // Capture-side finalize/write failure: the on-disk file is incomplete
@@ -3150,6 +3162,12 @@ pub async fn native_fullscreen_pending_uploads(
3150
3162
  continue;
3151
3163
  }
3152
3164
  let Ok(saved) = read_saved_recording_metadata_path(&path) else {
3165
+ if std::fs::metadata(&path)
3166
+ .map(|metadata| metadata.len() == 0)
3167
+ .unwrap_or(false)
3168
+ {
3169
+ let _ = std::fs::remove_file(&path);
3170
+ }
3153
3171
  continue;
3154
3172
  };
3155
3173
  if saved_recording_has_local_artifact(&saved) {
@@ -3316,6 +3334,7 @@ fn orphan_recording_intent(
3316
3334
  system_audio_captured: false,
3317
3335
  has_camera: false,
3318
3336
  custom_pipeline: is_rewind,
3337
+ audio_cleanup_applied: false,
3319
3338
  saved_at: now_iso(),
3320
3339
  }
3321
3340
  }
@@ -3418,6 +3437,7 @@ fn recover_orphaned_recording(
3418
3437
  retry_count: 0,
3419
3438
  retry_attempt_id: None,
3420
3439
  custom_pipeline: intent.custom_pipeline,
3440
+ audio_cleanup_applied: intent.audio_cleanup_applied,
3421
3441
  corrupt: false,
3422
3442
  };
3423
3443
  write_saved_recording_metadata(app, &saved)?;
@@ -3537,6 +3557,7 @@ mod orphan_recovery_tests {
3537
3557
  false,
3538
3558
  false,
3539
3559
  true,
3560
+ false,
3540
3561
  )
3541
3562
  .unwrap();
3542
3563
  let intent_path = recording_intent_path(&media);
@@ -3932,6 +3953,7 @@ pub(crate) fn persist_recording_intent(
3932
3953
  system_audio_captured: bool,
3933
3954
  has_camera: bool,
3934
3955
  custom_pipeline: bool,
3956
+ audio_cleanup_applied: bool,
3935
3957
  ) -> Result<(), String> {
3936
3958
  if recording_id.trim().is_empty() || server_url.trim().is_empty() {
3937
3959
  return Err("recording recovery intent requires a recording ID and server URL".into());
@@ -3952,6 +3974,7 @@ pub(crate) fn persist_recording_intent(
3952
3974
  system_audio_captured,
3953
3975
  has_camera,
3954
3976
  custom_pipeline,
3977
+ audio_cleanup_applied,
3955
3978
  saved_at: now_iso(),
3956
3979
  };
3957
3980
  let data = serde_json::to_vec_pretty(&intent)
@@ -4018,7 +4041,41 @@ fn describe_recording_path(path: &Path) -> String {
4018
4041
  }
4019
4042
 
4020
4043
  fn saved_recording_has_local_artifact(saved: &SavedNativeRecording) -> bool {
4021
- saved.file_path.exists() || saved.segment_paths.iter().any(|path| path.exists())
4044
+ recording_file_has_bytes(&saved.file_path)
4045
+ || saved
4046
+ .segment_paths
4047
+ .iter()
4048
+ .any(|path| recording_file_has_bytes(path))
4049
+ }
4050
+
4051
+ fn recording_file_has_bytes(path: &Path) -> bool {
4052
+ std::fs::metadata(path)
4053
+ .map(|metadata| metadata.len() > 0)
4054
+ .unwrap_or(false)
4055
+ }
4056
+
4057
+ fn remove_empty_recording_artifacts(session: &NativeFullscreenSession) {
4058
+ for path in session.segments.iter().chain(std::iter::once(&session.path)) {
4059
+ if !path.exists() {
4060
+ continue;
4061
+ }
4062
+ if std::fs::metadata(path)
4063
+ .map(|metadata| metadata.len() > 0)
4064
+ .unwrap_or(true)
4065
+ {
4066
+ continue;
4067
+ }
4068
+ if let Err(error) = std::fs::remove_file(path) {
4069
+ if error.kind() != ErrorKind::NotFound {
4070
+ eprintln!(
4071
+ "[clips-tray] empty native recording cleanup failed for {}: {error}",
4072
+ path.display()
4073
+ );
4074
+ }
4075
+ } else {
4076
+ remove_recording_intent(path);
4077
+ }
4078
+ }
4022
4079
  }
4023
4080
 
4024
4081
  fn pending_uploads_dir(app: &AppHandle) -> Result<PathBuf, String> {
@@ -4412,6 +4469,7 @@ fn saved_recording_from_path(
4412
4469
  retry_count: 0,
4413
4470
  retry_attempt_id: None,
4414
4471
  custom_pipeline: session.custom_pipeline,
4472
+ audio_cleanup_applied: session.audio_cleanup_applied,
4415
4473
  corrupt: false,
4416
4474
  })
4417
4475
  }
@@ -4423,8 +4481,7 @@ fn write_saved_recording_metadata(
4423
4481
  let path = saved_recording_metadata_path(app, &saved.recording_id)?;
4424
4482
  let data = serde_json::to_vec_pretty(saved)
4425
4483
  .map_err(|e| format!("pending recording metadata encode failed: {e}"))?;
4426
- std::fs::write(path, data)
4427
- .map_err(|e| format!("pending recording metadata write failed: {e}"))?;
4484
+ write_bytes_atomically(&path, &data, "pending recording metadata")?;
4428
4485
  remove_recording_intent(&saved.file_path);
4429
4486
  for segment_path in &saved.segment_paths {
4430
4487
  remove_recording_intent(segment_path);
@@ -4432,6 +4489,21 @@ fn write_saved_recording_metadata(
4432
4489
  Ok(())
4433
4490
  }
4434
4491
 
4492
+ fn write_bytes_atomically(path: &Path, data: &[u8], label: &str) -> Result<(), String> {
4493
+ let file_name = path
4494
+ .file_name()
4495
+ .and_then(|value| value.to_str())
4496
+ .unwrap_or("recording");
4497
+ let temporary = path.with_file_name(format!(".{file_name}.tmp"));
4498
+ std::fs::write(&temporary, data)
4499
+ .map_err(|error| format!("{label} write failed: {error}"))?;
4500
+ if let Err(error) = std::fs::rename(&temporary, path) {
4501
+ let _ = std::fs::remove_file(&temporary);
4502
+ return Err(format!("{label} finalize failed: {error}"));
4503
+ }
4504
+ Ok(())
4505
+ }
4506
+
4435
4507
  fn read_saved_recording_metadata_path(path: &Path) -> Result<SavedNativeRecording, String> {
4436
4508
  let data =
4437
4509
  std::fs::read(path).map_err(|e| format!("pending recording metadata read failed: {e}"))?;
@@ -4506,6 +4578,7 @@ pub(crate) fn persist_shared_clip_recording(
4506
4578
  include_mic: bool,
4507
4579
  include_system_audio: bool,
4508
4580
  has_camera: bool,
4581
+ audio_cleanup_applied: bool,
4509
4582
  error: Option<&str>,
4510
4583
  ) -> Result<(), String> {
4511
4584
  let bytes = std::fs::metadata(path)
@@ -4534,6 +4607,7 @@ pub(crate) fn persist_shared_clip_recording(
4534
4607
  retry_count: u32::from(error.is_some()),
4535
4608
  retry_attempt_id: None,
4536
4609
  custom_pipeline: true,
4610
+ audio_cleanup_applied,
4537
4611
  corrupt: false,
4538
4612
  };
4539
4613
  write_saved_recording_metadata(app, &saved)?;
@@ -4745,6 +4819,9 @@ fn start_screencapturekit_recording(
4745
4819
  capture_region,
4746
4820
  },
4747
4821
  );
4822
+ session.audio_cleanup_applied = use_custom_sck_pipeline
4823
+ && include_audio
4824
+ && crate::config::feature_config(app).voice_cleanup_enabled;
4748
4825
  session.pending_recording_output = defer_recording_output;
4749
4826
  session.disk_monitor_stop = Some(spawn_disk_monitor(app.clone(), session.path.clone()));
4750
4827
  Ok(session)
@@ -4807,6 +4884,7 @@ fn start_screencapture_recording(
4807
4884
  capture_region,
4808
4885
  },
4809
4886
  );
4887
+ session.audio_cleanup_applied = false;
4810
4888
  session.disk_monitor_stop = Some(spawn_disk_monitor(app.clone(), session.path.clone()));
4811
4889
  Ok(session)
4812
4890
  }
@@ -4846,6 +4924,7 @@ fn new_fullscreen_session(
4846
4924
  restart,
4847
4925
  pending_recording_output: false,
4848
4926
  custom_pipeline,
4927
+ audio_cleanup_applied: false,
4849
4928
  #[cfg(target_os = "macos")]
4850
4929
  live_upload: None,
4851
4930
  had_live_upload: false,
@@ -5167,6 +5246,7 @@ pub(crate) async fn upload_finalized_native_artifact(
5167
5246
  artifact.mic_captured,
5168
5247
  artifact.system_audio_captured,
5169
5248
  artifact.custom_pipeline,
5249
+ artifact.audio_cleanup_applied,
5170
5250
  )?;
5171
5251
  let upload_result = upload_prepared_recording_file(
5172
5252
  app,
@@ -5233,6 +5313,7 @@ fn prepare_saved_recording_file(
5233
5313
  saved.mic_captured,
5234
5314
  saved.system_audio_captured,
5235
5315
  saved.custom_pipeline,
5316
+ saved.audio_cleanup_applied,
5236
5317
  )?;
5237
5318
  Ok((prepared, retry_combined_path))
5238
5319
  }
@@ -6712,13 +6793,18 @@ fn prepare_recording_file(
6712
6793
  // track written by the custom pipeline; the L/R downmix repair assumes
6713
6794
  // mic and system on separate stereo channels and must not run on it.
6714
6795
  audio_premixed: bool,
6796
+ audio_cleanup_applied: bool,
6715
6797
  ) -> Result<PreparedRecordingFile, String> {
6716
6798
  // Downmix only repairs mic+system L/R split. Applying it to mic-only
6717
6799
  // capture halves speech energy (~6 dB) when SCK puts the mic on one channel.
6718
6800
  let downmix_audio = mic_captured_audio && system_audio_captured && !audio_premixed;
6719
- let denoise_audio = mic_captured_audio;
6801
+ let voice_cleanup_enabled = crate::config::feature_config(app).voice_cleanup_enabled;
6802
+ let denoise_audio = mic_captured_audio && !audio_cleanup_applied && voice_cleanup_enabled;
6720
6803
  // Pregain only for mic-only (no system audio compete) — SCK has no AGC.
6721
- let mic_pregain = mic_captured_audio && !system_audio_captured;
6804
+ let mic_pregain = mic_captured_audio
6805
+ && !system_audio_captured
6806
+ && !audio_cleanup_applied
6807
+ && voice_cleanup_enabled;
6722
6808
  let metadata = std::fs::metadata(path).map_err(|e| {
6723
6809
  let diag = describe_recording_path(path);
6724
6810
  eprintln!("[clips-tray] native recording file missing at prepare: {e}; {diag}");
@@ -8176,8 +8262,7 @@ mod audio_track_probe_tests {
8176
8262
  audio_filter_chain, decide_prepared_audio_signal, mp4_has_audio_track,
8177
8263
  parse_ffmpeg_volume_db, AudioSignalProbe, PreparedAudioSignalDecision,
8178
8264
  AUDIO_DENOISE_FILTER, AUDIO_DOWNMIX_FILTER, AUDIO_DOWNMIX_MAKEUP_FILTER,
8179
- AUDIO_LOUDNESS_FILTER, AUDIO_MIC_CLEANUP_FILTER, AUDIO_MIC_PREGAIN_FILTER,
8180
- AUDIO_MIXED_HUM_FILTER,
8265
+ AUDIO_LOUDNESS_FILTER, AUDIO_MIC_PREGAIN_FILTER,
8181
8266
  };
8182
8267
  use std::io::Write;
8183
8268
 
@@ -8230,14 +8315,12 @@ mod audio_track_probe_tests {
8230
8315
  );
8231
8316
  assert_eq!(
8232
8317
  audio_filter_chain(false, true, true),
8233
- format!(
8234
- "{AUDIO_MIC_CLEANUP_FILTER},{AUDIO_DENOISE_FILTER},{AUDIO_MIC_PREGAIN_FILTER},{AUDIO_LOUDNESS_FILTER}"
8235
- )
8318
+ format!("{AUDIO_DENOISE_FILTER},{AUDIO_MIC_PREGAIN_FILTER},{AUDIO_LOUDNESS_FILTER}")
8236
8319
  );
8237
8320
  assert_eq!(
8238
8321
  audio_filter_chain(true, true, false),
8239
8322
  format!(
8240
- "{AUDIO_MIXED_HUM_FILTER},{AUDIO_DOWNMIX_FILTER},{AUDIO_DOWNMIX_MAKEUP_FILTER},{AUDIO_DENOISE_FILTER},{AUDIO_LOUDNESS_FILTER}"
8323
+ "{AUDIO_DOWNMIX_FILTER},{AUDIO_DOWNMIX_MAKEUP_FILTER},{AUDIO_DENOISE_FILTER},{AUDIO_LOUDNESS_FILTER}"
8241
8324
  )
8242
8325
  );
8243
8326
  }
@@ -8450,6 +8533,7 @@ mod segment_recovery_tests {
8450
8533
  },
8451
8534
  pending_recording_output: false,
8452
8535
  custom_pipeline: false,
8536
+ audio_cleanup_applied: false,
8453
8537
  #[cfg(target_os = "macos")]
8454
8538
  live_upload: None,
8455
8539
  had_live_upload: false,
@@ -186,6 +186,7 @@ pub(crate) fn rewind_clip_prepare(
186
186
  include_system_audio,
187
187
  has_camera,
188
188
  true,
189
+ crate::config::feature_config(&app).voice_cleanup_enabled && include_mic,
189
190
  ) {
190
191
  shared_sink.cancel();
191
192
  release_temporary_audio(&app, temporary_audio);
@@ -1129,6 +1130,7 @@ fn materialize(
1129
1130
  include_system_audio,
1130
1131
  has_camera,
1131
1132
  true,
1133
+ false,
1132
1134
  )?;
1133
1135
  }
1134
1136
  native_screen::materialize_mp4_slices_exact(&slices, &output, audio)?;
@@ -1220,6 +1222,7 @@ pub(crate) async fn rewind_clip_stop_and_upload(
1220
1222
  include_mic,
1221
1223
  include_system_audio,
1222
1224
  has_camera,
1225
+ crate::config::feature_config(&app).voice_cleanup_enabled && include_mic,
1223
1226
  Some(&error),
1224
1227
  )
1225
1228
  .err();
@@ -1251,6 +1254,7 @@ pub(crate) async fn rewind_clip_stop_and_upload(
1251
1254
  include_mic,
1252
1255
  include_system_audio,
1253
1256
  has_camera,
1257
+ crate::config::feature_config(&app).voice_cleanup_enabled && include_mic,
1254
1258
  None,
1255
1259
  ) {
1256
1260
  sink.cancel_upload();
@@ -1278,6 +1282,7 @@ pub(crate) async fn rewind_clip_stop_and_upload(
1278
1282
  include_mic,
1279
1283
  include_system_audio,
1280
1284
  has_camera,
1285
+ crate::config::feature_config(&app).voice_cleanup_enabled && include_mic,
1281
1286
  Some(&error),
1282
1287
  );
1283
1288
  native_screen::emit_native_upload_finished(
@@ -1343,6 +1348,7 @@ pub(crate) async fn rewind_clip_stop_and_upload(
1343
1348
  include_mic,
1344
1349
  include_system_audio,
1345
1350
  has_camera,
1351
+ artifact.audio_cleanup_applied,
1346
1352
  None,
1347
1353
  )?;
1348
1354
  let recovery_server_url = server_url.clone();
@@ -1380,6 +1386,7 @@ pub(crate) async fn rewind_clip_stop_and_upload(
1380
1386
  include_mic,
1381
1387
  include_system_audio,
1382
1388
  has_camera,
1389
+ artifact.audio_cleanup_applied,
1383
1390
  Some(error),
1384
1391
  );
1385
1392
  }
@@ -86,6 +86,7 @@ registerShareableResource({
86
86
  `/share/meeting/${encodeURIComponent(meeting.id)}`,
87
87
  },
88
88
  getDb,
89
+ ownerAccessIgnoresOrg: true,
89
90
  });
90
91
 
91
92
  registerShareableResource({
@@ -725,6 +725,7 @@ export const recordingEvents = table("recording_events", {
725
725
  "resume",
726
726
  "cta-click",
727
727
  "reaction",
728
+ "access-request",
728
729
  ],
729
730
  }).notNull(),
730
731
  // Video-time position (for progress/seek/pause/resume/reaction/cta-click).
@@ -17,6 +17,7 @@ import {
17
17
  import {
18
18
  getActiveOrganizationId,
19
19
  getCurrentOwnerEmail,
20
+ getDefaultRecordingVisibility,
20
21
  nanoid,
21
22
  } from "./recordings.js";
22
23
 
@@ -423,6 +424,10 @@ export async function materializeCalendarMeetingFromVirtualId(
423
424
  const db = getDb();
424
425
  const ownerEmail = getCurrentOwnerEmail();
425
426
  const orgId = (await getActiveOrganizationId().catch(() => null)) ?? null;
427
+ const defaultVisibility = await getDefaultRecordingVisibility(
428
+ orgId ?? live.account.orgId,
429
+ ownerEmail,
430
+ );
426
431
  const joinUrl = pickJoinUrl(live.event);
427
432
  const meetingId = nanoid();
428
433
  const nowIso = new Date().toISOString();
@@ -508,7 +513,7 @@ export async function materializeCalendarMeetingFromVirtualId(
508
513
  updatedAt: nowIso,
509
514
  ownerEmail,
510
515
  orgId,
511
- visibility: "private",
516
+ visibility: defaultVisibility,
512
517
  } as any);
513
518
 
514
519
  const participants = calendarEventParticipants(live.event).filter(
@@ -12,6 +12,10 @@
12
12
  import { defineTransactionalEmail } from "@agent-native/core/email-catalog";
13
13
 
14
14
  import { renderClipsInviteEmail } from "../../actions/invite-member.js";
15
+ import {
16
+ CLIPS_ACCESS_REQUEST_EMAIL_ID,
17
+ renderRecordingAccessRequestEmail,
18
+ } from "../../actions/request-recording-access.js";
15
19
  import {
16
20
  CLIPS_ACTIVITY_COMMENT_EMAIL_ID,
17
21
  CLIPS_ACTIVITY_REACTION_EMAIL_ID,
@@ -53,6 +57,25 @@ export function registerClipsEmails(): void {
53
57
  if (registered) return;
54
58
  registered = true;
55
59
 
60
+ defineTransactionalEmail({
61
+ id: CLIPS_ACCESS_REQUEST_EMAIL_ID,
62
+ name: "Clip access request",
63
+ trigger:
64
+ "A signed-in viewer requests access to a private Clip from its public share page. One request is recorded per viewer and Clip.",
65
+ recipientLabel: "Clip owner",
66
+ recipient:
67
+ "The owner of the private Clip. The in-app notification is stored even when email delivery is unavailable.",
68
+ senderLabel: "Agent-Native Clips",
69
+ sender: CLIPS_SENDER,
70
+ preview: () =>
71
+ renderRecordingAccessRequestEmail({
72
+ requesterName: "Sam Rivera",
73
+ requesterEmail: "sam.rivera@example.com",
74
+ recordingTitle: SAMPLE_TITLE,
75
+ url: "https://example.com/share/rec_sample",
76
+ }),
77
+ });
78
+
56
79
  defineTransactionalEmail({
57
80
  id: CLIPS_FIRST_VIEW_EMAIL_ID,
58
81
  name: "First view on a Clip",
@@ -41,6 +41,25 @@ export type PublicAgentTranscript =
41
41
  export type PublicAgentBugReport =
42
42
  | typeof schema.recordingBugReports._.inferSelect
43
43
  | null;
44
+ export type PublicAgentComment = {
45
+ id: string;
46
+ recordingId: string;
47
+ threadId: string;
48
+ parentId: string | null;
49
+ authorName: string | null;
50
+ content: string;
51
+ videoTimestampMs: number;
52
+ resolved: boolean;
53
+ createdAt: string;
54
+ updatedAt: string;
55
+ };
56
+ export type PublicAgentReaction = {
57
+ id: string;
58
+ emoji: string;
59
+ videoTimestampMs: number;
60
+ viewerName: string | null;
61
+ createdAt: string;
62
+ };
44
63
 
45
64
  export interface PublicAgentAccess {
46
65
  recording: PublicAgentRecording;
@@ -58,6 +77,7 @@ export type PublicAgentAccessResult =
58
77
  | { ok: false; failure: PublicAgentFailure };
59
78
 
60
79
  const DEFAULT_MAX_AGENT_FRAME_MEDIA_BYTES = 200 * 1024 * 1024;
80
+ export const MAX_PUBLIC_AGENT_HISTORY_ITEMS = 100;
61
81
  export const CLIPS_AGENT_ACCESS_TTL_SECONDS = 2 * 60 * 60;
62
82
  export { CLIPS_AGENT_ACCESS_PARAM };
63
83
 
@@ -525,6 +545,12 @@ export function buildPublicAgentContext({
525
545
  transcript,
526
546
  agentSegments,
527
547
  chapters,
548
+ comments,
549
+ reactions,
550
+ commentCount = comments.length,
551
+ commentsTruncated = false,
552
+ reactionCount = reactions.length,
553
+ reactionsTruncated = false,
528
554
  ctas,
529
555
  browserDiagnostics,
530
556
  bugReport,
@@ -534,6 +560,12 @@ export function buildPublicAgentContext({
534
560
  transcript: PublicAgentTranscript;
535
561
  agentSegments: ReturnType<typeof toAgentTranscriptSegments>;
536
562
  chapters: ReturnType<typeof parseAgentChapters>;
563
+ comments: PublicAgentComment[];
564
+ reactions: PublicAgentReaction[];
565
+ commentCount?: number;
566
+ commentsTruncated?: boolean;
567
+ reactionCount?: number;
568
+ reactionsTruncated?: boolean;
537
569
  ctas: Awaited<ReturnType<typeof loadAgentCtas>>;
538
570
  browserDiagnostics?: BrowserDiagnosticsData | null;
539
571
  bugReport?: PublicAgentBugReport;
@@ -639,6 +671,29 @@ export function buildPublicAgentContext({
639
671
  segments: agentSegments,
640
672
  segmentCount: agentSegments.length,
641
673
  },
674
+ comments: comments.map((comment) => ({
675
+ id: comment.id,
676
+ recordingId: comment.recordingId,
677
+ threadId: comment.threadId,
678
+ parentId: comment.parentId,
679
+ authorName: comment.authorName,
680
+ content: comment.content,
681
+ videoTimestampMs: comment.videoTimestampMs,
682
+ resolved: comment.resolved,
683
+ createdAt: comment.createdAt,
684
+ updatedAt: comment.updatedAt,
685
+ })),
686
+ commentCount,
687
+ commentsTruncated,
688
+ reactions: reactions.map((reaction) => ({
689
+ id: reaction.id,
690
+ emoji: reaction.emoji,
691
+ videoTimestampMs: reaction.videoTimestampMs,
692
+ viewerName: reaction.viewerName,
693
+ createdAt: reaction.createdAt,
694
+ })),
695
+ reactionCount,
696
+ reactionsTruncated,
642
697
  chapters,
643
698
  recommendedFrames: suggestedFrames,
644
699
  bugReport: compactBugReport(bugReport ?? null),
@@ -112,8 +112,9 @@ export async function getOrganizationDefaultVisibility(
112
112
  */
113
113
  export async function getDefaultRecordingVisibility(
114
114
  organizationId: string | null | undefined,
115
+ userEmail: string | null | undefined = getRequestUserEmail(),
115
116
  ): Promise<RecordingVisibility> {
116
- const email = getRequestUserEmail();
117
+ const email = userEmail;
117
118
  if (email) {
118
119
  const prefs = (await getUserSetting(
119
120
  normalizeOwnerEmail(email),
@@ -0,0 +1,49 @@
1
+ import type { FileUploadProvider } from "@agent-native/core/file-upload";
2
+
3
+ import type { StoredResumableSession } from "./resumable-session.js";
4
+ import { resolveResumableUploadProvider } from "./resumable-upload-provider.js";
5
+
6
+ /**
7
+ * Best-effort provider cleanup for a resumable session.
8
+ *
9
+ * Callers decide when it is safe to delete the local session handle. Keeping
10
+ * that decision outside this helper matters for ambiguous completion errors:
11
+ * the provider may already have materialized the object while the response
12
+ * was in flight, so a later retry may still need the handle to reconcile it.
13
+ */
14
+ export async function abortResumableUploadSession(
15
+ session: StoredResumableSession,
16
+ options: {
17
+ provider?: FileUploadProvider | null;
18
+ label?: string;
19
+ } = {},
20
+ ): Promise<boolean> {
21
+ const label = options.label ?? "resumable upload";
22
+ let provider = options.provider;
23
+ if (provider === undefined) {
24
+ try {
25
+ provider = await resolveResumableUploadProvider(session.providerId);
26
+ } catch (error) {
27
+ console.warn(`[${label}] provider lookup failed:`, error);
28
+ return false;
29
+ }
30
+ }
31
+
32
+ if (!provider?.resumable?.abortSession) {
33
+ console.warn(
34
+ `[${label}] provider ${session.providerId} cannot abort the resumable session`,
35
+ );
36
+ return false;
37
+ }
38
+
39
+ try {
40
+ await provider.resumable.abortSession({
41
+ sessionId: session.sessionId,
42
+ meta: session.meta,
43
+ });
44
+ return true;
45
+ } catch (error) {
46
+ console.warn(`[${label}] provider session cleanup failed:`, error);
47
+ return false;
48
+ }
49
+ }