@agent-native/core 0.175.2-nightly-20260826150235 → 0.175.3

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 (64) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/clips/app/components/player/comments-panel.tsx +1 -1
  3. package/corpus/templates/clips/app/components/player/video-player.tsx +5 -3
  4. package/corpus/templates/clips/desktop/src/app.tsx +171 -51
  5. package/corpus/templates/clips/desktop/src/lib/pill-completion.ts +111 -0
  6. package/corpus/templates/clips/desktop/src/lib/pill-session.ts +34 -0
  7. package/corpus/templates/clips/desktop/src/lib/recorder.ts +181 -79
  8. package/corpus/templates/clips/desktop/src/lib/rewind-recording-start.ts +28 -4
  9. package/corpus/templates/clips/desktop/src/main.tsx +4 -4
  10. package/corpus/templates/clips/desktop/src/overlays/record-pill.tsx +1320 -0
  11. package/corpus/templates/clips/desktop/src/overlays/recording-pill.tsx +1 -1
  12. package/corpus/templates/clips/desktop/src/styles.css +20 -308
  13. package/corpus/templates/clips/desktop/src/tailwind.css +85 -0
  14. package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +153 -70
  15. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +15 -0
  16. package/corpus/templates/clips/desktop/src-tauri/src/native_screen/custom_capture.rs +25 -7
  17. package/corpus/templates/clips/desktop/src-tauri/src/native_screen/live_upload.rs +3 -3
  18. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +375 -24
  19. package/corpus/templates/clips/desktop/src-tauri/src/recording_indicator.rs +22 -30
  20. package/corpus/templates/clips/desktop/src-tauri/src/rewind_clip.rs +5 -0
  21. package/corpus/templates/clips/desktop/src-tauri/src/screen_memory.rs +134 -14
  22. package/corpus/templates/clips/desktop/src-tauri/src/tray.rs +233 -4
  23. package/corpus/templates/clips/desktop/src-tauri/src/util.rs +8 -5
  24. package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +1 -1
  25. package/corpus/templates/design/.generated/bridge/embedded-wheel.generated.ts +3 -2
  26. package/corpus/templates/design/.generated/bridge/zoom.generated.ts +8 -2
  27. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +46 -10
  28. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +89 -63
  29. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +3 -1
  30. package/corpus/templates/design/app/components/design/bridge/embedded-wheel.bridge.ts +8 -2
  31. package/corpus/templates/design/app/components/design/bridge/zoom.bridge.ts +11 -2
  32. package/corpus/templates/design/app/components/design/multi-screen/types.ts +3 -4
  33. package/corpus/templates/design/app/components/design/multi-screen/zoom-gesture.ts +19 -63
  34. package/corpus/templates/design/app/pages/design-editor/overview-camera.ts +2 -1
  35. package/corpus/templates/design/shared/canvas-math.ts +11 -1
  36. package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +44 -18
  37. package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +88 -23
  38. package/corpus/templates/slides/app/context/DeckContext.tsx +155 -86
  39. package/corpus/templates/slides/app/pages/DeckEditor.tsx +7 -5
  40. package/corpus/templates/slides/changelog/2026-08-26-thumbnail-arrow-keys-now-navigate-slides-after-selection.md +6 -0
  41. package/dist/client/hooks/index.d.ts +1 -0
  42. package/dist/client/hooks/index.js +1 -0
  43. package/dist/client/use-pinch-zoom.js +17 -4
  44. package/dist/client/zoom-gesture.d.ts +74 -0
  45. package/dist/client/zoom-gesture.js +100 -0
  46. package/dist/collab/routes.d.ts +1 -1
  47. package/dist/collab/struct-routes.d.ts +1 -1
  48. package/dist/observability/routes.d.ts +3 -3
  49. package/dist/provider-api/actions/custom-provider-registration.d.ts +12 -12
  50. package/dist/provider-api/actions/provider-api.d.ts +13 -13
  51. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  52. package/dist/server/builder-browser.d.ts +13 -13
  53. package/dist/server/builder-browser.js +46 -15
  54. package/dist/server/builder-oauth.d.ts +12 -0
  55. package/dist/server/builder-oauth.js +13 -2
  56. package/dist/server/core-routes-plugin.js +54 -19
  57. package/dist/server/realtime-token.d.ts +1 -1
  58. package/dist/server/transcribe-voice.d.ts +1 -1
  59. package/dist/vite/client.js +2 -0
  60. package/docs/content/template-chat-developers.mdx +100 -13
  61. package/docs/content/template-chat-first-edits.mdx +5 -5
  62. package/docs/content/template-chat.mdx +84 -36
  63. package/package.json +4 -3
  64. package/corpus/templates/clips/desktop/src/overlays/toolbar.tsx +0 -458
