1use anyhow::Result;
10use colored::*;
11use std::collections::HashSet;
12use std::path::PathBuf;
13
14use crate::error::CsmError;
15use crate::models::ChatSession;
16use crate::storage::{
17 add_session_to_index, get_workspace_storage_db, is_vscode_running, parse_session_json,
18 read_chat_session_index, register_all_sessions_from_directory,
19};
20use crate::workspace::find_workspace_by_path;
21
22fn resolve_path(path: Option<&str>) -> PathBuf {
24 match path {
25 Some(p) => {
26 let path = PathBuf::from(p);
27 path.canonicalize().unwrap_or(path)
28 }
29 None => std::env::current_dir().unwrap_or_default(),
30 }
31}
32
33
34pub fn register_all(project_path: Option<&str>, merge: bool, force: bool) -> Result<()> {
36 let path = resolve_path(project_path);
37
38 if merge {
39 println!(
40 "{} Merging and registering all sessions for: {}",
41 "[CSM]".cyan().bold(),
42 path.display()
43 );
44
45 let path_str = path.to_string_lossy().to_string();
47 return crate::commands::history_merge(
48 Some(&path_str),
49 None, force, false, );
53 }
54
55 println!(
56 "{} Registering all sessions for: {}",
57 "[CSM]".cyan().bold(),
58 path.display()
59 );
60
61 let path_str = path.to_string_lossy().to_string();
63 let (ws_id, ws_path, _folder) = find_workspace_by_path(&path_str)?
64 .ok_or_else(|| CsmError::WorkspaceNotFound(path.display().to_string()))?;
65
66 let chat_sessions_dir = ws_path.join("chatSessions");
67
68 if !chat_sessions_dir.exists() {
69 println!(
70 "{} No chatSessions directory found at: {}",
71 "[!]".yellow(),
72 chat_sessions_dir.display()
73 );
74 return Ok(());
75 }
76
77 if !force && is_vscode_running() {
79 println!(
80 "{} VS Code is running. Use {} to register anyway.",
81 "[!]".yellow(),
82 "--force".cyan()
83 );
84 println!(" Note: VS Code uses WAL mode so this is generally safe.");
85 return Err(CsmError::VSCodeRunning.into());
86 }
87
88 let sessions_on_disk = count_sessions_in_directory(&chat_sessions_dir)?;
90 println!(
91 " Found {} session files on disk",
92 sessions_on_disk.to_string().green()
93 );
94
95 let registered = register_all_sessions_from_directory(&ws_id, &chat_sessions_dir, force)?;
97
98 println!(
99 "\n{} Registered {} sessions in VS Code's index",
100 "[OK]".green().bold(),
101 registered.to_string().cyan()
102 );
103
104 println!(
106 "\n{} VS Code caches the session index in memory.",
107 "[!]".yellow()
108 );
109 println!(" To see the new sessions, do one of the following:");
110 println!(
111 " * Run: {} (if CSM extension is installed)",
112 "code --command csm.reloadAndShowChats".cyan()
113 );
114 println!(
115 " * Or press {} in VS Code and run {}",
116 "Ctrl+Shift+P".cyan(),
117 "Developer: Reload Window".cyan()
118 );
119 println!(" * Or restart VS Code");
120
121 Ok(())
122}
123
124pub fn register_sessions(
126 ids: &[String],
127 titles: Option<&[String]>,
128 project_path: Option<&str>,
129 force: bool,
130) -> Result<()> {
131 let path = resolve_path(project_path);
132
133 let path_str = path.to_string_lossy().to_string();
135 let (ws_id, ws_path, _folder) = find_workspace_by_path(&path_str)?
136 .ok_or_else(|| CsmError::WorkspaceNotFound(path.display().to_string()))?;
137
138 let chat_sessions_dir = ws_path.join("chatSessions");
139
140 if !force && is_vscode_running() {
142 println!(
143 "{} VS Code is running. Use {} to register anyway.",
144 "[!]".yellow(),
145 "--force".cyan()
146 );
147 return Err(CsmError::VSCodeRunning.into());
148 }
149
150 let db_path = get_workspace_storage_db(&ws_id)?;
152
153 let mut registered_count = 0;
154
155 if let Some(titles) = titles {
156 println!(
158 "{} Registering {} sessions by title:",
159 "[CSM]".cyan().bold(),
160 titles.len()
161 );
162
163 let sessions = find_sessions_by_titles(&chat_sessions_dir, titles)?;
164
165 for (session, session_path) in sessions {
166 let session_id = session.session_id.clone().unwrap_or_else(|| {
167 session_path
168 .file_stem()
169 .map(|s| s.to_string_lossy().to_string())
170 .unwrap_or_default()
171 });
172 let title = session.title();
173
174 add_session_to_index(
175 &db_path,
176 &session_id,
177 &title,
178 session.last_message_date,
179 session.is_imported,
180 &session.initial_location,
181 session.is_empty(),
182 )?;
183
184 let id_display = if session_id.len() > 12 {
185 &session_id[..12]
186 } else {
187 &session_id
188 };
189 println!(
190 " {} {} (\"{}\")",
191 "[OK]".green(),
192 id_display.cyan(),
193 title.yellow()
194 );
195 registered_count += 1;
196 }
197 } else {
198 println!(
200 "{} Registering {} sessions by ID:",
201 "[CSM]".cyan().bold(),
202 ids.len()
203 );
204
205 for session_id in ids {
206 match find_session_file(&chat_sessions_dir, session_id) {
207 Ok(session_file) => {
208 let content = std::fs::read_to_string(&session_file)?;
209 let session: ChatSession = serde_json::from_str(&content)?;
210
211 let title = session.title();
212 let actual_session_id = session
213 .session_id
214 .clone()
215 .unwrap_or_else(|| session_id.to_string());
216
217 add_session_to_index(
218 &db_path,
219 &actual_session_id,
220 &title,
221 session.last_message_date,
222 session.is_imported,
223 &session.initial_location,
224 session.is_empty(),
225 )?;
226
227 let id_display = if actual_session_id.len() > 12 {
228 &actual_session_id[..12]
229 } else {
230 &actual_session_id
231 };
232 println!(
233 " {} {} (\"{}\")",
234 "[OK]".green(),
235 id_display.cyan(),
236 title.yellow()
237 );
238 registered_count += 1;
239 }
240 Err(e) => {
241 println!(
242 " {} {} - {}",
243 "[ERR]".red(),
244 session_id.cyan(),
245 e.to_string().red()
246 );
247 }
248 }
249 }
250 }
251
252 println!(
253 "\n{} Registered {} sessions in VS Code's index",
254 "[OK]".green().bold(),
255 registered_count.to_string().cyan()
256 );
257
258 if force && is_vscode_running() {
259 println!(
260 " {} Sessions should appear in VS Code immediately",
261 "->".cyan()
262 );
263 }
264
265 Ok(())
266}
267
268pub fn list_orphaned(project_path: Option<&str>) -> Result<()> {
270 let path = resolve_path(project_path);
271
272 println!(
273 "{} Finding orphaned sessions for: {}",
274 "[CSM]".cyan().bold(),
275 path.display()
276 );
277
278 let path_str = path.to_string_lossy().to_string();
280 let (ws_id, ws_path, _folder) = find_workspace_by_path(&path_str)?
281 .ok_or_else(|| CsmError::WorkspaceNotFound(path.display().to_string()))?;
282
283 let chat_sessions_dir = ws_path.join("chatSessions");
284
285 if !chat_sessions_dir.exists() {
286 println!("{} No chatSessions directory found", "[!]".yellow());
287 return Ok(());
288 }
289
290 let db_path = get_workspace_storage_db(&ws_id)?;
292 let index = read_chat_session_index(&db_path)?;
293 let indexed_ids: HashSet<String> = index.entries.keys().cloned().collect();
294
295 println!(
296 " {} sessions currently in VS Code's index",
297 indexed_ids.len().to_string().cyan()
298 );
299
300 let mut orphaned_sessions = Vec::new();
302
303 for entry in std::fs::read_dir(&chat_sessions_dir)? {
304 let entry = entry?;
305 let path = entry.path();
306
307 if path.extension().map(|e| e == "json").unwrap_or(false) {
308 if let Ok(content) = std::fs::read_to_string(&path) {
309 if let Ok(session) = parse_session_json(&content) {
310 let session_id = session.session_id.clone().unwrap_or_else(|| {
311 path.file_stem()
312 .map(|s| s.to_string_lossy().to_string())
313 .unwrap_or_default()
314 });
315
316 if !indexed_ids.contains(&session_id) {
317 let title = session.title();
318 let msg_count = session.requests.len();
319 orphaned_sessions.push((session_id, title, msg_count, path.clone()));
320 }
321 }
322 }
323 }
324 }
325
326 if orphaned_sessions.is_empty() {
327 println!(
328 "\n{} No orphaned sessions found - all sessions are registered!",
329 "[OK]".green().bold()
330 );
331 return Ok(());
332 }
333
334 println!(
335 "\n{} Found {} orphaned sessions (on disk but not in index):\n",
336 "[!]".yellow().bold(),
337 orphaned_sessions.len().to_string().red()
338 );
339
340 for (session_id, title, msg_count, _path) in &orphaned_sessions {
341 let id_display = if session_id.len() > 12 {
342 &session_id[..12]
343 } else {
344 session_id
345 };
346 println!(
347 " {} {} ({} messages)",
348 id_display.cyan(),
349 format!("\"{}\"", title).yellow(),
350 msg_count
351 );
352 }
353
354 println!("\n{} To register all orphaned sessions:", "->".cyan());
355 println!(" csm register all --force");
356 println!("\n{} To register specific sessions:", "->".cyan());
357 println!(" csm register session <ID1> <ID2> ... --force");
358
359 Ok(())
360}
361
362fn count_sessions_in_directory(dir: &PathBuf) -> Result<usize> {
364 let mut count = 0;
365 for entry in std::fs::read_dir(dir)? {
366 let entry = entry?;
367 if entry
368 .path()
369 .extension()
370 .map(|e| e == "json")
371 .unwrap_or(false)
372 {
373 count += 1;
374 }
375 }
376 Ok(count)
377}
378
379fn find_session_file(chat_sessions_dir: &PathBuf, session_id: &str) -> Result<PathBuf> {
381 let exact_path = chat_sessions_dir.join(format!("{}.json", session_id));
383 if exact_path.exists() {
384 return Ok(exact_path);
385 }
386
387 for entry in std::fs::read_dir(chat_sessions_dir)? {
389 let entry = entry?;
390 let path = entry.path();
391
392 if path.extension().map(|e| e == "json").unwrap_or(false) {
393 let filename = path
394 .file_stem()
395 .map(|s| s.to_string_lossy().to_string())
396 .unwrap_or_default();
397
398 if filename.starts_with(session_id) {
399 return Ok(path);
400 }
401
402 if let Ok(content) = std::fs::read_to_string(&path) {
404 if let Ok(session) = parse_session_json(&content) {
405 if let Some(ref sid) = session.session_id {
406 if sid.starts_with(session_id) || sid == session_id {
407 return Ok(path);
408 }
409 }
410 }
411 }
412 }
413 }
414
415 Err(CsmError::SessionNotFound(session_id.to_string()).into())
416}
417
418fn find_sessions_by_titles(
420 chat_sessions_dir: &PathBuf,
421 titles: &[String],
422) -> Result<Vec<(ChatSession, PathBuf)>> {
423 let mut matches = Vec::new();
424 let title_patterns: Vec<String> = titles.iter().map(|t| t.to_lowercase()).collect();
425
426 for entry in std::fs::read_dir(chat_sessions_dir)? {
427 let entry = entry?;
428 let path = entry.path();
429
430 if path.extension().map(|e| e == "json").unwrap_or(false) {
431 if let Ok(content) = std::fs::read_to_string(&path) {
432 if let Ok(session) = parse_session_json(&content) {
433 let session_title = session.title().to_lowercase();
434
435 for pattern in &title_patterns {
436 if session_title.contains(pattern) {
437 matches.push((session, path.clone()));
438 break;
439 }
440 }
441 }
442 }
443 }
444 }
445
446 if matches.is_empty() {
447 println!(
448 "{} No sessions found matching the specified titles",
449 "[!]".yellow()
450 );
451 }
452
453 Ok(matches)
454}