rab/agent/
session_repo.rs1use crate::agent::session::{
2 SessionInfo, delete_session as delete_session_file, fork_session, load_session_info,
3};
4use std::path::{Path, PathBuf};
5
6pub trait SessionRepo {
10 fn list(
13 &self,
14 session_dir: &Path,
15 filter_cwd: Option<&Path>,
16 progress: Option<&dyn Fn(usize, usize)>,
17 ) -> Vec<SessionInfo>;
18
19 fn list_all(&self, progress: Option<&dyn Fn(usize, usize)>) -> Vec<SessionInfo>;
21
22 fn delete(&self, path: &Path) -> std::io::Result<()>;
24
25 fn fork(
28 &self,
29 source_path: &Path,
30 target_dir: &Path,
31 entry_id: Option<&str>,
32 position: Option<&str>,
33 ) -> std::io::Result<String>;
34
35 fn load_info(&self, path: &Path) -> Option<SessionInfo>;
37}
38
39pub struct DefaultSessionRepo;
43
44impl Default for DefaultSessionRepo {
45 fn default() -> Self {
46 Self
47 }
48}
49
50impl DefaultSessionRepo {
51 pub fn new() -> Self {
52 Self
53 }
54}
55
56impl SessionRepo for DefaultSessionRepo {
57 fn list(
58 &self,
59 session_dir: &Path,
60 filter_cwd: Option<&Path>,
61 progress: Option<&dyn Fn(usize, usize)>,
62 ) -> Vec<SessionInfo> {
63 list_sessions_with_progress(session_dir, filter_cwd, progress, 1)
64 }
65
66 fn list_all(&self, progress: Option<&dyn Fn(usize, usize)>) -> Vec<SessionInfo> {
67 let dir = directories::BaseDirs::new()
68 .map(|d| d.home_dir().join(".rab").join("sessions"))
69 .unwrap_or_else(|| PathBuf::from("/tmp/.rab/sessions"));
70
71 let mut all_sessions: Vec<SessionInfo> = Vec::new();
72
73 let mut dirs = vec![dir.clone()];
75 if let Ok(read_dir) = std::fs::read_dir(&dir) {
76 for entry in read_dir.flatten() {
77 let path = entry.path();
78 if path.is_dir() {
79 dirs.push(path);
80 }
81 }
82 }
83
84 let total_dirs = dirs.len();
85 let mut loaded = 0;
86
87 for session_dir in &dirs {
88 let sessions = list_sessions_with_progress(session_dir, None, progress, 1);
89 loaded += 1;
90 if let Some(ref cb) = progress {
91 cb(loaded, total_dirs);
92 }
93 all_sessions.extend(sessions);
94 }
95
96 all_sessions.sort_by_key(|b| std::cmp::Reverse(b.created));
97 all_sessions
98 }
99
100 fn delete(&self, path: &Path) -> std::io::Result<()> {
101 delete_session_file(path)
102 }
103
104 fn fork(
105 &self,
106 source_path: &Path,
107 target_dir: &Path,
108 entry_id: Option<&str>,
109 position: Option<&str>,
110 ) -> std::io::Result<String> {
111 fork_session(source_path, target_dir, entry_id, position)
112 }
113
114 fn load_info(&self, path: &Path) -> Option<SessionInfo> {
115 load_session_info(path)
116 }
117}
118
119fn list_sessions_with_progress(
124 session_dir: &Path,
125 filter_cwd: Option<&Path>,
126 progress: Option<&dyn Fn(usize, usize)>,
127 _concurrency: usize,
128) -> Vec<SessionInfo> {
129 let dir = match std::fs::read_dir(session_dir) {
130 Ok(d) => d,
131 Err(_) => return vec![],
132 };
133
134 let file_paths: Vec<PathBuf> = dir
136 .flatten()
137 .filter(|e| e.path().extension().is_some_and(|ext| ext == "jsonl"))
138 .map(|e| e.path())
139 .collect();
140
141 let total = file_paths.len();
142 let mut sessions: Vec<SessionInfo> = Vec::with_capacity(total);
143 let mut loaded = 0;
144
145 for path in &file_paths {
148 let header = crate::agent::session::read_session_header(path);
150 if let Some(ref h) = header
151 && let Some(filter) = filter_cwd
152 && h.cwd != filter.to_string_lossy().as_ref()
153 {
154 loaded += 1;
155 if let Some(ref cb) = progress {
156 cb(loaded, total);
157 }
158 continue;
159 }
160
161 if let Some(info) = load_session_info(path) {
163 sessions.push(info);
164 }
165 loaded += 1;
166 if let Some(ref cb) = progress {
167 cb(loaded, total);
168 }
169 }
170
171 sessions.sort_by_key(|b| std::cmp::Reverse(b.created));
172 sessions
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178 use tempfile::TempDir;
179
180 #[test]
181 fn test_list_empty_dir() {
182 let repo = DefaultSessionRepo::new();
183 let tmp = TempDir::new().unwrap();
184 let sessions = repo.list(tmp.path(), None, None);
185 assert!(sessions.is_empty());
186 }
187}