1use 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
14pub 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 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 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 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
92pub 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 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
175pub 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 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 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 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
291pub 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 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 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 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 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 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
445fn 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}
453
454pub fn detect_orphaned(path: Option<&str>, recover: bool) -> Result<()> {
457 use crate::models::WorkspaceJson;
458 use crate::workspace::{decode_workspace_folder, get_workspace_storage_path, normalize_path};
459
460 let project_path = path.map(|p| p.to_string()).unwrap_or_else(|| {
461 std::env::current_dir()
462 .map(|p| p.to_string_lossy().to_string())
463 .unwrap_or_else(|_| ".".to_string())
464 });
465
466 println!("\n{} Scanning for Orphaned Sessions", "[D]".blue().bold());
467 println!("{}", "=".repeat(60));
468 println!("{} Path: {}", "[*]".blue(), project_path.cyan());
469
470 let storage_path = get_workspace_storage_path()?;
471 let target_path = normalize_path(&project_path);
472
473 let mut all_workspaces: Vec<(String, std::path::PathBuf, usize, std::time::SystemTime)> =
475 Vec::new();
476
477 for entry in std::fs::read_dir(&storage_path)? {
478 let entry = entry?;
479 let workspace_dir = entry.path();
480
481 if !workspace_dir.is_dir() {
482 continue;
483 }
484
485 let workspace_json_path = workspace_dir.join("workspace.json");
486 if !workspace_json_path.exists() {
487 continue;
488 }
489
490 if let Ok(content) = std::fs::read_to_string(&workspace_json_path) {
491 if let Ok(ws_json) = serde_json::from_str::<WorkspaceJson>(&content) {
492 if let Some(folder) = &ws_json.folder {
493 let folder_path = decode_workspace_folder(folder);
494 if normalize_path(&folder_path) == target_path {
495 let chat_sessions_dir = workspace_dir.join("chatSessions");
497 let session_count = if chat_sessions_dir.exists() {
498 std::fs::read_dir(&chat_sessions_dir)
499 .map(|entries| {
500 entries
501 .filter_map(|e| e.ok())
502 .filter(|e| {
503 e.path()
504 .extension()
505 .map(|ext| ext == "json")
506 .unwrap_or(false)
507 })
508 .count()
509 })
510 .unwrap_or(0)
511 } else {
512 0
513 };
514
515 let last_modified = if chat_sessions_dir.exists() {
517 std::fs::read_dir(&chat_sessions_dir)
518 .ok()
519 .and_then(|entries| {
520 entries
521 .filter_map(|e| e.ok())
522 .filter_map(|e| e.metadata().ok())
523 .filter_map(|m| m.modified().ok())
524 .max()
525 })
526 .unwrap_or(std::time::UNIX_EPOCH)
527 } else {
528 std::time::UNIX_EPOCH
529 };
530
531 all_workspaces.push((
532 entry.file_name().to_string_lossy().to_string(),
533 workspace_dir,
534 session_count,
535 last_modified,
536 ));
537 }
538 }
539 }
540 }
541 }
542
543 if all_workspaces.is_empty() {
544 println!("\n{} No workspaces found for this path", "[X]".red());
545 return Ok(());
546 }
547
548 all_workspaces.sort_by(|a, b| b.3.cmp(&a.3));
550
551 let active_dir = all_workspaces[0].1.clone();
553
554 println!(
555 "\n{} Found {} workspace(s) for this path:",
556 "[+]".green().bold(),
557 all_workspaces.len()
558 );
559
560 let mut total_orphaned_sessions = 0;
561 let mut orphaned_workspaces: Vec<(String, std::path::PathBuf, usize)> = Vec::new();
562
563 for (i, (hash, dir, session_count, _)) in all_workspaces.iter().enumerate() {
564 let is_active = i == 0;
565 let status = if is_active {
566 format!("{}", "(active)".green())
567 } else {
568 format!("{}", "(orphaned)".yellow())
569 };
570
571 let session_str = if *session_count > 0 {
572 format!("{} sessions", session_count.to_string().cyan())
573 } else {
574 "0 sessions".dimmed().to_string()
575 };
576
577 println!(
578 " {} {}... {} - {}",
579 if is_active {
580 "[*]".green()
581 } else {
582 "[!]".yellow()
583 },
584 &hash[..16.min(hash.len())],
585 status,
586 session_str
587 );
588
589 if !is_active && *session_count > 0 {
590 total_orphaned_sessions += session_count;
591 orphaned_workspaces.push((hash.clone(), dir.clone(), *session_count));
592
593 let chat_sessions_dir = dir.join("chatSessions");
595 if let Ok(entries) = std::fs::read_dir(&chat_sessions_dir) {
596 for entry in entries.filter_map(|e| e.ok()).take(3) {
597 let path = entry.path();
598 if path.extension().map(|e| e == "json").unwrap_or(false) {
599 if let Ok(content) = std::fs::read_to_string(&path) {
600 if let Ok(session) = crate::storage::parse_session_json(&content) {
601 let size = entry.metadata().map(|m| m.len()).unwrap_or(0);
602 let size_str = if size > 1_000_000 {
603 format!("{:.1}MB", size as f64 / 1_000_000.0)
604 } else if size > 1000 {
605 format!("{:.1}KB", size as f64 / 1000.0)
606 } else {
607 format!("{}B", size)
608 };
609 println!(
610 " {} {} ({}, {} msgs)",
611 "`".dimmed(),
612 truncate(&session.title(), 45),
613 size_str.cyan(),
614 session.request_count()
615 );
616 }
617 }
618 }
619 }
620 }
621 }
622 }
623
624 println!();
626 if total_orphaned_sessions > 0 {
627 println!(
628 "{} {} orphaned session(s) found in {} workspace(s)",
629 "[!]".yellow().bold(),
630 total_orphaned_sessions.to_string().yellow(),
631 orphaned_workspaces.len()
632 );
633
634 if recover {
635 println!("\n{} Recovering orphaned sessions...", "[*]".blue());
637
638 let active_chat_sessions = active_dir.join("chatSessions");
639 if !active_chat_sessions.exists() {
640 std::fs::create_dir_all(&active_chat_sessions)?;
641 }
642
643 let mut recovered = 0;
644 for (hash, orphan_dir, _) in &orphaned_workspaces {
645 let orphan_sessions = orphan_dir.join("chatSessions");
646 if let Ok(entries) = std::fs::read_dir(&orphan_sessions) {
647 for entry in entries.filter_map(|e| e.ok()) {
648 let src = entry.path();
649 if src.extension().map(|e| e == "json").unwrap_or(false) {
650 let filename = src.file_name().unwrap();
651 let dest = active_chat_sessions.join(filename);
652 if !dest.exists() {
653 std::fs::copy(&src, &dest)?;
654 recovered += 1;
655 println!(
656 " {} Copied: {} (from {}...)",
657 "[+]".green(),
658 filename.to_string_lossy(),
659 &hash[..8]
660 );
661 }
662 }
663 }
664 }
665 }
666
667 println!(
668 "\n{} Recovered {} session(s)",
669 "[OK]".green().bold(),
670 recovered
671 );
672 println!(
673 "\n{} Run {} to make them visible in VS Code",
674 "[i]".cyan(),
675 "chasm register all --force".cyan()
676 );
677 } else {
678 println!(
679 "\n{} To recover, run: {}",
680 "[->]".cyan(),
681 format!(
682 "chasm detect orphaned --recover --path \"{}\"",
683 project_path
684 )
685 .cyan()
686 );
687 }
688 } else {
689 println!("{} No orphaned sessions found", "[OK]".green().bold());
690 }
691
692 Ok(())
693}