diff --git a/src/main.rs b/src/main.rs index fe9a437..339fd6f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -268,9 +268,10 @@ fn run(terminal: &mut tui::Tui, app: &mut App) -> Result<()> { fn resume_session(session: &session::Session) -> Result<()> { use std::os::unix::process::CommandExt; - // Change to conversation's working directory - if !session.cwd.is_empty() { - let _ = std::env::set_current_dir(&session.cwd); + // Change to the appropriate directory for resuming + let resume_cwd = session.resume_cwd(); + if !resume_cwd.is_empty() { + let _ = std::env::set_current_dir(&resume_cwd); } let (program, args) = session.resume_command(); @@ -284,9 +285,10 @@ fn resume_session(session: &session::Session) -> Result<()> { #[cfg(not(unix))] fn resume_session(session: &session::Session) -> Result<()> { - // Change to conversation's working directory - if !session.cwd.is_empty() { - let _ = std::env::set_current_dir(&session.cwd); + // Change to the appropriate directory for resuming + let resume_cwd = session.resume_cwd(); + if !resume_cwd.is_empty() { + let _ = std::env::set_current_dir(&resume_cwd); } let (program, args) = session.resume_command(); diff --git a/src/parser/claude.rs b/src/parser/claude.rs index ce429a4..de5e89c 100644 --- a/src/parser/claude.rs +++ b/src/parser/claude.rs @@ -12,8 +12,6 @@ use super::{join_consecutive_messages, SessionParser}; struct ClaudeLine { #[serde(rename = "type")] entry_type: String, - #[serde(rename = "sessionId")] - session_id: Option, cwd: Option, #[serde(rename = "gitBranch")] git_branch: Option, @@ -50,7 +48,13 @@ impl SessionParser for ClaudeParser { let file = File::open(path).context("Failed to open file")?; let reader = BufReader::with_capacity(64 * 1024, file); - let mut session_id: Option = None; + // Use filename as session ID (what Claude Code expects for --resume) + let session_id = path + .file_stem() + .and_then(|s| s.to_str()) + .unwrap_or("unknown") + .to_string(); + let mut cwd: Option = None; let mut git_branch: Option = None; let mut latest_timestamp: Option> = None; @@ -83,9 +87,6 @@ impl SessionParser for ClaudeParser { } // Extract session metadata from the first valid message - if session_id.is_none() { - session_id = entry.session_id.clone(); - } if cwd.is_none() { cwd = entry.cwd.clone(); } @@ -135,14 +136,6 @@ impl SessionParser for ClaudeParser { } } - // Fall back to filename for session ID if not found - let session_id = session_id.unwrap_or_else(|| { - path.file_stem() - .and_then(|s| s.to_str()) - .unwrap_or("unknown") - .to_string() - }); - Ok(Session { id: session_id, source: SessionSource::ClaudeCode, diff --git a/src/session.rs b/src/session.rs index a07e6c1..7ec7f7c 100644 --- a/src/session.rs +++ b/src/session.rs @@ -96,6 +96,35 @@ impl Session { .unwrap_or(&self.cwd) } + /// Get the directory to cd into before resuming the session. + /// For Claude Code: decodes the project folder from file_path since sessions + /// are stored in project-specific folders, not the cwd recorded in messages. + /// For other sources: uses the cwd field. + pub fn resume_cwd(&self) -> String { + match self.source { + SessionSource::ClaudeCode => { + // Extract project folder from file_path: ~/.claude/projects//session.jsonl + // The project folder encodes the original cwd: + // "-Users-bob--config-nvim" -> "/Users/bob/.config/nvim" + if let Some(project_dir) = self.file_path.parent() { + if let Some(project_name) = project_dir.file_name().and_then(|s| s.to_str()) { + // Decode: "--" -> "/." (hidden dir), "-" -> "/" + let decoded = project_name + .replace("--", "\x00") // Temporarily mark hidden dirs + .replace('-', "/") + .replace('\x00', "/."); + if std::path::Path::new(&decoded).exists() { + return decoded; + } + } + } + // Fall back to cwd if decoding fails + self.cwd.clone() + } + _ => self.cwd.clone(), + } + } + /// Get the resume command for this session /// Checks RECALL_CLAUDE_CMD / RECALL_CODEX_CMD / RECALL_FACTORY_CMD env vars first, falls back to defaults /// Env var format: "program arg1 arg2 {id}" where {id} is replaced with session ID diff --git a/tests/integration.rs b/tests/integration.rs index fd7f386..af392b9 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -167,7 +167,7 @@ fn test_search_finds_matching_content() { assert!(!app.results.is_empty(), "Should find results for 'hello'"); assert!( - app.results.iter().any(|r| r.session.id == "test-claude-123"), + app.results.iter().any(|r| r.session.id == "session"), "Should find Claude session" ); } @@ -521,7 +521,7 @@ fn test_cli_search_finds_fixture_content() { // Should find the Claude fixture session assert!( - results.iter().any(|r| r["session_id"] == "test-claude-123"), + results.iter().any(|r| r["session_id"] == "session"), "Should find Claude fixture session" ); } @@ -610,7 +610,7 @@ fn test_cli_read_returns_session() { let temp_dir = setup_test_env(); let (stdout, _stderr, success) = run_cli( - &["read", "test-claude-123"], + &["read", "session"], temp_dir.path(), ); @@ -619,7 +619,7 @@ fn test_cli_read_returns_session() { let json: serde_json::Value = serde_json::from_str(&stdout) .expect("Output should be valid JSON"); - assert_eq!(json["session_id"], "test-claude-123"); + assert_eq!(json["session_id"], "session"); assert_eq!(json["source"], "claude"); assert!(json["messages"].is_array()); assert!(!json["messages"].as_array().unwrap().is_empty());