1use crate::error::{CsmError, Result};
6use crate::models::{ChatSession, ChatSessionIndex, ChatSessionIndexEntry};
7use crate::workspace::{get_empty_window_sessions_path, get_workspace_storage_path};
8use rusqlite::Connection;
9use std::path::{Path, PathBuf};
10use sysinfo::System;
11
12pub fn get_workspace_storage_db(workspace_id: &str) -> Result<PathBuf> {
14 let storage_path = get_workspace_storage_path()?;
15 Ok(storage_path.join(workspace_id).join("state.vscdb"))
16}
17
18pub fn read_chat_session_index(db_path: &Path) -> Result<ChatSessionIndex> {
20 let conn = Connection::open(db_path)?;
21
22 let result: std::result::Result<String, rusqlite::Error> = conn.query_row(
23 "SELECT value FROM ItemTable WHERE key = ?",
24 ["chat.ChatSessionStore.index"],
25 |row| row.get(0),
26 );
27
28 match result {
29 Ok(json_str) => serde_json::from_str(&json_str)
30 .map_err(|e| CsmError::InvalidSessionFormat(e.to_string())),
31 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(ChatSessionIndex::default()),
32 Err(e) => Err(CsmError::SqliteError(e)),
33 }
34}
35
36pub fn write_chat_session_index(db_path: &Path, index: &ChatSessionIndex) -> Result<()> {
38 let conn = Connection::open(db_path)?;
39 let json_str = serde_json::to_string(index)?;
40
41 let exists: bool = conn.query_row(
43 "SELECT COUNT(*) > 0 FROM ItemTable WHERE key = ?",
44 ["chat.ChatSessionStore.index"],
45 |row| row.get(0),
46 )?;
47
48 if exists {
49 conn.execute(
50 "UPDATE ItemTable SET value = ? WHERE key = ?",
51 [&json_str, "chat.ChatSessionStore.index"],
52 )?;
53 } else {
54 conn.execute(
55 "INSERT INTO ItemTable (key, value) VALUES (?, ?)",
56 ["chat.ChatSessionStore.index", &json_str],
57 )?;
58 }
59
60 Ok(())
61}
62
63pub fn add_session_to_index(
65 db_path: &Path,
66 session_id: &str,
67 title: &str,
68 last_message_date_ms: i64,
69 is_imported: bool,
70 initial_location: &str,
71 is_empty: bool,
72) -> Result<()> {
73 let mut index = read_chat_session_index(db_path)?;
74
75 index.entries.insert(
76 session_id.to_string(),
77 ChatSessionIndexEntry {
78 session_id: session_id.to_string(),
79 title: title.to_string(),
80 last_message_date: last_message_date_ms,
81 is_imported,
82 initial_location: initial_location.to_string(),
83 is_empty,
84 },
85 );
86
87 write_chat_session_index(db_path, &index)
88}
89
90pub fn remove_session_from_index(db_path: &Path, session_id: &str) -> Result<bool> {
92 let mut index = read_chat_session_index(db_path)?;
93 let removed = index.entries.remove(session_id).is_some();
94 if removed {
95 write_chat_session_index(db_path, &index)?;
96 }
97 Ok(removed)
98}
99
100pub fn sync_session_index(
102 workspace_id: &str,
103 chat_sessions_dir: &Path,
104 force: bool,
105) -> Result<(usize, usize)> {
106 let db_path = get_workspace_storage_db(workspace_id)?;
107
108 if !db_path.exists() {
109 return Err(CsmError::WorkspaceNotFound(format!(
110 "Database not found: {}",
111 db_path.display()
112 )));
113 }
114
115 if !force && is_vscode_running() {
117 return Err(CsmError::VSCodeRunning);
118 }
119
120 let mut index = read_chat_session_index(&db_path)?;
122
123 let mut files_on_disk: std::collections::HashSet<String> = std::collections::HashSet::new();
125 if chat_sessions_dir.exists() {
126 for entry in std::fs::read_dir(chat_sessions_dir)? {
127 let entry = entry?;
128 let path = entry.path();
129 if path.extension().map(|e| e == "json").unwrap_or(false) {
130 if let Some(stem) = path.file_stem() {
131 files_on_disk.insert(stem.to_string_lossy().to_string());
132 }
133 }
134 }
135 }
136
137 let stale_ids: Vec<String> = index
139 .entries
140 .keys()
141 .filter(|id| !files_on_disk.contains(*id))
142 .cloned()
143 .collect();
144
145 let removed = stale_ids.len();
146 for id in &stale_ids {
147 index.entries.remove(id);
148 }
149
150 let mut added = 0;
152 for entry in std::fs::read_dir(chat_sessions_dir)? {
153 let entry = entry?;
154 let path = entry.path();
155
156 if path.extension().map(|e| e == "json").unwrap_or(false) {
157 if let Ok(content) = std::fs::read_to_string(&path) {
158 if let Ok(session) = serde_json::from_str::<ChatSession>(&content) {
159 let session_id = session.session_id.clone().unwrap_or_else(|| {
160 path.file_stem()
161 .map(|s| s.to_string_lossy().to_string())
162 .unwrap_or_else(|| uuid::Uuid::new_v4().to_string())
163 });
164
165 let title = session.title();
166 let is_empty = session.is_empty();
167 let last_message_date = session.last_message_date;
168 let initial_location = session.initial_location.clone();
169
170 index.entries.insert(
171 session_id.clone(),
172 ChatSessionIndexEntry {
173 session_id,
174 title,
175 last_message_date,
176 is_imported: session.is_imported,
177 initial_location,
178 is_empty,
179 },
180 );
181 added += 1;
182 }
183 }
184 }
185 }
186
187 write_chat_session_index(&db_path, &index)?;
189
190 Ok((added, removed))
191}
192
193pub fn register_all_sessions_from_directory(
195 workspace_id: &str,
196 chat_sessions_dir: &Path,
197 force: bool,
198) -> Result<usize> {
199 let db_path = get_workspace_storage_db(workspace_id)?;
200
201 if !db_path.exists() {
202 return Err(CsmError::WorkspaceNotFound(format!(
203 "Database not found: {}",
204 db_path.display()
205 )));
206 }
207
208 if !force && is_vscode_running() {
210 return Err(CsmError::VSCodeRunning);
211 }
212
213 let (added, removed) = sync_session_index(workspace_id, chat_sessions_dir, force)?;
215
216 for entry in std::fs::read_dir(chat_sessions_dir)? {
218 let entry = entry?;
219 let path = entry.path();
220
221 if path.extension().map(|e| e == "json").unwrap_or(false) {
222 if let Ok(content) = std::fs::read_to_string(&path) {
223 if let Ok(session) = serde_json::from_str::<ChatSession>(&content) {
224 let session_id = session.session_id.clone().unwrap_or_else(|| {
225 path.file_stem()
226 .map(|s| s.to_string_lossy().to_string())
227 .unwrap_or_else(|| uuid::Uuid::new_v4().to_string())
228 });
229
230 let title = session.title();
231
232 println!(
233 "[OK] Registered: {} ({}...)",
234 title,
235 &session_id[..12.min(session_id.len())]
236 );
237 }
238 }
239 }
240 }
241
242 if removed > 0 {
243 println!("[OK] Removed {} stale index entries", removed);
244 }
245
246 Ok(added)
247}
248
249pub fn is_vscode_running() -> bool {
251 let mut sys = System::new();
252 sys.refresh_processes();
253
254 for process in sys.processes().values() {
255 let name = process.name().to_lowercase();
256 if name.contains("code") && !name.contains("codec") {
257 return true;
258 }
259 }
260
261 false
262}
263
264pub fn backup_workspace_sessions(workspace_dir: &Path) -> Result<Option<PathBuf>> {
266 let chat_sessions_dir = workspace_dir.join("chatSessions");
267
268 if !chat_sessions_dir.exists() {
269 return Ok(None);
270 }
271
272 let timestamp = std::time::SystemTime::now()
273 .duration_since(std::time::UNIX_EPOCH)
274 .unwrap()
275 .as_secs();
276
277 let backup_dir = workspace_dir.join(format!("chatSessions-backup-{}", timestamp));
278
279 copy_dir_all(&chat_sessions_dir, &backup_dir)?;
281
282 Ok(Some(backup_dir))
283}
284
285fn copy_dir_all(src: &Path, dst: &Path) -> Result<()> {
287 std::fs::create_dir_all(dst)?;
288
289 for entry in std::fs::read_dir(src)? {
290 let entry = entry?;
291 let src_path = entry.path();
292 let dst_path = dst.join(entry.file_name());
293
294 if src_path.is_dir() {
295 copy_dir_all(&src_path, &dst_path)?;
296 } else {
297 std::fs::copy(&src_path, &dst_path)?;
298 }
299 }
300
301 Ok(())
302}
303
304pub fn read_empty_window_sessions() -> Result<Vec<ChatSession>> {
311 let sessions_path = get_empty_window_sessions_path()?;
312
313 if !sessions_path.exists() {
314 return Ok(Vec::new());
315 }
316
317 let mut sessions = Vec::new();
318
319 for entry in std::fs::read_dir(&sessions_path)? {
320 let entry = entry?;
321 let path = entry.path();
322
323 if path.extension().is_some_and(|e| e == "json") {
324 if let Ok(content) = std::fs::read_to_string(&path) {
325 if let Ok(session) = serde_json::from_str::<ChatSession>(&content) {
326 sessions.push(session);
327 }
328 }
329 }
330 }
331
332 sessions.sort_by(|a, b| b.last_message_date.cmp(&a.last_message_date));
334
335 Ok(sessions)
336}
337
338#[allow(dead_code)]
340pub fn get_empty_window_session(session_id: &str) -> Result<Option<ChatSession>> {
341 let sessions_path = get_empty_window_sessions_path()?;
342 let session_path = sessions_path.join(format!("{}.json", session_id));
343
344 if !session_path.exists() {
345 return Ok(None);
346 }
347
348 let content = std::fs::read_to_string(&session_path)?;
349 let session: ChatSession = serde_json::from_str(&content)
350 .map_err(|e| CsmError::InvalidSessionFormat(e.to_string()))?;
351
352 Ok(Some(session))
353}
354
355#[allow(dead_code)]
357pub fn write_empty_window_session(session: &ChatSession) -> Result<PathBuf> {
358 let sessions_path = get_empty_window_sessions_path()?;
359
360 std::fs::create_dir_all(&sessions_path)?;
362
363 let session_id = session.session_id.as_deref().unwrap_or("unknown");
364 let session_path = sessions_path.join(format!("{}.json", session_id));
365 let content = serde_json::to_string_pretty(session)?;
366 std::fs::write(&session_path, content)?;
367
368 Ok(session_path)
369}
370
371#[allow(dead_code)]
373pub fn delete_empty_window_session(session_id: &str) -> Result<bool> {
374 let sessions_path = get_empty_window_sessions_path()?;
375 let session_path = sessions_path.join(format!("{}.json", session_id));
376
377 if session_path.exists() {
378 std::fs::remove_file(&session_path)?;
379 Ok(true)
380 } else {
381 Ok(false)
382 }
383}
384
385pub fn count_empty_window_sessions() -> Result<usize> {
387 let sessions_path = get_empty_window_sessions_path()?;
388
389 if !sessions_path.exists() {
390 return Ok(0);
391 }
392
393 let count = std::fs::read_dir(&sessions_path)?
394 .filter_map(|e| e.ok())
395 .filter(|e| e.path().extension().is_some_and(|ext| ext == "json"))
396 .count();
397
398 Ok(count)
399}