From 479652b69ef5fbf7ea9da03ed68b9c053de4c884 Mon Sep 17 00:00:00 2001 From: Naomi Carrigan Date: Thu, 5 Feb 2026 19:21:36 -0800 Subject: [PATCH] fix: resolve the weird path issues from windows <-> WSL (#106) ### Explanation _No response_ ### Issue _No response_ ### Attestations - [ ] I have read and agree to the [Code of Conduct](https://docs.nhcarrigan.com/community/coc/) - [ ] I have read and agree to the [Community Guidelines](https://docs.nhcarrigan.com/community/guide/). - [ ] My contribution complies with the [Contributor Covenant](https://docs.nhcarrigan.com/dev/covenant/). ### Dependencies - [ ] I have pinned the dependencies to a specific patch version. ### Style - [ ] I have run the linter and resolved any errors. - [ ] My pull request uses an appropriate title, matching the conventional commit standards. - [ ] My scope of feat/fix/chore/etc. correctly matches the nature of changes in my pull request. ### Tests - [ ] My contribution adds new code, and I have added tests to cover it. - [ ] My contribution modifies existing code, and I have updated the tests to reflect these changes. - [ ] All new and existing tests pass locally with my changes. - [ ] Code coverage remains at or above the configured threshold. ### Documentation _No response_ ### Versioning _No response_ Reviewed-on: https://git.nhcarrigan.com/nhcarrigan/hikari-desktop/pulls/106 Co-authored-by: Naomi Carrigan Co-committed-by: Naomi Carrigan --- src-tauri/Cargo.lock | 111 +++++- src-tauri/Cargo.toml | 1 + src-tauri/src/commands.rs | 598 ++++++++++++++++++++++++++--- src/lib/components/InputBar.svelte | 9 +- 4 files changed, 648 insertions(+), 71 deletions(-) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index c5dfd73..fd68779 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -767,13 +767,34 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys 0.4.1", +] + [[package]] name = "dirs" version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" dependencies = [ - "dirs-sys", + "dirs-sys 0.5.0", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users 0.4.6", + "windows-sys 0.48.0", ] [[package]] @@ -784,7 +805,7 @@ checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" dependencies = [ "libc", "option-ext", - "redox_users", + "redox_users 0.5.2", "windows-sys 0.61.2", ] @@ -1618,6 +1639,7 @@ name = "hikari-desktop" version = "1.2.0" dependencies = [ "chrono", + "dirs 5.0.1", "discord-rich-presence", "parking_lot", "semver", @@ -3352,6 +3374,17 @@ dependencies = [ "bitflags 2.10.0", ] +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 1.0.69", +] + [[package]] name = "redox_users" version = "0.5.2" @@ -4187,7 +4220,7 @@ dependencies = [ "anyhow", "bytes", "cookie", - "dirs", + "dirs 6.0.0", "dunce", "embed_plist", "getrandom 0.3.4", @@ -4238,7 +4271,7 @@ checksum = "17fcb8819fd16463512a12f531d44826ce566f486d7ccd211c9c8cebdaec4e08" dependencies = [ "anyhow", "cargo_toml", - "dirs", + "dirs 6.0.0", "glob", "heck 0.5.0", "json-patch", @@ -4962,7 +4995,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5e85aa143ceb072062fc4d6356c1b520a51d636e7bc8e77ec94be3608e5e80c" dependencies = [ "crossbeam-channel", - "dirs", + "dirs 6.0.0", "libappindicator", "muda", "objc2", @@ -5710,6 +5743,15 @@ dependencies = [ "windows-targets 0.42.2", ] +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -5761,6 +5803,21 @@ dependencies = [ "windows_x86_64_msvc 0.42.2", ] +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -5827,6 +5884,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -5845,6 +5908,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -5863,6 +5932,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -5893,6 +5968,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -5911,6 +5992,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -5929,6 +6016,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -5947,6 +6040,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -6027,7 +6126,7 @@ dependencies = [ "block2", "cookie", "crossbeam-channel", - "dirs", + "dirs 6.0.0", "dpi", "dunce", "gdkx11", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 47628b2..d7bdd33 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -32,6 +32,7 @@ tempfile = "3" semver = "1" chrono = { version = "0.4.43", features = ["serde"] } discord-rich-presence = "0.2" +dirs = "5" [target.'cfg(windows)'.dependencies] windows = { version = "0.62", features = [ diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index fa70da4..1be5ccd 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -1,5 +1,5 @@ use std::path::PathBuf; -use tauri::{AppHandle, State}; +use tauri::{AppHandle, Manager, State}; use tauri_plugin_http::reqwest; use tauri_plugin_store::StoreExt; @@ -11,6 +11,43 @@ use crate::temp_manager::SharedTempFileManager; const CONFIG_STORE_KEY: &str = "config"; +/// Convert a Windows path to a WSL path +/// Example: C:\Users\accou\Documents\item.txt -> /mnt/c/Users/accou/Documents/item.txt +fn windows_path_to_wsl(windows_path: &str) -> Option { + // Check if it's a Windows path (has drive letter like C:\) + if windows_path.len() >= 3 && windows_path.chars().nth(1) == Some(':') { + let drive_letter = windows_path.chars().next()?.to_lowercase().to_string(); + let path_without_drive = &windows_path[2..]; // Remove "C:" + + // Replace backslashes with forward slashes and convert to WSL mount point + let wsl_path = path_without_drive.replace('\\', "/"); + Some(format!("/mnt/{}{}", drive_letter, wsl_path)) + } else { + None + } +} + +/// Convert a WSL path to a Windows path +/// Example: /mnt/c/Users/accou/Documents/item.txt -> C:\Users\accou\Documents\item.txt +#[allow(dead_code)] +fn wsl_path_to_windows(wsl_path: &str) -> Option { + // Check if it's a WSL mount point path + if wsl_path.starts_with("/mnt/") && wsl_path.len() > 6 { + let rest = &wsl_path[5..]; // Remove "/mnt/" + if let Some(drive_letter) = rest.chars().next() { + let path_after_drive = &rest[1..]; // Remove drive letter + + // Convert to Windows path with backslashes + let windows_path = path_after_drive.replace('/', "\\"); + Some(format!("{}:{}", drive_letter.to_uppercase(), windows_path)) + } else { + None + } + } else { + None + } +} + #[tauri::command] pub async fn start_claude( bridge_manager: State<'_, SharedBridgeManager>, @@ -69,7 +106,10 @@ pub async fn get_working_directory( #[tauri::command] pub async fn select_wsl_directory() -> Result { - Ok("/home".to_string()) + // Return the user's home directory cross-platform + dirs::home_dir() + .ok_or_else(|| "Could not determine home directory".to_string()) + .map(|p| p.to_string_lossy().to_string()) } #[tauri::command] @@ -120,53 +160,93 @@ pub async fn validate_directory( path: String, current_dir: Option, ) -> Result { - use std::path::Path; + use std::path::{Path, PathBuf}; - let path = Path::new(&path); + // Detect if we're dealing with a WSL path (starts with / on Windows, or current_dir is a WSL path) + let is_wsl_path = cfg!(windows) && (path.starts_with('/') || current_dir.as_ref().is_some_and(|p| p.starts_with('/'))); - // Expand ~ to home directory - let expanded_path = if path.starts_with("~") { - if let Some(home) = std::env::var_os("HOME") { - let home_path = Path::new(&home); - if path == Path::new("~") { - home_path.to_path_buf() + if is_wsl_path { + // WSL path - handle as Unix-style path without filesystem validation + // since the Windows binary can't validate WSL filesystem paths + let resolved = if path.starts_with('/') { + // Absolute WSL path - use as-is + path + } else if let Some(ref cwd) = current_dir { + // Relative path - resolve manually using Unix path logic + if path == "." { + cwd.clone() + } else if path == ".." { + // Go up one directory + cwd.rsplit_once('/').map(|x| x.0).unwrap_or("/").to_string() + } else if path.starts_with("../") { + // Handle ../ prefix + let parent = cwd.rsplit_once('/').map(|x| x.0).unwrap_or("/"); + let remainder = path.strip_prefix("../").unwrap(); + if remainder.is_empty() { + parent.to_string() + } else { + format!("{}/{}", parent, remainder) + } + } else if path.starts_with("./") { + // Handle ./ prefix + format!("{}/{}", cwd, path.strip_prefix("./").unwrap()) } else { - home_path.join(path.strip_prefix("~").unwrap()) + // Regular relative path + format!("{}/{}", cwd, path) } } else { - return Err("Could not determine home directory".to_string()); - } - } else if path.is_relative() { - // Handle relative paths (., .., or any relative path) by resolving against current_dir - if let Some(ref cwd) = current_dir { - Path::new(cwd).join(path) + return Err("Cannot resolve relative WSL path without current directory".to_string()); + }; + + // Normalize the path (remove duplicate slashes, etc.) + let normalized = resolved.split('/').filter(|s| !s.is_empty()).collect::>().join("/"); + Ok(if normalized.is_empty() { "/".to_string() } else { format!("/{}", normalized) }) + } else { + // Native path (Windows on Windows, Unix on Unix) - validate normally + let path = Path::new(&path); + + let expanded_path = if path.starts_with("~") { + if let Some(home) = dirs::home_dir() { + if path == Path::new("~") { + home + } else { + home.join(path.strip_prefix("~").unwrap()) + } + } else { + return Err("Could not determine home directory".to_string()); + } + } else if path.is_relative() { + if let Some(ref cwd) = current_dir { + let cwd_path = PathBuf::from(cwd); + cwd_path.join(path) + } else { + path.to_path_buf() + } } else { path.to_path_buf() + }; + + // Check if the path exists and is a directory + if !expanded_path.exists() { + return Err(format!( + "Directory does not exist: {}", + expanded_path.display() + )); } - } else { - path.to_path_buf() - }; - // Check if the path exists and is a directory - if !expanded_path.exists() { - return Err(format!( - "Directory does not exist: {}", - expanded_path.display() - )); + if !expanded_path.is_dir() { + return Err(format!( + "Path is not a directory: {}", + expanded_path.display() + )); + } + + // Return the canonicalized (absolute) path + expanded_path + .canonicalize() + .map(|p| p.to_string_lossy().to_string()) + .map_err(|e| format!("Failed to resolve path: {}", e)) } - - if !expanded_path.is_dir() { - return Err(format!( - "Path is not a directory: {}", - expanded_path.display() - )); - } - - // Return the canonicalized (absolute) path - expanded_path - .canonicalize() - .map(|p| p.to_string_lossy().to_string()) - .map_err(|e| format!("Failed to resolve path: {}", e)) } #[tauri::command] @@ -202,21 +282,22 @@ pub async fn answer_question( #[tauri::command] pub async fn list_skills() -> Result, String> { + // On Windows, we need to use WSL to access the skills directory + // since skills are stored in the WSL home directory + if cfg!(windows) { + return list_skills_via_wsl().await; + } + + // On Unix systems, use the native filesystem use std::fs; - use std::path::Path; - // Get the home directory - let home = - std::env::var_os("HOME").ok_or_else(|| "Could not determine home directory".to_string())?; + let home = dirs::home_dir().ok_or_else(|| "Could not determine home directory".to_string())?; + let skills_dir = home.join(".claude").join("skills"); - let skills_dir = Path::new(&home).join(".claude").join("skills"); - - // If the skills directory doesn't exist, return empty list if !skills_dir.exists() { return Ok(Vec::new()); } - // Read the directory and collect skill names let mut skills = Vec::new(); let entries = fs::read_dir(&skills_dir).map_err(|e| format!("Failed to read skills directory: {}", e))?; @@ -225,7 +306,6 @@ pub async fn list_skills() -> Result, String> { let entry = entry.map_err(|e| format!("Failed to read directory entry: {}", e))?; let path = entry.path(); - // Only include directories that contain a SKILL.md file if path.is_dir() { let skill_file = path.join("SKILL.md"); if skill_file.exists() { @@ -236,9 +316,42 @@ pub async fn list_skills() -> Result, String> { } } - // Sort alphabetically skills.sort(); + Ok(skills) +} +/// List skills by executing commands through WSL (for Windows) +#[allow(dead_code)] +async fn list_skills_via_wsl() -> Result, String> { + use std::process::Command; + + // Use WSL to list directories in ~/.claude/skills that contain SKILL.md + let output = Command::new("wsl") + .args([ + "-e", + "sh", + "-c", + "if [ -d ~/.claude/skills ]; then for d in ~/.claude/skills/*/; do [ -f \"${d}SKILL.md\" ] && basename \"$d\"; done; fi", + ]) + .output() + .map_err(|e| format!("Failed to execute WSL command: {}", e))?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + if stderr.contains("not found") || stderr.contains("No such file") { + return Ok(Vec::new()); + } + return Err(format!("WSL command failed: {}", stderr)); + } + + let stdout = String::from_utf8_lossy(&output.stdout); + let mut skills: Vec = stdout + .lines() + .filter(|line| !line.is_empty()) + .map(|line| line.to_string()) + .collect(); + + skills.sort(); Ok(skills) } @@ -335,8 +448,18 @@ pub async fn save_temp_file( .map(|n| n.to_string_lossy().to_string()) .unwrap_or_else(|| "unknown".to_string()); + let path_string = path.to_string_lossy().to_string(); + + // On Windows, convert the path to WSL format if needed + // so Claude Code (running in WSL) can access it via /mnt/c/... + let final_path = if cfg!(windows) { + windows_path_to_wsl(&path_string).unwrap_or(path_string) + } else { + path_string + }; + Ok(SavedFileInfo { - path: path.to_string_lossy().to_string(), + path: final_path, filename, }) } @@ -405,42 +528,142 @@ pub struct FileEntry { } #[tauri::command] -pub async fn list_directory(path: String) -> Result, String> { +pub async fn list_directory(app: AppHandle, path: String) -> Result, String> { + // Set up logging + let log_path = if let Ok(app_data_dir) = app.path().app_data_dir() { + let _ = std::fs::create_dir_all(&app_data_dir); + app_data_dir.join("hikari_editor_debug.log") + } else { + PathBuf::from("hikari_editor_debug.log") + }; + + let mut log_file = std::fs::OpenOptions::new() + .create(true) + .append(true) + .open(&log_path) + .ok(); + + let mut log = |msg: String| { + if let Some(ref mut file) = log_file { + use std::io::Write; + let timestamp = chrono::Local::now().format("%Y-%m-%d %H:%M:%S"); + let _ = writeln!(file, "[{}] {}", timestamp, msg); + } + }; + + log(format!("list_directory called with path: {}", path)); + log(format!("cfg!(windows) = {}", cfg!(windows))); + log(format!("path.starts_with('/') = {}", path.starts_with('/'))); + + // On Windows with a WSL path (starts with /), use WSL to list the directory + if cfg!(windows) && path.starts_with('/') { + log("Using WSL path".to_string()); + return list_directory_via_wsl(&path).await; + } + + log("Using native filesystem access".to_string()); + + // Native filesystem access use std::fs; use std::path::Path; let dir_path = Path::new(&path); if !dir_path.exists() { - return Err(format!("Directory does not exist: {}", path)); + let err = format!("Directory does not exist: {}", path); + log(format!("ERROR: {}", err)); + return Err(err); } if !dir_path.is_dir() { - return Err(format!("Path is not a directory: {}", path)); + let err = format!("Path is not a directory: {}", path); + log(format!("ERROR: {}", err)); + return Err(err); } let entries = fs::read_dir(dir_path) - .map_err(|e| format!("Failed to read directory: {}", e))?; + .map_err(|e| { + let err = format!("Failed to read directory: {}", e); + log(format!("ERROR: {}", err)); + err + })?; let mut file_entries = Vec::new(); for entry in entries { - let entry = entry.map_err(|e| format!("Failed to read entry: {}", e))?; + let entry = entry.map_err(|e| { + let err = format!("Failed to read entry: {}", e); + log(format!("ERROR: {}", err)); + err + })?; let path = entry.path(); let name = entry .file_name() .to_string_lossy() .to_string(); - // Skip hidden files by default (can be made configurable later) - if name.starts_with('.') { + file_entries.push(FileEntry { + name: name.clone(), + path: path.to_string_lossy().to_string(), + is_directory: path.is_dir(), + }); + } + + log(format!("Successfully listed {} entries", file_entries.len())); + Ok(file_entries) +} + +/// List directory contents via WSL (for Windows with WSL paths) +#[allow(dead_code)] +async fn list_directory_via_wsl(path: &str) -> Result, String> { + use std::process::Command; + + // Use WSL to list directory contents + // Output format: typename (d for directory, f for file) + let script = format!( + r#"if [ -d '{}' ]; then for f in '{}'/* '{}'/.* ; do [ -e "$f" ] || continue; name=$(basename "$f"); if [ "$name" = "." ] || [ "$name" = ".." ]; then continue; fi; if [ -d "$f" ]; then echo "d $name"; else echo "f $name"; fi; done; else echo "ERROR: Directory does not exist"; exit 1; fi"#, + path, path, path + ); + + let output = Command::new("wsl") + .args(["-e", "sh", "-c", &script]) + .output() + .map_err(|e| format!("Failed to execute WSL command: {}", e))?; + + let stdout = String::from_utf8_lossy(&output.stdout); + + if !output.status.success() || stdout.starts_with("ERROR:") { + let stderr = String::from_utf8_lossy(&output.stderr); + if stdout.starts_with("ERROR:") { + return Err(stdout.trim().to_string()); + } + return Err(format!("WSL command failed: {}", stderr)); + } + + let mut file_entries = Vec::new(); + + for line in stdout.lines() { + if line.is_empty() { continue; } + let parts: Vec<&str> = line.splitn(2, '\t').collect(); + if parts.len() != 2 { + continue; + } + + let is_directory = parts[0] == "d"; + let name = parts[1].to_string(); + let entry_path = if path == "/" { + format!("/{}", name) + } else { + format!("{}/{}", path, name) + }; + file_entries.push(FileEntry { name, - path: path.to_string_lossy().to_string(), - is_directory: path.is_dir(), + path: entry_path, + is_directory, }); } @@ -449,22 +672,80 @@ pub async fn list_directory(path: String) -> Result, String> { #[tauri::command] pub async fn read_file_content(path: String) -> Result { - use std::fs; + // On Windows with a WSL path, use WSL to read the file + if cfg!(windows) && path.starts_with('/') { + return read_file_via_wsl(&path).await; + } + use std::fs; fs::read_to_string(&path) .map_err(|e| format!("Failed to read file: {}", e)) } +/// Read file content via WSL (for Windows with WSL paths) +#[allow(dead_code)] +async fn read_file_via_wsl(path: &str) -> Result { + use std::process::Command; + + let output = Command::new("wsl") + .args(["-e", "cat", path]) + .output() + .map_err(|e| format!("Failed to execute WSL command: {}", e))?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(format!("Failed to read file: {}", stderr)); + } + + Ok(String::from_utf8_lossy(&output.stdout).to_string()) +} + #[tauri::command] pub async fn write_file_content(path: String, content: String) -> Result<(), String> { - use std::fs; + // On Windows with a WSL path, use WSL to write the file + if cfg!(windows) && path.starts_with('/') { + return write_file_via_wsl(&path, &content).await; + } + use std::fs; fs::write(&path, content) .map_err(|e| format!("Failed to write file: {}", e)) } +/// Write file content via WSL (for Windows with WSL paths) +#[allow(dead_code)] +async fn write_file_via_wsl(path: &str, content: &str) -> Result<(), String> { + use std::io::Write; + use std::process::{Command, Stdio}; + + let mut child = Command::new("wsl") + .args(["-e", "sh", "-c", &format!("cat > '{}'", path)]) + .stdin(Stdio::piped()) + .spawn() + .map_err(|e| format!("Failed to execute WSL command: {}", e))?; + + if let Some(mut stdin) = child.stdin.take() { + stdin.write_all(content.as_bytes()) + .map_err(|e| format!("Failed to write to stdin: {}", e))?; + } + + let status = child.wait() + .map_err(|e| format!("Failed to wait for WSL command: {}", e))?; + + if !status.success() { + return Err("Failed to write file via WSL".to_string()); + } + + Ok(()) +} + #[tauri::command] pub async fn create_file(path: String) -> Result<(), String> { + // On Windows with a WSL path, use WSL to create the file + if cfg!(windows) && path.starts_with('/') { + return create_file_via_wsl(&path).await; + } + use std::fs::File; use std::path::Path; @@ -479,8 +760,41 @@ pub async fn create_file(path: String) -> Result<(), String> { Ok(()) } +/// Create file via WSL (for Windows with WSL paths) +#[allow(dead_code)] +async fn create_file_via_wsl(path: &str) -> Result<(), String> { + use std::process::Command; + + // Check if file exists first + let check = Command::new("wsl") + .args(["-e", "test", "-e", path]) + .status() + .map_err(|e| format!("Failed to execute WSL command: {}", e))?; + + if check.success() { + return Err("File already exists".to_string()); + } + + let output = Command::new("wsl") + .args(["-e", "touch", path]) + .output() + .map_err(|e| format!("Failed to execute WSL command: {}", e))?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(format!("Failed to create file: {}", stderr)); + } + + Ok(()) +} + #[tauri::command] pub async fn create_directory(path: String) -> Result<(), String> { + // On Windows with a WSL path, use WSL to create the directory + if cfg!(windows) && path.starts_with('/') { + return create_directory_via_wsl(&path).await; + } + use std::fs; use std::path::Path; @@ -495,8 +809,41 @@ pub async fn create_directory(path: String) -> Result<(), String> { Ok(()) } +/// Create directory via WSL (for Windows with WSL paths) +#[allow(dead_code)] +async fn create_directory_via_wsl(path: &str) -> Result<(), String> { + use std::process::Command; + + // Check if directory exists first + let check = Command::new("wsl") + .args(["-e", "test", "-e", path]) + .status() + .map_err(|e| format!("Failed to execute WSL command: {}", e))?; + + if check.success() { + return Err("Directory already exists".to_string()); + } + + let output = Command::new("wsl") + .args(["-e", "mkdir", "-p", path]) + .output() + .map_err(|e| format!("Failed to execute WSL command: {}", e))?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(format!("Failed to create directory: {}", stderr)); + } + + Ok(()) +} + #[tauri::command] pub async fn delete_file(path: String) -> Result<(), String> { + // On Windows with a WSL path, use WSL to delete the file + if cfg!(windows) && path.starts_with('/') { + return delete_file_via_wsl(&path).await; + } + use std::fs; use std::path::Path; @@ -515,8 +862,51 @@ pub async fn delete_file(path: String) -> Result<(), String> { Ok(()) } +/// Delete file via WSL (for Windows with WSL paths) +#[allow(dead_code)] +async fn delete_file_via_wsl(path: &str) -> Result<(), String> { + use std::process::Command; + + // Check if path exists + let check_exists = Command::new("wsl") + .args(["-e", "test", "-e", path]) + .status() + .map_err(|e| format!("Failed to execute WSL command: {}", e))?; + + if !check_exists.success() { + return Err("File does not exist".to_string()); + } + + // Check if path is a directory + let check_dir = Command::new("wsl") + .args(["-e", "test", "-d", path]) + .status() + .map_err(|e| format!("Failed to execute WSL command: {}", e))?; + + if check_dir.success() { + return Err("Path is a directory, use delete_directory instead".to_string()); + } + + let output = Command::new("wsl") + .args(["-e", "rm", path]) + .output() + .map_err(|e| format!("Failed to execute WSL command: {}", e))?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(format!("Failed to delete file: {}", stderr)); + } + + Ok(()) +} + #[tauri::command] pub async fn delete_directory(path: String) -> Result<(), String> { + // On Windows with a WSL path, use WSL to delete the directory + if cfg!(windows) && path.starts_with('/') { + return delete_directory_via_wsl(&path).await; + } + use std::fs; use std::path::Path; @@ -535,8 +925,51 @@ pub async fn delete_directory(path: String) -> Result<(), String> { Ok(()) } +/// Delete directory via WSL (for Windows with WSL paths) +#[allow(dead_code)] +async fn delete_directory_via_wsl(path: &str) -> Result<(), String> { + use std::process::Command; + + // Check if path exists + let check_exists = Command::new("wsl") + .args(["-e", "test", "-e", path]) + .status() + .map_err(|e| format!("Failed to execute WSL command: {}", e))?; + + if !check_exists.success() { + return Err("Directory does not exist".to_string()); + } + + // Check if path is a directory + let check_dir = Command::new("wsl") + .args(["-e", "test", "-d", path]) + .status() + .map_err(|e| format!("Failed to execute WSL command: {}", e))?; + + if !check_dir.success() { + return Err("Path is not a directory".to_string()); + } + + let output = Command::new("wsl") + .args(["-e", "rm", "-rf", path]) + .output() + .map_err(|e| format!("Failed to execute WSL command: {}", e))?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(format!("Failed to delete directory: {}", stderr)); + } + + Ok(()) +} + #[tauri::command] pub async fn rename_path(old_path: String, new_path: String) -> Result<(), String> { + // On Windows with WSL paths, use WSL to rename + if cfg!(windows) && old_path.starts_with('/') { + return rename_path_via_wsl(&old_path, &new_path).await; + } + use std::fs; use std::path::Path; @@ -556,6 +989,44 @@ pub async fn rename_path(old_path: String, new_path: String) -> Result<(), Strin Ok(()) } +/// Rename path via WSL (for Windows with WSL paths) +#[allow(dead_code)] +async fn rename_path_via_wsl(old_path: &str, new_path: &str) -> Result<(), String> { + use std::process::Command; + + // Check if old path exists + let check_old = Command::new("wsl") + .args(["-e", "test", "-e", old_path]) + .status() + .map_err(|e| format!("Failed to execute WSL command: {}", e))?; + + if !check_old.success() { + return Err("Path does not exist".to_string()); + } + + // Check if new path already exists + let check_new = Command::new("wsl") + .args(["-e", "test", "-e", new_path]) + .status() + .map_err(|e| format!("Failed to execute WSL command: {}", e))?; + + if check_new.success() { + return Err("Destination already exists".to_string()); + } + + let output = Command::new("wsl") + .args(["-e", "mv", old_path, new_path]) + .output() + .map_err(|e| format!("Failed to execute WSL command: {}", e))?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(format!("Failed to rename: {}", stderr)); + } + + Ok(()) +} + // ==================== Cost Tracking Commands ==================== const COST_HISTORY_STORE_KEY: &str = "cost_history"; @@ -892,7 +1363,10 @@ mod tests { fn test_select_wsl_directory_returns_home() { let result = run_async(select_wsl_directory()); assert!(result.is_ok()); - assert_eq!(result.unwrap(), "/home"); + + // Should return the user's home directory + let home_dir = result.unwrap(); + assert!(home_dir.starts_with("/home/") || home_dir == "/root"); } // ==================== UpdateInfo struct tests ==================== diff --git a/src/lib/components/InputBar.svelte b/src/lib/components/InputBar.svelte index f6307fe..bbb09ac 100644 --- a/src/lib/components/InputBar.svelte +++ b/src/lib/components/InputBar.svelte @@ -447,11 +447,12 @@ User: ${formattedMessage}`; try { const arrayBuffer = await file.arrayBuffer(); const bytes = Array.from(new Uint8Array(arrayBuffer)); - savedPath = await invoke("save_temp_file", { + const result = await invoke<{ path: string; filename: string }>("save_temp_file", { conversationId, filename, data: bytes, }); + savedPath = result.path; } catch (error) { console.error("Failed to save dropped file to temp:", error); savedPath = file.name; @@ -585,11 +586,12 @@ User: ${formattedMessage}`; try { const arrayBuffer = await file.arrayBuffer(); const bytes = Array.from(new Uint8Array(arrayBuffer)); - savedPath = await invoke("save_temp_file", { + const result = await invoke<{ path: string; filename: string }>("save_temp_file", { conversationId, filename, data: bytes, }); + savedPath = result.path; } catch (error) { console.error("Failed to save pasted file to temp:", error); } @@ -647,11 +649,12 @@ User: ${formattedMessage}`; try { const arrayBuffer = await blob.arrayBuffer(); const bytes = Array.from(new Uint8Array(arrayBuffer)); - savedPath = await invoke("save_temp_file", { + const result = await invoke<{ path: string; filename: string }>("save_temp_file", { conversationId, filename, data: bytes, }); + savedPath = result.path; } catch (error) { console.error("Failed to save clipboard image to temp:", error); }