1use crate::error::{CsmError, Result};
6use crate::models::{ChatSession, ChatSessionIndex, ChatSessionIndexEntry};
7use crate::workspace::{get_empty_window_sessions_path, get_workspace_storage_path};
8use regex::Regex;
9use rusqlite::Connection;
10use std::path::{Path, PathBuf};
11use sysinfo::System;
12
13fn sanitize_json_unicode(content: &str) -> String {
16 let re = Regex::new(r"\\u[0-9a-fA-F]{4}").unwrap();
19 let mut result = content.to_string();
20
21 let matches: Vec<_> = re
23 .find_iter(content)
24 .map(|m| (m.start(), m.end(), m.as_str().to_string()))
25 .collect();
26
27 for i in (0..matches.len()).rev() {
29 let (start, end, escape) = &matches[i];
30 let hex = &escape[2..];
31 if let Ok(code) = u16::from_str_radix(hex, 16) {
32 if (0xD800..=0xDBFF).contains(&code) {
34 let has_low = matches
36 .get(i + 1)
37 .is_some_and(|(next_start, _, next_escape)| {
38 *next_start == *end && {
39 let next_hex = &next_escape[2..];
40 u16::from_str_radix(next_hex, 16)
41 .map(|c| (0xDC00..=0xDFFF).contains(&c))
42 .unwrap_or(false)
43 }
44 });
45 if !has_low {
46 result.replace_range(*start..*end, r"\uFFFD");
47 }
48 }
49 else if (0xDC00..=0xDFFF).contains(&code) {
51 let has_high = i > 0
53 && matches
54 .get(i - 1)
55 .is_some_and(|(_, prev_end, prev_escape)| {
56 *prev_end == *start && {
57 let prev_hex = &prev_escape[2..];
58 u16::from_str_radix(prev_hex, 16)
59 .map(|c| (0xD800..=0xDBFF).contains(&c))
60 .unwrap_or(false)
61 }
62 });
63 if !has_high {
64 result.replace_range(*start..*end, r"\uFFFD");
65 }
66 }
67 }
68 }
69
70 result
71}
72
73pub fn parse_session_json(content: &str) -> std::result::Result<ChatSession, serde_json::Error> {
75 match serde_json::from_str::<ChatSession>(content) {
76 Ok(session) => Ok(session),
77 Err(e) => {
78 if e.to_string().contains("surrogate") || e.to_string().contains("escape") {
80 let sanitized = sanitize_json_unicode(content);
81 serde_json::from_str::<ChatSession>(&sanitized)
82 } else {
83 Err(e)
84 }
85 }
86 }
87}
88
89pub fn get_workspace_storage_db(workspace_id: &str) -> Result<PathBuf> {
91 let storage_path = get_workspace_storage_path()?;
92 Ok(storage_path.join(workspace_id).join("state.vscdb"))
93}
94
95pub fn read_chat_session_index(db_path: &Path) -> Result<ChatSessionIndex> {
97 let conn = Connection::open(db_path)?;
98
99 let result: std::result::Result<String, rusqlite::Error> = conn.query_row(
100 "SELECT value FROM ItemTable WHERE key = ?",
101 ["chat.ChatSessionStore.index"],
102 |row| row.get(0),
103 );
104
105 match result {
106 Ok(json_str) => serde_json::from_str(&json_str)
107 .map_err(|e| CsmError::InvalidSessionFormat(e.to_string())),
108 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(ChatSessionIndex::default()),
109 Err(e) => Err(CsmError::SqliteError(e)),
110 }
111}
112
113pub fn write_chat_session_index(db_path: &Path, index: &ChatSessionIndex) -> Result<()> {
115 let conn = Connection::open(db_path)?;
116 let json_str = serde_json::to_string(index)?;
117
118 let exists: bool = conn.query_row(
120 "SELECT COUNT(*) > 0 FROM ItemTable WHERE key = ?",
121 ["chat.ChatSessionStore.index"],
122 |row| row.get(0),
123 )?;
124
125 if exists {
126 conn.execute(
127 "UPDATE ItemTable SET value = ? WHERE key = ?",
128 [&json_str, "chat.ChatSessionStore.index"],
129 )?;
130 } else {
131 conn.execute(
132 "INSERT INTO ItemTable (key, value) VALUES (?, ?)",
133 ["chat.ChatSessionStore.index", &json_str],
134 )?;
135 }
136
137 Ok(())
138}
139
140pub fn add_session_to_index(
142 db_path: &Path,
143 session_id: &str,
144 title: &str,
145 last_message_date_ms: i64,
146 is_imported: bool,
147 initial_location: &str,
148 is_empty: bool,
149) -> Result<()> {
150 let mut index = read_chat_session_index(db_path)?;
151
152 index.entries.insert(
153 session_id.to_string(),
154 ChatSessionIndexEntry {
155 session_id: session_id.to_string(),
156 title: title.to_string(),
157 last_message_date: last_message_date_ms,
158 is_imported,
159 initial_location: initial_location.to_string(),
160 is_empty,
161 },
162 );
163
164 write_chat_session_index(db_path, &index)
165}
166
167#[allow(dead_code)]
169pub fn remove_session_from_index(db_path: &Path, session_id: &str) -> Result<bool> {
170 let mut index = read_chat_session_index(db_path)?;
171 let removed = index.entries.remove(session_id).is_some();
172 if removed {
173 write_chat_session_index(db_path, &index)?;
174 }
175 Ok(removed)
176}
177
178pub fn sync_session_index(
180 workspace_id: &str,
181 chat_sessions_dir: &Path,
182 force: bool,
183) -> Result<(usize, usize)> {
184 let db_path = get_workspace_storage_db(workspace_id)?;
185
186 if !db_path.exists() {
187 return Err(CsmError::WorkspaceNotFound(format!(
188 "Database not found: {}",
189 db_path.display()
190 )));
191 }
192
193 if !force && is_vscode_running() {
195 return Err(CsmError::VSCodeRunning);
196 }
197
198 let mut index = read_chat_session_index(&db_path)?;
200
201 let mut files_on_disk: std::collections::HashSet<String> = std::collections::HashSet::new();
203 if chat_sessions_dir.exists() {
204 for entry in std::fs::read_dir(chat_sessions_dir)? {
205 let entry = entry?;
206 let path = entry.path();
207 if path.extension().map(|e| e == "json").unwrap_or(false) {
208 if let Some(stem) = path.file_stem() {
209 files_on_disk.insert(stem.to_string_lossy().to_string());
210 }
211 }
212 }
213 }
214
215 let stale_ids: Vec<String> = index
217 .entries
218 .keys()
219 .filter(|id| !files_on_disk.contains(*id))
220 .cloned()
221 .collect();
222
223 let removed = stale_ids.len();
224 for id in &stale_ids {
225 index.entries.remove(id);
226 }
227
228 let mut added = 0;
230 for entry in std::fs::read_dir(chat_sessions_dir)? {
231 let entry = entry?;
232 let path = entry.path();
233
234 if path.extension().map(|e| e == "json").unwrap_or(false) {
235 if let Ok(content) = std::fs::read_to_string(&path) {
236 if let Ok(session) = parse_session_json(&content) {
237 let session_id = session.session_id.clone().unwrap_or_else(|| {
238 path.file_stem()
239 .map(|s| s.to_string_lossy().to_string())
240 .unwrap_or_else(|| uuid::Uuid::new_v4().to_string())
241 });
242
243 let title = session.title();
244 let is_empty = session.is_empty();
245 let last_message_date = session.last_message_date;
246 let initial_location = session.initial_location.clone();
247
248 index.entries.insert(
249 session_id.clone(),
250 ChatSessionIndexEntry {
251 session_id,
252 title,
253 last_message_date,
254 is_imported: session.is_imported,
255 initial_location,
256 is_empty,
257 },
258 );
259 added += 1;
260 }
261 }
262 }
263 }
264
265 write_chat_session_index(&db_path, &index)?;
267
268 Ok((added, removed))
269}
270
271pub fn register_all_sessions_from_directory(
273 workspace_id: &str,
274 chat_sessions_dir: &Path,
275 force: bool,
276) -> Result<usize> {
277 let db_path = get_workspace_storage_db(workspace_id)?;
278
279 if !db_path.exists() {
280 return Err(CsmError::WorkspaceNotFound(format!(
281 "Database not found: {}",
282 db_path.display()
283 )));
284 }
285
286 if !force && is_vscode_running() {
288 return Err(CsmError::VSCodeRunning);
289 }
290
291 let (added, removed) = sync_session_index(workspace_id, chat_sessions_dir, force)?;
293
294 for entry in std::fs::read_dir(chat_sessions_dir)? {
296 let entry = entry?;
297 let path = entry.path();
298
299 if path.extension().map(|e| e == "json").unwrap_or(false) {
300 if let Ok(content) = std::fs::read_to_string(&path) {
301 if let Ok(session) = parse_session_json(&content) {
302 let session_id = session.session_id.clone().unwrap_or_else(|| {
303 path.file_stem()
304 .map(|s| s.to_string_lossy().to_string())
305 .unwrap_or_else(|| uuid::Uuid::new_v4().to_string())
306 });
307
308 let title = session.title();
309
310 println!(
311 "[OK] Registered: {} ({}...)",
312 title,
313 &session_id[..12.min(session_id.len())]
314 );
315 }
316 }
317 }
318 }
319
320 if removed > 0 {
321 println!("[OK] Removed {} stale index entries", removed);
322 }
323
324 Ok(added)
325}
326
327pub fn is_vscode_running() -> bool {
329 let mut sys = System::new();
330 sys.refresh_processes();
331
332 for process in sys.processes().values() {
333 let name = process.name().to_lowercase();
334 if name.contains("code") && !name.contains("codec") {
335 return true;
336 }
337 }
338
339 false
340}
341
342pub fn backup_workspace_sessions(workspace_dir: &Path) -> Result<Option<PathBuf>> {
344 let chat_sessions_dir = workspace_dir.join("chatSessions");
345
346 if !chat_sessions_dir.exists() {
347 return Ok(None);
348 }
349
350 let timestamp = std::time::SystemTime::now()
351 .duration_since(std::time::UNIX_EPOCH)
352 .unwrap()
353 .as_secs();
354
355 let backup_dir = workspace_dir.join(format!("chatSessions-backup-{}", timestamp));
356
357 copy_dir_all(&chat_sessions_dir, &backup_dir)?;
359
360 Ok(Some(backup_dir))
361}
362
363fn copy_dir_all(src: &Path, dst: &Path) -> Result<()> {
365 std::fs::create_dir_all(dst)?;
366
367 for entry in std::fs::read_dir(src)? {
368 let entry = entry?;
369 let src_path = entry.path();
370 let dst_path = dst.join(entry.file_name());
371
372 if src_path.is_dir() {
373 copy_dir_all(&src_path, &dst_path)?;
374 } else {
375 std::fs::copy(&src_path, &dst_path)?;
376 }
377 }
378
379 Ok(())
380}
381
382pub fn read_empty_window_sessions() -> Result<Vec<ChatSession>> {
389 let sessions_path = get_empty_window_sessions_path()?;
390
391 if !sessions_path.exists() {
392 return Ok(Vec::new());
393 }
394
395 let mut sessions = Vec::new();
396
397 for entry in std::fs::read_dir(&sessions_path)? {
398 let entry = entry?;
399 let path = entry.path();
400
401 if path.extension().is_some_and(|e| e == "json") {
402 if let Ok(content) = std::fs::read_to_string(&path) {
403 if let Ok(session) = parse_session_json(&content) {
404 sessions.push(session);
405 }
406 }
407 }
408 }
409
410 sessions.sort_by(|a, b| b.last_message_date.cmp(&a.last_message_date));
412
413 Ok(sessions)
414}
415
416#[allow(dead_code)]
418pub fn get_empty_window_session(session_id: &str) -> Result<Option<ChatSession>> {
419 let sessions_path = get_empty_window_sessions_path()?;
420 let session_path = sessions_path.join(format!("{}.json", session_id));
421
422 if !session_path.exists() {
423 return Ok(None);
424 }
425
426 let content = std::fs::read_to_string(&session_path)?;
427 let session: ChatSession = serde_json::from_str(&content)
428 .map_err(|e| CsmError::InvalidSessionFormat(e.to_string()))?;
429
430 Ok(Some(session))
431}
432
433#[allow(dead_code)]
435pub fn write_empty_window_session(session: &ChatSession) -> Result<PathBuf> {
436 let sessions_path = get_empty_window_sessions_path()?;
437
438 std::fs::create_dir_all(&sessions_path)?;
440
441 let session_id = session.session_id.as_deref().unwrap_or("unknown");
442 let session_path = sessions_path.join(format!("{}.json", session_id));
443 let content = serde_json::to_string_pretty(session)?;
444 std::fs::write(&session_path, content)?;
445
446 Ok(session_path)
447}
448
449#[allow(dead_code)]
451pub fn delete_empty_window_session(session_id: &str) -> Result<bool> {
452 let sessions_path = get_empty_window_sessions_path()?;
453 let session_path = sessions_path.join(format!("{}.json", session_id));
454
455 if session_path.exists() {
456 std::fs::remove_file(&session_path)?;
457 Ok(true)
458 } else {
459 Ok(false)
460 }
461}
462
463pub fn count_empty_window_sessions() -> Result<usize> {
465 let sessions_path = get_empty_window_sessions_path()?;
466
467 if !sessions_path.exists() {
468 return Ok(0);
469 }
470
471 let count = std::fs::read_dir(&sessions_path)?
472 .filter_map(|e| e.ok())
473 .filter(|e| e.path().extension().is_some_and(|ext| ext == "json"))
474 .count();
475
476 Ok(count)
477}