From a673a77c4e763d860a55971510eaa4ce136731ce Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 13 Mar 2026 02:58:30 -0500 Subject: [PATCH] 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 `, `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 --- README.md | 20 ++++- cli/src/commands.rs | 135 ++++++++++++++++++++++++++++++--- cli/src/flags.rs | 62 +++++++++++++++ cli/src/native/actions.rs | 34 +++++++-- cli/src/native/interaction.rs | 20 ++++- cli/src/native/screenshot.rs | 15 +++- cli/src/output.rs | 53 ++++++++++++- docs/src/app/commands/page.mdx | 18 ++++- skills/agent-browser/SKILL.md | 11 +++ src/actions.ts | 25 ++++-- src/protocol.ts | 4 +- src/types.ts | 4 +- 12 files changed, 369 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index be0c2e6..5d04863 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,8 @@ agent-browser drag # Drag and drop agent-browser upload # 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 # Save as PDF agent-browser snapshot # Accessibility tree with refs (best for AI) agent-browser eval # Run JavaScript (-b for base64, --stdin for piped input) @@ -179,14 +181,27 @@ agent-browser find nth 2 "a" text ```bash agent-browser wait # Wait for element to be visible agent-browser wait # 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 ` | Default screenshot output directory (or `AGENT_BROWSER_SCREENSHOT_DIR` env) | +| `--screenshot-quality ` | JPEG quality 0-100 (or `AGENT_BROWSER_SCREENSHOT_QUALITY` env) | +| `--screenshot-format ` | Screenshot format: `png`, `jpeg` (or `AGENT_BROWSER_SCREENSHOT_FORMAT` env) | | `--headed` | Show browser window (not headless) (or `AGENT_BROWSER_HEADED` env) | | `--cdp ` | Connect via Chrome DevTools Protocol (port or WebSocket URL) | | `--auto-connect` | Auto-discover and connect to running Chrome (or `AGENT_BROWSER_AUTO_CONNECT` env) | diff --git a/cli/src/commands.rs b/cli/src/commands.rs index 74cc698..dd99781 100644 --- a/cli/src/commands.rs +++ b/cli/src/commands.rs @@ -391,7 +391,7 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result Result", })?; - // 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::()) { + 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 (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 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 ", + })?; + 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 === diff --git a/cli/src/flags.rs b/cli/src/flags.rs index eaea9a4..3446d48 100644 --- a/cli/src/flags.rs +++ b/cli/src/flags.rs @@ -43,6 +43,9 @@ pub struct Config { pub confirm_interactive: Option, pub native: Option, pub engine: Option, + pub screenshot_dir: Option, + pub screenshot_quality: Option, + pub screenshot_format: Option, } 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> { "--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, + pub screenshot_dir: Option, + pub screenshot_quality: Option, + pub screenshot_format: Option, // 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::() { + 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 { "--confirm-actions", "--config", "--engine", + "--screenshot-dir", + "--screenshot-quality", + "--screenshot-format", ]; let mut i = 0; diff --git a/cli/src/native/actions.rs b/cli/src/native/actions.rs index a003e0c..8f04acc 100644 --- a/cli/src/native/actions.rs +++ b/cli/src/native/actions.rs @@ -1415,6 +1415,10 @@ async fn handle_screenshot(cmd: &Value, state: &mut DaemonState) -> Result Result Result Result { + "write" => { let text = cmd .get("text") .or_else(|| cmd.get("value")) @@ -3119,7 +3128,17 @@ async fn handle_clipboard(cmd: &Value, state: &DaemonState) -> Result { + 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 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, +) -> 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), ) diff --git a/cli/src/native/screenshot.rs b/cli/src/native/screenshot.rs index 3ff3fd3..5585949 100644 --- a/cli/src/native/screenshot.rs +++ b/cli/src/native/screenshot.rs @@ -57,6 +57,7 @@ pub struct ScreenshotOptions { pub format: String, pub quality: Option, pub annotate: bool, + pub output_dir: Option, } 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 { 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) diff --git a/cli/src/output.rs b/cli/src/output.rs index 746a83b..9b47f54 100644 --- a/cli/src/output.rs +++ b/cli/src/output.rs @@ -1313,12 +1313,18 @@ Modes: --url Wait for URL to match pattern --load Wait for load state (load, domcontentloaded, networkidle) --fn Wait for JavaScript expression to be truthy - --text Wait for text to appear on page + --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 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 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 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 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 [text] + +Read from or write to the browser clipboard. + +Operations: + read Read text from clipboard + write 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 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 Highlight element inspect Open Chrome DevTools for the active page + clipboard [text] Read/write clipboard (read, write, copy, paste) Auth Vault: auth save [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 Default screenshot output directory (or AGENT_BROWSER_SCREENSHOT_DIR) + --screenshot-quality JPEG quality 0-100; ignored for PNG (or AGENT_BROWSER_SCREENSHOT_QUALITY) + --screenshot-format Screenshot format: png, jpeg (or AGENT_BROWSER_SCREENSHOT_FORMAT) --headed Show browser window (not headless) (or AGENT_BROWSER_HEADED env) --cdp Connect via CDP (Chrome DevTools Protocol) --color-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 diff --git a/docs/src/app/commands/page.mdx b/docs/src/app/commands/page.mdx index af9f67a..0e954dd 100644 --- a/docs/src/app/commands/page.mdx +++ b/docs/src/app/commands/page.mdx @@ -28,6 +28,8 @@ agent-browser drag # Drag and drop agent-browser upload # 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 # Save page as PDF agent-browser snapshot # Accessibility tree with refs agent-browser eval # Run JavaScript @@ -96,11 +98,13 @@ agent-browser find nth 2 ".card" hover ```bash agent-browser wait # Wait for element agent-browser wait # 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 [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 # Default screenshot output directory (or AGENT_BROWSER_SCREENSHOT_DIR) +--screenshot-quality # JPEG quality 0-100 (or AGENT_BROWSER_SCREENSHOT_QUALITY) +--screenshot-format # Format: png (default), jpeg (or AGENT_BROWSER_SCREENSHOT_FORMAT) --headed # Show browser window (not headless) --cdp # Connect via Chrome DevTools Protocol (port or WebSocket URL) --auto-connect # Auto-discover and connect to running Chrome diff --git a/skills/agent-browser/SKILL.md b/skills/agent-browser/SKILL.md index 527c4e0..11c84fc 100644 --- a/skills/agent-browser/SKILL.md +++ b/skills/agent-browser/SKILL.md @@ -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 diff --git a/src/actions.ts b/src/actions.ts index b63e5cb..9e07de5 100644 --- a/src/actions.ts +++ b/src/actions.ts @@ -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 { 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'); } diff --git a/src/protocol.ts b/src/protocol.ts index 785d576..46963b0 100644 --- a/src/protocol.ts +++ b/src/protocol.ts @@ -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(), }); diff --git a/src/types.ts b/src/types.ts index cbe70f9..f476bb1 100644 --- a/src/types.ts +++ b/src/types.ts @@ -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'; }