Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,18 @@ use crate::adapters::parse_keydown_input;
use crate::adapters::parse_keystroke_input;
use crate::adapters::parse_keyup_input;
use crate::adapters::parse_type_input;
use crate::adapters::parse_mouse_click_input;
use crate::adapters::parse_mouse_move_input;
use crate::adapters::parse_mouse_down_input;
use crate::adapters::parse_mouse_up_input;
use crate::usecases::KeydownUseCase;
use crate::usecases::KeystrokeUseCase;
use crate::usecases::KeyupUseCase;
use crate::usecases::TypeUseCase;
use crate::usecases::MouseClickUseCase;
use crate::usecases::MouseMoveUseCase;
use crate::usecases::MouseDownUseCase;
use crate::usecases::MouseUpUseCase;

pub fn handle_keystroke_uc<U: KeystrokeUseCase>(usecase: &U, request: RpcRequest) -> RpcResponse {
let _span = common::handler_span(&request, "keystroke").entered();
Expand Down Expand Up @@ -69,3 +77,59 @@ pub fn handle_keyup_uc<U: KeyupUseCase>(usecase: &U, request: RpcRequest) -> Rpc
Err(e) => session_error_response(req_id, e),
}
}

pub fn handle_mouse_click_uc<U: MouseClickUseCase>(usecase: &U, request: RpcRequest) -> RpcResponse {
let _span = common::handler_span(&request, "mouse_click").entered();
let req_id = request.id;
let input = match parse_mouse_click_input(&request) {
Ok(i) => i,
Err(resp) => return resp,
};

match usecase.execute(input) {
Ok(_) => RpcResponse::action_success(req_id),
Err(e) => session_error_response(req_id, e),
}
}

pub fn handle_mouse_move_uc<U: MouseMoveUseCase>(usecase: &U, request: RpcRequest) -> RpcResponse {
let _span = common::handler_span(&request, "mouse_move").entered();
let req_id = request.id;
let input = match parse_mouse_move_input(&request) {
Ok(i) => i,
Err(resp) => return resp,
};

match usecase.execute(input) {
Ok(_) => RpcResponse::action_success(req_id),
Err(e) => session_error_response(req_id, e),
}
}

pub fn handle_mouse_down_uc<U: MouseDownUseCase>(usecase: &U, request: RpcRequest) -> RpcResponse {
let _span = common::handler_span(&request, "mouse_down").entered();
let req_id = request.id;
let input = match parse_mouse_down_input(&request) {
Ok(i) => i,
Err(resp) => return resp,
};

match usecase.execute(input) {
Ok(_) => RpcResponse::action_success(req_id),
Err(e) => session_error_response(req_id, e),
}
}

pub fn handle_mouse_up_uc<U: MouseUpUseCase>(usecase: &U, request: RpcRequest) -> RpcResponse {
let _span = common::handler_span(&request, "mouse_up").entered();
let req_id = request.id;
let input = match parse_mouse_up_input(&request) {
Ok(i) => i,
Err(resp) => return resp,
};

match usecase.execute(input) {
Ok(_) => RpcResponse::action_success(req_id),
Err(e) => session_error_response(req_id, e),
}
}
12 changes: 12 additions & 0 deletions cli/crates/agent-tui-adapters/src/adapters/daemon/router.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,18 @@ impl<'a, R: SessionRepository + 'static> Router<'a, R> {
}
"keydown" => handlers::input::handle_keydown_uc(&self.usecases.input.keydown, request),
"keyup" => handlers::input::handle_keyup_uc(&self.usecases.input.keyup, request),
"mouse_click" => {
handlers::input::handle_mouse_click_uc(&self.usecases.input.mouse_click, request)
}
"mouse_move" => {
handlers::input::handle_mouse_move_uc(&self.usecases.input.mouse_move, request)
}
"mouse_down" => {
handlers::input::handle_mouse_down_uc(&self.usecases.input.mouse_down, request)
}
"mouse_up" => {
handlers::input::handle_mouse_up_uc(&self.usecases.input.mouse_up, request)
}
"type" => handlers::input::handle_type_uc(&self.usecases.input.type_text, request),
"wait" => handlers::wait::handle_wait_uc(&self.usecases.wait, request),

Expand Down
16 changes: 16 additions & 0 deletions cli/crates/agent-tui-adapters/src/adapters/daemon/router_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,22 @@ impl SessionOps for TestSession {
Ok(())
}

fn mouse_click(&self, _col: u16, _row: u16, _button: &str) -> Result<(), SessionError> {
Ok(())
}

fn mouse_move(&self, _col: u16, _row: u16) -> Result<(), SessionError> {
Ok(())
}

fn mouse_down(&self, _col: u16, _row: u16, _button: &str) -> Result<(), SessionError> {
Ok(())
}

fn mouse_up(&self, _col: u16, _row: u16, _button: &str) -> Result<(), SessionError> {
Ok(())
}

