1use crate::SaorsaAgentError;
4use crate::session::{SessionId, SessionMetadata, SessionNode, SessionStorage};
5use chrono::{DateTime, Utc};
6use std::collections::HashMap;
7use std::str::FromStr;
8
9#[derive(Debug, Clone)]
11pub struct TreeNode {
12 pub id: SessionId,
14 pub metadata: SessionMetadata,
16 pub node: SessionNode,
18 pub children: Vec<TreeNode>,
20 pub message_count: usize,
22}
23
24#[derive(Debug, Clone, Default)]
26pub struct TreeRenderOptions {
27 pub highlight_id: Option<SessionId>,
29 pub after_date: Option<DateTime<Utc>>,
31 pub before_date: Option<DateTime<Utc>>,
33 pub tags: Vec<String>,
35}
36
37pub fn build_session_tree(storage: &SessionStorage) -> Result<Vec<TreeNode>, SaorsaAgentError> {
39 let sessions = list_all_sessions_with_metadata(storage)?;
41
42 if sessions.is_empty() {
43 return Ok(Vec::new());
44 }
45
46 let mut session_map: HashMap<SessionId, (SessionMetadata, SessionNode, usize)> = HashMap::new();
48
49 for (id, metadata) in sessions {
50 let node = storage
51 .load_tree(&id)
52 .unwrap_or_else(|_| SessionNode::new_root(id));
53
54 let message_count = storage.load_messages(&id).map(|m| m.len()).unwrap_or(0);
55
56 session_map.insert(id, (metadata, node, message_count));
57 }
58
59 let roots: Vec<SessionId> = session_map
61 .iter()
62 .filter(|(_, (_, node, _))| node.is_root())
63 .map(|(id, _)| *id)
64 .collect();
65
66 let mut tree_nodes = Vec::new();
68 for root_id in roots {
69 if let Some(tree_node) = build_tree_node_recursive(root_id, &session_map) {
70 tree_nodes.push(tree_node);
71 }
72 }
73
74 Ok(tree_nodes)
75}
76
77fn build_tree_node_recursive(
79 id: SessionId,
80 session_map: &HashMap<SessionId, (SessionMetadata, SessionNode, usize)>,
81) -> Option<TreeNode> {
82 let (metadata, node, message_count) = session_map.get(&id)?.clone();
83
84 let mut children = Vec::new();
85 for child_id in &node.child_ids {
86 if let Some(child_node) = build_tree_node_recursive(*child_id, session_map) {
87 children.push(child_node);
88 }
89 }
90
91 Some(TreeNode {
92 id,
93 metadata,
94 node,
95 children,
96 message_count,
97 })
98}
99
100pub fn render_tree(
102 nodes: &[TreeNode],
103 options: &TreeRenderOptions,
104) -> Result<String, SaorsaAgentError> {
105 if nodes.is_empty() {
106 return Ok("No sessions found. Start a conversation to create one.".to_string());
107 }
108
109 let mut output = String::new();
110 output.push_str("Session Tree\n");
111 output.push_str("────────────\n\n");
112
113 for (i, node) in nodes.iter().enumerate() {
114 let is_last = i == nodes.len() - 1;
115 render_node_recursive(node, "", is_last, options, &mut output);
116 }
117
118 Ok(output)
119}
120
121fn render_node_recursive(
123 node: &TreeNode,
124 prefix: &str,
125 is_last: bool,
126 options: &TreeRenderOptions,
127 output: &mut String,
128) {
129 if let Some(after) = options.after_date
131 && node.metadata.last_active < after
132 {
133 return;
134 }
135
136 if let Some(before) = options.before_date
137 && node.metadata.last_active > before
138 {
139 return;
140 }
141
142 if !options.tags.is_empty() {
143 let has_tag = options
144 .tags
145 .iter()
146 .any(|tag| node.metadata.tags.contains(tag));
147 if !has_tag {
148 return;
149 }
150 }
151
152 let connector = if is_last { "└──" } else { "├──" };
154
155 let highlight = if let Some(highlight_id) = options.highlight_id {
156 if highlight_id == node.id { "➤ " } else { "" }
157 } else {
158 ""
159 };
160
161 let title = node.metadata.title.as_deref().unwrap_or("(untitled)");
162
163 let last_active = node.metadata.last_active.format("%Y-%m-%d %H:%M");
164
165 output.push_str(&format!(
166 "{}{} {}{} │ {} │ {} msgs │ {}\n",
167 prefix,
168 connector,
169 highlight,
170 node.id.prefix(),
171 title,
172 node.message_count,
173 last_active
174 ));
175
176 let child_prefix = if is_last {
178 format!("{} ", prefix)
179 } else {
180 format!("{}│ ", prefix)
181 };
182
183 for (i, child) in node.children.iter().enumerate() {
184 let child_is_last = i == node.children.len() - 1;
185 render_node_recursive(child, &child_prefix, child_is_last, options, output);
186 }
187}
188
189pub fn find_in_tree(nodes: &[TreeNode], target_id: SessionId) -> Option<TreeNode> {
191 for node in nodes {
192 if node.id == target_id {
193 return Some(node.clone());
194 }
195
196 if let Some(found) = find_in_tree(&node.children, target_id) {
197 return Some(found);
198 }
199 }
200
201 None
202}
203
204fn list_all_sessions_with_metadata(
206 storage: &SessionStorage,
207) -> Result<Vec<(SessionId, SessionMetadata)>, SaorsaAgentError> {
208 let base_path = storage.base_path();
209
210 if !base_path.exists() {
211 return Ok(Vec::new());
212 }
213
214 let entries = std::fs::read_dir(base_path).map_err(|e| {
215 SaorsaAgentError::Session(format!("Failed to read sessions directory: {}", e))
216 })?;
217
218 let mut sessions = Vec::new();
219
220 for entry in entries {
221 let entry = entry.map_err(|e| {
222 SaorsaAgentError::Session(format!("Failed to read directory entry: {}", e))
223 })?;
224
225 let path = entry.path();
226 if path.is_dir()
227 && let Some(dir_name) = path.file_name().and_then(|s| s.to_str())
228 && let Ok(session_id) = SessionId::from_str(dir_name)
229 && let Ok(metadata) = storage.load_manifest(&session_id)
230 {
231 sessions.push((session_id, metadata));
232 }
233 }
234
235 Ok(sessions)
236}
237
238#[cfg(test)]
239mod tests {
240 use super::*;
241 use tempfile::TempDir;
242
243 fn test_storage() -> (TempDir, SessionStorage) {
244 let temp_dir = match TempDir::new() {
245 Ok(dir) => dir,
246 Err(_) => panic!("Failed to create temp dir for test"),
247 };
248 let storage = SessionStorage::with_base_path(temp_dir.path().to_path_buf());
249 (temp_dir, storage)
250 }
251
252 #[test]
253 fn test_empty_tree() {
254 let (_temp, storage) = test_storage();
255 let tree = build_session_tree(&storage);
256 assert!(tree.is_ok());
257 match tree {
258 Ok(nodes) => assert!(nodes.is_empty()),
259 Err(_) => unreachable!(),
260 }
261 }
262
263 #[test]
264 fn test_single_session_tree() {
265 let (_temp, storage) = test_storage();
266
267 let id = SessionId::new();
268 let metadata = SessionMetadata::new();
269 let node = SessionNode::new_root(id);
270
271 assert!(storage.save_manifest(&id, &metadata).is_ok());
272 assert!(storage.save_tree(&id, &node).is_ok());
273
274 let tree = build_session_tree(&storage);
275 assert!(tree.is_ok());
276 match tree {
277 Ok(nodes) => {
278 assert!(nodes.len() == 1);
279 assert!(nodes[0].id == id);
280 }
281 Err(_) => unreachable!(),
282 }
283 }
284
285 #[test]
286 fn test_render_empty_tree() {
287 let nodes = Vec::new();
288 let options = TreeRenderOptions::default();
289 let result = render_tree(&nodes, &options);
290 assert!(result.is_ok());
291 match result {
292 Ok(output) => {
293 assert!(output.contains("No sessions found"));
294 }
295 Err(_) => unreachable!(),
296 }
297 }
298
299 #[test]
300 fn test_render_single_node() {
301 let id = SessionId::new();
302 let mut metadata = SessionMetadata::new();
303 metadata.title = Some("Test Session".to_string());
304 let node = SessionNode::new_root(id);
305
306 let tree_node = TreeNode {
307 id,
308 metadata,
309 node,
310 children: Vec::new(),
311 message_count: 5,
312 };
313
314 let options = TreeRenderOptions::default();
315 let result = render_tree(&[tree_node], &options);
316 assert!(result.is_ok());
317 match result {
318 Ok(output) => {
319 assert!(output.contains("Test Session"));
320 assert!(output.contains("5 msgs"));
321 assert!(output.contains(&id.prefix()));
322 }
323 Err(_) => unreachable!(),
324 }
325 }
326
327 #[test]
328 fn test_render_with_highlight() {
329 let id = SessionId::new();
330 let metadata = SessionMetadata::new();
331 let node = SessionNode::new_root(id);
332
333 let tree_node = TreeNode {
334 id,
335 metadata,
336 node,
337 children: Vec::new(),
338 message_count: 0,
339 };
340
341 let options = TreeRenderOptions {
342 highlight_id: Some(id),
343 ..Default::default()
344 };
345
346 let result = render_tree(&[tree_node], &options);
347 assert!(result.is_ok());
348 match result {
349 Ok(output) => {
350 assert!(output.contains("➤"));
351 }
352 Err(_) => unreachable!(),
353 }
354 }
355
356 #[test]
357 fn test_render_multi_level_tree() {
358 let root_id = SessionId::new();
359 let child_id = SessionId::new();
360
361 let root_meta = SessionMetadata::new();
362 let mut child_meta = SessionMetadata::new();
363 child_meta.title = Some("Child Session".to_string());
364
365 let mut root_node = SessionNode::new_root(root_id);
366 root_node.add_child(child_id);
367 let child_node = SessionNode::new_child(child_id, root_id);
368
369 let child_tree_node = TreeNode {
370 id: child_id,
371 metadata: child_meta,
372 node: child_node,
373 children: Vec::new(),
374 message_count: 3,
375 };
376
377 let root_tree_node = TreeNode {
378 id: root_id,
379 metadata: root_meta,
380 node: root_node,
381 children: vec![child_tree_node],
382 message_count: 2,
383 };
384
385 let options = TreeRenderOptions::default();
386 let result = render_tree(&[root_tree_node], &options);
387 assert!(result.is_ok());
388 match result {
389 Ok(output) => {
390 assert!(output.contains("Child Session"));
391 assert!(output.contains("│"));
392 }
393 Err(_) => unreachable!(),
394 }
395 }
396
397 #[test]
398 fn test_filter_by_date() {
399 let id = SessionId::new();
400 let mut metadata = SessionMetadata::new();
401 metadata.last_active = Utc::now();
402 let node = SessionNode::new_root(id);
403
404 let tree_node = TreeNode {
405 id,
406 metadata,
407 node,
408 children: Vec::new(),
409 message_count: 0,
410 };
411
412 let options = TreeRenderOptions {
414 after_date: Some(Utc::now() + chrono::Duration::hours(1)),
415 ..Default::default()
416 };
417
418 let result = render_tree(std::slice::from_ref(&tree_node), &options);
419 assert!(result.is_ok());
420 match result {
421 Ok(output) => {
422 assert!(!output.contains(&id.prefix()));
424 }
425 Err(_) => unreachable!(),
426 }
427 }
428
429 #[test]
430 fn test_filter_by_tag() {
431 let id = SessionId::new();
432 let mut metadata = SessionMetadata::new();
433 metadata.add_tag("important".to_string());
434 let node = SessionNode::new_root(id);
435
436 let tree_node = TreeNode {
437 id,
438 metadata,
439 node,
440 children: Vec::new(),
441 message_count: 0,
442 };
443
444 let options = TreeRenderOptions {
446 tags: vec!["other".to_string()],
447 ..Default::default()
448 };
449
450 let result = render_tree(std::slice::from_ref(&tree_node), &options);
451 assert!(result.is_ok());
452 match result {
453 Ok(output) => {
454 assert!(!output.contains(&id.prefix()));
455 }
456 Err(_) => unreachable!(),
457 }
458
459 let options2 = TreeRenderOptions {
461 tags: vec!["important".to_string()],
462 ..Default::default()
463 };
464
465 let result2 = render_tree(&[tree_node], &options2);
466 assert!(result2.is_ok());
467 match result2 {
468 Ok(output) => {
469 assert!(output.contains(&id.prefix()));
470 }
471 Err(_) => unreachable!(),
472 }
473 }
474
475 #[test]
476 fn test_find_in_tree() {
477 let root_id = SessionId::new();
478 let child_id = SessionId::new();
479
480 let root_meta = SessionMetadata::new();
481 let child_meta = SessionMetadata::new();
482
483 let mut root_node = SessionNode::new_root(root_id);
484 root_node.add_child(child_id);
485 let child_node = SessionNode::new_child(child_id, root_id);
486
487 let child_tree_node = TreeNode {
488 id: child_id,
489 metadata: child_meta,
490 node: child_node,
491 children: Vec::new(),
492 message_count: 0,
493 };
494
495 let root_tree_node = TreeNode {
496 id: root_id,
497 metadata: root_meta,
498 node: root_node,
499 children: vec![child_tree_node],
500 message_count: 0,
501 };
502
503 let found = find_in_tree(&[root_tree_node], child_id);
505 assert!(found.is_some());
506 match found {
507 Some(node) => assert!(node.id == child_id),
508 None => unreachable!(),
509 }
510 }
511}