mirror of
https://github.com/jamiepine/voicebox.git
synced 2026-09-16 05:10:42 -07:00
fix(capture): conditional clipboard restore + always-attempt on paste failure
Two bugs in paste_final_text' clipboard handling: 1. Restore was unconditional. If the user ⌘C'd in the target app during the 400 ms paste-consume window — or a clipboard history tool (Paste, Pastebot, Maccy) or Universal Clipboard sync snapshotted our staged text — the blind restore overwrote their newer content with the pre-paste snapshot, silently losing user data. 2. send_paste' errors were propagated with ? before the restore, so a CGEventPost / SendInput failure left the user's clipboard stuck on the transcript. Fix folds both into one pattern: capture the post-write change count, re-read it after paste-consume, restore only when they match (plus treat a change-count read failure as "unknown, don't overwrite"). Isolate send_paste's error so the restore runs regardless of paste success, then propagate the paste error after. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
c65531bed1
commit
ea7a4e9f6d
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user