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