@agent-native/core 0.114.6 → 0.114.7

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 (141) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +6 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/cli/mcp-config-writers.ts +6 -4
  5. package/corpus/core/src/cli/mcp.ts +124 -0
  6. package/corpus/core/src/cli/skills-content/help.ts +4 -3
  7. package/corpus/core/src/cli/skills-content/index.ts +1 -0
  8. package/corpus/core/src/cli/skills-content/rewind-skill.ts +52 -0
  9. package/corpus/core/src/cli/skills.ts +115 -13
  10. package/corpus/core/src/mcp/screen-memory-stdio.ts +1360 -104
  11. package/corpus/core/src/mcp-client/index.ts +9 -0
  12. package/corpus/core/src/mcp-client/screen-memory-local.ts +847 -13
  13. package/corpus/templates/clips/actions/apply-rewind-extension.ts +195 -0
  14. package/corpus/templates/clips/actions/delete-agent-recording-if-unpromoted.ts +64 -0
  15. package/corpus/templates/clips/actions/get-rewind-extension-request.ts +22 -0
  16. package/corpus/templates/clips/actions/get-screen-memory-status.ts +18 -2
  17. package/corpus/templates/clips/actions/list-rewind-extension-requests.ts +52 -0
  18. package/corpus/templates/clips/actions/make-recording-private-for-rewind.ts +55 -0
  19. package/corpus/templates/clips/actions/query-screen-memory-context.ts +4 -4
  20. package/corpus/templates/clips/actions/request-rewind-extension.ts +81 -0
  21. package/corpus/templates/clips/actions/update-rewind-extension-request.ts +47 -0
  22. package/corpus/templates/clips/app/components/editor/editor-layout.tsx +25 -0
  23. package/corpus/templates/clips/app/components/editor/editor-toolbar.tsx +36 -0
  24. package/corpus/templates/clips/app/components/editor/rewind-extension-dialog.tsx +338 -0
  25. package/corpus/templates/clips/app/components/editor/stitch-manager.tsx +1 -16
  26. package/corpus/templates/clips/app/components/editor/timeline.tsx +26 -0
  27. package/corpus/templates/clips/app/i18n/en-US.ts +18 -0
  28. package/corpus/templates/clips/app/lib/ffmpeg-export.ts +19 -12
  29. package/corpus/templates/clips/app/lib/rewind-visibility.ts +31 -0
  30. package/corpus/templates/clips/app/lib/timestamp-mapping.ts +7 -0
  31. package/corpus/templates/clips/app/lib/upload-file-client.ts +17 -0
  32. package/corpus/templates/clips/changelog/2026-07-14-rewind-keeps-recent-screen-context-local-and-turns-it-into-clips.md +6 -0
  33. package/corpus/templates/clips/changelog/2026-07-17-active-clips-keep-rewind-stable-and-settings-clear.md +12 -0
  34. package/corpus/templates/clips/changelog/2026-07-17-clicking-the-clips-icon-during-a-recording-now-opens-the-app.md +6 -0
  35. package/corpus/templates/clips/changelog/2026-07-17-default-mic-now-uses-the-mac-default-and-transcription-shares-one-capture.md +6 -0
  36. package/corpus/templates/clips/changelog/2026-07-17-full-screen-clips-finalize-even-when-screencapturekit-stop-hangs.md +6 -0
  37. package/corpus/templates/clips/changelog/2026-07-17-normal-full-screen-clips-no-longer-stop-immediately-after-th.md +6 -0
  38. package/corpus/templates/clips/changelog/2026-07-17-rewind-can-hand-one-bounded-private-clip-to-your-agent.md +12 -0
  39. package/corpus/templates/clips/changelog/2026-07-17-rewind-home-can-copy-a-local-agent-prompt.md +6 -0
  40. package/corpus/templates/clips/changelog/2026-07-17-rewind-now-has-a-calm-home-status-focused-first-run-consent-.md +6 -0
  41. package/corpus/templates/clips/changelog/2026-07-17-rewind-settings-are-smaller-clearer-and-agent-first.md +6 -0
  42. package/corpus/templates/clips/changelog/2026-07-17-the-full-clips-interface-stays-available-while-recording.md +6 -0
  43. package/corpus/templates/clips/changelog/2026-07-18-rewind-organizes-recent-work-into-local-searchable-chapters.md +8 -0
  44. package/corpus/templates/clips/changelog/2026-07-18-searchable-rewind-chapters.md +6 -0
  45. package/corpus/templates/clips/changelog/2026-07-19-rewind-backed-recordings-now-start-immediately-after-the-cou.md +6 -0
  46. package/corpus/templates/clips/changelog/2026-07-20-rewind-can-set-up-your-local-agent-once-so-future-requests-w.md +6 -0
  47. package/corpus/templates/clips/desktop/package.json +1 -0
  48. package/corpus/templates/clips/desktop/scripts/sign-macos-local.ts +12 -1
  49. package/corpus/templates/clips/desktop/src/app.tsx +2498 -239
  50. package/corpus/templates/clips/desktop/src/components/ReadinessPanel.tsx +21 -1
  51. package/corpus/templates/clips/desktop/src/components/Switch.tsx +3 -0
  52. package/corpus/templates/clips/desktop/src/hooks/useMeetingTranscription.ts +156 -24
  53. package/corpus/templates/clips/desktop/src/lib/native-recording-warm.ts +6 -7
  54. package/corpus/templates/clips/desktop/src/lib/recorder.ts +1763 -1160
  55. package/corpus/templates/clips/desktop/src/lib/rewind-agent-prompt.ts +9 -0
  56. package/corpus/templates/clips/desktop/src/lib/rewind-recording-start.ts +20 -0
  57. package/corpus/templates/clips/desktop/src/main.tsx +3 -0
  58. package/corpus/templates/clips/desktop/src/overlays/countdown.tsx +14 -14
  59. package/corpus/templates/clips/desktop/src/overlays/onboarding.tsx +4 -4
  60. package/corpus/templates/clips/desktop/src/overlays/preparing.tsx +13 -0
  61. package/corpus/templates/clips/desktop/src/overlays/toolbar.tsx +27 -2
  62. package/corpus/templates/clips/desktop/src/shared/config.ts +16 -0
  63. package/corpus/templates/clips/desktop/src/styles.css +956 -134
  64. package/corpus/templates/clips/desktop/src-tauri/Cargo.toml +5 -0
  65. package/corpus/templates/clips/desktop/src-tauri/build.rs +69 -1
  66. package/corpus/templates/clips/desktop/src-tauri/src/accessibility.rs +386 -12
  67. package/corpus/templates/clips/desktop/src-tauri/src/capture_audio_bus.rs +382 -0
  68. package/corpus/templates/clips/desktop/src-tauri/src/capture_graph.rs +777 -0
  69. package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +249 -22
  70. package/corpus/templates/clips/desktop/src-tauri/src/config.rs +148 -1
  71. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +58 -4
  72. package/corpus/templates/clips/desktop/src-tauri/src/logfile.rs +13 -1
  73. package/corpus/templates/clips/desktop/src-tauri/src/native_screen/custom_capture.rs +1505 -51
  74. package/corpus/templates/clips/desktop/src-tauri/src/native_screen/live_upload.rs +124 -0
  75. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +1006 -95
  76. package/corpus/templates/clips/desktop/src-tauri/src/rewind_capture_suspension.rs +165 -0
  77. package/corpus/templates/clips/desktop/src-tauri/src/rewind_chapters.rs +2434 -0
  78. package/corpus/templates/clips/desktop/src-tauri/src/rewind_clip.rs +1650 -0
  79. package/corpus/templates/clips/desktop/src-tauri/src/rewind_egress.rs +643 -0
  80. package/corpus/templates/clips/desktop/src-tauri/src/rewind_local_ask.rs +803 -0
  81. package/corpus/templates/clips/desktop/src-tauri/src/rewind_meeting_history.rs +595 -0
  82. package/corpus/templates/clips/desktop/src-tauri/src/screen_memory.rs +2900 -91
  83. package/corpus/templates/clips/desktop/src-tauri/src/screen_memory_ocr.rs +360 -0
  84. package/corpus/templates/clips/desktop/src-tauri/src/screen_memory_transcript.rs +408 -0
  85. package/corpus/templates/clips/desktop/src-tauri/src/shortcuts.rs +114 -33
  86. package/corpus/templates/clips/desktop/src-tauri/src/state.rs +2 -2
  87. package/corpus/templates/clips/desktop/src-tauri/src/system_audio.rs +13 -3
  88. package/corpus/templates/clips/desktop/src-tauri/src/tray.rs +4 -1
  89. package/corpus/templates/clips/desktop/src-tauri/src/util.rs +7 -13
  90. package/corpus/templates/clips/desktop/src-tauri/src/whisper_speech.rs +198 -11
  91. package/corpus/templates/clips/desktop/src-tauri/tauri.alpha.conf.json +17 -0
  92. package/dist/cli/mcp-config-writers.d.ts +2 -2
  93. package/dist/cli/mcp-config-writers.d.ts.map +1 -1
  94. package/dist/cli/mcp-config-writers.js +6 -6
  95. package/dist/cli/mcp-config-writers.js.map +1 -1
  96. package/dist/cli/mcp.d.ts +14 -0
  97. package/dist/cli/mcp.d.ts.map +1 -1
  98. package/dist/cli/mcp.js +79 -0
  99. package/dist/cli/mcp.js.map +1 -1
  100. package/dist/cli/skills-content/help.d.ts +1 -1
  101. package/dist/cli/skills-content/help.d.ts.map +1 -1
  102. package/dist/cli/skills-content/help.js +4 -3
  103. package/dist/cli/skills-content/help.js.map +1 -1
  104. package/dist/cli/skills-content/index.d.ts +1 -0
  105. package/dist/cli/skills-content/index.d.ts.map +1 -1
  106. package/dist/cli/skills-content/index.js +1 -0
  107. package/dist/cli/skills-content/index.js.map +1 -1
  108. package/dist/cli/skills-content/rewind-skill.d.ts +2 -0
  109. package/dist/cli/skills-content/rewind-skill.d.ts.map +1 -0
  110. package/dist/cli/skills-content/rewind-skill.js +53 -0
  111. package/dist/cli/skills-content/rewind-skill.js.map +1 -0
  112. package/dist/cli/skills.d.ts +6 -0
  113. package/dist/cli/skills.d.ts.map +1 -1
  114. package/dist/cli/skills.js +90 -14
  115. package/dist/cli/skills.js.map +1 -1
  116. package/dist/mcp/screen-memory-stdio.d.ts +365 -0
  117. package/dist/mcp/screen-memory-stdio.d.ts.map +1 -1
  118. package/dist/mcp/screen-memory-stdio.js +1014 -91
  119. package/dist/mcp/screen-memory-stdio.js.map +1 -1
  120. package/dist/mcp-client/index.d.ts +1 -1
  121. package/dist/mcp-client/index.d.ts.map +1 -1
  122. package/dist/mcp-client/index.js +1 -1
  123. package/dist/mcp-client/index.js.map +1 -1
  124. package/dist/mcp-client/screen-memory-local.d.ts +90 -0
  125. package/dist/mcp-client/screen-memory-local.d.ts.map +1 -1
  126. package/dist/mcp-client/screen-memory-local.js +590 -12
  127. package/dist/mcp-client/screen-memory-local.js.map +1 -1
  128. package/dist/notifications/routes.d.ts +3 -3
  129. package/dist/observability/routes.d.ts +1 -1
  130. package/dist/provider-api/actions/provider-api.d.ts +4 -4
  131. package/dist/resources/handlers.d.ts +1 -1
  132. package/package.json +1 -1
  133. package/src/cli/mcp-config-writers.ts +6 -4
  134. package/src/cli/mcp.ts +124 -0
  135. package/src/cli/skills-content/help.ts +4 -3
  136. package/src/cli/skills-content/index.ts +1 -0
  137. package/src/cli/skills-content/rewind-skill.ts +52 -0
  138. package/src/cli/skills.ts +115 -13
  139. package/src/mcp/screen-memory-stdio.ts +1360 -104
  140. package/src/mcp-client/index.ts +9 -0
  141. package/src/mcp-client/screen-memory-local.ts +847 -13
