mirror of
https://github.com/lahfir/agent-desktop.git
synced 2026-08-04 13:16:06 +00:00
- fix(core): wait --notification uses index-diff detection instead of count, passes text filter, checks deadline before sleep - fix(core): dismiss-all-notifications uses single NC session with batch adapter method and reports individual failures - refactor(macos): extract dismiss_entry helper to eliminate duplicate dismiss logic between single and batch operations - refactor(macos): restrict nc_session visibility to pub(crate), use absolute paths for pgrep/osascript, add bounded wait with kill fallback - refactor(macos): single-pass is_notification_group check using matches! macro - refactor: extract notification CLI args and dispatch into separate files to keep cli_args.rs and dispatch.rs under 400 LOC - refactor: rename DismissAllNotificationsArgs to DismissAllNotificationsCliArgs for consistency - fix(core): remove unused timestamp field from NotificationInfo - fix: remove poll_interval_ms from wait command (hardcode 500ms) - docs: update phases.md with completed notification status and cross-platform reference notes
400 lines
13 KiB
Rust
400 lines
13 KiB
Rust
use agent_desktop_core::{
|
|
action::{Direction, MouseButton},
|
|
adapter::PlatformAdapter,
|
|
commands::{
|
|
batch, check, clear, click, clipboard_clear, clipboard_get, clipboard_set, close_app,
|
|
collapse, double_click, drag, expand, find, focus, focus_window, get, helpers, hover,
|
|
is_check, key_down, key_up, launch, list_apps, list_surfaces, list_windows, maximize,
|
|
minimize, mouse_click, mouse_down, mouse_move, mouse_up, move_window, permissions, press,
|
|
resize_window, restore, right_click, screenshot, scroll, scroll_to, select, set_value,
|
|
snapshot, status, toggle, triple_click, type_text, uncheck, version, wait,
|
|
},
|
|
error::AppError,
|
|
};
|
|
use serde_json::Value;
|
|
|
|
use crate::cli::Commands;
|
|
|
|
pub fn dispatch(cmd: Commands, adapter: &dyn PlatformAdapter) -> Result<Value, AppError> {
|
|
tracing::debug!("dispatch: {}", cmd.name());
|
|
match cmd {
|
|
Commands::Snapshot(a) => snapshot::execute(
|
|
snapshot::SnapshotArgs {
|
|
app: a.app,
|
|
window_id: a.window_id,
|
|
max_depth: a.max_depth,
|
|
include_bounds: a.include_bounds,
|
|
interactive_only: a.interactive_only,
|
|
compact: a.compact,
|
|
surface: a.surface.to_core(),
|
|
},
|
|
adapter,
|
|
),
|
|
|
|
Commands::Find(a) => find::execute(
|
|
find::FindArgs {
|
|
app: a.app,
|
|
role: a.role,
|
|
name: a.name,
|
|
value: a.value,
|
|
text: a.text,
|
|
count: a.count,
|
|
first: a.first,
|
|
last: a.last,
|
|
nth: a.nth,
|
|
},
|
|
adapter,
|
|
),
|
|
|
|
Commands::Screenshot(a) => screenshot::execute(
|
|
screenshot::ScreenshotArgs {
|
|
app: a.app,
|
|
window_id: a.window_id,
|
|
output_path: a.output_path,
|
|
},
|
|
adapter,
|
|
),
|
|
|
|
Commands::Get(a) => get::execute(
|
|
get::GetArgs {
|
|
ref_id: a.ref_id,
|
|
property: parse_get_property(&a.property)?,
|
|
},
|
|
adapter,
|
|
),
|
|
|
|
Commands::Is(a) => is_check::execute(
|
|
is_check::IsArgs {
|
|
ref_id: a.ref_id,
|
|
property: parse_is_property(&a.property)?,
|
|
},
|
|
adapter,
|
|
),
|
|
|
|
Commands::Click(a) => click::execute(click::ClickArgs { ref_id: a.ref_id }, adapter),
|
|
Commands::DoubleClick(a) => {
|
|
double_click::execute(double_click::DoubleClickArgs { ref_id: a.ref_id }, adapter)
|
|
}
|
|
Commands::TripleClick(a) => {
|
|
triple_click::execute(triple_click::TripleClickArgs { ref_id: a.ref_id }, adapter)
|
|
}
|
|
Commands::RightClick(a) => {
|
|
right_click::execute(right_click::RightClickArgs { ref_id: a.ref_id }, adapter)
|
|
}
|
|
|
|
Commands::Type(a) => type_text::execute(
|
|
type_text::TypeArgs {
|
|
ref_id: a.ref_id,
|
|
text: a.text,
|
|
},
|
|
adapter,
|
|
),
|
|
|
|
Commands::SetValue(a) => set_value::execute(
|
|
set_value::SetValueArgs {
|
|
ref_id: a.ref_id,
|
|
value: a.value,
|
|
},
|
|
adapter,
|
|
),
|
|
|
|
Commands::Clear(a) => clear::execute(clear::ClearArgs { ref_id: a.ref_id }, adapter),
|
|
|
|
Commands::Focus(a) => focus::execute(helpers::RefArgs { ref_id: a.ref_id }, adapter),
|
|
Commands::Toggle(a) => toggle::execute(helpers::RefArgs { ref_id: a.ref_id }, adapter),
|
|
Commands::Check(a) => check::execute(check::CheckArgs { ref_id: a.ref_id }, adapter),
|
|
Commands::Uncheck(a) => {
|
|
uncheck::execute(uncheck::UncheckArgs { ref_id: a.ref_id }, adapter)
|
|
}
|
|
Commands::Expand(a) => expand::execute(helpers::RefArgs { ref_id: a.ref_id }, adapter),
|
|
Commands::Collapse(a) => collapse::execute(helpers::RefArgs { ref_id: a.ref_id }, adapter),
|
|
|
|
Commands::Select(a) => select::execute(
|
|
select::SelectArgs {
|
|
ref_id: a.ref_id,
|
|
value: a.value,
|
|
},
|
|
adapter,
|
|
),
|
|
|
|
Commands::Scroll(a) => scroll::execute(
|
|
scroll::ScrollArgs {
|
|
ref_id: a.ref_id,
|
|
direction: parse_direction(&a.direction)?,
|
|
amount: a.amount,
|
|
},
|
|
adapter,
|
|
),
|
|
|
|
Commands::ScrollTo(a) => {
|
|
scroll_to::execute(scroll_to::ScrollToArgs { ref_id: a.ref_id }, adapter)
|
|
}
|
|
|
|
Commands::Press(a) => press::execute(
|
|
press::PressArgs {
|
|
combo: a.combo,
|
|
app: a.app,
|
|
},
|
|
adapter,
|
|
),
|
|
|
|
Commands::KeyDown(a) => {
|
|
key_down::execute(key_down::KeyDownArgs { combo: a.combo }, adapter)
|
|
}
|
|
|
|
Commands::KeyUp(a) => key_up::execute(key_up::KeyUpArgs { combo: a.combo }, adapter),
|
|
|
|
Commands::Hover(a) => hover::execute(
|
|
hover::HoverArgs {
|
|
ref_id: a.ref_id,
|
|
xy: parse_xy_opt(a.xy.as_deref())?,
|
|
duration_ms: a.duration,
|
|
},
|
|
adapter,
|
|
),
|
|
|
|
Commands::Drag(a) => drag::execute(
|
|
drag::DragArgs {
|
|
from_ref: a.from,
|
|
from_xy: parse_xy_opt(a.from_xy.as_deref())?,
|
|
to_ref: a.to,
|
|
to_xy: parse_xy_opt(a.to_xy.as_deref())?,
|
|
duration_ms: a.duration,
|
|
},
|
|
adapter,
|
|
),
|
|
|
|
Commands::MouseMove(a) => {
|
|
let (x, y) = parse_xy(&a.xy)?;
|
|
mouse_move::execute(mouse_move::MouseMoveArgs { x, y }, adapter)
|
|
}
|
|
|
|
Commands::MouseClick(a) => {
|
|
let (x, y) = parse_xy(&a.xy)?;
|
|
mouse_click::execute(
|
|
mouse_click::MouseClickArgs {
|
|
x,
|
|
y,
|
|
button: parse_mouse_button(&a.button)?,
|
|
count: a.count,
|
|
},
|
|
adapter,
|
|
)
|
|
}
|
|
|
|
Commands::MouseDown(a) => {
|
|
let (x, y) = parse_xy(&a.xy)?;
|
|
mouse_down::execute(
|
|
mouse_down::MouseDownArgs {
|
|
x,
|
|
y,
|
|
button: parse_mouse_button(&a.button)?,
|
|
},
|
|
adapter,
|
|
)
|
|
}
|
|
|
|
Commands::MouseUp(a) => {
|
|
let (x, y) = parse_xy(&a.xy)?;
|
|
mouse_up::execute(
|
|
mouse_up::MouseUpArgs {
|
|
x,
|
|
y,
|
|
button: parse_mouse_button(&a.button)?,
|
|
},
|
|
adapter,
|
|
)
|
|
}
|
|
|
|
Commands::Launch(a) => launch::execute(
|
|
launch::LaunchArgs {
|
|
app: a.app,
|
|
timeout_ms: a.timeout,
|
|
},
|
|
adapter,
|
|
),
|
|
|
|
Commands::CloseApp(a) => close_app::execute(
|
|
close_app::CloseAppArgs {
|
|
app: a.app,
|
|
force: a.force,
|
|
},
|
|
adapter,
|
|
),
|
|
|
|
Commands::ListWindows(a) => {
|
|
list_windows::execute(list_windows::ListWindowsArgs { app: a.app }, adapter)
|
|
}
|
|
|
|
Commands::ListApps => list_apps::execute(adapter),
|
|
|
|
Commands::ListSurfaces(a) => {
|
|
list_surfaces::execute(list_surfaces::ListSurfacesArgs { app: a.app }, adapter)
|
|
}
|
|
|
|
Commands::FocusWindow(a) => focus_window::execute(
|
|
focus_window::FocusWindowArgs {
|
|
window_id: a.window_id,
|
|
app: a.app,
|
|
title: a.title,
|
|
},
|
|
adapter,
|
|
),
|
|
|
|
Commands::ResizeWindow(a) => resize_window::execute(
|
|
resize_window::ResizeWindowArgs {
|
|
app: a.app,
|
|
width: a.width,
|
|
height: a.height,
|
|
},
|
|
adapter,
|
|
),
|
|
|
|
Commands::MoveWindow(a) => move_window::execute(
|
|
move_window::MoveWindowArgs {
|
|
app: a.app,
|
|
x: a.x,
|
|
y: a.y,
|
|
},
|
|
adapter,
|
|
),
|
|
|
|
Commands::Minimize(a) => minimize::execute(minimize::MinimizeArgs { app: a.app }, adapter),
|
|
|
|
Commands::Maximize(a) => maximize::execute(maximize::MaximizeArgs { app: a.app }, adapter),
|
|
|
|
Commands::Restore(a) => restore::execute(restore::RestoreArgs { app: a.app }, adapter),
|
|
|
|
Commands::ListNotifications(_)
|
|
| Commands::DismissNotification(_)
|
|
| Commands::DismissAllNotifications(_)
|
|
| Commands::NotificationAction(_) => {
|
|
crate::dispatch_notifications::dispatch_notification(cmd, adapter)
|
|
}
|
|
|
|
Commands::ClipboardGet => clipboard_get::execute(adapter),
|
|
Commands::ClipboardSet(a) => clipboard_set::execute(a.text, adapter),
|
|
Commands::ClipboardClear => clipboard_clear::execute(adapter),
|
|
|
|
Commands::Wait(a) => wait::execute(
|
|
wait::WaitArgs {
|
|
ms: a.ms,
|
|
element: a.element,
|
|
window: a.window,
|
|
text: a.text,
|
|
timeout_ms: a.timeout,
|
|
menu: a.menu,
|
|
menu_closed: a.menu_closed,
|
|
notification: a.notification,
|
|
app: a.app,
|
|
},
|
|
adapter,
|
|
),
|
|
|
|
Commands::Status => status::execute(adapter),
|
|
|
|
Commands::Permissions(a) => {
|
|
permissions::execute(permissions::PermissionsArgs { request: a.request }, adapter)
|
|
}
|
|
|
|
Commands::Version(a) => version::execute(version::VersionArgs { json: a.json }),
|
|
|
|
Commands::Batch(a) => {
|
|
let commands = batch::parse_commands(&a.commands_json)?;
|
|
let mut results = Vec::new();
|
|
for cmd in commands {
|
|
let result =
|
|
crate::batch_dispatch::dispatch_batch_command(&cmd.command, cmd.args, adapter);
|
|
let ok = result.is_ok();
|
|
let entry = match result {
|
|
Ok(data) => {
|
|
serde_json::json!({ "ok": true, "command": cmd.command, "data": data })
|
|
}
|
|
Err(e) => {
|
|
serde_json::json!({ "ok": false, "command": cmd.command, "error": e.to_string() })
|
|
}
|
|
};
|
|
results.push(entry);
|
|
if !ok && a.stop_on_error {
|
|
break;
|
|
}
|
|
}
|
|
Ok(serde_json::json!({ "results": results }))
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) fn parse_get_property(s: &str) -> Result<get::GetProperty, AppError> {
|
|
match s {
|
|
"text" => Ok(get::GetProperty::Text),
|
|
"value" => Ok(get::GetProperty::Value),
|
|
"title" => Ok(get::GetProperty::Title),
|
|
"bounds" => Ok(get::GetProperty::Bounds),
|
|
"role" => Ok(get::GetProperty::Role),
|
|
"states" => Ok(get::GetProperty::States),
|
|
other => Err(AppError::invalid_input(format!(
|
|
"Unknown property '{other}'. Valid: text, value, title, bounds, role, states"
|
|
))),
|
|
}
|
|
}
|
|
|
|
pub(crate) fn parse_is_property(s: &str) -> Result<is_check::IsProperty, AppError> {
|
|
match s {
|
|
"visible" => Ok(is_check::IsProperty::Visible),
|
|
"enabled" => Ok(is_check::IsProperty::Enabled),
|
|
"checked" => Ok(is_check::IsProperty::Checked),
|
|
"focused" => Ok(is_check::IsProperty::Focused),
|
|
"expanded" => Ok(is_check::IsProperty::Expanded),
|
|
other => Err(AppError::invalid_input(format!(
|
|
"Unknown property '{other}'. Valid: visible, enabled, checked, focused, expanded"
|
|
))),
|
|
}
|
|
}
|
|
|
|
pub(crate) fn parse_direction(s: &str) -> Result<Direction, AppError> {
|
|
match s {
|
|
"up" => Ok(Direction::Up),
|
|
"down" => Ok(Direction::Down),
|
|
"left" => Ok(Direction::Left),
|
|
"right" => Ok(Direction::Right),
|
|
other => Err(AppError::invalid_input(format!(
|
|
"Unknown direction '{other}'. Valid: up, down, left, right"
|
|
))),
|
|
}
|
|
}
|
|
|
|
pub(crate) fn parse_mouse_button(s: &str) -> Result<MouseButton, AppError> {
|
|
match s {
|
|
"left" => Ok(MouseButton::Left),
|
|
"right" => Ok(MouseButton::Right),
|
|
"middle" => Ok(MouseButton::Middle),
|
|
other => Err(AppError::invalid_input(format!(
|
|
"Unknown button '{other}'. Valid: left, right, middle"
|
|
))),
|
|
}
|
|
}
|
|
|
|
pub(crate) fn parse_xy(s: &str) -> Result<(f64, f64), AppError> {
|
|
let parts: Vec<&str> = s.split(',').collect();
|
|
if parts.len() != 2 {
|
|
return Err(AppError::invalid_input(format!(
|
|
"Invalid coordinates '{s}'. Expected format: x,y (e.g., 500,300)"
|
|
)));
|
|
}
|
|
let x: f64 = parts[0]
|
|
.trim()
|
|
.parse()
|
|
.map_err(|_| AppError::invalid_input(format!("Invalid x coordinate: '{}'", parts[0])))?;
|
|
let y: f64 = parts[1]
|
|
.trim()
|
|
.parse()
|
|
.map_err(|_| AppError::invalid_input(format!("Invalid y coordinate: '{}'", parts[1])))?;
|
|
Ok((x, y))
|
|
}
|
|
|
|
fn parse_xy_opt(s: Option<&str>) -> Result<Option<(f64, f64)>, AppError> {
|
|
match s {
|
|
Some(s) => parse_xy(s).map(Some),
|
|
None => Ok(None),
|
|
}
|
|
}
|