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:
@@ -115,6 +115,8 @@ agent-browser drag <src> <tgt> # Drag and drop
|
|||||||
agent-browser upload <sel> <files> # Upload files
|
agent-browser upload <sel> <files> # Upload files
|
||||||
agent-browser screenshot [path] # Take screenshot (--full for full page, saves to a temporary directory if no path)
|
agent-browser screenshot [path] # Take screenshot (--full for full page, saves to a temporary directory if no path)
|
||||||
agent-browser screenshot --annotate # Annotated screenshot with numbered element labels
|
agent-browser screenshot --annotate # Annotated screenshot with numbered element labels
|
||||||
|
agent-browser screenshot --screenshot-dir ./shots # Save to custom directory
|
||||||
|
agent-browser screenshot --screenshot-format jpeg --screenshot-quality 80
|
||||||
agent-browser pdf <path> # Save as PDF
|
agent-browser pdf <path> # Save as PDF
|
||||||
agent-browser snapshot # Accessibility tree with refs (best for AI)
|
agent-browser snapshot # Accessibility tree with refs (best for AI)
|
||||||
agent-browser eval <js> # Run JavaScript (-b for base64, --stdin for piped input)
|
agent-browser eval <js> # Run JavaScript (-b for base64, --stdin for piped input)
|
||||||
@@ -179,14 +181,27 @@ agent-browser find nth 2 "a" text
|
|||||||
```bash
|
```bash
|
||||||
agent-browser wait <selector> # Wait for element to be visible
|
agent-browser wait <selector> # Wait for element to be visible
|
||||||
agent-browser wait <ms> # Wait for time (milliseconds)
|
agent-browser wait <ms> # Wait for time (milliseconds)
|
||||||
agent-browser wait --text "Welcome" # Wait for text to appear
|
agent-browser wait --text "Welcome" # Wait for text to appear (substring match)
|
||||||
agent-browser wait --url "**/dash" # Wait for URL pattern
|
agent-browser wait --url "**/dash" # Wait for URL pattern
|
||||||
agent-browser wait --load networkidle # Wait for load state
|
agent-browser wait --load networkidle # Wait for load state
|
||||||
agent-browser wait --fn "window.ready === true" # Wait for JS condition
|
agent-browser wait --fn "window.ready === true" # Wait for JS condition
|
||||||
|
|
||||||
|
# Wait for text/element to disappear
|
||||||
|
agent-browser wait --fn "!document.body.innerText.includes('Loading...')"
|
||||||
|
agent-browser wait "#spinner" --state hidden
|
||||||
```
|
```
|
||||||
|
|
||||||
**Load states:** `load`, `domcontentloaded`, `networkidle`
|
**Load states:** `load`, `domcontentloaded`, `networkidle`
|
||||||
|
|
||||||
|
### Clipboard
|
||||||
|
|
||||||
|
```bash
|
||||||
|
agent-browser clipboard read # Read text from clipboard
|
||||||
|
agent-browser clipboard write "Hello, World!" # Write text to clipboard
|
||||||
|
agent-browser clipboard copy # Copy current selection (Ctrl+C)
|
||||||
|
agent-browser clipboard paste # Paste from clipboard (Ctrl+V)
|
||||||
|
```
|
||||||
|
|
||||||
### Mouse Control
|
### Mouse Control
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -532,6 +547,9 @@ This is useful for multimodal AI models that can reason about visual layout, unl
|
|||||||
| `--json` | JSON output (for agents) |
|
| `--json` | JSON output (for agents) |
|
||||||
| `--full, -f` | Full page screenshot |
|
| `--full, -f` | Full page screenshot |
|
||||||
| `--annotate` | Annotated screenshot with numbered element labels (or `AGENT_BROWSER_ANNOTATE` env) |
|
| `--annotate` | Annotated screenshot with numbered element labels (or `AGENT_BROWSER_ANNOTATE` env) |
|
||||||
|
| `--screenshot-dir <path>` | Default screenshot output directory (or `AGENT_BROWSER_SCREENSHOT_DIR` env) |
|
||||||
|
| `--screenshot-quality <n>` | JPEG quality 0-100 (or `AGENT_BROWSER_SCREENSHOT_QUALITY` env) |
|
||||||
|
| `--screenshot-format <fmt>` | Screenshot format: `png`, `jpeg` (or `AGENT_BROWSER_SCREENSHOT_FORMAT` env) |
|
||||||
| `--headed` | Show browser window (not headless) (or `AGENT_BROWSER_HEADED` env) |
|
| `--headed` | Show browser window (not headless) (or `AGENT_BROWSER_HEADED` env) |
|
||||||
| `--cdp <port\|url>` | Connect via Chrome DevTools Protocol (port or WebSocket URL) |
|
| `--cdp <port\|url>` | Connect via Chrome DevTools Protocol (port or WebSocket URL) |
|
||||||
| `--auto-connect` | Auto-discover and connect to running Chrome (or `AGENT_BROWSER_AUTO_CONNECT` env) |
|
| `--auto-connect` | Auto-discover and connect to running Chrome (or `AGENT_BROWSER_AUTO_CONNECT` env) |
|
||||||
|
|||||||
+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 }));
|
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") {
|
if let Some(idx) = rest.iter().position(|&s| s == "--text" || s == "-t") {
|
||||||
let text = rest
|
let text = rest
|
||||||
.get(idx + 1)
|
.get(idx + 1)
|
||||||
@@ -399,10 +399,13 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result<Value, ParseError
|
|||||||
context: "wait --text".to_string(),
|
context: "wait --text".to_string(),
|
||||||
usage: "wait --text <text>",
|
usage: "wait --text <text>",
|
||||||
})?;
|
})?;
|
||||||
// Use getByText locator to wait for text to appear
|
let mut cmd = json!({ "id": id, "action": "wait", "text": text });
|
||||||
return Ok(
|
if let Some(t_idx) = rest.iter().position(|&s| s == "--timeout") {
|
||||||
json!({ "id": id, "action": "wait", "selector": format!("text={}", text) }),
|
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]
|
// 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),
|
_ => (None, None),
|
||||||
};
|
};
|
||||||
Ok(
|
let mut cmd = json!({
|
||||||
json!({ "id": id, "action": "screenshot", "path": path, "selector": selector, "fullPage": flags.full, "annotate": flags.annotate }),
|
"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" => {
|
"pdf" => {
|
||||||
let path = rest.first().ok_or_else(|| ParseError::MissingArguments {
|
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 }))
|
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 ===
|
||||||
"state" => {
|
"state" => {
|
||||||
const VALID: &[&str] = &["save", "load", "list", "clear", "show", "clean", "rename"];
|
const VALID: &[&str] = &["save", "load", "list", "clear", "show", "clean", "rename"];
|
||||||
@@ -2133,6 +2179,9 @@ mod tests {
|
|||||||
confirm_interactive: false,
|
confirm_interactive: false,
|
||||||
native: false,
|
native: false,
|
||||||
engine: None,
|
engine: None,
|
||||||
|
screenshot_dir: None,
|
||||||
|
screenshot_quality: None,
|
||||||
|
screenshot_format: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2749,7 +2798,75 @@ mod tests {
|
|||||||
fn test_wait_text() {
|
fn test_wait_text() {
|
||||||
let cmd = parse_command(&args("wait --text Welcome"), &default_flags()).unwrap();
|
let cmd = parse_command(&args("wait --text Welcome"), &default_flags()).unwrap();
|
||||||
assert_eq!(cmd["action"], "wait");
|
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 ===
|
// === Unknown command ===
|
||||||
|
|||||||
@@ -43,6 +43,9 @@ pub struct Config {
|
|||||||
pub confirm_interactive: Option<bool>,
|
pub confirm_interactive: Option<bool>,
|
||||||
pub native: Option<bool>,
|
pub native: Option<bool>,
|
||||||
pub engine: Option<String>,
|
pub engine: Option<String>,
|
||||||
|
pub screenshot_dir: Option<String>,
|
||||||
|
pub screenshot_quality: Option<u32>,
|
||||||
|
pub screenshot_format: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
@@ -86,6 +89,9 @@ impl Config {
|
|||||||
confirm_interactive: other.confirm_interactive.or(self.confirm_interactive),
|
confirm_interactive: other.confirm_interactive.or(self.confirm_interactive),
|
||||||
native: other.native.or(self.native),
|
native: other.native.or(self.native),
|
||||||
engine: other.engine.or(self.engine),
|
engine: other.engine.or(self.engine),
|
||||||
|
screenshot_dir: other.screenshot_dir.or(self.screenshot_dir),
|
||||||
|
screenshot_quality: other.screenshot_quality.or(self.screenshot_quality),
|
||||||
|
screenshot_format: other.screenshot_format.or(self.screenshot_format),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -161,6 +167,9 @@ fn extract_config_path(args: &[String]) -> Option<Option<String>> {
|
|||||||
"--action-policy",
|
"--action-policy",
|
||||||
"--confirm-actions",
|
"--confirm-actions",
|
||||||
"--engine",
|
"--engine",
|
||||||
|
"--screenshot-dir",
|
||||||
|
"--screenshot-quality",
|
||||||
|
"--screenshot-format",
|
||||||
];
|
];
|
||||||
let mut i = 0;
|
let mut i = 0;
|
||||||
while i < args.len() {
|
while i < args.len() {
|
||||||
@@ -240,6 +249,9 @@ pub struct Flags {
|
|||||||
pub confirm_interactive: bool,
|
pub confirm_interactive: bool,
|
||||||
pub native: bool,
|
pub native: bool,
|
||||||
pub engine: Option<String>,
|
pub engine: Option<String>,
|
||||||
|
pub screenshot_dir: Option<String>,
|
||||||
|
pub screenshot_quality: Option<u32>,
|
||||||
|
pub screenshot_format: Option<String>,
|
||||||
|
|
||||||
// Track which launch-time options were explicitly passed via CLI
|
// Track which launch-time options were explicitly passed via CLI
|
||||||
// (as opposed to being set only via environment variables)
|
// (as opposed to being set only via environment variables)
|
||||||
@@ -347,6 +359,17 @@ pub fn parse_flags(args: &[String]) -> Flags {
|
|||||||
|| config.confirm_interactive.unwrap_or(false),
|
|| config.confirm_interactive.unwrap_or(false),
|
||||||
native: env_var_is_truthy("AGENT_BROWSER_NATIVE") || config.native.unwrap_or(false),
|
native: env_var_is_truthy("AGENT_BROWSER_NATIVE") || config.native.unwrap_or(false),
|
||||||
engine: env::var("AGENT_BROWSER_ENGINE").ok().or(config.engine),
|
engine: env::var("AGENT_BROWSER_ENGINE").ok().or(config.engine),
|
||||||
|
screenshot_dir: env::var("AGENT_BROWSER_SCREENSHOT_DIR")
|
||||||
|
.ok()
|
||||||
|
.or(config.screenshot_dir),
|
||||||
|
screenshot_quality: env::var("AGENT_BROWSER_SCREENSHOT_QUALITY")
|
||||||
|
.ok()
|
||||||
|
.and_then(|s| s.parse().ok())
|
||||||
|
.or(config.screenshot_quality),
|
||||||
|
screenshot_format: env::var("AGENT_BROWSER_SCREENSHOT_FORMAT")
|
||||||
|
.ok()
|
||||||
|
.or(config.screenshot_format)
|
||||||
|
.filter(|s| s == "png" || s == "jpeg"),
|
||||||
cli_executable_path: false,
|
cli_executable_path: false,
|
||||||
cli_extensions: false,
|
cli_extensions: false,
|
||||||
cli_profile: false,
|
cli_profile: false,
|
||||||
@@ -586,6 +609,42 @@ pub fn parse_flags(args: &[String]) -> Flags {
|
|||||||
i += 1;
|
i += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"--screenshot-dir" => {
|
||||||
|
if let Some(s) = args.get(i + 1) {
|
||||||
|
flags.screenshot_dir = Some(s.clone());
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"--screenshot-quality" => {
|
||||||
|
if let Some(s) = args.get(i + 1) {
|
||||||
|
if let Ok(n) = s.parse::<u32>() {
|
||||||
|
if n <= 100 {
|
||||||
|
flags.screenshot_quality = Some(n);
|
||||||
|
} else {
|
||||||
|
eprintln!(
|
||||||
|
"{} --screenshot-quality must be 0-100, got {}",
|
||||||
|
color::warning_indicator(),
|
||||||
|
n
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"--screenshot-format" => {
|
||||||
|
if let Some(s) = args.get(i + 1) {
|
||||||
|
if s == "png" || s == "jpeg" {
|
||||||
|
flags.screenshot_format = Some(s.clone());
|
||||||
|
} else {
|
||||||
|
eprintln!(
|
||||||
|
"{} --screenshot-format must be png or jpeg, got '{}'",
|
||||||
|
color::warning_indicator(),
|
||||||
|
s
|
||||||
|
);
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
"--config" => {
|
"--config" => {
|
||||||
// Already handled by load_config(); skip the value
|
// Already handled by load_config(); skip the value
|
||||||
i += 1;
|
i += 1;
|
||||||
@@ -640,6 +699,9 @@ pub fn clean_args(args: &[String]) -> Vec<String> {
|
|||||||
"--confirm-actions",
|
"--confirm-actions",
|
||||||
"--config",
|
"--config",
|
||||||
"--engine",
|
"--engine",
|
||||||
|
"--screenshot-dir",
|
||||||
|
"--screenshot-quality",
|
||||||
|
"--screenshot-format",
|
||||||
];
|
];
|
||||||
|
|
||||||
let mut i = 0;
|
let mut i = 0;
|
||||||
|
|||||||
@@ -1415,6 +1415,10 @@ async fn handle_screenshot(cmd: &Value, state: &mut DaemonState) -> Result<Value
|
|||||||
.and_then(|v| v.as_i64())
|
.and_then(|v| v.as_i64())
|
||||||
.map(|q| q as i32),
|
.map(|q| q as i32),
|
||||||
annotate,
|
annotate,
|
||||||
|
output_dir: cmd
|
||||||
|
.get("screenshotDir")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.map(String::from),
|
||||||
};
|
};
|
||||||
|
|
||||||
if annotate {
|
if annotate {
|
||||||
@@ -1641,6 +1645,11 @@ async fn handle_wait(cmd: &Value, state: &mut DaemonState) -> Result<Value, Stri
|
|||||||
let session_id = mgr.active_session_id()?.to_string();
|
let session_id = mgr.active_session_id()?.to_string();
|
||||||
let timeout_ms = cmd.get("timeout").and_then(|v| v.as_u64()).unwrap_or(30000);
|
let timeout_ms = cmd.get("timeout").and_then(|v| v.as_u64()).unwrap_or(30000);
|
||||||
|
|
||||||
|
if let Some(text) = cmd.get("text").and_then(|v| v.as_str()) {
|
||||||
|
wait_for_text(&mgr.client, &session_id, text, timeout_ms).await?;
|
||||||
|
return Ok(json!({ "waited": "text", "text": text }));
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(selector) = cmd.get("selector").and_then(|v| v.as_str()) {
|
if let Some(selector) = cmd.get("selector").and_then(|v| v.as_str()) {
|
||||||
let state_str = cmd
|
let state_str = cmd
|
||||||
.get("state")
|
.get("state")
|
||||||
@@ -1655,11 +1664,6 @@ async fn handle_wait(cmd: &Value, state: &mut DaemonState) -> Result<Value, Stri
|
|||||||
return Ok(json!({ "waited": "url", "url": url_pattern }));
|
return Ok(json!({ "waited": "url", "url": url_pattern }));
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(text) = cmd.get("text").and_then(|v| v.as_str()) {
|
|
||||||
wait_for_text(&mgr.client, &session_id, text, timeout_ms).await?;
|
|
||||||
return Ok(json!({ "waited": "text", "text": text }));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(fn_str) = cmd.get("function").and_then(|v| v.as_str()) {
|
if let Some(fn_str) = cmd.get("function").and_then(|v| v.as_str()) {
|
||||||
wait_for_function(&mgr.client, &session_id, fn_str, timeout_ms).await?;
|
wait_for_function(&mgr.client, &session_id, fn_str, timeout_ms).await?;
|
||||||
return Ok(json!({ "waited": "function" }));
|
return Ok(json!({ "waited": "function" }));
|
||||||
@@ -3107,8 +3111,13 @@ async fn handle_clipboard(cmd: &Value, state: &DaemonState) -> Result<Value, Str
|
|||||||
.and_then(|v| v.as_str())
|
.and_then(|v| v.as_str())
|
||||||
.unwrap_or("read");
|
.unwrap_or("read");
|
||||||
|
|
||||||
|
let session_id = mgr.active_session_id()?.to_string();
|
||||||
|
|
||||||
|
// cfg! is compile-time; assumes the browser runs on the same OS as the CLI binary.
|
||||||
|
let modifier: i32 = if cfg!(target_os = "macos") { 4 } else { 2 };
|
||||||
|
|
||||||
match action {
|
match action {
|
||||||
"write" | "copy" => {
|
"write" => {
|
||||||
let text = cmd
|
let text = cmd
|
||||||
.get("text")
|
.get("text")
|
||||||
.or_else(|| cmd.get("value"))
|
.or_else(|| cmd.get("value"))
|
||||||
@@ -3119,7 +3128,17 @@ async fn handle_clipboard(cmd: &Value, state: &DaemonState) -> Result<Value, Str
|
|||||||
serde_json::to_string(text).unwrap_or_default()
|
serde_json::to_string(text).unwrap_or_default()
|
||||||
);
|
);
|
||||||
mgr.evaluate(&js, None).await?;
|
mgr.evaluate(&js, None).await?;
|
||||||
Ok(json!({ "copied": text }))
|
Ok(json!({ "written": text }))
|
||||||
|
}
|
||||||
|
"copy" => {
|
||||||
|
interaction::press_key_with_modifiers(&mgr.client, &session_id, "c", Some(modifier))
|
||||||
|
.await?;
|
||||||
|
Ok(json!({ "copied": true }))
|
||||||
|
}
|
||||||
|
"paste" => {
|
||||||
|
interaction::press_key_with_modifiers(&mgr.client, &session_id, "v", Some(modifier))
|
||||||
|
.await?;
|
||||||
|
Ok(json!({ "pasted": true }))
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
let result = mgr.evaluate("navigator.clipboard.readText()", None).await?;
|
let result = mgr.evaluate("navigator.clipboard.readText()", None).await?;
|
||||||
@@ -4190,6 +4209,7 @@ async fn handle_diff_screenshot(cmd: &Value, state: &DaemonState) -> Result<Valu
|
|||||||
format: "png".to_string(),
|
format: "png".to_string(),
|
||||||
quality: None,
|
quality: None,
|
||||||
annotate: false,
|
annotate: false,
|
||||||
|
output_dir: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let result =
|
let result =
|
||||||
|
|||||||
@@ -206,6 +206,22 @@ pub async fn type_text(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn press_key(client: &CdpClient, session_id: &str, key: &str) -> Result<(), String> {
|
pub async fn press_key(client: &CdpClient, session_id: &str, key: &str) -> Result<(), String> {
|
||||||
|
press_key_with_modifiers(client, session_id, key, None).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Dispatch a keyDown+keyUp sequence for `key` with an optional CDP modifier bitmask.
|
||||||
|
///
|
||||||
|
/// Modifier values follow the CDP `Input.dispatchKeyEvent` spec:
|
||||||
|
/// 1 = Alt, 2 = Control, 4 = Meta (Cmd), 8 = Shift.
|
||||||
|
///
|
||||||
|
/// Callers that need a platform-appropriate modifier (e.g. Cmd on macOS,
|
||||||
|
/// Ctrl elsewhere) must choose the value themselves -- see `cfg!(target_os)`.
|
||||||
|
pub async fn press_key_with_modifiers(
|
||||||
|
client: &CdpClient,
|
||||||
|
session_id: &str,
|
||||||
|
key: &str,
|
||||||
|
modifiers: Option<i32>,
|
||||||
|
) -> Result<(), String> {
|
||||||
let (key_name, code, key_code) = named_key_info(key);
|
let (key_name, code, key_code) = named_key_info(key);
|
||||||
|
|
||||||
client
|
client
|
||||||
@@ -219,7 +235,7 @@ pub async fn press_key(client: &CdpClient, session_id: &str, key: &str) -> Resul
|
|||||||
unmodified_text: None,
|
unmodified_text: None,
|
||||||
windows_virtual_key_code: Some(key_code),
|
windows_virtual_key_code: Some(key_code),
|
||||||
native_virtual_key_code: Some(key_code),
|
native_virtual_key_code: Some(key_code),
|
||||||
modifiers: None,
|
modifiers,
|
||||||
},
|
},
|
||||||
Some(session_id),
|
Some(session_id),
|
||||||
)
|
)
|
||||||
@@ -236,7 +252,7 @@ pub async fn press_key(client: &CdpClient, session_id: &str, key: &str) -> Resul
|
|||||||
unmodified_text: None,
|
unmodified_text: None,
|
||||||
windows_virtual_key_code: Some(key_code),
|
windows_virtual_key_code: Some(key_code),
|
||||||
native_virtual_key_code: Some(key_code),
|
native_virtual_key_code: Some(key_code),
|
||||||
modifiers: None,
|
modifiers,
|
||||||
},
|
},
|
||||||
Some(session_id),
|
Some(session_id),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ pub struct ScreenshotOptions {
|
|||||||
pub format: String,
|
pub format: String,
|
||||||
pub quality: Option<i32>,
|
pub quality: Option<i32>,
|
||||||
pub annotate: bool,
|
pub annotate: bool,
|
||||||
|
pub output_dir: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ScreenshotOptions {
|
impl Default for ScreenshotOptions {
|
||||||
@@ -68,6 +69,7 @@ impl Default for ScreenshotOptions {
|
|||||||
format: "png".to_string(),
|
format: "png".to_string(),
|
||||||
quality: None,
|
quality: None,
|
||||||
annotate: false,
|
annotate: false,
|
||||||
|
output_dir: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -145,7 +147,12 @@ pub async fn take_screenshot(
|
|||||||
} else {
|
} else {
|
||||||
"png"
|
"png"
|
||||||
};
|
};
|
||||||
let path = save_screenshot(&base64, options.path.as_deref(), ext)?;
|
let path = save_screenshot(
|
||||||
|
&base64,
|
||||||
|
options.path.as_deref(),
|
||||||
|
ext,
|
||||||
|
options.output_dir.as_deref(),
|
||||||
|
)?;
|
||||||
|
|
||||||
Ok(ScreenshotResult {
|
Ok(ScreenshotResult {
|
||||||
path,
|
path,
|
||||||
@@ -479,11 +486,15 @@ fn save_screenshot(
|
|||||||
base64_data: &str,
|
base64_data: &str,
|
||||||
explicit_path: Option<&str>,
|
explicit_path: Option<&str>,
|
||||||
ext: &str,
|
ext: &str,
|
||||||
|
output_dir: Option<&str>,
|
||||||
) -> Result<String, String> {
|
) -> Result<String, String> {
|
||||||
let save_path = match explicit_path {
|
let save_path = match explicit_path {
|
||||||
Some(path) => path.to_string(),
|
Some(path) => path.to_string(),
|
||||||
None => {
|
None => {
|
||||||
let dir = get_screenshot_dir();
|
let dir = match output_dir {
|
||||||
|
Some(d) => PathBuf::from(d),
|
||||||
|
None => get_screenshot_dir(),
|
||||||
|
};
|
||||||
let _ = std::fs::create_dir_all(&dir);
|
let _ = std::fs::create_dir_all(&dir);
|
||||||
let timestamp = std::time::SystemTime::now()
|
let timestamp = std::time::SystemTime::now()
|
||||||
.duration_since(std::time::UNIX_EPOCH)
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
|||||||
+51
-2
@@ -1313,12 +1313,18 @@ Modes:
|
|||||||
--url <pattern> Wait for URL to match pattern
|
--url <pattern> Wait for URL to match pattern
|
||||||
--load <state> Wait for load state (load, domcontentloaded, networkidle)
|
--load <state> Wait for load state (load, domcontentloaded, networkidle)
|
||||||
--fn <expression> Wait for JavaScript expression to be truthy
|
--fn <expression> Wait for JavaScript expression to be truthy
|
||||||
--text <text> Wait for text to appear on page
|
--text <text> Wait for text to appear on page (substring match)
|
||||||
--download [path] Wait for a download to complete (optionally save to path)
|
--download [path] Wait for a download to complete (optionally save to path)
|
||||||
|
|
||||||
Download Options (with --download):
|
Download Options (with --download):
|
||||||
--timeout <ms> Timeout in milliseconds for download to start
|
--timeout <ms> Timeout in milliseconds for download to start
|
||||||
|
|
||||||
|
Wait for text to disappear:
|
||||||
|
Use --fn or --state hidden to wait for text or elements to go away:
|
||||||
|
wait --fn "!document.body.innerText.includes('Loading...')"
|
||||||
|
wait "#spinner" --state hidden
|
||||||
|
wait @e5 --state detached
|
||||||
|
|
||||||
Global Options:
|
Global Options:
|
||||||
--json Output as JSON
|
--json Output as JSON
|
||||||
--session <name> Use specific session
|
--session <name> Use specific session
|
||||||
@@ -1332,6 +1338,7 @@ Examples:
|
|||||||
agent-browser wait --text "Welcome back"
|
agent-browser wait --text "Welcome back"
|
||||||
agent-browser wait --download ./file.pdf
|
agent-browser wait --download ./file.pdf
|
||||||
agent-browser wait --download ./report.xlsx --timeout 30000
|
agent-browser wait --download ./report.xlsx --timeout 30000
|
||||||
|
agent-browser wait --fn "!document.body.innerText.includes('Loading...')"
|
||||||
"##
|
"##
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1340,7 +1347,7 @@ Examples:
|
|||||||
r##"
|
r##"
|
||||||
agent-browser screenshot - Take a screenshot
|
agent-browser screenshot - Take a screenshot
|
||||||
|
|
||||||
Usage: agent-browser screenshot [path]
|
Usage: agent-browser screenshot [selector] [path]
|
||||||
|
|
||||||
Captures a screenshot of the current page. If no path is provided,
|
Captures a screenshot of the current page. If no path is provided,
|
||||||
saves to a temporary directory with a generated filename.
|
saves to a temporary directory with a generated filename.
|
||||||
@@ -1353,6 +1360,12 @@ Options:
|
|||||||
With --json, annotations are included in the response.
|
With --json, annotations are included in the response.
|
||||||
In native mode, this is currently supported on the
|
In native mode, this is currently supported on the
|
||||||
CDP-backed browser path (Chromium/Lightpanda).
|
CDP-backed browser path (Chromium/Lightpanda).
|
||||||
|
--screenshot-dir <path> Default output directory for screenshots
|
||||||
|
(or AGENT_BROWSER_SCREENSHOT_DIR env)
|
||||||
|
--screenshot-quality <0-100> JPEG quality (0-100, only applies to jpeg format)
|
||||||
|
(or AGENT_BROWSER_SCREENSHOT_QUALITY env)
|
||||||
|
--screenshot-format <fmt> Image format: png (default) or jpeg
|
||||||
|
(or AGENT_BROWSER_SCREENSHOT_FORMAT env)
|
||||||
|
|
||||||
Global Options:
|
Global Options:
|
||||||
--json Output as JSON
|
--json Output as JSON
|
||||||
@@ -1365,6 +1378,8 @@ Examples:
|
|||||||
agent-browser screenshot --annotate # Labeled screenshot + legend
|
agent-browser screenshot --annotate # Labeled screenshot + legend
|
||||||
agent-browser screenshot --annotate ./page.png # Save annotated screenshot
|
agent-browser screenshot --annotate ./page.png # Save annotated screenshot
|
||||||
agent-browser screenshot --annotate --json # JSON output with annotations
|
agent-browser screenshot --annotate --json # JSON output with annotations
|
||||||
|
agent-browser screenshot --screenshot-dir ./shots # Save to custom directory
|
||||||
|
agent-browser screenshot --screenshot-format jpeg --screenshot-quality 80
|
||||||
"##
|
"##
|
||||||
}
|
}
|
||||||
"pdf" => {
|
"pdf" => {
|
||||||
@@ -2097,6 +2112,33 @@ Examples:
|
|||||||
"##
|
"##
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// === Clipboard ===
|
||||||
|
"clipboard" => {
|
||||||
|
r##"
|
||||||
|
agent-browser clipboard - Read and write clipboard
|
||||||
|
|
||||||
|
Usage: agent-browser clipboard <operation> [text]
|
||||||
|
|
||||||
|
Read from or write to the browser clipboard.
|
||||||
|
|
||||||
|
Operations:
|
||||||
|
read Read text from clipboard
|
||||||
|
write <text> Write text to clipboard
|
||||||
|
copy Copy current selection (simulates Ctrl+C)
|
||||||
|
paste Paste from clipboard (simulates Ctrl+V)
|
||||||
|
|
||||||
|
Global Options:
|
||||||
|
--json Output as JSON
|
||||||
|
--session <name> Use specific session
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
agent-browser clipboard read
|
||||||
|
agent-browser clipboard write "Hello, World!"
|
||||||
|
agent-browser clipboard copy
|
||||||
|
agent-browser clipboard paste
|
||||||
|
"##
|
||||||
|
}
|
||||||
|
|
||||||
// === State ===
|
// === State ===
|
||||||
"state" => {
|
"state" => {
|
||||||
r##"
|
r##"
|
||||||
@@ -2434,6 +2476,7 @@ Debug:
|
|||||||
errors [--clear] View page errors
|
errors [--clear] View page errors
|
||||||
highlight <sel> Highlight element
|
highlight <sel> Highlight element
|
||||||
inspect Open Chrome DevTools for the active page
|
inspect Open Chrome DevTools for the active page
|
||||||
|
clipboard <op> [text] Read/write clipboard (read, write, copy, paste)
|
||||||
|
|
||||||
Auth Vault:
|
Auth Vault:
|
||||||
auth save <name> [opts] Save auth profile (--url, --username, --password/--password-stdin)
|
auth save <name> [opts] Save auth profile (--url, --username, --password/--password-stdin)
|
||||||
@@ -2489,6 +2532,9 @@ Options:
|
|||||||
--json JSON output
|
--json JSON output
|
||||||
--full, -f Full page screenshot
|
--full, -f Full page screenshot
|
||||||
--annotate Annotated screenshot with numbered labels and legend
|
--annotate Annotated screenshot with numbered labels and legend
|
||||||
|
--screenshot-dir <path> Default screenshot output directory (or AGENT_BROWSER_SCREENSHOT_DIR)
|
||||||
|
--screenshot-quality <n> JPEG quality 0-100; ignored for PNG (or AGENT_BROWSER_SCREENSHOT_QUALITY)
|
||||||
|
--screenshot-format <fmt> Screenshot format: png, jpeg (or AGENT_BROWSER_SCREENSHOT_FORMAT)
|
||||||
--headed Show browser window (not headless) (or AGENT_BROWSER_HEADED env)
|
--headed Show browser window (not headless) (or AGENT_BROWSER_HEADED env)
|
||||||
--cdp <port> Connect via CDP (Chrome DevTools Protocol)
|
--cdp <port> Connect via CDP (Chrome DevTools Protocol)
|
||||||
--color-scheme <scheme> Color scheme: dark, light, no-preference (or AGENT_BROWSER_COLOR_SCHEME)
|
--color-scheme <scheme> Color scheme: dark, light, no-preference (or AGENT_BROWSER_COLOR_SCHEME)
|
||||||
@@ -2558,6 +2604,9 @@ Environment:
|
|||||||
AGENT_BROWSER_CONFIRM_INTERACTIVE Enable interactive confirmation prompts
|
AGENT_BROWSER_CONFIRM_INTERACTIVE Enable interactive confirmation prompts
|
||||||
AGENT_BROWSER_ENGINE Browser engine: chrome (default), lightpanda
|
AGENT_BROWSER_ENGINE Browser engine: chrome (default), lightpanda
|
||||||
AGENT_BROWSER_NATIVE Use native Rust daemon (experimental, no Node.js/Playwright)
|
AGENT_BROWSER_NATIVE Use native Rust daemon (experimental, no Node.js/Playwright)
|
||||||
|
AGENT_BROWSER_SCREENSHOT_DIR Default screenshot output directory
|
||||||
|
AGENT_BROWSER_SCREENSHOT_QUALITY JPEG quality 0-100
|
||||||
|
AGENT_BROWSER_SCREENSHOT_FORMAT Screenshot format: png, jpeg
|
||||||
|
|
||||||
Install (recommended, fastest - native Rust CLI):
|
Install (recommended, fastest - native Rust CLI):
|
||||||
npm install -g agent-browser
|
npm install -g agent-browser
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ agent-browser drag <src> <dst> # Drag and drop
|
|||||||
agent-browser upload <sel> <files> # Upload files
|
agent-browser upload <sel> <files> # Upload files
|
||||||
agent-browser screenshot [path] # Screenshot (--full for full page)
|
agent-browser screenshot [path] # Screenshot (--full for full page)
|
||||||
agent-browser screenshot --annotate # Annotated screenshot with numbered element labels
|
agent-browser screenshot --annotate # Annotated screenshot with numbered element labels
|
||||||
|
agent-browser screenshot --screenshot-dir ./shots # Save to custom directory
|
||||||
|
agent-browser screenshot --screenshot-format jpeg --screenshot-quality 80
|
||||||
agent-browser pdf <path> # Save page as PDF
|
agent-browser pdf <path> # Save page as PDF
|
||||||
agent-browser snapshot # Accessibility tree with refs
|
agent-browser snapshot # Accessibility tree with refs
|
||||||
agent-browser eval <js> # Run JavaScript
|
agent-browser eval <js> # Run JavaScript
|
||||||
@@ -96,11 +98,13 @@ agent-browser find nth 2 ".card" hover
|
|||||||
```bash
|
```bash
|
||||||
agent-browser wait <selector> # Wait for element
|
agent-browser wait <selector> # Wait for element
|
||||||
agent-browser wait <ms> # Wait for time
|
agent-browser wait <ms> # Wait for time
|
||||||
agent-browser wait --text "Welcome" # Wait for text
|
agent-browser wait --text "Welcome" # Wait for text (substring match)
|
||||||
agent-browser wait --url "**/dash" # Wait for URL pattern
|
agent-browser wait --url "**/dash" # Wait for URL pattern
|
||||||
agent-browser wait --load networkidle # Wait for load state
|
agent-browser wait --load networkidle # Wait for load state
|
||||||
agent-browser wait --fn "condition" # Wait for JS condition
|
agent-browser wait --fn "condition" # Wait for JS condition
|
||||||
agent-browser wait --download [path] # Wait for download
|
agent-browser wait --download [path] # Wait for download
|
||||||
|
agent-browser wait --fn "!document.body.innerText.includes('Loading...')" # Wait for text to disappear
|
||||||
|
agent-browser wait "#spinner" --state hidden # Wait for element to disappear
|
||||||
```
|
```
|
||||||
|
|
||||||
## Downloads
|
## Downloads
|
||||||
@@ -121,6 +125,15 @@ agent-browser mouse up [button] # Release button
|
|||||||
agent-browser mouse wheel <dy> [dx] # Scroll wheel
|
agent-browser mouse wheel <dy> [dx] # Scroll wheel
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Clipboard
|
||||||
|
|
||||||
|
```bash
|
||||||
|
agent-browser clipboard read # Read text from clipboard
|
||||||
|
agent-browser clipboard write "Hello, World!" # Write text to clipboard
|
||||||
|
agent-browser clipboard copy # Copy current selection (Ctrl+C)
|
||||||
|
agent-browser clipboard paste # Paste from clipboard (Ctrl+V)
|
||||||
|
```
|
||||||
|
|
||||||
## Settings
|
## Settings
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -295,6 +308,9 @@ agent-browser reload # Reload page
|
|||||||
--json # JSON output (for scripts)
|
--json # JSON output (for scripts)
|
||||||
--full, -f # Full page screenshot
|
--full, -f # Full page screenshot
|
||||||
--annotate # Annotated screenshot with numbered element labels
|
--annotate # Annotated screenshot with numbered element labels
|
||||||
|
--screenshot-dir <path> # Default screenshot output directory (or AGENT_BROWSER_SCREENSHOT_DIR)
|
||||||
|
--screenshot-quality <n> # JPEG quality 0-100 (or AGENT_BROWSER_SCREENSHOT_QUALITY)
|
||||||
|
--screenshot-format <fmt> # Format: png (default), jpeg (or AGENT_BROWSER_SCREENSHOT_FORMAT)
|
||||||
--headed # Show browser window (not headless)
|
--headed # Show browser window (not headless)
|
||||||
--cdp <port|url> # Connect via Chrome DevTools Protocol (port or WebSocket URL)
|
--cdp <port|url> # Connect via Chrome DevTools Protocol (port or WebSocket URL)
|
||||||
--auto-connect # Auto-discover and connect to running Chrome
|
--auto-connect # Auto-discover and connect to running Chrome
|
||||||
|
|||||||
@@ -136,6 +136,9 @@ agent-browser wait @e1 # Wait for element
|
|||||||
agent-browser wait --load networkidle # Wait for network idle
|
agent-browser wait --load networkidle # Wait for network idle
|
||||||
agent-browser wait --url "**/page" # Wait for URL pattern
|
agent-browser wait --url "**/page" # Wait for URL pattern
|
||||||
agent-browser wait 2000 # Wait milliseconds
|
agent-browser wait 2000 # Wait milliseconds
|
||||||
|
agent-browser wait --text "Welcome" # Wait for text to appear (substring match)
|
||||||
|
agent-browser wait --fn "!document.body.innerText.includes('Loading...')" # Wait for text to disappear
|
||||||
|
agent-browser wait "#spinner" --state hidden # Wait for element to disappear
|
||||||
|
|
||||||
# Downloads
|
# Downloads
|
||||||
agent-browser download @e1 ./file.pdf # Click element to trigger download
|
agent-browser download @e1 ./file.pdf # Click element to trigger download
|
||||||
@@ -151,8 +154,16 @@ agent-browser set device "iPhone 14" # Emulate device (viewport + user
|
|||||||
agent-browser screenshot # Screenshot to temp dir
|
agent-browser screenshot # Screenshot to temp dir
|
||||||
agent-browser screenshot --full # Full page screenshot
|
agent-browser screenshot --full # Full page screenshot
|
||||||
agent-browser screenshot --annotate # Annotated screenshot with numbered element labels
|
agent-browser screenshot --annotate # Annotated screenshot with numbered element labels
|
||||||
|
agent-browser screenshot --screenshot-dir ./shots # Save to custom directory
|
||||||
|
agent-browser screenshot --screenshot-format jpeg --screenshot-quality 80
|
||||||
agent-browser pdf output.pdf # Save as PDF
|
agent-browser pdf output.pdf # Save as PDF
|
||||||
|
|
||||||
|
# Clipboard
|
||||||
|
agent-browser clipboard read # Read text from clipboard
|
||||||
|
agent-browser clipboard write "Hello, World!" # Write text to clipboard
|
||||||
|
agent-browser clipboard copy # Copy current selection
|
||||||
|
agent-browser clipboard paste # Paste from clipboard
|
||||||
|
|
||||||
# Diff (compare page states)
|
# Diff (compare page states)
|
||||||
agent-browser diff snapshot # Compare current vs last snapshot
|
agent-browser diff snapshot # Compare current vs last snapshot
|
||||||
agent-browser diff snapshot --baseline before.txt # Compare current vs saved file
|
agent-browser diff snapshot --baseline before.txt # Compare current vs saved file
|
||||||
|
|||||||
+19
-6
@@ -741,7 +741,7 @@ async function handleScreenshot(
|
|||||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||||
const random = Math.random().toString(36).substring(2, 8);
|
const random = Math.random().toString(36).substring(2, 8);
|
||||||
const filename = `screenshot-${timestamp}-${random}.${ext}`;
|
const filename = `screenshot-${timestamp}-${random}.${ext}`;
|
||||||
const screenshotDir = path.join(getAppDir(), 'tmp', 'screenshots');
|
const screenshotDir = command.screenshotDir ?? path.join(getAppDir(), 'tmp', 'screenshots');
|
||||||
mkdirSync(screenshotDir, { recursive: true });
|
mkdirSync(screenshotDir, { recursive: true });
|
||||||
savePath = path.join(screenshotDir, filename);
|
savePath = path.join(screenshotDir, filename);
|
||||||
}
|
}
|
||||||
@@ -954,7 +954,13 @@ async function handleEvaluate(
|
|||||||
async function handleWait(command: WaitCommand, browser: BrowserManager): Promise<Response> {
|
async function handleWait(command: WaitCommand, browser: BrowserManager): Promise<Response> {
|
||||||
const page = browser.getPage();
|
const page = browser.getPage();
|
||||||
|
|
||||||
if (command.selector) {
|
if (command.text) {
|
||||||
|
await page.waitForFunction(
|
||||||
|
(t: string) => (document.body.innerText || '').includes(t),
|
||||||
|
command.text,
|
||||||
|
{ timeout: command.timeout }
|
||||||
|
);
|
||||||
|
} else if (command.selector) {
|
||||||
await page.waitForSelector(command.selector, {
|
await page.waitForSelector(command.selector, {
|
||||||
state: command.state ?? 'visible',
|
state: command.state ?? 'visible',
|
||||||
timeout: command.timeout,
|
timeout: command.timeout,
|
||||||
@@ -962,7 +968,6 @@ async function handleWait(command: WaitCommand, browser: BrowserManager): Promis
|
|||||||
} else if (command.timeout) {
|
} else if (command.timeout) {
|
||||||
await page.waitForTimeout(command.timeout);
|
await page.waitForTimeout(command.timeout);
|
||||||
} else {
|
} else {
|
||||||
// Default: wait for load state
|
|
||||||
await page.waitForLoadState('load');
|
await page.waitForLoadState('load');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2119,14 +2124,22 @@ async function handleClipboard(
|
|||||||
|
|
||||||
switch (command.operation) {
|
switch (command.operation) {
|
||||||
case 'copy':
|
case 'copy':
|
||||||
await page.keyboard.press('Control+c');
|
await page.keyboard.press('ControlOrMeta+c');
|
||||||
return successResponse(command.id, { copied: true });
|
return successResponse(command.id, { copied: true });
|
||||||
case 'paste':
|
case 'paste':
|
||||||
await page.keyboard.press('Control+v');
|
await page.keyboard.press('ControlOrMeta+v');
|
||||||
return successResponse(command.id, { pasted: true });
|
return successResponse(command.id, { pasted: true });
|
||||||
case 'read':
|
case 'read': {
|
||||||
const text = await page.evaluate('navigator.clipboard.readText()');
|
const text = await page.evaluate('navigator.clipboard.readText()');
|
||||||
return successResponse(command.id, { text });
|
return successResponse(command.id, { text });
|
||||||
|
}
|
||||||
|
case 'write': {
|
||||||
|
if (!command.text) {
|
||||||
|
return errorResponse(command.id, "Missing 'text' parameter for clipboard write");
|
||||||
|
}
|
||||||
|
await page.evaluate(`navigator.clipboard.writeText(${JSON.stringify(command.text)})`);
|
||||||
|
return successResponse(command.id, { written: command.text });
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return errorResponse(command.id, 'Unknown clipboard operation');
|
return errorResponse(command.id, 'Unknown clipboard operation');
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-1
@@ -468,7 +468,7 @@ const tapSchema = baseCommandSchema.extend({
|
|||||||
|
|
||||||
const clipboardSchema = baseCommandSchema.extend({
|
const clipboardSchema = baseCommandSchema.extend({
|
||||||
action: z.literal('clipboard'),
|
action: z.literal('clipboard'),
|
||||||
operation: z.enum(['copy', 'paste', 'read']),
|
operation: z.enum(['copy', 'paste', 'read', 'write']),
|
||||||
text: z.string().optional(),
|
text: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -794,6 +794,7 @@ const screenshotSchema = baseCommandSchema.extend({
|
|||||||
format: z.enum(['png', 'jpeg']).optional(),
|
format: z.enum(['png', 'jpeg']).optional(),
|
||||||
quality: z.number().min(0).max(100).optional(),
|
quality: z.number().min(0).max(100).optional(),
|
||||||
annotate: z.boolean().optional(),
|
annotate: z.boolean().optional(),
|
||||||
|
screenshotDir: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const snapshotSchema = baseCommandSchema.extend({
|
const snapshotSchema = baseCommandSchema.extend({
|
||||||
@@ -814,6 +815,7 @@ const evaluateSchema = baseCommandSchema.extend({
|
|||||||
const waitSchema = baseCommandSchema.extend({
|
const waitSchema = baseCommandSchema.extend({
|
||||||
action: z.literal('wait'),
|
action: z.literal('wait'),
|
||||||
selector: z.string().min(1).optional(),
|
selector: z.string().min(1).optional(),
|
||||||
|
text: z.string().min(1).optional(),
|
||||||
timeout: z.number().positive().optional(),
|
timeout: z.number().positive().optional(),
|
||||||
state: z.enum(['attached', 'detached', 'visible', 'hidden']).optional(),
|
state: z.enum(['attached', 'detached', 'visible', 'hidden']).optional(),
|
||||||
});
|
});
|
||||||
|
|||||||
+3
-1
@@ -707,7 +707,7 @@ export interface TapCommand extends BaseCommand {
|
|||||||
// Clipboard
|
// Clipboard
|
||||||
export interface ClipboardCommand extends BaseCommand {
|
export interface ClipboardCommand extends BaseCommand {
|
||||||
action: 'clipboard';
|
action: 'clipboard';
|
||||||
operation: 'copy' | 'paste' | 'read';
|
operation: 'copy' | 'paste' | 'read' | 'write';
|
||||||
text?: string;
|
text?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -826,6 +826,7 @@ export interface ScreenshotCommand extends BaseCommand {
|
|||||||
format?: 'png' | 'jpeg';
|
format?: 'png' | 'jpeg';
|
||||||
quality?: number;
|
quality?: number;
|
||||||
annotate?: boolean;
|
annotate?: boolean;
|
||||||
|
screenshotDir?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SnapshotCommand extends BaseCommand {
|
export interface SnapshotCommand extends BaseCommand {
|
||||||
@@ -841,6 +842,7 @@ export interface EvaluateCommand extends BaseCommand {
|
|||||||
export interface WaitCommand extends BaseCommand {
|
export interface WaitCommand extends BaseCommand {
|
||||||
action: 'wait';
|
action: 'wait';
|
||||||
selector?: string;
|
selector?: string;
|
||||||
|
text?: string;
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
state?: 'attached' | 'detached' | 'visible' | 'hidden';
|
state?: 'attached' | 'detached' | 'visible' | 'hidden';
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user