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:
Chris Tate
2026-03-13 02:58:30 -05:00
committed by GitHub
parent 1129a3e7fc
commit a673a77c4e
12 changed files with 369 additions and 32 deletions
+19 -1
View File
@@ -115,6 +115,8 @@ agent-browser drag <src> <tgt> # Drag and drop
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 --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 snapshot # Accessibility tree with refs (best for AI)
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
agent-browser wait <selector> # Wait for element to be visible
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 --load networkidle # Wait for load state
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`
### 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
```bash
@@ -532,6 +547,9 @@ This is useful for multimodal AI models that can reason about visual layout, unl
| `--json` | JSON output (for agents) |
| `--full, -f` | Full page screenshot |
| `--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) |
| `--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) |
+126 -9
View File
@@ -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 ===
+62
View File
@@ -43,6 +43,9 @@ pub struct Config {
pub confirm_interactive: Option<bool>,
pub native: Option<bool>,
pub engine: Option<String>,
pub screenshot_dir: Option<String>,
pub screenshot_quality: Option<u32>,
pub screenshot_format: Option<String>,
}
impl Config {
@@ -86,6 +89,9 @@ impl Config {
confirm_interactive: other.confirm_interactive.or(self.confirm_interactive),
native: other.native.or(self.native),
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",
"--confirm-actions",
"--engine",
"--screenshot-dir",
"--screenshot-quality",
"--screenshot-format",
];
let mut i = 0;
while i < args.len() {
@@ -240,6 +249,9 @@ pub struct Flags {
pub confirm_interactive: bool,
pub native: bool,
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
// (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),
native: env_var_is_truthy("AGENT_BROWSER_NATIVE") || config.native.unwrap_or(false),
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_extensions: false,
cli_profile: false,
@@ -586,6 +609,42 @@ pub fn parse_flags(args: &[String]) -> Flags {
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" => {
// Already handled by load_config(); skip the value
i += 1;
@@ -640,6 +699,9 @@ pub fn clean_args(args: &[String]) -> Vec<String> {
"--confirm-actions",
"--config",
"--engine",
"--screenshot-dir",
"--screenshot-quality",
"--screenshot-format",
];
let mut i = 0;
+27 -7
View File
@@ -1415,6 +1415,10 @@ async fn handle_screenshot(cmd: &Value, state: &mut DaemonState) -> Result<Value
.and_then(|v| v.as_i64())
.map(|q| q as i32),
annotate,
output_dir: cmd
.get("screenshotDir")
.and_then(|v| v.as_str())
.map(String::from),
};
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 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()) {
let state_str = cmd
.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 }));
}
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()) {
wait_for_function(&mgr.client, &session_id, fn_str, timeout_ms).await?;
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())
.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 {
"write" | "copy" => {
"write" => {
let text = cmd
.get("text")
.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()
);
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?;
@@ -4190,6 +4209,7 @@ async fn handle_diff_screenshot(cmd: &Value, state: &DaemonState) -> Result<Valu
format: "png".to_string(),
quality: None,
annotate: false,
output_dir: None,
};
let result =
+18 -2
View File
@@ -206,6 +206,22 @@ pub async fn type_text(
}
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);
client
@@ -219,7 +235,7 @@ pub async fn press_key(client: &CdpClient, session_id: &str, key: &str) -> Resul
unmodified_text: None,
windows_virtual_key_code: Some(key_code),
native_virtual_key_code: Some(key_code),
modifiers: None,
modifiers,
},
Some(session_id),
)
@@ -236,7 +252,7 @@ pub async fn press_key(client: &CdpClient, session_id: &str, key: &str) -> Resul
unmodified_text: None,
windows_virtual_key_code: Some(key_code),
native_virtual_key_code: Some(key_code),
modifiers: None,
modifiers,
},
Some(session_id),
)
+13 -2
View File
@@ -57,6 +57,7 @@ pub struct ScreenshotOptions {
pub format: String,
pub quality: Option<i32>,
pub annotate: bool,
pub output_dir: Option<String>,
}
impl Default for ScreenshotOptions {
@@ -68,6 +69,7 @@ impl Default for ScreenshotOptions {
format: "png".to_string(),
quality: None,
annotate: false,
output_dir: None,
}
}
}
@@ -145,7 +147,12 @@ pub async fn take_screenshot(
} else {
"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 {
path,
@@ -479,11 +486,15 @@ fn save_screenshot(
base64_data: &str,
explicit_path: Option<&str>,
ext: &str,
output_dir: Option<&str>,
) -> Result<String, String> {
let save_path = match explicit_path {
Some(path) => path.to_string(),
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 timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
+51 -2
View File
@@ -1313,12 +1313,18 @@ Modes:
--url <pattern> Wait for URL to match pattern
--load <state> Wait for load state (load, domcontentloaded, networkidle)
--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 Options (with --download):
--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:
--json Output as JSON
--session <name> Use specific session
@@ -1332,6 +1338,7 @@ Examples:
agent-browser wait --text "Welcome back"
agent-browser wait --download ./file.pdf
agent-browser wait --download ./report.xlsx --timeout 30000
agent-browser wait --fn "!document.body.innerText.includes('Loading...')"
"##
}
@@ -1340,7 +1347,7 @@ Examples:
r##"
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,
saves to a temporary directory with a generated filename.
@@ -1353,6 +1360,12 @@ Options:
With --json, annotations are included in the response.
In native mode, this is currently supported on the
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:
--json Output as JSON
@@ -1365,6 +1378,8 @@ Examples:
agent-browser screenshot --annotate # Labeled screenshot + legend
agent-browser screenshot --annotate ./page.png # Save annotated screenshot
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" => {
@@ -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" => {
r##"
@@ -2434,6 +2476,7 @@ Debug:
errors [--clear] View page errors
highlight <sel> Highlight element
inspect Open Chrome DevTools for the active page
clipboard <op> [text] Read/write clipboard (read, write, copy, paste)
Auth Vault:
auth save <name> [opts] Save auth profile (--url, --username, --password/--password-stdin)
@@ -2489,6 +2532,9 @@ Options:
--json JSON output
--full, -f Full page screenshot
--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)
--cdp <port> Connect via CDP (Chrome DevTools Protocol)
--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_ENGINE Browser engine: chrome (default), lightpanda
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):
npm install -g agent-browser
+17 -1
View File
@@ -28,6 +28,8 @@ agent-browser drag <src> <dst> # Drag and drop
agent-browser upload <sel> <files> # Upload files
agent-browser screenshot [path] # Screenshot (--full for full page)
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 snapshot # Accessibility tree with refs
agent-browser eval <js> # Run JavaScript
@@ -96,11 +98,13 @@ agent-browser find nth 2 ".card" hover
```bash
agent-browser wait <selector> # Wait for element
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 --load networkidle # Wait for load state
agent-browser wait --fn "condition" # Wait for JS condition
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
@@ -121,6 +125,15 @@ agent-browser mouse up [button] # Release button
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
```bash
@@ -295,6 +308,9 @@ agent-browser reload # Reload page
--json # JSON output (for scripts)
--full, -f # Full page screenshot
--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)
--cdp <port|url> # Connect via Chrome DevTools Protocol (port or WebSocket URL)
--auto-connect # Auto-discover and connect to running Chrome
+11
View File
@@ -136,6 +136,9 @@ agent-browser wait @e1 # Wait for element
agent-browser wait --load networkidle # Wait for network idle
agent-browser wait --url "**/page" # Wait for URL pattern
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
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 --full # Full page screenshot
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
# 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)
agent-browser diff snapshot # Compare current vs last snapshot
agent-browser diff snapshot --baseline before.txt # Compare current vs saved file
+19 -6
View File
@@ -741,7 +741,7 @@ async function handleScreenshot(
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const random = Math.random().toString(36).substring(2, 8);
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 });
savePath = path.join(screenshotDir, filename);
}
@@ -954,7 +954,13 @@ async function handleEvaluate(
async function handleWait(command: WaitCommand, browser: BrowserManager): Promise<Response> {
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, {
state: command.state ?? 'visible',
timeout: command.timeout,
@@ -962,7 +968,6 @@ async function handleWait(command: WaitCommand, browser: BrowserManager): Promis
} else if (command.timeout) {
await page.waitForTimeout(command.timeout);
} else {
// Default: wait for load state
await page.waitForLoadState('load');
}
@@ -2119,14 +2124,22 @@ async function handleClipboard(
switch (command.operation) {
case 'copy':
await page.keyboard.press('Control+c');
await page.keyboard.press('ControlOrMeta+c');
return successResponse(command.id, { copied: true });
case 'paste':
await page.keyboard.press('Control+v');
await page.keyboard.press('ControlOrMeta+v');
return successResponse(command.id, { pasted: true });
case 'read':
case 'read': {
const text = await page.evaluate('navigator.clipboard.readText()');
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:
return errorResponse(command.id, 'Unknown clipboard operation');
}
+3 -1
View File
@@ -468,7 +468,7 @@ const tapSchema = baseCommandSchema.extend({
const clipboardSchema = baseCommandSchema.extend({
action: z.literal('clipboard'),
operation: z.enum(['copy', 'paste', 'read']),
operation: z.enum(['copy', 'paste', 'read', 'write']),
text: z.string().optional(),
});
@@ -794,6 +794,7 @@ const screenshotSchema = baseCommandSchema.extend({
format: z.enum(['png', 'jpeg']).optional(),
quality: z.number().min(0).max(100).optional(),
annotate: z.boolean().optional(),
screenshotDir: z.string().optional(),
});
const snapshotSchema = baseCommandSchema.extend({
@@ -814,6 +815,7 @@ const evaluateSchema = baseCommandSchema.extend({
const waitSchema = baseCommandSchema.extend({
action: z.literal('wait'),
selector: z.string().min(1).optional(),
text: z.string().min(1).optional(),
timeout: z.number().positive().optional(),
state: z.enum(['attached', 'detached', 'visible', 'hidden']).optional(),
});
+3 -1
View File
@@ -707,7 +707,7 @@ export interface TapCommand extends BaseCommand {
// Clipboard
export interface ClipboardCommand extends BaseCommand {
action: 'clipboard';
operation: 'copy' | 'paste' | 'read';
operation: 'copy' | 'paste' | 'read' | 'write';
text?: string;
}
@@ -826,6 +826,7 @@ export interface ScreenshotCommand extends BaseCommand {
format?: 'png' | 'jpeg';
quality?: number;
annotate?: boolean;
screenshotDir?: string;
}
export interface SnapshotCommand extends BaseCommand {
@@ -841,6 +842,7 @@ export interface EvaluateCommand extends BaseCommand {
export interface WaitCommand extends BaseCommand {
action: 'wait';
selector?: string;
text?: string;
timeout?: number;
state?: 'attached' | 'detached' | 'visible' | 'hidden';
}