fn is_running(&self) -> bool {
true
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ use crate::usecases::KeydownUseCaseImpl;
use crate::usecases::KeystrokeUseCaseImpl;
use crate::usecases::KeyupUseCaseImpl;
use crate::usecases::KillUseCaseImpl;
use crate::usecases::MouseClickUseCaseImpl;
use crate::usecases::MouseDownUseCaseImpl;
use crate::usecases::MouseMoveUseCaseImpl;
use crate::usecases::MouseUpUseCaseImpl;
use crate::usecases::ResizeUseCaseImpl;
use crate::usecases::RestartUseCaseImpl;
use crate::usecases::SessionsUseCaseImpl;
Expand Down Expand Up @@ -51,6 +55,10 @@ pub struct InputUseCases<R: SessionRepository + 'static> {
pub type_text: TypeUseCaseImpl<R>,
pub keydown: KeydownUseCaseImpl<R>,
pub keyup: KeyupUseCaseImpl<R>,
pub mouse_click: MouseClickUseCaseImpl<R>,
pub mouse_move: MouseMoveUseCaseImpl<R>,
pub mouse_down: MouseDownUseCaseImpl<R>,
pub mouse_up: MouseUpUseCaseImpl<R>,
}

pub struct DiagnosticsUseCases<R: SessionRepository + 'static> {
Expand Down Expand Up @@ -84,6 +92,10 @@ impl<R: SessionRepository + 'static> UseCaseContainer<R> {
type_text: TypeUseCaseImpl::new(Arc::clone(&repository)),
keydown: KeydownUseCaseImpl::new(Arc::clone(&repository)),
keyup: KeyupUseCaseImpl::new(Arc::clone(&repository)),
mouse_click: MouseClickUseCaseImpl::new(Arc::clone(&repository)),
mouse_move: MouseMoveUseCaseImpl::new(Arc::clone(&repository)),
mouse_down: MouseDownUseCaseImpl::new(Arc::clone(&repository)),
mouse_up: MouseUpUseCaseImpl::new(Arc::clone(&repository)),
},
diagnostics: DiagnosticsUseCases {
terminal_write: TerminalWriteUseCaseImpl::new(Arc::clone(&repository)),
Expand Down
75 changes: 75 additions & 0 deletions cli/crates/agent-tui-adapters/src/adapters/rpc/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,81 @@ pub fn parse_keyup_input(request: &RpcRequest) -> Result<KeyupInput, RpcResponse
})
}

#[allow(clippy::result_large_err)]
pub fn parse_mouse_click_input(request: &RpcRequest) -> Result<crate::domain::MouseClickInput, RpcResponse> {
let col = request.require_u16("col")?;
let row = request.require_u16("row")?;
let button = request.param_str("button").unwrap_or("left");
let button = crate::domain::MouseButton::parse(button).ok_or_else(|| {
RpcResponse::error(request.id, -32602, "Invalid button. Must be: left, right, or middle")
})?;

Ok(crate::domain::MouseClickInput {
session_id: parse_session_selector(
request.id,
request.param_str("session").map(String::from),
)?,
col,
row,
button,
})
}

#[allow(clippy::result_large_err)]
pub fn parse_mouse_move_input(request: &RpcRequest) -> Result<crate::domain::MouseMoveInput, RpcResponse> {
let col = request.require_u16("col")?;
let row = request.require_u16("row")?;

Ok(crate::domain::MouseMoveInput {
session_id: parse_session_selector(
request.id,
request.param_str("session").map(String::from),
)?,
col,
row,
})
}

#[allow(clippy::result_large_err)]
pub fn parse_mouse_down_input(request: &RpcRequest) -> Result<crate::domain::MouseDownInput, RpcResponse> {
let col = request.require_u16("col")?;
let row = request.require_u16("row")?;
let button = request.param_str("button").unwrap_or("left");
let button = crate::domain::MouseButton::parse(button).ok_or_else(|| {
RpcResponse::error(request.id, -32602, "Invalid button. Must be: left, right, or middle")
})?;

Ok(crate::domain::MouseDownInput {
session_id: parse_session_selector(
request.id,
request.param_str("session").map(String::from),
)?,
col,
row,
button,
})
}

#[allow(clippy::result_large_err)]
pub fn parse_mouse_up_input(request: &RpcRequest) -> Result<crate::domain::MouseUpInput, RpcResponse> {
let col = request.require_u16("col")?;
let row = request.require_u16("row")?;
let button = request.param_str("button").unwrap_or("left");
let button = crate::domain::MouseButton::parse(button).ok_or_else(|| {
RpcResponse::error(request.id, -32602, "Invalid button. Must be: left, right, or middle")
})?;

Ok(crate::domain::MouseUpInput {
session_id: parse_session_selector(
request.id,
request.param_str("session").map(String::from),
)?,
col,
row,
button,
})
}

