use super::keyboard::{self, MAX_WAIT_MS};
use super::touch;
use super::types::{ActOutput, ComputerAct};
pub const ACT_CHAIN_MAX_DEPTH: usize = 8;
pub async fn act(input: &ComputerAct) -> Result<ActOutput, String> {
validate_chain(input, ACT_CHAIN_MAX_DEPTH)?;
let owned = input.clone();
tokio::task::spawn_blocking(move || act_blocking(&owned))
.await
.map_err(|e| format!("computer_act: blocking task failed: {e}"))?
}
pub(crate) fn validate_chain(step: &ComputerAct, remaining: usize) -> Result<(), String> {
if remaining == 0 {
return Err(format!(
"computer_act: `then` chain exceeds {ACT_CHAIN_MAX_DEPTH} steps"
));
}
validate_step(step)?;
if let Some(next) = &step.then {
validate_chain(next, remaining - 1)?;
}
Ok(())
}
fn validate_step(step: &ComputerAct) -> Result<(), String> {
const TOOL: &str = "computer_act";
if let Some(ms) = step.wait {
let extra: Vec<&str> = [
(step.name.is_some(), "`name`"),
(step.pid.is_some(), "`pid`"),
(step.surface.is_some(), "`surface`"),
(step.selector.is_some(), "`selector`"),
(step.nth.is_some(), "`nth`"),
(step.action.is_some(), "`action`"),
(step.value.is_some(), "`value`"),
(step.numeric_value.is_some(), "`numeric_value`"),
(step.range.is_some(), "`range`"),
(step.timeout_ms.is_some(), "`timeout_ms`"),
(step.key.is_some(), "`key`"),
(step.text.is_some(), "`text`"),
(step.held.is_some(), "`held`"),
]
.iter()
.filter(|(present, _)| *present)
.map(|(_, name)| *name)
.collect();
if !extra.is_empty() {
return Err(format!(
"{TOOL}: `wait` is a standalone step — it takes no {}",
extra.join(", ")
));
}
if ms == 0 {
return Err(format!("{TOOL}: `wait` must be >= 1 ms"));
}
if ms > MAX_WAIT_MS {
return Err(format!(
"{TOOL}: `wait` is capped at {MAX_WAIT_MS} ms per step — \
chain several wait steps for longer pauses"
));
}
return Ok(());
}
let key = step.key.as_deref().map(str::trim).filter(|s| !s.is_empty());
let text = step.text.as_deref().filter(|t| !t.is_empty());
if key.is_some() && text.is_some() {
return Err(format!("{TOOL}: provide `key` or `text`, not both"));
}
if key.is_some() || text.is_some() {
for (present, name) in [
(step.name.is_some(), "`name`"),
(step.pid.is_some(), "`pid`"),
(step.surface.is_some(), "`surface`"),
(step.selector.is_some(), "`selector`"),
(step.nth.is_some(), "`nth`"),
(step.action.is_some(), "`action`"),
(step.value.is_some(), "`value`"),
(step.numeric_value.is_some(), "`numeric_value`"),
(step.range.is_some(), "`range`"),
(step.timeout_ms.is_some(), "`timeout_ms`"),
] {
if present {
return Err(format!(
"{TOOL}: {name} belongs to a semantic-action step — a keyboard step \
types into the focused element; act on the target element in an \
earlier step, then chain the typing via `then`. On a SHELL SURFACE \
the pattern is: a semantic `press` with `surface` first (that is \
what moves keyboard focus to the flyout), then this keyboard step \
with no `surface`. If the typed text keeps missing the field, click \
the field element with computer_control instead — a real click is \
the one mechanism that reliably moves keyboard focus"
));
}
}
if text.is_some() {
if step.held.is_some() {
return Err(format!(
"{TOOL}: `held` is rejected with `text` ({text:?}) — backends \
synthesize case/shift; use `key` with `held` for chords"
));
}
} else {
keyboard::parse_key(key.unwrap_or_default(), TOOL)?;
}
keyboard::parse_keys(step.held.as_deref().unwrap_or_default(), TOOL)?;
return Ok(());
}
touch::validate(step, TOOL)
}
pub(crate) fn chain_needs_sim(step: &ComputerAct) -> bool {
keyboard::is_keyboard_step(step.key.as_deref(), step.text.as_deref())
|| step.then.as_deref().is_some_and(chain_needs_sim)
}
fn act_blocking(input: &ComputerAct) -> Result<ActOutput, String> {
let sim = if chain_needs_sim(input) {
Some(keyboard::shared_sim().map_err(|e| format!("computer_act: input backend: {e}"))?)
} else {
None
};
let mut report = String::new();
let mut step = input;
let mut index = 1usize;
loop {
let sent = run_step(sim.as_ref(), step).map_err(|e| {
if report.is_empty() {
format!("computer_act: step {index}: {e}")
} else {
format!("computer_act: step {index}: {e} — completed: {report}")
}
})?;
if !report.is_empty() {
report.push_str(" → ");
}
report.push_str(&sent);
match &step.then {
Some(next) => {
step = next;
index += 1;
}
None => return Ok(ActOutput { sent: report }),
}
}
}
fn run_step(sim: Option<&xa11y::InputSim>, step: &ComputerAct) -> Result<String, String> {
if let Some(ms) = step.wait {
return Ok(keyboard::run_wait_step(ms));
}
let key = step.key.as_deref();
let text = step.text.as_deref();
if keyboard::is_keyboard_step(key, text) {
let sim = sim.expect("chain_needs_sim: a keyboard step guarantees a simulator");
keyboard::run_keyboard_step(
sim,
&keyboard::KeyboardStep {
key,
text,
held: step.held.as_deref(),
},
"computer_act",
)
} else {
touch::run_step(step, "computer_act")
}
}