use std::path::{Path, PathBuf};
mod claude;
mod codex;
const TITLE_HINT_MAX: usize = 80;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DiscoveredSession {
pub agent_id: String,
pub external_session_id: String,
pub title_hint: Option<String>,
pub last_active_at: i64,
pub source_path: PathBuf,
}
pub fn scan_for_cwd(home: &Path, cwd: &Path, agents: &[String]) -> Vec<DiscoveredSession> {
let mut out = Vec::new();
for agent_id in agents {
match agent_id.as_str() {
"claude-code" => out.extend(claude::scan(home, cwd)),
"codex" => out.extend(codex::scan(home, cwd)),
_ => {}
}
}
out
}
pub(super) fn truncate_hint(s: &str) -> Option<String> {
let trimmed: String = s.split_whitespace().collect::<Vec<_>>().join(" ");
if trimmed.is_empty() {
return None;
}
if trimmed.chars().count() <= TITLE_HINT_MAX {
Some(trimmed)
} else {
let mut out: String = trimmed.chars().take(TITLE_HINT_MAX - 1).collect();
out.push('…');
Some(out)
}
}
pub(super) fn mtime_unix_seconds(path: &Path) -> i64 {
let Ok(meta) = std::fs::metadata(path) else {
return 0;
};
let Ok(modified) = meta.modified() else {
return 0;
};
match modified.duration_since(std::time::UNIX_EPOCH) {
Ok(d) => d.as_secs() as i64,
Err(_) => 0,
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn truncate_hint_short_text_passes_through() {
assert_eq!(
truncate_hint("refactor router"),
Some("refactor router".to_string())
);
}
#[test]
fn truncate_hint_collapses_whitespace() {
assert_eq!(
truncate_hint(" hello\n\nworld "),
Some("hello world".to_string())
);
}
#[test]
fn truncate_hint_blank_returns_none() {
assert_eq!(truncate_hint(""), None);
assert_eq!(truncate_hint(" \n\t"), None);
}
#[test]
fn truncate_hint_long_text_ellipsis() {
let long = "a".repeat(TITLE_HINT_MAX + 20);
let hint = truncate_hint(&long).expect("non-empty");
assert_eq!(hint.chars().count(), TITLE_HINT_MAX);
assert!(hint.ends_with('…'));
}
#[test]
fn scan_unknown_agent_returns_empty() {
let home = TempDir::new().expect("tempdir");
let cwd = std::path::PathBuf::from("/Users/x/proj");
let result = scan_for_cwd(home.path(), &cwd, &["mystery-agent".to_string()]);
assert!(result.is_empty());
}
#[test]
fn scan_with_empty_home_returns_empty() {
let home = TempDir::new().expect("tempdir");
let cwd = std::path::PathBuf::from("/Users/x/proj");
let result = scan_for_cwd(
home.path(),
&cwd,
&["claude-code".to_string(), "codex".to_string()],
);
assert!(result.is_empty());
}
}