Skip to main content

agent_tui_app/app/
error.rs

1//! Application error types and CLI error helpers.
2
3use std::io;
4
5use crate::app::commands::OutputFormat;
6use crate::common::error_codes;
7use crate::common::error_codes::ErrorCategory;
8use serde::Serialize;
9use thiserror::Error;
10
11#[derive(Error, Debug)]
12pub enum AttachError {
13    #[error("Terminal error: {0}")]
14    Terminal(#[from] io::Error),
15
16    #[error("PTY write failed: {0}")]
17    PtyWrite(String),
18
19    #[error("PTY read failed: {0}")]
20    PtyRead(String),
21
22    #[error("Event read failed")]
23    EventRead,
24}
25
26#[derive(Debug, Error)]
27#[error("{message}")]
28pub(crate) struct CliError {
29    pub(crate) exit_code: i32,
30    pub(crate) format: OutputFormat,
31    pub(crate) message: String,
32    pub(crate) json: Option<String>,
33}
34
35impl CliError {
36    pub fn new(
37        format: OutputFormat,
38        message: impl Into<String>,
39        json: Option<String>,
40        exit_code: i32,
41    ) -> Self {
42        Self {
43            exit_code,
44            format,
45            message: message.into(),
46            json,
47        }
48    }
49}
50
51#[derive(Debug, Error)]
52#[error("Daemon is not running")]
53pub(crate) struct DaemonNotRunningError;
54
55impl AttachError {
56    pub fn code(&self) -> i32 {
57        match self {
58            AttachError::Terminal(_) => error_codes::PTY_ERROR,
59            AttachError::PtyWrite(_) => error_codes::PTY_ERROR,
60            AttachError::PtyRead(_) => error_codes::PTY_ERROR,
61            AttachError::EventRead => error_codes::PTY_ERROR,
62        }
63    }
64
65    pub fn category(&self) -> ErrorCategory {
66        ErrorCategory::External
67    }
68
69    pub fn context(&self) -> AttachErrorContext {
70        match self {
71            AttachError::Terminal(e) => AttachErrorContext {
72                operation: "terminal",
73                reason: e.to_string(),
74            },
75            AttachError::PtyWrite(reason) => AttachErrorContext {
76                operation: "pty_write",
77                reason: reason.clone(),
78            },
79            AttachError::PtyRead(reason) => AttachErrorContext {
80                operation: "pty_read",
81                reason: reason.clone(),
82            },
83            AttachError::EventRead => AttachErrorContext {
84                operation: "event_read",
85                reason: "Failed to read terminal events".to_string(),
86            },
87        }
88    }
89
90    pub fn suggestion(&self) -> String {
91        match self {
92            AttachError::Terminal(_) => {
93                "Terminal mode error. Try restarting your terminal.".to_string()
94            }
95            AttachError::PtyWrite(_) => {
96                "Failed to send input to session. The session may have ended. Run 'sessions' to check status."
97                    .to_string()
98            }
99            AttachError::PtyRead(_) => {
100                "Failed to read from session. The session may have ended. Run 'sessions' to check status."
101                    .to_string()
102            }
103            AttachError::EventRead => {
104                "Failed to read terminal events. Try restarting your terminal.".to_string()
105            }
106        }
107    }
108
109    pub fn is_retryable(&self) -> bool {
110        matches!(self, AttachError::PtyWrite(_) | AttachError::PtyRead(_))
111    }
112
113    pub fn exit_code(&self) -> i32 {
114        match self.category() {
115            ErrorCategory::InvalidInput => 64,
116            ErrorCategory::NotFound => 69,
117            ErrorCategory::Busy => 73,
118            ErrorCategory::External => 74,
119            ErrorCategory::Internal => 74,
120            ErrorCategory::Timeout => 75,
121        }
122    }
123
124    pub fn to_payload(&self) -> AttachErrorPayload {
125        AttachErrorPayload {
126            code: self.code(),
127            message: self.to_string(),
128            category: self.category().as_str().to_string(),
129            retryable: self.is_retryable(),
130            context: self.context(),
131            suggestion: self.suggestion(),
132        }
133    }
134}
135
136#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
137pub struct AttachErrorContext {
138    pub operation: &'static str,
139    pub reason: String,
140}
141
142#[derive(Debug, Clone, Serialize)]
143pub struct AttachErrorPayload {
144    pub code: i32,
145    pub message: String,
146    pub category: String,
147    pub retryable: bool,
148    pub context: AttachErrorContext,
149    pub suggestion: String,
150}
151
152#[cfg(test)]
153#[path = "error_tests.rs"]
154mod tests;