Skip to main content

winx_code_agent/
errors.rs

1use std::path::PathBuf;
2use thiserror::Error;
3
4/// Errors that can occur in the Winx application
5#[derive(Error, Debug)]
6pub enum WinxError {
7    /// Error when initializing the shell
8    #[error("Failed to initialize shell: {0}")]
9    ShellInitializationError(String),
10
11    /// Error when operating on a workspace path
12    #[error("Workspace path error: {0}")]
13    WorkspacePathError(String),
14
15    /// Error when locking the bash state
16    #[error("Failed to lock the bash state: {0}")]
17    BashStateLockError(String),
18
19    /// Error when the bash state is not initialized
20    #[error("Bash state not initialized. Please call Initialize first with type=\"first_call\" and a valid workspace path.")]
21    BashStateNotInitialized,
22
23    /// Error when a command fails for an internal execution reason.
24    #[error("Command execution failed: {0}")]
25    CommandExecutionError(String),
26
27    /// A status/input action targeted a shell with no running command.
28    #[error("{0}")]
29    NoActiveCommand(String),
30
31    /// A background command identifier is unknown or has already exited.
32    #[error("{0}")]
33    BackgroundSessionNotFound(String),
34
35    /// An interactive input action was supplied without any payload.
36    #[error("Failure: {action} cannot be empty")]
37    EmptyInteractiveInput { action: String },
38
39    /// Interactive bytes were sent to an idle shell instead of a running program.
40    #[error("{0}")]
41    InteractiveTargetNotRunning(String),
42
43    /// Error when parsing arguments
44    #[error("Failed to parse arguments: {0}")]
45    ArgumentParseError(String),
46
47    /// Error when trying to access a file or directory
48    #[error("File access error for {path}: {message}")]
49    FileAccessError { path: PathBuf, message: String },
50
51    /// Security error - path traversal or symlink escape attempt
52    #[error("Security violation: {message}")]
53    PathSecurityError { path: PathBuf, message: String },
54
55    /// Error when a command is not allowed in the current mode
56    #[error("Command not allowed: {0}")]
57    CommandNotAllowed(String),
58
59    /// Error when chat IDs don't match
60    #[error("Thread ID mismatch: {0}")]
61    ThreadIdMismatch(String),
62
63    /// Error when deserializing data
64    #[error("Deserialization error: {0}")]
65    DeserializationError(String),
66
67    /// Error when serializing data
68    #[error("Serialization error: {0}")]
69    SerializationError(String),
70
71    /// Error in the search/replace format
72    #[error("Search/replace syntax error: {0}")]
73    SearchReplaceSyntaxError(String),
74
75    /// Error when search block is not found in content
76    #[error("Search block not found in content: {0}")]
77    SearchBlockNotFound(String),
78
79    /// Error when search block matches multiple locations (WCGW-style)
80    #[error("Search block matched multiple times")]
81    SearchBlockAmbiguous { block_content: String, match_count: usize, suggestions: Vec<String> },
82
83    /// Path-aware context for a planning failure in an atomic multi-file edit.
84    /// The boxed source preserves the original recovery class instead of
85    /// flattening it into an argument/string error.
86    #[error(
87        "MultiFileEdit aborted before writing anything - file {index} ({path}) failed validation: {source}"
88    )]
89    MultiFilePlanError { index: usize, path: PathBuf, source: Box<WinxError> },
90
91    /// Enhanced search/replace syntax error with detailed context
92    #[error("Search/replace syntax error: {message}")]
93    SearchReplaceSyntaxErrorDetailed {
94        message: String,
95        line_number: Option<usize>,
96        block_type: Option<String>,
97        suggestions: Vec<String>,
98    },
99
100    /// Error when JSON parsing fails
101    #[error("Invalid JSON: {0}")]
102    JsonParseError(String),
103
104    /// Error when a file is too large for operation
105    #[error("File {path} is too large: {size} bytes (max {max_size})")]
106    FileTooLarge { path: PathBuf, size: u64, max_size: u64 },
107
108    /// Error when writing to a file
109    #[error("Failed to write file {path}: {message}")]
110    FileWriteError { path: PathBuf, message: String },
111
112    /// Error loading data
113    #[error("Failed to load data: {0}")]
114    DataLoadingError(String),
115
116    /// Parameter validation error
117    #[error("Invalid parameter: {field} - {message}")]
118    ParameterValidationError { field: String, message: String },
119
120    /// Required parameter missing error
121    #[error("Required parameter missing: {field} - {message}")]
122    MissingParameterError { field: String, message: String },
123
124    /// Null or undefined value error
125    #[error("Null or undefined value where object expected: {field}")]
126    NullValueError { field: String },
127
128    /// Recovery suggestion error with potential solutions
129    #[error("{message} - {suggestion}")]
130    RecoverableSuggestionError { message: String, suggestion: String },
131
132    /// Context save error
133    #[error("Context save error: {0}")]
134    ContextSaveError(String),
135
136    /// Command timeout error
137    #[error("Command timed out after {timeout_seconds}s: {command}")]
138    CommandTimeout { command: String, timeout_seconds: u64 },
139
140    /// Interactive command detected error
141    #[error(
142        "Interactive command detected: {command}. Use appropriate flags or consider alternatives."
143    )]
144    InteractiveCommandDetected { command: String },
145
146    /// Command already running error
147    #[error("A command is already running: '{current_command}' (for {duration_seconds:.1}s). Use status_check, send_text, or interrupt.")]
148    CommandAlreadyRunning { current_command: String, duration_seconds: f64 },
149
150    /// Process cleanup error
151    #[error("Failed to cleanup process: {message}")]
152    ProcessCleanupError { message: String },
153
154    /// Buffer overflow error
155    #[error("Command output exceeded maximum size: {size} bytes (max {max_size})")]
156    BufferOverflow { size: usize, max_size: usize },
157
158    /// Session recovery error
159    #[error("Failed to recover bash session: {message}")]
160    SessionRecoveryError { message: String },
161
162    /// Resource allocation error
163    #[error("Resource allocation failed: {message}")]
164    ResourceAllocationError { message: String },
165
166    /// IO error
167    #[error("IO error: {0}")]
168    IoError(#[from] std::io::Error),
169
170    /// Configuration error
171    #[error("Configuration error: {0}")]
172    ConfigurationError(String),
173
174    /// Parse error for responses
175    #[error("Parse error: {0}")]
176    ParseError(String),
177
178    /// Invalid input error
179    #[error("Invalid input: {0}")]
180    InvalidInput(String),
181
182    /// File error for file operations
183    #[error("File error: {0}")]
184    FileError(String),
185}
186
187/// Type alias for Result with `WinxError`
188pub type Result<T> = std::result::Result<T, WinxError>;
189
190/// Conversion from `anyhow::Error` to `WinxError`
191impl From<anyhow::Error> for WinxError {
192    fn from(error: anyhow::Error) -> Self {
193        // `{:#}` renders the full anyhow context chain inline ("failed X: caused
194        // by Y: root Z") instead of just the outermost message, so a PTY-spawn or
195        // persistence failure keeps its real cause instead of a bare one-liner.
196        WinxError::CommandExecutionError(format!("{error:#}"))
197    }
198}
199
200/// Advanced error recovery and suggestion options
201pub struct ErrorRecovery;
202
203impl ErrorRecovery {
204    pub fn suggest(error: WinxError, _suggestion: &str) -> WinxError {
205        error
206    }
207
208    pub fn param_error(field: &str, message: &str) -> WinxError {
209        WinxError::ParameterValidationError {
210            field: field.to_string(),
211            message: message.to_string(),
212        }
213    }
214
215    pub fn missing_param(field: &str, message: &str) -> WinxError {
216        WinxError::MissingParameterError { field: field.to_string(), message: message.to_string() }
217    }
218
219    pub fn null_value(field: &str) -> WinxError {
220        WinxError::NullValueError { field: field.to_string() }
221    }
222}
223
224/// Enable cloning for `WinxError`
225impl Clone for WinxError {
226    #[allow(clippy::too_many_lines)] // exhaustive variant-preserving clone
227    fn clone(&self) -> Self {
228        match self {
229            Self::ShellInitializationError(msg) => Self::ShellInitializationError(msg.clone()),
230            Self::WorkspacePathError(msg) => Self::WorkspacePathError(msg.clone()),
231            Self::BashStateLockError(msg) => Self::BashStateLockError(msg.clone()),
232            Self::BashStateNotInitialized => Self::BashStateNotInitialized,
233            Self::CommandExecutionError(msg) => Self::CommandExecutionError(msg.clone()),
234            Self::NoActiveCommand(msg) => Self::NoActiveCommand(msg.clone()),
235            Self::BackgroundSessionNotFound(msg) => Self::BackgroundSessionNotFound(msg.clone()),
236            Self::EmptyInteractiveInput { action } => {
237                Self::EmptyInteractiveInput { action: action.clone() }
238            }
239            Self::InteractiveTargetNotRunning(msg) => {
240                Self::InteractiveTargetNotRunning(msg.clone())
241            }
242            Self::CommandNotAllowed(msg) => Self::CommandNotAllowed(msg.clone()),
243            Self::ThreadIdMismatch(msg) => Self::ThreadIdMismatch(msg.clone()),
244            Self::ArgumentParseError(msg) => Self::ArgumentParseError(msg.clone()),
245            Self::FileAccessError { path, message } => {
246                Self::FileAccessError { path: path.clone(), message: message.clone() }
247            }
248            Self::DeserializationError(msg) => Self::DeserializationError(msg.clone()),
249            Self::SerializationError(msg) => Self::SerializationError(msg.clone()),
250            Self::SearchReplaceSyntaxError(msg) => Self::SearchReplaceSyntaxError(msg.clone()),
251            Self::SearchBlockNotFound(msg) => Self::SearchBlockNotFound(msg.clone()),
252            Self::SearchBlockAmbiguous { block_content, match_count, suggestions } => {
253                Self::SearchBlockAmbiguous {
254                    block_content: block_content.clone(),
255                    match_count: *match_count,
256                    suggestions: suggestions.clone(),
257                }
258            }
259            Self::MultiFilePlanError { index, path, source } => Self::MultiFilePlanError {
260                index: *index,
261                path: path.clone(),
262                source: Box::new((**source).clone()),
263            },
264            Self::SearchReplaceSyntaxErrorDetailed {
265                message,
266                line_number,
267                block_type,
268                suggestions,
269            } => Self::SearchReplaceSyntaxErrorDetailed {
270                message: message.clone(),
271                line_number: *line_number,
272                block_type: block_type.clone(),
273                suggestions: suggestions.clone(),
274            },
275            Self::JsonParseError(msg) => Self::JsonParseError(msg.clone()),
276            Self::FileTooLarge { path, size, max_size } => {
277                Self::FileTooLarge { path: path.clone(), size: *size, max_size: *max_size }
278            }
279            Self::FileWriteError { path, message } => {
280                Self::FileWriteError { path: path.clone(), message: message.clone() }
281            }
282            Self::DataLoadingError(msg) => Self::DataLoadingError(msg.clone()),
283            Self::ParameterValidationError { field, message } => {
284                Self::ParameterValidationError { field: field.clone(), message: message.clone() }
285            }
286            Self::MissingParameterError { field, message } => {
287                Self::MissingParameterError { field: field.clone(), message: message.clone() }
288            }
289            Self::NullValueError { field } => Self::NullValueError { field: field.clone() },
290            Self::RecoverableSuggestionError { message, suggestion } => {
291                Self::RecoverableSuggestionError {
292                    message: message.clone(),
293                    suggestion: suggestion.clone(),
294                }
295            }
296            Self::ContextSaveError(msg) => Self::ContextSaveError(msg.clone()),
297            Self::CommandTimeout { command, timeout_seconds } => {
298                Self::CommandTimeout { command: command.clone(), timeout_seconds: *timeout_seconds }
299            }
300            Self::InteractiveCommandDetected { command } => {
301                Self::InteractiveCommandDetected { command: command.clone() }
302            }
303            Self::CommandAlreadyRunning { current_command, duration_seconds } => {
304                Self::CommandAlreadyRunning {
305                    current_command: current_command.clone(),
306                    duration_seconds: *duration_seconds,
307                }
308            }
309            Self::ProcessCleanupError { message } => {
310                Self::ProcessCleanupError { message: message.clone() }
311            }
312            Self::BufferOverflow { size, max_size } => {
313                Self::BufferOverflow { size: *size, max_size: *max_size }
314            }
315            Self::SessionRecoveryError { message } => {
316                Self::SessionRecoveryError { message: message.clone() }
317            }
318            Self::ResourceAllocationError { message } => {
319                Self::ResourceAllocationError { message: message.clone() }
320            }
321            Self::IoError(err) => Self::IoError(std::io::Error::new(err.kind(), err.to_string())),
322            Self::ConfigurationError(msg) => Self::ConfigurationError(msg.clone()),
323            Self::ParseError(msg) => Self::ParseError(msg.clone()),
324            Self::InvalidInput(msg) => Self::InvalidInput(msg.clone()),
325            Self::FileError(msg) => Self::FileError(msg.clone()),
326            Self::PathSecurityError { path, message } => {
327                Self::PathSecurityError { path: path.clone(), message: message.clone() }
328            }
329        }
330    }
331}