@@ -40,6 +40,12 @@ const COUNTDOWN_CONTROL_HIT_PAD: f64 = 8.0;
40
40
  // Guards the single cursor-poll loop that toggles click-through on the
41
41
  // countdown overlay so only the button zones are interactive.
42
42
  static COUNTDOWN_CONTROL_TRACKING: AtomicBool = AtomicBool::new(false);
43
+ // Set by the recording pill the instant Stop is clicked, before it emits
44
+ // `clips:recorder-stop`. While held, `hide_overlays` / `hide_recording_chrome`
45
+ // skip the toolbar window so the pill can swap to its completion card in
46
+ // place; every other teardown path (cancel, restart, popover lifecycle) still
47
+ // closes it. Cleared when the card is dismissed or the next session starts.
48
+ static TOOLBAR_FINISHING: AtomicBool = AtomicBool::new(false);
43
49
  const BUBBLE_LABEL: &str = "bubble";
44
50
  const PREPARING_LABEL: &str = "preparing";
45
51
  const FINALIZING_LABEL: &str = "finalizing";
@@ -66,7 +72,6 @@ const OVERLAY_LABELS: &[&str] = &[
66
72
  /// launch — this matches Loom's out-of-the-box behavior.
67
73
  const BUBBLE_SIZE_SMALL: u32 = 360;
68
74
  const BUBBLE_SIZE_MEDIUM: u32 = 504;
69
- const POPOVER_SHADOW_GUTTER_LOGICAL: f64 = 24.0;
70
75
  const POPOVER_DEFAULT_WIDTH_LOGICAL: f64 = 320.0;
71
76
  const POPOVER_DEFAULT_HEIGHT_LOGICAL: f64 = 520.0;
72
77
  const OVERLAY_SHADOW_GUTTER_LOGICAL: f64 = 18.0;
@@ -116,11 +121,6 @@ fn overlay_shadow_gutter_physical(app: &AppHandle) -> u32 {
116
121
  (OVERLAY_SHADOW_GUTTER_LOGICAL * overlay_scale_factor(app)).round() as u32
117
122
  }
118
123
 
119
- fn popover_window_size_logical(content_width: f64, content_height: f64) -> (f64, f64) {
120
- let gutter = POPOVER_SHADOW_GUTTER_LOGICAL * 2.0;
121
- (content_width + gutter, content_height + gutter)
122
- }
123
-
124
124
  fn bubble_size_for_name(name: &str) -> u32 {
125
125
  match name {
126
126
  "medium" => BUBBLE_SIZE_MEDIUM,
@@ -954,30 +954,89 @@ pub async fn set_recording_display_override(
954
954
  Ok(())
955
955
  }
956
956
 
957
- /// Vertical recording pill anchored to the left edge. Stop + timer + pause,
958
- /// with hover-revealed restart/cancel controls matching Loom's left-rail
959
- /// placement. Draggable, always on top.
957
+ fn toolbar_position_path(app: &AppHandle) -> Option<PathBuf> {
958
+ let dir = app.path().app_data_dir().ok()?;
959
+ if std::fs::create_dir_all(&dir).is_err() {
960
+ return None;
961
+ }
962
+ Some(dir.join("toolbar-pill-position.json"))
963
+ }
964
+
965
+ fn load_toolbar_position(app: &AppHandle) -> Option<(i32, i32)> {
966
+ let path = toolbar_position_path(app)?;
967
+ let bytes = std::fs::read(&path).ok()?;
968
+ let value: serde_json::Value = serde_json::from_slice(&bytes).ok()?;
969
+ let x = value.get("x")?.as_i64()? as i32;
970
+ let y = value.get("y")?.as_i64()? as i32;
971
+ Some((x, y))
972
+ }
973
+
974
+ /// Persist the pill's dragged position (outer physical px). The renderer
975
+ /// calls this debounced from the window's move events while at rest — it
976
+ /// skips saves mid-reveal so a grown pill never becomes the stored anchor.
977
+ #[tauri::command]
978
+ pub async fn toolbar_save_position(app: AppHandle, x: i32, y: i32) -> Result<(), String> {
979
+ let Some(path) = toolbar_position_path(&app) else {
980
+ return Ok(());
981
+ };
982
+ let body =
983
+ serde_json::to_vec(&serde_json::json!({ "x": x, "y": y })).map_err(|e| e.to_string())?;
984
+ let tmp = path.with_extension("json.tmp");
985
+ if std::fs::write(&tmp, &body).is_err() {
986
+ return Ok(());
987
+ }
988
+ if std::fs::rename(&tmp, &path).is_err() {
989
+ let _ = std::fs::remove_file(&tmp);
990
+ }
991
+ Ok(())
992
+ }
993
+
994
+ /// Hold or release the stop-flow toolbar preservation described on
995
+ /// `TOOLBAR_FINISHING`. The pill sets the hold synchronously before emitting
996
+ /// `clips:recorder-stop` so the recorder's teardown cannot race it.
997
+ #[tauri::command]
998
+ pub async fn set_toolbar_finishing(hold: bool) -> Result<(), String> {
999
+ TOOLBAR_FINISHING.store(hold, Ordering::SeqCst);
1000
+ Ok(())
1001
+ }
1002
+
1003
+ /// Horizontal recording pill — dot, timer, level meter, pause, and a white
1004
+ /// Stop capsule that grows rightward from an anchored left edge for hover
1005
+ /// extras and the inline delete confirm. Draggable, always on top. The
1006
+ /// renderer owns exact sizing: it measures its content and resizes this
1007
+ /// window around the fixed anchor edge, so the values here only need to fit
1008
+ /// the resting pill for first paint.
960
1009
  #[tauri::command]
961
1010
  pub async fn show_toolbar(app: AppHandle) -> Result<(), String> {
962
1011
  dlog!("[clips-tray] show_toolbar invoked");
1012
+ // A fresh session can never start inside the finishing hold — if a stale
1013
+ // completion card is still open its window gets replaced below anyway.
1014
+ TOOLBAR_FINISHING.store(false, Ordering::SeqCst);
963
1015
  // Reset the blur guard — spawning an overlay can briefly steal focus
964
1016
  // from the popover on some macOS versions even with .focused(false).
965
1017
  mark_popover_shown(&app);
966
- let (mx, my, _mw, mh) = tray_monitor_physical_rect(&app);
1018
+ let (mx, my, mw, mh) = tray_monitor_physical_rect(&app);
967
1019
  let scale = overlay_scale_factor(&app);
968
- let gutter = overlay_shadow_gutter_physical(&app);
1020
+ // The window is sized to the pill EXACTLY — elevation comes from the
1021
+ // native NSWindow shadow (shadow(true) below), which macOS derives from
1022
+ // the drawn rounded shape. A transparent CSS-shadow apron is never used
1023
+ // here: its invisible margin eats clicks and fights screen-edge docking.
969
1024
  // CSS is authored in logical px, while this command sizes the native
970
- // window in physical px. Keep the visible toolbar large enough for the
971
- // fixed 30px circular controls on high-DPI displays.
972
- let content_w: u32 = (72.0 * scale).round() as u32;
973
- let collapsed_content_h: u32 = (150.0 * scale).round() as u32;
974
- let w: u32 = content_w + gutter * 2;
975
- let h: u32 = collapsed_content_h + gutter * 2;
976
- // Flush-left with a small margin; vertically center the collapsed pill.
977
- // The React toolbar temporarily resizes this window while hover/focus
978
- // reveals extra controls so transparent pixels don't block clicks.
979
- let x: i32 = mx + 48 - gutter as i32;
980
- let y: i32 = my + (mh as i32 - collapsed_content_h as i32) / 2 - gutter as i32;
1025
+ // window in physical px.
1026
+ let w: u32 = (248.0 * scale).round() as u32;
1027
+ let h: u32 = (42.0 * scale).round() as u32;
1028
+ // Bottom-center by default; a user-dragged position wins when it still
1029
+ // lands on the tray monitor (clamped so a monitor change can't strand
1030
+ // the pill off-screen).
1031
+ let default_x: i32 = mx + (mw as i32 - w as i32) / 2;
1032
+ let default_y: i32 = my + mh as i32 - h as i32 - (48.0 * scale).round() as i32;
1033
+ let (x, y) = match load_toolbar_position(&app) {
1034
+ Some((sx, sy)) => (
1035
+ sx.clamp(mx, mx + mw as i32 - w as i32),
1036
+ sy.clamp(my, my + mh as i32 - h as i32),
1037
+ ),
1038
+ None => (default_x, default_y),
1039
+ };
981
1040
  dlog!("[clips-tray] toolbar pos=({},{}) size={}x{}", x, y, w, h);
982
1041
  if let Some(existing) = app.get_webview_window(TOOLBAR_LABEL) {
983
1042
  let _ = existing.set_size(tauri::Size::Physical(PhysicalSize::new(w, h)));
@@ -985,7 +1044,6 @@ pub async fn show_toolbar(app: AppHandle) -> Result<(), String> {
985
1044
  set_capture_excluded(&existing);
986
1045
  configure_overlay_behavior(&existing);
987
1046
  raise_to_status_level(&existing);
988
- crate::util::show_without_activation(&existing);
989
1047
  return Ok(());
990
1048
  }
991
1049
  #[allow(unused_mut)]
@@ -996,11 +1054,11 @@ pub async fn show_toolbar(app: AppHandle) -> Result<(), String> {
996
1054
  .always_on_top(true)
997
1055
  .skip_taskbar(true)
998
1056
  .resizable(false)
999
- // IMPORTANT: native window shadow MUST stay off — macOS draws it
1000
- // based on the rectangular window bounds, not the rounded React
1001
- // content, so it shows up as a hard-edged black rectangle around
1002
- // the rounded pill.
1003
- .shadow(false)
1057
+ // Native elevation: on a transparent window macOS computes the
1058
+ // shadow from the drawn content's alpha, so the capsule gets a
1059
+ // correctly rounded OS shadow with the window sized to the pill
1060
+ // exactly — no transparent CSS-shadow apron eating clicks.
1061
+ .shadow(true)
1004
1062
  .visible(false)
1005
1063
  .focused(false);
1006
1064
  // macOS: without this, the first click on an unfocused window is
@@ -1022,9 +1080,30 @@ pub async fn show_toolbar(app: AppHandle) -> Result<(), String> {
1022
1080
  set_capture_excluded(&win);
1023
1081
  configure_overlay_behavior(&win);
1024
1082
  raise_to_status_level(&win);
1025
- let _ = win.show();
1026
- dlog!("[clips-tray] toolbar shown");
1083
+ // Deliberately NOT shown here. The pill owns its visibility through
1084
+ // `toolbar_set_visible`: it stays hidden through pre-record and the
1085
+ // countdown and appears only once the recorder reports capture live —
1086
+ // recording controls before recording exists read as a broken state.
1087
+ dlog!("[clips-tray] toolbar created (hidden until capture is live)");
1088
+
1089
+ Ok(())
1090
+ }
1027
1091
 
1092
+ /// Show or hide the recording pill without activating it. Visibility is
1093
+ /// driven entirely by the pill renderer: hidden while the recorder is
1094
+ /// preparing or counting down, visible while capture is live or the
1095
+ /// completion card is up.
1096
+ #[tauri::command]
1097
+ pub async fn toolbar_set_visible(app: AppHandle, visible: bool) -> Result<(), String> {
1098
+ let Some(win) = app.get_webview_window(TOOLBAR_LABEL) else {
1099
+ return Ok(());
1100
+ };
1101
+ if visible {
1102
+ raise_to_status_level(&win);
1103
+ crate::util::show_without_activation(&win);
1104
+ } else {
1105
+ let _ = win.hide();
1106
+ }
1028
1107
  Ok(())
1029
1108
  }
1030
1109
 
@@ -1149,10 +1228,16 @@ pub async fn set_bubble_capture_excluded(app: AppHandle, excluded: bool) -> Resu
1149
1228
  }
1150
1229
 
1151
1230
  fn overlay_labels_to_hide(preserve_finalizing: bool) -> impl Iterator<Item = &'static str> {
1152
- OVERLAY_LABELS
1153
- .iter()
1154
- .copied()
1155
- .filter(move |label| !preserve_finalizing || *label != FINALIZING_LABEL)
1231
+ let preserve_toolbar = TOOLBAR_FINISHING.load(Ordering::SeqCst);
1232
+ OVERLAY_LABELS.iter().copied().filter(move |label| {
1233
+ if preserve_finalizing && *label == FINALIZING_LABEL {
1234
+ return false;
1235
+ }
1236
+ if preserve_toolbar && *label == TOOLBAR_LABEL {
1237
+ return false;
1238
+ }
1239
+ true
1240
+ })
1156
1241
  }
1157
1242
 
1158
1243
  #[tauri::command]
@@ -1194,7 +1279,10 @@ pub async fn hide_recording_chrome(
1194
1279
  let keep_region_guides = g.always_visible && g.enabled && !g.rects.is_empty();
1195
1280
  // The recording-region border belongs to a single recording (never pinned),
1196
1281
  // so it always tears down here alongside the countdown + toolbar.
1197
- let mut labels: Vec<&str> = vec![COUNTDOWN_LABEL, TOOLBAR_LABEL, REGION_RECORD_BORDER_LABEL];
1282
+ let mut labels: Vec<&str> = vec![COUNTDOWN_LABEL, REGION_RECORD_BORDER_LABEL];
1283
+ if !TOOLBAR_FINISHING.load(Ordering::SeqCst) {
1284
+ labels.push(TOOLBAR_LABEL);
1285
+ }
1198
1286
  if !keep_region_guides {
1199
1287
  labels.push(REGION_GUIDES_LABEL);
1200
1288
  }
@@ -1284,10 +1372,9 @@ pub async fn resize_popover(app: AppHandle, height: f64, width: Option<f64>) ->
1284
1372
  .as_ref()
1285
1373
  .map(|monitor| {
1286
1374
  let scale = monitor.scale_factor().max(1.0);
1287
- ((monitor.size().height as f64) / scale
1288
- - 24.0
1289
- - POPOVER_SHADOW_GUTTER_LOGICAL * 2.0)
1290
- .clamp(260.0, 820.0)
1375
+ // The window IS the visible panel (native shadow, no apron),
1376
+ // so only the 24px menu-bar margin is reserved.
1377
+ ((monitor.size().height as f64) / scale - 24.0).clamp(260.0, 820.0)
1291
1378
  })
1292
1379
  .unwrap_or(820.0);
1293
1380
  // Same idea as height: a monitor narrower than the requested width
@@ -1297,18 +1384,18 @@ pub async fn resize_popover(app: AppHandle, height: f64, width: Option<f64>) ->
1297
1384
  let max_logical_width = monitor
1298
1385
  .map(|monitor| {
1299
1386
  let scale = monitor.scale_factor().max(1.0);
1300
- ((monitor.size().width as f64) / scale - 16.0 - POPOVER_SHADOW_GUTTER_LOGICAL * 2.0)
1301
- .clamp(320.0, 960.0)
1387
+ ((monitor.size().width as f64) / scale - 16.0).clamp(320.0, 960.0)
1302
1388
  })
1303
1389
  .unwrap_or(960.0);
1304
1390
  let clamped = height.clamp(200.0, max_logical_height);
1305
1391
  let width = width
1306
1392
  .unwrap_or(320.0)
1307
1393
  .clamp(320.0, max_logical_width.max(320.0));
1308
- let (window_width, window_height) = popover_window_size_logical(width, clamped);
1394
+ // The window IS the panel now — elevation is the native NSWindow
1395
+ // shadow on exact bounds, so there is no apron to add here.
1396
+ let (window_width, window_height) = (width, clamped);
1309
1397
  let _ = w.set_size(tauri::Size::Logical(tauri::LogicalSize::new(
1310
- window_width,
1311
- window_height,
1398
+ width, clamped,
1312
1399
  )));
1313
1400
  // Re-anchor to the tray icon so the window doesn't drift below the
1314
1401
  // bottom of the monitor after a growth. Pass the size we just asked
@@ -1643,14 +1730,17 @@ pub async fn show_flow_bar(app: AppHandle) -> Result<(), String> {
1643
1730
 
1644
1731
  let (mx, my, mw, mh) = tray_monitor_physical_rect(&app);
1645
1732
  let scale = overlay_scale_factor(&app);
1646
- // Wide + tall enough for a 5-line transcript preview above the pill.
1647
- let content_w: u32 = (640.0 * scale).round() as u32;
1648
- let content_h: u32 = (160.0 * scale).round() as u32;
1649
- let bottom_margin: i32 = (14.0 * scale).round() as i32;
1650
- let gutter = overlay_shadow_gutter_physical(&app);
1651
- let w: u32 = content_w + gutter * 2;
1652
- let h: u32 = content_h + gutter * 2;
1653
- let x: i32 = (mx + (mw as i32 - content_w as i32) / 2 - gutter as i32).max(mx);
1733
+ // The window is sized to the content EXACTLY — wide + tall enough for a
1734
+ // 5-line transcript preview above the pill. Elevation comes from the
1735
+ // native NSWindow shadow (shadow(true) below), which macOS derives from
1736
+ // the drawn pill/preview alpha. A transparent CSS-shadow apron is never
1737
+ // used here: its invisible margin eats clicks.
1738
+ let w: u32 = (640.0 * scale).round() as u32;
1739
+ let h: u32 = (160.0 * scale).round() as u32;
1740
+ // 32 = the old 14px visible margin + the removed 18px shadow gutter, so
1741
+ // the visible pill keeps its exact on-screen position.
1742
+ let bottom_margin: i32 = (32.0 * scale).round() as i32;
1743
+ let x: i32 = (mx + (mw as i32 - w as i32) / 2).max(mx);
1654
1744
  let y: i32 = (my + mh as i32 - h as i32 - bottom_margin).max(my);
1655
1745
 
1656
1746
  if let Some(existing) = app.get_webview_window(FLOW_BAR_LABEL) {
@@ -1673,7 +1763,11 @@ pub async fn show_flow_bar(app: AppHandle) -> Result<(), String> {
1673
1763
  .always_on_top(true)
1674
1764
  .skip_taskbar(true)
1675
1765
  .resizable(false)
1676
- .shadow(false)
1766
+ // Native elevation: on a transparent window macOS computes the
1767
+ // shadow from the drawn content's alpha, so the pill and preview
1768
+ // get correctly rounded OS shadows with the window sized to the
1769
+ // content exactly — no transparent CSS-shadow apron eating clicks.
1770
+ .shadow(true)
1677
1771
  .visible(false)
1678
1772
  .focused(false)
1679
1773
  .build()
@@ -2649,11 +2743,10 @@ fn present_popover(app: &AppHandle, window: &WebviewWindow) {
2649
2743
  // recording or voice wake. The content's ResizeObserver will fine-tune the
2650
2744
  // height on the next render, but we need a sensible starting size so
2651
2745
  // `position_popover` can anchor correctly.
2652
- let (w, h) = popover_window_size_logical(
2746
+ let _ = window.set_size(tauri::Size::Logical(tauri::LogicalSize::new(
2653
2747
  POPOVER_DEFAULT_WIDTH_LOGICAL,
2654
2748
  POPOVER_DEFAULT_HEIGHT_LOGICAL,
2655
- );
2656
- let _ = window.set_size(tauri::Size::Logical(tauri::LogicalSize::new(w, h)));
2749
+ )));
2657
2750
  position_popover(app, window);
2658
2751
  mark_popover_shown(app);
2659
2752
  present_interactive_window(window);
@@ -2755,9 +2848,9 @@ pub fn position_popover_with_size(
2755
2848
 
2756
2849
  // Center the popover horizontally on the icon.
2757
2850
  let mut x = icon_x + icon_w / 2 - (win_size.width as i32) / 2;
2758
- // Drop the visible panel below the icon with a tiny gap. The native
2759
- // window itself starts a shadow-gutter earlier so the top shadow has
2760
- // real transparent pixels to paint into without moving the panel down.
2851
+ // Drop the panel below the icon with a tiny gap. The window IS the
2852
+ // visible panel — its elevation is the native NSWindow shadow, which
2853
+ // paints outside the window bounds, so no shadow-gutter offset here.
2761
2854
  let gap = 6_i32;
2762
2855
  let mut y = icon_y + icon_h + gap;
2763
2856
 
@@ -2777,15 +2870,6 @@ pub fn position_popover_with_size(
2777
2870
  && icon_cy < mp.y + ms.height as i32
2778
2871
  })
2779
2872
  });
2780
- let popover_gutter = (POPOVER_SHADOW_GUTTER_LOGICAL
2781
- * tray_monitor
2782
- .as_ref()
2783
- .map(|m| m.scale_factor())
2784
- .unwrap_or_else(|| monitor.scale_factor())
2785
- .max(1.0))
2786
- .round() as i32;
2787
- y -= popover_gutter;
2788
-
2789
2873
  let (clamp_pos, clamp_size) = tray_monitor
2790
2874
  .map(|m| (*m.position(), *m.size()))
2791
2875
  .unwrap_or((*mon_pos, *mon_size));
@@ -2817,13 +2901,12 @@ pub fn position_popover_with_size(
2817
2901
  let scale = monitor.scale_factor();
2818
2902
  let margin_right = (12.0 * scale) as i32;
2819
2903
  let margin_top = (36.0 * scale) as i32;
2820
- let popover_gutter = (POPOVER_SHADOW_GUTTER_LOGICAL * scale.max(1.0)).round() as i32;
2821
2904
  let min_x = mon_pos.x + 8;
2822
2905
  let max_x = mon_pos.x + mon_size.width as i32 - win_size.width as i32 - 8;
2823
2906
  let min_y = mon_pos.y + 8;
2824
2907
  let max_y = mon_pos.y + mon_size.height as i32 - win_size.height as i32 - 8;
2825
2908
  let x = (mon_pos.x + mon_size.width as i32 - win_size.width as i32 - margin_right)
2826
2909
  .clamp(min_x, max_x.max(min_x));
2827
- let y = (mon_pos.y + margin_top - popover_gutter).clamp(min_y, max_y.max(min_y));
2910
+ let y = (mon_pos.y + margin_top).clamp(min_y, max_y.max(min_y));
2828
2911
  let _ = window.set_position(PhysicalPosition::new(x, y));
2829
2912
  }
@@ -81,6 +81,16 @@ pub fn run() {
81
81
  // over focus and neither popover shows.
82
82
  present_popover(app);
83
83
  }))
84
+ .on_window_event(|window, event| {
85
+ // Backstop for the tray's recording mode: the pill window is its
86
+ // single writer, so the one report it can never deliver is its
87
+ // own death. Any toolbar teardown restores the plain status item.
88
+ if window.label() == "toolbar" {
89
+ if let tauri::WindowEvent::Destroyed = event {
90
+ tray::reset_tray_recording(window.app_handle());
91
+ }
92
+ }
93
+ })
84
94
  .invoke_handler(tauri::generate_handler![
85
95
  // clips commands
86
96
  clips::show_countdown,
@@ -90,6 +100,10 @@ pub fn run() {
90
100
  clips::show_finalizing,
91
101
  clips::hide_finalizing,
92
102
  clips::show_toolbar,
103
+ clips::toolbar_save_position,
104
+ clips::toolbar_set_visible,
105
+ clips::set_toolbar_finishing,
106
+ tray::tray_recording_status,
93
107
  clips::show_bubble,
94
108
  clips::set_bubble_capture_excluded,
95
109
  clips::hide_overlays,
@@ -150,6 +164,7 @@ pub fn run() {
150
164
  native_screen::native_fullscreen_recording_available,
151
165
  native_screen::native_fullscreen_take_upload_finished,
152
166
  native_screen::native_fullscreen_claim_upload_open,
167
+ native_screen::native_fullscreen_prefetch_capture_content,
153
168
  native_screen::native_fullscreen_recording_warm,
154
169
  native_screen::native_fullscreen_recording_begin,
155
170
  native_screen::native_fullscreen_recording_stop_and_upload,
@@ -3832,7 +3832,11 @@ impl CustomCaptureResume {
3832
3832
  // re-measures `paused_for` from the same (uncleared) pause instant, so
3833
3833
  // advancing the offset here would compound on every failed attempt.
3834
3834
  let replacement_handler = self.handler.replacement_stream();
3835
- let new_stream = build_custom_scstream(&self.params, &replacement_handler, &self.watch)?;
3835
+ // `None`: a resume can run long after the display topology changed, so
3836
+ // it must resolve against fresh shareable content rather than the
3837
+ // snapshot the initial start was sized from.
3838
+ let new_stream =
3839
+ build_custom_scstream(&self.params, &replacement_handler, &self.watch, None)?;
3836
3840
 
3837
3841
  // Apply the pause gap just before the stream starts delivering, so the
3838
3842
  // first rebased frame already skips it. Roll back if startup fails so
@@ -3907,9 +3911,17 @@ fn build_custom_scstream(
3907
3911
  params: &RestartParams,
3908
3912
  handler: &CustomScreenCaptureOutputHandler,
3909
3913
  watch: &Arc<CaptureWatch>,
3914
+ // The initial start passes the snapshot it already fetched to size the
3915
+ // output, sparing a second multi-second lookup. Watchdog rebuilds and
3916
+ // resume pass `None` — they may run long after the display topology
3917
+ // changed, so they must resolve against fresh content.
3918
+ prefetched_content: Option<SCShareableContent>,
3910
3919
  ) -> Result<SCStream, String> {
3911
- let content =
3912
- SCShareableContent::get().map_err(|e| format!("shareable content lookup failed: {e:?}"))?;
3920
+ let content = match prefetched_content {
3921
+ Some(content) => content,
3922
+ None => SCShareableContent::get()
3923
+ .map_err(|e| format!("shareable content lookup failed: {e:?}"))?,
3924
+ };
3913
3925
  let displays = content.displays();
3914
3926
  let display = params
3915
3927
  .target_display_id
@@ -4196,7 +4208,7 @@ fn spawn_capture_watchdog(
4196
4208
  return;
4197
4209
  }
4198
4210
 
4199
- match build_custom_scstream(&params, &replacement_handler, &watch).and_then(|s| {
4211
+ match build_custom_scstream(&params, &replacement_handler, &watch, None).and_then(|s| {
4200
4212
  s.start_capture()
4201
4213
  .map(|()| s)
4202
4214
  .map_err(|e| format!("start_capture failed: {e:?}"))
@@ -4258,10 +4270,16 @@ pub(crate) fn start_custom_screencapturekit_backend_at(
4258
4270
  capture_region: Option<NativeCaptureRegion>,
4259
4271
  defer_recording_output: bool,
4260
4272
  force_segmented_output: bool,
4273
+ // Popover-open prefetch from `take_prefetched_shareable_content`; `None`
4274
+ // (resume/segment/Rewind callers) keeps the self-contained fresh fetch.
4275
+ prefetched_content: Option<SCShareableContent>,
4261
4276
  ) -> Result<(NativeFullscreenBackend, Option<u32>, Option<u32>), String> {
4262
4277
  eprintln!("[clips-tray] starting custom screen capture backend");
4263
- let content =
4264
- SCShareableContent::get().map_err(|e| format!("shareable content lookup failed: {e:?}"))?;
4278
+ let content = match prefetched_content {
4279
+ Some(content) => content,
4280
+ None => SCShareableContent::get()
4281
+ .map_err(|e| format!("shareable content lookup failed: {e:?}"))?,
4282
+ };
4265
4283
  let displays = content.displays();
4266
4284
  let display = target_display_id
4267
4285
  .and_then(|id| displays.iter().find(|d| d.display_id() == id))
@@ -4338,7 +4356,7 @@ pub(crate) fn start_custom_screencapturekit_backend_at(
4338
4356
  watch: Arc::clone(&watch),
4339
4357
  };
4340
4358
 
4341
- let stream = build_custom_scstream(&params, &handler, &watch)?;
4359
+ let stream = build_custom_scstream(&params, &handler, &watch, Some(content))?;
4342
4360
  if let Err(err) = stream.start_capture() {
4343
4361
  let _ = std::fs::remove_file(output_path);
4344
4362
  return Err(format!("custom capture start failed: {err:?}"));
@@ -355,7 +355,7 @@ async fn live_upload_loop(
355
355
  "[live-upload] {rec}: acknowledged {offset} bytes before finalize={finalize}"
356
356
  ));
357
357
  index += 1;
358
- emit_native_upload_progress(&app, "uploading", "Uploading clip", None, None);
358
+ emit_native_upload_progress(&app, &rec, "uploading", "Uploading clip", None, None);
359
359
  }
360
360
 
361
361
  if finalize {
@@ -366,7 +366,7 @@ async fn live_upload_loop(
366
366
  eprintln!(
367
367
  "[live-upload] {rec}: finalizing — {index} chunk(s) sent, draining tail (offset={offset}, final_len={final_len})"
368
368
  );
369
- emit_native_upload_progress(&app, "processing", "Uploading clip", None, None);
369
+ emit_native_upload_progress(&app, &rec, "processing", "Uploading clip", None, None);
370
370
 
371
371
  // The last tail chunk doubles as the final post (data + is_final).
372
372
  // The server's resumable-session path relays chunks to GCS, whose
@@ -445,7 +445,7 @@ async fn live_upload_loop(
445
445
  }
446
446
  };
447
447
  }
448
- emit_native_upload_progress(&app, "opening", "Uploading clip", None, Some(1.0));
448
+ emit_native_upload_progress(&app, &rec, "opening", "Uploading clip", None, Some(1.0));
449
449
  eprintln!("[live-upload] {rec}: done — {index} post(s), {final_len} bytes total");
450
450
  crate::logfile::diagnostic(&format!(
451
451
  "[live-upload] {rec}: complete with {final_len} acknowledged bytes"