chasm_cli/commands/
detect.rs

1// Copyright (c) 2024-2026 Nervosys LLC
2// SPDX-License-Identifier: Apache-2.0
3//! Auto-detection commands for workspaces and providers
4
5use anyhow::Result;
6use colored::*;
7
8use crate::models::Workspace;
9use crate::providers::{ProviderRegistry, ProviderType};
10use crate::workspace::{
11    discover_workspaces, find_workspace_by_path, get_chat_sessions_from_workspace,
12};
13
14/// Detect workspace information for a given path
15pub fn detect_workspace(path: Option<&str>) -> Result<()> {
16    let project_path = path.map(|p| p.to_string()).unwrap_or_else(|| {
17        std::env::current_dir()
18            .map(|p| p.to_string_lossy().to_string())
19            .unwrap_or_else(|_| ".".to_string())
20    });
21
22    println!("\n{} Detecting Workspace", "[D]".blue().bold());
23    println!("{}", "=".repeat(60));
24    println!("{} Path: {}", "[*]".blue(), project_path.cyan());
25
26    match find_workspace_by_path(&project_path)? {
27        Some((ws_id, ws_dir, ws_name)) => {
28            println!("\n{} Workspace Found!", "[+]".green().bold());
29            println!("   {} ID: {}", "[*]".blue(), &ws_id[..16.min(ws_id.len())]);
30            println!("   {} Directory: {}", "[*]".blue(), ws_dir.display());
31            if let Some(name) = ws_name {
32                println!("   {} Name: {}", "[*]".blue(), name.cyan());
33            }
34
35            // Get session count
36            if let Ok(sessions) = get_chat_sessions_from_workspace(&ws_dir) {
37                println!("   {} Sessions: {}", "[*]".blue(), sessions.len());
38
39                if !sessions.is_empty() {
40                    let total_messages: usize =
41                        sessions.iter().map(|s| s.session.request_count()).sum();
42                    println!("   {} Total Messages: {}", "[*]".blue(), total_messages);
43                }
44            }
45
46            // Detect provider
47            println!("\n{} Provider Detection:", "[*]".blue());
48            println!(
49                "   {} Provider: {}",
50                "[*]".blue(),
51                "GitHub Copilot (VS Code)".cyan()
52            );
53        }
54        None => {
55            println!("\n{} No workspace found for this path", "[X]".red());
56            println!(
57                "{} The project may not have been opened in VS Code yet",
58                "[i]".yellow()
59            );
60
61            // Check if there are similar workspaces
62            let all_workspaces = discover_workspaces()?;
63            let path_lower = project_path.to_lowercase();
64            let similar: Vec<&Workspace> = all_workspaces
65                .iter()
66                .filter(|ws| {
67                    ws.project_path
68                        .as_ref()
69                        .map(|p| {
70                            p.to_lowercase().contains(&path_lower)
71                                || path_lower.contains(&p.to_lowercase())
72                        })
73                        .unwrap_or(false)
74                })
75                .take(5)
76                .collect();
77
78            if !similar.is_empty() {
79                println!("\n{} Similar workspaces found:", "[i]".cyan());
80                for ws in similar {
81                    if let Some(p) = &ws.project_path {
82                        println!("   {} {}...", p.cyan(), &ws.hash[..8.min(ws.hash.len())]);
83                    }
84                }
85            }
86        }
87    }
88
89    Ok(())
90}
91
92/// Detect available providers and their status
93pub fn detect_providers(with_sessions: bool) -> Result<()> {
94    println!("\n{} Detecting Providers", "[D]".blue().bold());
95    println!("{}", "=".repeat(60));
96
97    let registry = ProviderRegistry::new();
98    let mut found_count = 0;
99    let mut with_sessions_count = 0;
100
101    let all_provider_types = vec![
102        ProviderType::Copilot,
103        ProviderType::Cursor,
104        ProviderType::Ollama,
105        ProviderType::Vllm,
106        ProviderType::Foundry,
107        ProviderType::LmStudio,
108        ProviderType::LocalAI,
109        ProviderType::TextGenWebUI,
110        ProviderType::Jan,
111        ProviderType::Gpt4All,
112        ProviderType::Llamafile,
113    ];
114
115    for provider_type in all_provider_types {
116        if let Some(provider) = registry.get_provider(provider_type) {
117            let available = provider.is_available();
118            let session_count = if available {
119                provider.list_sessions().map(|s| s.len()).unwrap_or(0)
120            } else {
121                0
122            };
123
124            if with_sessions && session_count == 0 {
125                continue;
126            }
127
128            found_count += 1;
129            if session_count > 0 {
130                with_sessions_count += 1;
131            }
132
133            let status = if available {
134                if session_count > 0 {
135                    format!(
136                        "{} ({} sessions)",
137                        "+".green(),
138                        session_count.to_string().cyan()
139                    )
140                } else {
141                    format!("{} (no sessions)", "+".green())
142                }
143            } else {
144                format!("{} not available", "x".red())
145            };
146
147            println!("   {} {}: {}", "[*]".blue(), provider.name().bold(), status);
148
149            // Show endpoint for API-based providers
150            if available {
151                if let Some(endpoint) = provider_type.default_endpoint() {
152                    println!("      {} Endpoint: {}", "`".dimmed(), endpoint.dimmed());
153                }
154                if let Some(path) = provider.sessions_path() {
155                    println!(
156                        "      {} Path: {}",
157                        "`".dimmed(),
158                        path.display().to_string().dimmed()
159                    );
160                }
161            }
162        }
163    }
164
165    println!("\n{} Summary:", "[*]".green().bold());
166    println!("   {} providers available", found_count.to_string().cyan());
167    println!(
168        "   {} providers with sessions",
169        with_sessions_count.to_string().cyan()
170    );
171
172    Ok(())
173}
174
175/// Detect which provider a session belongs to
176pub fn detect_session(session_id: &str, path: Option<&str>) -> Result<()> {
177    println!("\n{} Detecting Session Provider", "[D]".blue().bold());
178    println!("{}", "=".repeat(60));
179    println!("{} Session: {}", "[*]".blue(), session_id.cyan());
180
181    let registry = ProviderRegistry::new();
182    let session_lower = session_id.to_lowercase();
183    let mut found = false;
184
185    // First check VS Code workspaces
186    let project_path = path.map(|p| p.to_string()).unwrap_or_else(|| {
187        std::env::current_dir()
188            .map(|p| p.to_string_lossy().to_string())
189            .unwrap_or_else(|_| ".".to_string())
190    });
191
192    // Check in VS Code/Copilot workspaces
193    if let Ok(Some((_ws_id, ws_dir, ws_name))) = find_workspace_by_path(&project_path) {
194        if let Ok(sessions) = get_chat_sessions_from_workspace(&ws_dir) {
195            for swp in &sessions {
196                let sid = swp
197                    .session
198                    .session_id
199                    .as_ref()
200                    .map(|s| s.to_lowercase())
201                    .unwrap_or_default();
202                let title = swp.session.title().to_lowercase();
203                let filename = swp
204                    .path
205                    .file_name()
206                    .map(|f| f.to_string_lossy().to_lowercase())
207                    .unwrap_or_default();
208
209                if sid.contains(&session_lower)
210                    || title.contains(&session_lower)
211                    || filename.contains(&session_lower)
212                {
213                    found = true;
214                    println!("\n{} Session Found!", "[+]".green().bold());
215                    println!("   {} Provider: {}", "[*]".blue(), "GitHub Copilot".cyan());
216                    println!("   {} Title: {}", "[*]".blue(), swp.session.title());
217                    println!("   {} File: {}", "[*]".blue(), swp.path.display());
218                    println!(
219                        "   {} Messages: {}",
220                        "[*]".blue(),
221                        swp.session.request_count()
222                    );
223                    if let Some(name) = &ws_name {
224                        println!("   {} Workspace: {}", "[*]".blue(), name);
225                    }
226                    break;
227                }
228            }
229        }
230    }
231
232    // Check other providers
233    if !found {
234        let provider_types = vec![
235            ProviderType::Cursor,
236            ProviderType::Ollama,
237            ProviderType::Jan,
238            ProviderType::Gpt4All,
239            ProviderType::LmStudio,
240        ];
241
242        for provider_type in provider_types {
243            if let Some(provider) = registry.get_provider(provider_type) {
244                if provider.is_available() {
245                    if let Ok(sessions) = provider.list_sessions() {
246                        for session in sessions {
247                            let sid = session
248                                .session_id
249                                .as_ref()
250                                .map(|s| s.to_lowercase())
251                                .unwrap_or_default();
252                            let title = session.title().to_lowercase();
253
254                            if sid.contains(&session_lower) || title.contains(&session_lower) {
255                                found = true;
256                                println!("\n{} Session Found!", "[+]".green().bold());
257                                println!(
258                                    "   {} Provider: {}",
259                                    "[*]".blue(),
260                                    provider.name().cyan()
261                                );
262                                println!("   {} Title: {}", "[*]".blue(), session.title());
263                                println!(
264                                    "   {} Messages: {}",
265                                    "[*]".blue(),
266                                    session.request_count()
267                                );
268                                break;
269                            }
270                        }
271                    }
272                }
273            }
274            if found {
275                break;
276            }
277        }
278    }
279
280    if !found {
281        println!("\n{} Session not found", "[X]".red());
282        println!(
283            "{} Try providing a more specific session ID or check the path",
284            "[i]".yellow()
285        );
286    }
287
288    Ok(())
289}
290
291/// Detect everything (workspace, providers, sessions) for a path
292pub fn detect_all(path: Option<&str>, verbose: bool) -> Result<()> {
293    let project_path = path.map(|p| p.to_string()).unwrap_or_else(|| {
294        std::env::current_dir()
295            .map(|p| p.to_string_lossy().to_string())
296            .unwrap_or_else(|_| ".".to_string())
297    });
298
299    println!("\n{} Auto-Detection Report", "[D]".blue().bold());
300    println!("{}", "=".repeat(70));
301    println!("{} Path: {}", "[*]".blue(), project_path.cyan());
302    println!();
303
304    // 1. Workspace Detection
305    println!("{} Workspace", "---".dimmed());
306    let workspace_info = find_workspace_by_path(&project_path)?;
307
308    match &workspace_info {
309        Some((ws_id, ws_dir, ws_name)) => {
310            println!("   {} Status: {}", "[+]".green(), "Found".green());
311            println!(
312                "   {} ID: {}...",
313                "[*]".blue(),
314                &ws_id[..16.min(ws_id.len())]
315            );
316            if let Some(name) = ws_name {
317                println!("   {} Name: {}", "[*]".blue(), name.cyan());
318            }
319
320            // Get sessions from workspace
321            if let Ok(sessions) = get_chat_sessions_from_workspace(ws_dir) {
322                println!("   {} Sessions: {}", "[*]".blue(), sessions.len());
323
324                if verbose && !sessions.is_empty() {
325                    println!("\n   {} Recent Sessions:", "[*]".blue());
326                    for (i, swp) in sessions.iter().take(5).enumerate() {
327                        println!(
328                            "      {}. {} ({} messages)",
329                            i + 1,
330                            truncate(&swp.session.title(), 40),
331                            swp.session.request_count()
332                        );
333                    }
334                    if sessions.len() > 5 {
335                        println!("      ... and {} more", sessions.len() - 5);
336                    }
337                }
338            }
339        }
340        None => {
341            println!("   {} Status: {}", "[X]".red(), "Not found".red());
342            println!(
343                "   {} Open this project in VS Code to create a workspace",
344                "[i]".yellow()
345            );
346        }
347    }
348    println!();
349
350    // 2. Provider Detection
351    println!("{} Available Providers", "---".dimmed());
352
353    let registry = ProviderRegistry::new();
354    let provider_types = vec![
355        ProviderType::Copilot,
356        ProviderType::Cursor,
357        ProviderType::Ollama,
358        ProviderType::Vllm,
359        ProviderType::Foundry,
360        ProviderType::LmStudio,
361        ProviderType::LocalAI,
362        ProviderType::TextGenWebUI,
363        ProviderType::Jan,
364        ProviderType::Gpt4All,
365        ProviderType::Llamafile,
366    ];
367
368    let mut total_sessions = 0;
369    let mut provider_summary: Vec<(String, usize)> = Vec::new();
370
371    for provider_type in provider_types {
372        if let Some(provider) = registry.get_provider(provider_type) {
373            if provider.is_available() {
374                let session_count = provider.list_sessions().map(|s| s.len()).unwrap_or(0);
375
376                if session_count > 0 || verbose {
377                    let status = if session_count > 0 {
378                        format!("{} sessions", session_count.to_string().cyan())
379                    } else {
380                        "ready".dimmed().to_string()
381                    };
382                    println!("   {} {}: {}", "[+]".green(), provider.name(), status);
383
384                    total_sessions += session_count;
385                    if session_count > 0 {
386                        provider_summary.push((provider.name().to_string(), session_count));
387                    }
388                }
389            }
390        }
391    }
392
393    if provider_summary.is_empty() && !verbose {
394        println!("   {} No providers with sessions found", "[i]".yellow());
395        println!(
396            "   {} Use --verbose to see all available providers",
397            "[i]".dimmed()
398        );
399    }
400    println!();
401
402    // 3. Summary
403    println!("{} Summary", "---".dimmed());
404
405    let ws_status = if workspace_info.is_some() {
406        "Yes".green()
407    } else {
408        "No".red()
409    };
410    println!("   {} Workspace detected: {}", "[*]".blue(), ws_status);
411    println!(
412        "   {} Total providers with sessions: {}",
413        "[*]".blue(),
414        provider_summary.len()
415    );
416    println!(
417        "   {} Total sessions across providers: {}",
418        "[*]".blue(),
419        total_sessions
420    );
421
422    // 4. Recommendations
423    if workspace_info.is_none() || total_sessions == 0 {
424        println!();
425        println!("{} Recommendations", "---".dimmed());
426
427        if workspace_info.is_none() {
428            println!(
429                "   {} Open this project in VS Code to enable chat history tracking",
430                "[->]".cyan()
431            );
432        }
433
434        if total_sessions == 0 {
435            println!(
436                "   {} Start a chat session in your IDE to create history",
437                "[->]".cyan()
438            );
439        }
440    }
441
442    Ok(())
443}
444
445/// Helper function to truncate strings
446fn truncate(s: &str, max_len: usize) -> String {
447    if s.len() <= max_len {
448        s.to_string()
449    } else {
450        format!("{}...", &s[..max_len - 3])
451    }
452}