From f319195974ff38436e5d0862c949b4e616a2e0ed Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Tue, 24 Feb 2026 07:40:46 -0600 Subject: [PATCH] add --selector flag to scroll command (#537) * add --selector flag to scroll command The `scroll` command uses `window.scrollBy()`, which has no effect on apps that use custom scrollable containers (e.g. a nested div with overflow-y: auto). The backend `handleScroll` already supports a `selector` parameter, but the CLI never exposed it. This adds `-s` / `--selector` to the `scroll` command so users can target a specific scrollable element: agent-browser scroll down 500 --selector "div.scroll-container" Also fixes the backend to apply `direction`/`amount` when a selector is present (previously those fields were only used in the no-selector branch). Closes #501 * fixes --- README.md | 2 +- cli/src/commands.rs | 129 +++++++++++++++++++++++++++++++-- cli/src/output.rs | 8 +- docs/src/app/commands/page.mdx | 2 +- skills/agent-browser/SKILL.md | 1 + src/actions.ts | 50 ++++++------- 6 files changed, 157 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index fc0eca3..acf519b 100644 --- a/README.md +++ b/README.md @@ -109,7 +109,7 @@ agent-browser hover # Hover element agent-browser select # Select dropdown option agent-browser check # Check checkbox agent-browser uncheck # Uncheck checkbox -agent-browser scroll [px] # Scroll (up/down/left/right) +agent-browser scroll [px] # Scroll (up/down/left/right, --selector ) agent-browser scrollintoview # Scroll element into view (alias: scrollinto) agent-browser drag # Drag and drop agent-browser upload # Upload files diff --git a/cli/src/commands.rs b/cli/src/commands.rs index 46abdb2..21e0e80 100644 --- a/cli/src/commands.rs +++ b/cli/src/commands.rs @@ -298,12 +298,48 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result { - let dir = rest.first().unwrap_or(&"down"); - let amount = rest - .get(1) - .and_then(|s| s.parse::().ok()) - .unwrap_or(300); - Ok(json!({ "id": id, "action": "scroll", "direction": dir, "amount": amount })) + let mut cmd = json!({ "id": id, "action": "scroll" }); + let obj = cmd.as_object_mut().unwrap(); + let mut positional_index = 0; + let mut i = 0; + while i < rest.len() { + match rest[i] { + "-s" | "--selector" => { + if let Some(s) = rest.get(i + 1) { + obj.insert("selector".to_string(), json!(s)); + i += 1; + } else { + return Err(ParseError::MissingArguments { + context: "scroll --selector".to_string(), + usage: "scroll [direction] [amount] [--selector ]", + }); + } + } + arg if arg.starts_with('-') => {} + _ => { + match positional_index { + 0 => { + obj.insert("direction".to_string(), json!(rest[i])); + } + 1 => { + if let Ok(n) = rest[i].parse::() { + obj.insert("amount".to_string(), json!(n)); + } + } + _ => {} + } + positional_index += 1; + } + } + i += 1; + } + if !obj.contains_key("direction") { + obj.insert("direction".to_string(), json!("down")); + } + if !obj.contains_key("amount") { + obj.insert("amount".to_string(), json!(300)); + } + Ok(cmd) } "scrollintoview" | "scrollinto" => { let sel = rest.first().ok_or_else(|| ParseError::MissingArguments { @@ -3436,4 +3472,85 @@ mod tests { ParseError::MissingArguments { .. } )); } + + // === Scroll Tests === + + #[test] + fn test_scroll_defaults() { + let cmd = parse_command(&args("scroll"), &default_flags()).unwrap(); + assert_eq!(cmd["action"], "scroll"); + assert_eq!(cmd["direction"], "down"); + assert_eq!(cmd["amount"], 300); + assert!(cmd.get("selector").is_none()); + } + + #[test] + fn test_scroll_direction_and_amount() { + let cmd = parse_command(&args("scroll up 200"), &default_flags()).unwrap(); + assert_eq!(cmd["action"], "scroll"); + assert_eq!(cmd["direction"], "up"); + assert_eq!(cmd["amount"], 200); + } + + #[test] + fn test_scroll_with_selector() { + let cmd = parse_command( + &args("scroll down 500 --selector div.scroll-container"), + &default_flags(), + ) + .unwrap(); + assert_eq!(cmd["action"], "scroll"); + assert_eq!(cmd["direction"], "down"); + assert_eq!(cmd["amount"], 500); + assert_eq!(cmd["selector"], "div.scroll-container"); + } + + #[test] + fn test_scroll_with_selector_short_flag() { + let cmd = parse_command( + &args("scroll left 100 -s .sidebar"), + &default_flags(), + ) + .unwrap(); + assert_eq!(cmd["action"], "scroll"); + assert_eq!(cmd["direction"], "left"); + assert_eq!(cmd["amount"], 100); + assert_eq!(cmd["selector"], ".sidebar"); + } + + #[test] + fn test_scroll_selector_before_positional() { + let cmd = parse_command( + &args("scroll --selector .panel down 400"), + &default_flags(), + ) + .unwrap(); + assert_eq!(cmd["action"], "scroll"); + assert_eq!(cmd["direction"], "down"); + assert_eq!(cmd["amount"], 400); + assert_eq!(cmd["selector"], ".panel"); + } + + #[test] + fn test_scroll_selector_only() { + let cmd = parse_command( + &args("scroll --selector .content"), + &default_flags(), + ) + .unwrap(); + assert_eq!(cmd["action"], "scroll"); + assert_eq!(cmd["direction"], "down"); + assert_eq!(cmd["amount"], 300); + assert_eq!(cmd["selector"], ".content"); + } + + #[test] + fn test_scroll_selector_missing_value() { + let result = parse_command(&args("scroll down 500 --selector"), &default_flags()); + assert!(result.is_err()); + assert!(matches!( + result.unwrap_err(), + ParseError::MissingArguments { .. } + )); + } } diff --git a/cli/src/output.rs b/cli/src/output.rs index 18d2921..0d986d3 100644 --- a/cli/src/output.rs +++ b/cli/src/output.rs @@ -973,14 +973,17 @@ Use Cases: r##" agent-browser scroll - Scroll the page -Usage: agent-browser scroll [direction] [amount] +Usage: agent-browser scroll [direction] [amount] [options] -Scrolls the page in the specified direction. +Scrolls the page or a specific element in the specified direction. Arguments: direction up, down, left, right (default: down) amount Pixels to scroll (default: 300) +Options: + -s, --selector CSS selector for a scrollable container + Global Options: --json Output as JSON --session Use specific session @@ -990,6 +993,7 @@ Examples: agent-browser scroll down 500 agent-browser scroll up 200 agent-browser scroll left 100 + agent-browser scroll down 500 --selector "div.scroll-container" "## } "scrollintoview" | "scrollinto" => { diff --git a/docs/src/app/commands/page.mdx b/docs/src/app/commands/page.mdx index fd97517..1a56432 100644 --- a/docs/src/app/commands/page.mdx +++ b/docs/src/app/commands/page.mdx @@ -22,7 +22,7 @@ agent-browser focus # Focus element agent-browser select # Select dropdown option agent-browser check # Check checkbox agent-browser uncheck # Uncheck checkbox -agent-browser scroll [px] # Scroll (up/down/left/right) +agent-browser scroll [px] # Scroll (up/down/left/right, --selector ) agent-browser scrollintoview # Scroll element into view agent-browser drag # Drag and drop agent-browser upload # Upload files diff --git a/skills/agent-browser/SKILL.md b/skills/agent-browser/SKILL.md index 25d237f..e691527 100644 --- a/skills/agent-browser/SKILL.md +++ b/skills/agent-browser/SKILL.md @@ -67,6 +67,7 @@ agent-browser press Enter # Press key agent-browser keyboard type "text" # Type at current focus (no selector) agent-browser keyboard inserttext "text" # Insert without key events agent-browser scroll down 500 # Scroll page +agent-browser scroll down 500 --selector "div.content" # Scroll within a specific container # Get information agent-browser get text @e1 # Get element text diff --git a/src/actions.ts b/src/actions.ts index aa92865..fc012e6 100644 --- a/src/actions.ts +++ b/src/actions.ts @@ -882,41 +882,41 @@ async function handleWait(command: WaitCommand, browser: BrowserManager): Promis async function handleScroll(command: ScrollCommand, browser: BrowserManager): Promise { const page = browser.getPage(); + let deltaX = command.x ?? 0; + let deltaY = command.y ?? 0; + const hasExplicitDelta = command.x !== undefined || command.y !== undefined; + + if (command.direction) { + const amount = command.amount ?? 100; + switch (command.direction) { + case 'up': + deltaY = -amount; + break; + case 'down': + deltaY = amount; + break; + case 'left': + deltaX = -amount; + break; + case 'right': + deltaX = amount; + break; + } + } + if (command.selector) { const element = browser.getLocator(command.selector); await element.scrollIntoViewIfNeeded(); - if (command.x !== undefined || command.y !== undefined) { + if (hasExplicitDelta || deltaX !== 0 || deltaY !== 0) { await element.evaluate( (el, { x, y }) => { - el.scrollBy(x ?? 0, y ?? 0); + el.scrollBy(x, y); }, - { x: command.x, y: command.y } + { x: deltaX, y: deltaY } ); } } else { - // Scroll the page - let deltaX = command.x ?? 0; - let deltaY = command.y ?? 0; - - if (command.direction) { - const amount = command.amount ?? 100; - switch (command.direction) { - case 'up': - deltaY = -amount; - break; - case 'down': - deltaY = amount; - break; - case 'left': - deltaX = -amount; - break; - case 'right': - deltaX = amount; - break; - } - } - await page.evaluate(`window.scrollBy(${deltaX}, ${deltaY})`); }