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