diff --git a/cli/src/commands.rs b/cli/src/commands.rs index 465b8d2..1b6e6b1 100644 --- a/cli/src/commands.rs +++ b/cli/src/commands.rs @@ -583,11 +583,27 @@ fn parse_command_inner(args: &[String], flags: &Flags) -> Result { - let key = rest.first().ok_or_else(|| ParseError::MissingArguments { - context: "press".to_string(), - usage: "press ", + let key = rest.iter().find(|a| !a.starts_with("--")).ok_or_else(|| { + ParseError::MissingArguments { + context: "press".to_string(), + usage: "press [--hold ]", + } })?; - Ok(json!({ "id": id, "action": "press", "key": key })) + let mut c = json!({ "id": id, "action": "press", "key": key }); + // `--hold `: hold the key down for then release, timed inside + // the daemon (one round-trip, no shell-sleep jitter) — for games and + // hold-to-charge where keydown+sleep+keyup over 3 round-trips is too + // imprecise. + if let Some(i) = rest.iter().position(|a| *a == "--hold") { + let ms = rest.get(i + 1).and_then(|s| s.parse::().ok()).ok_or( + ParseError::MissingArguments { + context: "press --hold".to_string(), + usage: "press --hold ", + }, + )?; + c["hold"] = json!(ms); + } + Ok(c) } "keydown" => { let key = rest.first().ok_or_else(|| ParseError::MissingArguments { @@ -3581,6 +3597,22 @@ mod tests { assert_eq!(cmd["url"], "https://example.com"); } + #[test] + fn test_press_plain_and_hold() { + let cmd = parse_command(&args("press d"), &default_flags()).unwrap(); + assert_eq!(cmd["action"], "press"); + assert_eq!(cmd["key"], "d"); + assert!(cmd.get("hold").is_none()); + + let held = parse_command(&args("press d --hold 800"), &default_flags()).unwrap(); + assert_eq!(held["key"], "d"); + assert_eq!(held["hold"], 800); + + // Missing/invalid duration is an error, not a silent no-hold. + assert!(parse_command(&args("press d --hold"), &default_flags()).is_err()); + assert!(parse_command(&args("press d --hold abc"), &default_flags()).is_err()); + } + #[test] fn test_navigate_reuse_tab_flag() { let cmd = parse_command( diff --git a/cli/src/native/actions.rs b/cli/src/native/actions.rs index 551e594..7c4c441 100644 --- a/cli/src/native/actions.rs +++ b/cli/src/native/actions.rs @@ -3334,6 +3334,16 @@ async fn handle_press(cmd: &Value, state: &mut DaemonState) -> Result`: keyDown, wait, keyUp — all inside the daemon so the hold + // duration is precise (no shell-sleep / round-trip jitter). For games + // (hold-to-move/charge) and any press-and-hold interaction. + if let Some(ms) = cmd.get("hold").and_then(|v| v.as_u64()) { + interaction::dispatch_single_key(&mgr.client, &session_id, &actual_key, "keyDown").await?; + tokio::time::sleep(std::time::Duration::from_millis(ms)).await; + interaction::dispatch_single_key(&mgr.client, &session_id, &actual_key, "keyUp").await?; + return Ok(json!({ "pressed": key, "heldMs": ms })); + } + interaction::press_key_with_modifiers(&mgr.client, &session_id, &actual_key, modifiers).await?; Ok(json!({ "pressed": key })) } diff --git a/cli/src/output.rs b/cli/src/output.rs index 30d48b5..7f43840 100644 --- a/cli/src/output.rs +++ b/cli/src/output.rs @@ -3070,7 +3070,9 @@ Core Commands: dblclick Double-click element type Type into element fill Clear and fill - press Press key (Enter, Tab, Control+a) + press [--hold ] Press key (Enter, Tab, Control+a). --hold keeps it + down then releases — precise (in-daemon), for + games/charge: `press d --hold 800` keydown Hold a key down (no auto-release) — for games/shortcuts keyup Release a held key. Pair with keydown to hold-to-move: `keydown d` … `keyup d` diff --git a/skill-data/core/SKILL.md b/skill-data/core/SKILL.md index fb07e9e..7d4162b 100644 --- a/skill-data/core/SKILL.md +++ b/skill-data/core/SKILL.md @@ -309,12 +309,26 @@ detects this and prints a one-line hint. Drive them the screenshot way: chrome-use screenshot /tmp/s.png # SEE the state (your only read path — # eval/get text return nothing useful) chrome-use click 640 360 # interact by viewport coordinate -chrome-use keydown d; sleep 0.6; chrome-use keyup d # hold-to-move +chrome-use press d --hold 800 # hold-to-move, precise (timed in-daemon — + # NOT keydown+shell-sleep+keyup, which + # adds ~250ms jitter per round-trip) chrome-use press Space # discrete actions (jump/attack/confirm) ``` -Each command is a ~250ms round-trip, so this is fine for turn-based / canvas -*apps* but too slow to play a real-time 60fps action game frame-by-frame. +**Don't drive frame-by-frame with one CLI call per action** — that's the slowest, +lowest-fidelity way (each call is a process spawn + round-trip). Script a *timed +sequence in a single round-trip* with `batch` (it sends each step to the running +daemon; `press --hold` and `wait` block in-daemon, so timing is precise): + +```bash +chrome-use batch "press d --hold 900" "press j" "press j" "wait 200" "press d --hold 500" +``` + +Also try reading real state instead of pixels: `eval` runs in the page's main +world, so for a framework/engine game you can often reach its globals (e.g. a +Phaser/PIXI/Three instance, a store, `window.__GAME__`) and read positions/score +directly — far better than guessing from a screenshot. Still, a real-time 60fps +action game is not playable frame-perfect over a CLI; expect to script bursts. ## Waiting (read this)