From 08bb5a8279d13e0b1f7edc2c26f2130dc38ae328 Mon Sep 17 00:00:00 2001 From: Adam Date: Thu, 23 Jul 2026 10:04:18 +0100 Subject: [PATCH] fix(desktop): suppress console window flashes from background spawns on Windows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On Windows, every std::process::Command spawn of a console-subsystem binary (git, node, ffmpeg, agent CLIs) opens a visible console window unless CREATE_NO_WINDOW is set. The desktop app spawns these in the background constantly — git exec, agent runtime readiness/version probes, auth status probes, ffmpeg transcodes, provider invocations — so windows flash over the UI throughout normal use. Set the CREATE_NO_WINDOW creation flag (0x08000000) on all background spawn sites under desktop/src-tauri. Guarded by #[cfg(windows)]; no behavior change on other platforms. Interactive/foreground spawns are untouched. Signed-off-by: Adam --- desktop/src-tauri/src/commands/agent_auth.rs | 18 ++++++++-- .../commands/agent_discovery/managed_node.rs | 13 ++++++-- .../src/commands/agent_model_process.rs | 6 ++++ .../src-tauri/src/commands/media_transcode.rs | 33 +++++++++++++++---- .../src/commands/project_git_exec.rs | 6 ++++ .../src-tauri/src/commands/relay_reconnect.rs | 14 +++++--- .../src-tauri/src/managed_agents/agent_env.rs | 12 +++++++ .../src-tauri/src/managed_agents/backend.rs | 12 +++++-- .../src-tauri/src/managed_agents/discovery.rs | 22 ++++++++++++- 9 files changed, 116 insertions(+), 20 deletions(-) diff --git a/desktop/src-tauri/src/commands/agent_auth.rs b/desktop/src-tauri/src/commands/agent_auth.rs index c23263de7e..555d3f1f6b 100644 --- a/desktop/src-tauri/src/commands/agent_auth.rs +++ b/desktop/src-tauri/src/commands/agent_auth.rs @@ -194,6 +194,12 @@ fn run_buzz_acp_auth_command_with_paths( if let Some(path) = augmented_path { command.env("PATH", path); } + #[cfg(windows)] + { + use std::os::windows::process::CommandExt; + const CREATE_NO_WINDOW: u32 = 0x0800_0000; + command.creation_flags(CREATE_NO_WINDOW); + } command .output() @@ -226,11 +232,19 @@ fn run_claude_subscription_login(runtime_id: &str, method: &AcpAuthMethod) -> Re let (command, args) = argv .split_first() .ok_or_else(|| "Claude login command is empty".to_string())?; - let status = Command::new(command) + let mut login = Command::new(command); + login .args(args) .stdin(Stdio::null()) .stdout(Stdio::null()) - .stderr(Stdio::null()) + .stderr(Stdio::null()); + #[cfg(windows)] + { + use std::os::windows::process::CommandExt; + const CREATE_NO_WINDOW: u32 = 0x0800_0000; + login.creation_flags(CREATE_NO_WINDOW); + } + let status = login .status() .map_err(|error| format!("failed to run Claude login: {error}"))?; if !status.success() { diff --git a/desktop/src-tauri/src/commands/agent_discovery/managed_node.rs b/desktop/src-tauri/src/commands/agent_discovery/managed_node.rs index fca5a602b0..9321fc6502 100644 --- a/desktop/src-tauri/src/commands/agent_discovery/managed_node.rs +++ b/desktop/src-tauri/src/commands/agent_discovery/managed_node.rs @@ -93,12 +93,19 @@ fn managed_node_runtime_ready() -> bool { if !node.is_file() { return false; } - let output = std::process::Command::new(&node) + let mut command = std::process::Command::new(&node); + command .arg("--version") .stdin(std::process::Stdio::null()) .stdout(std::process::Stdio::piped()) - .stderr(std::process::Stdio::null()) - .output(); + .stderr(std::process::Stdio::null()); + #[cfg(windows)] + { + use std::os::windows::process::CommandExt; + const CREATE_NO_WINDOW: u32 = 0x0800_0000; + command.creation_flags(CREATE_NO_WINDOW); + } + let output = command.output(); output .ok() .filter(|output| output.status.success()) diff --git a/desktop/src-tauri/src/commands/agent_model_process.rs b/desktop/src-tauri/src/commands/agent_model_process.rs index 2093d4fe29..8cb5668a0e 100644 --- a/desktop/src-tauri/src/commands/agent_model_process.rs +++ b/desktop/src-tauri/src/commands/agent_model_process.rs @@ -49,6 +49,12 @@ pub(super) async fn run_agent_models_command( cmd.env(k, v); } crate::managed_agents::configure_runtime_cli(&mut cmd, known_acp_runtime(&agent_command)); + #[cfg(windows)] + { + use std::os::windows::process::CommandExt; + const CREATE_NO_WINDOW: u32 = 0x0800_0000; + cmd.creation_flags(CREATE_NO_WINDOW); + } cmd.stdout(std::process::Stdio::piped()) .stderr(std::process::Stdio::piped()) .output() diff --git a/desktop/src-tauri/src/commands/media_transcode.rs b/desktop/src-tauri/src/commands/media_transcode.rs index 48f14b5aaf..57b30775df 100644 --- a/desktop/src-tauri/src/commands/media_transcode.rs +++ b/desktop/src-tauri/src/commands/media_transcode.rs @@ -24,6 +24,12 @@ fn ffmpeg_command(path: &std::path::Path) -> std::process::Command { for (name, value) in required_windows_env { command.env(name, value); } + #[cfg(windows)] + { + use std::os::windows::process::CommandExt; + const CREATE_NO_WINDOW: u32 = 0x0800_0000; + command.creation_flags(CREATE_NO_WINDOW); + } command } @@ -576,7 +582,8 @@ mod tests { }; let source = std::env::temp_dir().join(format!("buzz-metadata-test-{}.mp4", uuid::Uuid::new_v4())); - let generated = std::process::Command::new(&ffmpeg) + let mut generate = std::process::Command::new(&ffmpeg); + generate .args(["-y", "-loglevel", "error", "-f", "lavfi", "-i"]) .arg("testsrc2=size=64x64:rate=1") .args([ @@ -589,9 +596,14 @@ mod tests { "-metadata:s:v:0", "title=private camera stream", ]) - .arg(&source) - .output() - .expect("run ffmpeg fixture generation"); + .arg(&source); + #[cfg(windows)] + { + use std::os::windows::process::CommandExt; + const CREATE_NO_WINDOW: u32 = 0x0800_0000; + generate.creation_flags(CREATE_NO_WINDOW); + } + let generated = generate.output().expect("run ffmpeg fixture generation"); if !generated.status.success() { eprintln!("skipping metadata round-trip: ffmpeg cannot encode H.264"); let _ = std::fs::remove_file(&source); @@ -623,12 +635,19 @@ mod tests { // Generate a small HEIC test image from a synthetic color source. let heic_path = std::env::temp_dir().join(format!("buzz-test-{}.heic", uuid::Uuid::new_v4())); - let gen = std::process::Command::new(&ffmpeg) + let mut generate = std::process::Command::new(&ffmpeg); + generate .args(["-y", "-loglevel", "error", "-f", "lavfi", "-i"]) .arg("color=c=red:s=64x64:d=1") .args(["-frames:v", "1"]) - .arg(&heic_path) - .output(); + .arg(&heic_path); + #[cfg(windows)] + { + use std::os::windows::process::CommandExt; + const CREATE_NO_WINDOW: u32 = 0x0800_0000; + generate.creation_flags(CREATE_NO_WINDOW); + } + let gen = generate.output(); let gen = match gen { Ok(o) if o.status.success() && heic_path.exists() => o, diff --git a/desktop/src-tauri/src/commands/project_git_exec.rs b/desktop/src-tauri/src/commands/project_git_exec.rs index 3f16c0fba9..c9b3371e10 100644 --- a/desktop/src-tauri/src/commands/project_git_exec.rs +++ b/desktop/src-tauri/src/commands/project_git_exec.rs @@ -80,6 +80,12 @@ pub(crate) fn run_git( command.stdin(Stdio::null()); command.stdout(Stdio::piped()); command.stderr(Stdio::piped()); + #[cfg(windows)] + { + use std::os::windows::process::CommandExt; + const CREATE_NO_WINDOW: u32 = 0x0800_0000; + command.creation_flags(CREATE_NO_WINDOW); + } let mut child = command .spawn() diff --git a/desktop/src-tauri/src/commands/relay_reconnect.rs b/desktop/src-tauri/src/commands/relay_reconnect.rs index f1e68d6c72..106dd34e77 100644 --- a/desktop/src-tauri/src/commands/relay_reconnect.rs +++ b/desktop/src-tauri/src/commands/relay_reconnect.rs @@ -55,12 +55,18 @@ fn run_with_timeout( argv: &[String], timeout: std::time::Duration, ) -> Result { - let mut child = std::process::Command::new(&argv[0]) + let mut command = std::process::Command::new(&argv[0]); + command .args(&argv[1..]) .stdout(std::process::Stdio::piped()) - .stderr(std::process::Stdio::null()) - .spawn() - .map_err(|e| format!("spawn failed: {e}"))?; + .stderr(std::process::Stdio::null()); + #[cfg(windows)] + { + use std::os::windows::process::CommandExt; + const CREATE_NO_WINDOW: u32 = 0x0800_0000; + command.creation_flags(CREATE_NO_WINDOW); + } + let mut child = command.spawn().map_err(|e| format!("spawn failed: {e}"))?; let deadline = std::time::Instant::now() + timeout; loop { diff --git a/desktop/src-tauri/src/managed_agents/agent_env.rs b/desktop/src-tauri/src/managed_agents/agent_env.rs index bf6bcb2298..b6536534f0 100644 --- a/desktop/src-tauri/src/managed_agents/agent_env.rs +++ b/desktop/src-tauri/src/managed_agents/agent_env.rs @@ -119,6 +119,12 @@ mod tests { let mut cmd = std::process::Command::new("env"); cmd.env_clear(); build_buzz_agent_provider_defaults(&mut cmd); + #[cfg(windows)] + { + use std::os::windows::process::CommandExt; + const CREATE_NO_WINDOW: u32 = 0x0800_0000; + cmd.creation_flags(CREATE_NO_WINDOW); + } let output = cmd.output().expect("env should run"); let stdout = String::from_utf8_lossy(&output.stdout); assert!( @@ -229,6 +235,12 @@ mod tests { cmd.env("BUZZ_AGENT_PROVIDER", "databricks"); // Simulate what runtime_metadata_env_vars writes from the record (comes after). cmd.env("BUZZ_AGENT_PROVIDER", "anthropic"); + #[cfg(windows)] + { + use std::os::windows::process::CommandExt; + const CREATE_NO_WINDOW: u32 = 0x0800_0000; + cmd.creation_flags(CREATE_NO_WINDOW); + } let output = cmd.output().expect("env should run"); let stdout = String::from_utf8_lossy(&output.stdout); assert!( diff --git a/desktop/src-tauri/src/managed_agents/backend.rs b/desktop/src-tauri/src/managed_agents/backend.rs index 5e7a9cbf77..b4e8059dac 100644 --- a/desktop/src-tauri/src/managed_agents/backend.rs +++ b/desktop/src-tauri/src/managed_agents/backend.rs @@ -30,10 +30,16 @@ pub fn invoke_provider( if let Some(home) = super::default_agent_workdir() { cmd.current_dir(home); } - let mut child = cmd - .stdin(std::process::Stdio::piped()) + cmd.stdin(std::process::Stdio::piped()) .stdout(std::process::Stdio::piped()) - .stderr(std::process::Stdio::piped()) + .stderr(std::process::Stdio::piped()); + #[cfg(windows)] + { + use std::os::windows::process::CommandExt; + const CREATE_NO_WINDOW: u32 = 0x0800_0000; + cmd.creation_flags(CREATE_NO_WINDOW); + } + let mut child = cmd .spawn() .map_err(|e| format!("failed to spawn {}: {e}", binary.display()))?; diff --git a/desktop/src-tauri/src/managed_agents/discovery.rs b/desktop/src-tauri/src/managed_agents/discovery.rs index 50206567ad..6d8741260e 100644 --- a/desktop/src-tauri/src/managed_agents/discovery.rs +++ b/desktop/src-tauri/src/managed_agents/discovery.rs @@ -677,7 +677,15 @@ fn login_shell_candidates() -> Vec { /// Returns trimmed stdout if the command succeeds with non-empty output. fn run_in_login_shell(args: &[&str]) -> Option { for shell in login_shell_candidates() { - let Ok(output) = Command::new(&shell).args(args).output() else { + let mut command = Command::new(&shell); + command.args(args); + #[cfg(windows)] + { + use std::os::windows::process::CommandExt; + const CREATE_NO_WINDOW: u32 = 0x0800_0000; + command.creation_flags(CREATE_NO_WINDOW); + } + let Ok(output) = command.output() else { continue; }; if !output.status.success() { @@ -916,6 +924,12 @@ fn probe_auth_status(binary_path: &Path, probe_args: &[&str]) -> AuthStatus { .stdin(std::process::Stdio::null()) .stdout(std::process::Stdio::piped()) .stderr(std::process::Stdio::piped()); + #[cfg(windows)] + { + use std::os::windows::process::CommandExt; + const CREATE_NO_WINDOW: u32 = 0x0800_0000; + command.creation_flags(CREATE_NO_WINDOW); + } let mut child = match command.spawn() { Ok(c) => c, @@ -1079,6 +1093,12 @@ pub(crate) fn probe_codex_acp_major_version_with_path( if let Some(path) = augmented_path { command.env("PATH", path); } + #[cfg(windows)] + { + use std::os::windows::process::CommandExt; + const CREATE_NO_WINDOW: u32 = 0x0800_0000; + command.creation_flags(CREATE_NO_WINDOW); + } let mut child = command .stdout(tmp.try_clone().ok()?) .stderr(std::process::Stdio::null())