Compare commits

..

1 Commits

Author SHA1 Message Date
minori ddc9f3b26c deps: update @sveltejs/vite-plugin-svelte to 6.2.4
Security Scan and Upload / Security & DefectDojo Upload (pull_request) Successful in 1m59s
CI / Lint & Test (pull_request) Successful in 20m18s
CI / Build Linux (pull_request) Successful in 22m59s
CI / Build Windows (cross-compile) (pull_request) Successful in 36m49s
2026-02-04 08:55:40 -08:00
11 changed files with 50 additions and 328 deletions
+1 -1
View File
@@ -70,7 +70,7 @@
"@eslint/js": "^9.39.2", "@eslint/js": "^9.39.2",
"@sveltejs/adapter-static": "^3.0.6", "@sveltejs/adapter-static": "^3.0.6",
"@sveltejs/kit": "^2.9.0", "@sveltejs/kit": "^2.9.0",
"@sveltejs/vite-plugin-svelte": "^5.0.0", "@sveltejs/vite-plugin-svelte": "6.2.4",
"@tailwindcss/vite": "^4.1.18", "@tailwindcss/vite": "^4.1.18",
"@tauri-apps/cli": "^2", "@tauri-apps/cli": "^2",
"@testing-library/jest-dom": "^6.9.1", "@testing-library/jest-dom": "^6.9.1",
+23 -28
View File
@@ -128,13 +128,13 @@ importers:
version: 9.39.2 version: 9.39.2
'@sveltejs/adapter-static': '@sveltejs/adapter-static':
specifier: ^3.0.6 specifier: ^3.0.6
version: 3.0.10(@sveltejs/kit@2.49.4(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.46.3)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.46.3)(typescript@5.6.3)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2))) version: 3.0.10(@sveltejs/kit@2.49.4(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.46.3)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.46.3)(typescript@5.6.3)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2)))
'@sveltejs/kit': '@sveltejs/kit':
specifier: ^2.9.0 specifier: ^2.9.0
version: 2.49.4(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.46.3)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.46.3)(typescript@5.6.3)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2)) version: 2.49.4(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.46.3)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.46.3)(typescript@5.6.3)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2))
'@sveltejs/vite-plugin-svelte': '@sveltejs/vite-plugin-svelte':
specifier: ^5.0.0 specifier: 6.2.4
version: 5.1.1(svelte@5.46.3)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2)) version: 6.2.4(svelte@5.46.3)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2))
'@tailwindcss/vite': '@tailwindcss/vite':
specifier: ^4.1.18 specifier: ^4.1.18
version: 4.1.18(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2)) version: 4.1.18(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2))
@@ -801,20 +801,20 @@ packages:
typescript: typescript:
optional: true optional: true
'@sveltejs/vite-plugin-svelte-inspector@4.0.1': '@sveltejs/vite-plugin-svelte-inspector@5.0.2':
resolution: {integrity: sha512-J/Nmb2Q2y7mck2hyCX4ckVHcR5tu2J+MtBEQqpDrrgELZ2uvraQcK/ioCV61AqkdXFgriksOKIceDcQmqnGhVw==} resolution: {integrity: sha512-TZzRTcEtZffICSAoZGkPSl6Etsj2torOVrx6Uw0KpXxrec9Gg6jFWQ60Q3+LmNGfZSxHRCZL7vXVZIWmuV50Ig==}
engines: {node: ^18.0.0 || ^20.0.0 || >=22} engines: {node: ^20.19 || ^22.12 || >=24}
peerDependencies: peerDependencies:
'@sveltejs/vite-plugin-svelte': ^5.0.0 '@sveltejs/vite-plugin-svelte': ^6.0.0-next.0
svelte: ^5.0.0 svelte: ^5.0.0
vite: ^6.0.0 vite: ^6.3.0 || ^7.0.0
'@sveltejs/vite-plugin-svelte@5.1.1': '@sveltejs/vite-plugin-svelte@6.2.4':
resolution: {integrity: sha512-Y1Cs7hhTc+a5E9Va/xwKlAJoariQyHY+5zBgCZg4PFWNYQ1nMN9sjK1zhw1gK69DuqVP++sht/1GZg1aRwmAXQ==} resolution: {integrity: sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA==}
engines: {node: ^18.0.0 || ^20.0.0 || >=22} engines: {node: ^20.19 || ^22.12 || >=24}
peerDependencies: peerDependencies:
svelte: ^5.0.0 svelte: ^5.0.0
vite: ^6.0.0 vite: ^6.3.0 || ^7.0.0
'@tailwindcss/node@4.1.18': '@tailwindcss/node@4.1.18':
resolution: {integrity: sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==} resolution: {integrity: sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==}
@@ -2746,15 +2746,15 @@ snapshots:
dependencies: dependencies:
acorn: 8.15.0 acorn: 8.15.0
'@sveltejs/adapter-static@3.0.10(@sveltejs/kit@2.49.4(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.46.3)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.46.3)(typescript@5.6.3)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2)))': '@sveltejs/adapter-static@3.0.10(@sveltejs/kit@2.49.4(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.46.3)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.46.3)(typescript@5.6.3)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2)))':
dependencies: dependencies:
'@sveltejs/kit': 2.49.4(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.46.3)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.46.3)(typescript@5.6.3)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2)) '@sveltejs/kit': 2.49.4(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.46.3)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.46.3)(typescript@5.6.3)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2))
'@sveltejs/kit@2.49.4(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.46.3)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.46.3)(typescript@5.6.3)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2))': '@sveltejs/kit@2.49.4(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.46.3)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.46.3)(typescript@5.6.3)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2))':
dependencies: dependencies:
'@standard-schema/spec': 1.1.0 '@standard-schema/spec': 1.1.0
'@sveltejs/acorn-typescript': 1.0.8(acorn@8.15.0) '@sveltejs/acorn-typescript': 1.0.8(acorn@8.15.0)
'@sveltejs/vite-plugin-svelte': 5.1.1(svelte@5.46.3)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2)) '@sveltejs/vite-plugin-svelte': 6.2.4(svelte@5.46.3)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2))
'@types/cookie': 0.6.0 '@types/cookie': 0.6.0
acorn: 8.15.0 acorn: 8.15.0
cookie: 0.6.0 cookie: 0.6.0
@@ -2771,27 +2771,22 @@ snapshots:
optionalDependencies: optionalDependencies:
typescript: 5.6.3 typescript: 5.6.3
'@sveltejs/vite-plugin-svelte-inspector@4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.46.3)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.46.3)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2))': '@sveltejs/vite-plugin-svelte-inspector@5.0.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.46.3)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.46.3)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2))':
dependencies: dependencies:
'@sveltejs/vite-plugin-svelte': 5.1.1(svelte@5.46.3)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2)) '@sveltejs/vite-plugin-svelte': 6.2.4(svelte@5.46.3)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2))
debug: 4.4.3 obug: 2.1.1
svelte: 5.46.3 svelte: 5.46.3
vite: 6.4.1(jiti@2.6.1)(lightningcss@1.30.2) vite: 6.4.1(jiti@2.6.1)(lightningcss@1.30.2)
transitivePeerDependencies:
- supports-color
'@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.46.3)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2))': '@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.46.3)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2))':
dependencies: dependencies:
'@sveltejs/vite-plugin-svelte-inspector': 4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.46.3)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.46.3)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2)) '@sveltejs/vite-plugin-svelte-inspector': 5.0.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.46.3)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.46.3)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2))
debug: 4.4.3
deepmerge: 4.3.1 deepmerge: 4.3.1
kleur: 4.1.5
magic-string: 0.30.21 magic-string: 0.30.21
obug: 2.1.1
svelte: 5.46.3 svelte: 5.46.3
vite: 6.4.1(jiti@2.6.1)(lightningcss@1.30.2) vite: 6.4.1(jiti@2.6.1)(lightningcss@1.30.2)
vitefu: 1.1.1(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2)) vitefu: 1.1.1(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2))
transitivePeerDependencies:
- supports-color
'@tailwindcss/node@4.1.18': '@tailwindcss/node@4.1.18':
dependencies: dependencies:
+7 -32
View File
@@ -8,7 +8,6 @@ use crate::bridge_manager::SharedBridgeManager;
use crate::config::{ClaudeStartOptions, HikariConfig}; use crate::config::{ClaudeStartOptions, HikariConfig};
use crate::stats::UsageStats; use crate::stats::UsageStats;
use crate::temp_manager::SharedTempFileManager; use crate::temp_manager::SharedTempFileManager;
use crate::utils::normalize_path_separators;
const CONFIG_STORE_KEY: &str = "config"; const CONFIG_STORE_KEY: &str = "config";
@@ -127,7 +126,7 @@ pub async fn validate_directory(
// Expand ~ to home directory // Expand ~ to home directory
let expanded_path = if path.starts_with("~") { let expanded_path = if path.starts_with("~") {
if let Some(home) = std::env::var_os("HOME").or_else(|| std::env::var_os("USERPROFILE")) { if let Some(home) = std::env::var_os("HOME") {
let home_path = Path::new(&home); let home_path = Path::new(&home);
if path == Path::new("~") { if path == Path::new("~") {
home_path.to_path_buf() home_path.to_path_buf()
@@ -163,10 +162,10 @@ pub async fn validate_directory(
)); ));
} }
// Return the canonicalized (absolute) path with forward slashes // Return the canonicalized (absolute) path
expanded_path expanded_path
.canonicalize() .canonicalize()
.map(|p| normalize_path_separators(&p.to_string_lossy())) .map(|p| p.to_string_lossy().to_string())
.map_err(|e| format!("Failed to resolve path: {}", e)) .map_err(|e| format!("Failed to resolve path: {}", e))
} }
@@ -206,10 +205,9 @@ pub async fn list_skills() -> Result<Vec<String>, String> {
use std::fs; use std::fs;
use std::path::Path; use std::path::Path;
// Get the home directory - use HOME on Unix, USERPROFILE on Windows // Get the home directory
let home = std::env::var_os("HOME") let home =
.or_else(|| std::env::var_os("USERPROFILE")) std::env::var_os("HOME").ok_or_else(|| "Could not determine home directory".to_string())?;
.ok_or_else(|| "Could not determine home directory".to_string())?;
let skills_dir = Path::new(&home).join(".claude").join("skills"); let skills_dir = Path::new(&home).join(".claude").join("skills");
@@ -439,11 +437,9 @@ pub async fn list_directory(path: String) -> Result<Vec<FileEntry>, String> {
continue; continue;
} }
let path_str = normalize_path_separators(&path.to_string_lossy());
file_entries.push(FileEntry { file_entries.push(FileEntry {
name, name,
path: path_str, path: path.to_string_lossy().to_string(),
is_directory: path.is_dir(), is_directory: path.is_dir(),
}); });
} }
@@ -818,25 +814,4 @@ mod tests {
assert!(json.contains("/tmp/test.txt")); assert!(json.contains("/tmp/test.txt"));
assert!(json.contains("test.txt")); assert!(json.contains("test.txt"));
} }
#[test]
fn test_validate_directory_normalizes_backslashes() {
// This test ensures that paths with backslashes are normalized
// Create a temp directory for testing
let temp_dir = TempDir::new().unwrap();
// On Windows, the canonicalized path will have backslashes
// Our normalize_path_separators should convert them to forward slashes
let result = run_async(validate_directory(
temp_dir.path().to_string_lossy().to_string(),
None,
));
assert!(result.is_ok());
let normalized_path = result.unwrap();
// The result should never contain backslashes
assert!(!normalized_path.contains('\\'),
"Path should not contain backslashes: {}", normalized_path);
}
} }
-1
View File
@@ -12,7 +12,6 @@ mod stats;
mod temp_manager; mod temp_manager;
mod tray; mod tray;
mod types; mod types;
mod utils;
mod vbs_notification; mod vbs_notification;
mod windows_toast; mod windows_toast;
mod wsl_bridge; mod wsl_bridge;
-71
View File
@@ -1,71 +0,0 @@
/// Utility functions for cross-platform compatibility
/// Normalize path separators to forward slashes for consistent handling across platforms
/// This always normalizes backslashes to forward slashes, regardless of platform,
/// because Windows paths may be passed to WSL which expects Unix-style paths
pub fn normalize_path_separators(path: &str) -> String {
path.replace('\\', "/")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[cfg(target_os = "windows")]
fn test_normalize_path_windows() {
assert_eq!(normalize_path_separators("C:\\Users\\test"), "C:/Users/test");
assert_eq!(normalize_path_separators("path\\to\\file"), "path/to/file");
assert_eq!(normalize_path_separators("already/forward"), "already/forward");
assert_eq!(normalize_path_separators("mixed\\path/file"), "mixed/path/file");
}
#[test]
#[cfg(not(target_os = "windows"))]
fn test_normalize_path_unix() {
assert_eq!(normalize_path_separators("/home/user"), "/home/user");
assert_eq!(normalize_path_separators("path/to/file"), "path/to/file");
// Even on Unix, we normalize backslashes since paths may come from Windows
assert_eq!(normalize_path_separators("weird\\path"), "weird/path");
assert_eq!(normalize_path_separators("/home/user\\file"), "/home/user/file");
}
#[test]
fn test_normalize_wsl_paths() {
// Test the exact issue that was happening
assert_eq!(
normalize_path_separators("/home/naomi/code/naomi/portfolio\\.."),
"/home/naomi/code/naomi/portfolio/.."
);
// Test mixed separators in WSL paths
assert_eq!(
normalize_path_separators("/mnt/c\\Users\\naomi\\Documents"),
"/mnt/c/Users/naomi/Documents"
);
// Test Windows paths that might be passed to WSL
assert_eq!(
normalize_path_separators("C:\\Users\\naomi\\.claude\\skills"),
"C:/Users/naomi/.claude/skills"
);
// Test that forward slashes remain unchanged
assert_eq!(
normalize_path_separators("/home/naomi/.claude/skills"),
"/home/naomi/.claude/skills"
);
// Test empty string
assert_eq!(normalize_path_separators(""), "");
// Test single backslash
assert_eq!(normalize_path_separators("\\"), "/");
// Test multiple consecutive backslashes
assert_eq!(
normalize_path_separators("path\\\\to\\\\file"),
"path//to//file"
);
}
}
+4 -6
View File
@@ -16,7 +16,6 @@ use crate::types::{
PermissionPromptEvent, QuestionOption, SessionEvent, StateChangeEvent, UserQuestionEvent, PermissionPromptEvent, QuestionOption, SessionEvent, StateChangeEvent, UserQuestionEvent,
WorkingDirectoryEvent, WorkingDirectoryEvent,
}; };
use crate::utils::normalize_path_separators;
use parking_lot::RwLock; use parking_lot::RwLock;
const SEARCH_TOOLS: [&str; 5] = ["Read", "Glob", "Grep", "WebSearch", "WebFetch"]; const SEARCH_TOOLS: [&str; 5] = ["Read", "Glob", "Grep", "WebSearch", "WebFetch"];
@@ -137,8 +136,7 @@ impl WslBridge {
}); });
let working_dir = &options.working_dir; let working_dir = &options.working_dir;
// Normalize path separators to forward slashes self.working_directory = working_dir.clone();
self.working_directory = normalize_path_separators(working_dir);
emit_connection_status( emit_connection_status(
&app, &app,
@@ -185,7 +183,7 @@ impl WslBridge {
})?; })?;
eprintln!("[DEBUG] Found claude at: {}", claude_path); eprintln!("[DEBUG] Found claude at: {}", claude_path);
eprintln!("[DEBUG] Working dir: {}", &self.working_directory); eprintln!("[DEBUG] Working dir: {}", working_dir);
let mut cmd = Command::new(&claude_path); let mut cmd = Command::new(&claude_path);
cmd.args([ cmd.args([
@@ -227,7 +225,7 @@ impl WslBridge {
} }
} }
cmd.current_dir(&self.working_directory); cmd.current_dir(working_dir);
// Set API key as environment variable if specified // Set API key as environment variable if specified
if let Some(ref api_key) = options.api_key { if let Some(ref api_key) = options.api_key {
@@ -243,7 +241,7 @@ impl WslBridge {
let mut cmd = Command::new("wsl"); let mut cmd = Command::new("wsl");
// Build the claude command with all arguments // Build the claude command with all arguments
let mut claude_cmd = format!("cd '{}' && ", &self.working_directory); let mut claude_cmd = format!("cd '{}' && ", working_dir);
// Set API key as environment variable if specified // Set API key as environment variable if specified
if let Some(ref api_key) = options.api_key { if let Some(ref api_key) = options.api_key {
+2 -3
View File
@@ -8,7 +8,6 @@ import type {
} from "$lib/types/messages"; } from "$lib/types/messages";
import type { CharacterState } from "$lib/types/states"; import type { CharacterState } from "$lib/types/states";
import { cleanupConversationTracking } from "$lib/tauri"; import { cleanupConversationTracking } from "$lib/tauri";
import { normalizePath } from "$lib/utils/paths";
import { characterState } from "$lib/stores/character"; import { characterState } from "$lib/stores/character";
import { sessionsStore } from "$lib/stores/sessions"; import { sessionsStore } from "$lib/stores/sessions";
@@ -389,7 +388,7 @@ function createConversationsStore() {
conversations.update((convs) => { conversations.update((convs) => {
const conv = convs.get(activeId); const conv = convs.get(activeId);
if (conv) { if (conv) {
conv.workingDirectory = normalizePath(dir); conv.workingDirectory = dir;
conv.lastActivityAt = new Date(); conv.lastActivityAt = new Date();
} }
return convs; return convs;
@@ -400,7 +399,7 @@ function createConversationsStore() {
conversations.update((convs) => { conversations.update((convs) => {
const conv = convs.get(conversationId); const conv = convs.get(conversationId);
if (conv) { if (conv) {
conv.workingDirectory = normalizePath(dir); conv.workingDirectory = dir;
conv.lastActivityAt = new Date(); conv.lastActivityAt = new Date();
} }
return convs; return convs;
+11 -15
View File
@@ -1,7 +1,6 @@
import { writable, derived, get } from "svelte/store"; import { writable, derived, get } from "svelte/store";
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
import type { EditorState, EditorTab, FileEntry } from "$lib/types/editor"; import type { EditorState, EditorTab, FileEntry } from "$lib/types/editor";
import { normalizePath, joinPath, getParentPath, getFilename } from "$lib/utils/paths";
const defaultState: EditorState = { const defaultState: EditorState = {
tabs: [], tabs: [],
@@ -159,7 +158,7 @@ function createEditorStore() {
try { try {
const content = await invoke<string>("read_file_content", { path: filePath }); const content = await invoke<string>("read_file_content", { path: filePath });
const fileName = getFilename(filePath) || "untitled"; const fileName = filePath.split(/[/\\]/).pop() || "untitled";
const language = getLanguageFromPath(filePath); const language = getLanguageFromPath(filePath);
const newTab: EditorTab = { const newTab: EditorTab = {
id: generateTabId(), id: generateTabId(),
@@ -248,7 +247,7 @@ function createEditorStore() {
} }
async function createFile(parentPath: string, fileName: string): Promise<boolean> { async function createFile(parentPath: string, fileName: string): Promise<boolean> {
const filePath = joinPath(parentPath, fileName); const filePath = `${parentPath}/${fileName}`;
try { try {
await invoke("create_file", { path: filePath }); await invoke("create_file", { path: filePath });
// Refresh the parent directory // Refresh the parent directory
@@ -262,7 +261,7 @@ function createEditorStore() {
} }
async function createDirectory(parentPath: string, dirName: string): Promise<boolean> { async function createDirectory(parentPath: string, dirName: string): Promise<boolean> {
const dirPath = joinPath(parentPath, dirName); const dirPath = `${parentPath}/${dirName}`;
try { try {
await invoke("create_directory", { path: dirPath }); await invoke("create_directory", { path: dirPath });
// Refresh the parent directory // Refresh the parent directory
@@ -285,7 +284,7 @@ function createEditorStore() {
closeTab(openTab.id); closeTab(openTab.id);
} }
// Refresh the parent directory // Refresh the parent directory
const parentPath = getParentPath(filePath); const parentPath = filePath.substring(0, filePath.lastIndexOf("/"));
await refreshDirectory(parentPath); await refreshDirectory(parentPath);
return true; return true;
} catch (error) { } catch (error) {
@@ -300,11 +299,10 @@ function createEditorStore() {
await invoke("delete_directory", { path: dirPath }); await invoke("delete_directory", { path: dirPath });
// Close any tabs that are in this directory // Close any tabs that are in this directory
const currentState = get(state); const currentState = get(state);
const normalizedDirPath = normalizePath(dirPath); const tabsToClose = currentState.tabs.filter((t) => t.filePath.startsWith(dirPath + "/"));
const tabsToClose = currentState.tabs.filter((t) => normalizePath(t.filePath).startsWith(normalizedDirPath + "/"));
tabsToClose.forEach((tab) => closeTab(tab.id)); tabsToClose.forEach((tab) => closeTab(tab.id));
// Refresh the parent directory // Refresh the parent directory
const parentPath = getParentPath(dirPath); const parentPath = dirPath.substring(0, dirPath.lastIndexOf("/"));
await refreshDirectory(parentPath); await refreshDirectory(parentPath);
return true; return true;
} catch (error) { } catch (error) {
@@ -343,8 +341,8 @@ function createEditorStore() {
} }
async function renamePath(oldPath: string, newName: string): Promise<boolean> { async function renamePath(oldPath: string, newName: string): Promise<boolean> {
const parentPath = getParentPath(oldPath); const parentPath = oldPath.substring(0, oldPath.lastIndexOf("/"));
const newPath = joinPath(parentPath, newName); const newPath = `${parentPath}/${newName}`;
try { try {
await invoke("rename_path", { oldPath, newPath }); await invoke("rename_path", { oldPath, newPath });
@@ -361,14 +359,12 @@ function createEditorStore() {
fileName: newName, fileName: newName,
}; };
} }
const normalizedOldPath = normalizePath(oldPath); if (t.filePath.startsWith(oldPath + "/")) {
const normalizedFilePath = normalizePath(t.filePath);
if (normalizedFilePath.startsWith(normalizedOldPath + "/")) {
// File is inside a renamed directory // File is inside a renamed directory
const relativePath = normalizedFilePath.substring(normalizedOldPath.length); const relativePath = t.filePath.substring(oldPath.length);
return { return {
...t, ...t,
filePath: normalizePath(newPath + relativePath), filePath: newPath + relativePath,
}; };
} }
return t; return t;
+2 -6
View File
@@ -18,7 +18,6 @@ import {
handleConnectionStatusChange, handleConnectionStatusChange,
handleNewUserMessage, handleNewUserMessage,
} from "$lib/notifications/rules"; } from "$lib/notifications/rules";
import { normalizePath } from "$lib/utils/paths";
interface StateChangePayload { interface StateChangePayload {
state: CharacterState; state: CharacterState;
@@ -291,15 +290,12 @@ export async function initializeTauriListeners() {
const cwdUnlisten = await listen<WorkingDirectoryPayload>("claude:cwd", (event) => { const cwdUnlisten = await listen<WorkingDirectoryPayload>("claude:cwd", (event) => {
const { directory, conversation_id } = event.payload; const { directory, conversation_id } = event.payload;
// Normalize path separators to forward slashes
const normalizedDirectory = normalizePath(directory);
// Store working directory for the correct conversation // Store working directory for the correct conversation
if (conversation_id) { if (conversation_id) {
claudeStore.setWorkingDirectoryForConversation(conversation_id, normalizedDirectory); claudeStore.setWorkingDirectoryForConversation(conversation_id, directory);
} else { } else {
// Fallback to active conversation if no conversation_id // Fallback to active conversation if no conversation_id
claudeStore.setWorkingDirectory(normalizedDirectory); claudeStore.setWorkingDirectory(directory);
} }
}); });
unlisteners.push(cwdUnlisten); unlisteners.push(cwdUnlisten);
-98
View File
@@ -1,98 +0,0 @@
import { describe, it, expect } from "vitest";
import { normalizePath, joinPath, getParentPath, getFilename } from "./paths";
describe("paths utilities", () => {
describe("normalizePath", () => {
it("should normalize Windows paths", () => {
expect(normalizePath("C:\\Users\\test")).toBe("C:/Users/test");
expect(normalizePath("path\\to\\file")).toBe("path/to/file");
expect(normalizePath("C:\\Users\\test\\file.txt")).toBe("C:/Users/test/file.txt");
});
it("should leave Unix paths unchanged", () => {
expect(normalizePath("/home/user")).toBe("/home/user");
expect(normalizePath("path/to/file")).toBe("path/to/file");
expect(normalizePath("/usr/local/bin")).toBe("/usr/local/bin");
});
it("should handle mixed separators", () => {
expect(normalizePath("mixed\\path/file")).toBe("mixed/path/file");
expect(normalizePath("C:\\Users/test\\mixed/path")).toBe("C:/Users/test/mixed/path");
});
it("should handle empty strings", () => {
expect(normalizePath("")).toBe("");
});
});
describe("joinPath", () => {
it("should join path segments with forward slashes", () => {
expect(joinPath("parent", "child")).toBe("parent/child");
expect(joinPath("/home", "user", "documents")).toBe("/home/user/documents");
expect(joinPath("C:", "Users", "test")).toBe("C:/Users/test");
});
it("should filter out empty segments", () => {
expect(joinPath("parent", "", "child")).toBe("parent/child");
expect(joinPath("", "home", "")).toBe("home");
expect(joinPath("", "", "")).toBe("");
});
it("should normalize mixed separators in segments", () => {
expect(joinPath("parent\\dir", "child")).toBe("parent/dir/child");
expect(joinPath("C:\\Users", "test\\file")).toBe("C:/Users/test/file");
});
});
describe("getParentPath", () => {
it("should get parent directory of Unix paths", () => {
expect(getParentPath("/home/user/file.txt")).toBe("/home/user");
expect(getParentPath("/home/user/")).toBe("/home");
expect(getParentPath("/home")).toBe("/");
expect(getParentPath("/")).toBe("/");
});
it("should get parent directory of Windows paths", () => {
expect(getParentPath("C:\\Users\\test\\file.txt")).toBe("C:/Users/test");
expect(getParentPath("C:\\Users\\test")).toBe("C:/Users");
expect(getParentPath("C:\\")).toBe("C:");
});
it("should handle relative paths", () => {
expect(getParentPath("parent/child/file.txt")).toBe("parent/child");
expect(getParentPath("file.txt")).toBe(".");
expect(getParentPath("")).toBe(".");
});
it("should handle paths with trailing slashes", () => {
expect(getParentPath("/home/user/")).toBe("/home");
expect(getParentPath("parent/child/")).toBe("parent");
});
});
describe("getFilename", () => {
it("should extract filename from Unix paths", () => {
expect(getFilename("/home/user/file.txt")).toBe("file.txt");
expect(getFilename("/usr/local/bin/node")).toBe("node");
expect(getFilename("/home/user/")).toBe("");
});
it("should extract filename from Windows paths", () => {
expect(getFilename("C:\\Users\\test\\file.txt")).toBe("file.txt");
expect(getFilename("C:\\Program Files\\app.exe")).toBe("app.exe");
expect(getFilename("D:\\folder\\")).toBe("");
});
it("should handle relative paths", () => {
expect(getFilename("parent/child/file.txt")).toBe("file.txt");
expect(getFilename("file.txt")).toBe("file.txt");
expect(getFilename("")).toBe("");
});
it("should handle paths without filename", () => {
expect(getFilename("/home/user/")).toBe("");
expect(getFilename("folder/")).toBe("");
expect(getFilename("/")).toBe("");
});
});
});
-67
View File
@@ -1,67 +0,0 @@
/**
* Normalize path separators to forward slashes for consistent handling across platforms
*/
export function normalizePath(path: string): string {
// Replace all backslashes with forward slashes
return path.replace(/\\/g, "/");
}
/**
* Join path segments with forward slashes
*/
export function joinPath(...segments: string[]): string {
// Filter out empty segments
const filtered = segments.filter((s) => s);
// Join with forward slash and normalize
return normalizePath(filtered.join("/"));
}
/**
* Get the parent directory of a path
*/
export function getParentPath(path: string): string {
let normalized = normalizePath(path);
// Remove trailing slash if present (unless it's the root)
if (normalized.endsWith("/") && normalized.length > 1) {
normalized = normalized.slice(0, -1);
}
// Handle Windows drive roots (e.g., "C:" or "C:/")
if (/^[A-Za-z]:$/.test(normalized)) {
return normalized; // Drive root has no parent
}
const lastSlash = normalized.lastIndexOf("/");
if (lastSlash === -1) {
// No slash found - could be just a filename or Windows drive letter
if (/^[A-Za-z]:/.test(normalized)) {
// It's a Windows path like "C:file.txt" - parent is "C:"
return normalized.substring(0, 2);
}
return ".";
}
if (lastSlash === 0) {
return "/";
}
// Check if parent would be a Windows drive root
const parent = normalized.substring(0, lastSlash);
if (/^[A-Za-z]:$/.test(parent)) {
return parent;
}
return parent;
}
/**
* Get the filename from a path
*/
export function getFilename(path: string): string {
const normalized = normalizePath(path);
const lastSlash = normalized.lastIndexOf("/");
if (lastSlash === -1) {
return normalized;
}
return normalized.substring(lastSlash + 1);
}