2026-09-03 12:46:14 +00:00
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
|
|
|
#[serde(rename_all = "lowercase")]
|
|
|
|
|
pub enum PointerType {
|
|
|
|
|
Move,
|
|
|
|
|
Down,
|
|
|
|
|
Up,
|
|
|
|
|
Click,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
|
|
#[serde(rename_all = "lowercase")]
|
|
|
|
|
pub enum PointerButton {
|
|
|
|
|
Left,
|
|
|
|
|
Right,
|
|
|
|
|
Middle,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
|
|
#[serde(rename_all = "lowercase")]
|
|
|
|
|
pub enum ScrollDirection {
|
|
|
|
|
Up,
|
|
|
|
|
Down,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Canonical actions the control plane understands.
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
|
|
|
#[serde(tag = "kind", rename_all = "lowercase")]
|
|
|
|
|
pub enum ComputerAction {
|
|
|
|
|
Pointer {
|
|
|
|
|
x: u32,
|
|
|
|
|
y: u32,
|
|
|
|
|
#[serde(rename = "type")]
|
|
|
|
|
pointer_type: PointerType,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
button: Option<PointerButton>,
|
|
|
|
|
},
|
|
|
|
|
Clipboard {
|
|
|
|
|
text: String,
|
|
|
|
|
},
|
|
|
|
|
Key {
|
|
|
|
|
key: String,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
modifiers: Option<Vec<String>>,
|
|
|
|
|
},
|
|
|
|
|
Scroll {
|
|
|
|
|
direction: ScrollDirection,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
amount: Option<u32>,
|
|
|
|
|
},
|
|
|
|
|
Wait {
|
|
|
|
|
ms: u32,
|
|
|
|
|
},
|
|
|
|
|
Open {
|
|
|
|
|
path: String,
|
|
|
|
|
},
|
|
|
|
|
Launch {
|
|
|
|
|
application: String,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
uri: Option<String>,
|
|
|
|
|
},
|
|
|
|
|
Focus {
|
|
|
|
|
title: String,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
|
|
|
pub struct CursorPosition {
|
|
|
|
|
pub x: i32,
|
|
|
|
|
pub y: i32,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
|
|
pub struct ActiveWindow {
|
|
|
|
|
pub id: String,
|
|
|
|
|
pub title: Option<String>,
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-04 09:08:56 +00:00
|
|
|
/// A numbered on-screen target the model can click without guessing pixels.
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
|
|
|
|
|
pub struct UiElement {
|
|
|
|
|
pub id: u32,
|
|
|
|
|
pub title: String,
|
|
|
|
|
pub x: u32,
|
|
|
|
|
pub y: u32,
|
|
|
|
|
pub w: u32,
|
|
|
|
|
pub h: u32,
|
|
|
|
|
/// CSS selector for in-page controls (Chromium CDP). Native windows leave this empty.
|
|
|
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
|
|
|
pub selector: Option<String>,
|
|
|
|
|
/// "dom" for page controls, "window" for native windows.
|
|
|
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
|
|
|
pub kind: Option<String>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl UiElement {
|
|
|
|
|
pub fn center(&self) -> (u32, u32) {
|
|
|
|
|
(
|
|
|
|
|
self.x.saturating_add(self.w / 2),
|
|
|
|
|
self.y.saturating_add(self.h / 2),
|
|
|
|
|
)
|
|
|
|
|
}
|
2026-09-04 15:59:50 +00:00
|
|
|
|
|
|
|
|
/// Page controls outside the viewport are reported with zero size: they
|
|
|
|
|
/// can be clicked through the DOM but have no pixel position.
|
|
|
|
|
pub fn is_offscreen(&self) -> bool {
|
|
|
|
|
self.w == 0 && self.h == 0 && self.selector.is_some()
|
|
|
|
|
}
|
2026-09-04 09:08:56 +00:00
|
|
|
}
|
|
|
|
|
|
2026-09-03 12:46:14 +00:00
|
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
|
|
|
pub struct ComputerObservation {
|
|
|
|
|
pub frame_id: String,
|
|
|
|
|
pub captured_at: String,
|
|
|
|
|
pub mime_type: String,
|
|
|
|
|
#[serde(with = "serde_bytes_opt")]
|
|
|
|
|
pub image: Vec<u8>,
|
|
|
|
|
pub width: u32,
|
|
|
|
|
pub height: u32,
|
|
|
|
|
pub cursor: Option<CursorPosition>,
|
|
|
|
|
pub active_window: Option<ActiveWindow>,
|
2026-09-04 09:08:56 +00:00
|
|
|
#[serde(default)]
|
|
|
|
|
pub elements: Vec<UiElement>,
|
2026-09-03 12:46:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
mod serde_bytes_opt {
|
|
|
|
|
use base64::Engine;
|
2026-09-04 09:08:56 +00:00
|
|
|
use base64::engine::general_purpose::STANDARD;
|
2026-09-03 12:46:14 +00:00
|
|
|
use serde::{Deserialize, Deserializer, Serializer};
|
|
|
|
|
|
|
|
|
|
pub fn serialize<S: Serializer>(bytes: &[u8], serializer: S) -> Result<S::Ok, S::Error> {
|
|
|
|
|
serializer.serialize_str(&STANDARD.encode(bytes))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Vec<u8>, D::Error> {
|
|
|
|
|
let text = String::deserialize(deserializer)?;
|
|
|
|
|
STANDARD.decode(text).map_err(serde::de::Error::custom)
|
|
|
|
|
}
|
|
|
|
|
}
|