1use std::path::PathBuf;
2use thiserror::Error;
3
4#[derive(Error, Debug)]
6pub enum WinxError {
7 #[error("Failed to initialize shell: {0}")]
9 ShellInitializationError(String),
10
11 #[error("Workspace path error: {0}")]
13 WorkspacePathError(String),
14
15 #[error("Failed to lock the bash state: {0}")]
17 BashStateLockError(String),
18
19 #[error("Bash state not initialized. Please call Initialize first with type=\"first_call\" and a valid workspace path.")]
21 BashStateNotInitialized,
22
23 #[error("Command execution failed: {0}")]
25 CommandExecutionError(String),
26
27 #[error("{0}")]
29 NoActiveCommand(String),
30
31 #[error("{0}")]
33 BackgroundSessionNotFound(String),
34
35 #[error("Failure: {action} cannot be empty")]
37 EmptyInteractiveInput { action: String },
38
39 #[error("{0}")]
41 InteractiveTargetNotRunning(String),
42
43 #[error("Failed to parse arguments: {0}")]
45 ArgumentParseError(String),
46
47 #[error("File access error for {path}: {message}")]
49 FileAccessError { path: PathBuf, message: String },
50
51 #[error("Security violation: {message}")]
53 PathSecurityError { path: PathBuf, message: String },
54
55 #[error("Temporary artifact policy rejected {path}: {message}")]
57 TemporaryArtifactPolicy { path: PathBuf, temporary_artifact_dir: PathBuf, message: String },
58
59 #[error("Derived CodeMap budget rejected {path}: {message}")]
61 DerivedCodeMapBudget {
62 path: PathBuf,
63 temporary_artifact_dir: PathBuf,
64 calls_used: usize,
65 calls_limit: usize,
66 unique_files_used: usize,
67 unique_files_limit: usize,
68 message: String,
69 },
70
71 #[error("Command not allowed: {0}")]
73 CommandNotAllowed(String),
74
75 #[error("Thread ID mismatch: {0}")]
77 ThreadIdMismatch(String),
78
79 #[error(
82 "Remote session binding is incomplete for thread_id `{thread_id}`: pass the exact workspace_root returned by Initialize. No operation was executed. workspace_root identifies the project session; it does not restrict target paths."
83 )]
84 WorkspaceBindingRequired { thread_id: String },
85
86 #[error(
89 "Session coherence check failed: thread_id `{thread_id}` does not belong to workspace_root `{workspace_root}`. No operation was executed. Initialize that workspace and preserve the returned thread_id/workspace_root pair."
90 )]
91 WorkspaceThreadMismatch { thread_id: String, workspace_root: PathBuf },
92
93 #[error(
96 "Session coherence check failed: thread_id `{thread_id}` is bound to `{bound_workspace}`, not `{requested_workspace}`. No operation was executed. Initialize the intended workspace and use its returned pair."
97 )]
98 WorkspaceBindingMismatch {
99 thread_id: String,
100 requested_workspace: PathBuf,
101 bound_workspace: PathBuf,
102 },
103
104 #[error(
107 "Remote workspace changes require a new session binding. No operation was executed. Call Initialize with type=\"first_call\" for `{workspace_root}` and use the new thread_id/workspace_root pair."
108 )]
109 WorkspaceChangeRequiresNewSession { workspace_root: PathBuf },
110
111 #[error("Deserialization error: {0}")]
113 DeserializationError(String),
114
115 #[error("Serialization error: {0}")]
117 SerializationError(String),
118
119 #[error("Search/replace syntax error: {0}")]
121 SearchReplaceSyntaxError(String),
122
123 #[error("Search block not found in content: {0}")]
125 SearchBlockNotFound(String),
126
127 #[error("Search block matched multiple times")]
129 SearchBlockAmbiguous { block_content: String, match_count: usize, suggestions: Vec<String> },
130
131 #[error(
135 "MultiFileEdit aborted before writing anything - file {index} ({path}) failed validation: {source}"
136 )]
137 MultiFilePlanError { index: usize, path: PathBuf, source: Box<WinxError> },
138
139 #[error("Search/replace syntax error: {message}")]
141 SearchReplaceSyntaxErrorDetailed {
142 message: String,
143 line_number: Option<usize>,
144 block_type: Option<String>,
145 suggestions: Vec<String>,
146 },
147
148 #[error("Invalid JSON: {0}")]
150 JsonParseError(String),
151
152 #[error("File {path} is too large: {size} bytes (max {max_size})")]
154 FileTooLarge { path: PathBuf, size: u64, max_size: u64 },
155
156 #[error("Failed to write file {path}: {message}")]
158 FileWriteError { path: PathBuf, message: String },
159
160 #[error("Failed to load data: {0}")]
162 DataLoadingError(String),
163
164 #[error("Invalid parameter: {field} - {message}")]
166 ParameterValidationError { field: String, message: String },
167
168 #[error("Required parameter missing: {field} - {message}")]
170 MissingParameterError { field: String, message: String },
171
172 #[error("Null or undefined value where object expected: {field}")]
174 NullValueError { field: String },
175
176 #[error("{message} - {suggestion}")]
178 RecoverableSuggestionError { message: String, suggestion: String },
179
180 #[error("Context save error: {0}")]
182 ContextSaveError(String),
183
184 #[error("Command timed out after {timeout_seconds}s: {command}")]
186 CommandTimeout { command: String, timeout_seconds: u64 },
187
188 #[error(
190 "Interactive command detected: {command}. Use appropriate flags or consider alternatives."
191 )]
192 InteractiveCommandDetected { command: String },
193
194 #[error("A command is already running: '{current_command}' (for {duration_seconds:.1}s). Use status_check, send_text, or interrupt.")]
196 CommandAlreadyRunning { current_command: String, duration_seconds: f64 },
197
198 #[error("Failed to cleanup process: {message}")]
200 ProcessCleanupError { message: String },
201
202 #[error("Command output exceeded maximum size: {size} bytes (max {max_size})")]
204 BufferOverflow { size: usize, max_size: usize },
205
206 #[error("Failed to recover bash session: {message}")]
208 SessionRecoveryError { message: String },
209
210 #[error("Resource allocation failed: {message}")]
212 ResourceAllocationError { message: String },
213
214 #[error("IO error: {0}")]
216 IoError(#[from] std::io::Error),
217
218 #[error("Configuration error: {0}")]
220 ConfigurationError(String),
221
222 #[error("Parse error: {0}")]
224 ParseError(String),
225
226 #[error("Invalid input: {0}")]
228 InvalidInput(String),
229
230 #[error("File error: {0}")]
232 FileError(String),
233}
234
235pub type Result<T> = std::result::Result<T, WinxError>;
237
238impl From<anyhow::Error> for WinxError {
240 fn from(error: anyhow::Error) -> Self {
241 WinxError::CommandExecutionError(format!("{error:#}"))
245 }
246}
247
248pub struct ErrorRecovery;
250
251impl ErrorRecovery {
252 pub fn suggest(error: WinxError, _suggestion: &str) -> WinxError {
253 error
254 }
255
256 pub fn param_error(field: &str, message: &str) -> WinxError {
257 WinxError::ParameterValidationError {
258 field: field.to_string(),
259 message: message.to_string(),
260 }
261 }
262
263 pub fn missing_param(field: &str, message: &str) -> WinxError {
264 WinxError::MissingParameterError { field: field.to_string(), message: message.to_string() }
265 }
266
267 pub fn null_value(field: &str) -> WinxError {
268 WinxError::NullValueError { field: field.to_string() }
269 }
270}
271
272impl Clone for WinxError {
274 #[allow(clippy::too_many_lines)] fn clone(&self) -> Self {
276 match self {
277 Self::ShellInitializationError(msg) => Self::ShellInitializationError(msg.clone()),
278 Self::WorkspacePathError(msg) => Self::WorkspacePathError(msg.clone()),
279 Self::BashStateLockError(msg) => Self::BashStateLockError(msg.clone()),
280 Self::BashStateNotInitialized => Self::BashStateNotInitialized,
281 Self::CommandExecutionError(msg) => Self::CommandExecutionError(msg.clone()),
282 Self::NoActiveCommand(msg) => Self::NoActiveCommand(msg.clone()),
283 Self::BackgroundSessionNotFound(msg) => Self::BackgroundSessionNotFound(msg.clone()),
284 Self::EmptyInteractiveInput { action } => {
285 Self::EmptyInteractiveInput { action: action.clone() }
286 }
287 Self::InteractiveTargetNotRunning(msg) => {
288 Self::InteractiveTargetNotRunning(msg.clone())
289 }
290 Self::CommandNotAllowed(msg) => Self::CommandNotAllowed(msg.clone()),
291 Self::ThreadIdMismatch(msg) => Self::ThreadIdMismatch(msg.clone()),
292 Self::WorkspaceBindingRequired { thread_id } => {
293 Self::WorkspaceBindingRequired { thread_id: thread_id.clone() }
294 }
295 Self::WorkspaceThreadMismatch { thread_id, workspace_root } => {
296 Self::WorkspaceThreadMismatch {
297 thread_id: thread_id.clone(),
298 workspace_root: workspace_root.clone(),
299 }
300 }
301 Self::WorkspaceBindingMismatch { thread_id, requested_workspace, bound_workspace } => {
302 Self::WorkspaceBindingMismatch {
303 thread_id: thread_id.clone(),
304 requested_workspace: requested_workspace.clone(),
305 bound_workspace: bound_workspace.clone(),
306 }
307 }
308 Self::WorkspaceChangeRequiresNewSession { workspace_root } => {
309 Self::WorkspaceChangeRequiresNewSession { workspace_root: workspace_root.clone() }
310 }
311 Self::ArgumentParseError(msg) => Self::ArgumentParseError(msg.clone()),
312 Self::FileAccessError { path, message } => {
313 Self::FileAccessError { path: path.clone(), message: message.clone() }
314 }
315 Self::TemporaryArtifactPolicy { path, temporary_artifact_dir, message } => {
316 Self::TemporaryArtifactPolicy {
317 path: path.clone(),
318 temporary_artifact_dir: temporary_artifact_dir.clone(),
319 message: message.clone(),
320 }
321 }
322 Self::DerivedCodeMapBudget {
323 path,
324 temporary_artifact_dir,
325 calls_used,
326 calls_limit,
327 unique_files_used,
328 unique_files_limit,
329 message,
330 } => Self::DerivedCodeMapBudget {
331 path: path.clone(),
332 temporary_artifact_dir: temporary_artifact_dir.clone(),
333 calls_used: *calls_used,
334 calls_limit: *calls_limit,
335 unique_files_used: *unique_files_used,
336 unique_files_limit: *unique_files_limit,
337 message: message.clone(),
338 },
339 Self::DeserializationError(msg) => Self::DeserializationError(msg.clone()),
340 Self::SerializationError(msg) => Self::SerializationError(msg.clone()),
341 Self::SearchReplaceSyntaxError(msg) => Self::SearchReplaceSyntaxError(msg.clone()),
342 Self::SearchBlockNotFound(msg) => Self::SearchBlockNotFound(msg.clone()),
343 Self::SearchBlockAmbiguous { block_content, match_count, suggestions } => {
344 Self::SearchBlockAmbiguous {
345 block_content: block_content.clone(),
346 match_count: *match_count,
347 suggestions: suggestions.clone(),
348 }
349 }
350 Self::MultiFilePlanError { index, path, source } => Self::MultiFilePlanError {
351 index: *index,
352 path: path.clone(),
353 source: Box::new((**source).clone()),
354 },
355 Self::SearchReplaceSyntaxErrorDetailed {
356 message,
357 line_number,
358 block_type,
359 suggestions,
360 } => Self::SearchReplaceSyntaxErrorDetailed {
361 message: message.clone(),
362 line_number: *line_number,
363 block_type: block_type.clone(),
364 suggestions: suggestions.clone(),
365 },
366 Self::JsonParseError(msg) => Self::JsonParseError(msg.clone()),
367 Self::FileTooLarge { path, size, max_size } => {
368 Self::FileTooLarge { path: path.clone(), size: *size, max_size: *max_size }
369 }
370 Self::FileWriteError { path, message } => {
371 Self::FileWriteError { path: path.clone(), message: message.clone() }
372 }
373 Self::DataLoadingError(msg) => Self::DataLoadingError(msg.clone()),
374 Self::ParameterValidationError { field, message } => {
375 Self::ParameterValidationError { field: field.clone(), message: message.clone() }
376 }
377 Self::MissingParameterError { field, message } => {
378 Self::MissingParameterError { field: field.clone(), message: message.clone() }
379 }
380 Self::NullValueError { field } => Self::NullValueError { field: field.clone() },
381 Self::RecoverableSuggestionError { message, suggestion } => {
382 Self::RecoverableSuggestionError {
383 message: message.clone(),
384 suggestion: suggestion.clone(),
385 }
386 }
387 Self::ContextSaveError(msg) => Self::ContextSaveError(msg.clone()),
388 Self::CommandTimeout { command, timeout_seconds } => {
389 Self::CommandTimeout { command: command.clone(), timeout_seconds: *timeout_seconds }
390 }
391 Self::InteractiveCommandDetected { command } => {
392 Self::InteractiveCommandDetected { command: command.clone() }
393 }
394 Self::CommandAlreadyRunning { current_command, duration_seconds } => {
395 Self::CommandAlreadyRunning {
396 current_command: current_command.clone(),
397 duration_seconds: *duration_seconds,
398 }
399 }
400 Self::ProcessCleanupError { message } => {
401 Self::ProcessCleanupError { message: message.clone() }
402 }
403 Self::BufferOverflow { size, max_size } => {
404 Self::BufferOverflow { size: *size, max_size: *max_size }
405 }
406 Self::SessionRecoveryError { message } => {
407 Self::SessionRecoveryError { message: message.clone() }
408 }
409 Self::ResourceAllocationError { message } => {
410 Self::ResourceAllocationError { message: message.clone() }
411 }
412 Self::IoError(err) => Self::IoError(std::io::Error::new(err.kind(), err.to_string())),
413 Self::ConfigurationError(msg) => Self::ConfigurationError(msg.clone()),
414 Self::ParseError(msg) => Self::ParseError(msg.clone()),
415 Self::InvalidInput(msg) => Self::InvalidInput(msg.clone()),
416 Self::FileError(msg) => Self::FileError(msg.clone()),
417 Self::PathSecurityError { path, message } => {
418 Self::PathSecurityError { path: path.clone(), message: message.clone() }
419 }
420 }
421 }
422}