From e32819db20739ce8be8e06fc79a70a107ae87492 Mon Sep 17 00:00:00 2001 From: Yelsin Sepulveda Date: Thu, 29 Jan 2026 22:45:54 -0500 Subject: [PATCH] fix(windows): prevent console window flickering when spawning subprocesses On Windows, shell commands were causing PowerShell/Command Prompt windows to briefly flash on screen. This was a regression where the CREATE_NO_WINDOW flag (0x08000000) was not being applied when spawning subprocesses. Apply the flag to all subprocess creation points in goose-mcp Signed-off-by: Yelsin Sepulveda --- .../goose-mcp/src/computercontroller/mod.rs | 46 +++++++++++-------- .../computercontroller/platform/windows.rs | 4 ++ crates/goose-mcp/src/developer/paths.rs | 24 ++++++---- crates/goose-mcp/src/developer/shell.rs | 19 ++++++-- 4 files changed, 61 insertions(+), 32 deletions(-) diff --git a/crates/goose-mcp/src/computercontroller/mod.rs b/crates/goose-mcp/src/computercontroller/mod.rs index 297d2524912d..f8ba435cbdb0 100644 --- a/crates/goose-mcp/src/computercontroller/mod.rs +++ b/crates/goose-mcp/src/computercontroller/mod.rs @@ -19,6 +19,12 @@ use tokio::process::Command; #[cfg(unix)] use std::os::unix::fs::PermissionsExt; +#[cfg(windows)] +use std::os::windows::process::CommandExt; + +#[cfg(windows)] +const CREATE_NO_WINDOW: u32 = 0x08000000; + mod docx_tool; mod pdf_tool; mod xlsx_tool; @@ -703,35 +709,35 @@ impl ComputerControllerServer { let output = match language { ScriptLanguage::Powershell => { // For PowerShell, we need to use -File instead of -Command - Command::new("powershell") - .arg("-NoProfile") + let mut cmd = Command::new("powershell"); + cmd.arg("-NoProfile") .arg("-NonInteractive") .arg("-File") .arg(&command) - .env("GOOSE_TERMINAL", "1") - .output() - .await - .map_err(|e| { - ErrorData::new( - ErrorCode::INTERNAL_ERROR, - format!("Failed to run script: {}", e), - None, - ) - })? + .env("GOOSE_TERMINAL", "1"); + #[cfg(windows)] + cmd.creation_flags(CREATE_NO_WINDOW); + cmd.output().await.map_err(|e| { + ErrorData::new( + ErrorCode::INTERNAL_ERROR, + format!("Failed to run script: {}", e), + None, + ) + })? } - _ => Command::new(shell) - .arg(shell_arg) - .arg(&command) - .env("GOOSE_TERMINAL", "1") - .output() - .await - .map_err(|e| { + _ => { + let mut cmd = Command::new(shell); + cmd.arg(shell_arg).arg(&command).env("GOOSE_TERMINAL", "1"); + #[cfg(windows)] + cmd.creation_flags(CREATE_NO_WINDOW); + cmd.output().await.map_err(|e| { ErrorData::new( ErrorCode::INTERNAL_ERROR, format!("Failed to run script: {}", e), None, ) - })?, + })? + } }; let output_str = String::from_utf8_lossy(&output.stdout).into_owned(); diff --git a/crates/goose-mcp/src/computercontroller/platform/windows.rs b/crates/goose-mcp/src/computercontroller/platform/windows.rs index 8ea17f461c38..9931df7f2f59 100644 --- a/crates/goose-mcp/src/computercontroller/platform/windows.rs +++ b/crates/goose-mcp/src/computercontroller/platform/windows.rs @@ -2,6 +2,9 @@ use super::SystemAutomation; use std::path::PathBuf; use std::process::Command; +use std::os::windows::process::CommandExt; +const CREATE_NO_WINDOW: u32 = 0x08000000; + pub struct WindowsAutomation; impl SystemAutomation for WindowsAutomation { @@ -12,6 +15,7 @@ impl SystemAutomation for WindowsAutomation { .arg("-Command") .arg(script) .env("GOOSE_TERMINAL", "1") + .creation_flags(CREATE_NO_WINDOW) .output()?; Ok(String::from_utf8_lossy(&output.stdout).into_owned()) diff --git a/crates/goose-mcp/src/developer/paths.rs b/crates/goose-mcp/src/developer/paths.rs index 29d3f2a0f3bf..feb1c51bba9b 100644 --- a/crates/goose-mcp/src/developer/paths.rs +++ b/crates/goose-mcp/src/developer/paths.rs @@ -4,6 +4,12 @@ use std::path::PathBuf; use tokio::process::Command; use tokio::sync::OnceCell; +#[cfg(windows)] +use std::os::windows::process::CommandExt; + +#[cfg(windows)] +const CREATE_NO_WINDOW: u32 = 0x08000000; + static SHELL_PATH_DIRS: OnceCell, anyhow::Error>> = OnceCell::const_new(); pub async fn get_shell_path_dirs() -> Result<&'static Vec> { @@ -79,16 +85,18 @@ async fn get_windows_path_async(shell: &str) -> Result { let output = match shell_name { "pwsh" | "powershell" => { - Command::new(shell) - .args(["-NoLogo", "-Command", "$env:PATH"]) - .output() - .await + let mut cmd = Command::new(shell); + cmd.args(["-NoLogo", "-Command", "$env:PATH"]); + #[cfg(windows)] + cmd.creation_flags(CREATE_NO_WINDOW); + cmd.output().await } _ => { - Command::new(shell) - .args(["/c", "echo %PATH%"]) - .output() - .await + let mut cmd = Command::new(shell); + cmd.args(["/c", "echo %PATH%"]); + #[cfg(windows)] + cmd.creation_flags(CREATE_NO_WINDOW); + cmd.output().await } }; diff --git a/crates/goose-mcp/src/developer/shell.rs b/crates/goose-mcp/src/developer/shell.rs index a05242833d7b..d7ad89d9a3fc 100644 --- a/crates/goose-mcp/src/developer/shell.rs +++ b/crates/goose-mcp/src/developer/shell.rs @@ -4,6 +4,12 @@ use std::{env, ffi::OsString, process::Stdio}; #[allow(unused_imports)] // False positive: trait is used for process_group method use std::os::unix::process::CommandExt; +#[cfg(windows)] +use std::os::windows::process::CommandExt; + +#[cfg(windows)] +const CREATE_NO_WINDOW: u32 = 0x08000000; + #[derive(Debug, Clone)] pub struct ShellConfig { pub executable: String, @@ -132,6 +138,11 @@ pub fn configure_shell_command( .args(&shell_config.args) .arg(command); + #[cfg(windows)] + { + command_builder.creation_flags(CREATE_NO_WINDOW); + } + // On Unix systems, create a new process group so we can kill child processes #[cfg(unix)] { @@ -170,10 +181,10 @@ pub async fn kill_process_group( { if let Some(pid) = pid { // Use taskkill to kill the process tree on Windows - let _kill_result = tokio::process::Command::new("taskkill") - .args(&["/F", "/T", "/PID", &pid.to_string()]) - .output() - .await; + let mut kill_cmd = tokio::process::Command::new("taskkill") + .args(["/F", "/T", "/PID", &pid.to_string()]) + .creation_flags(CREATE_NO_WINDOW); + let _kill_result = kill_cmd.output().await; } // Return the result of tokio's kill