1use crate::error::Result;
4use crate::paths::PathResolver;
5use crate::reader::EventReader;
6use crate::types::{Session, SessionMetadata};
7use std::path::PathBuf;
8
9#[derive(Debug, Clone, Default)]
10pub struct ConvoIO {
11 resolver: PathResolver,
12}
13
14impl ConvoIO {
15 pub fn new() -> Self {
16 Self {
17 resolver: PathResolver::new(),
18 }
19 }
20
21 pub fn with_resolver(resolver: PathResolver) -> Self {
22 Self { resolver }
23 }
24
25 pub fn resolver(&self) -> &PathResolver {
26 &self.resolver
27 }
28
29 pub fn exists(&self) -> bool {
30 self.resolver.exists()
31 }
32
33 pub fn copilot_dir_path(&self) -> Result<PathBuf> {
34 self.resolver.copilot_dir()
35 }
36
37 pub fn list_session_dirs(&self) -> Result<Vec<PathBuf>> {
39 self.resolver.list_session_dirs()
40 }
41
42 pub fn list_sessions(&self) -> Result<Vec<SessionMetadata>> {
44 let dirs = self.list_session_dirs()?;
45 let mut metas = Vec::with_capacity(dirs.len());
46 for dir in dirs {
47 match self.read_metadata(&dir) {
48 Ok(m) => metas.push(m),
49 Err(e) => eprintln!("Warning: failed to read {}: {}", dir.display(), e),
50 }
51 }
52 metas.sort_by_key(|m| std::cmp::Reverse(m.last_activity));
53 Ok(metas)
54 }
55
56 pub fn read_session(&self, session_id: &str) -> Result<Session> {
58 let dir = self.resolver.find_session_dir(session_id)?;
59 EventReader::read_session_dir(&dir)
60 }
61
62 pub fn read_session_dir<P: AsRef<std::path::Path>>(&self, dir: P) -> Result<Session> {
64 EventReader::read_session_dir(dir)
65 }
66
67 pub fn read_metadata<P: AsRef<std::path::Path>>(&self, dir: P) -> Result<SessionMetadata> {
70 let session = EventReader::read_session_dir(dir)?;
71 Ok(SessionMetadata {
72 id: session.id.clone(),
73 dir_path: session.dir_path.clone(),
74 started_at: session.started_at(),
75 last_activity: session.last_activity(),
76 cwd: session.cwd(),
77 version: session.version(),
78 first_user_message: session.first_user_text(),
79 line_count: session.lines.len(),
80 })
81 }
82
83 pub fn session_exists(&self, session_id: &str) -> bool {
84 self.resolver.find_session_dir(session_id).is_ok()
85 }
86}
87
88#[cfg(test)]
89mod tests {
90 use super::*;
91 use std::fs;
92 use tempfile::TempDir;
93
94 fn setup() -> (TempDir, ConvoIO) {
95 let temp = TempDir::new().unwrap();
96 let copilot = temp.path().join(".copilot");
97 let dir = copilot.join("session-state").join("sess-abc");
98 fs::create_dir_all(&dir).unwrap();
99 let body = [
100 r#"{"type":"session.start","timestamp":"2026-06-30T10:00:00.000Z","data":{"copilotVersion":"1.0.66","context":{"cwd":"/tmp/proj"}}}"#,
101 r#"{"type":"user.message","timestamp":"2026-06-30T10:00:01.000Z","data":{"content":"hi there"}}"#,
102 ]
103 .join("\n");
104 fs::write(dir.join("events.jsonl"), body).unwrap();
105 let resolver = PathResolver::new().with_copilot_dir(&copilot);
106 (temp, ConvoIO::with_resolver(resolver))
107 }
108
109 #[test]
110 fn lists_sessions_with_metadata() {
111 let (_t, io) = setup();
112 let sessions = io.list_sessions().unwrap();
113 assert_eq!(sessions.len(), 1);
114 assert_eq!(sessions[0].id, "sess-abc");
115 assert_eq!(sessions[0].cwd.as_deref(), Some("/tmp/proj"));
116 assert_eq!(sessions[0].version.as_deref(), Some("1.0.66"));
117 assert_eq!(sessions[0].first_user_message.as_deref(), Some("hi there"));
118 assert_eq!(sessions[0].line_count, 2);
119 }
120
121 #[test]
122 fn reads_session_by_id_and_prefix() {
123 let (_t, io) = setup();
124 assert_eq!(io.read_session("sess-abc").unwrap().lines.len(), 2);
125 assert_eq!(io.read_session("sess").unwrap().id, "sess-abc");
126 }
127
128 #[test]
129 fn session_exists_reflects_disk() {
130 let (_t, io) = setup();
131 assert!(io.session_exists("sess-abc"));
132 assert!(!io.session_exists("nope"));
133 }
134}