use axum::{
extract::{Path, Query, State, WebSocketUpgrade},
response::IntoResponse,
};
use serde::{Deserialize, Serialize};
use crate::AppState;
#[derive(Debug, Deserialize, PartialEq)]
#[serde(tag = "type")]
pub enum ClientControl {
#[serde(rename = "resize")]
Resize { cols: u16, rows: u16 },
#[serde(rename = "ping")]
Ping,
#[serde(rename = "resync")]
Resync,
#[serde(rename = "viewport_request")]
ViewportRequest {
y: i32,
#[serde(default)]
fp: Option<String>,
},
}
#[derive(Debug, Serialize)]
#[serde(tag = "type")]
#[allow(dead_code)] pub enum ServerControl<'a> {
#[serde(rename = "attached")]
Attached { session: &'a str },
#[serde(rename = "pong")]
Pong,
#[serde(rename = "error")]
Error { message: &'a str },
#[serde(rename = "exit")]
Exit { code: Option<i32> },
#[serde(rename = "agent_state")]
AgentState {
agent_kind: Option<&'a str>,
state: &'a str,
attention_reason: Option<&'a str>,
agent_event: Option<&'a str>,
agent_nonce: Option<&'a str>,
},
}
#[derive(Debug, Deserialize)]
pub struct TerminalQuery {
pub cols: Option<u16>,
pub rows: Option<u16>,
}
pub async fn ws_terminal_handler(
ws: WebSocketUpgrade,
Path(session_id): Path<String>,
Query(query): Query<TerminalQuery>,
State(state): State<AppState>,
) -> impl IntoResponse {
ws.on_upgrade(move |socket| async move {
let runtime_kind: Option<(String,)> =
sqlx::query_as("SELECT runtime_kind FROM sessions WHERE id = ?")
.bind(&session_id)
.fetch_optional(&state.db)
.await
.ok()
.flatten();
crate::engine::run_terminal_session(
runtime_kind.map(|(kind,)| kind),
socket,
session_id,
query,
state,
)
.await
})
}
pub async fn ws_external_terminal_handler(
ws: WebSocketUpgrade,
Path(session_name): Path<String>,
Query(query): Query<TerminalQuery>,
State(state): State<AppState>,
) -> impl IntoResponse {
ws.on_upgrade(move |socket| {
crate::engine::run_external_terminal_session(socket, session_name, query, state)
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn viewport_request_parses() {
let ctrl: ClientControl =
serde_json::from_str(r#"{"type":"viewport_request","y":42}"#).unwrap();
assert_eq!(ctrl, ClientControl::ViewportRequest { y: 42, fp: None });
let ctrl: ClientControl =
serde_json::from_str(r#"{"type":"viewport_request","y":42,"fp":"00000000deadbeef"}"#)
.unwrap();
assert_eq!(
ctrl,
ClientControl::ViewportRequest { y: 42, fp: Some("00000000deadbeef".to_string()) }
);
let ctrl: ClientControl =
serde_json::from_str(r#"{"type":"viewport_request","y":0,"fp":null}"#).unwrap();
assert_eq!(ctrl, ClientControl::ViewportRequest { y: 0, fp: None });
}
#[test]
fn legacy_control_frames_still_parse() {
let resync: ClientControl = serde_json::from_str(r#"{"type":"resync"}"#).unwrap();
assert_eq!(resync, ClientControl::Resync);
let resize: ClientControl =
serde_json::from_str(r#"{"type":"resize","cols":80,"rows":24}"#).unwrap();
assert_eq!(resize, ClientControl::Resize { cols: 80, rows: 24 });
}
}