Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions scripts/fetch-and-build.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
*/

import { spawn } from 'child_process';
import { mkdir, rm, readdir, copyFile, access } from 'fs/promises';
import { mkdir, rm, readdir, copyFile, access, cp } from 'fs/promises';
import { existsSync } from 'fs';
import { join, resolve } from 'path';

Expand Down Expand Up @@ -207,8 +207,8 @@ async function copyRequiredFiles(packageDir) {
await rm(destPath, { recursive: true, force: true });
}

// Copy directory recursively using cp command
await runCommand('cp', ['-r', srcPath, destPath]);
// Copy directory recursively using Node.js built-in function
await cp(srcPath, destPath, { recursive: true });
} else {
console.warn(`Warning: ${dir}/ directory not found in package`);
}
Expand Down
74 changes: 61 additions & 13 deletions src-tauri/src/claude_binary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ use std::path::PathBuf;
use std::process::Command;
use tauri::Manager;

#[cfg(target_os = "windows")]
use std::os::windows::process::CommandExt;

#[cfg(target_os = "windows")]
const CREATE_NO_WINDOW: u32 = 0x08000000;

/// Type of Claude installation
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum InstallationType {
Expand Down Expand Up @@ -226,7 +232,14 @@ fn discover_system_installations() -> Vec<ClaudeInstallation> {
fn try_which_command() -> Option<ClaudeInstallation> {
debug!("Trying 'which claude' to find binary...");

match Command::new("which").arg("claude").output() {
let mut cmd = Command::new("which");
cmd.arg("claude");
#[cfg(target_os = "windows")]
{
cmd.creation_flags(CREATE_NO_WINDOW);
}

match cmd.output() {
Ok(output) if output.status.success() => {
let output_str = String::from_utf8_lossy(&output.stdout).trim().to_string();

Expand Down Expand Up @@ -370,18 +383,46 @@ fn find_standard_installations() -> Vec<ClaudeInstallation> {
}
}

// Also check if claude is available in PATH (without full path)
if let Ok(output) = Command::new("claude").arg("--version").output() {
// Use platform-specific commands to find claude in PATH with full path resolution
let path_finder_cmd = if cfg!(target_os = "windows") { "where" } else { "which" };

let mut cmd = Command::new(path_finder_cmd);
cmd.arg("claude");
#[cfg(target_os = "windows")]
{
cmd.creation_flags(CREATE_NO_WINDOW);
}

if let Ok(output) = cmd.output() {
if output.status.success() {
debug!("claude is available in PATH");
let version = extract_version_from_output(&output.stdout);

installations.push(ClaudeInstallation {
path: "claude".to_string(),
version,
source: "PATH".to_string(),
installation_type: InstallationType::System,
});
let stdout = String::from_utf8_lossy(&output.stdout);
// 'where' on Windows can return multiple paths, one per line
// 'which' on Unix typically returns a single path
for line in stdout.lines() {
let path = line.trim();
if !path.is_empty() {
debug!("Found claude at: {}", path);
// Use the full path to get version
let mut version_cmd = Command::new(path);
version_cmd.arg("--version");
#[cfg(target_os = "windows")]
{
version_cmd.creation_flags(CREATE_NO_WINDOW);
}

if let Ok(version_output) = version_cmd.output() {
if version_output.status.success() {
let version = extract_version_from_output(&version_output.stdout);
installations.push(ClaudeInstallation {
path: path.to_string(),
version,
source: "PATH".to_string(),
installation_type: InstallationType::System,
});
}
}
}
}
}
}

Expand All @@ -390,7 +431,14 @@ fn find_standard_installations() -> Vec<ClaudeInstallation> {

/// Get Claude version by running --version command
fn get_claude_version(path: &str) -> Result<Option<String>, String> {
match Command::new(path).arg("--version").output() {
let mut cmd = Command::new(path);
cmd.arg("--version");
#[cfg(target_os = "windows")]
{
cmd.creation_flags(CREATE_NO_WINDOW);
}

match cmd.output() {
Ok(output) => {
if output.status.success() {
Ok(extract_version_from_output(&output.stdout))
Expand Down
56 changes: 41 additions & 15 deletions src-tauri/src/commands/agents.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ use tauri_plugin_shell::ShellExt;
use tokio::io::{AsyncBufReadExt, BufReader as TokioBufReader};
use tokio::process::Command;

#[cfg(target_os = "windows")]
use std::os::windows::process::CommandExt;

#[cfg(target_os = "windows")]
const CREATE_NO_WINDOW: u32 = 0x08000000;

/// Finds the full path to the claude binary
/// This is necessary because macOS apps have a limited PATH environment
fn find_claude_binary(app_handle: &AppHandle) -> Result<String, String> {
Expand Down Expand Up @@ -1284,21 +1290,27 @@ async fn spawn_agent_system(
"🔍 Process likely stuck waiting for input, attempting to kill PID: {}",
pid
);
let kill_result = std::process::Command::new("kill")
.arg("-TERM")
.arg(pid.to_string())
.output();
let mut kill_cmd = std::process::Command::new("kill");
kill_cmd.arg("-TERM").arg(pid.to_string());
#[cfg(target_os = "windows")]
{
kill_cmd.creation_flags(CREATE_NO_WINDOW);
}
let kill_result = kill_cmd.output();

match kill_result {
Ok(output) if output.status.success() => {
warn!("🔍 Successfully sent TERM signal to process");
}
Ok(_) => {
warn!("🔍 Failed to kill process with TERM, trying KILL");
let _ = std::process::Command::new("kill")
.arg("-KILL")
.arg(pid.to_string())
.output();
let mut kill_kill_cmd = std::process::Command::new("kill");
kill_kill_cmd.arg("-KILL").arg(pid.to_string());
#[cfg(target_os = "windows")]
{
kill_kill_cmd.creation_flags(CREATE_NO_WINDOW);
}
let _ = kill_kill_cmd.output();
}
Err(e) => {
warn!("🔍 Error killing process: {}", e);
Expand Down Expand Up @@ -1537,10 +1549,14 @@ pub async fn cleanup_finished_processes(db: State<'_, AgentDb>) -> Result<Vec<i6
// Check if the process is still running
let is_running = if cfg!(target_os = "windows") {
// On Windows, use tasklist to check if process exists
match std::process::Command::new("tasklist")
.args(["/FI", &format!("PID eq {}", pid)])
.args(["/FO", "CSV"])
.output()
let mut cmd = std::process::Command::new("tasklist");
cmd.args(["/FI", &format!("PID eq {}", pid)])
.args(["/FO", "CSV"]);
#[cfg(target_os = "windows")]
{
cmd.creation_flags(CREATE_NO_WINDOW);
}
match cmd.output()
{
Ok(output) => {
let output_str = String::from_utf8_lossy(&output.stdout);
Expand All @@ -1550,9 +1566,13 @@ pub async fn cleanup_finished_processes(db: State<'_, AgentDb>) -> Result<Vec<i6
}
} else {
// On Unix-like systems, use kill -0 to check if process exists
match std::process::Command::new("kill")
.args(["-0", &pid.to_string()])
.output()
let mut check_cmd = std::process::Command::new("kill");
check_cmd.args(["-0", &pid.to_string()]);
#[cfg(target_os = "windows")]
{
check_cmd.creation_flags(CREATE_NO_WINDOW);
}
match check_cmd.output()
{
Ok(output) => output.status.success(),
Err(_) => false,
Expand Down Expand Up @@ -1985,6 +2005,12 @@ fn create_command_with_env(program: &str) -> Command {
tokio_cmd.env(&key, &value);
}
}

// Hide console window on Windows
#[cfg(target_os = "windows")]
{
tokio_cmd.creation_flags(CREATE_NO_WINDOW);
}

// Add NVM support if the program is in an NVM directory
if program.contains("/.nvm/versions/node/") {
Expand Down
51 changes: 45 additions & 6 deletions src-tauri/src/commands/claude.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ use tauri_plugin_shell::ShellExt;
use tauri_plugin_shell::process::CommandEvent;
use regex;

#[cfg(target_os = "windows")]
use std::os::windows::process::CommandExt;

#[cfg(target_os = "windows")]
const CREATE_NO_WINDOW: u32 = 0x08000000;

/// Global state to track current Claude process
pub struct ClaudeProcessState {
pub current_process: Arc<Mutex<Option<Child>>>,
Expand Down Expand Up @@ -250,6 +256,12 @@ fn create_command_with_env(program: &str) -> Command {
tokio_cmd.env(&key, &value);
}
}

// Hide console window on Windows
#[cfg(target_os = "windows")]
{
tokio_cmd.creation_flags(CREATE_NO_WINDOW);
}

// Add NVM support if the program is in an NVM directory
if program.contains("/.nvm/versions/node/") {
Expand Down Expand Up @@ -308,6 +320,12 @@ fn create_system_command(
.stdout(Stdio::piped())
.stderr(Stdio::piped());

// Hide the console window on Windows
#[cfg(target_os = "windows")]
{
cmd.creation_flags(CREATE_NO_WINDOW);
}

cmd
}

Expand Down Expand Up @@ -532,6 +550,12 @@ pub async fn open_new_session(app: AppHandle, path: Option<String>) -> Result<St
cmd.current_dir(&project_path);
}

// Hide console window on Windows
#[cfg(target_os = "windows")]
{
cmd.creation_flags(CREATE_NO_WINDOW);
}

// Execute the command
match cmd.spawn() {
Ok(_) => {
Expand Down Expand Up @@ -687,9 +711,14 @@ pub async fn check_claude_version(app: AppHandle) -> Result<ClaudeVersionStatus,

#[cfg(debug_assertions)]
{
let output = std::process::Command::new(claude_path)
.arg("--version")
.output();
let mut cmd = std::process::Command::new(claude_path);
cmd.arg("--version");
#[cfg(target_os = "windows")]
{
cmd.creation_flags(CREATE_NO_WINDOW);
}

let output = cmd.output();

match output {
Ok(output) => {
Expand Down Expand Up @@ -1102,9 +1131,13 @@ pub async fn cancel_claude_execution(
if let Some(pid) = pid {
log::info!("Attempting system kill as last resort for PID: {}", pid);
let kill_result = if cfg!(target_os = "windows") {
std::process::Command::new("taskkill")
.args(["/F", "/PID", &pid.to_string()])
.output()
let mut cmd = std::process::Command::new("taskkill");
cmd.args(["/F", "/PID", &pid.to_string()]);
#[cfg(target_os = "windows")]
{
cmd.creation_flags(CREATE_NO_WINDOW);
}
cmd.output()
} else {
std::process::Command::new("kill")
.args(["-KILL", &pid.to_string()])
Expand Down Expand Up @@ -2303,6 +2336,12 @@ pub async fn validate_hook_command(command: String) -> Result<serde_json::Value,
.arg("-c")
.arg(&command);

// Hide console window on Windows
#[cfg(target_os = "windows")]
{
cmd.creation_flags(CREATE_NO_WINDOW);
}

match cmd.output() {
Ok(output) => {
if output.status.success() {
Expand Down
16 changes: 13 additions & 3 deletions src-tauri/src/process/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use tokio::process::Child;

#[cfg(target_os = "windows")]
use std::os::windows::process::CommandExt;

#[cfg(target_os = "windows")]
const CREATE_NO_WINDOW: u32 = 0x08000000;

/// Type of process being tracked
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ProcessType {
Expand Down Expand Up @@ -325,9 +331,13 @@ impl ProcessRegistry {
info!("Attempting to kill process {} by PID {}", run_id, pid);

let kill_result = if cfg!(target_os = "windows") {
std::process::Command::new("taskkill")
.args(["/F", "/PID", &pid.to_string()])
.output()
let mut cmd = std::process::Command::new("taskkill");
cmd.args(["/F", "/PID", &pid.to_string()]);
#[cfg(target_os = "windows")]
{
cmd.creation_flags(CREATE_NO_WINDOW);
}
cmd.output()
} else {
// First try SIGTERM
let term_result = std::process::Command::new("kill")
Expand Down
1 change: 1 addition & 0 deletions src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
"icons/128x128.png",
"icons/[email protected]",
"icons/icon.icns",
"icons/icon.ico",
"icons/icon.png"
],
"externalBin": [
Expand Down