winx_code_agent/tools/
undo_edit.rs1use std::path::{Path, PathBuf};
10use std::sync::Arc;
11
12use tokio::sync::Mutex;
13use tracing::instrument;
14
15use crate::errors::{Result, WinxError};
16use crate::state::bash_state::BashState;
17use crate::tools::file_write_or_edit::{ensure_parent_dirs, hash_content, write_no_follow};
18use crate::types::{normalize_thread_id, UndoEdit};
19use crate::utils::path::{expand_user, validate_path_in_workspace};
20
21#[instrument(level = "info", skip(bash_state_arc, undo))]
22pub async fn handle_tool_call(
23 bash_state_arc: &Arc<Mutex<Option<BashState>>>,
24 undo: UndoEdit,
25) -> Result<String> {
26 let mut bash_state_guard = bash_state_arc.lock().await;
27 let bash_state = bash_state_guard.as_mut().ok_or(WinxError::BashStateNotInitialized)?;
28
29 let thread_id = normalize_thread_id(&undo.thread_id);
30 if thread_id != bash_state.current_thread_id {
31 return Err(WinxError::ThreadIdMismatch(thread_id));
32 }
33
34 let expanded = expand_user(&undo.file_path);
37 let path = if Path::new(&expanded).is_absolute() {
38 PathBuf::from(&expanded)
39 } else {
40 bash_state.cwd.join(&expanded)
41 };
42 let path = validate_path_in_workspace(&path, &bash_state.workspace_root)
43 .map_err(|e| WinxError::PathSecurityError { path: path.clone(), message: e.to_string() })?;
44 let file_path_str = path.to_string_lossy().to_string();
45
46 let on_disk = std::fs::read_to_string(&path).ok();
50 let wrote_hash =
51 bash_state.whitelist_for_overwrite.get(&file_path_str).map(|w| w.file_hash.clone());
52 let unchanged = matches!((&on_disk, &wrote_hash), (Some(c), Some(h)) if &hash_content(c) == h);
53 if !unchanged {
54 return Err(WinxError::FileAccessError {
55 path,
56 message: format!(
57 "{file_path_str} changed since its last winx edit (or was deleted), so UndoEdit \
58 was refused to avoid discarding those changes. Re-read the file and edit it \
59 manually."
60 ),
61 });
62 }
63
64 let Some(checkpoint) = bash_state.pop_edit_checkpoint_for(&file_path_str) else {
65 return Err(WinxError::FileAccessError {
66 path,
67 message: format!(
68 "No undo checkpoint for {file_path_str} in this session. winx keeps the last few \
69 edits per session in memory; a brand-new file's creation is not undoable."
70 ),
71 });
72 };
73
74 ensure_parent_dirs(&path)?;
78 write_no_follow(&path, checkpoint.prior_content.as_bytes())?;
79 match checkpoint.prior_whitelist {
80 Some(whitelist) => {
81 bash_state.whitelist_for_overwrite.insert(file_path_str.clone(), whitelist);
82 }
83 None => {
84 bash_state.whitelist_for_overwrite.remove(&file_path_str);
85 }
86 }
87
88 let remaining =
89 bash_state.edit_checkpoints.iter().filter(|cp| cp.file_path_str == file_path_str).count();
90 let lines = checkpoint.prior_content.lines().count();
91 Ok(format!(
92 "Reverted {file_path_str} to its content before the last edit ({lines} lines). \
93 {remaining} earlier checkpoint(s) remain for this file."
94 ))
95}