use crate::error::{HindsightError, Result};
use std::fs;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
pub struct SessionFile {
pub path: PathBuf,
pub session_id: String,
pub project_name: String,
pub file_size: u64,
pub created_at: i64,
pub modified_at: i64,
pub has_subagents: bool,
pub model: Option<String>,
pub error_count: usize,
pub first_message: Option<String>,
pub source_dir: String,
pub subagent_models: Option<String>,
}
pub fn discover_sessions() -> Result<Vec<SessionFile>> {
let home = dirs::home_dir()
.ok_or_else(|| HindsightError::Config("Could not determine home directory".to_string()))?;
let config = crate::config::Config::load().unwrap_or_default();
let claude_dirs: Vec<(PathBuf, String)> = config
.paths
.claude_dirs
.iter()
.map(|d| {
let expanded = if let Some(stripped) = d.path.strip_prefix("~/") {
home.join(stripped)
} else {
PathBuf::from(&d.path)
};
(expanded, d.path.clone())
})
.filter(|(p, _)| p.exists())
.collect();
let mut sessions = Vec::new();
for (claude_dir, source_dir) in &claude_dirs {
if !claude_dir.exists() {
continue;
}
for project_entry in fs::read_dir(claude_dir)? {
let project_entry = project_entry?;
let project_path = project_entry.path();
if !project_path.is_dir() {
continue;
}
let project_name = decode_project_name(&project_path);
for file_entry in fs::read_dir(&project_path)? {
let file_entry = file_entry?;
let file_path = file_entry.path();
if file_path.is_file()
&& file_path.extension().and_then(|s| s.to_str()) == Some("jsonl")
{
let metadata = fs::metadata(&file_path)?;
let session_id = file_path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("unknown")
.to_string();
let modified_at = metadata
.modified()?
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0);
let subagents_dir = project_path.join(&session_id).join("subagents");
let has_subagents = subagents_dir.exists() && subagents_dir.is_dir();
sessions.push(SessionFile {
path: file_path,
session_id,
project_name: project_name.clone(),
file_size: metadata.len(),
created_at: modified_at, modified_at,
has_subagents,
model: None,
error_count: 0,
first_message: None,
source_dir: source_dir.clone(),
subagent_models: None,
});
}
}
}
}
if sessions.is_empty() {
return Err(HindsightError::NoSessionsFound);
}
sessions.sort_by(|a, b| b.modified_at.cmp(&a.modified_at));
Ok(sessions)
}
fn decode_project_name(path: &Path) -> String {
path.file_name()
.and_then(|s| s.to_str())
.map(|s| {
if s.starts_with('-') {
s.split('-').next_back().unwrap_or(s).to_string()
} else {
s.to_string()
}
})
.unwrap_or_else(|| "unknown".to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_decode_project_name() {
let path = Path::new("-Users-ediazestrada-Documents-Projects-experiment");
assert_eq!(decode_project_name(path), "experiment");
let path = Path::new("my-project");
assert_eq!(decode_project_name(path), "my-project");
}
}