#[allow(clippy::result_large_err)]
pub fn parse_wait_input(request: &RpcRequest) -> Result<WaitInput, RpcResponse> {
let rpc_params: params::WaitParams = deserialize_optional_params(request)?;
Expand Down
87 changes: 87 additions & 0 deletions cli/crates/agent-tui-adapters/src/adapters/rpc/mod_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -237,3 +237,90 @@ fn test_parse_resize_input_rejects_blank_explicit_session() {
let value = serde_json::to_value(response).expect("response should serialize");
assert_eq!(value["error"]["code"], -32602);
}

#[test]
fn test_parse_mouse_click_input() {
let request = make_request(
1,
"mouse_click",
Some(json!({"col": 5, "row": 10, "button": "left"})),
);
let input = parse_mouse_click_input(&request).expect("mouse_click input should parse");
assert_eq!(input.col, 5);
assert_eq!(input.row, 10);
assert_eq!(input.button, crate::domain::MouseButton::Left);
}

#[test]
fn test_parse_mouse_click_input_defaults_button() {
let request = make_request(1, "mouse_click", Some(json!({"col": 5, "row": 10})));
let input = parse_mouse_click_input(&request).expect("mouse_click should default to left button");
assert_eq!(input.button, crate::domain::MouseButton::Left);
}

#[test]
fn test_parse_mouse_click_input_right_button() {
let request = make_request(
1,
"mouse_click",
Some(json!({"col": 5, "row": 10, "button": "right"})),
);
let input = parse_mouse_click_input(&request).expect("mouse_click should parse right button");
assert_eq!(input.button, crate::domain::MouseButton::Right);
}

#[test]
fn test_parse_mouse_click_input_invalid_button() {
let request = make_request(
1,
"mouse_click",
Some(json!({"col": 5, "row": 10, "button": "invalid"})),
);
let response = parse_mouse_click_input(&request).expect_err("invalid button should error");
let value = serde_json::to_value(response).expect("response should serialize");
assert_eq!(value["error"]["code"], -32602);
}

#[test]
fn test_parse_mouse_click_input_missing_col() {
let request = make_request(1, "mouse_click", Some(json!({"row": 10})));
let response = parse_mouse_click_input(&request).expect_err("missing col should error");
let value = serde_json::to_value(response).expect("response should serialize");
assert_eq!(value["error"]["code"], -32602);
}

#[test]
fn test_parse_mouse_move_input() {
let request = make_request(
1,
"mouse_move",
Some(json!({"col": 5, "row": 10})),
);
let input = parse_mouse_move_input(&request).expect("mouse_move input should parse");
assert_eq!(input.col, 5);
assert_eq!(input.row, 10);
}

#[test]
fn test_parse_mouse_down_input() {
let request = make_request(
1,
"mouse_down",
Some(json!({"col": 5, "row": 10, "button": "middle"})),
);
let input = parse_mouse_down_input(&request).expect("mouse_down input should parse");
assert_eq!(input.col, 5);
assert_eq!(input.row, 10);
assert_eq!(input.button, crate::domain::MouseButton::Middle);
}

#[test]
fn test_parse_mouse_up_input() {
let request = make_request(
1,
"mouse_up",
Some(json!({"col": 5, "row": 10, "button": "right"})),
);
let input = parse_mouse_up_input(&request).expect("mouse_up input should parse");
assert_eq!(input.button, crate::domain::MouseButton::Right);
}
22 changes: 22 additions & 0 deletions cli/crates/agent-tui-adapters/src/adapters/rpc/params.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,28 @@ pub struct TypeParams {
pub session: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MouseParams {
pub col: u16,
pub row: u16,
#[serde(default = "default_mouse_button")]
pub button: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub session: Option<String>,
}

fn default_mouse_button() -> String {
"left".to_string()
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MouseMoveParams {
pub col: u16,
pub row: u16,
#[serde(skip_serializing_if = "Option::is_none")]
pub session: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WaitParams {
#[serde(skip_serializing_if = "Option::is_none")]
Expand Down
14 changes: 14 additions & 0 deletions cli/crates/agent-tui-adapters/src/adapters/rpc/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,20 @@ impl RpcRequest {
self.param_str(key)
.ok_or_else(|| RpcResponse::error(self.id, -32602, &format!("Missing '{key}' param")))
}

#[allow(clippy::result_large_err)]
pub fn require_u64(&self, key: &str) -> Result<u64, RpcResponse> {
self.param_u64_opt(key)
.ok_or_else(|| RpcResponse::error(self.id, -32602, &format!("Missing '{key}' param")))
}

#[allow(clippy::result_large_err)]
pub fn require_u16(&self, key: &str) -> Result<u16, RpcResponse> {
let value = self.require_u64(key)?;
value.try_into().map_err(|_| {
RpcResponse::error(self.id, -32602, &format!("'{key}' must be a valid u16"))
})
}
}

#[derive(Debug, Serialize)]
Expand Down
Loading