diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 30d81fd..efc58c3 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -7,6 +7,7 @@ use tauri_plugin_store::StoreExt; use crate::achievements::{get_achievement_info, load_achievements, AchievementUnlockedEvent}; use crate::bridge_manager::SharedBridgeManager; use crate::config::{ClaudeStartOptions, HikariConfig}; +use crate::process_ext::HideWindow; use crate::stats::UsageStats; use crate::temp_manager::SharedTempFileManager; @@ -59,6 +60,7 @@ fn create_claude_command() -> std::process::Command { // Non-login shells launched by `wsl` don't inherit the full user PATH, // so we need to use a login shell to get the correct PATH let which_output = std::process::Command::new("wsl") + .hide_window() .args(["-e", "bash", "-l", "-c", "which claude"]) .output(); @@ -66,6 +68,7 @@ fn create_claude_command() -> std::process::Command { Ok(output) if output.status.success() => { let claude_path = String::from_utf8_lossy(&output.stdout).trim().to_string(); let mut cmd = std::process::Command::new("wsl"); + cmd.hide_window(); cmd.arg(claude_path); cmd } @@ -73,6 +76,7 @@ fn create_claude_command() -> std::process::Command { // Fallback to just "claude" if which fails // This maintains backwards compatibility let mut cmd = std::process::Command::new("wsl"); + cmd.hide_window(); cmd.arg("claude"); cmd } @@ -85,18 +89,23 @@ fn create_claude_command() -> std::process::Command { // This works regardless of how Claude Code was installed (standalone, npm, etc.) // and avoids hardcoding paths let which_output = std::process::Command::new("which") + .hide_window() .arg("claude") .output(); match which_output { Ok(output) if output.status.success() => { let claude_path = String::from_utf8_lossy(&output.stdout).trim().to_string(); - std::process::Command::new(claude_path) + let mut cmd = std::process::Command::new(claude_path); + cmd.hide_window(); + cmd } _ => { // Fallback to just "claude" if which fails // This maintains backwards compatibility - std::process::Command::new("claude") + let mut cmd = std::process::Command::new("claude"); + cmd.hide_window(); + cmd } } } @@ -412,6 +421,7 @@ async fn list_workspace_commands(working_dir: &str, use_wsl: bool) -> Vec Result, String> { // Use WSL to list directories in ~/.claude/skills that contain SKILL.md let output = Command::new("wsl") + .hide_window() .args([ "-e", "sh", @@ -794,6 +805,7 @@ async fn list_directory_via_wsl(path: &str) -> Result, String> { ); let output = Command::new("wsl") + .hide_window() .args(["-e", "sh", "-c", &script]) .output() .map_err(|e| format!("Failed to execute WSL command: {}", e))?; @@ -856,6 +868,7 @@ async fn read_file_via_wsl(path: &str) -> Result { use std::process::Command; let output = Command::new("wsl") + .hide_window() .args(["-e", "cat", path]) .output() .map_err(|e| format!("Failed to execute WSL command: {}", e))?; @@ -887,6 +900,7 @@ async fn write_file_via_wsl(path: &str, content: &str) -> Result<(), String> { use std::process::{Command, Stdio}; let mut child = Command::new("wsl") + .hide_window() .args(["-e", "sh", "-c", &format!("cat > '{}'", path)]) .stdin(Stdio::piped()) .spawn() @@ -935,6 +949,7 @@ async fn create_file_via_wsl(path: &str) -> Result<(), String> { // Check if file exists first let check = Command::new("wsl") + .hide_window() .args(["-e", "test", "-e", path]) .status() .map_err(|e| format!("Failed to execute WSL command: {}", e))?; @@ -944,6 +959,7 @@ async fn create_file_via_wsl(path: &str) -> Result<(), String> { } let output = Command::new("wsl") + .hide_window() .args(["-e", "touch", path]) .output() .map_err(|e| format!("Failed to execute WSL command: {}", e))?; @@ -984,6 +1000,7 @@ async fn create_directory_via_wsl(path: &str) -> Result<(), String> { // Check if directory exists first let check = Command::new("wsl") + .hide_window() .args(["-e", "test", "-e", path]) .status() .map_err(|e| format!("Failed to execute WSL command: {}", e))?; @@ -993,6 +1010,7 @@ async fn create_directory_via_wsl(path: &str) -> Result<(), String> { } let output = Command::new("wsl") + .hide_window() .args(["-e", "mkdir", "-p", path]) .output() .map_err(|e| format!("Failed to execute WSL command: {}", e))?; @@ -1037,6 +1055,7 @@ async fn delete_file_via_wsl(path: &str) -> Result<(), String> { // Check if path exists let check_exists = Command::new("wsl") + .hide_window() .args(["-e", "test", "-e", path]) .status() .map_err(|e| format!("Failed to execute WSL command: {}", e))?; @@ -1047,6 +1066,7 @@ async fn delete_file_via_wsl(path: &str) -> Result<(), String> { // Check if path is a directory let check_dir = Command::new("wsl") + .hide_window() .args(["-e", "test", "-d", path]) .status() .map_err(|e| format!("Failed to execute WSL command: {}", e))?; @@ -1056,6 +1076,7 @@ async fn delete_file_via_wsl(path: &str) -> Result<(), String> { } let output = Command::new("wsl") + .hide_window() .args(["-e", "rm", path]) .output() .map_err(|e| format!("Failed to execute WSL command: {}", e))?; @@ -1100,6 +1121,7 @@ async fn delete_directory_via_wsl(path: &str) -> Result<(), String> { // Check if path exists let check_exists = Command::new("wsl") + .hide_window() .args(["-e", "test", "-e", path]) .status() .map_err(|e| format!("Failed to execute WSL command: {}", e))?; @@ -1110,6 +1132,7 @@ async fn delete_directory_via_wsl(path: &str) -> Result<(), String> { // Check if path is a directory let check_dir = Command::new("wsl") + .hide_window() .args(["-e", "test", "-d", path]) .status() .map_err(|e| format!("Failed to execute WSL command: {}", e))?; @@ -1119,6 +1142,7 @@ async fn delete_directory_via_wsl(path: &str) -> Result<(), String> { } let output = Command::new("wsl") + .hide_window() .args(["-e", "rm", "-rf", path]) .output() .map_err(|e| format!("Failed to execute WSL command: {}", e))?; @@ -1164,6 +1188,7 @@ async fn rename_path_via_wsl(old_path: &str, new_path: &str) -> Result<(), Strin // Check if old path exists let check_old = Command::new("wsl") + .hide_window() .args(["-e", "test", "-e", old_path]) .status() .map_err(|e| format!("Failed to execute WSL command: {}", e))?; @@ -1174,6 +1199,7 @@ async fn rename_path_via_wsl(old_path: &str, new_path: &str) -> Result<(), Strin // Check if new path already exists let check_new = Command::new("wsl") + .hide_window() .args(["-e", "test", "-e", new_path]) .status() .map_err(|e| format!("Failed to execute WSL command: {}", e))?; @@ -1183,6 +1209,7 @@ async fn rename_path_via_wsl(old_path: &str, new_path: &str) -> Result<(), Strin } let output = Command::new("wsl") + .hide_window() .args(["-e", "mv", old_path, new_path]) .output() .map_err(|e| format!("Failed to execute WSL command: {}", e))?; @@ -1360,6 +1387,7 @@ async fn list_memory_files_via_wsl() -> Result { "#; let output = Command::new("wsl") + .hide_window() .args(["-e", "bash", "-l", "-c", script]) .output() .map_err(|e| format!("Failed to execute WSL command: {}", e))?; diff --git a/src-tauri/src/git.rs b/src-tauri/src/git.rs index 8a3fd9e..356b16a 100644 --- a/src-tauri/src/git.rs +++ b/src-tauri/src/git.rs @@ -1,6 +1,8 @@ use serde::{Deserialize, Serialize}; use std::process::Command; +use crate::process_ext::HideWindow; + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct GitStatus { pub is_repo: bool, @@ -37,6 +39,7 @@ pub struct GitLogEntry { fn run_git_command(working_dir: &str, args: &[&str]) -> Result { let output = Command::new("git") + .hide_window() .args(args) .current_dir(working_dir) .output() diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index e1b8b18..0c0d9b3 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -8,6 +8,7 @@ mod debug_logger; mod discord_rpc; mod git; mod notifications; +mod process_ext; mod quick_actions; mod sessions; mod snippets; diff --git a/src-tauri/src/notifications.rs b/src-tauri/src/notifications.rs index 590fc35..976087a 100644 --- a/src-tauri/src/notifications.rs +++ b/src-tauri/src/notifications.rs @@ -1,6 +1,8 @@ use std::process::Command; use tauri::command; +use crate::process_ext::HideWindow; + /// Generate PowerShell script for Windows Toast Notification fn generate_powershell_toast_script(title: &str, body: &str) -> String { format!( @@ -82,6 +84,7 @@ fn build_simple_notification_command(title: &str, body: &str) -> (String, Vec Result<(), String> { // Use notify-send for Linux/WSL let output = Command::new("notify-send") + .hide_window() .arg(&title) .arg(&body) .arg("--urgency=normal") @@ -109,6 +112,7 @@ pub async fn send_windows_notification(title: String, body: String) -> Result<() // Try PowerShell Core first (pwsh), then fall back to Windows PowerShell let output = Command::new("pwsh.exe") + .hide_window() .arg("-NoProfile") .arg("-WindowStyle") .arg("Hidden") @@ -117,6 +121,7 @@ pub async fn send_windows_notification(title: String, body: String) -> Result<() .output() .or_else(|_| { Command::new("powershell.exe") + .hide_window() .arg("-NoProfile") .arg("-WindowStyle") .arg("Hidden") @@ -140,6 +145,7 @@ pub async fn send_simple_notification(title: String, body: String) -> Result<(), let message = format_simple_notification(&title, &body); Command::new("cmd.exe") + .hide_window() .arg("/c") .arg("msg") .arg("*") diff --git a/src-tauri/src/process_ext.rs b/src-tauri/src/process_ext.rs new file mode 100644 index 0000000..49c3d57 --- /dev/null +++ b/src-tauri/src/process_ext.rs @@ -0,0 +1,21 @@ +use std::process::Command; + +/// Extension trait for `Command` that hides the console window on Windows. +/// +/// On non-Windows platforms this is a no-op, so callers can unconditionally +/// chain `.hide_window()` without any `#[cfg]` guards at the call sites. +pub trait HideWindow { + fn hide_window(&mut self) -> &mut Self; +} + +impl HideWindow for Command { + fn hide_window(&mut self) -> &mut Self { + #[cfg(target_os = "windows")] + { + use std::os::windows::process::CommandExt; + const CREATE_NO_WINDOW: u32 = 0x08000000; + self.creation_flags(CREATE_NO_WINDOW); + } + self + } +} diff --git a/src-tauri/src/vbs_notification.rs b/src-tauri/src/vbs_notification.rs index f100d91..c3a6670 100644 --- a/src-tauri/src/vbs_notification.rs +++ b/src-tauri/src/vbs_notification.rs @@ -3,6 +3,8 @@ use std::process::Command; use tauri::command; use tempfile::NamedTempFile; +use crate::process_ext::HideWindow; + #[command] pub async fn send_vbs_notification(title: String, body: String) -> Result<(), String> { // Create a VBScript that shows a Windows notification @@ -40,7 +42,7 @@ objShell.Popup "{}" & vbCrLf & vbCrLf & "{}", 5, "{}", 64 } else if temp_path.starts_with("/tmp/") { // WSL temp files might be in a different location // Try to use wslpath to convert - let output = Command::new("wslpath").arg("-w").arg(&temp_path).output(); + let output = Command::new("wslpath").hide_window().arg("-w").arg(&temp_path).output(); if let Ok(result) = output { if result.status.success() { @@ -57,6 +59,7 @@ objShell.Popup "{}" & vbCrLf & vbCrLf & "{}", 5, "{}", 64 // Execute the VBScript using wscript.exe let output = Command::new("/mnt/c/Windows/System32/wscript.exe") + .hide_window() .arg("//NoLogo") .arg(&windows_path) .output() diff --git a/src-tauri/src/wsl_bridge.rs b/src-tauri/src/wsl_bridge.rs index 77119c6..6cca66a 100644 --- a/src-tauri/src/wsl_bridge.rs +++ b/src-tauri/src/wsl_bridge.rs @@ -12,6 +12,7 @@ use std::os::windows::process::CommandExt; use crate::achievements::{get_achievement_info, AchievementUnlockedEvent}; use crate::commands::record_cost; use crate::config::ClaudeStartOptions; +use crate::process_ext::HideWindow; use crate::stats::{calculate_cost, StatsUpdateEvent, UsageStats}; use crate::types::{ AgentEndEvent, AgentStartEvent, CharacterState, ClaudeMessage, ConnectionEvent, @@ -89,7 +90,7 @@ fn find_claude_binary() -> Option { // Use a login shell to resolve claude via the user's PATH - GUI apps don't // inherit shell PATH, so bare `which` may miss ~/.local/bin entries - if let Ok(output) = Command::new("bash").args(["-lc", "which claude"]).output() { + if let Ok(output) = Command::new("bash").hide_window().args(["-lc", "which claude"]).output() { if output.status.success() { let path = String::from_utf8_lossy(&output.stdout).trim().to_string(); if !path.is_empty() { @@ -224,6 +225,7 @@ impl WslBridge { tracing::debug!("Working dir: {}", working_dir); let mut cmd = Command::new(&claude_path); + cmd.hide_window(); cmd.args([ "--output-format", "stream-json", @@ -291,6 +293,7 @@ impl WslBridge { // Check if Claude binary is installed inside WSL let binary_check = Command::new("wsl") + .hide_window() .args(["-e", "bash", "-lc", "which claude"]) .output(); if let Ok(output) = binary_check { @@ -301,6 +304,7 @@ impl WslBridge { // Validate the working directory exists inside WSL before spawning let dir_check = Command::new("wsl") + .hide_window() .args(["-e", "test", "-d", working_dir]) .output(); if let Ok(output) = dir_check { @@ -375,8 +379,7 @@ impl WslBridge { cmd.args(["-e", "bash", "-lc", &claude_cmd]); // Hide the console window on Windows - #[cfg(target_os = "windows")] - cmd.creation_flags(0x08000000); // CREATE_NO_WINDOW + cmd.hide_window(); cmd }; @@ -2059,7 +2062,7 @@ mod tests { #[test] fn test_stale_process_detection_with_try_wait() { // Spawn a real process that exits immediately so we can verify try_wait detects it - let mut child = Command::new("true").spawn().expect("Failed to spawn 'true'"); + let mut child = Command::new("true").hide_window().spawn().expect("Failed to spawn 'true'"); // Wait for it to exit let _ = child.wait(); @@ -2078,7 +2081,7 @@ mod tests { fn test_stale_process_is_some_after_exit() { // Verify the logic used in start(): a process that has exited is detected // and the handle is cleaned up so start() can proceed - let mut child = Command::new("true").spawn().expect("Failed to spawn 'true'"); + let mut child = Command::new("true").hide_window().spawn().expect("Failed to spawn 'true'"); // Let it exit let _ = child.wait(); diff --git a/src-tauri/src/wsl_notifications.rs b/src-tauri/src/wsl_notifications.rs index 2b71121..c928590 100644 --- a/src-tauri/src/wsl_notifications.rs +++ b/src-tauri/src/wsl_notifications.rs @@ -1,6 +1,8 @@ use std::process::Command; use tauri::command; +use crate::process_ext::HideWindow; + #[command] pub async fn send_wsl_notification(title: String, body: String) -> Result<(), String> { // Method 1: Try Windows 10/11 toast notification using PowerShell @@ -36,6 +38,7 @@ $notifier.Show($toast) // Try PowerShell.exe through WSL let output = Command::new("/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe") + .hide_window() .arg("-NoProfile") .arg("-ExecutionPolicy") .arg("Bypass") @@ -65,6 +68,7 @@ $notifier.Show($toast) // Method 3: Try wsl-notify-send if available let notify_result = Command::new("wsl-notify-send") + .hide_window() .arg("--appId") .arg("HikariDesktop") .arg("--category")