@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.
- package/corpus/README.md +1 -1
- package/corpus/templates/clips/app/components/player/comments-panel.tsx +1 -1
- package/corpus/templates/clips/app/components/player/video-player.tsx +5 -3
- package/corpus/templates/clips/desktop/src/app.tsx +171 -51
- package/corpus/templates/clips/desktop/src/lib/pill-completion.ts +111 -0
- package/corpus/templates/clips/desktop/src/lib/pill-session.ts +34 -0
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +181 -79
- package/corpus/templates/clips/desktop/src/lib/rewind-recording-start.ts +28 -4
- package/corpus/templates/clips/desktop/src/main.tsx +4 -4
- package/corpus/templates/clips/desktop/src/overlays/record-pill.tsx +1320 -0
- package/corpus/templates/clips/desktop/src/overlays/recording-pill.tsx +1 -1
- package/corpus/templates/clips/desktop/src/styles.css +20 -308
- package/corpus/templates/clips/desktop/src/tailwind.css +85 -0
- package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +153 -70
- package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +15 -0
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen/custom_capture.rs +25 -7
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen/live_upload.rs +3 -3
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +375 -24
- package/corpus/templates/clips/desktop/src-tauri/src/recording_indicator.rs +22 -30
- package/corpus/templates/clips/desktop/src-tauri/src/rewind_clip.rs +5 -0
- package/corpus/templates/clips/desktop/src-tauri/src/screen_memory.rs +134 -14
- package/corpus/templates/clips/desktop/src-tauri/src/tray.rs +233 -4
- package/corpus/templates/clips/desktop/src-tauri/src/util.rs +8 -5
- package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +1 -1
- package/corpus/templates/design/.generated/bridge/embedded-wheel.generated.ts +3 -2
- package/corpus/templates/design/.generated/bridge/zoom.generated.ts +8 -2
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +46 -10
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +89 -63
- package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +3 -1
- package/corpus/templates/design/app/components/design/bridge/embedded-wheel.bridge.ts +8 -2
- package/corpus/templates/design/app/components/design/bridge/zoom.bridge.ts +11 -2
- package/corpus/templates/design/app/components/design/multi-screen/types.ts +3 -4
- package/corpus/templates/design/app/components/design/multi-screen/zoom-gesture.ts +19 -63
- package/corpus/templates/design/app/pages/design-editor/overview-camera.ts +2 -1
- package/corpus/templates/design/shared/canvas-math.ts +11 -1
- package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +44 -18
- package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +88 -23
- package/corpus/templates/slides/app/context/DeckContext.tsx +155 -86
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +7 -5
- package/corpus/templates/slides/changelog/2026-08-26-thumbnail-arrow-keys-now-navigate-slides-after-selection.md +6 -0
- package/dist/client/hooks/index.d.ts +1 -0
- package/dist/client/hooks/index.js +1 -0
- package/dist/client/use-pinch-zoom.js +17 -4
- package/dist/client/zoom-gesture.d.ts +74 -0
- package/dist/client/zoom-gesture.js +100 -0
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +3 -3
- package/dist/provider-api/actions/custom-provider-registration.d.ts +12 -12
- package/dist/provider-api/actions/provider-api.d.ts +13 -13
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/builder-browser.d.ts +13 -13
- package/dist/server/builder-browser.js +46 -15
- package/dist/server/builder-oauth.d.ts +12 -0
- package/dist/server/builder-oauth.js +13 -2
- package/dist/server/core-routes-plugin.js +54 -19
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/vite/client.js +2 -0
- package/docs/content/template-chat-developers.mdx +100 -13
- package/docs/content/template-chat-first-edits.mdx +5 -5
- package/docs/content/template-chat.mdx +84 -36
- package/package.json +4 -3
- 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
|
-
|
|
958
|
-
|
|
959
|
-
|
|
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,
|
|
1018
|
+
let (mx, my, mw, mh) = tray_monitor_physical_rect(&app);
|
|
967
1019
|
let scale = overlay_scale_factor(&app);
|
|
968
|
-
|
|
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.
|
|
971
|
-
|
|
972
|
-
let
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
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
|
-
//
|
|
1000
|
-
//
|
|
1001
|
-
//
|
|
1002
|
-
//
|
|
1003
|
-
.shadow(
|
|
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
|
-
|
|
1026
|
-
|
|
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
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
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,
|
|
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
|
-
(
|
|
1288
|
-
|
|
1289
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
let w: u32 =
|
|
1652
|
-
let h: u32 =
|
|
1653
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
2759
|
-
//
|
|
2760
|
-
//
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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(¶ms, &replacement_handler, &watch).and_then(|s| {
|
|
4211
|
+
match build_custom_scstream(¶ms, &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
|
-
|
|
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(¶ms, &handler, &watch)?;
|
|
4359
|
+
let stream = build_custom_scstream(¶ms, &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"
|