1use std::collections::HashMap;
4use std::time::Duration;
5
6use serde::{Deserialize, Serialize};
7
8use crate::session::{SessionId, SessionState};
9
10#[derive(Debug, Clone, Deserialize, Default)]
12pub struct CreateSessionRequest {
13 #[serde(default)]
15 pub shell: Option<String>,
16 #[serde(default)]
18 pub working_dir: Option<String>,
19 #[serde(default)]
21 pub env: HashMap<String, String>,
22}
23
24#[derive(Debug, Clone, Serialize)]
26pub struct CreateSessionResponse {
27 pub session_id: u64,
29 pub session_id_str: String,
31}
32
33impl CreateSessionResponse {
34 pub fn new(id: SessionId) -> Self {
35 Self {
36 session_id: id.as_u64(),
37 session_id_str: id.to_string(),
38 }
39 }
40}
41
42#[derive(Debug, Clone, Serialize)]
44pub struct SessionStatusResponse {
45 pub session_id: u64,
47 pub state: String,
49 #[serde(skip_serializing_if = "Option::is_none")]
51 pub working_dir: Option<String>,
52 #[serde(skip_serializing_if = "Option::is_none")]
54 pub last_exit_code: Option<i32>,
55 pub execution_count: u64,
57 pub idle_seconds: f64,
59}
60
61impl SessionStatusResponse {
62 pub fn from_session(session: &crate::session::Session) -> Self {
63 Self {
64 session_id: session.id.as_u64(),
65 state: format!("{:?}", session.state),
66 working_dir: session
67 .context
68 .cwd()
69 .map(|p| p.to_string_lossy().to_string()),
70 last_exit_code: session.context.last_exit_code(),
71 execution_count: session.context.execution_count(),
72 idle_seconds: session.idle_duration().as_secs_f64(),
73 }
74 }
75}
76
77#[derive(Debug, Clone, Deserialize)]
79pub struct ExecuteCommandRequest {
80 pub command: String,
82 #[serde(default)]
84 pub working_dir: Option<String>,
85 #[serde(default)]
87 pub env: HashMap<String, String>,
88 #[serde(default)]
90 pub timeout_secs: Option<u64>,
91}
92
93impl ExecuteCommandRequest {
94 pub fn timeout(&self) -> Option<Duration> {
95 self.timeout_secs.map(Duration::from_secs)
96 }
97}
98
99#[derive(Debug, Clone, Serialize)]
101pub struct ExecuteCommandResponse {
102 pub success: bool,
104 #[serde(skip_serializing_if = "Option::is_none")]
106 pub exit_code: Option<i32>,
107 pub output: String,
109 #[serde(skip_serializing_if = "Option::is_none")]
111 pub raw_output: Option<String>,
112 pub duration_ms: u64,
114 pub timed_out: bool,
116}
117
118impl ExecuteCommandResponse {
119 pub fn from_result(result: &crate::execution::ExecutionResult) -> Self {
120 Self {
121 success: result.exit_code.map(|c| c == 0).unwrap_or(false) && !result.timed_out,
122 exit_code: result.exit_code,
123 output: result.text_output.clone(),
124 raw_output: None, duration_ms: result.duration.as_millis() as u64,
126 timed_out: result.timed_out,
127 }
128 }
129
130 pub fn with_raw_output(mut self, include: bool, raw: &[u8]) -> Self {
131 if include {
132 self.raw_output = Some(String::from_utf8_lossy(raw).to_string());
134 }
135 self
136 }
137}
138
139#[derive(Debug, Clone, Serialize)]
141pub struct ErrorResponse {
142 pub code: String,
144 pub message: String,
146 #[serde(skip_serializing_if = "Option::is_none")]
148 pub details: Option<String>,
149}
150
151impl ErrorResponse {
152 pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
153 Self {
154 code: code.into(),
155 message: message.into(),
156 details: None,
157 }
158 }
159
160 pub fn with_details(mut self, details: impl Into<String>) -> Self {
161 self.details = Some(details.into());
162 self
163 }
164
165 pub fn session_not_found(id: &str) -> Self {
166 Self::new("SESSION_NOT_FOUND", format!("Session '{}' not found", id))
167 }
168
169 pub fn invalid_state(state: SessionState) -> Self {
170 Self::new(
171 "INVALID_STATE",
172 format!(
173 "Session is in {:?} state and cannot execute commands",
174 state
175 ),
176 )
177 }
178
179 pub fn internal_error(message: impl Into<String>) -> Self {
180 Self::new("INTERNAL_ERROR", message)
181 }
182
183 pub fn bad_request(message: impl Into<String>) -> Self {
184 Self::new("BAD_REQUEST", message)
185 }
186}
187
188#[derive(Debug, Clone, Serialize, Deserialize)]
190#[serde(tag = "type", rename_all = "snake_case")]
191pub enum WsMessage {
192 Execute {
194 command: String,
195 #[serde(default)]
196 timeout_secs: Option<u64>,
197 },
198 Output {
200 data: String,
201 #[serde(default)]
203 is_final: bool,
204 },
205 Result {
207 success: bool,
208 exit_code: Option<i32>,
209 duration_ms: u64,
210 timed_out: bool,
211 },
212 Error {
214 code: String,
215 message: String,
216 },
217 Ping,
219 Pong,
220}
221
222#[derive(Debug, Clone, Serialize)]
224pub struct ListSessionsResponse {
225 pub count: usize,
227 pub sessions: Vec<SessionSummary>,
229}
230
231#[derive(Debug, Clone, Serialize)]
233pub struct SessionSummary {
234 pub session_id: u64,
235 pub state: String,
236 pub idle_seconds: f64,
237}
238
239#[cfg(test)]
240mod tests {
241 use super::*;
242
243 #[test]
244 fn test_create_session_request_default() {
245 let req: CreateSessionRequest = serde_json::from_str("{}").unwrap();
246 assert!(req.shell.is_none());
247 assert!(req.working_dir.is_none());
248 assert!(req.env.is_empty());
249 }
250
251 #[test]
252 fn test_create_session_request_with_fields() {
253 let json = r#"{"shell": "bash", "working_dir": "/tmp"}"#;
254 let req: CreateSessionRequest = serde_json::from_str(json).unwrap();
255 assert_eq!(req.shell, Some("bash".to_string()));
256 assert_eq!(req.working_dir, Some("/tmp".to_string()));
257 }
258
259 #[test]
260 fn test_execute_command_request() {
261 let json = r#"{"command": "echo hello", "timeout_secs": 30}"#;
262 let req: ExecuteCommandRequest = serde_json::from_str(json).unwrap();
263 assert_eq!(req.command, "echo hello");
264 assert_eq!(req.timeout(), Some(Duration::from_secs(30)));
265 }
266
267 #[test]
268 fn test_error_response_serialization() {
269 let err = ErrorResponse::new("TEST_ERROR", "Test message");
270 let json = serde_json::to_string(&err).unwrap();
271 assert!(json.contains("TEST_ERROR"));
272 assert!(json.contains("Test message"));
273 assert!(!json.contains("details")); }
275
276 #[test]
277 fn test_ws_message_execute() {
278 let msg = WsMessage::Execute {
279 command: "ls".to_string(),
280 timeout_secs: Some(10),
281 };
282 let json = serde_json::to_string(&msg).unwrap();
283 assert!(json.contains("execute"));
284 assert!(json.contains("ls"));
285 }
286
287 #[test]
288 fn test_ws_message_output() {
289 let msg = WsMessage::Output {
290 data: "hello\n".to_string(),
291 is_final: false,
292 };
293 let json = serde_json::to_string(&msg).unwrap();
294 assert!(json.contains("output"));
295 }
296}