lazyBoy/crates/contracts/src/action.rs

171 lines
4.5 KiB
Rust
Raw Normal View History

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,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum RefVerb {
Click,
Focus,
SetValue,
}
2026-09-03 12:46:14 +00:00
/// 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>,
},
2026-09-08 13:27:38 +00:00
/// Snapshot-scoped semantic target resolved by Cua.
Ref {
verb: RefVerb,
target: String,
#[serde(default, rename = "refKind")]
ref_kind: String,
#[serde(default)]
text: Option<String>,
},
2026-09-08 13:27:38 +00:00
CopySelection,
2026-09-03 12:46:14 +00:00
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,
2026-09-08 13:27:38 +00:00
/// Opaque Cua browser or native element reference. Windows leave this empty.
2026-09-04 09:08:56 +00:00
#[serde(default, skip_serializing_if = "Option::is_none")]
pub selector: Option<String>,
/// "dom" for page controls, "a11y" for AT-SPI widgets, "window" for native windows.
2026-09-04 09:08:56 +00:00
#[serde(default, skip_serializing_if = "Option::is_none")]
pub kind: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub role: Option<String>,
2026-09-04 09:08:56 +00:00
}
impl UiElement {
pub fn has_ref(&self) -> bool {
self.selector
.as_deref()
.is_some_and(|value| !value.is_empty())
&& matches!(self.kind.as_deref(), Some("dom") | Some("a11y"))
}
2026-09-04 09:08:56 +00:00
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 {
2026-09-08 13:27:38 +00:00
/// The controller already populated native semantics; skip duplicate enrichment.
2026-09-07 16:40:59 +00:00
#[serde(default)]
pub native_observation_complete: bool,
2026-09-03 12:46:14 +00:00
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)
}
}