use crate::paths::Paths;
use std::path::{Path, PathBuf};
#[derive(Clone, Debug, PartialEq)]
pub struct Found {
pub account: String,
pub session_id: String,
pub project: String,
pub modified: u64,
pub config_dir: PathBuf,
}
impl Found {
pub fn resume_command(&self) -> String {
format!(
"CLAUDE_CONFIG_DIR={} claude -r {}",
self.config_dir.display(),
self.session_id
)
}
}
pub fn stores(paths: &Paths) -> Vec<(String, PathBuf)> {
let mut out: Vec<(String, PathBuf)> = crate::slots::Slots::open_for(paths, "claude-code")
.map(|s| {
s.list()
.into_iter()
.map(|r| (r.name, r.config_dir))
.collect()
})
.unwrap_or_default();
let bare = paths.claude_dir().to_path_buf();
if !out.iter().any(|(_, d)| d == &bare) {
out.push(("(default ~/.claude)".to_string(), bare));
}
out
}
pub fn find(paths: &Paths, project_filter: Option<&str>, limit: usize) -> Vec<Found> {
let mut out = Vec::new();
for (account, dir) in stores(paths) {
collect_store(&account, &dir, project_filter, &mut out);
}
out.sort_by_key(|f| std::cmp::Reverse(f.modified));
out.truncate(limit);
out
}
fn collect_store(account: &str, dir: &Path, filter: Option<&str>, out: &mut Vec<Found>) {
let Ok(projects) = std::fs::read_dir(dir.join("projects")) else {
return;
};
for p in projects.flatten() {
let project = p.file_name().to_string_lossy().to_string();
if filter.is_some_and(|f| !matches_project(&project, f)) {
continue;
}
let Ok(files) = std::fs::read_dir(p.path()) else {
continue;
};
for f in files.flatten() {
let path = f.path();
if path.extension().is_none_or(|e| e != "jsonl") {
continue;
}
let Some(session_id) = path.file_stem().map(|s| s.to_string_lossy().to_string()) else {
continue;
};
out.push(Found {
account: account.to_string(),
session_id,
project: project.clone(),
modified: mtime_secs(&path),
config_dir: dir.to_path_buf(),
});
}
}
}
fn matches_project(encoded: &str, needle: &str) -> bool {
let hay = encoded.to_ascii_lowercase();
let n = needle.to_ascii_lowercase().replace(['/', '_'], "-");
hay.contains(n.trim_matches('-'))
}
fn mtime_secs(p: &Path) -> u64 {
std::fs::metadata(p)
.and_then(|m| m.modified())
.ok()
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
.map(|d| d.as_secs())
.unwrap_or(0)
}
#[cfg(test)]
mod tests {
use super::*;
fn write_session(dir: &Path, project: &str, id: &str) {
let d = dir.join("projects").join(project);
std::fs::create_dir_all(&d).unwrap();
std::fs::write(d.join(format!("{id}.jsonl")), b"{}\n").unwrap();
}
#[test]
fn a_conversation_is_found_in_whichever_account_holds_it() {
let root = tempfile::tempdir().unwrap();
let paths = Paths::rooted(root.path());
write_session(paths.claude_dir(), "-Users-me-Project-ROS", "sess-ros");
let slot = root.path().join("company");
write_session(&slot, "-Users-me-other", "sess-other");
std::fs::create_dir_all(paths.store_dir()).unwrap();
std::fs::write(
paths.store_dir().join("slots.json"),
serde_json::to_vec(&serde_json::json!([{
"name": "work", "id": "i1", "config_dir": slot, "adopted": true,
"tool": "claude-code"
}]))
.unwrap(),
)
.unwrap();
let all = find(&paths, None, 10);
assert_eq!(all.len(), 2, "both stores are searched: {all:?}");
let ros = find(&paths, Some("Project/ROS"), 10);
assert_eq!(ros.len(), 1, "a path spelled the human way still matches");
assert_eq!(ros[0].session_id, "sess-ros");
assert_eq!(
ros[0].account, "(default ~/.claude)",
"and it names the account whose store has it"
);
let cmd = ros[0].resume_command();
assert!(cmd.contains("CLAUDE_CONFIG_DIR="), "{cmd}");
assert!(
!cmd.contains('~'),
"a path meant to be RUN cannot be tilde-shortened: {cmd}"
);
assert!(
cmd.contains(&paths.claude_dir().display().to_string()),
"it names the store in full: {cmd}"
);
assert!(cmd.ends_with("claude -r sess-ros"), "{cmd}");
let other = find(&paths, Some("other"), 10);
assert_eq!(other.len(), 1);
assert_eq!(other[0].account, "work");
}
#[test]
fn a_store_with_no_conversations_is_simply_empty() {
let root = tempfile::tempdir().unwrap();
let paths = Paths::rooted(root.path());
assert!(find(&paths, None, 10).is_empty(), "no stores, no results");
std::fs::create_dir_all(paths.claude_dir().join("projects")).unwrap();
assert!(find(&paths, None, 10).is_empty());
let d = paths.claude_dir().join("projects").join("-p");
std::fs::create_dir_all(&d).unwrap();
std::fs::write(d.join("notes.txt"), b"x").unwrap();
assert!(find(&paths, None, 10).is_empty());
}
}