diff --git a/src-tauri/src/backend/app_server.rs b/src-tauri/src/backend/app_server.rs index 3b68ad65..96801a28 100644 --- a/src-tauri/src/backend/app_server.rs +++ b/src-tauri/src/backend/app_server.rs @@ -13,6 +13,7 @@ use tokio::sync::{mpsc, oneshot, Mutex}; use tokio::time::timeout; use crate::backend::events::{AppServerEvent, EventSink}; +use crate::shared::process_core::tokio_command; use crate::codex::args::apply_codex_args; use crate::types::WorkspaceEntry; @@ -138,7 +139,7 @@ pub(crate) fn build_codex_command_with_bin(codex_bin: Option) -> Command .clone() .filter(|value| !value.trim().is_empty()) .unwrap_or_else(|| "codex".into()); - let mut command = Command::new(bin); + let mut command = tokio_command(bin); if let Some(path_env) = build_codex_path_env(codex_bin.as_deref()) { command.env("PATH", path_env); } diff --git a/src-tauri/src/codex/mod.rs b/src-tauri/src/codex/mod.rs index c0ae9ed7..d417dbe2 100644 --- a/src-tauri/src/codex/mod.rs +++ b/src-tauri/src/codex/mod.rs @@ -5,7 +5,6 @@ use std::sync::Arc; use std::time::Duration; use tauri::{AppHandle, Emitter, State}; -use tokio::process::Command; use tokio::sync::mpsc; use tokio::time::timeout; @@ -19,6 +18,7 @@ use crate::backend::app_server::{ build_codex_command_with_bin, build_codex_path_env, check_codex_installation, spawn_workspace_session as spawn_workspace_session_inner, }; +use crate::shared::process_core::tokio_command; use crate::event_sink::TauriEventSink; use crate::remote_backend; use crate::shared::codex_core; @@ -77,7 +77,7 @@ pub(crate) async fn codex_doctor( Err(_) => false, }; let (node_ok, node_version, node_details) = { - let mut node_command = Command::new("node"); + let mut node_command = tokio_command("node"); if let Some(ref path_env) = path_env { node_command.env("PATH", path_env); } diff --git a/src-tauri/src/git/mod.rs b/src-tauri/src/git/mod.rs index 933ec8c2..3791c180 100644 --- a/src-tauri/src/git/mod.rs +++ b/src-tauri/src/git/mod.rs @@ -5,8 +5,8 @@ use base64::{engine::general_purpose::STANDARD, Engine as _}; use git2::{BranchType, DiffOptions, Repository, Sort, Status, StatusOptions}; use serde_json::json; use tauri::State; -use tokio::process::Command; +use crate::shared::process_core::tokio_command; use crate::git_utils::{ checkout_branch, commit_to_entry, diff_patch_to_string, diff_stats_for_path, image_mime_type, list_git_roots as scan_git_roots, parse_github_repo, resolve_git_root, @@ -47,7 +47,7 @@ fn read_image_base64(path: &Path) -> Option { async fn run_git_command(repo_root: &Path, args: &[&str]) -> Result<(), String> { let git_bin = resolve_git_binary().map_err(|e| format!("Failed to run git: {e}"))?; - let output = Command::new(git_bin) + let output = tokio_command(git_bin) .args(args) .current_dir(repo_root) .env("PATH", git_env_path()) @@ -1127,7 +1127,7 @@ pub(crate) async fn get_github_issues( let repo_root = resolve_git_root(&entry)?; let repo_name = github_repo_from_path(&repo_root)?; - let output = Command::new("gh") + let output = tokio_command("gh") .args([ "issue", "list", @@ -1162,7 +1162,7 @@ pub(crate) async fn get_github_issues( let search_query = format!("repo:{repo_name} is:issue is:open"); let search_query = search_query.replace(' ', "+"); - let total = match Command::new("gh") + let total = match tokio_command("gh") .args([ "api", &format!("/search/issues?q={search_query}"), @@ -1197,7 +1197,7 @@ pub(crate) async fn get_github_pull_requests( let repo_root = resolve_git_root(&entry)?; let repo_name = github_repo_from_path(&repo_root)?; - let output = Command::new("gh") + let output = tokio_command("gh") .args([ "pr", "list", @@ -1234,7 +1234,7 @@ pub(crate) async fn get_github_pull_requests( let search_query = format!("repo:{repo_name} is:pr is:open"); let search_query = search_query.replace(' ', "+"); - let total = match Command::new("gh") + let total = match tokio_command("gh") .args([ "api", &format!("/search/issues?q={search_query}"), @@ -1273,7 +1273,7 @@ pub(crate) async fn get_github_pull_request_diff( let repo_root = resolve_git_root(&entry)?; let repo_name = github_repo_from_path(&repo_root)?; - let output = Command::new("gh") + let output = tokio_command("gh") .args([ "pr", "diff", @@ -1325,7 +1325,7 @@ pub(crate) async fn get_github_pull_request_comments( format!("/repos/{repo_name}/issues/{pr_number}/comments?per_page=30"); let jq_filter = r#"[.[] | {id, body, createdAt: .created_at, url: .html_url, author: (if .user then {login: .user.login} else null end)}]"#; - let output = Command::new("gh") + let output = tokio_command("gh") .args(["api", &comments_endpoint, "--jq", jq_filter]) .current_dir(&repo_root) .output() diff --git a/src-tauri/src/shared/git_core.rs b/src-tauri/src/shared/git_core.rs index 8bf9e86a..cbe24b6c 100644 --- a/src-tauri/src/shared/git_core.rs +++ b/src-tauri/src/shared/git_core.rs @@ -2,8 +2,7 @@ use std::path::PathBuf; -use tokio::process::Command; - +use crate::shared::process_core::tokio_command; use crate::utils::{git_env_path, resolve_git_binary}; fn format_git_error(stdout: &[u8], stderr: &[u8]) -> String { @@ -23,7 +22,7 @@ fn format_git_error(stdout: &[u8], stderr: &[u8]) -> String { pub(crate) async fn run_git_command(repo_path: &PathBuf, args: &[&str]) -> Result { let git_bin = resolve_git_binary().map_err(|err| format!("Failed to run git: {err}"))?; - let output = Command::new(git_bin) + let output = tokio_command(git_bin) .args(args) .current_dir(repo_path) .env("PATH", git_env_path()) @@ -52,7 +51,7 @@ pub(crate) async fn run_git_command_bytes( args: &[&str], ) -> Result, String> { let git_bin = resolve_git_binary().map_err(|err| format!("Failed to run git: {err}"))?; - let output = Command::new(git_bin) + let output = tokio_command(git_bin) .args(args) .current_dir(repo_path) .env("PATH", git_env_path()) @@ -67,7 +66,7 @@ pub(crate) async fn run_git_command_bytes( pub(crate) async fn run_git_diff(repo_path: &PathBuf, args: &[&str]) -> Result, String> { let git_bin = resolve_git_binary().map_err(|err| format!("Failed to run git: {err}"))?; - let output = Command::new(git_bin) + let output = tokio_command(git_bin) .args(args) .current_dir(repo_path) .env("PATH", git_env_path()) @@ -86,7 +85,7 @@ pub(crate) fn is_missing_worktree_error(error: &str) -> bool { pub(crate) async fn git_branch_exists(repo_path: &PathBuf, branch: &str) -> Result { let git_bin = resolve_git_binary().map_err(|err| format!("Failed to run git: {err}"))?; - let status = Command::new(git_bin) + let status = tokio_command(git_bin) .args(["show-ref", "--verify", &format!("refs/heads/{branch}")]) .current_dir(repo_path) .env("PATH", git_env_path()) @@ -98,7 +97,7 @@ pub(crate) async fn git_branch_exists(repo_path: &PathBuf, branch: &str) -> Resu pub(crate) async fn git_remote_exists(repo_path: &PathBuf, remote: &str) -> Result { let git_bin = resolve_git_binary().map_err(|err| format!("Failed to run git: {err}"))?; - let status = Command::new(git_bin) + let status = tokio_command(git_bin) .args(["remote", "get-url", remote]) .current_dir(repo_path) .env("PATH", git_env_path()) @@ -114,7 +113,7 @@ pub(crate) async fn git_remote_branch_exists_live( branch: &str, ) -> Result { let git_bin = resolve_git_binary().map_err(|err| format!("Failed to run git: {err}"))?; - let output = Command::new(git_bin) + let output = tokio_command(git_bin) .args([ "ls-remote", "--heads", @@ -138,7 +137,7 @@ pub(crate) async fn git_remote_branch_exists_local( branch: &str, ) -> Result { let git_bin = resolve_git_binary().map_err(|err| format!("Failed to run git: {err}"))?; - let status = Command::new(git_bin) + let status = tokio_command(git_bin) .args([ "show-ref", "--verify", diff --git a/src-tauri/src/shared/mod.rs b/src-tauri/src/shared/mod.rs index 65833bc5..f9c0d99f 100644 --- a/src-tauri/src/shared/mod.rs +++ b/src-tauri/src/shared/mod.rs @@ -2,6 +2,7 @@ pub(crate) mod account; pub(crate) mod codex_core; pub(crate) mod files_core; pub(crate) mod git_core; +pub(crate) mod process_core; pub(crate) mod settings_core; pub(crate) mod worktree_core; pub(crate) mod workspaces_core; diff --git a/src-tauri/src/shared/process_core.rs b/src-tauri/src/shared/process_core.rs new file mode 100644 index 00000000..d9eb90eb --- /dev/null +++ b/src-tauri/src/shared/process_core.rs @@ -0,0 +1,20 @@ +use std::ffi::OsStr; + +use tokio::process::Command; + +/// On Windows, spawning a console app from a GUI subsystem app will open a new +/// console window unless we explicitly disable it. +fn hide_console_on_windows(_command: &mut std::process::Command) { + #[cfg(windows)] + { + use std::os::windows::process::CommandExt; + const CREATE_NO_WINDOW: u32 = 0x08000000; + _command.creation_flags(CREATE_NO_WINDOW); + } +} + +pub(crate) fn tokio_command(program: impl AsRef) -> Command { + let mut command = Command::new(program); + hide_console_on_windows(command.as_std_mut()); + command +} \ No newline at end of file diff --git a/src-tauri/src/workspaces/commands.rs b/src-tauri/src/workspaces/commands.rs index d48fbdce..f7e7f217 100644 --- a/src-tauri/src/workspaces/commands.rs +++ b/src-tauri/src/workspaces/commands.rs @@ -5,7 +5,6 @@ use std::sync::Arc; use serde_json::json; use tauri::{AppHandle, Manager, State}; use tokio::io::AsyncWriteExt; -use tokio::process::Command; use uuid::Uuid; #[cfg(target_os = "macos")] @@ -28,6 +27,7 @@ use crate::codex::args::resolve_workspace_codex_args; use crate::codex::home::resolve_workspace_codex_home; use crate::git_utils::resolve_git_root; use crate::remote_backend; +use crate::shared::process_core::tokio_command; use crate::shared::workspaces_core; use crate::state::AppState; use crate::storage::write_workspaces; @@ -633,7 +633,7 @@ pub(crate) async fn apply_worktree_changes( } let git_bin = resolve_git_binary().map_err(|e| format!("Failed to run git: {e}"))?; - let mut child = Command::new(git_bin) + let mut child = tokio_command(git_bin) .args(["apply", "--3way", "--whitespace=nowarn", "-"]) .current_dir(&parent_root) .env("PATH", git_env_path())