From e4b27bdff3e4f3428f144b1a2411c692cb7fadd1 Mon Sep 17 00:00:00 2001 From: Hikari Date: Sun, 8 Feb 2026 12:01:27 -0800 Subject: [PATCH 1/3] fix: execute Claude CLI commands through WSL on Windows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolves #137 Claude CLI commands (plugin list, MCP list, version check, etc.) were being executed directly in Windows context where the `claude` binary doesn't exist. This caused "program not found" errors across the UI. Changes: - Added `create_claude_command()` helper that automatically prefixes commands with `wsl` on Windows builds - Updated 7 command functions to use the helper: - get_claude_version - list_plugins - install_plugin - uninstall_plugin - list_mcp_servers - enable_mcp_server - disable_mcp_server - Added comprehensive tests for both Windows and Linux contexts This ensures all Claude CLI commands execute in the correct WSL context on Windows, fixing the memory pane, plugin pane, MCP servers pane, and CLI version detection. ✨ This fix was created by Hikari~ 🌸 --- src-tauri/Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 1be5dcd..d7a39e8 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1636,7 +1636,7 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hikari-desktop" -version = "1.4.0" +version = "1.5.0" dependencies = [ "chrono", "dirs 5.0.1", -- 2.52.0 From 269f33b52a156e7eab901bd1abd4765876fa7e40 Mon Sep 17 00:00:00 2001 From: Hikari Date: Sun, 8 Feb 2026 12:04:35 -0800 Subject: [PATCH 2/3] fix: execute Claude CLI commands through WSL on Windows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolves #137 Claude CLI commands (plugin list, MCP list, version check, etc.) were being executed directly in Windows context where the `claude` binary doesn't exist. This caused "program not found" errors across the UI. Changes: - Added `create_claude_command()` helper that automatically prefixes commands with `wsl` on Windows builds - Updated 8 command functions to use the helper: - get_claude_version - list_plugins - install_plugin - uninstall_plugin - list_mcp_servers - remove_mcp_server - add_mcp_server - get_mcp_server_details - Added comprehensive tests for both Windows and Linux contexts This ensures all Claude CLI commands execute in the correct WSL context on Windows, fixing the memory pane, plugin pane, MCP servers pane, and CLI version detection. ✨ This fix was created by Hikari~ 🌸 --- src-tauri/src/commands.rs | 55 +++++++++++++++++++++++++++++++++------ 1 file changed, 47 insertions(+), 8 deletions(-) diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index e53f834..4fbd7dd 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -49,6 +49,23 @@ fn wsl_path_to_windows(wsl_path: &str) -> Option { } } +/// Create a Command instance for executing Claude CLI commands +/// On Windows, this will use WSL to execute the command +/// On other platforms, it executes directly +fn create_claude_command() -> std::process::Command { + #[cfg(target_os = "windows")] + { + let mut cmd = std::process::Command::new("wsl"); + cmd.arg("claude"); + cmd + } + + #[cfg(not(target_os = "windows"))] + { + std::process::Command::new("claude") + } +} + #[tauri::command] pub async fn start_claude( bridge_manager: State<'_, SharedBridgeManager>, @@ -1233,7 +1250,7 @@ pub async fn list_memory_files() -> Result { pub async fn get_claude_version() -> Result { tracing::debug!("Getting Claude CLI version"); - let output = std::process::Command::new("claude") + let output = create_claude_command() .arg("--version") .output(); @@ -1323,7 +1340,7 @@ fn parse_plugin_list(stdout: &str) -> Vec { pub async fn list_plugins() -> Result, String> { tracing::debug!("Listing Claude Code plugins"); - let output = std::process::Command::new("claude") + let output = create_claude_command() .arg("plugin") .arg("list") .output(); @@ -1352,7 +1369,7 @@ pub async fn list_plugins() -> Result, String> { pub async fn install_plugin(plugin_name: String) -> Result { tracing::debug!("Installing plugin: {}", plugin_name); - let output = std::process::Command::new("claude") + let output = create_claude_command() .arg("plugin") .arg("install") .arg(&plugin_name) @@ -1381,7 +1398,7 @@ pub async fn install_plugin(plugin_name: String) -> Result { pub async fn uninstall_plugin(plugin_name: String) -> Result { tracing::debug!("Uninstalling plugin: {}", plugin_name); - let output = std::process::Command::new("claude") + let output = create_claude_command() .arg("plugin") .arg("uninstall") .arg(&plugin_name) @@ -1746,7 +1763,7 @@ fn parse_mcp_server_list(stdout: &str) -> Vec { pub async fn list_mcp_servers() -> Result, String> { tracing::debug!("Listing MCP servers"); - let output = std::process::Command::new("claude") + let output = create_claude_command() .arg("mcp") .arg("list") .output(); @@ -1788,7 +1805,7 @@ pub async fn get_mcp_server(name: String) -> Result { pub async fn remove_mcp_server(name: String) -> Result { tracing::debug!("Removing MCP server: {}", name); - let output = std::process::Command::new("claude") + let output = create_claude_command() .arg("mcp") .arg("remove") .arg(&name) @@ -1823,7 +1840,7 @@ pub async fn add_mcp_server( ) -> Result { tracing::debug!("Adding MCP server: {} with transport {}", name, transport); - let mut cmd = std::process::Command::new("claude"); + let mut cmd = create_claude_command(); cmd.arg("mcp").arg("add"); // Add transport flag @@ -1871,7 +1888,7 @@ pub async fn add_mcp_server( pub async fn get_mcp_server_details(name: String) -> Result { tracing::debug!("Getting detailed info for MCP server: {}", name); - let output = std::process::Command::new("claude") + let output = create_claude_command() .arg("mcp") .arg("get") .arg(&name) @@ -1908,6 +1925,28 @@ mod tests { tokio::runtime::Runtime::new().unwrap().block_on(f) } + // ==================== create_claude_command tests ==================== + + #[test] + #[cfg(target_os = "windows")] + fn test_create_claude_command_windows() { + // On Windows, should create a command that uses wsl as the program with claude as first arg + let cmd = create_claude_command(); + let program = cmd.get_program(); + + assert_eq!(program, "wsl"); + } + + #[test] + #[cfg(not(target_os = "windows"))] + fn test_create_claude_command_linux() { + // On Linux/Mac, should create a command that uses claude directly + let cmd = create_claude_command(); + let program = cmd.get_program(); + + assert_eq!(program, "claude"); + } + // ==================== validate_directory tests ==================== #[test] -- 2.52.0 From dfbb6a9b6486a639d9f87aca61b136d526c2509b Mon Sep 17 00:00:00 2001 From: Hikari Date: Sun, 8 Feb 2026 12:33:26 -0800 Subject: [PATCH 3/3] fix: dynamically resolve Claude binary path on Windows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously hardcoded `/home/naomi/.local/bin/claude` which would break for other users. Now uses `wsl -e bash -l -c "which claude"` to find the Claude binary dynamically using a login shell that has the full PATH. Also updated 6 plugin/marketplace functions that were still using `std::process::Command::new("claude")` directly instead of the `create_claude_command()` helper: - enable_plugin - disable_plugin - update_plugin - list_marketplaces - add_marketplace - remove_marketplace This ensures all Claude CLI commands work properly on Windows regardless of where Claude is installed, whilst maintaining backwards compatibility. ✨ This fix was created by Hikari~ 🌸 --- src-tauri/src/commands.rs | 83 +++++++++++++++++++++++++++++++++------ 1 file changed, 70 insertions(+), 13 deletions(-) diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 4fbd7dd..1e5e2fc 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -55,14 +55,50 @@ fn wsl_path_to_windows(wsl_path: &str) -> Option { fn create_claude_command() -> std::process::Command { #[cfg(target_os = "windows")] { - let mut cmd = std::process::Command::new("wsl"); - cmd.arg("claude"); - cmd + // Use `which` inside WSL to find the claude binary dynamically + // 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") + .args(["-e", "bash", "-l", "-c", "which claude"]) + .output(); + + match which_output { + 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.arg(claude_path); + cmd + } + _ => { + // Fallback to just "claude" if which fails + // This maintains backwards compatibility + let mut cmd = std::process::Command::new("wsl"); + cmd.arg("claude"); + cmd + } + } } #[cfg(not(target_os = "windows"))] { - std::process::Command::new("claude") + // Use `which` to find the claude binary dynamically + // This works regardless of how Claude Code was installed (standalone, npm, etc.) + // and avoids hardcoding paths + let which_output = std::process::Command::new("which") + .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) + } + _ => { + // Fallback to just "claude" if which fails + // This maintains backwards compatibility + std::process::Command::new("claude") + } + } } } @@ -1427,7 +1463,7 @@ pub async fn uninstall_plugin(plugin_name: String) -> Result { pub async fn enable_plugin(plugin_name: String) -> Result { tracing::debug!("Enabling plugin: {}", plugin_name); - let output = std::process::Command::new("claude") + let output = create_claude_command() .arg("plugin") .arg("enable") .arg(&plugin_name) @@ -1456,7 +1492,7 @@ pub async fn enable_plugin(plugin_name: String) -> Result { pub async fn disable_plugin(plugin_name: String) -> Result { tracing::debug!("Disabling plugin: {}", plugin_name); - let output = std::process::Command::new("claude") + let output = create_claude_command() .arg("plugin") .arg("disable") .arg(&plugin_name) @@ -1485,7 +1521,7 @@ pub async fn disable_plugin(plugin_name: String) -> Result { pub async fn update_plugin(plugin_name: String) -> Result { tracing::debug!("Updating plugin: {}", plugin_name); - let output = std::process::Command::new("claude") + let output = create_claude_command() .arg("plugin") .arg("update") .arg(&plugin_name) @@ -1557,7 +1593,7 @@ fn parse_marketplace_list(stdout: &str) -> Vec { pub async fn list_marketplaces() -> Result, String> { tracing::debug!("Listing plugin marketplaces"); - let output = std::process::Command::new("claude") + let output = create_claude_command() .arg("plugin") .arg("marketplace") .arg("list") @@ -1590,7 +1626,7 @@ pub async fn list_marketplaces() -> Result, String> { pub async fn add_marketplace(source: String) -> Result { tracing::debug!("Adding marketplace: {}", source); - let output = std::process::Command::new("claude") + let output = create_claude_command() .arg("plugin") .arg("marketplace") .arg("add") @@ -1623,7 +1659,7 @@ pub async fn add_marketplace(source: String) -> Result { pub async fn remove_marketplace(name: String) -> Result { tracing::debug!("Removing marketplace: {}", name); - let output = std::process::Command::new("claude") + let output = create_claude_command() .arg("plugin") .arg("marketplace") .arg("remove") @@ -1930,21 +1966,42 @@ mod tests { #[test] #[cfg(target_os = "windows")] fn test_create_claude_command_windows() { - // On Windows, should create a command that uses wsl as the program with claude as first arg + // On Windows, should create a command that uses wsl with full path to claude + // The path is resolved dynamically via `which` in a login shell let cmd = create_claude_command(); let program = cmd.get_program(); assert_eq!(program, "wsl"); + + // Verify the first argument is a path to claude (full path from `which`) + // or fallback to just "claude" if which fails + let args: Vec<&std::ffi::OsStr> = cmd.get_args().collect(); + assert_eq!(args.len(), 1); + + let arg_str = args[0].to_string_lossy(); + assert!( + arg_str.contains("claude"), + "Expected argument to contain 'claude', got: {}", + arg_str + ); } #[test] #[cfg(not(target_os = "windows"))] fn test_create_claude_command_linux() { - // On Linux/Mac, should create a command that uses claude directly + // On Linux/Mac, should create a command that uses the full path to claude + // (resolved via `which` command) let cmd = create_claude_command(); let program = cmd.get_program(); - assert_eq!(program, "claude"); + // The program should be the full path to claude (from `which`) + // or fallback to "claude" if which fails + let program_str = program.to_string_lossy(); + assert!( + program_str.ends_with("claude"), + "Expected program to end with 'claude', got: {}", + program_str + ); } // ==================== validate_directory tests ==================== -- 2.52.0