1use anyhow::Result;
6use colored::*;
7use std::collections::HashMap;
8
9use crate::models::{Workspace, WorkspaceJson};
10use crate::providers::{ProviderRegistry, ProviderType};
11use crate::storage::{is_vscode_running, register_all_sessions_from_directory};
12use crate::workspace::{
13 decode_workspace_folder, discover_workspaces, find_workspace_by_path,
14 get_chat_sessions_from_workspace, get_workspace_storage_path, normalize_path,
15};
16
17pub fn detect_workspace(path: Option<&str>) -> Result<()> {
19 let project_path = path.map(|p| p.to_string()).unwrap_or_else(|| {
20 std::env::current_dir()
21 .map(|p| p.to_string_lossy().to_string())
22 .unwrap_or_else(|_| ".".to_string())
23 });
24
25 println!("\n{} Detecting Workspace", "[D]".blue().bold());
26 println!("{}", "=".repeat(60));
27 println!("{} Path: {}", "[*]".blue(), project_path.cyan());
28
29 match find_workspace_by_path(&project_path)? {
30 Some((ws_id, ws_dir, ws_name)) => {
31 println!("\n{} Workspace Found!", "[+]".green().bold());
32 println!(" {} ID: {}", "[*]".blue(), &ws_id[..16.min(ws_id.len())]);
33 println!(" {} Directory: {}", "[*]".blue(), ws_dir.display());
34 if let Some(name) = ws_name {
35 println!(" {} Name: {}", "[*]".blue(), name.cyan());
36 }
37
38 if let Ok(sessions) = get_chat_sessions_from_workspace(&ws_dir) {
40 println!(" {} Sessions: {}", "[*]".blue(), sessions.len());
41
42 if !sessions.is_empty() {
43 let total_messages: usize =
44 sessions.iter().map(|s| s.session.request_count()).sum();
45 println!(" {} Total Messages: {}", "[*]".blue(), total_messages);
46 }
47 }
48
49 println!("\n{} Provider Detection:", "[*]".blue());
51 println!(
52 " {} Provider: {}",
53 "[*]".blue(),
54 "GitHub Copilot (VS Code)".cyan()
55 );
56 }
57 None => {
58 println!("\n{} No workspace found for this path", "[X]".red());
59 println!(
60 "{} The project may not have been opened in VS Code yet",
61 "[i]".yellow()
62 );
63
64 let all_workspaces = discover_workspaces()?;
66 let path_lower = project_path.to_lowercase();
67 let similar: Vec<&Workspace> = all_workspaces
68 .iter()
69 .filter(|ws| {
70 ws.project_path
71 .as_ref()
72 .map(|p| {
73 p.to_lowercase().contains(&path_lower)
74 || path_lower.contains(&p.to_lowercase())
75 })
76 .unwrap_or(false)
77 })
78 .take(5)
79 .collect();
80
81 if !similar.is_empty() {
82 println!("\n{} Similar workspaces found:", "[i]".cyan());
83 for ws in similar {
84 if let Some(p) = &ws.project_path {
85 println!(" {} {}...", p.cyan(), &ws.hash[..8.min(ws.hash.len())]);
86 }
87 }
88 }
89 }
90 }
91
92 Ok(())
93}
94
95pub fn detect_providers(with_sessions: bool) -> Result<()> {
97 println!("\n{} Detecting Providers", "[D]".blue().bold());
98 println!("{}", "=".repeat(60));
99
100 let registry = ProviderRegistry::new();
101 let mut found_count = 0;
102 let mut with_sessions_count = 0;
103
104 let all_provider_types = vec![
105 ProviderType::Copilot,
106 ProviderType::Cursor,
107 ProviderType::Ollama,
108 ProviderType::Vllm,
109 ProviderType::Foundry,
110 ProviderType::LmStudio,
111 ProviderType::LocalAI,
112 ProviderType::TextGenWebUI,
113 ProviderType::Jan,
114 ProviderType::Gpt4All,
115 ProviderType::Llamafile,
116 ];
117
118 for provider_type in all_provider_types {
119 if let Some(provider) = registry.get_provider(provider_type) {
120 let available = provider.is_available();
121 let session_count = if available {
122 provider.list_sessions().map(|s| s.len()).unwrap_or(0)
123 } else {
124 0
125 };
126
127 if with_sessions && session_count == 0 {
128 continue;
129 }
130
131 found_count += 1;
132 if session_count > 0 {
133 with_sessions_count += 1;
134 }
135
136 let status = if available {
137 if session_count > 0 {
138 format!(
139 "{} ({} sessions)",
140 "+".green(),
141 session_count.to_string().cyan()
142 )
143 } else {
144 format!("{} (no sessions)", "+".green())
145 }
146 } else {
147 format!("{} not available", "x".red())
148 };
149
150 println!(" {} {}: {}", "[*]".blue(), provider.name().bold(), status);
151
152 if available {
154 if let Some(endpoint) = provider_type.default_endpoint() {
155 println!(" {} Endpoint: {}", "`".dimmed(), endpoint.dimmed());
156 }
157 if let Some(path) = provider.sessions_path() {
158 println!(
159 " {} Path: {}",
160 "`".dimmed(),
161 path.display().to_string().dimmed()
162 );
163 }
164 }
165 }
166 }
167
168 println!("\n{} Summary:", "[*]".green().bold());
169 println!(" {} providers available", found_count.to_string().cyan());
170 println!(
171 " {} providers with sessions",
172 with_sessions_count.to_string().cyan()
173 );
174
175 Ok(())
176}
177
178pub fn detect_session(session_id: &str, path: Option<&str>) -> Result<()> {
180 println!("\n{} Detecting Session Provider", "[D]".blue().bold());
181 println!("{}", "=".repeat(60));
182 println!("{} Session: {}", "[*]".blue(), session_id.cyan());
183
184 let registry = ProviderRegistry::new();
185 let session_lower = session_id.to_lowercase();
186 let mut found = false;
187
188 let project_path = path.map(|p| p.to_string()).unwrap_or_else(|| {
190 std::env::current_dir()
191 .map(|p| p.to_string_lossy().to_string())
192 .unwrap_or_else(|_| ".".to_string())
193 });
194
195 if let Ok(Some((_ws_id, ws_dir, ws_name))) = find_workspace_by_path(&project_path) {
197 if let Ok(sessions) = get_chat_sessions_from_workspace(&ws_dir) {
198 for swp in &sessions {
199 let sid = swp
200 .session
201 .session_id
202 .as_ref()
203 .map(|s| s.to_lowercase())
204 .unwrap_or_default();
205 let title = swp.session.title().to_lowercase();
206 let filename = swp
207 .path
208 .file_name()
209 .map(|f| f.to_string_lossy().to_lowercase())
210 .unwrap_or_default();
211
212 if sid.contains(&session_lower)
213 || title.contains(&session_lower)
214 || filename.contains(&session_lower)
215 {
216 found = true;
217 println!("\n{} Session Found!", "[+]".green().bold());
218 println!(" {} Provider: {}", "[*]".blue(), "GitHub Copilot".cyan());
219 println!(" {} Title: {}", "[*]".blue(), swp.session.title());
220 println!(" {} File: {}", "[*]".blue(), swp.path.display());
221 println!(
222 " {} Messages: {}",
223 "[*]".blue(),
224 swp.session.request_count()
225 );
226 if let Some(name) = &ws_name {
227 println!(" {} Workspace: {}", "[*]".blue(), name);
228 }
229 break;
230 }
231 }
232 }
233 }
234
235 if !found {
237 let provider_types = vec![
238 ProviderType::Cursor,
239 ProviderType::Ollama,
240 ProviderType::Jan,
241 ProviderType::Gpt4All,
242 ProviderType::LmStudio,
243 ];
244
245 for provider_type in provider_types {
246 if let Some(provider) = registry.get_provider(provider_type) {
247 if provider.is_available() {
248 if let Ok(sessions) = provider.list_sessions() {
249 for session in sessions {
250 let sid = session
251 .session_id
252 .as_ref()
253 .map(|s| s.to_lowercase())
254 .unwrap_or_default();
255 let title = session.title().to_lowercase();
256
257 if sid.contains(&session_lower) || title.contains(&session_lower) {
258 found = true;
259 println!("\n{} Session Found!", "[+]".green().bold());
260 println!(
261 " {} Provider: {}",
262 "[*]".blue(),
263 provider.name().cyan()
264 );
265 println!(" {} Title: {}", "[*]".blue(), session.title());
266 println!(
267 " {} Messages: {}",
268 "[*]".blue(),
269 session.request_count()
270 );
271 break;
272 }
273 }
274 }
275 }
276 }
277 if found {
278 break;
279 }
280 }
281 }
282
283 if !found {
284 println!("\n{} Session not found", "[X]".red());
285 println!(
286 "{} Try providing a more specific session ID or check the path",
287 "[i]".yellow()
288 );
289 }
290
291 Ok(())
292}
293
294pub fn detect_all(path: Option<&str>, verbose: bool) -> Result<()> {
296 let project_path = path.map(|p| p.to_string()).unwrap_or_else(|| {
297 std::env::current_dir()
298 .map(|p| p.to_string_lossy().to_string())
299 .unwrap_or_else(|_| ".".to_string())
300 });
301
302 println!("\n{} Auto-Detection Report", "[D]".blue().bold());
303 println!("{}", "=".repeat(70));
304 println!("{} Path: {}", "[*]".blue(), project_path.cyan());
305 println!();
306
307 println!("{} Workspace", "---".dimmed());
309 let workspace_info = find_workspace_by_path(&project_path)?;
310
311 match &workspace_info {
312 Some((ws_id, ws_dir, ws_name)) => {
313 println!(" {} Status: {}", "[+]".green(), "Found".green());
314 println!(
315 " {} ID: {}...",
316 "[*]".blue(),
317 &ws_id[..16.min(ws_id.len())]
318 );
319 if let Some(name) = ws_name {
320 println!(" {} Name: {}", "[*]".blue(), name.cyan());
321 }
322
323 if let Ok(sessions) = get_chat_sessions_from_workspace(ws_dir) {
325 println!(" {} Sessions: {}", "[*]".blue(), sessions.len());
326
327 if verbose && !sessions.is_empty() {
328 println!("\n {} Recent Sessions:", "[*]".blue());
329 for (i, swp) in sessions.iter().take(5).enumerate() {
330 println!(
331 " {}. {} ({} messages)",
332 i + 1,
333 truncate(&swp.session.title(), 40),
334 swp.session.request_count()
335 );
336 }
337 if sessions.len() > 5 {
338 println!(" ... and {} more", sessions.len() - 5);
339 }
340 }
341 }
342 }
343 None => {
344 println!(" {} Status: {}", "[X]".red(), "Not found".red());
345 println!(
346 " {} Open this project in VS Code to create a workspace",
347 "[i]".yellow()
348 );
349 }
350 }
351 println!();
352
353 println!("{} Available Providers", "---".dimmed());
355
356 let registry = ProviderRegistry::new();
357 let provider_types = vec![
358 ProviderType::Copilot,
359 ProviderType::Cursor,
360 ProviderType::Ollama,
361 ProviderType::Vllm,
362 ProviderType::Foundry,
363 ProviderType::LmStudio,
364 ProviderType::LocalAI,
365 ProviderType::TextGenWebUI,
366 ProviderType::Jan,
367 ProviderType::Gpt4All,
368 ProviderType::Llamafile,
369 ];
370
371 let mut total_sessions = 0;
372 let mut provider_summary: Vec<(String, usize)> = Vec::new();
373
374 for provider_type in provider_types {
375 if let Some(provider) = registry.get_provider(provider_type) {
376 if provider.is_available() {
377 let session_count = provider.list_sessions().map(|s| s.len()).unwrap_or(0);
378
379 if session_count > 0 || verbose {
380 let status = if session_count > 0 {
381 format!("{} sessions", session_count.to_string().cyan())
382 } else {
383 "ready".dimmed().to_string()
384 };
385 println!(" {} {}: {}", "[+]".green(), provider.name(), status);
386
387 total_sessions += session_count;
388 if session_count > 0 {
389 provider_summary.push((provider.name().to_string(), session_count));
390 }
391 }
392 }
393 }
394 }
395
396 if provider_summary.is_empty() && !verbose {
397 println!(" {} No providers with sessions found", "[i]".yellow());
398 println!(
399 " {} Use --verbose to see all available providers",
400 "[i]".dimmed()
401 );
402 }
403 println!();
404
405 println!("{} Summary", "---".dimmed());
407
408 let ws_status = if workspace_info.is_some() {
409 "Yes".green()
410 } else {
411 "No".red()
412 };
413 println!(" {} Workspace detected: {}", "[*]".blue(), ws_status);
414 println!(
415 " {} Total providers with sessions: {}",
416 "[*]".blue(),
417 provider_summary.len()
418 );
419 println!(
420 " {} Total sessions across providers: {}",
421 "[*]".blue(),
422 total_sessions
423 );
424
425 if workspace_info.is_none() || total_sessions == 0 {
427 println!();
428 println!("{} Recommendations", "---".dimmed());
429
430 if workspace_info.is_none() {
431 println!(
432 " {} Open this project in VS Code to enable chat history tracking",
433 "[->]".cyan()
434 );
435 }
436
437 if total_sessions == 0 {
438 println!(
439 " {} Start a chat session in your IDE to create history",
440 "[->]".cyan()
441 );
442 }
443 }
444
445 Ok(())
446}
447
448fn truncate(s: &str, max_len: usize) -> String {
450 if s.len() <= max_len {
451 s.to_string()
452 } else {
453 format!("{}...", &s[..max_len - 3])
454 }
455}
456
457pub fn detect_orphaned(path: Option<&str>, recover: bool) -> Result<()> {
460 use crate::models::WorkspaceJson;
461 use crate::workspace::{decode_workspace_folder, get_workspace_storage_path, normalize_path};
462
463 let project_path = path.map(|p| p.to_string()).unwrap_or_else(|| {
464 std::env::current_dir()
465 .map(|p| p.to_string_lossy().to_string())
466 .unwrap_or_else(|_| ".".to_string())
467 });
468
469 println!("\n{} Scanning for Orphaned Sessions", "[D]".blue().bold());
470 println!("{}", "=".repeat(60));
471 println!("{} Path: {}", "[*]".blue(), project_path.cyan());
472
473 let storage_path = get_workspace_storage_path()?;
474 let target_path = normalize_path(&project_path);
475
476 let mut all_workspaces: Vec<(String, std::path::PathBuf, usize, std::time::SystemTime)> =
478 Vec::new();
479
480 for entry in std::fs::read_dir(&storage_path)? {
481 let entry = entry?;
482 let workspace_dir = entry.path();
483
484 if !workspace_dir.is_dir() {
485 continue;
486 }
487
488 let workspace_json_path = workspace_dir.join("workspace.json");
489 if !workspace_json_path.exists() {
490 continue;
491 }
492
493 if let Ok(content) = std::fs::read_to_string(&workspace_json_path) {
494 if let Ok(ws_json) = serde_json::from_str::<WorkspaceJson>(&content) {
495 if let Some(folder) = &ws_json.folder {
496 let folder_path = decode_workspace_folder(folder);
497 if normalize_path(&folder_path) == target_path {
498 let chat_sessions_dir = workspace_dir.join("chatSessions");
500 let session_count = if chat_sessions_dir.exists() {
501 std::fs::read_dir(&chat_sessions_dir)
502 .map(|entries| {
503 entries
504 .filter_map(|e| e.ok())
505 .filter(|e| {
506 e.path()
507 .extension()
508 .map(|ext| {
509 ext == "json"
510 || ext == "jsonl"
511 || ext == "backup"
512 })
513 .unwrap_or(false)
514 })
515 .count()
516 })
517 .unwrap_or(0)
518 } else {
519 0
520 };
521
522 let last_modified = if chat_sessions_dir.exists() {
524 std::fs::read_dir(&chat_sessions_dir)
525 .ok()
526 .and_then(|entries| {
527 entries
528 .filter_map(|e| e.ok())
529 .filter_map(|e| e.metadata().ok())
530 .filter_map(|m| m.modified().ok())
531 .max()
532 })
533 .unwrap_or(std::time::UNIX_EPOCH)
534 } else {
535 std::time::UNIX_EPOCH
536 };
537
538 all_workspaces.push((
539 entry.file_name().to_string_lossy().to_string(),
540 workspace_dir,
541 session_count,
542 last_modified,
543 ));
544 }
545 }
546 }
547 }
548 }
549
550 if all_workspaces.is_empty() {
551 println!("\n{} No workspaces found for this path", "[X]".red());
552 return Ok(());
553 }
554
555 all_workspaces.sort_by(|a, b| b.3.cmp(&a.3));
557
558 let active_dir = all_workspaces[0].1.clone();
560
561 println!(
562 "\n{} Found {} workspace(s) for this path:",
563 "[+]".green().bold(),
564 all_workspaces.len()
565 );
566
567 let mut total_orphaned_sessions = 0;
568 let mut orphaned_workspaces: Vec<(String, std::path::PathBuf, usize)> = Vec::new();
569
570 for (i, (hash, dir, session_count, _)) in all_workspaces.iter().enumerate() {
571 let is_active = i == 0;
572 let status = if is_active {
573 format!("{}", "(active)".green())
574 } else {
575 format!("{}", "(orphaned)".yellow())
576 };
577
578 let session_str = if *session_count > 0 {
579 format!("{} sessions", session_count.to_string().cyan())
580 } else {
581 "0 sessions".dimmed().to_string()
582 };
583
584 println!(
585 " {} {}... {} - {}",
586 if is_active {
587 "[*]".green()
588 } else {
589 "[!]".yellow()
590 },
591 &hash[..16.min(hash.len())],
592 status,
593 session_str
594 );
595
596 if !is_active && *session_count > 0 {
597 total_orphaned_sessions += session_count;
598 orphaned_workspaces.push((hash.clone(), dir.clone(), *session_count));
599
600 let chat_sessions_dir = dir.join("chatSessions");
602 if let Ok(entries) = std::fs::read_dir(&chat_sessions_dir) {
603 for entry in entries.filter_map(|e| e.ok()).take(3) {
604 let path = entry.path();
605 if path.extension().map(|e| e == "json").unwrap_or(false) {
606 if let Ok(content) = std::fs::read_to_string(&path) {
607 if let Ok(session) = crate::storage::parse_session_json(&content) {
608 let size = entry.metadata().map(|m| m.len()).unwrap_or(0);
609 let size_str = if size > 1_000_000 {
610 format!("{:.1}MB", size as f64 / 1_000_000.0)
611 } else if size > 1000 {
612 format!("{:.1}KB", size as f64 / 1000.0)
613 } else {
614 format!("{}B", size)
615 };
616 println!(
617 " {} {} ({}, {} msgs)",
618 "`".dimmed(),
619 truncate(&session.title(), 45),
620 size_str.cyan(),
621 session.request_count()
622 );
623 }
624 }
625 }
626 }
627 }
628 }
629 }
630
631 println!();
633 if total_orphaned_sessions > 0 {
634 println!(
635 "{} {} orphaned session(s) found in {} workspace(s)",
636 "[!]".yellow().bold(),
637 total_orphaned_sessions.to_string().yellow(),
638 orphaned_workspaces.len()
639 );
640
641 if recover {
642 println!("\n{} Recovering orphaned sessions...", "[*]".blue());
644
645 let active_chat_sessions = active_dir.join("chatSessions");
646 if !active_chat_sessions.exists() {
647 std::fs::create_dir_all(&active_chat_sessions)?;
648 }
649
650 let mut recovered = 0;
651 for (hash, orphan_dir, _) in &orphaned_workspaces {
652 let orphan_sessions = orphan_dir.join("chatSessions");
653 if let Ok(entries) = std::fs::read_dir(&orphan_sessions) {
654 for entry in entries.filter_map(|e| e.ok()) {
655 let src = entry.path();
656 let ext_match = src
657 .extension()
658 .map(|e| e == "json" || e == "jsonl" || e == "backup")
659 .unwrap_or(false);
660 let is_bak = src.to_string_lossy().ends_with(".bak")
661 || src.to_string_lossy().ends_with(".corrupt");
662 if ext_match && !is_bak {
663 let filename = src.file_name().unwrap();
664 let dest = active_chat_sessions.join(filename);
665 if !dest.exists() {
666 std::fs::copy(&src, &dest)?;
667 recovered += 1;
668 println!(
669 " {} Copied: {} (from {}...)",
670 "[+]".green(),
671 filename.to_string_lossy(),
672 &hash[..8]
673 );
674 }
675 }
676 }
677 }
678 }
679
680 println!(
681 "\n{} Recovered {} session(s)",
682 "[OK]".green().bold(),
683 recovered
684 );
685 println!(
686 "\n{} Run {} to make them visible in VS Code",
687 "[i]".cyan(),
688 "chasm register all --force".cyan()
689 );
690 } else {
691 println!(
692 "\n{} To recover, run: {}",
693 "[->]".cyan(),
694 format!(
695 "chasm detect orphaned --recover --path \"{}\"",
696 project_path
697 )
698 .cyan()
699 );
700 }
701 } else {
702 println!("{} No orphaned sessions found", "[OK]".green().bold());
703 }
704
705 Ok(())
706}
707
708pub fn recover_recursive(
714 root_path: Option<&str>,
715 max_depth: Option<usize>,
716 force: bool,
717 dry_run: bool,
718 exclude_patterns: &[String],
719 register: bool,
720) -> Result<()> {
721 let root = super::register::resolve_path(root_path);
722 let root_normalized = normalize_path(&root.to_string_lossy());
723
724 println!(
725 "\n{} Recovering orphaned sessions under: {}",
726 "[R]".green().bold(),
727 root.display()
728 );
729 println!("{}", "=".repeat(60));
730
731 if dry_run {
732 println!("{} Dry run mode — no changes will be made", "[!]".yellow());
733 }
734
735 if register && !force && !dry_run && is_vscode_running() {
737 println!(
738 "{} VS Code is running. Use {} to register anyway.",
739 "[!]".yellow(),
740 "--force".cyan()
741 );
742 }
743
744 let storage_path = get_workspace_storage_path()?;
745
746 let mut path_workspaces: HashMap<
750 String,
751 Vec<(String, std::path::PathBuf, usize, std::time::SystemTime)>,
752 > = HashMap::new();
753
754 for entry in std::fs::read_dir(&storage_path)? {
755 let entry = entry?;
756 let workspace_dir = entry.path();
757 if !workspace_dir.is_dir() {
758 continue;
759 }
760
761 let workspace_json_path = workspace_dir.join("workspace.json");
762 if !workspace_json_path.exists() {
763 continue;
764 }
765
766 let content = match std::fs::read_to_string(&workspace_json_path) {
767 Ok(c) => c,
768 Err(_) => continue,
769 };
770 let ws_json: WorkspaceJson = match serde_json::from_str(&content) {
771 Ok(j) => j,
772 Err(_) => continue,
773 };
774 let folder = match &ws_json.folder {
775 Some(f) => f.clone(),
776 None => continue,
777 };
778
779 let folder_path = decode_workspace_folder(&folder);
780 let normalized = normalize_path(&folder_path);
781
782 if !normalized.starts_with(&root_normalized) {
784 continue;
785 }
786
787 if let Some(max) = max_depth {
789 let suffix = &normalized[root_normalized.len()..];
790 let suffix = suffix.trim_start_matches(['/', '\\']);
791 let depth = if suffix.is_empty() {
792 0
793 } else {
794 suffix.matches(['/', '\\']).count() + 1
795 };
796 if depth > max {
797 continue;
798 }
799 }
800
801 let relative = &normalized[root_normalized.len()..];
803 let relative = relative.trim_start_matches(['/', '\\']);
804 let default_excludes = [
805 "node_modules",
806 ".git",
807 "target",
808 "build",
809 "dist",
810 ".venv",
811 "venv",
812 "__pycache__",
813 ".cache",
814 "vendor",
815 ".cargo",
816 ];
817 let skip = relative
818 .split(['/', '\\'])
819 .any(|c| default_excludes.contains(&c));
820 if skip {
821 continue;
822 }
823
824 let exclude_matchers: Vec<glob::Pattern> = exclude_patterns
825 .iter()
826 .filter_map(|p| glob::Pattern::new(p).ok())
827 .collect();
828 let dir_name = folder_path
829 .rsplit(['/', '\\'])
830 .next()
831 .unwrap_or("")
832 .to_lowercase();
833 let excluded_by_user = exclude_matchers
834 .iter()
835 .any(|p| p.matches(relative) || p.matches(&dir_name));
836 if excluded_by_user {
837 continue;
838 }
839
840 let chat_sessions_dir = workspace_dir.join("chatSessions");
842 let session_count = if chat_sessions_dir.exists() {
843 std::fs::read_dir(&chat_sessions_dir)
844 .map(|entries| {
845 entries
846 .filter_map(|e| e.ok())
847 .filter(|e| {
848 e.path()
849 .extension()
850 .map(|ext| ext == "json" || ext == "jsonl" || ext == "backup")
851 .unwrap_or(false)
852 })
853 .count()
854 })
855 .unwrap_or(0)
856 } else {
857 0
858 };
859
860 let last_modified = if chat_sessions_dir.exists() {
861 std::fs::read_dir(&chat_sessions_dir)
862 .ok()
863 .and_then(|entries| {
864 entries
865 .filter_map(|e| e.ok())
866 .filter_map(|e| e.metadata().ok())
867 .filter_map(|m| m.modified().ok())
868 .max()
869 })
870 .unwrap_or(std::time::UNIX_EPOCH)
871 } else {
872 std::time::UNIX_EPOCH
873 };
874
875 let hash = entry.file_name().to_string_lossy().to_string();
876 path_workspaces
877 .entry(normalized.clone())
878 .or_default()
879 .push((hash, workspace_dir, session_count, last_modified));
880 }
881
882 for workspaces in path_workspaces.values_mut() {
884 workspaces.sort_by(|a, b| b.3.cmp(&a.3));
885 }
886
887 let total_projects = path_workspaces.len();
890 let mut projects_with_orphans = 0;
891 let mut total_sessions_recovered = 0;
892 let mut total_sessions_registered = 0;
893 let mut processed = 0;
894
895 let mut project_paths: Vec<String> = path_workspaces.keys().cloned().collect();
897 project_paths.sort();
898
899 println!(
900 " Found {} unique project paths under this root",
901 total_projects.to_string().cyan()
902 );
903
904 for project_normalized in &project_paths {
905 let workspaces = &path_workspaces[project_normalized];
906 processed += 1;
907
908 if (processed) % 50 == 0 || processed == total_projects {
909 println!(
910 " ... scanning {}/{}",
911 processed.to_string().cyan(),
912 total_projects.to_string().white()
913 );
914 }
915
916 if workspaces.len() <= 1 {
917 continue;
919 }
920
921 let (ref active_hash, ref active_dir, active_count, _) = workspaces[0];
923 let orphaned = &workspaces[1..];
924
925 let orphaned_with_sessions: Vec<&(
927 String,
928 std::path::PathBuf,
929 usize,
930 std::time::SystemTime,
931 )> = orphaned
932 .iter()
933 .filter(|(_, _, count, _)| *count > 0)
934 .collect();
935
936 if orphaned_with_sessions.is_empty() {
937 continue;
938 }
939
940 let orphan_session_count: usize = orphaned_with_sessions.iter().map(|(_, _, c, _)| c).sum();
941 projects_with_orphans += 1;
942
943 let display_path = if project_normalized.len() > root_normalized.len() {
945 project_normalized[root_normalized.len()..].trim_start_matches(['/', '\\'])
946 } else {
947 project_normalized.as_str()
948 };
949
950 if dry_run {
951 println!(
952 " {} {} — {} workspace(s), active has {} sessions, {} orphaned session(s) in {} hash(es)",
953 "[DRY]".yellow(),
954 display_path.cyan(),
955 workspaces.len().to_string().white(),
956 active_count.to_string().white(),
957 orphan_session_count.to_string().yellow(),
958 orphaned_with_sessions.len().to_string().yellow(),
959 );
960 total_sessions_recovered += orphan_session_count;
961 continue;
962 }
963
964 let active_chat_sessions = active_dir.join("chatSessions");
966 if !active_chat_sessions.exists() {
967 std::fs::create_dir_all(&active_chat_sessions)?;
968 }
969
970 let mut recovered_this_project = 0;
971 for (_orphan_hash, orphan_dir, _, _) in &orphaned_with_sessions {
972 let orphan_sessions = orphan_dir.join("chatSessions");
973 if let Ok(entries) = std::fs::read_dir(&orphan_sessions) {
974 for entry in entries.filter_map(|e| e.ok()) {
975 let src = entry.path();
976 let ext_match = src
977 .extension()
978 .map(|e| e == "json" || e == "jsonl" || e == "backup")
979 .unwrap_or(false);
980 let is_bak = src.to_string_lossy().ends_with(".bak")
981 || src.to_string_lossy().ends_with(".corrupt");
982 if ext_match && !is_bak {
983 let filename = src.file_name().unwrap();
984 let dest = active_chat_sessions.join(filename);
985 if !dest.exists() {
986 std::fs::copy(&src, &dest)?;
987 recovered_this_project += 1;
988 }
989 }
990 }
991 }
992 }
993
994 if recovered_this_project > 0 {
995 println!(
996 " {} {} — recovered {} session(s) from {} orphaned hash(es)",
997 "[+]".green(),
998 display_path.cyan(),
999 recovered_this_project.to_string().green(),
1000 orphaned_with_sessions.len().to_string().white(),
1001 );
1002 total_sessions_recovered += recovered_this_project;
1003
1004 if register {
1006 match register_all_sessions_from_directory(
1007 active_hash,
1008 &active_chat_sessions,
1009 force,
1010 ) {
1011 Ok(registered) => {
1012 total_sessions_registered += registered;
1013 }
1014 Err(e) => {
1015 println!(
1016 " {} Failed to register for {}: {}",
1017 "[!]".red(),
1018 display_path,
1019 e
1020 );
1021 }
1022 }
1023 }
1024 }
1025 }
1026
1027 println!("\n{}", "═".repeat(60).cyan());
1029 println!("{} Recursive recovery complete", "[OK]".green().bold());
1030 println!("{}", "═".repeat(60).cyan());
1031 println!(
1032 " Projects scanned: {}",
1033 total_projects.to_string().cyan()
1034 );
1035 println!(
1036 " Projects with orphans: {}",
1037 projects_with_orphans.to_string().yellow()
1038 );
1039 println!(
1040 " Sessions recovered: {}",
1041 total_sessions_recovered.to_string().green()
1042 );
1043 if register {
1044 println!(
1045 " Sessions registered: {}",
1046 total_sessions_registered.to_string().green()
1047 );
1048 }
1049
1050 if !dry_run && total_sessions_recovered > 0 {
1051 if !register {
1052 println!(
1053 "\n{} Run {} to make them visible in VS Code",
1054 "[i]".cyan(),
1055 format!("chasm register recursive --force \"{}\"", root.display()).cyan()
1056 );
1057 } else {
1058 println!(
1059 "\n{} Reload VS Code (Developer: Reload Window) to see recovered sessions",
1060 "[i]".cyan()
1061 );
1062 }
1063 }
1064
1065 Ok(())
1066}