Skip to main content

chasm_cli/commands/
workspace_cmds.rs

1// Copyright (c) 2024-2026 Nervosys LLC
2// SPDX-License-Identifier: Apache-2.0
3//! Workspace listing commands
4
5use 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
36/// List all VS Code workspaces
37pub 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    // Show empty window sessions count (ALL SESSIONS)
68    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
77/// List all chat sessions
78pub 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    // Add empty window sessions (ALL SESSIONS) if no specific project filter
99    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
165/// Find workspaces by search pattern
166pub fn find_workspaces(pattern: &str) -> Result<()> {
167    let workspaces = discover_workspaces()?;
168
169    // Resolve "." to current directory name
170    let pattern = if pattern == "." {
171        std::env::current_dir()
172            .ok()
173            .and_then(|p| p.file_name().map(|n| n.to_string_lossy().to_string()))
174            .unwrap_or_else(|| pattern.to_string())
175    } else {
176        pattern.to_string()
177    };
178    let pattern_lower = pattern.to_lowercase();
179
180    let matching: Vec<&Workspace> = workspaces
181        .iter()
182        .filter(|ws| {
183            ws.project_path
184                .as_ref()
185                .map(|p| p.to_lowercase().contains(&pattern_lower))
186                .unwrap_or(false)
187                || ws.hash.to_lowercase().contains(&pattern_lower)
188        })
189        .collect();
190
191    if matching.is_empty() {
192        println!("No workspaces found matching '{}'", pattern);
193        return Ok(());
194    }
195
196    let rows: Vec<WorkspaceRow> = matching
197        .iter()
198        .map(|ws| WorkspaceRow {
199            hash: format!("{}...", &ws.hash[..12.min(ws.hash.len())]),
200            project_path: ws
201                .project_path
202                .clone()
203                .unwrap_or_else(|| "(none)".to_string()),
204            sessions: ws.chat_session_count,
205            has_chats: if ws.has_chat_sessions {
206                "Yes".to_string()
207            } else {
208                "No".to_string()
209            },
210        })
211        .collect();
212
213    let table = Table::new(rows).with(Style::ascii_rounded()).to_string();
214
215    println!("{}", table);
216    println!("\nFound {} matching workspace(s)", matching.len());
217
218    // Show session paths for each matching workspace
219    for ws in &matching {
220        if ws.has_chat_sessions {
221            let project = ws.project_path.as_deref().unwrap_or("(none)");
222            println!("\nSessions for {}:", project);
223
224            if let Ok(sessions) =
225                crate::workspace::get_chat_sessions_from_workspace(&ws.workspace_path)
226            {
227                for session_with_path in sessions {
228                    println!("  {}", session_with_path.path.display());
229                }
230            }
231        }
232    }
233
234    Ok(())
235}
236
237/// Find sessions by search pattern
238#[allow(dead_code)]
239pub fn find_sessions(pattern: &str, project_path: Option<&str>) -> Result<()> {
240    let workspaces = discover_workspaces()?;
241    let pattern_lower = pattern.to_lowercase();
242
243    let filtered_workspaces: Vec<&Workspace> = if let Some(path) = project_path {
244        let normalized = crate::workspace::normalize_path(path);
245        workspaces
246            .iter()
247            .filter(|ws| {
248                ws.project_path
249                    .as_ref()
250                    .map(|p| crate::workspace::normalize_path(p) == normalized)
251                    .unwrap_or(false)
252            })
253            .collect()
254    } else {
255        workspaces.iter().collect()
256    };
257
258    let mut rows: Vec<SessionRow> = Vec::new();
259
260    for ws in filtered_workspaces {
261        if !ws.has_chat_sessions {
262            continue;
263        }
264
265        let sessions = crate::workspace::get_chat_sessions_from_workspace(&ws.workspace_path)?;
266
267        for session_with_path in sessions {
268            // Check if session matches the pattern
269            let session_id_matches = session_with_path
270                .session
271                .session_id
272                .as_ref()
273                .map(|id| id.to_lowercase().contains(&pattern_lower))
274                .unwrap_or(false);
275            let title_matches = session_with_path
276                .session
277                .title()
278                .to_lowercase()
279                .contains(&pattern_lower);
280            let content_matches = session_with_path.session.requests.iter().any(|r| {
281                r.message
282                    .as_ref()
283                    .map(|m| {
284                        m.text
285                            .as_ref()
286                            .map(|t| t.to_lowercase().contains(&pattern_lower))
287                            .unwrap_or(false)
288                    })
289                    .unwrap_or(false)
290            });
291
292            if !session_id_matches && !title_matches && !content_matches {
293                continue;
294            }
295
296            let modified = session_with_path
297                .path
298                .metadata()
299                .ok()
300                .and_then(|m| m.modified().ok())
301                .map(|t| {
302                    let datetime: chrono::DateTime<chrono::Utc> = t.into();
303                    datetime.format("%Y-%m-%d %H:%M").to_string()
304                })
305                .unwrap_or_else(|| "unknown".to_string());
306
307            rows.push(SessionRow {
308                project_path: ws
309                    .project_path
310                    .clone()
311                    .unwrap_or_else(|| "(none)".to_string()),
312                session_file: session_with_path
313                    .path
314                    .file_name()
315                    .map(|n| n.to_string_lossy().to_string())
316                    .unwrap_or_else(|| "unknown".to_string()),
317                last_modified: modified,
318                messages: session_with_path.session.request_count(),
319            });
320        }
321    }
322
323    if rows.is_empty() {
324        println!("No sessions found matching '{}'", pattern);
325        return Ok(());
326    }
327
328    let table = Table::new(&rows).with(Style::ascii_rounded()).to_string();
329
330    println!("{}", table);
331    println!("\nFound {} matching session(s)", rows.len());
332
333    Ok(())
334}
335
336/// Optimized session search with filtering
337///
338/// This function is optimized for speed by:
339/// 1. Filtering workspaces first (by name/path)
340/// 2. Filtering by file modification date before reading content
341/// 3. Only parsing JSON when needed
342/// 4. Content search is opt-in (expensive)
343/// 5. Parallel file scanning with rayon
344pub fn find_sessions_filtered(
345    pattern: &str,
346    workspace_filter: Option<&str>,
347    title_only: bool,
348    search_content: bool,
349    after: Option<&str>,
350    before: Option<&str>,
351    limit: usize,
352) -> Result<()> {
353    use chrono::{NaiveDate, Utc};
354    use rayon::prelude::*;
355    use std::sync::atomic::{AtomicUsize, Ordering};
356
357    let pattern_lower = pattern.to_lowercase();
358
359    // Parse date filters upfront
360    let after_date = after.and_then(|s| NaiveDate::parse_from_str(s, "%Y-%m-%d").ok());
361    let before_date = before.and_then(|s| NaiveDate::parse_from_str(s, "%Y-%m-%d").ok());
362
363    // Get workspace storage path directly - avoid full discovery if filtering
364    let storage_path = crate::workspace::get_workspace_storage_path()?;
365    if !storage_path.exists() {
366        println!("No workspaces found");
367        return Ok(());
368    }
369
370    // Collect workspace directories with minimal I/O
371    let ws_filter_lower = workspace_filter.map(|s| s.to_lowercase());
372
373    let workspace_dirs: Vec<_> = std::fs::read_dir(&storage_path)?
374        .filter_map(|e| e.ok())
375        .filter(|e| e.path().is_dir())
376        .filter_map(|entry| {
377            let workspace_dir = entry.path();
378            let workspace_json_path = workspace_dir.join("workspace.json");
379
380            // Quick check: does chatSessions exist?
381            let chat_sessions_dir = workspace_dir.join("chatSessions");
382            if !chat_sessions_dir.exists() {
383                return None;
384            }
385
386            // Parse workspace.json for project path (needed for filtering)
387            let project_path =
388                std::fs::read_to_string(&workspace_json_path)
389                    .ok()
390                    .and_then(|content| {
391                        serde_json::from_str::<crate::models::WorkspaceJson>(&content)
392                            .ok()
393                            .and_then(|ws| {
394                                ws.folder
395                                    .map(|f| crate::workspace::decode_workspace_folder(&f))
396                            })
397                    });
398
399            // Apply workspace filter early
400            if let Some(ref filter) = ws_filter_lower {
401                let hash = entry.file_name().to_string_lossy().to_lowercase();
402                let path_matches = project_path
403                    .as_ref()
404                    .map(|p| p.to_lowercase().contains(filter))
405                    .unwrap_or(false);
406                if !hash.contains(filter) && !path_matches {
407                    return None;
408                }
409            }
410
411            let ws_name = project_path
412                .as_ref()
413                .and_then(|p| std::path::Path::new(p).file_name())
414                .map(|n| n.to_string_lossy().to_string())
415                .unwrap_or_else(|| {
416                    entry.file_name().to_string_lossy()[..8.min(entry.file_name().len())]
417                        .to_string()
418                });
419
420            Some((chat_sessions_dir, ws_name))
421        })
422        .collect();
423
424    if workspace_dirs.is_empty() {
425        if let Some(ws) = workspace_filter {
426            println!("No workspaces found matching '{}'", ws);
427        } else {
428            println!("No workspaces with chat sessions found");
429        }
430        return Ok(());
431    }
432
433    // Collect all session file paths
434    let session_files: Vec<_> = workspace_dirs
435        .iter()
436        .flat_map(|(chat_dir, ws_name)| {
437            std::fs::read_dir(chat_dir)
438                .into_iter()
439                .flatten()
440                .filter_map(|e| e.ok())
441                .filter(|e| {
442                    e.path()
443                        .extension()
444                        .map(|ext| ext == "json")
445                        .unwrap_or(false)
446                })
447                .map(|e| (e.path(), ws_name.clone()))
448                .collect::<Vec<_>>()
449        })
450        .collect();
451
452    let total_files = session_files.len();
453    let scanned = AtomicUsize::new(0);
454    let skipped_by_date = AtomicUsize::new(0);
455
456    // Process files in parallel
457    let mut results: Vec<_> = session_files
458        .par_iter()
459        .filter_map(|(path, ws_name)| {
460            // Date filter using file metadata (very fast)
461            if after_date.is_some() || before_date.is_some() {
462                if let Ok(metadata) = path.metadata() {
463                    if let Ok(modified) = metadata.modified() {
464                        let file_date: chrono::DateTime<Utc> = modified.into();
465                        let file_naive = file_date.date_naive();
466
467                        if let Some(after) = after_date {
468                            if file_naive < after {
469                                skipped_by_date.fetch_add(1, Ordering::Relaxed);
470                                return None;
471                            }
472                        }
473                        if let Some(before) = before_date {
474                            if file_naive > before {
475                                skipped_by_date.fetch_add(1, Ordering::Relaxed);
476                                return None;
477                            }
478                        }
479                    }
480                }
481            }
482
483            scanned.fetch_add(1, Ordering::Relaxed);
484
485            // Read file content once
486            let content = match std::fs::read_to_string(path) {
487                Ok(c) => c,
488                Err(_) => return None,
489            };
490
491            // Extract title from content
492            let title =
493                extract_title_from_content(&content).unwrap_or_else(|| "Untitled".to_string());
494            let title_lower = title.to_lowercase();
495
496            // Check session ID from filename
497            let session_id = path
498                .file_stem()
499                .map(|n| n.to_string_lossy().to_string())
500                .unwrap_or_default();
501            let id_matches =
502                !pattern_lower.is_empty() && session_id.to_lowercase().contains(&pattern_lower);
503
504            // Check title match
505            let title_matches = !pattern_lower.is_empty() && title_lower.contains(&pattern_lower);
506
507            // Content search if requested
508            let content_matches = if search_content
509                && !title_only
510                && !id_matches
511                && !title_matches
512                && !pattern_lower.is_empty()
513            {
514                content.to_lowercase().contains(&pattern_lower)
515            } else {
516                false
517            };
518
519            // Empty pattern matches everything (for listing)
520            let matches =
521                pattern_lower.is_empty() || id_matches || title_matches || content_matches;
522            if !matches {
523                return None;
524            }
525
526            let match_type = if pattern_lower.is_empty() {
527                ""
528            } else if id_matches {
529                "ID"
530            } else if title_matches {
531                "title"
532            } else {
533                "content"
534            };
535
536            // Count messages from content (already loaded)
537            let message_count = content.matches("\"message\":").count();
538
539            // Get modification time
540            let modified = path
541                .metadata()
542                .ok()
543                .and_then(|m| m.modified().ok())
544                .map(|t| {
545                    let datetime: chrono::DateTime<chrono::Utc> = t.into();
546                    datetime.format("%Y-%m-%d %H:%M").to_string()
547                })
548                .unwrap_or_else(|| "unknown".to_string());
549
550            Some((
551                title,
552                ws_name.clone(),
553                modified,
554                message_count,
555                match_type.to_string(),
556            ))
557        })
558        .collect();
559
560    let scanned_count = scanned.load(Ordering::Relaxed);
561    let skipped_count = skipped_by_date.load(Ordering::Relaxed);
562
563    if results.is_empty() {
564        println!("No sessions found matching '{}'", pattern);
565        if skipped_count > 0 {
566            println!("  ({} sessions skipped due to date filter)", skipped_count);
567        }
568        return Ok(());
569    }
570
571    // Sort by modification date (newest first)
572    results.sort_by(|a, b| b.2.cmp(&a.2));
573
574    // Apply limit
575    results.truncate(limit);
576
577    #[derive(Tabled)]
578    struct SearchResultRow {
579        #[tabled(rename = "Title")]
580        title: String,
581        #[tabled(rename = "Workspace")]
582        workspace: String,
583        #[tabled(rename = "Modified")]
584        modified: String,
585        #[tabled(rename = "Msgs")]
586        messages: usize,
587        #[tabled(rename = "Match")]
588        match_type: String,
589    }
590
591    let rows: Vec<SearchResultRow> = results
592        .into_iter()
593        .map(
594            |(title, workspace, modified, messages, match_type)| SearchResultRow {
595                title: truncate_string(&title, 40),
596                workspace: truncate_string(&workspace, 20),
597                modified,
598                messages,
599                match_type,
600            },
601        )
602        .collect();
603
604    let table = Table::new(&rows).with(Style::ascii_rounded()).to_string();
605
606    println!("{}", table);
607    println!(
608        "\nFound {} session(s) (scanned {} of {} files{})",
609        rows.len(),
610        scanned_count,
611        total_files,
612        if skipped_count > 0 {
613            format!(", {} skipped by date", skipped_count)
614        } else {
615            String::new()
616        }
617    );
618    if rows.len() >= limit {
619        println!("  (results limited to {}; use --limit to show more)", limit);
620    }
621
622    Ok(())
623}
624
625/// Extract title from full JSON content (more reliable than header-only)
626fn extract_title_from_content(content: &str) -> Option<String> {
627    // Look for "customTitle" first (user-set title)
628    if let Some(start) = content.find("\"customTitle\"") {
629        if let Some(colon) = content[start..].find(':') {
630            let after_colon = &content[start + colon + 1..];
631            let trimmed = after_colon.trim_start();
632            if let Some(stripped) = trimmed.strip_prefix('"') {
633                if let Some(end) = stripped.find('"') {
634                    let title = &stripped[..end];
635                    if !title.is_empty() && title != "null" {
636                        return Some(title.to_string());
637                    }
638                }
639            }
640        }
641    }
642
643    // Fall back to first request's message text
644    if let Some(start) = content.find("\"text\"") {
645        if let Some(colon) = content[start..].find(':') {
646            let after_colon = &content[start + colon + 1..];
647            let trimmed = after_colon.trim_start();
648            if let Some(stripped) = trimmed.strip_prefix('"') {
649                if let Some(end) = stripped.find('"') {
650                    let title = &stripped[..end];
651                    if !title.is_empty() && title.len() < 100 {
652                        return Some(title.to_string());
653                    }
654                }
655            }
656        }
657    }
658
659    None
660}
661
662/// Fast title extraction from JSON header
663#[allow(dead_code)]
664fn extract_title_fast(header: &str) -> Option<String> {
665    extract_title_from_content(header)
666}
667
668/// Truncate string to max length with ellipsis
669fn truncate_string(s: &str, max_len: usize) -> String {
670    if s.len() <= max_len {
671        s.to_string()
672    } else {
673        format!("{}...", &s[..max_len.saturating_sub(3)])
674    }
675}
676
677/// Show workspace details
678pub fn show_workspace(workspace: &str) -> Result<()> {
679    use colored::Colorize;
680
681    let workspaces = discover_workspaces()?;
682    let workspace_lower = workspace.to_lowercase();
683
684    // Find workspace by name or hash
685    let matching: Vec<&Workspace> = workspaces
686        .iter()
687        .filter(|ws| {
688            ws.hash.to_lowercase().contains(&workspace_lower)
689                || ws
690                    .project_path
691                    .as_ref()
692                    .map(|p| p.to_lowercase().contains(&workspace_lower))
693                    .unwrap_or(false)
694        })
695        .collect();
696
697    if matching.is_empty() {
698        println!(
699            "{} No workspace found matching '{}'",
700            "!".yellow(),
701            workspace
702        );
703        return Ok(());
704    }
705
706    for ws in matching {
707        println!("\n{}", "=".repeat(60).bright_blue());
708        println!("{}", "Workspace Details".bright_blue().bold());
709        println!("{}", "=".repeat(60).bright_blue());
710
711        println!("{}: {}", "Hash".bright_white().bold(), ws.hash);
712        println!(
713            "{}: {}",
714            "Path".bright_white().bold(),
715            ws.project_path.as_ref().unwrap_or(&"(none)".to_string())
716        );
717        println!(
718            "{}: {}",
719            "Has Sessions".bright_white().bold(),
720            if ws.has_chat_sessions {
721                "Yes".green()
722            } else {
723                "No".red()
724            }
725        );
726        println!(
727            "{}: {}",
728            "Workspace Path".bright_white().bold(),
729            ws.workspace_path.display()
730        );
731
732        if ws.has_chat_sessions {
733            let sessions = crate::workspace::get_chat_sessions_from_workspace(&ws.workspace_path)?;
734            println!(
735                "{}: {}",
736                "Session Count".bright_white().bold(),
737                sessions.len()
738            );
739
740            if !sessions.is_empty() {
741                println!("\n{}", "Sessions:".bright_yellow());
742                for (i, s) in sessions.iter().enumerate() {
743                    let title = s.session.title();
744                    let msg_count = s.session.request_count();
745                    println!(
746                        "  {}. {} ({} messages)",
747                        i + 1,
748                        title.bright_cyan(),
749                        msg_count
750                    );
751                }
752            }
753        }
754    }
755
756    Ok(())
757}
758
759/// Show session details
760pub fn show_session(session_id: &str, project_path: Option<&str>) -> Result<()> {
761    use colored::Colorize;
762
763    let workspaces = discover_workspaces()?;
764    let session_id_lower = session_id.to_lowercase();
765
766    let filtered_workspaces: Vec<&Workspace> = if let Some(path) = project_path {
767        let normalized = crate::workspace::normalize_path(path);
768        workspaces
769            .iter()
770            .filter(|ws| {
771                ws.project_path
772                    .as_ref()
773                    .map(|p| crate::workspace::normalize_path(p) == normalized)
774                    .unwrap_or(false)
775            })
776            .collect()
777    } else {
778        workspaces.iter().collect()
779    };
780
781    for ws in filtered_workspaces {
782        if !ws.has_chat_sessions {
783            continue;
784        }
785
786        let sessions = crate::workspace::get_chat_sessions_from_workspace(&ws.workspace_path)?;
787
788        for s in sessions {
789            let filename = s
790                .path
791                .file_name()
792                .map(|n| n.to_string_lossy().to_string())
793                .unwrap_or_default();
794
795            let matches = s
796                .session
797                .session_id
798                .as_ref()
799                .map(|id| id.to_lowercase().contains(&session_id_lower))
800                .unwrap_or(false)
801                || filename.to_lowercase().contains(&session_id_lower);
802
803            if matches {
804                println!("\n{}", "=".repeat(60).bright_blue());
805                println!("{}", "Session Details".bright_blue().bold());
806                println!("{}", "=".repeat(60).bright_blue());
807
808                println!(
809                    "{}: {}",
810                    "Title".bright_white().bold(),
811                    s.session.title().bright_cyan()
812                );
813                println!("{}: {}", "File".bright_white().bold(), filename);
814                println!(
815                    "{}: {}",
816                    "Session ID".bright_white().bold(),
817                    s.session
818                        .session_id
819                        .as_ref()
820                        .unwrap_or(&"(none)".to_string())
821                );
822                println!(
823                    "{}: {}",
824                    "Messages".bright_white().bold(),
825                    s.session.request_count()
826                );
827                println!(
828                    "{}: {}",
829                    "Workspace".bright_white().bold(),
830                    ws.project_path.as_ref().unwrap_or(&"(none)".to_string())
831                );
832
833                // Show first few messages as preview
834                println!("\n{}", "Preview:".bright_yellow());
835                for (i, req) in s.session.requests.iter().take(3).enumerate() {
836                    if let Some(msg) = &req.message {
837                        if let Some(text) = &msg.text {
838                            let preview: String = text.chars().take(100).collect();
839                            let truncated = if text.len() > 100 { "..." } else { "" };
840                            println!("  {}. {}{}", i + 1, preview.dimmed(), truncated);
841                        }
842                    }
843                }
844
845                return Ok(());
846            }
847        }
848    }
849
850    println!(
851        "{} No session found matching '{}'",
852        "!".yellow(),
853        session_id
854    );
855    Ok(())
856}