feat: add screenshot output config, clipboard CLI commands, and fix wait --text native path (#749)
* feat: add screenshot output config, clipboard CLI commands, and fix wait --text native path
## Summary
- Add `--screenshot-dir`, `--screenshot-quality`, and `--screenshot-format` CLI flags (with corresponding `AGENT_BROWSER_SCREENSHOT_DIR`, `AGENT_BROWSER_SCREENSHOT_QUALITY`, `AGENT_BROWSER_SCREENSHOT_FORMAT` env vars) so users can configure where and how screenshots are saved without specifying a full path every time
- Add `clipboard read`, `clipboard write <text>`, `clipboard copy`, and `clipboard paste` CLI commands, exposing the existing protocol-level clipboard handlers that were previously only accessible via JSON-RPC
- Fix `wait --text` in native mode: the CLI was emitting `selector: "text=..."` (a Playwright-style locator) which native's `querySelector` can't handle. Now emits a `text` field that correctly hits the native `wait_for_text` polling path
- Add native clipboard `copy` and `paste` support via CDP `Input.dispatchKeyEvent`, and a `write` operation to the Node.js handler
* fix: resolve CI failures in Rust formatting and TypeScript typecheck
Use string-based page.evaluate for clipboard writeText to avoid
referencing `navigator` in Node.js compilation context. Run cargo fmt
to fix formatting in commands.rs and screenshot.rs.
* fix: clipboard write captures full multi-word text
Use rest[1..].join(" ") instead of rest.get(1) so unquoted multi-word
input like `clipboard write hello world` sends the full string rather
than silently dropping everything after the first word.
* improvements
* fixes
* improvements
* improvements
This commit is contained in:
+126
-9
@@ -391,7 +391,7 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result<Value, ParseError
|
||||
return Ok(json!({ "id": id, "action": "waitforfunction", "expression": expr }));
|
||||
}
|
||||
|
||||
// Check for --text flag: wait --text "Welcome"
|
||||
// Check for --text flag: wait --text "Welcome" [--timeout ms]
|
||||
if let Some(idx) = rest.iter().position(|&s| s == "--text" || s == "-t") {
|
||||
let text = rest
|
||||
.get(idx + 1)
|
||||
@@ -399,10 +399,13 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result<Value, ParseError
|
||||
context: "wait --text".to_string(),
|
||||
usage: "wait --text <text>",
|
||||
})?;
|
||||
// Use getByText locator to wait for text to appear
|
||||
return Ok(
|
||||
json!({ "id": id, "action": "wait", "selector": format!("text={}", text) }),
|
||||
);
|
||||
let mut cmd = json!({ "id": id, "action": "wait", "text": text });
|
||||
if let Some(t_idx) = rest.iter().position(|&s| s == "--timeout") {
|
||||
if let Some(Ok(ms)) = rest.get(t_idx + 1).map(|s| s.parse::<u64>()) {
|
||||
cmd["timeout"] = json!(ms);
|
||||
}
|
||||
}
|
||||
return Ok(cmd);
|
||||
}
|
||||
|
||||
// Check for --download flag: wait --download [path] [--timeout ms]
|
||||
@@ -474,9 +477,27 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result<Value, ParseError
|
||||
}
|
||||
_ => (None, None),
|
||||
};
|
||||
Ok(
|
||||
json!({ "id": id, "action": "screenshot", "path": path, "selector": selector, "fullPage": flags.full, "annotate": flags.annotate }),
|
||||
)
|
||||
let mut cmd = json!({
|
||||
"id": id, "action": "screenshot",
|
||||
"path": path, "selector": selector,
|
||||
"fullPage": flags.full, "annotate": flags.annotate
|
||||
});
|
||||
if let Some(ref fmt) = flags.screenshot_format {
|
||||
cmd["format"] = json!(fmt);
|
||||
}
|
||||
if let Some(q) = flags.screenshot_quality {
|
||||
cmd["quality"] = json!(q);
|
||||
if flags.screenshot_format.as_deref() != Some("jpeg") {
|
||||
eprintln!(
|
||||
"{} --screenshot-quality is ignored for PNG; use --screenshot-format jpeg",
|
||||
color::warning_indicator()
|
||||
);
|
||||
}
|
||||
}
|
||||
if let Some(ref dir) = flags.screenshot_dir {
|
||||
cmd["screenshotDir"] = json!(dir);
|
||||
}
|
||||
Ok(cmd)
|
||||
}
|
||||
"pdf" => {
|
||||
let path = rest.first().ok_or_else(|| ParseError::MissingArguments {
|
||||
@@ -1109,6 +1130,31 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result<Value, ParseError
|
||||
Ok(json!({ "id": id, "action": "highlight", "selector": sel }))
|
||||
}
|
||||
|
||||
// === Clipboard ===
|
||||
"clipboard" => match rest.first().copied() {
|
||||
Some("read") | None => {
|
||||
Ok(json!({ "id": id, "action": "clipboard", "operation": "read" }))
|
||||
}
|
||||
Some("write") => {
|
||||
rest.get(1).ok_or_else(|| ParseError::MissingArguments {
|
||||
context: "clipboard write".to_string(),
|
||||
usage: "clipboard write <text>",
|
||||
})?;
|
||||
let text = rest[1..].join(" ");
|
||||
Ok(
|
||||
json!({ "id": id, "action": "clipboard", "operation": "write", "text": text }),
|
||||
)
|
||||
}
|
||||
Some("copy") => Ok(json!({ "id": id, "action": "clipboard", "operation": "copy" })),
|
||||
Some("paste") => {
|
||||
Ok(json!({ "id": id, "action": "clipboard", "operation": "paste" }))
|
||||
}
|
||||
Some(sub) => Err(ParseError::UnknownSubcommand {
|
||||
subcommand: sub.to_string(),
|
||||
valid_options: &["read", "write", "copy", "paste"],
|
||||
}),
|
||||
},
|
||||
|
||||
// === State ===
|
||||
"state" => {
|
||||
const VALID: &[&str] = &["save", "load", "list", "clear", "show", "clean", "rename"];
|
||||
@@ -2133,6 +2179,9 @@ mod tests {
|
||||
confirm_interactive: false,
|
||||
native: false,
|
||||
engine: None,
|
||||
screenshot_dir: None,
|
||||
screenshot_quality: None,
|
||||
screenshot_format: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2749,7 +2798,75 @@ mod tests {
|
||||
fn test_wait_text() {
|
||||
let cmd = parse_command(&args("wait --text Welcome"), &default_flags()).unwrap();
|
||||
assert_eq!(cmd["action"], "wait");
|
||||
assert_eq!(cmd["selector"], "text=Welcome");
|
||||
assert_eq!(cmd["text"], "Welcome");
|
||||
assert!(cmd.get("timeout").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_wait_text_with_timeout() {
|
||||
let cmd =
|
||||
parse_command(&args("wait --text Welcome --timeout 5000"), &default_flags()).unwrap();
|
||||
assert_eq!(cmd["action"], "wait");
|
||||
assert_eq!(cmd["text"], "Welcome");
|
||||
assert_eq!(cmd["timeout"], 5000);
|
||||
}
|
||||
|
||||
// === Clipboard Tests ===
|
||||
|
||||
#[test]
|
||||
fn test_clipboard_read_default() {
|
||||
let cmd = parse_command(&args("clipboard"), &default_flags()).unwrap();
|
||||
assert_eq!(cmd["action"], "clipboard");
|
||||
assert_eq!(cmd["operation"], "read");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_clipboard_read_explicit() {
|
||||
let cmd = parse_command(&args("clipboard read"), &default_flags()).unwrap();
|
||||
assert_eq!(cmd["action"], "clipboard");
|
||||
assert_eq!(cmd["operation"], "read");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_clipboard_write() {
|
||||
let cmd = parse_command(&args("clipboard write hello"), &default_flags()).unwrap();
|
||||
assert_eq!(cmd["action"], "clipboard");
|
||||
assert_eq!(cmd["operation"], "write");
|
||||
assert_eq!(cmd["text"], "hello");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_clipboard_write_multi_word() {
|
||||
let cmd = parse_command(&args("clipboard write hello world"), &default_flags()).unwrap();
|
||||
assert_eq!(cmd["action"], "clipboard");
|
||||
assert_eq!(cmd["operation"], "write");
|
||||
assert_eq!(cmd["text"], "hello world");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_clipboard_copy() {
|
||||
let cmd = parse_command(&args("clipboard copy"), &default_flags()).unwrap();
|
||||
assert_eq!(cmd["action"], "clipboard");
|
||||
assert_eq!(cmd["operation"], "copy");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_clipboard_paste() {
|
||||
let cmd = parse_command(&args("clipboard paste"), &default_flags()).unwrap();
|
||||
assert_eq!(cmd["action"], "clipboard");
|
||||
assert_eq!(cmd["operation"], "paste");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_clipboard_write_missing_text() {
|
||||
let result = parse_command(&args("clipboard write"), &default_flags());
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_clipboard_unknown_subcommand() {
|
||||
let result = parse_command(&args("clipboard clear"), &default_flags());
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
// === Unknown command ===
|
||||
|
||||
Reference in New Issue
Block a user