From 7fe7ee2e31f5b6d069c7b5dad2c7d9d4415f6c5a Mon Sep 17 00:00:00 2001 From: Alex Garcia Gil Date: Wed, 2 Sep 2026 21:54:36 +1200 Subject: [PATCH 1/3] fix(desktop): retry transient clipboard image writes --- apps/desktop/src-tauri/src/automation.rs | 90 ++++++++++++++++++++---- 1 file changed, 76 insertions(+), 14 deletions(-) diff --git a/apps/desktop/src-tauri/src/automation.rs b/apps/desktop/src-tauri/src/automation.rs index dc58a5d92e..5e84f6969a 100644 --- a/apps/desktop/src-tauri/src/automation.rs +++ b/apps/desktop/src-tauri/src/automation.rs @@ -21,6 +21,8 @@ use crate::general_settings::PostStudioRecordingBehaviour; const WEBHOOK_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30); const COMMAND_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(300); +const CLIPBOARD_WRITE_ATTEMPTS: usize = 4; +const CLIPBOARD_WRITE_RETRY_BASE_MS: u64 = 50; #[derive(Debug, PartialEq, Eq)] enum ClipboardImageSource { @@ -57,6 +59,30 @@ fn resolve_clipboard_image_source( } } +fn clipboard_write_retry_delay(attempt: usize) -> Option { + (attempt + 1 < CLIPBOARD_WRITE_ATTEMPTS) + .then(|| std::time::Duration::from_millis(CLIPBOARD_WRITE_RETRY_BASE_MS << attempt)) +} + +enum ClipboardImage { + File(PathBuf), + Encoded(Vec), +} + +impl ClipboardImage { + fn load(&self) -> Result { + match self { + Self::File(path) => { + let path = path.to_string_lossy().to_string(); + clipboard_rs::RustImageData::from_path(&path) + .map_err(|e| format!("Failed to load image for clipboard: {e}")) + } + Self::Encoded(bytes) => clipboard_rs::RustImageData::from_bytes(bytes) + .map_err(|e| format!("Failed to load rendered screenshot for clipboard: {e}")), + } + } +} + pub struct DesktopAutomationHost { app: AppHandle, clipboard: Arc>, @@ -66,6 +92,34 @@ impl DesktopAutomationHost { pub fn new(app: AppHandle, clipboard: Arc>) -> Self { Self { app, clipboard } } + + async fn set_clipboard_image(&self, image: &ClipboardImage) -> Result<(), String> { + for attempt in 0..CLIPBOARD_WRITE_ATTEMPTS { + let image_data = image.load()?; + let result = { + let clipboard = self.clipboard.write().await; + clipboard.set_image(image_data) + }; + + match result { + Ok(()) => return Ok(()), + Err(error) => { + let Some(retry_delay) = clipboard_write_retry_delay(attempt) else { + return Err(format!("Failed to set clipboard image: {error}")); + }; + warn!( + attempt = attempt + 1, + retry_delay_ms = retry_delay.as_millis(), + %error, + "Clipboard image write failed; retrying" + ); + tokio::time::sleep(retry_delay).await; + } + } + } + + unreachable!("clipboard retry loop always returns") + } } impl AutomationHost for DesktopAutomationHost { @@ -94,12 +148,10 @@ impl AutomationHost for DesktopAutomationHost { source: &ClipboardSource, ) -> Result<(), String> { let source = resolve_clipboard_image_source(ctx, *source)?; - let img_data = match source { + let image = match source { ClipboardImageSource::File(path) => { - let path = path.to_string_lossy().to_string(); - info!(%path, "Automation: copying file to clipboard"); - clipboard_rs::RustImageData::from_path(&path) - .map_err(|e| format!("Failed to load image for clipboard: {e}"))? + info!(path = %path.display(), "Automation: copying file to clipboard"); + ClipboardImage::File(path) } ClipboardImageSource::ScreenshotProject(path) => { info!(project = %path.display(), "Automation: rendering screenshot for clipboard"); @@ -108,18 +160,11 @@ impl AutomationHost for DesktopAutomationHost { path, ) .await?; - clipboard_rs::RustImageData::from_bytes(&rendered.image_bytes) - .map_err(|e| format!("Failed to load rendered screenshot for clipboard: {e}"))? + ClipboardImage::Encoded(rendered.image_bytes) } }; - self.clipboard - .write() - .await - .set_image(img_data) - .map_err(|e| format!("Failed to set clipboard image: {e}"))?; - - Ok(()) + self.set_clipboard_image(&image).await } async fn save_to_location( @@ -1019,4 +1064,21 @@ mod tests { ))) ); } + + #[test] + fn clipboard_write_retries_use_bounded_exponential_backoff() { + assert_eq!( + clipboard_write_retry_delay(0), + Some(std::time::Duration::from_millis(50)) + ); + assert_eq!( + clipboard_write_retry_delay(1), + Some(std::time::Duration::from_millis(100)) + ); + assert_eq!( + clipboard_write_retry_delay(2), + Some(std::time::Duration::from_millis(200)) + ); + assert_eq!(clipboard_write_retry_delay(3), None); + } } From b96dead058e806964acf95fffc1d646338fefbfb Mon Sep 17 00:00:00 2001 From: Alex Garcia Gil Date: Wed, 2 Sep 2026 21:57:32 +1200 Subject: [PATCH 2/3] perf(desktop): reuse clipboard image across retries --- apps/desktop/src-tauri/src/automation.rs | 33 ++++++------------------ 1 file changed, 8 insertions(+), 25 deletions(-) diff --git a/apps/desktop/src-tauri/src/automation.rs b/apps/desktop/src-tauri/src/automation.rs index 5e84f6969a..2c20f5c429 100644 --- a/apps/desktop/src-tauri/src/automation.rs +++ b/apps/desktop/src-tauri/src/automation.rs @@ -64,25 +64,6 @@ fn clipboard_write_retry_delay(attempt: usize) -> Option { .then(|| std::time::Duration::from_millis(CLIPBOARD_WRITE_RETRY_BASE_MS << attempt)) } -enum ClipboardImage { - File(PathBuf), - Encoded(Vec), -} - -impl ClipboardImage { - fn load(&self) -> Result { - match self { - Self::File(path) => { - let path = path.to_string_lossy().to_string(); - clipboard_rs::RustImageData::from_path(&path) - .map_err(|e| format!("Failed to load image for clipboard: {e}")) - } - Self::Encoded(bytes) => clipboard_rs::RustImageData::from_bytes(bytes) - .map_err(|e| format!("Failed to load rendered screenshot for clipboard: {e}")), - } - } -} - pub struct DesktopAutomationHost { app: AppHandle, clipboard: Arc>, @@ -93,12 +74,11 @@ impl DesktopAutomationHost { Self { app, clipboard } } - async fn set_clipboard_image(&self, image: &ClipboardImage) -> Result<(), String> { + async fn set_clipboard_image(&self, image: clipboard_rs::RustImageData) -> Result<(), String> { for attempt in 0..CLIPBOARD_WRITE_ATTEMPTS { - let image_data = image.load()?; let result = { let clipboard = self.clipboard.write().await; - clipboard.set_image(image_data) + clipboard.set_image(image.clone()) }; match result { @@ -151,7 +131,9 @@ impl AutomationHost for DesktopAutomationHost { let image = match source { ClipboardImageSource::File(path) => { info!(path = %path.display(), "Automation: copying file to clipboard"); - ClipboardImage::File(path) + let path = path.to_string_lossy().to_string(); + clipboard_rs::RustImageData::from_path(&path) + .map_err(|e| format!("Failed to load image for clipboard: {e}"))? } ClipboardImageSource::ScreenshotProject(path) => { info!(project = %path.display(), "Automation: rendering screenshot for clipboard"); @@ -160,11 +142,12 @@ impl AutomationHost for DesktopAutomationHost { path, ) .await?; - ClipboardImage::Encoded(rendered.image_bytes) + clipboard_rs::RustImageData::from_bytes(&rendered.image_bytes) + .map_err(|e| format!("Failed to load rendered screenshot for clipboard: {e}"))? } }; - self.set_clipboard_image(&image).await + self.set_clipboard_image(image).await } async fn save_to_location( From 54db4fc8bd55ac55c835cbfe9e3b51f5d5dfe057 Mon Sep 17 00:00:00 2001 From: Alex Garcia Gil Date: Wed, 2 Sep 2026 22:48:04 +1200 Subject: [PATCH 3/3] Revert "perf(desktop): reuse clipboard image across retries" This reverts commit b96dead058e806964acf95fffc1d646338fefbfb. --- apps/desktop/src-tauri/src/automation.rs | 33 ++++++++++++++++++------ 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/apps/desktop/src-tauri/src/automation.rs b/apps/desktop/src-tauri/src/automation.rs index 2c20f5c429..5e84f6969a 100644 --- a/apps/desktop/src-tauri/src/automation.rs +++ b/apps/desktop/src-tauri/src/automation.rs @@ -64,6 +64,25 @@ fn clipboard_write_retry_delay(attempt: usize) -> Option { .then(|| std::time::Duration::from_millis(CLIPBOARD_WRITE_RETRY_BASE_MS << attempt)) } +enum ClipboardImage { + File(PathBuf), + Encoded(Vec), +} + +impl ClipboardImage { + fn load(&self) -> Result { + match self { + Self::File(path) => { + let path = path.to_string_lossy().to_string(); + clipboard_rs::RustImageData::from_path(&path) + .map_err(|e| format!("Failed to load image for clipboard: {e}")) + } + Self::Encoded(bytes) => clipboard_rs::RustImageData::from_bytes(bytes) + .map_err(|e| format!("Failed to load rendered screenshot for clipboard: {e}")), + } + } +} + pub struct DesktopAutomationHost { app: AppHandle, clipboard: Arc>, @@ -74,11 +93,12 @@ impl DesktopAutomationHost { Self { app, clipboard } } - async fn set_clipboard_image(&self, image: clipboard_rs::RustImageData) -> Result<(), String> { + async fn set_clipboard_image(&self, image: &ClipboardImage) -> Result<(), String> { for attempt in 0..CLIPBOARD_WRITE_ATTEMPTS { + let image_data = image.load()?; let result = { let clipboard = self.clipboard.write().await; - clipboard.set_image(image.clone()) + clipboard.set_image(image_data) }; match result { @@ -131,9 +151,7 @@ impl AutomationHost for DesktopAutomationHost { let image = match source { ClipboardImageSource::File(path) => { info!(path = %path.display(), "Automation: copying file to clipboard"); - let path = path.to_string_lossy().to_string(); - clipboard_rs::RustImageData::from_path(&path) - .map_err(|e| format!("Failed to load image for clipboard: {e}"))? + ClipboardImage::File(path) } ClipboardImageSource::ScreenshotProject(path) => { info!(project = %path.display(), "Automation: rendering screenshot for clipboard"); @@ -142,12 +160,11 @@ impl AutomationHost for DesktopAutomationHost { path, ) .await?; - clipboard_rs::RustImageData::from_bytes(&rendered.image_bytes) - .map_err(|e| format!("Failed to load rendered screenshot for clipboard: {e}"))? + ClipboardImage::Encoded(rendered.image_bytes) } }; - self.set_clipboard_image(image).await + self.set_clipboard_image(&image).await } async fn save_to_location(