1use anyhow::Result;
10use colored::*;
11use std::collections::HashSet;
12use std::io::Write;
13use std::path::{Path, PathBuf};
14
15use crate::error::CsmError;
16use crate::models::ChatSession;
17use crate::storage::{
18 add_session_to_index, close_vscode_and_wait, diagnose_workspace_sessions,
19 get_workspace_storage_db, is_session_file_extension, is_vscode_running, parse_session_file,
20 parse_session_json, read_chat_session_index, register_all_sessions_from_directory,
21 reopen_vscode, repair_workspace_sessions, trim_session_jsonl,
22};
23use crate::workspace::{discover_workspaces, find_workspace_by_path, normalize_path};
24
25fn confirm_close_vscode(force: bool) -> bool {
28 if force {
29 return true;
30 }
31 print!(
32 "{} VS Code will be closed. Continue? [y/N] ",
33 "[?]".yellow()
34 );
35 std::io::stdout().flush().ok();
36 let mut input = String::new();
37 if std::io::stdin().read_line(&mut input).is_err() {
38 return false;
39 }
40 matches!(input.trim().to_lowercase().as_str(), "y" | "yes")
41}
42
43pub fn resolve_path(path: Option<&str>) -> PathBuf {
45 match path {
46 Some(p) => {
47 let path = PathBuf::from(p);
48 path.canonicalize().unwrap_or(path)
49 }
50 None => std::env::current_dir().unwrap_or_default(),
51 }
52}
53
54pub fn register_all(
56 project_path: Option<&str>,
57 merge: bool,
58 force: bool,
59 close_vscode: bool,
60 reopen: bool,
61) -> Result<()> {
62 let path = resolve_path(project_path);
63 let should_close = close_vscode || reopen;
65
66 if merge {
67 println!(
68 "{} Merging and registering all sessions for: {}",
69 "[CSM]".cyan().bold(),
70 path.display()
71 );
72
73 let path_str = path.to_string_lossy().to_string();
75 return crate::commands::history_merge(
76 Some(&path_str),
77 None, force, false, );
81 }
82
83 println!(
84 "{} Registering all sessions for: {}",
85 "[CSM]".cyan().bold(),
86 path.display()
87 );
88
89 let path_str = path.to_string_lossy().to_string();
91 let (ws_id, ws_path, _folder) = find_workspace_by_path(&path_str)?
92 .ok_or_else(|| CsmError::WorkspaceNotFound(path.display().to_string()))?;
93
94 let chat_sessions_dir = ws_path.join("chatSessions");
95
96 if !chat_sessions_dir.exists() {
97 println!(
98 "{} No chatSessions directory found at: {}",
99 "[!]".yellow(),
100 chat_sessions_dir.display()
101 );
102 return Ok(());
103 }
104
105 let vscode_was_running = is_vscode_running();
107 if vscode_was_running {
108 if should_close {
109 if !confirm_close_vscode(force) {
110 println!("{} Aborted.", "[!]".yellow());
111 return Ok(());
112 }
113 println!(" {} Closing VS Code (saving state)...", "[*]".yellow());
114 close_vscode_and_wait(30)?;
115 println!(" {} VS Code closed.", "[OK]".green());
116 } else if !force {
117 println!(
118 "{} VS Code is running. Its in-memory cache will overwrite index changes.",
119 "[!]".yellow()
120 );
121 println!(
122 " Use {} to close VS Code first, register, and reopen.",
123 "--reopen".cyan()
124 );
125 println!(
126 " Use {} to just close VS Code first.",
127 "--close-vscode".cyan()
128 );
129 println!(
130 " Use {} to write anyway (works after restarting VS Code).",
131 "--force".cyan()
132 );
133 return Err(CsmError::VSCodeRunning.into());
134 }
135 }
136
137 let sessions_on_disk = count_sessions_in_directory(&chat_sessions_dir)?;
139 println!(
140 " Found {} session files on disk",
141 sessions_on_disk.to_string().green()
142 );
143
144 let registered = register_all_sessions_from_directory(&ws_id, &chat_sessions_dir, true)?;
146
147 println!(
148 "\n{} Registered {} sessions in VS Code's index",
149 "[OK]".green().bold(),
150 registered.to_string().cyan()
151 );
152
153 if reopen && vscode_was_running {
155 println!(" {} Reopening VS Code...", "[*]".yellow());
156 reopen_vscode(Some(&path_str))?;
157 println!(
158 " {} VS Code launched. Sessions should appear in Copilot Chat history.",
159 "[OK]".green()
160 );
161 } else if should_close && vscode_was_running {
162 println!(
163 "\n{} VS Code was closed. Reopen it to see the recovered sessions.",
164 "[!]".yellow()
165 );
166 println!(" Run: {}", format!("code {}", path.display()).cyan());
167 } else if force && vscode_was_running {
168 println!(
170 "\n{} VS Code caches the session index in memory.",
171 "[!]".yellow()
172 );
173 println!(" To see the new sessions, do one of the following:");
174 println!(
175 " * Press {} and run {}",
176 "Ctrl+Shift+P".cyan(),
177 "Developer: Reload Window".cyan()
178 );
179 println!(" * Or restart VS Code");
180 }
181
182 Ok(())
183}
184
185pub fn register_sessions(
187 ids: &[String],
188 titles: Option<&[String]>,
189 project_path: Option<&str>,
190 force: bool,
191) -> Result<()> {
192 let path = resolve_path(project_path);
193
194 let path_str = path.to_string_lossy().to_string();
196 let (ws_id, ws_path, _folder) = find_workspace_by_path(&path_str)?
197 .ok_or_else(|| CsmError::WorkspaceNotFound(path.display().to_string()))?;
198
199 let chat_sessions_dir = ws_path.join("chatSessions");
200
201 if !force && is_vscode_running() {
203 println!(
204 "{} VS Code is running. Use {} to register anyway.",
205 "[!]".yellow(),
206 "--force".cyan()
207 );
208 return Err(CsmError::VSCodeRunning.into());
209 }
210
211 let db_path = get_workspace_storage_db(&ws_id)?;
213
214 let mut registered_count = 0;
215
216 if let Some(titles) = titles {
217 println!(
219 "{} Registering {} sessions by title:",
220 "[CSM]".cyan().bold(),
221 titles.len()
222 );
223
224 let sessions = find_sessions_by_titles(&chat_sessions_dir, titles)?;
225
226 for (session, session_path) in sessions {
227 let session_id = session.session_id.clone().unwrap_or_else(|| {
228 session_path
229 .file_stem()
230 .map(|s| s.to_string_lossy().to_string())
231 .unwrap_or_default()
232 });
233 let title = session.title();
234
235 add_session_to_index(
236 &db_path,
237 &session_id,
238 &title,
239 session.last_message_date,
240 session.is_imported,
241 &session.initial_location,
242 session.is_empty(),
243 )?;
244
245 let id_display = if session_id.len() > 12 {
246 &session_id[..12]
247 } else {
248 &session_id
249 };
250 println!(
251 " {} {} (\"{}\")",
252 "[OK]".green(),
253 id_display.cyan(),
254 title.yellow()
255 );
256 registered_count += 1;
257 }
258 } else {
259 println!(
261 "{} Registering {} sessions by ID:",
262 "[CSM]".cyan().bold(),
263 ids.len()
264 );
265
266 for session_id in ids {
267 match find_session_file(&chat_sessions_dir, session_id) {
268 Ok(session_file) => {
269 let session = parse_session_file(&session_file)?;
270
271 let title = session.title();
272 let actual_session_id = session
273 .session_id
274 .clone()
275 .unwrap_or_else(|| session_id.to_string());
276
277 add_session_to_index(
278 &db_path,
279 &actual_session_id,
280 &title,
281 session.last_message_date,
282 session.is_imported,
283 &session.initial_location,
284 session.is_empty(),
285 )?;
286
287 let id_display = if actual_session_id.len() > 12 {
288 &actual_session_id[..12]
289 } else {
290 &actual_session_id
291 };
292 println!(
293 " {} {} (\"{}\")",
294 "[OK]".green(),
295 id_display.cyan(),
296 title.yellow()
297 );
298 registered_count += 1;
299 }
300 Err(e) => {
301 println!(
302 " {} {} - {}",
303 "[ERR]".red(),
304 session_id.cyan(),
305 e.to_string().red()
306 );
307 }
308 }
309 }
310 }
311
312 println!(
313 "\n{} Registered {} sessions in VS Code's index",
314 "[OK]".green().bold(),
315 registered_count.to_string().cyan()
316 );
317
318 if force && is_vscode_running() {
319 println!(
320 " {} Sessions should appear in VS Code immediately",
321 "->".cyan()
322 );
323 }
324
325 Ok(())
326}
327
328pub fn list_orphaned(project_path: Option<&str>) -> Result<()> {
330 let path = resolve_path(project_path);
331
332 println!(
333 "{} Finding orphaned sessions for: {}",
334 "[CSM]".cyan().bold(),
335 path.display()
336 );
337
338 let path_str = path.to_string_lossy().to_string();
340 let (ws_id, ws_path, _folder) = find_workspace_by_path(&path_str)?
341 .ok_or_else(|| CsmError::WorkspaceNotFound(path.display().to_string()))?;
342
343 let chat_sessions_dir = ws_path.join("chatSessions");
344
345 if !chat_sessions_dir.exists() {
346 println!("{} No chatSessions directory found", "[!]".yellow());
347 return Ok(());
348 }
349
350 let db_path = get_workspace_storage_db(&ws_id)?;
352 let index = read_chat_session_index(&db_path)?;
353 let indexed_ids: HashSet<String> = index.entries.keys().cloned().collect();
354
355 println!(
356 " {} sessions currently in VS Code's index",
357 indexed_ids.len().to_string().cyan()
358 );
359
360 let mut orphaned_sessions = Vec::new();
362
363 let mut session_files: std::collections::HashMap<String, PathBuf> =
365 std::collections::HashMap::new();
366 for entry in std::fs::read_dir(&chat_sessions_dir)? {
367 let entry = entry?;
368 let path = entry.path();
369 if path
370 .extension()
371 .map(is_session_file_extension)
372 .unwrap_or(false)
373 {
374 if let Some(stem) = path.file_stem() {
375 let stem_str = stem.to_string_lossy().to_string();
376 let is_jsonl = path.extension().is_some_and(|e| e == "jsonl");
377 if !session_files.contains_key(&stem_str) || is_jsonl {
378 session_files.insert(stem_str, path);
379 }
380 }
381 }
382 }
383
384 for (_, path) in &session_files {
385 if let Ok(session) = parse_session_file(path) {
386 let session_id = session.session_id.clone().unwrap_or_else(|| {
387 path.file_stem()
388 .map(|s| s.to_string_lossy().to_string())
389 .unwrap_or_default()
390 });
391
392 if !indexed_ids.contains(&session_id) {
393 let title = session.title();
394 let msg_count = session.requests.len();
395 orphaned_sessions.push((session_id, title, msg_count, path.clone()));
396 }
397 }
398 }
399
400 if orphaned_sessions.is_empty() {
401 println!(
402 "\n{} No orphaned sessions found - all sessions are registered!",
403 "[OK]".green().bold()
404 );
405 return Ok(());
406 }
407
408 println!(
409 "\n{} Found {} orphaned sessions (on disk but not in index):\n",
410 "[!]".yellow().bold(),
411 orphaned_sessions.len().to_string().red()
412 );
413
414 for (session_id, title, msg_count, _path) in &orphaned_sessions {
415 let id_display = if session_id.len() > 12 {
416 &session_id[..12]
417 } else {
418 session_id
419 };
420 println!(
421 " {} {} ({} messages)",
422 id_display.cyan(),
423 format!("\"{}\"", title).yellow(),
424 msg_count
425 );
426 }
427
428 println!("\n{} To register all orphaned sessions:", "->".cyan());
429 println!(" csm register all --force");
430 println!("\n{} To register specific sessions:", "->".cyan());
431 println!(" csm register session <ID1> <ID2> ... --force");
432
433 Ok(())
434}
435
436fn count_sessions_in_directory(dir: &PathBuf) -> Result<usize> {
438 let mut session_ids: HashSet<String> = HashSet::new();
439 for entry in std::fs::read_dir(dir)? {
440 let entry = entry?;
441 let path = entry.path();
442 if path
443 .extension()
444 .map(is_session_file_extension)
445 .unwrap_or(false)
446 {
447 if let Some(stem) = path.file_stem() {
448 session_ids.insert(stem.to_string_lossy().to_string());
449 }
450 }
451 }
452 Ok(session_ids.len())
453}
454
455fn find_session_file(chat_sessions_dir: &PathBuf, session_id: &str) -> Result<PathBuf> {
457 let exact_jsonl = chat_sessions_dir.join(format!("{}.jsonl", session_id));
459 if exact_jsonl.exists() {
460 return Ok(exact_jsonl);
461 }
462 let exact_json = chat_sessions_dir.join(format!("{}.json", session_id));
463 if exact_json.exists() {
464 return Ok(exact_json);
465 }
466
467 let mut best_match: Option<PathBuf> = None;
469 for entry in std::fs::read_dir(chat_sessions_dir)? {
470 let entry = entry?;
471 let path = entry.path();
472
473 if path
474 .extension()
475 .map(is_session_file_extension)
476 .unwrap_or(false)
477 {
478 let filename = path
479 .file_stem()
480 .map(|s| s.to_string_lossy().to_string())
481 .unwrap_or_default();
482
483 if filename.starts_with(session_id) {
484 let is_jsonl = path.extension().is_some_and(|e| e == "jsonl");
485 if best_match.is_none() || is_jsonl {
486 best_match = Some(path.clone());
487 if is_jsonl {
488 return Ok(path);
489 }
490 }
491 continue;
492 }
493
494 if let Ok(session) = parse_session_file(&path) {
496 if let Some(ref sid) = session.session_id {
497 if sid.starts_with(session_id) || sid == session_id {
498 let is_jsonl = path.extension().is_some_and(|e| e == "jsonl");
499 if best_match.is_none() || is_jsonl {
500 best_match = Some(path.clone());
501 }
502 }
503 }
504 }
505 }
506 }
507
508 best_match.ok_or_else(|| CsmError::SessionNotFound(session_id.to_string()).into())
509}
510
511fn find_sessions_by_titles(
513 chat_sessions_dir: &PathBuf,
514 titles: &[String],
515) -> Result<Vec<(ChatSession, PathBuf)>> {
516 let mut matches = Vec::new();
517 let title_patterns: Vec<String> = titles.iter().map(|t| t.to_lowercase()).collect();
518
519 let mut session_files: std::collections::HashMap<String, PathBuf> =
521 std::collections::HashMap::new();
522 for entry in std::fs::read_dir(chat_sessions_dir)? {
523 let entry = entry?;
524 let path = entry.path();
525 if path
526 .extension()
527 .map(is_session_file_extension)
528 .unwrap_or(false)
529 {
530 if let Some(stem) = path.file_stem() {
531 let stem_str = stem.to_string_lossy().to_string();
532 let is_jsonl = path.extension().is_some_and(|e| e == "jsonl");
533 if !session_files.contains_key(&stem_str) || is_jsonl {
534 session_files.insert(stem_str, path);
535 }
536 }
537 }
538 }
539
540 for (_, path) in &session_files {
541 if let Ok(session) = parse_session_file(path) {
542 let session_title = session.title().to_lowercase();
543
544 for pattern in &title_patterns {
545 if session_title.contains(pattern) {
546 matches.push((session, path.clone()));
547 break;
548 }
549 }
550 }
551 }
552
553 if matches.is_empty() {
554 println!(
555 "{} No sessions found matching the specified titles",
556 "[!]".yellow()
557 );
558 }
559
560 Ok(matches)
561}
562
563pub fn register_recursive(
565 root_path: Option<&str>,
566 max_depth: Option<usize>,
567 force: bool,
568 dry_run: bool,
569 exclude_patterns: &[String],
570) -> Result<()> {
571 let root = resolve_path(root_path);
572
573 println!(
574 "{} Scanning for workspaces recursively from: {}",
575 "[CSM]".cyan().bold(),
576 root.display()
577 );
578
579 if dry_run {
580 println!("{} Dry run mode - no changes will be made", "[!]".yellow());
581 }
582
583 if !force && !dry_run && is_vscode_running() {
585 println!(
586 "{} VS Code is running. Use {} to register anyway.",
587 "[!]".yellow(),
588 "--force".cyan()
589 );
590 println!(" Note: VS Code uses WAL mode so this is generally safe.");
591 return Err(CsmError::VSCodeRunning.into());
592 }
593
594 let workspaces = discover_workspaces()?;
596 println!(
597 " Found {} VS Code workspaces to check",
598 workspaces.len().to_string().cyan()
599 );
600
601 let mut workspace_map: std::collections::HashMap<String, Vec<&crate::models::Workspace>> =
603 std::collections::HashMap::new();
604 for ws in &workspaces {
605 if let Some(ref project_path) = ws.project_path {
606 let normalized = normalize_path(project_path);
607 workspace_map.entry(normalized).or_default().push(ws);
608 }
609 }
610
611 let exclude_matchers: Vec<glob::Pattern> = exclude_patterns
613 .iter()
614 .filter_map(|p| glob::Pattern::new(p).ok())
615 .collect();
616
617 let default_excludes = [
619 "node_modules",
620 ".git",
621 "target",
622 "build",
623 "dist",
624 ".venv",
625 "venv",
626 "__pycache__",
627 ".cache",
628 "vendor",
629 ".cargo",
630 ];
631
632 let mut total_dirs_scanned = 0;
633 let mut workspaces_found = 0;
634 let mut total_sessions_registered = 0;
635 let mut workspaces_with_orphans: Vec<(String, usize, usize)> = Vec::new();
636
637 walk_directory(
639 &root,
640 &root,
641 0,
642 max_depth,
643 &workspace_map,
644 &exclude_matchers,
645 &default_excludes,
646 force,
647 dry_run,
648 &mut total_dirs_scanned,
649 &mut workspaces_found,
650 &mut total_sessions_registered,
651 &mut workspaces_with_orphans,
652 )?;
653
654 println!("\n{}", "═".repeat(60).cyan());
656 println!("{} Recursive scan complete", "[OK]".green().bold());
657 println!("{}", "═".repeat(60).cyan());
658 println!(
659 " Directories scanned: {}",
660 total_dirs_scanned.to_string().cyan()
661 );
662 println!(
663 " Workspaces found: {}",
664 workspaces_found.to_string().cyan()
665 );
666 println!(
667 " Sessions registered: {}",
668 total_sessions_registered.to_string().green()
669 );
670
671 if !workspaces_with_orphans.is_empty() {
672 println!("\n {} Workspaces with orphaned sessions:", "[+]".green());
673 for (path, orphaned, registered) in &workspaces_with_orphans {
674 let reg_str = if dry_run {
675 format!("would register {}", registered)
676 } else {
677 format!("registered {}", registered)
678 };
679 println!(
680 " {} ({} orphaned, {})",
681 path.cyan(),
682 orphaned.to_string().yellow(),
683 reg_str.green()
684 );
685 }
686 }
687
688 if total_sessions_registered > 0 && !dry_run {
689 println!(
690 "\n{} VS Code caches the session index in memory.",
691 "[!]".yellow()
692 );
693 println!(" To see the new sessions, do one of the following:");
694 println!(
695 " * Run: {} (if CSM extension is installed)",
696 "code --command csm.reloadAndShowChats".cyan()
697 );
698 println!(
699 " * Or press {} in VS Code and run {}",
700 "Ctrl+Shift+P".cyan(),
701 "Developer: Reload Window".cyan()
702 );
703 println!(" * Or restart VS Code");
704 }
705
706 Ok(())
707}
708
709#[allow(clippy::too_many_arguments)]
711fn walk_directory(
712 current_dir: &Path,
713 root: &Path,
714 current_depth: usize,
715 max_depth: Option<usize>,
716 workspace_map: &std::collections::HashMap<String, Vec<&crate::models::Workspace>>,
717 exclude_matchers: &[glob::Pattern],
718 default_excludes: &[&str],
719 force: bool,
720 dry_run: bool,
721 total_dirs_scanned: &mut usize,
722 workspaces_found: &mut usize,
723 total_sessions_registered: &mut usize,
724 workspaces_with_orphans: &mut Vec<(String, usize, usize)>,
725) -> Result<()> {
726 if let Some(max) = max_depth {
728 if current_depth > max {
729 return Ok(());
730 }
731 }
732
733 *total_dirs_scanned += 1;
734
735 let dir_name = current_dir
737 .file_name()
738 .map(|n| n.to_string_lossy().to_string())
739 .unwrap_or_default();
740
741 if default_excludes.contains(&dir_name.as_str()) {
743 return Ok(());
744 }
745
746 let relative_path = current_dir
748 .strip_prefix(root)
749 .unwrap_or(current_dir)
750 .to_string_lossy();
751 for pattern in exclude_matchers {
752 if pattern.matches(&relative_path) || pattern.matches(&dir_name) {
753 return Ok(());
754 }
755 }
756
757 let normalized_path = normalize_path(¤t_dir.to_string_lossy());
759 if let Some(workspace_entries) = workspace_map.get(&normalized_path) {
760 *workspaces_found += 1;
761
762 for ws in workspace_entries {
763 if ws.has_chat_sessions {
765 let chat_sessions_dir = &ws.chat_sessions_path;
766
767 match count_orphaned_sessions(&ws.hash, chat_sessions_dir) {
769 Ok((on_disk, in_index, orphaned_count)) => {
770 if orphaned_count > 0 {
771 let display_path = ws.project_path.as_deref().unwrap_or(&ws.hash);
772
773 if dry_run {
774 println!(
775 " {} {} - {} sessions on disk, {} in index, {} orphaned",
776 "[DRY]".yellow(),
777 display_path.cyan(),
778 on_disk.to_string().white(),
779 in_index.to_string().white(),
780 orphaned_count.to_string().yellow()
781 );
782 workspaces_with_orphans.push((
783 display_path.to_string(),
784 orphaned_count,
785 orphaned_count,
786 ));
787 } else {
788 match register_all_sessions_from_directory(
790 &ws.hash,
791 chat_sessions_dir,
792 force,
793 ) {
794 Ok(registered) => {
795 *total_sessions_registered += registered;
796 println!(
797 " {} {} - registered {} sessions",
798 "[+]".green(),
799 display_path.cyan(),
800 registered.to_string().green()
801 );
802 workspaces_with_orphans.push((
803 display_path.to_string(),
804 orphaned_count,
805 registered,
806 ));
807 }
808 Err(e) => {
809 println!(
810 " {} {} - error: {}",
811 "[!]".red(),
812 display_path.cyan(),
813 e
814 );
815 }
816 }
817 }
818 }
819 }
820 Err(e) => {
821 let display_path = ws.project_path.as_deref().unwrap_or(&ws.hash);
822 println!(
823 " {} {} - error checking: {}",
824 "[!]".yellow(),
825 display_path,
826 e
827 );
828 }
829 }
830 }
831 }
832 }
833
834 match std::fs::read_dir(current_dir) {
836 Ok(entries) => {
837 for entry in entries.flatten() {
838 let path = entry.path();
839 if path.is_dir() {
840 let name = path
842 .file_name()
843 .map(|n| n.to_string_lossy().to_string())
844 .unwrap_or_default();
845 if name.starts_with('.') {
846 continue;
847 }
848
849 walk_directory(
850 &path,
851 root,
852 current_depth + 1,
853 max_depth,
854 workspace_map,
855 exclude_matchers,
856 default_excludes,
857 force,
858 dry_run,
859 total_dirs_scanned,
860 workspaces_found,
861 total_sessions_registered,
862 workspaces_with_orphans,
863 )?;
864 }
865 }
866 }
867 Err(e) => {
868 if e.kind() != std::io::ErrorKind::PermissionDenied {
870 eprintln!(
871 " {} Could not read {}: {}",
872 "[!]".yellow(),
873 current_dir.display(),
874 e
875 );
876 }
877 }
878 }
879
880 Ok(())
881}
882
883fn count_orphaned_sessions(
885 workspace_id: &str,
886 chat_sessions_dir: &Path,
887) -> Result<(usize, usize, usize)> {
888 let db_path = get_workspace_storage_db(workspace_id)?;
890 let indexed_sessions = read_chat_session_index(&db_path)?;
891 let indexed_ids: HashSet<String> = indexed_sessions.entries.keys().cloned().collect();
892
893 let mut disk_sessions: HashSet<String> = HashSet::new();
895
896 for entry in std::fs::read_dir(chat_sessions_dir)? {
897 let entry = entry?;
898 let path = entry.path();
899
900 if path
901 .extension()
902 .map(is_session_file_extension)
903 .unwrap_or(false)
904 {
905 if let Some(stem) = path.file_stem() {
906 disk_sessions.insert(stem.to_string_lossy().to_string());
907 }
908 }
909 }
910
911 let on_disk = disk_sessions.len();
912 let orphaned = disk_sessions
913 .iter()
914 .filter(|id| !indexed_ids.contains(*id))
915 .count();
916
917 Ok((on_disk, indexed_ids.len(), orphaned))
918}
919
920pub fn register_repair(
922 project_path: Option<&str>,
923 all: bool,
924 recursive: bool,
925 max_depth: Option<usize>,
926 exclude_patterns: &[String],
927 dry_run: bool,
928 force: bool,
929 close_vscode: bool,
930 reopen: bool,
931) -> Result<()> {
932 if all {
933 return register_repair_all(force, close_vscode, reopen);
934 }
935
936 if recursive {
937 return register_repair_recursive(
938 project_path,
939 max_depth,
940 exclude_patterns,
941 dry_run,
942 force,
943 close_vscode,
944 reopen,
945 );
946 }
947
948 let path = resolve_path(project_path);
949 let should_close = close_vscode || reopen;
950
951 println!(
952 "{} Repairing sessions for: {}",
953 "[CSM]".cyan().bold(),
954 path.display()
955 );
956
957 let path_str = path.to_string_lossy().to_string();
959 let (ws_id, ws_path, _folder) = find_workspace_by_path(&path_str)?
960 .ok_or_else(|| CsmError::WorkspaceNotFound(path.display().to_string()))?;
961
962 let chat_sessions_dir = ws_path.join("chatSessions");
963
964 if !chat_sessions_dir.exists() {
965 println!(
966 "{} No chatSessions directory found at: {}",
967 "[!]".yellow(),
968 chat_sessions_dir.display()
969 );
970 return Ok(());
971 }
972
973 let vscode_was_running = is_vscode_running();
975 if vscode_was_running {
976 if should_close {
977 if !confirm_close_vscode(force) {
978 println!("{} Aborted.", "[!]".yellow());
979 return Ok(());
980 }
981 println!(" {} Closing VS Code (saving state)...", "[*]".yellow());
982 close_vscode_and_wait(30)?;
983 println!(" {} VS Code closed.", "[OK]".green());
984 } else if !force {
985 println!(
986 "{} VS Code is running. Its in-memory cache will overwrite index changes.",
987 "[!]".yellow()
988 );
989 println!(
990 " Use {} to close VS Code first, or {} to force.",
991 "--reopen".cyan(),
992 "--force".cyan()
993 );
994 return Err(CsmError::VSCodeRunning.into());
995 }
996 }
997
998 println!(
1000 " {} Pass 1: Compacting JSONL files & fixing compat fields...",
1001 "[*]".cyan()
1002 );
1003 println!(
1004 " {} Pass 1.5: Converting skeleton .json files...",
1005 "[*]".cyan()
1006 );
1007 println!(" {} Pass 2: Fixing cancelled modelState...", "[*]".cyan());
1008 let (compacted, index_fixed) = repair_workspace_sessions(&ws_id, &chat_sessions_dir, true)?;
1009
1010 println!(" {} Pass 3: Index rebuilt.", "[*]".cyan());
1011 println!(
1012 "\n{} Repair complete: {} files compacted, {} index entries synced",
1013 "[OK]".green().bold(),
1014 compacted.to_string().cyan(),
1015 index_fixed.to_string().cyan()
1016 );
1017
1018 let mut deleted_json = 0;
1020 if chat_sessions_dir.exists() {
1021 let mut jsonl_sessions: HashSet<String> = HashSet::new();
1022 for entry in std::fs::read_dir(&chat_sessions_dir)? {
1023 let entry = entry?;
1024 let p = entry.path();
1025 if p.extension().is_some_and(|e| e == "jsonl") {
1026 if let Some(stem) = p.file_stem() {
1027 jsonl_sessions.insert(stem.to_string_lossy().to_string());
1028 }
1029 }
1030 }
1031 for entry in std::fs::read_dir(&chat_sessions_dir)? {
1032 let entry = entry?;
1033 let p = entry.path();
1034 if p.extension().is_some_and(|e| e == "json") {
1035 if let Some(stem) = p.file_stem() {
1036 if jsonl_sessions.contains(&stem.to_string_lossy().to_string()) {
1037 let bak = p.with_extension("json.bak");
1039 std::fs::rename(&p, &bak)?;
1040 println!(
1041 " {} Backed up stale .json: {} → {}",
1042 "[*]".yellow(),
1043 p.file_name().unwrap_or_default().to_string_lossy(),
1044 bak.file_name().unwrap_or_default().to_string_lossy()
1045 );
1046 deleted_json += 1;
1047 }
1048 }
1049 }
1050 }
1051 if deleted_json > 0 {
1052 repair_workspace_sessions(&ws_id, &chat_sessions_dir, true)?;
1054 println!(
1055 " {} Removed {} stale .json duplicates (backed up as .json.bak)",
1056 "[OK]".green(),
1057 deleted_json
1058 );
1059 }
1060 }
1061
1062 if reopen && vscode_was_running {
1064 println!(" {} Reopening VS Code...", "[*]".yellow());
1065 reopen_vscode(Some(&path_str))?;
1066 println!(
1067 " {} VS Code launched. Sessions should now load correctly.",
1068 "[OK]".green()
1069 );
1070 } else if should_close && vscode_was_running {
1071 println!(
1072 "\n{} VS Code was closed. Reopen it to see the repaired sessions.",
1073 "[!]".yellow()
1074 );
1075 println!(" Run: {}", format!("code {}", path.display()).cyan());
1076 }
1077
1078 Ok(())
1079}
1080
1081fn register_repair_recursive(
1083 root_path: Option<&str>,
1084 max_depth: Option<usize>,
1085 exclude_patterns: &[String],
1086 dry_run: bool,
1087 force: bool,
1088 close_vscode: bool,
1089 reopen: bool,
1090) -> Result<()> {
1091 let root = resolve_path(root_path);
1092 let should_close = close_vscode || reopen;
1093
1094 println!(
1095 "{} Recursively scanning for workspaces to repair from: {}",
1096 "[CSM]".cyan().bold(),
1097 root.display()
1098 );
1099
1100 if dry_run {
1101 println!("{} Dry run mode — no changes will be made", "[!]".yellow());
1102 }
1103
1104 let vscode_was_running = is_vscode_running();
1106 if vscode_was_running && !dry_run {
1107 if should_close {
1108 if !confirm_close_vscode(force) {
1109 println!("{} Aborted.", "[!]".yellow());
1110 return Ok(());
1111 }
1112 println!(" {} Closing VS Code (saving state)...", "[*]".yellow());
1113 close_vscode_and_wait(30)?;
1114 println!(" {} VS Code closed.\n", "[OK]".green());
1115 } else if !force {
1116 println!(
1117 "{} VS Code is running. Its in-memory cache will overwrite index changes.",
1118 "[!]".yellow()
1119 );
1120 println!(
1121 " Use {} to close VS Code first, or {} to force.",
1122 "--reopen".cyan(),
1123 "--force".cyan()
1124 );
1125 return Err(CsmError::VSCodeRunning.into());
1126 }
1127 }
1128
1129 let workspaces = discover_workspaces()?;
1131 println!(
1132 " Found {} VS Code workspaces to check",
1133 workspaces.len().to_string().cyan()
1134 );
1135
1136 let mut workspace_map: std::collections::HashMap<String, Vec<&crate::models::Workspace>> =
1138 std::collections::HashMap::new();
1139 for ws in &workspaces {
1140 if let Some(ref project_path) = ws.project_path {
1141 let normalized = normalize_path(project_path);
1142 workspace_map.entry(normalized).or_default().push(ws);
1143 }
1144 }
1145
1146 let exclude_matchers: Vec<glob::Pattern> = exclude_patterns
1148 .iter()
1149 .filter_map(|p| glob::Pattern::new(p).ok())
1150 .collect();
1151
1152 let default_excludes = [
1153 "node_modules",
1154 ".git",
1155 "target",
1156 "build",
1157 "dist",
1158 ".venv",
1159 "venv",
1160 "__pycache__",
1161 ".cache",
1162 "vendor",
1163 ".cargo",
1164 ];
1165
1166 let mut total_dirs_scanned = 0usize;
1167 let mut workspaces_found = 0usize;
1168 let mut total_compacted = 0usize;
1169 let mut total_synced = 0usize;
1170 let mut total_issues_found = 0usize;
1171 let mut total_issues_fixed = 0usize;
1172 let mut repair_results: Vec<(String, usize, bool, String)> = Vec::new(); fn walk_for_repair(
1176 dir: &Path,
1177 root: &Path,
1178 current_depth: usize,
1179 max_depth: Option<usize>,
1180 workspace_map: &std::collections::HashMap<String, Vec<&crate::models::Workspace>>,
1181 exclude_matchers: &[glob::Pattern],
1182 default_excludes: &[&str],
1183 dry_run: bool,
1184 force: bool,
1185 total_dirs_scanned: &mut usize,
1186 workspaces_found: &mut usize,
1187 total_compacted: &mut usize,
1188 total_synced: &mut usize,
1189 total_issues_found: &mut usize,
1190 total_issues_fixed: &mut usize,
1191 repair_results: &mut Vec<(String, usize, bool, String)>,
1192 ) -> Result<()> {
1193 if let Some(max) = max_depth {
1194 if current_depth > max {
1195 return Ok(());
1196 }
1197 }
1198
1199 *total_dirs_scanned += 1;
1200
1201 let normalized = normalize_path(&dir.to_string_lossy());
1203 if let Some(ws_list) = workspace_map.get(&normalized) {
1204 for ws in ws_list {
1205 if ws.has_chat_sessions && ws.chat_session_count > 0 {
1206 *workspaces_found += 1;
1207
1208 let display_name = ws.project_path.as_deref().unwrap_or(&ws.hash);
1209
1210 let chat_dir = ws.workspace_path.join("chatSessions");
1212 match crate::storage::diagnose_workspace_sessions(&ws.hash, &chat_dir) {
1213 Ok(diag) => {
1214 let issue_count = diag.issues.len();
1215 *total_issues_found += issue_count;
1216
1217 if issue_count == 0 {
1218 println!(
1219 " {} {} — {} sessions, healthy",
1220 "[OK]".green(),
1221 display_name.cyan(),
1222 ws.chat_session_count
1223 );
1224 repair_results.push((
1225 display_name.to_string(),
1226 0,
1227 true,
1228 "healthy".to_string(),
1229 ));
1230 } else {
1231 let issue_kinds: Vec<String> = {
1232 let mut kinds: Vec<String> = Vec::new();
1233 for issue in &diag.issues {
1234 let s = format!("{}", issue.kind);
1235 if !kinds.contains(&s) {
1236 kinds.push(s);
1237 }
1238 }
1239 kinds
1240 };
1241
1242 println!(
1243 " {} {} — {} sessions, {} issue(s): {}",
1244 "[!]".yellow(),
1245 display_name.cyan(),
1246 ws.chat_session_count,
1247 issue_count,
1248 issue_kinds.join(", ")
1249 );
1250
1251 if !dry_run {
1252 match repair_workspace_sessions(
1253 &ws.hash,
1254 &chat_dir,
1255 force || true,
1256 ) {
1257 Ok((compacted, synced)) => {
1258 *total_compacted += compacted;
1259 *total_synced += synced;
1260 *total_issues_fixed += issue_count;
1261
1262 let mut deleted_json = 0;
1264 let mut jsonl_sessions: HashSet<String> =
1265 HashSet::new();
1266 if let Ok(entries) = std::fs::read_dir(&chat_dir) {
1267 for entry in entries.flatten() {
1268 let p = entry.path();
1269 if p.extension().is_some_and(|e| e == "jsonl") {
1270 if let Some(stem) = p.file_stem() {
1271 jsonl_sessions.insert(
1272 stem.to_string_lossy().to_string(),
1273 );
1274 }
1275 }
1276 }
1277 }
1278 if let Ok(entries) = std::fs::read_dir(&chat_dir) {
1279 for entry in entries.flatten() {
1280 let p = entry.path();
1281 if p.extension().is_some_and(|e| e == "json") {
1282 if let Some(stem) = p.file_stem() {
1283 if jsonl_sessions.contains(
1284 &stem.to_string_lossy().to_string(),
1285 ) {
1286 let bak =
1287 p.with_extension("json.bak");
1288 let _ = std::fs::rename(&p, &bak);
1289 deleted_json += 1;
1290 }
1291 }
1292 }
1293 }
1294 }
1295 if deleted_json > 0 {
1296 let _ = repair_workspace_sessions(
1297 &ws.hash, &chat_dir, true,
1298 );
1299 }
1300
1301 let detail = format!(
1302 "{} compacted, {} synced{}",
1303 compacted,
1304 synced,
1305 if deleted_json > 0 {
1306 format!(
1307 ", {} stale .json backed up",
1308 deleted_json
1309 )
1310 } else {
1311 String::new()
1312 }
1313 );
1314 println!(" {} Fixed: {}", "[OK]".green(), detail);
1315 repair_results.push((
1316 display_name.to_string(),
1317 issue_count,
1318 true,
1319 detail,
1320 ));
1321 }
1322 Err(e) => {
1323 println!(" {} Failed: {}", "[ERR]".red(), e);
1324 repair_results.push((
1325 display_name.to_string(),
1326 issue_count,
1327 false,
1328 e.to_string(),
1329 ));
1330 }
1331 }
1332 } else {
1333 for issue in &diag.issues {
1335 println!(
1336 " {} {} — {}",
1337 "→".bright_black(),
1338 issue.session_id[..8.min(issue.session_id.len())]
1339 .to_string(),
1340 issue.kind
1341 );
1342 }
1343 repair_results.push((
1344 display_name.to_string(),
1345 issue_count,
1346 true,
1347 "dry run".to_string(),
1348 ));
1349 }
1350 }
1351 }
1352 Err(e) => {
1353 println!(" {} {} — scan failed: {}", "[ERR]".red(), display_name, e);
1354 }
1355 }
1356 }
1357 }
1358 }
1359
1360 let entries = match std::fs::read_dir(dir) {
1362 Ok(e) => e,
1363 Err(_) => return Ok(()),
1364 };
1365
1366 for entry in entries {
1367 let entry = match entry {
1368 Ok(e) => e,
1369 Err(_) => continue,
1370 };
1371 let path = entry.path();
1372 if !path.is_dir() {
1373 continue;
1374 }
1375
1376 let dir_name = entry.file_name().to_string_lossy().to_string();
1377
1378 if dir_name.starts_with('.') {
1380 continue;
1381 }
1382
1383 if default_excludes.iter().any(|e| dir_name == *e) {
1385 continue;
1386 }
1387
1388 if exclude_matchers.iter().any(|p| p.matches(&dir_name)) {
1390 continue;
1391 }
1392
1393 walk_for_repair(
1394 &path,
1395 root,
1396 current_depth + 1,
1397 max_depth,
1398 workspace_map,
1399 exclude_matchers,
1400 default_excludes,
1401 dry_run,
1402 force,
1403 total_dirs_scanned,
1404 workspaces_found,
1405 total_compacted,
1406 total_synced,
1407 total_issues_found,
1408 total_issues_fixed,
1409 repair_results,
1410 )?;
1411 }
1412
1413 Ok(())
1414 }
1415
1416 walk_for_repair(
1417 &root,
1418 &root,
1419 0,
1420 max_depth,
1421 &workspace_map,
1422 &exclude_matchers,
1423 &default_excludes,
1424 dry_run,
1425 force,
1426 &mut total_dirs_scanned,
1427 &mut workspaces_found,
1428 &mut total_compacted,
1429 &mut total_synced,
1430 &mut total_issues_found,
1431 &mut total_issues_fixed,
1432 &mut repair_results,
1433 )?;
1434
1435 println!("\n{}", "═".repeat(60).cyan());
1437 println!("{} Recursive repair scan complete", "[OK]".green().bold());
1438 println!("{}", "═".repeat(60).cyan());
1439 println!(
1440 " Directories scanned: {}",
1441 total_dirs_scanned.to_string().cyan()
1442 );
1443 println!(
1444 " Workspaces found: {}",
1445 workspaces_found.to_string().cyan()
1446 );
1447 println!(
1448 " Issues detected: {}",
1449 if total_issues_found > 0 {
1450 total_issues_found.to_string().yellow()
1451 } else {
1452 total_issues_found.to_string().green()
1453 }
1454 );
1455 if !dry_run {
1456 println!(
1457 " Issues fixed: {}",
1458 total_issues_fixed.to_string().green()
1459 );
1460 println!(
1461 " Files compacted: {}",
1462 total_compacted.to_string().cyan()
1463 );
1464 println!(
1465 " Index entries synced: {}",
1466 total_synced.to_string().cyan()
1467 );
1468 }
1469
1470 let failed_count = repair_results.iter().filter(|(_, _, ok, _)| !ok).count();
1471 if failed_count > 0 {
1472 println!(
1473 "\n {} {} workspace(s) had repair errors",
1474 "[!]".yellow(),
1475 failed_count.to_string().red()
1476 );
1477 }
1478
1479 if reopen && vscode_was_running {
1481 println!(" {} Reopening VS Code...", "[*]".yellow());
1482 reopen_vscode(None)?;
1483 println!(
1484 " {} VS Code launched. Sessions should now load correctly.",
1485 "[OK]".green()
1486 );
1487 } else if should_close && vscode_was_running {
1488 println!(
1489 "\n{} VS Code was closed. Reopen it to see the repaired sessions.",
1490 "[!]".yellow()
1491 );
1492 }
1493
1494 Ok(())
1495}
1496
1497fn register_repair_all(force: bool, close_vscode: bool, reopen: bool) -> Result<()> {
1499 let should_close = close_vscode || reopen;
1500
1501 println!(
1502 "{} Repairing all workspaces with chat sessions...\n",
1503 "[CSM]".cyan().bold(),
1504 );
1505
1506 let vscode_was_running = is_vscode_running();
1508 if vscode_was_running {
1509 if should_close {
1510 if !confirm_close_vscode(force) {
1511 println!("{} Aborted.", "[!]".yellow());
1512 return Ok(());
1513 }
1514 println!(" {} Closing VS Code (saving state)...", "[*]".yellow());
1515 close_vscode_and_wait(30)?;
1516 println!(" {} VS Code closed.\n", "[OK]".green());
1517 } else if !force {
1518 println!(
1519 "{} VS Code is running. Its in-memory cache will overwrite index changes.",
1520 "[!]".yellow()
1521 );
1522 println!(
1523 " Use {} to close VS Code first, or {} to force.",
1524 "--reopen".cyan(),
1525 "--force".cyan()
1526 );
1527 return Err(CsmError::VSCodeRunning.into());
1528 }
1529 }
1530
1531 let workspaces = discover_workspaces()?;
1532 let ws_with_sessions: Vec<_> = workspaces
1533 .iter()
1534 .filter(|w| w.has_chat_sessions && w.chat_session_count > 0)
1535 .collect();
1536
1537 if ws_with_sessions.is_empty() {
1538 println!("{} No workspaces with chat sessions found.", "[!]".yellow());
1539 return Ok(());
1540 }
1541
1542 println!(
1543 " Found {} workspaces with chat sessions\n",
1544 ws_with_sessions.len().to_string().cyan()
1545 );
1546
1547 let mut total_compacted = 0usize;
1548 let mut total_synced = 0usize;
1549 let mut succeeded = 0usize;
1550 let mut failed = 0usize;
1551
1552 for (i, ws) in ws_with_sessions.iter().enumerate() {
1553 let display_name = ws.project_path.as_deref().unwrap_or(&ws.hash);
1554 println!(
1555 "[{}/{}] {} {}",
1556 i + 1,
1557 ws_with_sessions.len(),
1558 "===".dimmed(),
1559 display_name.cyan()
1560 );
1561
1562 let chat_sessions_dir = ws.workspace_path.join("chatSessions");
1563 if !chat_sessions_dir.exists() {
1564 println!(
1565 " {} No chatSessions directory, skipping.\n",
1566 "[!]".yellow()
1567 );
1568 continue;
1569 }
1570
1571 match repair_workspace_sessions(&ws.hash, &chat_sessions_dir, true) {
1572 Ok((compacted, index_fixed)) => {
1573 let mut deleted_json = 0;
1575 let mut jsonl_sessions: HashSet<String> = HashSet::new();
1576 for entry in std::fs::read_dir(&chat_sessions_dir)? {
1577 let entry = entry?;
1578 let p = entry.path();
1579 if p.extension().is_some_and(|e| e == "jsonl") {
1580 if let Some(stem) = p.file_stem() {
1581 jsonl_sessions.insert(stem.to_string_lossy().to_string());
1582 }
1583 }
1584 }
1585 for entry in std::fs::read_dir(&chat_sessions_dir)? {
1586 let entry = entry?;
1587 let p = entry.path();
1588 if p.extension().is_some_and(|e| e == "json") {
1589 if let Some(stem) = p.file_stem() {
1590 if jsonl_sessions.contains(&stem.to_string_lossy().to_string()) {
1591 let bak = p.with_extension("json.bak");
1592 std::fs::rename(&p, &bak)?;
1593 deleted_json += 1;
1594 }
1595 }
1596 }
1597 }
1598 if deleted_json > 0 {
1599 repair_workspace_sessions(&ws.hash, &chat_sessions_dir, true)?;
1600 }
1601
1602 total_compacted += compacted;
1603 total_synced += index_fixed;
1604 succeeded += 1;
1605 println!(
1606 " {} {} compacted, {} synced{}\n",
1607 "[OK]".green(),
1608 compacted,
1609 index_fixed,
1610 if deleted_json > 0 {
1611 format!(", {} stale .json backed up", deleted_json)
1612 } else {
1613 String::new()
1614 }
1615 );
1616 }
1617 Err(e) => {
1618 failed += 1;
1619 println!(" {} {}\n", "[ERR]".red(), e);
1620 }
1621 }
1622 }
1623
1624 println!(
1625 "{} Repair complete: {}/{} workspaces, {} compacted, {} index entries synced",
1626 "[OK]".green().bold(),
1627 succeeded.to_string().green(),
1628 ws_with_sessions.len(),
1629 total_compacted.to_string().cyan(),
1630 total_synced.to_string().cyan()
1631 );
1632 if failed > 0 {
1633 println!(
1634 " {} {} workspace(s) had errors",
1635 "[!]".yellow(),
1636 failed.to_string().red()
1637 );
1638 }
1639
1640 if reopen && vscode_was_running {
1642 println!(" {} Reopening VS Code...", "[*]".yellow());
1643 reopen_vscode(None)?;
1644 println!(
1645 " {} VS Code launched. Sessions should now load correctly.",
1646 "[OK]".green()
1647 );
1648 } else if should_close && vscode_was_running {
1649 println!(
1650 "\n{} VS Code was closed. Reopen it to see the repaired sessions.",
1651 "[!]".yellow()
1652 );
1653 }
1654
1655 Ok(())
1656}
1657
1658pub fn register_trim(
1665 project_path: Option<&str>,
1666 keep: usize,
1667 session_id: Option<&str>,
1668 all: bool,
1669 threshold_mb: u64,
1670 force: bool,
1671) -> Result<()> {
1672 let path = resolve_path(project_path);
1673
1674 println!(
1675 "{} Trimming oversized sessions for: {}",
1676 "[CSM]".cyan().bold(),
1677 path.display()
1678 );
1679
1680 let path_str = path.to_string_lossy().to_string();
1682 let (ws_id, ws_path, _folder) = find_workspace_by_path(&path_str)?
1683 .ok_or_else(|| CsmError::WorkspaceNotFound(path.display().to_string()))?;
1684
1685 let chat_sessions_dir = ws_path.join("chatSessions");
1686
1687 if !chat_sessions_dir.exists() {
1688 println!(
1689 "{} No chatSessions directory found at: {}",
1690 "[!]".yellow(),
1691 chat_sessions_dir.display()
1692 );
1693 return Ok(());
1694 }
1695
1696 if !force && is_vscode_running() {
1698 println!(
1699 "{} VS Code is running. Use {} to force.",
1700 "[!]".yellow(),
1701 "--force".cyan()
1702 );
1703 return Err(CsmError::VSCodeRunning.into());
1704 }
1705
1706 let mut trimmed_count = 0;
1707
1708 if let Some(sid) = session_id {
1709 let jsonl_path = chat_sessions_dir.join(format!("{}.jsonl", sid));
1711 if !jsonl_path.exists() {
1712 return Err(
1713 CsmError::InvalidSessionFormat(format!("Session not found: {}", sid)).into(),
1714 );
1715 }
1716
1717 let size_mb = std::fs::metadata(&jsonl_path)?.len() / (1024 * 1024);
1718 println!(
1719 " {} Trimming {} ({}MB, keeping last {} requests)...",
1720 "[*]".cyan(),
1721 sid,
1722 size_mb,
1723 keep
1724 );
1725
1726 match trim_session_jsonl(&jsonl_path, keep) {
1727 Ok((orig, kept, orig_mb, new_mb)) => {
1728 println!(
1729 " {} Trimmed: {} → {} requests, {:.1}MB → {:.1}MB",
1730 "[OK]".green(),
1731 orig,
1732 kept,
1733 orig_mb,
1734 new_mb
1735 );
1736 trimmed_count += 1;
1737 }
1738 Err(e) => {
1739 println!(" {} Failed to trim {}: {}", "[ERR]".red(), sid, e);
1740 }
1741 }
1742 } else if all {
1743 for entry in std::fs::read_dir(&chat_sessions_dir)? {
1745 let entry = entry?;
1746 let p = entry.path();
1747 if p.extension().is_some_and(|e| e == "jsonl") {
1748 let size = std::fs::metadata(&p)?.len();
1749 let size_mb_val = size / (1024 * 1024);
1750
1751 if size_mb_val >= threshold_mb {
1752 let stem = p
1753 .file_stem()
1754 .map(|s| s.to_string_lossy().to_string())
1755 .unwrap_or_default();
1756 println!(
1757 " {} Trimming {} ({}MB, keeping last {} requests)...",
1758 "[*]".cyan(),
1759 stem,
1760 size_mb_val,
1761 keep
1762 );
1763
1764 match trim_session_jsonl(&p, keep) {
1765 Ok((orig, kept, orig_mb, new_mb)) => {
1766 println!(
1767 " {} Trimmed: {} → {} requests, {:.1}MB → {:.1}MB",
1768 "[OK]".green(),
1769 orig,
1770 kept,
1771 orig_mb,
1772 new_mb
1773 );
1774 trimmed_count += 1;
1775 }
1776 Err(e) => {
1777 println!(" {} Failed to trim {}: {}", "[WARN]".yellow(), stem, e);
1778 }
1779 }
1780 }
1781 }
1782 }
1783 } else {
1784 let mut largest: Option<(PathBuf, u64)> = None;
1786
1787 for entry in std::fs::read_dir(&chat_sessions_dir)? {
1788 let entry = entry?;
1789 let p = entry.path();
1790 if p.extension().is_some_and(|e| e == "jsonl") {
1791 let size = std::fs::metadata(&p)?.len();
1792 let size_mb_val = size / (1024 * 1024);
1793
1794 if size_mb_val >= threshold_mb {
1795 if largest.as_ref().map_or(true, |(_, s)| size > *s) {
1796 largest = Some((p, size));
1797 }
1798 }
1799 }
1800 }
1801
1802 match largest {
1803 Some((p, size)) => {
1804 let stem = p
1805 .file_stem()
1806 .map(|s| s.to_string_lossy().to_string())
1807 .unwrap_or_default();
1808 let size_mb_val = size / (1024 * 1024);
1809 println!(
1810 " {} Trimming largest session: {} ({}MB, keeping last {} requests)...",
1811 "[*]".cyan(),
1812 stem,
1813 size_mb_val,
1814 keep
1815 );
1816
1817 match trim_session_jsonl(&p, keep) {
1818 Ok((orig, kept, orig_mb, new_mb)) => {
1819 println!(
1820 " {} Trimmed: {} → {} requests, {:.1}MB → {:.1}MB",
1821 "[OK]".green(),
1822 orig,
1823 kept,
1824 orig_mb,
1825 new_mb
1826 );
1827 trimmed_count += 1;
1828 }
1829 Err(e) => {
1830 println!(" {} Failed to trim: {}", "[ERR]".red(), e);
1831 }
1832 }
1833 }
1834 None => {
1835 println!(
1836 " {} No sessions found over {}MB threshold. Use {} to lower the threshold.",
1837 "[*]".cyan(),
1838 threshold_mb,
1839 "--threshold-mb".cyan()
1840 );
1841 }
1842 }
1843 }
1844
1845 if trimmed_count > 0 {
1846 let _ = repair_workspace_sessions(&ws_id, &chat_sessions_dir, true);
1848 println!(
1849 "\n{} Trim complete: {} session(s) trimmed. Full history backed up as .jsonl.bak",
1850 "[OK]".green().bold(),
1851 trimmed_count.to_string().cyan()
1852 );
1853 }
1854
1855 Ok(())
1856}