agent_tui/terminal/
error.rs1use crate::common::error_codes::{self, ErrorCategory};
7use serde_json::{Value, json};
8use thiserror::Error;
9
10#[derive(Error, Debug)]
12pub enum PtyError {
13 #[error("Failed to open PTY: {0}")]
14 Open(String),
15 #[error("Failed to spawn process: {0}")]
16 Spawn(String),
17 #[error("Failed to write to PTY: {0}")]
18 Write(String),
19 #[error("Failed to read from PTY: {0}")]
20 Read(String),
21 #[error("Failed to resize PTY: {0}")]
22 Resize(String),
23}
24
25impl PtyError {
26 pub fn code(&self) -> i32 {
32 error_codes::PTY_ERROR
33 }
34
35 pub fn category(&self) -> ErrorCategory {
37 ErrorCategory::External
38 }
39
40 pub fn context(&self) -> Value {
42 match self {
43 PtyError::Open(reason) => json!({
44 "operation": "open",
45 "reason": reason
46 }),
47 PtyError::Spawn(reason) => json!({
48 "operation": "spawn",
49 "reason": reason
50 }),
51 PtyError::Write(reason) => json!({
52 "operation": "write",
53 "reason": reason
54 }),
55 PtyError::Read(reason) => json!({
56 "operation": "read",
57 "reason": reason
58 }),
59 PtyError::Resize(reason) => json!({
60 "operation": "resize",
61 "reason": reason
62 }),
63 }
64 }
65
66 pub fn suggestion(&self) -> String {
68 match self {
69 PtyError::Open(_) => {
70 "PTY allocation failed. Check system resource limits (ulimit -n) or try restarting."
71 .to_string()
72 }
73 PtyError::Spawn(reason) => {
74 if reason.contains("not found") || reason.contains("No such file") {
75 "Command not found. Check if the command exists and is in PATH.".to_string()
76 } else if reason.contains("Permission denied") {
77 "Permission denied. Check file permissions.".to_string()
78 } else {
79 "Process spawn failed. Check command syntax and permissions.".to_string()
80 }
81 }
82 PtyError::Write(_) => {
83 "Failed to send input to terminal. The session may have ended. Run 'sessions' to check status."
84 .to_string()
85 }
86 PtyError::Read(_) => {
87 "Failed to read terminal output. The session may have ended. Run 'sessions' to check status."
88 .to_string()
89 }
90 PtyError::Resize(_) => {
91 "Failed to resize terminal. Try again or restart the session.".to_string()
92 }
93 }
94 }
95
96 pub fn is_retryable(&self) -> bool {
98 matches!(self, PtyError::Read(_) | PtyError::Write(_))
99 }
100
101 pub fn operation(&self) -> &'static str {
103 match self {
104 PtyError::Open(_) => "open",
105 PtyError::Spawn(_) => "spawn",
106 PtyError::Write(_) => "write",
107 PtyError::Read(_) => "read",
108 PtyError::Resize(_) => "resize",
109 }
110 }
111
112 pub fn reason(&self) -> &str {
114 match self {
115 PtyError::Open(r)
116 | PtyError::Spawn(r)
117 | PtyError::Write(r)
118 | PtyError::Read(r)
119 | PtyError::Resize(r) => r,
120 }
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127
128 #[test]
129 fn test_pty_error_code() {
130 let err = PtyError::Open("test".into());
131 assert_eq!(err.code(), error_codes::PTY_ERROR);
132 }
133
134 #[test]
135 fn test_pty_error_category() {
136 let err = PtyError::Write("broken pipe".into());
137 assert_eq!(err.category(), ErrorCategory::External);
138 }
139
140 #[test]
141 fn test_pty_error_context() {
142 let err = PtyError::Spawn("command not found".into());
143 let ctx = err.context();
144 assert_eq!(ctx["operation"], "spawn");
145 assert_eq!(ctx["reason"], "command not found");
146 }
147
148 #[test]
149 fn test_pty_error_suggestion_not_found() {
150 let err = PtyError::Spawn("No such file or directory".into());
151 assert!(err.suggestion().contains("not found"));
152 }
153
154 #[test]
155 fn test_pty_error_suggestion_permission() {
156 let err = PtyError::Spawn("Permission denied".into());
157 assert!(err.suggestion().contains("Permission"));
158 }
159
160 #[test]
161 fn test_pty_error_is_retryable() {
162 assert!(PtyError::Read("timeout".into()).is_retryable());
163 assert!(PtyError::Write("broken pipe".into()).is_retryable());
164 assert!(!PtyError::Open("failed".into()).is_retryable());
165 assert!(!PtyError::Spawn("not found".into()).is_retryable());
166 }
167
168 #[test]
169 fn test_pty_error_operation() {
170 assert_eq!(PtyError::Open("x".into()).operation(), "open");
171 assert_eq!(PtyError::Spawn("x".into()).operation(), "spawn");
172 assert_eq!(PtyError::Write("x".into()).operation(), "write");
173 assert_eq!(PtyError::Read("x".into()).operation(), "read");
174 assert_eq!(PtyError::Resize("x".into()).operation(), "resize");
175 }
176
177 #[test]
178 fn test_pty_error_reason() {
179 let err = PtyError::Open("allocation failed".into());
180 assert_eq!(err.reason(), "allocation failed");
181 }
182}