diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 3868081..d970133 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -105,6 +105,51 @@ pub async fn get_usage_stats( manager.get_usage_stats(&conversation_id) } +#[tauri::command] +pub async fn validate_directory(path: String, current_dir: Option) -> Result { + use std::path::Path; + + let path = Path::new(&path); + + // 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() + } else { + home_path.join(path.strip_prefix("~").unwrap()) + } + } 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) + } 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())); + } + + 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] pub async fn load_saved_achievements(app: AppHandle) -> Result, String> { use chrono::Utc; diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs index 56b79e1..5ab84fc 100644 --- a/src-tauri/src/config.rs +++ b/src-tauri/src/config.rs @@ -22,6 +22,9 @@ pub struct ClaudeStartOptions { #[serde(default)] pub skip_greeting: bool, + + #[serde(default)] + pub resume_session_id: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 0abb223..b2b0a18 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -53,6 +53,7 @@ pub fn run() { send_notify_send, send_wsl_notification, send_vbs_notification, + validate_directory, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src-tauri/src/wsl_bridge.rs b/src-tauri/src/wsl_bridge.rs index 61d33c1..1477eb1 100644 --- a/src-tauri/src/wsl_bridge.rs +++ b/src-tauri/src/wsl_bridge.rs @@ -195,6 +195,13 @@ impl WslBridge { cmd.args(["--mcp-config", mcp_path]); } + // Add resume flag if session ID provided + if let Some(ref session_id) = options.resume_session_id { + if !session_id.is_empty() { + cmd.args(["--resume", session_id]); + } + } + cmd.current_dir(working_dir); // Set API key as environment variable if specified @@ -251,6 +258,13 @@ impl WslBridge { claude_cmd.push_str(&format!(" --mcp-config '{}'", mcp_path)); } + // Add resume flag if session ID provided + if let Some(ref session_id) = options.resume_session_id { + if !session_id.is_empty() { + claude_cmd.push_str(&format!(" --resume '{}'", session_id)); + } + } + // Use bash -lc to load login profile (ensures PATH includes claude) cmd.args(["-e", "bash", "-lc", &claude_cmd]); diff --git a/src/lib/commands/slashCommands.ts b/src/lib/commands/slashCommands.ts index cfab588..4066f9c 100644 --- a/src/lib/commands/slashCommands.ts +++ b/src/lib/commands/slashCommands.ts @@ -11,6 +11,71 @@ export interface SlashCommand { execute: (args: string) => Promise | void; } +async function changeDirectory(path: string): Promise { + const conversationId = get(claudeStore.activeConversationId); + if (!conversationId) { + claudeStore.addLine("error", "No active conversation"); + return; + } + + if (!path.trim()) { + const currentDir = get(claudeStore.currentWorkingDirectory); + claudeStore.addLine("system", `Current directory: ${currentDir}`); + return; + } + + try { + characterState.setState("thinking"); + claudeStore.addLine("system", `Changing directory to: ${path}`); + + const currentDir = get(claudeStore.currentWorkingDirectory); + const validatedPath = await invoke("validate_directory", { path, currentDir }); + + // Capture conversation history before disconnecting + const conversationHistory = claudeStore.getConversationHistory(); + + await invoke("stop_claude", { conversationId }); + + // Wait for clean shutdown + await new Promise((resolve) => setTimeout(resolve, 500)); + + claudeStore.setWorkingDirectory(validatedPath); + + setSkipNextGreeting(true); + + await invoke("start_claude", { + conversationId, + options: { + working_dir: validatedPath, + }, + }); + + // Wait for connection to establish + await new Promise((resolve) => setTimeout(resolve, 1000)); + + // Restore context if there was conversation history + if (conversationHistory) { + const contextMessage = `[CONTEXT RESTORATION] +I just changed the working directory from ${currentDir} to ${validatedPath}. Here's our conversation so far: + +${conversationHistory} + +Please continue where we left off. You are now operating in the new directory.`; + + await invoke("send_prompt", { + conversationId, + message: contextMessage, + }); + } + + claudeStore.addLine("system", `Changed directory to: ${validatedPath}`); + characterState.setState("idle"); + } catch (error) { + claudeStore.addLine("error", `Failed to change directory: ${error}`); + characterState.setTemporaryState("error", 3000); + } +} + async function startNewConversation(): Promise { const conversationId = get(claudeStore.activeConversationId); if (!conversationId) { @@ -48,6 +113,12 @@ async function startNewConversation(): Promise { } export const slashCommands: SlashCommand[] = [ + { + name: "cd", + description: "Change the working directory", + usage: "/cd ", + execute: changeDirectory, + }, { name: "clear", description: "Clear the terminal display (keeps conversation context)",