@@ -1,3 +1,4 @@
1
+ use serde::Serialize;
1
2
  #[cfg(target_os = "macos")]
2
3
  use std::io::Write;
3
4
  use std::path::PathBuf;
@@ -40,6 +41,7 @@ const COUNTDOWN_CONTROL_HIT_PAD: f64 = 8.0;
40
41
  // countdown overlay so only the button zones are interactive.
41
42
  static COUNTDOWN_CONTROL_TRACKING: AtomicBool = AtomicBool::new(false);
42
43
  const BUBBLE_LABEL: &str = "bubble";
44
+ const PREPARING_LABEL: &str = "preparing";
43
45
  const FINALIZING_LABEL: &str = "finalizing";
44
46
  const FLOW_BAR_LABEL: &str = "flow-bar";
45
47
  const REGION_GUIDES_LABEL: &str = "region-guides";
@@ -50,6 +52,7 @@ const OVERLAY_LABELS: &[&str] = &[
50
52
  COUNTDOWN_LABEL,
51
53
  TOOLBAR_LABEL,
52
54
  BUBBLE_LABEL,
55
+ PREPARING_LABEL,
53
56
  FINALIZING_LABEL,
54
57
  FLOW_BAR_LABEL,
55
58
  REGION_GUIDES_LABEL,
@@ -355,12 +358,11 @@ fn load_bubble_position(app: &AppHandle) -> Option<(i32, i32)> {
355
358
  /// cursor events so the user can still click into whatever they're about to
356
359
  /// record, and closes itself when the countdown finishes.
357
360
  #[tauri::command]
358
- pub async fn show_countdown(app: AppHandle) -> Result<(), String> {
361
+ pub async fn show_countdown(app: AppHandle) -> Result<u64, String> {
359
362
  dlog!("[clips-tray] show_countdown invoked");
360
363
  mark_popover_shown(&app);
361
364
  if let Some(existing) = app.get_webview_window(COUNTDOWN_LABEL) {
362
365
  stop_countdown_control_tracking();
363
- let _ = app.emit("clips:countdown-shortcuts-active", false);
364
366
  let _ = existing.close();
365
367
  }
366
368
  let (mx, my, mw, mh) = tray_monitor_physical_rect(&app);
@@ -379,11 +381,10 @@ pub async fn show_countdown(app: AppHandle) -> Result<(), String> {
379
381
  .skip_taskbar(true)
380
382
  .shadow(false)
381
383
  .visible(false)
382
- // Don't steal focus from the popover when the overlay opens —
383
- // otherwise macOS fires Focused(false) on the popover, which
384
- // kicks off a cascade of blur-related React re-renders and
385
- // eventually (past the 1500ms guard) auto-hides the popover.
386
- .focused(false)
384
+ // The countdown is intentionally modal for three seconds so its
385
+ // advertised Return/Escape controls work without system-wide key
386
+ // monitoring. The recorder has already parked the popover.
387
+ .focused(true)
387
388
  .build()
388
389
  .map_err(|e| {
389
390
  eprintln!("[clips-tray] countdown build failed: {}", e);
@@ -394,11 +395,26 @@ pub async fn show_countdown(app: AppHandle) -> Result<(), String> {
394
395
  let _ = win.set_ignore_cursor_events(true);
395
396
  set_capture_excluded(&win);
396
397
  configure_overlay_behavior(&win);
397
- let _ = win.show();
398
- let _ = app.emit("clips:countdown-shortcuts-active", true);
398
+ let generation = match crate::shortcuts::prepare_countdown_shortcuts(app.clone()).await {
399
+ Ok(generation) => generation,
400
+ Err(error) => {
401
+ let _ = win.close();
402
+ return Err(error);
403
+ }
404
+ };
405
+ // This is a short, explicit modal moment. Making the overlay the actual
406
+ // key window lets its ordinary DOM handler receive Return/Escape without
407
+ // broad, system-wide Input Monitoring. Closing it restores the user's
408
+ // prior application before capture begins.
409
+ present_interactive_window(&win);
399
410
  start_countdown_control_tracking(&app);
400
411
  dlog!("[clips-tray] countdown shown");
401
- Ok(())
412
+ Ok(generation)
413
+ }
414
+
415
+ #[tauri::command]
416
+ pub async fn finish_countdown_shortcuts(app: AppHandle, generation: u64) -> Result<(), String> {
417
+ crate::shortcuts::finish_countdown_shortcuts(app, generation).await
402
418
  }
403
419
 
404
420
  /// True when the global cursor sits inside either circular cancel/skip button
@@ -458,6 +474,65 @@ fn stop_countdown_control_tracking() {
458
474
  COUNTDOWN_CONTROL_TRACKING.store(false, Ordering::SeqCst);
459
475
  }
460
476
 
477
+ /// Compact visible readiness state for the slow work that intentionally runs
478
+ /// before the numeric countdown. This capture-excluded card briefly takes
479
+ /// focus as the first stage of the explicit modal start flow, so the user sees
480
+ /// that Start was accepted without changing the exact countdown-zero boundary.
481
+ #[tauri::command]
482
+ pub async fn show_preparing(app: AppHandle) -> Result<(), String> {
483
+ if let Some(existing) = app.get_webview_window(PREPARING_LABEL) {
484
+ let _ = existing.close();
485
+ }
486
+ let (mx, my, mw, mh) = tray_monitor_physical_rect(&app);
487
+ let scale = overlay_scale_factor(&app);
488
+ let content_w: u32 = (260.0 * scale).round() as u32;
489
+ let content_h: u32 = (58.0 * scale).round() as u32;
490
+ let margin: i32 = (14.0 * scale).round() as i32;
491
+ let gutter = overlay_shadow_gutter_physical(&app);
492
+ let w = content_w + gutter * 2;
493
+ let h = content_h + gutter * 2;
494
+ let x = (mx + (mw.saturating_sub(w) / 2) as i32).max(mx);
495
+ let y = (my + mh as i32 - h as i32 - margin).max(my);
496
+ let win = WebviewWindowBuilder::new(&app, PREPARING_LABEL, build_overlay_url("preparing"))
497
+ .title("Preparing recording")
498
+ .decorations(false)
499
+ .transparent(true)
500
+ .always_on_top(true)
501
+ .skip_taskbar(true)
502
+ .resizable(false)
503
+ .shadow(false)
504
+ .visible(false)
505
+ .focused(true)
506
+ .build()
507
+ .map_err(|error| format!("preparing window build failed: {error}"))?;
508
+ let _ = win.set_size(tauri::Size::Physical(PhysicalSize::new(w, h)));
509
+ let _ = win.set_position(PhysicalPosition::new(x, y));
510
+ let _ = win.set_ignore_cursor_events(true);
511
+ set_capture_excluded(&win);
512
+ configure_overlay_behavior(&win);
513
+ // Preparation and countdown are one explicit modal start flow. Making the
514
+ // compact readiness card key ensures it gets a real first paint and is
515
+ // announced before the full-screen countdown replaces it.
516
+ present_interactive_window(&win);
517
+ let _ = app.emit("clips:toolbar-preparing", true);
518
+ // A newly-created webview needs one paint before the async preparation can
519
+ // race ahead and replace it with the countdown. Without this brief yield,
520
+ // fast machines can show only a disabled toolbar and never render the
521
+ // promised textual readiness state.
522
+ tokio::time::sleep(Duration::from_millis(120)).await;
523
+ dlog!("[clips-tray] preparing recording shown");
524
+ Ok(())
525
+ }
526
+
527
+ #[tauri::command]
528
+ pub async fn hide_preparing(app: AppHandle) -> Result<(), String> {
529
+ let _ = app.emit("clips:toolbar-preparing", false);
530
+ if let Some(window) = app.get_webview_window(PREPARING_LABEL) {
531
+ let _ = window.close();
532
+ }
533
+ Ok(())
534
+ }
535
+
461
536
  /// Compact bottom-left status window shown while the recorder flushes its
462
537
  /// final chunks and awaits the server finalize. Keeping the native window near
463
538
  /// the card's bounds makes its open/dismiss controls clickable without placing
@@ -988,7 +1063,6 @@ pub async fn hide_overlays(
988
1063
  preserve_finalizing: Option<bool>,
989
1064
  ) -> Result<(), String> {
990
1065
  stop_countdown_control_tracking();
991
- let _ = app.emit("clips:countdown-shortcuts-active", false);
992
1066
  for label in overlay_labels_to_hide(preserve_finalizing.unwrap_or(false)) {
993
1067
  if let Some(w) = app.get_webview_window(label) {
994
1068
  let _ = w.close();
@@ -1011,7 +1085,6 @@ pub async fn hide_overlays(
1011
1085
  #[tauri::command]
1012
1086
  pub async fn hide_recording_chrome(app: AppHandle) -> Result<(), String> {
1013
1087
  stop_countdown_control_tracking();
1014
- let _ = app.emit("clips:countdown-shortcuts-active", false);
1015
1088
  // The countdown + toolbar always tear down on recording stop. The region
1016
1089
  // guides only tear down when they aren't pinned on-screen via the always-on
1017
1090
  // toggle — otherwise we'd flicker close→reopen right after stop.
@@ -1128,21 +1201,21 @@ pub fn open_macos_privacy_settings(pane: String) -> Result<(), String> {
1128
1201
  #[cfg(target_os = "macos")]
1129
1202
  {
1130
1203
  let url = match pane.as_str() {
1131
- "camera" => "x-apple.systempreferences:com.apple.preference.security?Privacy_Camera",
1204
+ "camera" => "x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_Camera",
1132
1205
  "microphone" => {
1133
- "x-apple.systempreferences:com.apple.preference.security?Privacy_Microphone"
1206
+ "x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_Microphone"
1134
1207
  }
1135
1208
  "screen" => {
1136
- "x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture"
1209
+ "x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_ScreenCapture"
1137
1210
  }
1138
1211
  "speech" => {
1139
- "x-apple.systempreferences:com.apple.preference.security?Privacy_SpeechRecognition"
1212
+ "x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_SpeechRecognition"
1140
1213
  }
1141
1214
  "accessibility" => {
1142
- "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility"
1215
+ "x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_Accessibility"
1143
1216
  }
1144
1217
  "input-monitoring" => {
1145
- "x-apple.systempreferences:com.apple.preference.security?Privacy_ListenEvent"
1218
+ "x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_ListenEvent"
1146
1219
  }
1147
1220
  _ => return Err(format!("Unknown macOS privacy pane: {pane}")),
1148
1221
  };
@@ -1154,6 +1227,157 @@ pub fn open_macos_privacy_settings(pane: String) -> Result<(), String> {
1154
1227
  }
1155
1228
  }
1156
1229
 
1230
+ #[derive(Clone, Debug, Serialize)]
1231
+ #[serde(rename_all = "camelCase")]
1232
+ pub struct RewindExcludedApplication {
1233
+ bundle_id: String,
1234
+ name: String,
1235
+ path: Option<String>,
1236
+ installed: bool,
1237
+ }
1238
+
1239
+ #[cfg(target_os = "macos")]
1240
+ fn app_bundle_id(path: &str) -> Option<String> {
1241
+ let output = Command::new("/usr/bin/mdls")
1242
+ .args(["-name", "kMDItemCFBundleIdentifier", "-raw", path])
1243
+ .output()
1244
+ .ok()?;
1245
+ if !output.status.success() {
1246
+ return None;
1247
+ }
1248
+ let bundle_id = String::from_utf8_lossy(&output.stdout).trim().to_string();
1249
+ (!bundle_id.is_empty() && bundle_id != "(null)").then_some(bundle_id)
1250
+ }
1251
+
1252
+ #[cfg(target_os = "macos")]
1253
+ fn app_name_from_path(path: &str) -> String {
1254
+ std::path::Path::new(path)
1255
+ .file_stem()
1256
+ .and_then(|name| name.to_str())
1257
+ .unwrap_or("Application")
1258
+ .to_string()
1259
+ }
1260
+
1261
+ fn fallback_app_name(bundle_id: &str) -> String {
1262
+ match bundle_id {
1263
+ "com.1password.1password" | "com.agilebits.onepassword7" => "1Password".to_string(),
1264
+ "com.bitwarden.desktop" => "Bitwarden".to_string(),
1265
+ "com.dashlane.dashlane" => "Dashlane".to_string(),
1266
+ "com.lastpass.lastpass" => "LastPass".to_string(),
1267
+ _ => bundle_id
1268
+ .rsplit('.')
1269
+ .next()
1270
+ .filter(|name| !name.is_empty())
1271
+ .unwrap_or("Application")
1272
+ .to_string(),
1273
+ }
1274
+ }
1275
+
1276
+ #[cfg(target_os = "macos")]
1277
+ fn find_installed_app(bundle_id: &str) -> Option<String> {
1278
+ let query = format!("kMDItemCFBundleIdentifier == '{bundle_id}'");
1279
+ let output = Command::new("/usr/bin/mdfind").arg(query).output().ok()?;
1280
+ if !output.status.success() {
1281
+ return None;
1282
+ }
1283
+ String::from_utf8_lossy(&output.stdout)
1284
+ .lines()
1285
+ .find(|path| path.ends_with(".app"))
1286
+ .map(str::to_string)
1287
+ }
1288
+
1289
+ #[tauri::command]
1290
+ pub fn resolve_rewind_excluded_apps(
1291
+ bundle_ids: Vec<String>,
1292
+ ) -> Result<Vec<RewindExcludedApplication>, String> {
1293
+ #[cfg(target_os = "macos")]
1294
+ {
1295
+ return Ok(bundle_ids
1296
+ .into_iter()
1297
+ .map(|bundle_id| {
1298
+ let path = find_installed_app(&bundle_id);
1299
+ let name = path
1300
+ .as_deref()
1301
+ .map(app_name_from_path)
1302
+ .unwrap_or_else(|| fallback_app_name(&bundle_id));
1303
+ RewindExcludedApplication {
1304
+ bundle_id,
1305
+ name,
1306
+ installed: path.is_some(),
1307
+ path,
1308
+ }
1309
+ })
1310
+ .collect());
1311
+ }
1312
+ #[cfg(not(target_os = "macos"))]
1313
+ {
1314
+ Ok(bundle_ids
1315
+ .into_iter()
1316
+ .map(|bundle_id| RewindExcludedApplication {
1317
+ name: fallback_app_name(&bundle_id),
1318
+ bundle_id,
1319
+ path: None,
1320
+ installed: false,
1321
+ })
1322
+ .collect())
1323
+ }
1324
+ }
1325
+
1326
+ #[cfg(target_os = "macos")]
1327
+ fn choose_rewind_excluded_apps_blocking() -> Result<Vec<RewindExcludedApplication>, String> {
1328
+ let script = r#"
1329
+ set chosenApps to choose application with prompt "Choose apps Rewind should never remember" with multiple selections allowed
1330
+ set chosenPaths to {}
1331
+ repeat with chosenApp in chosenApps
1332
+ set end of chosenPaths to POSIX path of (chosenApp as alias)
1333
+ end repeat
1334
+ set AppleScript's text item delimiters to linefeed
1335
+ return chosenPaths as text
1336
+ "#;
1337
+ let output = Command::new("/usr/bin/osascript")
1338
+ .args(["-e", script])
1339
+ .output()
1340
+ .map_err(|error| format!("Could not open the application picker: {error}"))?;
1341
+ if !output.status.success() {
1342
+ let error = String::from_utf8_lossy(&output.stderr);
1343
+ if error.contains("-128") || error.to_ascii_lowercase().contains("canceled") {
1344
+ return Ok(Vec::new());
1345
+ }
1346
+ return Err(format!("Could not choose applications: {}", error.trim()));
1347
+ }
1348
+ let mut apps = Vec::new();
1349
+ for path in String::from_utf8_lossy(&output.stdout)
1350
+ .lines()
1351
+ .map(str::trim)
1352
+ .filter(|path| !path.is_empty())
1353
+ {
1354
+ let Some(bundle_id) = app_bundle_id(path) else {
1355
+ continue;
1356
+ };
1357
+ apps.push(RewindExcludedApplication {
1358
+ bundle_id,
1359
+ name: app_name_from_path(path),
1360
+ path: Some(path.to_string()),
1361
+ installed: true,
1362
+ });
1363
+ }
1364
+ Ok(apps)
1365
+ }
1366
+
1367
+ #[tauri::command]
1368
+ pub async fn choose_rewind_excluded_apps() -> Result<Vec<RewindExcludedApplication>, String> {
1369
+ #[cfg(target_os = "macos")]
1370
+ {
1371
+ return tauri::async_runtime::spawn_blocking(choose_rewind_excluded_apps_blocking)
1372
+ .await
1373
+ .map_err(|error| format!("The application picker stopped unexpectedly: {error}"))?;
1374
+ }
1375
+ #[cfg(not(target_os = "macos"))]
1376
+ {
1377
+ Err("Choosing excluded applications is currently available on macOS only.".to_string())
1378
+ }
1379
+ }
1380
+
1157
1381
  #[tauri::command]
1158
1382
  pub fn open_local_recording_folder(path: String) -> Result<(), String> {
1159
1383
  let trimmed = path.trim();
@@ -1902,9 +2126,9 @@ fn type_text_unicode(text: &str, target_bundle_id: Option<String>) {
1902
2126
  #[cfg(not(target_os = "macos"))]
1903
2127
  fn type_text_unicode(_text: &str) {}
1904
2128
 
1905
- /// Record the popover's current recording state. When active, clicking the
1906
- /// tray icon emits a stop event instead of toggling the popover — so the
1907
- /// user can stop a recording from anywhere with one click.
2129
+ /// Record the popover's current recording state. While active, ordinary app
2130
+ /// and tray opens restore the parked popover; stopping remains an explicit
2131
+ /// action in the popover, toolbar, or tray menu.
1908
2132
  #[tauri::command]
1909
2133
  pub async fn set_recording_state(app: AppHandle, active: bool) -> Result<(), String> {
1910
2134
  dlog!("[clips-tray] set_recording_state active={}", active);
@@ -2254,7 +2478,10 @@ fn is_pinhole_popover(window: &WebviewWindow) -> bool {
2254
2478
 
2255
2479
  fn present_popover(app: &AppHandle, window: &WebviewWindow) {
2256
2480
  clear_voice_wake_state(app);
2257
- set_capture_included(window);
2481
+ // Reopening Clips must not silently override the user's capture-visibility
2482
+ // preference. `set_capture_excluded` keeps the window private by default
2483
+ // and includes it only when "Show Clips in screen captures" is enabled.
2484
+ set_capture_excluded(window);
2258
2485
  // Re-apply Space behavior — `orderOut:` resets it, so without this the
2259
2486
  // popover sticks to whichever Space it was first shown on.
2260
2487
  configure_overlay_behavior(window);
@@ -49,6 +49,18 @@ pub struct ScreenMemoryConfig {
49
49
  pub segment_seconds: u64,
50
50
  #[serde(default = "default_screen_memory_sample_interval_seconds")]
51
51
  pub sample_interval_seconds: u64,
52
+ #[serde(default)]
53
+ pub capture_mode: RewindCaptureMode,
54
+ #[serde(default = "default_rewind_review_before_sending")]
55
+ pub review_before_sending: bool,
56
+ #[serde(default = "default_rewind_auto_preview_before_sending")]
57
+ pub auto_preview_before_sending: bool,
58
+ #[serde(default)]
59
+ pub agent_clip_retention: RewindAgentClipRetention,
60
+ #[serde(default = "default_screen_memory_excluded_bundle_ids")]
61
+ pub excluded_bundle_ids: Vec<String>,
62
+ #[serde(default = "default_screen_memory_exclude_private_windows")]
63
+ pub exclude_private_windows: bool,
52
64
  }
53
65
 
54
66
  impl Default for ScreenMemoryConfig {
@@ -60,10 +72,39 @@ impl Default for ScreenMemoryConfig {
60
72
  max_bytes: default_screen_memory_max_bytes(),
61
73
  segment_seconds: default_screen_memory_segment_seconds(),
62
74
  sample_interval_seconds: default_screen_memory_sample_interval_seconds(),
75
+ capture_mode: RewindCaptureMode::default(),
76
+ review_before_sending: default_rewind_review_before_sending(),
77
+ auto_preview_before_sending: default_rewind_auto_preview_before_sending(),
78
+ agent_clip_retention: RewindAgentClipRetention::default(),
79
+ excluded_bundle_ids: default_screen_memory_excluded_bundle_ids(),
80
+ exclude_private_windows: default_screen_memory_exclude_private_windows(),
63
81
  }
64
82
  }
65
83
  }
66
84
 
85
+ /// The local capture tracks Rewind is allowed to retain. Audio collection is
86
+ /// explicit so an existing local buffer never begins recording sound by default.
87
+ #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
88
+ #[serde(rename_all = "kebab-case")]
89
+ pub enum RewindCaptureMode {
90
+ #[default]
91
+ Visuals,
92
+ VisualsAudio,
93
+ }
94
+
95
+ #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
96
+ #[serde(rename_all = "kebab-case")]
97
+ pub enum RewindAgentClipRetention {
98
+ #[default]
99
+ Forever,
100
+ #[serde(rename = "24-hours")]
101
+ Hours24,
102
+ #[serde(rename = "7-days")]
103
+ Days7,
104
+ #[serde(rename = "30-days")]
105
+ Days30,
106
+ }
107
+
67
108
  #[derive(Debug, Clone, Serialize, Deserialize)]
68
109
  #[serde(rename_all = "camelCase")]
69
110
  pub struct FeatureConfig {
@@ -130,7 +171,24 @@ fn default_whisper_model_enabled() -> bool {
130
171
  }
131
172
 
132
173
  fn default_screen_memory_retention_hours() -> u32 {
133
- 24
174
+ 8
175
+ }
176
+
177
+ fn default_screen_memory_excluded_bundle_ids() -> Vec<String> {
178
+ [
179
+ "com.1password.1password",
180
+ "com.agilebits.onepassword7",
181
+ "com.bitwarden.desktop",
182
+ "com.dashlane.dashlane",
183
+ "com.lastpass.lastpass",
184
+ ]
185
+ .into_iter()
186
+ .map(str::to_string)
187
+ .collect()
188
+ }
189
+
190
+ fn default_screen_memory_exclude_private_windows() -> bool {
191
+ false
134
192
  }
135
193
 
136
194
  fn default_screen_memory_max_bytes() -> u64 {
@@ -145,6 +203,14 @@ fn default_screen_memory_sample_interval_seconds() -> u64 {
145
203
  10
146
204
  }
147
205
 
206
+ fn default_rewind_review_before_sending() -> bool {
207
+ true
208
+ }
209
+
210
+ fn default_rewind_auto_preview_before_sending() -> bool {
211
+ true
212
+ }
213
+
148
214
  impl Default for FeatureConfig {
149
215
  fn default() -> Self {
150
216
  Self {
@@ -268,6 +334,14 @@ pub async fn get_feature_config(app: AppHandle) -> Result<FeatureConfig, String>
268
334
  #[tauri::command]
269
335
  pub async fn set_feature_config(app: AppHandle, config: FeatureConfig) -> Result<(), String> {
270
336
  let previous = load_config(&app);
337
+ if (crate::rewind_clip::is_active(&app) || crate::util::is_recording_active(&app))
338
+ && rewind_capture_contract_changed(&previous.screen_memory, &config.screen_memory)
339
+ {
340
+ return Err(
341
+ "Rewind capture settings stay unchanged while a Clip is recording. Stop the Clip before turning Rewind on or off, pausing it, or changing what it remembers."
342
+ .to_string(),
343
+ );
344
+ }
271
345
  if previous.launch_at_login_enabled != config.launch_at_login_enabled {
272
346
  if let Err(err) = apply_launch_at_login(&app, config.launch_at_login_enabled) {
273
347
  eprintln!("[clips-tray] launch-at-login apply failed: {err}");
@@ -296,3 +370,76 @@ pub async fn set_feature_config(app: AppHandle, config: FeatureConfig) -> Result
296
370
  let _ = app.emit("app:feature-config-changed", config);
297
371
  Ok(())
298
372
  }
373
+
374
+ fn rewind_capture_contract_changed(
375
+ previous: &ScreenMemoryConfig,
376
+ next: &ScreenMemoryConfig,
377
+ ) -> bool {
378
+ previous.enabled != next.enabled
379
+ || previous.paused != next.paused
380
+ || previous.capture_mode != next.capture_mode
381
+ }
382
+
383
+ #[cfg(test)]
384
+ mod tests {
385
+ use super::*;
386
+
387
+ #[test]
388
+ fn screen_memory_config_defaults_agent_handoff_fields_when_loading_legacy_json() {
389
+ let config: ScreenMemoryConfig = serde_json::from_value(serde_json::json!({
390
+ "enabled": true,
391
+ "retentionHours": 24
392
+ }))
393
+ .unwrap();
394
+
395
+ assert_eq!(config.retention_hours, 24);
396
+ assert_eq!(config.capture_mode, RewindCaptureMode::Visuals);
397
+ assert!(config.review_before_sending);
398
+ assert!(config.auto_preview_before_sending);
399
+ assert_eq!(
400
+ config.agent_clip_retention,
401
+ RewindAgentClipRetention::Forever
402
+ );
403
+ assert!(!config.exclude_private_windows);
404
+ assert!(config
405
+ .excluded_bundle_ids
406
+ .contains(&"com.1password.1password".to_string()));
407
+ }
408
+
409
+ #[test]
410
+ fn rewind_enums_use_stable_kebab_case_values() {
411
+ assert_eq!(
412
+ serde_json::to_string(&RewindCaptureMode::VisualsAudio).unwrap(),
413
+ "\"visuals-audio\""
414
+ );
415
+ assert_eq!(
416
+ serde_json::to_string(&RewindAgentClipRetention::Days7).unwrap(),
417
+ "\"7-days\""
418
+ );
419
+ }
420
+
421
+ #[test]
422
+ fn active_clip_interlock_only_blocks_capture_contract_changes() {
423
+ let previous = ScreenMemoryConfig {
424
+ enabled: true,
425
+ paused: false,
426
+ capture_mode: RewindCaptureMode::VisualsAudio,
427
+ ..ScreenMemoryConfig::default()
428
+ };
429
+ let mut retention_only = previous.clone();
430
+ retention_only.retention_hours = 24;
431
+ assert!(!rewind_capture_contract_changed(&previous, &retention_only));
432
+
433
+ let mut paused = previous.clone();
434
+ paused.paused = true;
435
+ assert!(rewind_capture_contract_changed(&previous, &paused));
436
+
437
+ let mut disabled = previous.clone();
438
+ disabled.enabled = false;
439
+ assert!(rewind_capture_contract_changed(&previous, &disabled));
440
+
441
+ let mut mode_changed = previous.clone();
442
+ mode_changed.capture_mode = RewindCaptureMode::Visuals;
443
+ assert!(rewind_capture_contract_changed(&previous, &mode_changed));
444
+ }
445
+ }