fix: expand registry PATH vars and fix debug window sync

- Expand %USERPROFILE% etc. via ExpandEnvironmentStringsW when reading
  registry PATH entries
- Use is_file() instead of exists() for binary detection
- Fix debug window showing blank: use emitTo() for cross-window events
  instead of emit() which only targets the current window
- Update step numbering and doc comments
- Improve error message to avoid double-wrapping

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
GunwoongP 2026-03-30 17:43:38 +09:00
parent e37cf55367
commit dd6ca0cb54
3 changed files with 67 additions and 13 deletions

View file

@ -54,6 +54,7 @@ objc2-foundation = { version = "0.3", features = ["NSData"] }
[target.'cfg(target_os = "windows")'.dependencies] [target.'cfg(target_os = "windows")'.dependencies]
winreg = "0.55" winreg = "0.55"
windows-sys = { version = "0.52", features = ["Win32_System_Environment"] }
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-updater = "2" tauri-plugin-updater = "2"

View file

@ -59,6 +59,8 @@ impl Default for ClaudeProcessState {
/// On Windows, read User + System PATH from the registry and search for claude. /// On Windows, read User + System PATH from the registry and search for claude.
/// This catches cases where claude was installed after the GUI app launched, /// This catches cases where claude was installed after the GUI app launched,
/// since the process PATH is stale but the registry PATH is up to date. /// since the process PATH is stale but the registry PATH is up to date.
/// Registry values may contain unexpanded variables like `%USERPROFILE%`,
/// so we expand them via `ExpandEnvironmentStringsW` before searching.
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
fn find_claude_in_registry_path() -> Option<String> { fn find_claude_in_registry_path() -> Option<String> {
use winreg::enums::{HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE}; use winreg::enums::{HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE};
@ -69,7 +71,12 @@ fn find_claude_in_registry_path() -> Option<String> {
// User PATH // User PATH
if let Ok(env) = RegKey::predef(HKEY_CURRENT_USER).open_subkey("Environment") { if let Ok(env) = RegKey::predef(HKEY_CURRENT_USER).open_subkey("Environment") {
if let Ok(user_path) = env.get_value::<String, _>("Path") { if let Ok(user_path) = env.get_value::<String, _>("Path") {
dirs.extend(user_path.split(';').filter(|s| !s.is_empty()).map(String::from)); dirs.extend(
user_path
.split(';')
.filter(|s| !s.is_empty())
.map(|s| expand_env_vars(s)),
);
} }
} }
@ -78,7 +85,12 @@ fn find_claude_in_registry_path() -> Option<String> {
.open_subkey(r"SYSTEM\CurrentControlSet\Control\Session Manager\Environment") .open_subkey(r"SYSTEM\CurrentControlSet\Control\Session Manager\Environment")
{ {
if let Ok(sys_path) = env.get_value::<String, _>("Path") { if let Ok(sys_path) = env.get_value::<String, _>("Path") {
dirs.extend(sys_path.split(';').filter(|s| !s.is_empty()).map(String::from)); dirs.extend(
sys_path
.split(';')
.filter(|s| !s.is_empty())
.map(|s| expand_env_vars(s)),
);
} }
} }
@ -86,7 +98,7 @@ fn find_claude_in_registry_path() -> Option<String> {
for dir in &dirs { for dir in &dirs {
for name in &candidates { for name in &candidates {
let p = PathBuf::from(dir).join(name); let p = PathBuf::from(dir).join(name);
if p.exists() { if p.is_file() {
return Some(p.to_string_lossy().to_string()); return Some(p.to_string_lossy().to_string());
} }
} }
@ -95,8 +107,49 @@ fn find_claude_in_registry_path() -> Option<String> {
None None
} }
/// Expand Windows environment variables like `%USERPROFILE%` in a string.
#[cfg(target_os = "windows")]
fn expand_env_vars(s: &str) -> String {
use std::ffi::OsString;
use std::os::windows::ffi::{OsStrExt, OsStringExt};
let wide: Vec<u16> = OsString::from(s).encode_wide().chain(std::iter::once(0)).collect();
// First call to get required buffer size
let size = unsafe {
windows_sys::Win32::System::Environment::ExpandEnvironmentStringsW(
wide.as_ptr(),
std::ptr::null_mut(),
0,
)
};
if size == 0 {
return s.to_string();
}
let mut buf: Vec<u16> = vec![0u16; size as usize];
let result = unsafe {
windows_sys::Win32::System::Environment::ExpandEnvironmentStringsW(
wide.as_ptr(),
buf.as_mut_ptr(),
size,
)
};
if result == 0 {
return s.to_string();
}
// Trim trailing null
if let Some(pos) = buf.iter().position(|&c| c == 0) {
buf.truncate(pos);
}
OsString::from_wide(&buf).to_string_lossy().to_string()
}
/// Discover the claude binary on the system. /// Discover the claude binary on the system.
/// Checks: ~/.local/bin (native install) → which → NVM paths → standard paths → bare fallback. /// Search order: ~/.local/bin → NVM_BIN → which → registry PATH (Windows) →
/// login shell (Unix) → npm/nvm global → standard paths → user-specific paths.
/// Returns Err if not found.
fn find_claude_binary() -> Result<String, String> { fn find_claude_binary() -> Result<String, String> {
// 1. Check the native installer's default location first // 1. Check the native installer's default location first
// (GUI apps often don't have ~/.local/bin in PATH) // (GUI apps often don't have ~/.local/bin in PATH)
@ -155,7 +208,7 @@ fn find_claude_binary() -> Result<String, String> {
} }
} }
// 6. Check NVM directories (Unix) or npm global (Windows) // 6. Check NVM directories (Unix) or npm/nvm global (Windows)
#[allow(unused_variables)] #[allow(unused_variables)]
if let Some(home) = dirs::home_dir() { if let Some(home) = dirs::home_dir() {
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
@ -213,7 +266,7 @@ fn find_claude_binary() -> Result<String, String> {
} }
} }
// 6. Check standard paths (Unix only) // 7. Check standard paths (Unix only)
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
{ {
let standard_paths = [ let standard_paths = [
@ -229,7 +282,7 @@ fn find_claude_binary() -> Result<String, String> {
} }
} }
// 7. Check user-specific paths // 8. Check user-specific paths
if let Some(home) = dirs::home_dir() { if let Some(home) = dirs::home_dir() {
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
let user_paths = vec![ let user_paths = vec![
@ -273,7 +326,7 @@ fn find_claude_binary() -> Result<String, String> {
} }
} }
Err("Claude CLI not found. Install it from https://claude.ai".to_string()) Err("Not found in any known location. Install from https://claude.ai".to_string())
} }
/// Strip ANSI escape sequences from CLI output before sending to the frontend. /// Strip ANSI escape sequences from CLI output before sending to the frontend.

View file

@ -1,6 +1,6 @@
import { create } from "zustand"; import { create } from "zustand";
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
import { emit, listen } from "@tauri-apps/api/event"; import { emitTo, listen } from "@tauri-apps/api/event";
export type LogLevel = "debug" | "info" | "warn" | "error"; export type LogLevel = "debug" | "info" | "warn" | "error";
@ -93,7 +93,7 @@ export const useLogStore = create<LogStore>((set) => ({
// Only emit to debug window if one is connected // Only emit to debug window if one is connected
if (_debugWindowConnected && !_isDebugWindow) { if (_debugWindowConnected && !_isDebugWindow) {
emit("debug-log-entry", entry).catch(() => {}); emitTo("debug", "debug-log-entry", entry).catch(() => {});
} }
// Forward warn/error to Rust stderr via existing js_log command // Forward warn/error to Rust stderr via existing js_log command
@ -143,8 +143,8 @@ if (_isDebugWindow) {
useLogStore.setState({ version: ++_version }); useLogStore.setState({ version: ++_version });
}); });
// Request bulk sync on open // Request bulk sync on open — send to main window
emit("debug-log-sync-request").catch(() => {}); emitTo("main", "debug-log-sync-request").catch(() => {});
} }
// Main window responds to sync requests by sending the full buffer. // Main window responds to sync requests by sending the full buffer.
@ -152,7 +152,7 @@ if (!_isDebugWindow) {
listen("debug-log-sync-request", () => { listen("debug-log-sync-request", () => {
_debugWindowConnected = true; _debugWindowConnected = true;
if (_buffer.length > 0) { if (_buffer.length > 0) {
emit("debug-log-sync", _buffer).catch(() => {}); emitTo("debug", "debug-log-sync", _buffer).catch(() => {});
} }
}); });
} }