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    /// Enhanced search/replace syntax error with detailed context
84    #[error("Search/replace syntax error: {message}")]
85    SearchReplaceSyntaxErrorDetailed {
86        message: String,
87        line_number: Option<usize>,
88        block_type: Option<String>,
89        suggestions: Vec<String>,
90    },
91
92    /// Error when JSON parsing fails
93    #[error("Invalid JSON: {0}")]
94    JsonParseError(String),
95
96    /// Error when a file is too large for operation
97    #[error("File {path} is too large: {size} bytes (max {max_size})")]
98    FileTooLarge { path: PathBuf, size: u64, max_size: u64 },
99
100    /// Error when writing to a file
101    #[error("Failed to write file {path}: {message}")]
102    FileWriteError { path: PathBuf, message: String },
103
104    /// Error loading data
105    #[error("Failed to load data: {0}")]
106    DataLoadingError(String),
107
108    /// Parameter validation error
109    #[error("Invalid parameter: {field} - {message}")]
110    ParameterValidationError { field: String, message: String },
111
112    /// Required parameter missing error
113    #[error("Required parameter missing: {field} - {message}")]
114    MissingParameterError { field: String, message: String },
115
116    /// Null or undefined value error
117    #[error("Null or undefined value where object expected: {field}")]
118    NullValueError { field: String },
119
120    /// Recovery suggestion error with potential solutions
121    #[error("{message} - {suggestion}")]
122    RecoverableSuggestionError { message: String, suggestion: String },
123
124    /// Context save error
125    #[error("Context save error: {0}")]
126    ContextSaveError(String),
127
128    /// Command timeout error
129    #[error("Command timed out after {timeout_seconds}s: {command}")]
130    CommandTimeout { command: String, timeout_seconds: u64 },
131
132    /// Interactive command detected error
133    #[error(
134        "Interactive command detected: {command}. Use appropriate flags or consider alternatives."
135    )]
136    InteractiveCommandDetected { command: String },
137
138    /// Command already running error
139    #[error("A command is already running: '{current_command}' (for {duration_seconds:.1}s). Use status_check, send_text, or interrupt.")]
140    CommandAlreadyRunning { current_command: String, duration_seconds: f64 },
141
142    /// Process cleanup error
143    #[error("Failed to cleanup process: {message}")]
144    ProcessCleanupError { message: String },
145
146    /// Buffer overflow error
147    #[error("Command output exceeded maximum size: {size} bytes (max {max_size})")]
148    BufferOverflow { size: usize, max_size: usize },
149
150    /// Session recovery error
151    #[error("Failed to recover bash session: {message}")]
152    SessionRecoveryError { message: String },
153
154    /// Resource allocation error
155    #[error("Resource allocation failed: {message}")]
156    ResourceAllocationError { message: String },
157
158    /// IO error
159    #[error("IO error: {0}")]
160    IoError(#[from] std::io::Error),
161
162    /// Configuration error
163    #[error("Configuration error: {0}")]
164    ConfigurationError(String),
165
166    /// Parse error for responses
167    #[error("Parse error: {0}")]
168    ParseError(String),
169
170    /// Invalid input error
171    #[error("Invalid input: {0}")]
172    InvalidInput(String),
173
174    /// File error for file operations
175    #[error("File error: {0}")]
176    FileError(String),
177}
178
179/// Type alias for Result with `WinxError`
180pub type Result<T> = std::result::Result<T, WinxError>;
181
182/// Conversion from `anyhow::Error` to `WinxError`
183impl From<anyhow::Error> for WinxError {
184    fn from(error: anyhow::Error) -> Self {
185        // `{:#}` renders the full anyhow context chain inline ("failed X: caused
186        // by Y: root Z") instead of just the outermost message, so a PTY-spawn or
187        // persistence failure keeps its real cause instead of a bare one-liner.
188        WinxError::CommandExecutionError(format!("{error:#}"))
189    }
190}
191
192/// Advanced error recovery and suggestion options
193pub struct ErrorRecovery;
194
195impl ErrorRecovery {
196    pub fn suggest(error: WinxError, _suggestion: &str) -> WinxError {
197        error
198    }
199
200    pub fn param_error(field: &str, message: &str) -> WinxError {
201        WinxError::ParameterValidationError {
202            field: field.to_string(),
203            message: message.to_string(),
204        }
205    }
206
207    pub fn missing_param(field: &str, message: &str) -> WinxError {
208        WinxError::MissingParameterError { field: field.to_string(), message: message.to_string() }
209    }
210
211    pub fn null_value(field: &str) -> WinxError {
212        WinxError::NullValueError { field: field.to_string() }
213    }
214}
215
216/// Enable cloning for `WinxError`
217impl Clone for WinxError {
218    fn clone(&self) -> Self {
219        match self {
220            Self::ShellInitializationError(msg) => Self::ShellInitializationError(msg.clone()),
221            Self::WorkspacePathError(msg) => Self::WorkspacePathError(msg.clone()),
222            Self::BashStateLockError(msg) => Self::BashStateLockError(msg.clone()),
223            Self::BashStateNotInitialized => Self::BashStateNotInitialized,
224            Self::CommandExecutionError(msg) => Self::CommandExecutionError(msg.clone()),
225            Self::NoActiveCommand(msg) => Self::NoActiveCommand(msg.clone()),
226            Self::BackgroundSessionNotFound(msg) => Self::BackgroundSessionNotFound(msg.clone()),
227            Self::EmptyInteractiveInput { action } => {
228                Self::EmptyInteractiveInput { action: action.clone() }
229            }
230            Self::InteractiveTargetNotRunning(msg) => {
231                Self::InteractiveTargetNotRunning(msg.clone())
232            }
233            Self::CommandNotAllowed(msg) => Self::CommandNotAllowed(msg.clone()),
234            Self::ThreadIdMismatch(msg) => Self::ThreadIdMismatch(msg.clone()),
235            Self::ArgumentParseError(msg) => Self::ArgumentParseError(msg.clone()),
236            Self::FileAccessError { path, message } => {
237                Self::FileAccessError { path: path.clone(), message: message.clone() }
238            }
239            Self::DeserializationError(msg) => Self::DeserializationError(msg.clone()),
240            Self::SerializationError(msg) => Self::SerializationError(msg.clone()),
241            Self::SearchReplaceSyntaxError(msg) => Self::SearchReplaceSyntaxError(msg.clone()),
242            Self::SearchBlockNotFound(msg) => Self::SearchBlockNotFound(msg.clone()),
243            Self::SearchBlockAmbiguous { block_content, match_count, suggestions } => {
244                Self::SearchBlockAmbiguous {
245                    block_content: block_content.clone(),
246                    match_count: *match_count,
247                    suggestions: suggestions.clone(),
248                }
249            }
250            Self::SearchReplaceSyntaxErrorDetailed {
251                message,
252                line_number,
253                block_type,
254                suggestions,
255            } => Self::SearchReplaceSyntaxErrorDetailed {
256                message: message.clone(),
257                line_number: *line_number,
258                block_type: block_type.clone(),
259                suggestions: suggestions.clone(),
260            },
261            Self::JsonParseError(msg) => Self::JsonParseError(msg.clone()),
262            Self::FileTooLarge { path, size, max_size } => {
263                Self::FileTooLarge { path: path.clone(), size: *size, max_size: *max_size }
264            }
265            Self::FileWriteError { path, message } => {
266                Self::FileWriteError { path: path.clone(), message: message.clone() }
267            }
268            Self::DataLoadingError(msg) => Self::DataLoadingError(msg.clone()),
269            Self::ParameterValidationError { field, message } => {
270                Self::ParameterValidationError { field: field.clone(), message: message.clone() }
271            }
272            Self::MissingParameterError { field, message } => {
273                Self::MissingParameterError { field: field.clone(), message: message.clone() }
274            }
275            Self::NullValueError { field } => Self::NullValueError { field: field.clone() },
276            Self::RecoverableSuggestionError { message, suggestion } => {
277                Self::RecoverableSuggestionError {
278                    message: message.clone(),
279                    suggestion: suggestion.clone(),
280                }
281            }
282            Self::ContextSaveError(msg) => Self::ContextSaveError(msg.clone()),
283            Self::CommandTimeout { command, timeout_seconds } => {
284                Self::CommandTimeout { command: command.clone(), timeout_seconds: *timeout_seconds }
285            }
286            Self::InteractiveCommandDetected { command } => {
287                Self::InteractiveCommandDetected { command: command.clone() }
288            }
289            Self::CommandAlreadyRunning { current_command, duration_seconds } => {
290                Self::CommandAlreadyRunning {
291                    current_command: current_command.clone(),
292                    duration_seconds: *duration_seconds,
293                }
294            }
295            Self::ProcessCleanupError { message } => {
296                Self::ProcessCleanupError { message: message.clone() }
297            }
298            Self::BufferOverflow { size, max_size } => {
299                Self::BufferOverflow { size: *size, max_size: *max_size }
300            }
301            Self::SessionRecoveryError { message } => {
302                Self::SessionRecoveryError { message: message.clone() }
303            }
304            Self::ResourceAllocationError { message } => {
305                Self::ResourceAllocationError { message: message.clone() }
306            }
307            Self::IoError(err) => Self::IoError(std::io::Error::new(err.kind(), err.to_string())),
308            Self::ConfigurationError(msg) => Self::ConfigurationError(msg.clone()),
309            Self::ParseError(msg) => Self::ParseError(msg.clone()),
310            Self::InvalidInput(msg) => Self::InvalidInput(msg.clone()),
311            Self::FileError(msg) => Self::FileError(msg.clone()),
312            Self::PathSecurityError { path, message } => {
313                Self::PathSecurityError { path: path.clone(), message: message.clone() }
314            }
315        }
316    }
317}