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::{
18 ensure_parent_dirs, hash_content, write_no_follow_if_unchanged,
19};
20use crate::types::{normalize_thread_id, UndoEdit};
21use crate::utils::path::{expand_user, validate_path_in_workspace};
22
23#[instrument(level = "info", skip(bash_state_arc, undo))]
24pub async fn handle_tool_call(
25 bash_state_arc: &Arc<Mutex<Option<BashState>>>,
26 undo: UndoEdit,
27) -> Result<String> {
28 let mut bash_state_guard = bash_state_arc.lock().await;
29 let bash_state = bash_state_guard.as_mut().ok_or(WinxError::BashStateNotInitialized)?;
30
31 let thread_id = normalize_thread_id(&undo.thread_id);
32 if thread_id != bash_state.current_thread_id {
33 return Err(WinxError::ThreadIdMismatch(thread_id));
34 }
35
36 let expanded = expand_user(&undo.file_path);
39 let path = if Path::new(&expanded).is_absolute() {
40 PathBuf::from(&expanded)
41 } else {
42 bash_state.cwd.join(&expanded)
43 };
44 let path = validate_path_in_workspace(&path, &bash_state.workspace_root)
45 .map_err(|e| WinxError::PathSecurityError { path: path.clone(), message: e.to_string() })?;
46 let file_path_str = path.to_string_lossy().to_string();
47
48 let on_disk = std::fs::read_to_string(&path).ok();
52 let wrote_hash =
53 bash_state.whitelist_for_overwrite.get(&file_path_str).map(|w| w.file_hash.clone());
54 let unchanged = matches!((&on_disk, &wrote_hash), (Some(c), Some(h)) if &hash_content(c) == h);
55 if !unchanged {
56 return Err(WinxError::FileChangedAfterEdit {
57 path,
58 message: format!(
59 "{file_path_str} changed since its last winx edit (or was deleted), so UndoEdit \
60 was refused to avoid discarding those changes. Re-read the file and edit it \
61 manually."
62 ),
63 });
64 }
65
66 let Some(checkpoint) = bash_state.latest_edit_checkpoint_for(&file_path_str) else {
67 return Err(WinxError::UndoCheckpointNotFound { path });
68 };
69
70 ensure_parent_dirs(&path)?;
74 write_no_follow_if_unchanged(&path, checkpoint.prior_content.as_bytes(), on_disk.as_deref())?;
75 let removed = bash_state.pop_edit_checkpoint_for(&file_path_str);
76 debug_assert!(removed.is_some(), "checkpoint disappeared while the session lock was held");
77 match checkpoint.prior_whitelist {
78 Some(whitelist) => {
79 bash_state.set_whitelist_entry(&file_path_str, whitelist);
80 }
81 None => {
82 bash_state.remove_whitelist_entry(&file_path_str);
83 }
84 }
85
86 let remaining =
87 bash_state.edit_checkpoints.iter().filter(|cp| cp.file_path_str == file_path_str).count();
88 let lines = checkpoint.prior_content.lines().count();
89 Ok(format!(
90 "Reverted {file_path_str} to its content before the last edit ({lines} lines). \
91 {remaining} earlier checkpoint(s) remain for this file."
92 ))
93}