fix: suppress terminal window flash on Windows for all subprocesses (#165)

This commit is contained in:
2026-02-25 19:02:39 -08:00
committed by Naomi Carrigan
parent 5fb3f1a44b
commit dfc3d71c42
8 changed files with 77 additions and 8 deletions
+30 -2
View File
@@ -7,6 +7,7 @@ use tauri_plugin_store::StoreExt;
use crate::achievements::{get_achievement_info, load_achievements, AchievementUnlockedEvent}; use crate::achievements::{get_achievement_info, load_achievements, AchievementUnlockedEvent};
use crate::bridge_manager::SharedBridgeManager; use crate::bridge_manager::SharedBridgeManager;
use crate::config::{ClaudeStartOptions, HikariConfig}; use crate::config::{ClaudeStartOptions, HikariConfig};
use crate::process_ext::HideWindow;
use crate::stats::UsageStats; use crate::stats::UsageStats;
use crate::temp_manager::SharedTempFileManager; 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, // 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 // so we need to use a login shell to get the correct PATH
let which_output = std::process::Command::new("wsl") let which_output = std::process::Command::new("wsl")
.hide_window()
.args(["-e", "bash", "-l", "-c", "which claude"]) .args(["-e", "bash", "-l", "-c", "which claude"])
.output(); .output();
@@ -66,6 +68,7 @@ fn create_claude_command() -> std::process::Command {
Ok(output) if output.status.success() => { Ok(output) if output.status.success() => {
let claude_path = String::from_utf8_lossy(&output.stdout).trim().to_string(); let claude_path = String::from_utf8_lossy(&output.stdout).trim().to_string();
let mut cmd = std::process::Command::new("wsl"); let mut cmd = std::process::Command::new("wsl");
cmd.hide_window();
cmd.arg(claude_path); cmd.arg(claude_path);
cmd cmd
} }
@@ -73,6 +76,7 @@ fn create_claude_command() -> std::process::Command {
// Fallback to just "claude" if which fails // Fallback to just "claude" if which fails
// This maintains backwards compatibility // This maintains backwards compatibility
let mut cmd = std::process::Command::new("wsl"); let mut cmd = std::process::Command::new("wsl");
cmd.hide_window();
cmd.arg("claude"); cmd.arg("claude");
cmd cmd
} }
@@ -85,18 +89,23 @@ fn create_claude_command() -> std::process::Command {
// This works regardless of how Claude Code was installed (standalone, npm, etc.) // This works regardless of how Claude Code was installed (standalone, npm, etc.)
// and avoids hardcoding paths // and avoids hardcoding paths
let which_output = std::process::Command::new("which") let which_output = std::process::Command::new("which")
.hide_window()
.arg("claude") .arg("claude")
.output(); .output();
match which_output { match which_output {
Ok(output) if output.status.success() => { Ok(output) if output.status.success() => {
let claude_path = String::from_utf8_lossy(&output.stdout).trim().to_string(); 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 // Fallback to just "claude" if which fails
// This maintains backwards compatibility // 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<String
commands_dir commands_dir
); );
let Ok(output) = std::process::Command::new("wsl") let Ok(output) = std::process::Command::new("wsl")
.hide_window()
.args(["-e", "sh", "-c", &script]) .args(["-e", "sh", "-c", &script])
.output() .output()
else { else {
@@ -495,6 +505,7 @@ async fn list_skills_via_wsl() -> Result<Vec<String>, String> {
// Use WSL to list directories in ~/.claude/skills that contain SKILL.md // Use WSL to list directories in ~/.claude/skills that contain SKILL.md
let output = Command::new("wsl") let output = Command::new("wsl")
.hide_window()
.args([ .args([
"-e", "-e",
"sh", "sh",
@@ -794,6 +805,7 @@ async fn list_directory_via_wsl(path: &str) -> Result<Vec<FileEntry>, String> {
); );
let output = Command::new("wsl") let output = Command::new("wsl")
.hide_window()
.args(["-e", "sh", "-c", &script]) .args(["-e", "sh", "-c", &script])
.output() .output()
.map_err(|e| format!("Failed to execute WSL command: {}", e))?; .map_err(|e| format!("Failed to execute WSL command: {}", e))?;
@@ -856,6 +868,7 @@ async fn read_file_via_wsl(path: &str) -> Result<String, String> {
use std::process::Command; use std::process::Command;
let output = Command::new("wsl") let output = Command::new("wsl")
.hide_window()
.args(["-e", "cat", path]) .args(["-e", "cat", path])
.output() .output()
.map_err(|e| format!("Failed to execute WSL command: {}", e))?; .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}; use std::process::{Command, Stdio};
let mut child = Command::new("wsl") let mut child = Command::new("wsl")
.hide_window()
.args(["-e", "sh", "-c", &format!("cat > '{}'", path)]) .args(["-e", "sh", "-c", &format!("cat > '{}'", path)])
.stdin(Stdio::piped()) .stdin(Stdio::piped())
.spawn() .spawn()
@@ -935,6 +949,7 @@ async fn create_file_via_wsl(path: &str) -> Result<(), String> {
// Check if file exists first // Check if file exists first
let check = Command::new("wsl") let check = Command::new("wsl")
.hide_window()
.args(["-e", "test", "-e", path]) .args(["-e", "test", "-e", path])
.status() .status()
.map_err(|e| format!("Failed to execute WSL command: {}", e))?; .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") let output = Command::new("wsl")
.hide_window()
.args(["-e", "touch", path]) .args(["-e", "touch", path])
.output() .output()
.map_err(|e| format!("Failed to execute WSL command: {}", e))?; .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 // Check if directory exists first
let check = Command::new("wsl") let check = Command::new("wsl")
.hide_window()
.args(["-e", "test", "-e", path]) .args(["-e", "test", "-e", path])
.status() .status()
.map_err(|e| format!("Failed to execute WSL command: {}", e))?; .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") let output = Command::new("wsl")
.hide_window()
.args(["-e", "mkdir", "-p", path]) .args(["-e", "mkdir", "-p", path])
.output() .output()
.map_err(|e| format!("Failed to execute WSL command: {}", e))?; .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 // Check if path exists
let check_exists = Command::new("wsl") let check_exists = Command::new("wsl")
.hide_window()
.args(["-e", "test", "-e", path]) .args(["-e", "test", "-e", path])
.status() .status()
.map_err(|e| format!("Failed to execute WSL command: {}", e))?; .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 // Check if path is a directory
let check_dir = Command::new("wsl") let check_dir = Command::new("wsl")
.hide_window()
.args(["-e", "test", "-d", path]) .args(["-e", "test", "-d", path])
.status() .status()
.map_err(|e| format!("Failed to execute WSL command: {}", e))?; .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") let output = Command::new("wsl")
.hide_window()
.args(["-e", "rm", path]) .args(["-e", "rm", path])
.output() .output()
.map_err(|e| format!("Failed to execute WSL command: {}", e))?; .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 // Check if path exists
let check_exists = Command::new("wsl") let check_exists = Command::new("wsl")
.hide_window()
.args(["-e", "test", "-e", path]) .args(["-e", "test", "-e", path])
.status() .status()
.map_err(|e| format!("Failed to execute WSL command: {}", e))?; .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 // Check if path is a directory
let check_dir = Command::new("wsl") let check_dir = Command::new("wsl")
.hide_window()
.args(["-e", "test", "-d", path]) .args(["-e", "test", "-d", path])
.status() .status()
.map_err(|e| format!("Failed to execute WSL command: {}", e))?; .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") let output = Command::new("wsl")
.hide_window()
.args(["-e", "rm", "-rf", path]) .args(["-e", "rm", "-rf", path])
.output() .output()
.map_err(|e| format!("Failed to execute WSL command: {}", e))?; .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 // Check if old path exists
let check_old = Command::new("wsl") let check_old = Command::new("wsl")
.hide_window()
.args(["-e", "test", "-e", old_path]) .args(["-e", "test", "-e", old_path])
.status() .status()
.map_err(|e| format!("Failed to execute WSL command: {}", e))?; .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 // Check if new path already exists
let check_new = Command::new("wsl") let check_new = Command::new("wsl")
.hide_window()
.args(["-e", "test", "-e", new_path]) .args(["-e", "test", "-e", new_path])
.status() .status()
.map_err(|e| format!("Failed to execute WSL command: {}", e))?; .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") let output = Command::new("wsl")
.hide_window()
.args(["-e", "mv", old_path, new_path]) .args(["-e", "mv", old_path, new_path])
.output() .output()
.map_err(|e| format!("Failed to execute WSL command: {}", e))?; .map_err(|e| format!("Failed to execute WSL command: {}", e))?;
@@ -1360,6 +1387,7 @@ async fn list_memory_files_via_wsl() -> Result<MemoryFilesResponse, String> {
"#; "#;
let output = Command::new("wsl") let output = Command::new("wsl")
.hide_window()
.args(["-e", "bash", "-l", "-c", script]) .args(["-e", "bash", "-l", "-c", script])
.output() .output()
.map_err(|e| format!("Failed to execute WSL command: {}", e))?; .map_err(|e| format!("Failed to execute WSL command: {}", e))?;
+3
View File
@@ -1,6 +1,8 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::process::Command; use std::process::Command;
use crate::process_ext::HideWindow;
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GitStatus { pub struct GitStatus {
pub is_repo: bool, pub is_repo: bool,
@@ -37,6 +39,7 @@ pub struct GitLogEntry {
fn run_git_command(working_dir: &str, args: &[&str]) -> Result<String, String> { fn run_git_command(working_dir: &str, args: &[&str]) -> Result<String, String> {
let output = Command::new("git") let output = Command::new("git")
.hide_window()
.args(args) .args(args)
.current_dir(working_dir) .current_dir(working_dir)
.output() .output()
+1
View File
@@ -8,6 +8,7 @@ mod debug_logger;
mod discord_rpc; mod discord_rpc;
mod git; mod git;
mod notifications; mod notifications;
mod process_ext;
mod quick_actions; mod quick_actions;
mod sessions; mod sessions;
mod snippets; mod snippets;
+6
View File
@@ -1,6 +1,8 @@
use std::process::Command; use std::process::Command;
use tauri::command; use tauri::command;
use crate::process_ext::HideWindow;
/// Generate PowerShell script for Windows Toast Notification /// Generate PowerShell script for Windows Toast Notification
fn generate_powershell_toast_script(title: &str, body: &str) -> String { fn generate_powershell_toast_script(title: &str, body: &str) -> String {
format!( format!(
@@ -82,6 +84,7 @@ fn build_simple_notification_command(title: &str, body: &str) -> (String, Vec<St
pub async fn send_notify_send(title: String, body: String) -> Result<(), String> { pub async fn send_notify_send(title: String, body: String) -> Result<(), String> {
// Use notify-send for Linux/WSL // Use notify-send for Linux/WSL
let output = Command::new("notify-send") let output = Command::new("notify-send")
.hide_window()
.arg(&title) .arg(&title)
.arg(&body) .arg(&body)
.arg("--urgency=normal") .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 // Try PowerShell Core first (pwsh), then fall back to Windows PowerShell
let output = Command::new("pwsh.exe") let output = Command::new("pwsh.exe")
.hide_window()
.arg("-NoProfile") .arg("-NoProfile")
.arg("-WindowStyle") .arg("-WindowStyle")
.arg("Hidden") .arg("Hidden")
@@ -117,6 +121,7 @@ pub async fn send_windows_notification(title: String, body: String) -> Result<()
.output() .output()
.or_else(|_| { .or_else(|_| {
Command::new("powershell.exe") Command::new("powershell.exe")
.hide_window()
.arg("-NoProfile") .arg("-NoProfile")
.arg("-WindowStyle") .arg("-WindowStyle")
.arg("Hidden") .arg("Hidden")
@@ -140,6 +145,7 @@ pub async fn send_simple_notification(title: String, body: String) -> Result<(),
let message = format_simple_notification(&title, &body); let message = format_simple_notification(&title, &body);
Command::new("cmd.exe") Command::new("cmd.exe")
.hide_window()
.arg("/c") .arg("/c")
.arg("msg") .arg("msg")
.arg("*") .arg("*")
+21
View File
@@ -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
}
}
+4 -1
View File
@@ -3,6 +3,8 @@ use std::process::Command;
use tauri::command; use tauri::command;
use tempfile::NamedTempFile; use tempfile::NamedTempFile;
use crate::process_ext::HideWindow;
#[command] #[command]
pub async fn send_vbs_notification(title: String, body: String) -> Result<(), String> { pub async fn send_vbs_notification(title: String, body: String) -> Result<(), String> {
// Create a VBScript that shows a Windows notification // 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/") { } else if temp_path.starts_with("/tmp/") {
// WSL temp files might be in a different location // WSL temp files might be in a different location
// Try to use wslpath to convert // 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 let Ok(result) = output {
if result.status.success() { if result.status.success() {
@@ -57,6 +59,7 @@ objShell.Popup "{}" & vbCrLf & vbCrLf & "{}", 5, "{}", 64
// Execute the VBScript using wscript.exe // Execute the VBScript using wscript.exe
let output = Command::new("/mnt/c/Windows/System32/wscript.exe") let output = Command::new("/mnt/c/Windows/System32/wscript.exe")
.hide_window()
.arg("//NoLogo") .arg("//NoLogo")
.arg(&windows_path) .arg(&windows_path)
.output() .output()
+8 -5
View File
@@ -12,6 +12,7 @@ use std::os::windows::process::CommandExt;
use crate::achievements::{get_achievement_info, AchievementUnlockedEvent}; use crate::achievements::{get_achievement_info, AchievementUnlockedEvent};
use crate::commands::record_cost; use crate::commands::record_cost;
use crate::config::ClaudeStartOptions; use crate::config::ClaudeStartOptions;
use crate::process_ext::HideWindow;
use crate::stats::{calculate_cost, StatsUpdateEvent, UsageStats}; use crate::stats::{calculate_cost, StatsUpdateEvent, UsageStats};
use crate::types::{ use crate::types::{
AgentEndEvent, AgentStartEvent, CharacterState, ClaudeMessage, ConnectionEvent, AgentEndEvent, AgentStartEvent, CharacterState, ClaudeMessage, ConnectionEvent,
@@ -89,7 +90,7 @@ fn find_claude_binary() -> Option<String> {
// Use a login shell to resolve claude via the user's PATH - GUI apps don't // 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 // 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() { if output.status.success() {
let path = String::from_utf8_lossy(&output.stdout).trim().to_string(); let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
if !path.is_empty() { if !path.is_empty() {
@@ -224,6 +225,7 @@ impl WslBridge {
tracing::debug!("Working dir: {}", working_dir); tracing::debug!("Working dir: {}", working_dir);
let mut cmd = Command::new(&claude_path); let mut cmd = Command::new(&claude_path);
cmd.hide_window();
cmd.args([ cmd.args([
"--output-format", "--output-format",
"stream-json", "stream-json",
@@ -291,6 +293,7 @@ impl WslBridge {
// Check if Claude binary is installed inside WSL // Check if Claude binary is installed inside WSL
let binary_check = Command::new("wsl") let binary_check = Command::new("wsl")
.hide_window()
.args(["-e", "bash", "-lc", "which claude"]) .args(["-e", "bash", "-lc", "which claude"])
.output(); .output();
if let Ok(output) = binary_check { if let Ok(output) = binary_check {
@@ -301,6 +304,7 @@ impl WslBridge {
// Validate the working directory exists inside WSL before spawning // Validate the working directory exists inside WSL before spawning
let dir_check = Command::new("wsl") let dir_check = Command::new("wsl")
.hide_window()
.args(["-e", "test", "-d", working_dir]) .args(["-e", "test", "-d", working_dir])
.output(); .output();
if let Ok(output) = dir_check { if let Ok(output) = dir_check {
@@ -375,8 +379,7 @@ impl WslBridge {
cmd.args(["-e", "bash", "-lc", &claude_cmd]); cmd.args(["-e", "bash", "-lc", &claude_cmd]);
// Hide the console window on Windows // Hide the console window on Windows
#[cfg(target_os = "windows")] cmd.hide_window();
cmd.creation_flags(0x08000000); // CREATE_NO_WINDOW
cmd cmd
}; };
@@ -2059,7 +2062,7 @@ mod tests {
#[test] #[test]
fn test_stale_process_detection_with_try_wait() { fn test_stale_process_detection_with_try_wait() {
// Spawn a real process that exits immediately so we can verify try_wait detects it // 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 // Wait for it to exit
let _ = child.wait(); let _ = child.wait();
@@ -2078,7 +2081,7 @@ mod tests {
fn test_stale_process_is_some_after_exit() { fn test_stale_process_is_some_after_exit() {
// Verify the logic used in start(): a process that has exited is detected // Verify the logic used in start(): a process that has exited is detected
// and the handle is cleaned up so start() can proceed // 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 it exit
let _ = child.wait(); let _ = child.wait();
+4
View File
@@ -1,6 +1,8 @@
use std::process::Command; use std::process::Command;
use tauri::command; use tauri::command;
use crate::process_ext::HideWindow;
#[command] #[command]
pub async fn send_wsl_notification(title: String, body: String) -> Result<(), String> { pub async fn send_wsl_notification(title: String, body: String) -> Result<(), String> {
// Method 1: Try Windows 10/11 toast notification using PowerShell // Method 1: Try Windows 10/11 toast notification using PowerShell
@@ -36,6 +38,7 @@ $notifier.Show($toast)
// Try PowerShell.exe through WSL // Try PowerShell.exe through WSL
let output = Command::new("/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe") let output = Command::new("/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe")
.hide_window()
.arg("-NoProfile") .arg("-NoProfile")
.arg("-ExecutionPolicy") .arg("-ExecutionPolicy")
.arg("Bypass") .arg("Bypass")
@@ -65,6 +68,7 @@ $notifier.Show($toast)
// Method 3: Try wsl-notify-send if available // Method 3: Try wsl-notify-send if available
let notify_result = Command::new("wsl-notify-send") let notify_result = Command::new("wsl-notify-send")
.hide_window()
.arg("--appId") .arg("--appId")
.arg("HikariDesktop") .arg("HikariDesktop")
.arg("--category") .arg("--category")