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 #[serde(default)]
97 pub max_output_bytes: Option<u64>,
98}
99
100impl ExecuteCommandRequest {
101 pub fn timeout(&self) -> Option<Duration> {
102 self.timeout_secs.map(Duration::from_secs)
103 }
104}
105
106#[derive(Debug, Clone, Serialize)]
108pub struct ExecuteCommandResponse {
109 pub success: bool,
111 #[serde(skip_serializing_if = "Option::is_none")]
113 pub exit_code: Option<i32>,
114 pub output: String,
116 #[serde(skip_serializing_if = "Option::is_none")]
118 pub raw_output: Option<String>,
119 pub duration_ms: u64,
121 pub timed_out: bool,
123 pub total_bytes: u64,
127 pub truncated: bool,
132}
133
134impl ExecuteCommandResponse {
135 pub fn from_result(result: &crate::execution::ExecutionResult) -> Self {
136 Self {
137 success: result.exit_code.map(|c| c == 0).unwrap_or(false) && !result.timed_out,
138 exit_code: result.exit_code,
139 output: result.text_output.clone(),
140 raw_output: None, duration_ms: result.duration.as_millis() as u64,
142 timed_out: result.timed_out,
143 total_bytes: result.total_bytes,
144 truncated: result.truncated,
145 }
146 }
147
148 pub fn with_raw_output(mut self, include: bool, raw: &[u8]) -> Self {
149 if include {
150 self.raw_output = Some(String::from_utf8_lossy(raw).to_string());
152 }
153 self
154 }
155}
156
157#[derive(Debug, Clone, Serialize)]
159pub struct ErrorResponse {
160 pub code: String,
162 pub message: String,
164 #[serde(skip_serializing_if = "Option::is_none")]
166 pub details: Option<String>,
167}
168
169impl ErrorResponse {
170 pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
171 Self {
172 code: code.into(),
173 message: message.into(),
174 details: None,
175 }
176 }
177
178 pub fn with_details(mut self, details: impl Into<String>) -> Self {
179 self.details = Some(details.into());
180 self
181 }
182
183 pub fn session_not_found(id: &str) -> Self {
184 Self::new("SESSION_NOT_FOUND", format!("Session '{}' not found", id))
185 }
186
187 pub fn invalid_state(state: SessionState) -> Self {
188 Self::new(
189 "INVALID_STATE",
190 format!(
191 "Session is in {:?} state and cannot execute commands",
192 state
193 ),
194 )
195 }
196
197 pub fn internal_error(message: impl Into<String>) -> Self {
198 Self::new("INTERNAL_ERROR", message)
199 }
200
201 pub fn bad_request(message: impl Into<String>) -> Self {
202 Self::new("BAD_REQUEST", message)
203 }
204}
205
206#[derive(Debug, Clone, Serialize, Deserialize)]
208#[serde(tag = "type", rename_all = "snake_case")]
209pub enum WsMessage {
210 Execute {
212 command: String,
213 #[serde(default)]
214 timeout_secs: Option<u64>,
215 },
216 Output {
218 data: String,
219 #[serde(default)]
221 is_final: bool,
222 },
223 Result {
230 success: bool,
231 exit_code: Option<i32>,
232 duration_ms: u64,
233 timed_out: bool,
234 total_bytes: u64,
235 },
236 Error {
238 code: String,
239 message: String,
240 },
241 Ping,
243 Pong,
244}
245
246#[derive(Debug, Clone, Serialize)]
248pub struct ListSessionsResponse {
249 pub count: usize,
251 pub sessions: Vec<SessionSummary>,
253}
254
255#[derive(Debug, Clone, Serialize)]
257pub struct SessionSummary {
258 pub session_id: u64,
259 pub state: String,
260 pub idle_seconds: f64,
261}
262
263#[cfg(test)]
264mod tests {
265 use super::*;
266
267 #[test]
268 fn test_create_session_request_default() {
269 let req: CreateSessionRequest = serde_json::from_str("{}").unwrap();
270 assert!(req.shell.is_none());
271 assert!(req.working_dir.is_none());
272 assert!(req.env.is_empty());
273 }
274
275 #[test]
276 fn test_create_session_request_with_fields() {
277 let json = r#"{"shell": "bash", "working_dir": "/tmp"}"#;
278 let req: CreateSessionRequest = serde_json::from_str(json).unwrap();
279 assert_eq!(req.shell, Some("bash".to_string()));
280 assert_eq!(req.working_dir, Some("/tmp".to_string()));
281 }
282
283 #[test]
284 fn test_execute_command_request() {
285 let json = r#"{"command": "echo hello", "timeout_secs": 30}"#;
286 let req: ExecuteCommandRequest = serde_json::from_str(json).unwrap();
287 assert_eq!(req.command, "echo hello");
288 assert_eq!(req.timeout(), Some(Duration::from_secs(30)));
289 }
290
291 #[test]
292 fn test_error_response_serialization() {
293 let err = ErrorResponse::new("TEST_ERROR", "Test message");
294 let json = serde_json::to_string(&err).unwrap();
295 assert!(json.contains("TEST_ERROR"));
296 assert!(json.contains("Test message"));
297 assert!(!json.contains("details")); }
299
300 #[test]
301 fn test_ws_message_execute() {
302 let msg = WsMessage::Execute {
303 command: "ls".to_string(),
304 timeout_secs: Some(10),
305 };
306 let json = serde_json::to_string(&msg).unwrap();
307 assert!(json.contains("execute"));
308 assert!(json.contains("ls"));
309 }
310
311 #[test]
312 fn test_ws_message_output() {
313 let msg = WsMessage::Output {
314 data: "hello\n".to_string(),
315 is_final: false,
316 };
317 let json = serde_json::to_string(&msg).unwrap();
318 assert!(json.contains("output"));
319 }
320}