diff --git a/tauri/src-tauri/src/main.rs b/tauri/src-tauri/src/main.rs index 50855ab0..1de56a30 100644 --- a/tauri/src-tauri/src/main.rs +++ b/tauri/src-tauri/src/main.rs @@ -1036,7 +1036,22 @@ fn open_input_monitoring_settings(app: tauri::AppHandle) -> Result<(), String> { /// /// Pipeline: activate the captured PID → settle → save the user's /// clipboard → write `text` → fire ⌘V → wait for the target to consume it -/// → restore the original clipboard. +/// → conditionally restore the original clipboard. +/// +/// The restore is conditional on `NSPasteboard.changeCount` (or the +/// Windows sequence number) matching the value captured right after +/// `write_text`: if something else wrote to the clipboard during the +/// paste-consume window — the user's own ⌘C in the target app, a +/// clipboard history tool (Paste, Pastebot, Maccy), Universal Clipboard +/// sync, 1Password inserting a secret — their newer content takes +/// priority over our snapshot and is preserved. A +/// [`clipboard::current_change_count`] read failure is treated the same +/// way: unknown state is safer than an unconditional overwrite. +/// +/// `send_paste` failure is isolated from the restore decision: we always +/// attempt the conditional restore before propagating the paste error, +/// so a failed `CGEventPost` / `SendInput` never leaves the user's +/// clipboard stuck on the transcript. /// /// Skips (returns `false`) without touching anything when: /// - `focus.bundle_id` is Voicebox itself — step 6 will inject directly @@ -1066,11 +1081,24 @@ async fn paste_final_text( tokio::time::sleep(std::time::Duration::from_millis(POST_ACTIVATE_SETTLE_MS)).await; let snapshot = clipboard::save_clipboard()?; - clipboard::write_text(&text)?; - synthetic_keys::send_paste()?; - tokio::time::sleep(std::time::Duration::from_millis(PASTE_CONSUME_MS)).await; - clipboard::restore_clipboard(&snapshot)?; + let after_write = clipboard::write_text(&text)?; + let paste_result = synthetic_keys::send_paste(); + tokio::time::sleep(std::time::Duration::from_millis(PASTE_CONSUME_MS)).await; + + let safe_to_restore = matches!( + clipboard::current_change_count(), + Ok(current) if current == after_write + ); + if safe_to_restore { + clipboard::restore_clipboard(&snapshot)?; + } else { + eprintln!( + "[voicebox] clipboard mutated during paste window — skipping restore to preserve newer content" + ); + } + + paste_result?; Ok(true) }