1use crate::tree::SessionTree;
2use crate::tui::App;
3use std::sync::mpsc;
4use std::thread;
5
6impl App {
7 pub fn enter_tree_mode(&mut self) {
9 let file_path = match self.selected_group() {
10 Some(group) => group.file_path.clone(),
11 None => return,
12 };
13 self.enter_tree_mode_for_file(&file_path);
14 }
15
16 pub fn enter_tree_mode_direct(&mut self, target: &str) {
18 let file_path = if target.contains('/') || target.ends_with(".jsonl") {
20 target.to_string()
21 } else {
22 match self.find_session_file(target) {
24 Some(path) => path,
25 None => {
26 self.error = Some(format!("Session not found: {}", target));
27 return;
28 }
29 }
30 };
31 self.tree_mode_standalone = true;
32 self.enter_tree_mode_for_file(&file_path);
33 }
34
35 pub(crate) fn enter_tree_mode_for_file(&mut self, file_path: &str) {
36 self.tree_mode = true;
37 self.tree_loading = true;
38 self.tree_cursor = 0;
39 self.tree_scroll_offset = 0;
40 self.session_tree = None;
41 self.preview_mode = false;
42 self.needs_full_redraw = true;
43
44 let fp = file_path.to_string();
45 let (tx, rx) = mpsc::channel();
46 self.tree_load_rx = Some(rx);
47
48 thread::spawn(move || {
49 let result = SessionTree::from_file(&fp);
50 let _ = tx.send(result);
51 });
52 }
53
54 fn find_session_file(&self, session_id: &str) -> Option<String> {
58 use std::fs;
59 let target_filename = format!("{}.jsonl", session_id);
60
61 for search_path in &self.search_paths {
62 if let Ok(entries) = fs::read_dir(search_path) {
63 for entry in entries.flatten() {
64 let path = entry.path();
65 if path.is_dir() {
66 let candidate = path.join(&target_filename);
68 if candidate.exists() {
69 return Some(candidate.to_string_lossy().to_string());
70 }
71 if let Ok(subentries) = fs::read_dir(&path) {
73 for subentry in subentries.flatten() {
74 let subpath = subentry.path();
75 if subpath.is_dir() {
76 let candidate = subpath.join(&target_filename);
77 if candidate.exists() {
78 return Some(candidate.to_string_lossy().to_string());
79 }
80 let audit = subpath.join("audit.jsonl");
82 if audit.exists()
83 && Self::file_contains_session_id(&audit, session_id)
84 {
85 return Some(audit.to_string_lossy().to_string());
86 }
87 }
88 }
89 }
90 }
91 }
92 }
93 }
94 None
95 }
96
97 fn file_contains_session_id(path: &std::path::Path, session_id: &str) -> bool {
99 use std::io::{BufRead, BufReader};
100 let Ok(file) = std::fs::File::open(path) else {
101 return false;
102 };
103 let reader = BufReader::new(file);
104 for line in reader.lines().take(5).flatten() {
105 if line.contains(session_id) {
106 return true;
107 }
108 }
109 false
110 }
111
112 pub fn exit_tree_mode(&mut self) {
113 if self.tree_mode_standalone {
114 self.should_quit = true;
115 return;
116 }
117 self.tree_mode = false;
118 self.session_tree = None;
119 self.tree_loading = false;
120 self.tree_load_rx = None;
121 self.preview_mode = false;
122 self.needs_full_redraw = true;
123 }
124
125 pub fn on_up_tree(&mut self) {
126 if self.tree_cursor > 0 {
127 self.tree_cursor -= 1;
128 self.adjust_tree_scroll();
129 if self.preview_mode {
130 self.needs_full_redraw = true;
131 }
132 }
133 }
134
135 pub fn on_down_tree(&mut self) {
136 if let Some(ref tree) = self.session_tree {
137 if self.tree_cursor < tree.rows.len().saturating_sub(1) {
138 self.tree_cursor += 1;
139 self.adjust_tree_scroll();
140 if self.preview_mode {
141 self.needs_full_redraw = true;
142 }
143 }
144 }
145 }
146
147 pub fn on_left_tree(&mut self) {
148 if let Some(ref tree) = self.session_tree {
150 for i in (0..self.tree_cursor).rev() {
151 if tree.rows[i].is_branch_point {
152 self.tree_cursor = i;
153 self.adjust_tree_scroll();
154 if self.preview_mode {
155 self.needs_full_redraw = true;
156 }
157 return;
158 }
159 }
160 }
161 }
162
163 pub fn on_right_tree(&mut self) {
164 if let Some(ref tree) = self.session_tree {
166 for i in (self.tree_cursor + 1)..tree.rows.len() {
167 if tree.rows[i].is_branch_point {
168 self.tree_cursor = i;
169 self.adjust_tree_scroll();
170 if self.preview_mode {
171 self.needs_full_redraw = true;
172 }
173 return;
174 }
175 }
176 }
177 }
178
179 pub fn on_enter_tree(&mut self) {
180 if self.preview_mode {
181 self.preview_mode = false;
182 self.needs_full_redraw = true;
183 return;
184 }
185
186 if let Some(ref tree) = self.session_tree {
187 if let Some(row) = tree.rows.get(self.tree_cursor) {
188 self.resume_uuid = Some(row.uuid.clone());
189 self.resume_id = Some(tree.session_id.clone());
190 self.resume_file_path = Some(tree.file_path.clone());
191 self.resume_source = Some(tree.source);
192 self.should_quit = true;
193 }
194 }
195 }
196
197 pub fn on_tab_tree(&mut self) {
198 if let Some(ref tree) = self.session_tree {
199 if !tree.rows.is_empty() {
200 self.preview_mode = !self.preview_mode;
201 self.needs_full_redraw = true;
202 }
203 }
204 }
205
206 fn adjust_tree_scroll(&mut self) {
207 let visible = 20; if self.tree_cursor < self.tree_scroll_offset {
209 self.tree_scroll_offset = self.tree_cursor;
210 } else if self.tree_cursor >= self.tree_scroll_offset + visible {
211 self.tree_scroll_offset = self.tree_cursor.saturating_sub(visible) + 1;
212 }
213 }
214}
215
216#[cfg(test)]
217mod tests {
218 use crate::tui::App;
219
220 #[test]
221 fn test_exit_tree_mode_returns_to_search() {
222 let mut app = App::new(vec!["/test".to_string()]);
223 app.tree_mode = true;
224 app.tree_mode_standalone = false;
225
226 app.exit_tree_mode();
227
228 assert!(!app.tree_mode);
229 assert!(!app.should_quit);
230 }
231
232 #[test]
233 fn test_exit_tree_mode_standalone_quits() {
234 let mut app = App::new(vec!["/test".to_string()]);
235 app.tree_mode = true;
236 app.tree_mode_standalone = true;
237
238 app.exit_tree_mode();
239
240 assert!(app.should_quit);
241 }
242}