diff --git a/src/core/agent_hooks.rs b/src/core/agent_hooks.rs index de31a4f9..ddce9d08 100644 --- a/src/core/agent_hooks.rs +++ b/src/core/agent_hooks.rs @@ -762,10 +762,9 @@ fn enable_codex_hooks_feature() -> Result<(), String> { .chain(home_dir().map(|h| h.join(".local/bin/codex"))) .find(|p| p.exists()); let program = candidates.unwrap_or_else(|| PathBuf::from("codex")); - match std::process::Command::new(&program) - .args(["features", "enable", "hooks"]) - .output() - { + let mut cmd = std::process::Command::new(&program); + cmd.args(["features", "enable", "hooks"]); + match crate::core::proc::hide_console(&mut cmd).output() { Ok(out) if out.status.success() => Ok(()), Ok(out) => Err(format!( "codex exited with {}: {}", diff --git a/src/core/mod.rs b/src/core/mod.rs index 073b26a7..aa190f83 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -18,6 +18,7 @@ pub mod config; #[allow(dead_code)] pub mod keychain; pub mod osc; +pub mod proc; pub mod session; pub mod shells; pub mod ssh_config; diff --git a/src/core/proc.rs b/src/core/proc.rs new file mode 100644 index 00000000..a23bdb0e --- /dev/null +++ b/src/core/proc.rs @@ -0,0 +1,40 @@ +//! One place for the Windows subprocess flag every helper shell-out needs. +//! +//! tty7 is a GUI process with no console of its own, so launching a console +//! subsystem program (`git.exe`, `wsl.exe`, …) makes Windows allocate a fresh +//! console for it — a black window that pops up and vanishes. That is invisible +//! on a one-off invocation and very visible on the git-status probe, which runs +//! four `git` calls every time a pane's cwd changes or a command ends. +//! +//! `CREATE_NO_WINDOW` suppresses the console entirely; stdout/stderr pipes are +//! unaffected, so output capture keeps working. Every non-PTY `Command` in the +//! app should go through [`hide_console`] (or [`hide_console_tokio`] for the +//! async flavor) before it runs. PTY children are not in scope — the daemon +//! owns those and passes its own flags (see [`crate::daemon::spawn`]). + +use std::process::Command; + +#[cfg(windows)] +const CREATE_NO_WINDOW: u32 = 0x0800_0000; + +/// Suppress the console window Windows would otherwise allocate for a console +/// subsystem child. No-op on Unix, so callers stay `cfg`-free. +pub fn hide_console(cmd: &mut Command) -> &mut Command { + #[cfg(windows)] + { + use std::os::windows::process::CommandExt as _; + cmd.creation_flags(CREATE_NO_WINDOW); + } + cmd +} + +/// [`hide_console`] for `tokio::process::Command`. Separate because tokio's +/// builder is a distinct type with its own `creation_flags`, not a `Deref` to +/// the std one. +pub fn hide_console_tokio(cmd: &mut tokio::process::Command) -> &mut tokio::process::Command { + #[cfg(windows)] + { + cmd.creation_flags(CREATE_NO_WINDOW); + } + cmd +} diff --git a/src/core/shells.rs b/src/core/shells.rs index 97e6bb21..e4249bcf 100644 --- a/src/core/shells.rs +++ b/src/core/shells.rs @@ -319,17 +319,13 @@ fn find_git_bash() -> Option { } /// Installed WSL distribution names via `wsl.exe -l -q`, or empty when WSL is -/// absent. `CREATE_NO_WINDOW` keeps the probe from flashing a console window -/// (we're a GUI process). +/// absent. [`hide_console`](crate::core::proc::hide_console) keeps the probe +/// from flashing a console window (we're a GUI process). #[cfg(windows)] fn list_wsl_distros() -> Vec { - use std::os::windows::process::CommandExt as _; - const CREATE_NO_WINDOW: u32 = 0x0800_0000; - let Ok(output) = std::process::Command::new("wsl.exe") - .args(["-l", "-q"]) - .creation_flags(CREATE_NO_WINDOW) - .output() - else { + let mut cmd = std::process::Command::new("wsl.exe"); + cmd.args(["-l", "-q"]); + let Ok(output) = crate::core::proc::hide_console(&mut cmd).output() else { return Vec::new(); }; if !output.status.success() { diff --git a/src/core/worktree.rs b/src/core/worktree.rs index d88796d7..e908ff37 100644 --- a/src/core/worktree.rs +++ b/src/core/worktree.rs @@ -62,10 +62,9 @@ pub fn is_inside_repo(cwd: &Path) -> bool { /// Run `git -C `, returning trimmed stdout on success and trimmed /// stderr as the error otherwise. fn git(dir: &Path, args: &[&str]) -> Result { - let out = std::process::Command::new("git") - .arg("-C") - .arg(dir) - .args(args) + let mut cmd = std::process::Command::new("git"); + cmd.arg("-C").arg(dir).args(args); + let out = crate::core::proc::hide_console(&mut cmd) .output() .map_err(|e| format!("failed to run git: {e}"))?; if out.status.success() { diff --git a/src/daemon/ssh/connect.rs b/src/daemon/ssh/connect.rs index bc10c484..319d989c 100644 --- a/src/daemon/ssh/connect.rs +++ b/src/daemon/ssh/connect.rs @@ -173,6 +173,10 @@ fn spawn_proxy_command( .stdout(std::process::Stdio::piped()) .stderr(std::process::Stdio::inherit()) .kill_on_drop(true); + // The daemon is detached and has no console to lend this child, so without + // the flag a `ProxyCommand` (`ssh -W`, `connect.exe`, `cloudflared`) gets a + // console of its own that stays up for the whole session. + crate::core::proc::hide_console_tokio(&mut cmd); let mut child = cmd .spawn() .map_err(|e| anyhow::anyhow!("spawn ProxyCommand failed: {e}"))?; diff --git a/src/terminal/git_status.rs b/src/terminal/git_status.rs index 098165d6..8bca6ed5 100644 --- a/src/terminal/git_status.rs +++ b/src/terminal/git_status.rs @@ -251,18 +251,19 @@ fn diff_numstat(cwd: &Path) -> Option<(u32, u32)> { /// Run `git -C ` and return stdout on success, `None` on a /// non-zero exit or a missing `git`. `GIT_OPTIONAL_LOCKS=0` makes the read /// truly read-only; stdin is nulled so a misconfigured git can't block on a -/// prompt. Shared with [`git_diff`](crate::terminal::git_diff) so every git -/// read in the app goes through the same lock-free, prompt-proof invocation. +/// prompt; `hide_console` keeps this GUI process from flashing a console window +/// on Windows for every probe. Shared with [`git_diff`](crate::terminal::git_diff) +/// so every git read in the app goes through the same lock-free, prompt-proof +/// invocation. pub(crate) fn git(cwd: &Path, args: &[&str]) -> Option { - let out = Command::new("git") - .arg("-C") + let mut cmd = Command::new("git"); + cmd.arg("-C") .arg(cwd) .args(args) .env("GIT_OPTIONAL_LOCKS", "0") .stdin(Stdio::null()) - .stderr(Stdio::null()) - .output() - .ok()?; + .stderr(Stdio::null()); + let out = crate::core::proc::hide_console(&mut cmd).output().ok()?; if !out.status.success() { return None; } diff --git a/src/ui/app.rs b/src/ui/app.rs index a4fb4435..fdcab0dc 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -3048,9 +3048,9 @@ impl Tty7App { // which is what a review pass wants. Both invocations are quick; the // prompt builder caps runaway diffs. let run = |args: &[&str]| { - std::process::Command::new("git") - .args(args) - .current_dir(&cwd) + let mut cmd = std::process::Command::new("git"); + cmd.args(args).current_dir(&cwd); + crate::core::proc::hide_console(&mut cmd) .output() .ok() .filter(|o| o.status.success())