1use anyhow::Result;
6use tabled::{settings::Style, Table, Tabled};
7
8use crate::models::Workspace;
9use crate::storage::read_empty_window_sessions;
10use crate::workspace::discover_workspaces;
11
12#[derive(Tabled)]
13struct WorkspaceRow {
14 #[tabled(rename = "Hash")]
15 hash: String,
16 #[tabled(rename = "Project Path")]
17 project_path: String,
18 #[tabled(rename = "Sessions")]
19 sessions: usize,
20 #[tabled(rename = "Has Chats")]
21 has_chats: String,
22}
23
24#[derive(Tabled)]
25struct SessionRow {
26 #[tabled(rename = "Project Path")]
27 project_path: String,
28 #[tabled(rename = "Session File")]
29 session_file: String,
30 #[tabled(rename = "Last Modified")]
31 last_modified: String,
32 #[tabled(rename = "Messages")]
33 messages: usize,
34}
35
36pub fn list_workspaces() -> Result<()> {
38 let workspaces = discover_workspaces()?;
39
40 if workspaces.is_empty() {
41 println!("No workspaces found.");
42 return Ok(());
43 }
44
45 let rows: Vec<WorkspaceRow> = workspaces
46 .iter()
47 .map(|ws| WorkspaceRow {
48 hash: format!("{}...", &ws.hash[..12.min(ws.hash.len())]),
49 project_path: ws
50 .project_path
51 .clone()
52 .unwrap_or_else(|| "(none)".to_string()),
53 sessions: ws.chat_session_count,
54 has_chats: if ws.has_chat_sessions {
55 "Yes".to_string()
56 } else {
57 "No".to_string()
58 },
59 })
60 .collect();
61
62 let table = Table::new(rows).with(Style::ascii_rounded()).to_string();
63
64 println!("{}", table);
65 println!("\nTotal workspaces: {}", workspaces.len());
66
67 if let Ok(empty_count) = crate::storage::count_empty_window_sessions() {
69 if empty_count > 0 {
70 println!("Empty window sessions (ALL SESSIONS): {}", empty_count);
71 }
72 }
73
74 Ok(())
75}
76
77pub fn list_sessions(project_path: Option<&str>) -> Result<()> {
79 let workspaces = discover_workspaces()?;
80
81 let filtered_workspaces: Vec<&Workspace> = if let Some(path) = project_path {
82 let normalized = crate::workspace::normalize_path(path);
83 workspaces
84 .iter()
85 .filter(|ws| {
86 ws.project_path
87 .as_ref()
88 .map(|p| crate::workspace::normalize_path(p) == normalized)
89 .unwrap_or(false)
90 })
91 .collect()
92 } else {
93 workspaces.iter().collect()
94 };
95
96 let mut rows: Vec<SessionRow> = Vec::new();
97
98 if project_path.is_none() {
100 if let Ok(empty_sessions) = read_empty_window_sessions() {
101 for session in empty_sessions {
102 let modified = chrono::DateTime::from_timestamp_millis(session.last_message_date)
103 .map(|dt| dt.format("%Y-%m-%d %H:%M").to_string())
104 .unwrap_or_else(|| "unknown".to_string());
105
106 let session_id = session.session_id.as_deref().unwrap_or("unknown");
107 rows.push(SessionRow {
108 project_path: "(ALL SESSIONS)".to_string(),
109 session_file: format!("{}.json", session_id),
110 last_modified: modified,
111 messages: session.request_count(),
112 });
113 }
114 }
115 }
116
117 for ws in filtered_workspaces {
118 if !ws.has_chat_sessions {
119 continue;
120 }
121
122 let sessions = crate::workspace::get_chat_sessions_from_workspace(&ws.workspace_path)?;
123
124 for session_with_path in sessions {
125 let modified = session_with_path
126 .path
127 .metadata()
128 .ok()
129 .and_then(|m| m.modified().ok())
130 .map(|t| {
131 let datetime: chrono::DateTime<chrono::Utc> = t.into();
132 datetime.format("%Y-%m-%d %H:%M").to_string()
133 })
134 .unwrap_or_else(|| "unknown".to_string());
135
136 rows.push(SessionRow {
137 project_path: ws
138 .project_path
139 .clone()
140 .unwrap_or_else(|| "(none)".to_string()),
141 session_file: session_with_path
142 .path
143 .file_name()
144 .map(|n| n.to_string_lossy().to_string())
145 .unwrap_or_else(|| "unknown".to_string()),
146 last_modified: modified,
147 messages: session_with_path.session.request_count(),
148 });
149 }
150 }
151
152 if rows.is_empty() {
153 println!("No chat sessions found.");
154 return Ok(());
155 }
156
157 let table = Table::new(&rows).with(Style::ascii_rounded()).to_string();
158
159 println!("{}", table);
160 println!("\nTotal sessions: {}", rows.len());
161
162 Ok(())
163}
164
165pub fn find_workspaces(pattern: &str) -> Result<()> {
167 let workspaces = discover_workspaces()?;
168 let pattern_lower = pattern.to_lowercase();
169
170 let matching: Vec<&Workspace> = workspaces
171 .iter()
172 .filter(|ws| {
173 ws.project_path
174 .as_ref()
175 .map(|p| p.to_lowercase().contains(&pattern_lower))
176 .unwrap_or(false)
177 || ws.hash.to_lowercase().contains(&pattern_lower)
178 })
179 .collect();
180
181 if matching.is_empty() {
182 println!("No workspaces found matching '{}'", pattern);
183 return Ok(());
184 }
185
186 let rows: Vec<WorkspaceRow> = matching
187 .iter()
188 .map(|ws| WorkspaceRow {
189 hash: format!("{}...", &ws.hash[..12.min(ws.hash.len())]),
190 project_path: ws
191 .project_path
192 .clone()
193 .unwrap_or_else(|| "(none)".to_string()),
194 sessions: ws.chat_session_count,
195 has_chats: if ws.has_chat_sessions {
196 "Yes".to_string()
197 } else {
198 "No".to_string()
199 },
200 })
201 .collect();
202
203 let table = Table::new(rows).with(Style::ascii_rounded()).to_string();
204
205 println!("{}", table);
206 println!("\nFound {} matching workspace(s)", matching.len());
207
208 for ws in &matching {
210 if ws.has_chat_sessions {
211 let project = ws.project_path.as_deref().unwrap_or("(none)");
212 println!("\nSessions for {}:", project);
213
214 if let Ok(sessions) =
215 crate::workspace::get_chat_sessions_from_workspace(&ws.workspace_path)
216 {
217 for session_with_path in sessions {
218 println!(" {}", session_with_path.path.display());
219 }
220 }
221 }
222 }
223
224 Ok(())
225}
226
227pub fn find_sessions(pattern: &str, project_path: Option<&str>) -> Result<()> {
229 let workspaces = discover_workspaces()?;
230 let pattern_lower = pattern.to_lowercase();
231
232 let filtered_workspaces: Vec<&Workspace> = if let Some(path) = project_path {
233 let normalized = crate::workspace::normalize_path(path);
234 workspaces
235 .iter()
236 .filter(|ws| {
237 ws.project_path
238 .as_ref()
239 .map(|p| crate::workspace::normalize_path(p) == normalized)
240 .unwrap_or(false)
241 })
242 .collect()
243 } else {
244 workspaces.iter().collect()
245 };
246
247 let mut rows: Vec<SessionRow> = Vec::new();
248
249 for ws in filtered_workspaces {
250 if !ws.has_chat_sessions {
251 continue;
252 }
253
254 let sessions = crate::workspace::get_chat_sessions_from_workspace(&ws.workspace_path)?;
255
256 for session_with_path in sessions {
257 let session_id_matches = session_with_path
259 .session
260 .session_id
261 .as_ref()
262 .map(|id| id.to_lowercase().contains(&pattern_lower))
263 .unwrap_or(false);
264 let title_matches = session_with_path
265 .session
266 .title()
267 .to_lowercase()
268 .contains(&pattern_lower);
269 let content_matches = session_with_path.session.requests.iter().any(|r| {
270 r.message
271 .as_ref()
272 .map(|m| {
273 m.text
274 .as_ref()
275 .map(|t| t.to_lowercase().contains(&pattern_lower))
276 .unwrap_or(false)
277 })
278 .unwrap_or(false)
279 });
280
281 if !session_id_matches && !title_matches && !content_matches {
282 continue;
283 }
284
285 let modified = session_with_path
286 .path
287 .metadata()
288 .ok()
289 .and_then(|m| m.modified().ok())
290 .map(|t| {
291 let datetime: chrono::DateTime<chrono::Utc> = t.into();
292 datetime.format("%Y-%m-%d %H:%M").to_string()
293 })
294 .unwrap_or_else(|| "unknown".to_string());
295
296 rows.push(SessionRow {
297 project_path: ws
298 .project_path
299 .clone()
300 .unwrap_or_else(|| "(none)".to_string()),
301 session_file: session_with_path
302 .path
303 .file_name()
304 .map(|n| n.to_string_lossy().to_string())
305 .unwrap_or_else(|| "unknown".to_string()),
306 last_modified: modified,
307 messages: session_with_path.session.request_count(),
308 });
309 }
310 }
311
312 if rows.is_empty() {
313 println!("No sessions found matching '{}'", pattern);
314 return Ok(());
315 }
316
317 let table = Table::new(&rows).with(Style::ascii_rounded()).to_string();
318
319 println!("{}", table);
320 println!("\nFound {} matching session(s)", rows.len());
321
322 Ok(())
323}
324
325pub fn show_workspace(workspace: &str) -> Result<()> {
327 use colored::Colorize;
328
329 let workspaces = discover_workspaces()?;
330 let workspace_lower = workspace.to_lowercase();
331
332 let matching: Vec<&Workspace> = workspaces
334 .iter()
335 .filter(|ws| {
336 ws.hash.to_lowercase().contains(&workspace_lower)
337 || ws
338 .project_path
339 .as_ref()
340 .map(|p| p.to_lowercase().contains(&workspace_lower))
341 .unwrap_or(false)
342 })
343 .collect();
344
345 if matching.is_empty() {
346 println!(
347 "{} No workspace found matching '{}'",
348 "!".yellow(),
349 workspace
350 );
351 return Ok(());
352 }
353
354 for ws in matching {
355 println!("\n{}", "=".repeat(60).bright_blue());
356 println!("{}", "Workspace Details".bright_blue().bold());
357 println!("{}", "=".repeat(60).bright_blue());
358
359 println!("{}: {}", "Hash".bright_white().bold(), ws.hash);
360 println!(
361 "{}: {}",
362 "Path".bright_white().bold(),
363 ws.project_path.as_ref().unwrap_or(&"(none)".to_string())
364 );
365 println!(
366 "{}: {}",
367 "Has Sessions".bright_white().bold(),
368 if ws.has_chat_sessions {
369 "Yes".green()
370 } else {
371 "No".red()
372 }
373 );
374 println!(
375 "{}: {}",
376 "Workspace Path".bright_white().bold(),
377 ws.workspace_path.display()
378 );
379
380 if ws.has_chat_sessions {
381 let sessions = crate::workspace::get_chat_sessions_from_workspace(&ws.workspace_path)?;
382 println!(
383 "{}: {}",
384 "Session Count".bright_white().bold(),
385 sessions.len()
386 );
387
388 if !sessions.is_empty() {
389 println!("\n{}", "Sessions:".bright_yellow());
390 for (i, s) in sessions.iter().enumerate() {
391 let title = s.session.title();
392 let msg_count = s.session.request_count();
393 println!(
394 " {}. {} ({} messages)",
395 i + 1,
396 title.bright_cyan(),
397 msg_count
398 );
399 }
400 }
401 }
402 }
403
404 Ok(())
405}
406
407pub fn show_session(session_id: &str, project_path: Option<&str>) -> Result<()> {
409 use colored::Colorize;
410
411 let workspaces = discover_workspaces()?;
412 let session_id_lower = session_id.to_lowercase();
413
414 let filtered_workspaces: Vec<&Workspace> = if let Some(path) = project_path {
415 let normalized = crate::workspace::normalize_path(path);
416 workspaces
417 .iter()
418 .filter(|ws| {
419 ws.project_path
420 .as_ref()
421 .map(|p| crate::workspace::normalize_path(p) == normalized)
422 .unwrap_or(false)
423 })
424 .collect()
425 } else {
426 workspaces.iter().collect()
427 };
428
429 for ws in filtered_workspaces {
430 if !ws.has_chat_sessions {
431 continue;
432 }
433
434 let sessions = crate::workspace::get_chat_sessions_from_workspace(&ws.workspace_path)?;
435
436 for s in sessions {
437 let filename = s
438 .path
439 .file_name()
440 .map(|n| n.to_string_lossy().to_string())
441 .unwrap_or_default();
442
443 let matches = s
444 .session
445 .session_id
446 .as_ref()
447 .map(|id| id.to_lowercase().contains(&session_id_lower))
448 .unwrap_or(false)
449 || filename.to_lowercase().contains(&session_id_lower);
450
451 if matches {
452 println!("\n{}", "=".repeat(60).bright_blue());
453 println!("{}", "Session Details".bright_blue().bold());
454 println!("{}", "=".repeat(60).bright_blue());
455
456 println!(
457 "{}: {}",
458 "Title".bright_white().bold(),
459 s.session.title().bright_cyan()
460 );
461 println!("{}: {}", "File".bright_white().bold(), filename);
462 println!(
463 "{}: {}",
464 "Session ID".bright_white().bold(),
465 s.session
466 .session_id
467 .as_ref()
468 .unwrap_or(&"(none)".to_string())
469 );
470 println!(
471 "{}: {}",
472 "Messages".bright_white().bold(),
473 s.session.request_count()
474 );
475 println!(
476 "{}: {}",
477 "Workspace".bright_white().bold(),
478 ws.project_path.as_ref().unwrap_or(&"(none)".to_string())
479 );
480
481 println!("\n{}", "Preview:".bright_yellow());
483 for (i, req) in s.session.requests.iter().take(3).enumerate() {
484 if let Some(msg) = &req.message {
485 if let Some(text) = &msg.text {
486 let preview: String = text.chars().take(100).collect();
487 let truncated = if text.len() > 100 { "..." } else { "" };
488 println!(" {}. {}{}", i + 1, preview.dimmed(), truncated);
489 }
490 }
491 }
492
493 return Ok(());
494 }
495 }
496 }
497
498 println!(
499 "{} No session found matching '{}'",
500 "!".yellow(),
501 session_id
502 );
503 Ok(())
504}