1use super::file_history::{FileHistoryManager, RewindResult};
6use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
10pub enum RewindOption {
11 Code,
12 Conversation,
13 Both,
14 Nevermind,
15}
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct RewindableMessage {
20 pub uuid: String,
21 pub index: usize,
22 pub preview: String,
23 pub timestamp: Option<i64>,
24 pub has_file_changes: bool,
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct RewindOperationResult {
30 pub success: bool,
31 pub option: RewindOption,
32 pub code_result: Option<RewindResult>,
33 pub conversation_result: Option<ConversationRewindResult>,
34 pub error: Option<String>,
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct ConversationRewindResult {
40 pub messages_removed: i32,
41 pub new_message_count: usize,
42}
43
44pub struct RewindManager {
46 file_history: FileHistoryManager,
47 message_count: usize,
48}
49
50impl RewindManager {
51 pub fn new(session_id: impl Into<String>) -> Self {
53 Self {
54 file_history: FileHistoryManager::new(session_id),
55 message_count: 0,
56 }
57 }
58
59 pub fn get_file_history_manager(&self) -> &FileHistoryManager {
61 &self.file_history
62 }
63
64 pub fn get_file_history_manager_mut(&mut self) -> &mut FileHistoryManager {
66 &mut self.file_history
67 }
68
69 pub fn record_user_message(&mut self, message_id: impl Into<String>) {
71 self.file_history.create_snapshot(message_id);
72 self.message_count += 1;
73 }
74
75 pub fn record_file_change(&mut self, file_path: impl AsRef<std::path::Path>) {
77 self.file_history
78 .backup_file_before_change(file_path.as_ref());
79 self.file_history.track_file(file_path);
80 }
81
82 pub fn rewind(&mut self, message_id: &str, option: RewindOption) -> RewindOperationResult {
84 if option == RewindOption::Nevermind {
85 return RewindOperationResult {
86 success: true,
87 option,
88 code_result: None,
89 conversation_result: None,
90 error: None,
91 };
92 }
93
94 let mut result = RewindOperationResult {
95 success: true,
96 option,
97 code_result: None,
98 conversation_result: None,
99 error: None,
100 };
101
102 if option == RewindOption::Code || option == RewindOption::Both {
104 let code_result = self.file_history.rewind_to_message(message_id, false);
105 if !code_result.success {
106 result.success = false;
107 result.error = code_result.error.clone();
108 }
109 result.code_result = Some(code_result);
110 }
111
112 if option == RewindOption::Conversation || option == RewindOption::Both {
114 result.conversation_result = Some(ConversationRewindResult {
115 messages_removed: 0,
116 new_message_count: self.message_count,
117 });
118 }
119
120 result
121 }
122
123 pub fn preview_rewind(&self, message_id: &str, option: RewindOption) -> RewindPreview {
125 let mut preview = RewindPreview::default();
126
127 if option == RewindOption::Code || option == RewindOption::Both {
128 let result = self.file_history.rewind_to_message(message_id, true);
129 preview.files_will_change = result.files_changed;
130 preview.insertions = result.insertions;
131 preview.deletions = result.deletions;
132 }
133
134 preview
135 }
136
137 pub fn get_rewindable_messages(&self) -> Vec<RewindableMessage> {
139 self.file_history
140 .get_snapshots()
141 .iter()
142 .enumerate()
143 .map(|(index, snapshot)| RewindableMessage {
144 uuid: snapshot.message_id.clone(),
145 index,
146 preview: format!("快照 #{}", index + 1),
147 timestamp: Some(snapshot.timestamp),
148 has_file_changes: !snapshot.tracked_file_backups.is_empty(),
149 })
150 .collect()
151 }
152
153 pub fn get_last_rewind_point(&self) -> Option<RewindableMessage> {
155 self.get_rewindable_messages().pop()
156 }
157
158 pub fn can_rewind(&self) -> bool {
160 self.file_history.get_snapshots_count() > 0
161 }
162
163 pub fn cleanup(&self) {
165 self.file_history.cleanup();
166 }
167}
168
169#[derive(Debug, Clone, Default, Serialize, Deserialize)]
171pub struct RewindPreview {
172 pub files_will_change: Vec<String>,
173 pub messages_will_remove: usize,
174 pub insertions: u32,
175 pub deletions: u32,
176}
177
178use once_cell::sync::Lazy;
181use std::collections::HashMap;
182use std::sync::{Arc, RwLock};
183
184static MANAGERS: Lazy<RwLock<HashMap<String, Arc<RwLock<RewindManager>>>>> =
186 Lazy::new(|| RwLock::new(HashMap::new()));
187
188pub fn get_rewind_manager(session_id: &str) -> Arc<RwLock<RewindManager>> {
190 let mut managers = MANAGERS.write().unwrap();
191
192 if let Some(manager) = managers.get(session_id) {
193 return Arc::clone(manager);
194 }
195
196 let manager = Arc::new(RwLock::new(RewindManager::new(session_id)));
197 managers.insert(session_id.to_string(), Arc::clone(&manager));
198 manager
199}
200
201pub fn cleanup_rewind_manager(session_id: &str) {
203 let mut managers = MANAGERS.write().unwrap();
204
205 if let Some(manager) = managers.remove(session_id) {
206 if let Ok(m) = manager.read() {
207 m.cleanup();
208 }
209 }
210}
211
212pub fn cleanup_all_rewind_managers() {
214 let mut managers = MANAGERS.write().unwrap();
215
216 for (_, manager) in managers.drain() {
217 if let Ok(m) = manager.read() {
218 m.cleanup();
219 }
220 }
221}
222
223impl RewindManager {
226 pub fn session_id(&self) -> &str {
228 self.file_history.session_id()
229 }
230
231 pub fn message_count(&self) -> usize {
233 self.message_count
234 }
235
236 pub fn tracked_files_count(&self) -> usize {
238 self.file_history.get_tracked_files_count()
239 }
240
241 pub fn snapshots_count(&self) -> usize {
243 self.file_history.get_snapshots_count()
244 }
245
246 pub fn has_snapshot(&self, message_id: &str) -> bool {
248 self.file_history.has_snapshot(message_id)
249 }
250
251 pub fn backup_size(&self) -> u64 {
253 self.file_history.get_backup_size()
254 }
255
256 pub fn record_file_changes(&mut self, file_paths: &[impl AsRef<std::path::Path>]) {
258 for path in file_paths {
259 self.record_file_change(path);
260 }
261 }
262
263 pub fn rewind_to_last(&mut self, option: RewindOption) -> RewindOperationResult {
265 match self.get_last_rewind_point() {
266 Some(msg) => self.rewind(&msg.uuid, option),
267 None => RewindOperationResult {
268 success: false,
269 option,
270 code_result: None,
271 conversation_result: None,
272 error: Some("没有可回退的快照".to_string()),
273 },
274 }
275 }
276
277 pub fn get_snapshot_details(&self, message_id: &str) -> Option<SnapshotDetails> {
279 let snapshot = self.file_history.get_snapshot(message_id)?;
280 Some(SnapshotDetails {
281 message_id: snapshot.message_id.clone(),
282 timestamp: snapshot.timestamp,
283 files_count: snapshot.tracked_file_backups.len(),
284 files: snapshot.tracked_file_backups.keys().cloned().collect(),
285 })
286 }
287}
288
289#[derive(Debug, Clone, Serialize, Deserialize)]
291pub struct SnapshotDetails {
292 pub message_id: String,
293 pub timestamp: i64,
294 pub files_count: usize,
295 pub files: Vec<String>,
296}
297
298#[cfg(test)]
301mod tests {
302 use super::*;
303 use std::fs;
304 use std::io::Write;
305 use tempfile::TempDir;
306
307 fn create_test_file(dir: &std::path::Path, name: &str, content: &str) -> std::path::PathBuf {
308 let path = dir.join(name);
309 let mut file = fs::File::create(&path).unwrap();
310 file.write_all(content.as_bytes()).unwrap();
311 path
312 }
313
314 #[test]
315 fn test_new_manager() {
316 let manager = RewindManager::new("test-session");
317 assert_eq!(manager.session_id(), "test-session");
318 assert_eq!(manager.message_count(), 0);
319 assert!(!manager.can_rewind());
320 manager.cleanup();
321 }
322
323 #[test]
324 fn test_record_user_message() {
325 let mut manager = RewindManager::new("test-msg");
326 manager.record_user_message("msg-1");
327 assert_eq!(manager.message_count(), 1);
328 assert_eq!(manager.snapshots_count(), 1);
329 assert!(manager.has_snapshot("msg-1"));
330 assert!(manager.can_rewind());
331 manager.cleanup();
332 }
333
334 #[test]
335 fn test_record_file_change() {
336 let temp_dir = TempDir::new().unwrap();
337 let test_file = create_test_file(temp_dir.path(), "test.txt", "content");
338
339 let mut manager = RewindManager::new("test-file");
340 manager.record_file_change(&test_file);
341 assert_eq!(manager.tracked_files_count(), 1);
342 manager.cleanup();
343 }
344
345 #[test]
346 fn test_rewindable_messages() {
347 let mut manager = RewindManager::new("test-rewindable");
348 manager.record_user_message("msg-1");
349 manager.record_user_message("msg-2");
350
351 let messages = manager.get_rewindable_messages();
352 assert_eq!(messages.len(), 2);
353 assert_eq!(messages[0].uuid, "msg-1");
354 assert_eq!(messages[1].uuid, "msg-2");
355
356 let last = manager.get_last_rewind_point();
357 assert!(last.is_some());
358 assert_eq!(last.unwrap().uuid, "msg-2");
359
360 manager.cleanup();
361 }
362
363 #[test]
364 fn test_rewind_nevermind() {
365 let mut manager = RewindManager::new("test-nevermind");
366 manager.record_user_message("msg-1");
367
368 let result = manager.rewind("msg-1", RewindOption::Nevermind);
369 assert!(result.success);
370 assert_eq!(result.option, RewindOption::Nevermind);
371 assert!(result.code_result.is_none());
372
373 manager.cleanup();
374 }
375
376 #[test]
377 fn test_rewind_code() {
378 let temp_dir = TempDir::new().unwrap();
379 let test_file = create_test_file(temp_dir.path(), "test.txt", "original");
380
381 let mut manager = RewindManager::new("test-rewind-code");
382 manager.record_file_change(&test_file);
383 manager.record_user_message("msg-1");
384
385 fs::write(&test_file, "modified").unwrap();
387
388 let result = manager.rewind("msg-1", RewindOption::Code);
390 assert!(result.success);
391 assert!(result.code_result.is_some());
392
393 let content = fs::read_to_string(&test_file).unwrap();
395 assert_eq!(content, "original");
396
397 manager.cleanup();
398 }
399
400 #[test]
401 fn test_preview_rewind() {
402 let temp_dir = TempDir::new().unwrap();
403 let test_file = create_test_file(temp_dir.path(), "test.txt", "line1\nline2\n");
404
405 let mut manager = RewindManager::new("test-preview");
406 manager.record_file_change(&test_file);
407 manager.record_user_message("msg-1");
408
409 fs::write(&test_file, "line1\nline2\nline3\n").unwrap();
411
412 let preview = manager.preview_rewind("msg-1", RewindOption::Code);
413 assert!(!preview.files_will_change.is_empty());
414
415 let content = fs::read_to_string(&test_file).unwrap();
417 assert_eq!(content, "line1\nline2\nline3\n");
418
419 manager.cleanup();
420 }
421
422 #[test]
423 fn test_rewind_to_last() {
424 let mut manager = RewindManager::new("test-last");
425
426 let result = manager.rewind_to_last(RewindOption::Code);
428 assert!(!result.success);
429
430 manager.record_user_message("msg-1");
432 let result = manager.rewind_to_last(RewindOption::Code);
433 assert!(result.success);
434
435 manager.cleanup();
436 }
437
438 #[test]
439 fn test_snapshot_details() {
440 let temp_dir = TempDir::new().unwrap();
441 let test_file = create_test_file(temp_dir.path(), "test.txt", "content");
442
443 let mut manager = RewindManager::new("test-details");
444 manager.record_file_change(&test_file);
445 manager.record_user_message("msg-1");
446
447 let details = manager.get_snapshot_details("msg-1");
448 assert!(details.is_some());
449 let details = details.unwrap();
450 assert_eq!(details.message_id, "msg-1");
451 assert_eq!(details.files_count, 1);
452
453 manager.cleanup();
454 }
455
456 #[test]
457 fn test_global_manager() {
458 let manager1 = get_rewind_manager("global-test");
459 let manager2 = get_rewind_manager("global-test");
460
461 assert!(Arc::ptr_eq(&manager1, &manager2));
463
464 cleanup_rewind_manager("global-test");
465 }
466
467 #[test]
468 fn test_batch_file_changes() {
469 let temp_dir = TempDir::new().unwrap();
470 let file1 = create_test_file(temp_dir.path(), "a.txt", "a");
471 let file2 = create_test_file(temp_dir.path(), "b.txt", "b");
472
473 let mut manager = RewindManager::new("test-batch");
474 manager.record_file_changes(&[&file1, &file2]);
475 assert_eq!(manager.tracked_files_count(), 2);
476
477 manager.cleanup();
478 }
479}