From d027659571af9790c24793d7a657f96f0675d107 Mon Sep 17 00:00:00 2001 From: leeguooooo Date: Mon, 1 Jun 2026 10:35:20 +0900 Subject: [PATCH] feat(screenshot): hide scrollbars in headless screenshots (cherry-pick b4f2f37) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cherry-picks upstream agent-browser #1396. Adds a configurable --hide-scrollbars flag (AGENT_BROWSER_HIDE_SCROLLBARS env, hideScrollbars config key, default true) that appends Chrome's --hide-scrollbars launch arg for headless (non-extension) launches so native scrollbars aren't painted into screenshots. Plumbed through flags.rs, connection.rs, main.rs, native/actions.rs and native/cdp/chrome.rs; help text in output.rs + skill-data. Fork adaptation: - the arg lands in the headless && !has_extensions block, separate from the stealth base args — no interaction with anti-detection. - dropped upstream docs/, agent-browser.schema.json and README hunks (removed or rewritten in this fork). Verified: cargo check --tests passes. --- cli/src/commands.rs | 2 + cli/src/connection.rs | 5 +++ cli/src/doctor/launch.rs | 1 + cli/src/flags.rs | 57 +++++++++++++++++++++++++- cli/src/main.rs | 48 ++++++++++++++++++++++ cli/src/native/actions.rs | 54 ++++++++++++++++++++++-- cli/src/native/cdp/chrome.rs | 34 +++++++++++++++ cli/src/output.rs | 8 +++- skill-data/core/SKILL.md | 3 ++ skill-data/core/references/commands.md | 5 +++ 10 files changed, 212 insertions(+), 5 deletions(-) diff --git a/cli/src/commands.rs b/cli/src/commands.rs index cb90f63..2ec3c95 100644 --- a/cli/src/commands.rs +++ b/cli/src/commands.rs @@ -2751,6 +2751,7 @@ mod tests { provider: None, ignore_https_errors: false, allow_file_access: false, + hide_scrollbars: true, device: None, auto_connect: false, force_launch: false, @@ -2766,6 +2767,7 @@ mod tests { cli_proxy: false, cli_proxy_bypass: false, cli_allow_file_access: false, + cli_hide_scrollbars: false, cli_annotate: false, cli_download_path: false, cli_headed: false, diff --git a/cli/src/connection.rs b/cli/src/connection.rs index 4a9d00e..2ba1f93 100644 --- a/cli/src/connection.rs +++ b/cli/src/connection.rs @@ -412,6 +412,7 @@ pub struct DaemonOptions<'a> { pub proxy_password: Option<&'a str>, pub ignore_https_errors: bool, pub allow_file_access: bool, + pub hide_scrollbars: bool, pub profile: Option<&'a str>, pub state: Option<&'a str>, pub provider: Option<&'a str>, @@ -476,6 +477,10 @@ fn apply_daemon_env(cmd: &mut Command, session: &str, opts: &DaemonOptions) { if opts.allow_file_access { cmd.env("AGENT_BROWSER_ALLOW_FILE_ACCESS", "1"); } + cmd.env( + "AGENT_BROWSER_HIDE_SCROLLBARS", + if opts.hide_scrollbars { "1" } else { "0" }, + ); if let Some(prof) = opts.profile { cmd.env("AGENT_BROWSER_PROFILE", prof); } diff --git a/cli/src/doctor/launch.rs b/cli/src/doctor/launch.rs index f844387..7d8a09c 100644 --- a/cli/src/doctor/launch.rs +++ b/cli/src/doctor/launch.rs @@ -65,6 +65,7 @@ pub(super) fn check(checks: &mut Vec) { proxy_password: None, ignore_https_errors: false, allow_file_access: false, + hide_scrollbars: true, profile: None, state: None, provider: None, diff --git a/cli/src/flags.rs b/cli/src/flags.rs index e69326f..2f11a86 100644 --- a/cli/src/flags.rs +++ b/cli/src/flags.rs @@ -70,6 +70,7 @@ pub struct Config { pub user_agent: Option, pub provider: Option, pub device: Option, + pub hide_scrollbars: Option, pub ignore_https_errors: Option, pub allow_file_access: Option, pub cdp: Option, @@ -131,6 +132,7 @@ impl Config { user_agent: other.user_agent.or(self.user_agent), provider: other.provider.or(self.provider), device: other.device.or(self.device), + hide_scrollbars: other.hide_scrollbars.or(self.hide_scrollbars), ignore_https_errors: other.ignore_https_errors.or(self.ignore_https_errors), allow_file_access: other.allow_file_access.or(self.allow_file_access), cdp: other.cdp.or(self.cdp), @@ -187,6 +189,12 @@ fn env_var_is_truthy(name: &str) -> bool { } } +fn env_var_bool(name: &str) -> Option { + env::var(name) + .ok() + .map(|val| !matches!(val.to_lowercase().as_str(), "0" | "false" | "no" | "")) +} + /// Parse an optional boolean value after a flag. Returns (value, consumed_next_arg). /// Recognizes "true" as true, "false" as false. Bare flag defaults to true. fn parse_bool_arg(args: &[String], i: usize) -> (bool, bool) { @@ -306,6 +314,7 @@ pub struct Flags { pub provider: Option, pub ignore_https_errors: bool, pub allow_file_access: bool, + pub hide_scrollbars: bool, pub device: Option, pub auto_connect: bool, pub force_launch: bool, @@ -343,6 +352,7 @@ pub struct Flags { pub cli_proxy: bool, pub cli_proxy_bypass: bool, pub cli_allow_file_access: bool, + pub cli_hide_scrollbars: bool, pub cli_annotate: bool, pub cli_download_path: bool, pub cli_headed: bool, @@ -443,6 +453,9 @@ pub fn parse_flags(args: &[String]) -> Flags { || config.ignore_https_errors.unwrap_or(false), allow_file_access: env_var_is_truthy("AGENT_BROWSER_ALLOW_FILE_ACCESS") || config.allow_file_access.unwrap_or(false), + hide_scrollbars: env_var_bool("AGENT_BROWSER_HIDE_SCROLLBARS") + .or(config.hide_scrollbars) + .unwrap_or(true), device: env::var("AGENT_BROWSER_IOS_DEVICE").ok().or(config.device), auto_connect: !env_var_is_truthy("AGENT_BROWSER_NO_AUTO_CONNECT") && (env_var_is_truthy("AGENT_BROWSER_AUTO_CONNECT") @@ -518,6 +531,7 @@ pub fn parse_flags(args: &[String]) -> Flags { cli_proxy: false, cli_proxy_bypass: false, cli_allow_file_access: false, + cli_hide_scrollbars: false, cli_annotate: false, cli_download_path: false, cli_headed: false, @@ -677,6 +691,14 @@ pub fn parse_flags(args: &[String]) -> Flags { i += 1; } } + "--hide-scrollbars" => { + let (val, consumed) = parse_bool_arg(args, i); + flags.hide_scrollbars = val; + flags.cli_hide_scrollbars = true; + if consumed { + i += 1; + } + } "--device" => { if let Some(d) = args.get(i + 1) { flags.device = Some(d.clone()); @@ -852,6 +874,7 @@ pub fn clean_args(args: &[String]) -> Vec { "--debug", "--ignore-https-errors", "--allow-file-access", + "--hide-scrollbars", "--auto-connect", "--launch", "--new", @@ -933,6 +956,7 @@ pub fn clean_args(args: &[String]) -> Vec { #[cfg(test)] mod tests { use super::*; + use crate::test_utils::EnvGuard; fn args(s: &str) -> Vec { s.split_whitespace().map(String::from).collect() @@ -1176,6 +1200,7 @@ mod tests { "userAgent": "test-agent", "provider": "ios", "device": "iPhone 15", + "hideScrollbars": false, "ignoreHttpsErrors": true, "allowFileAccess": true, "cdp": "9222", @@ -1201,6 +1226,7 @@ mod tests { assert_eq!(config.user_agent.as_deref(), Some("test-agent")); assert_eq!(config.provider.as_deref(), Some("ios")); assert_eq!(config.device.as_deref(), Some("iPhone 15")); + assert_eq!(config.hide_scrollbars, Some(false)); assert_eq!(config.ignore_https_errors, Some(true)); assert_eq!(config.allow_file_access, Some(true)); assert_eq!(config.cdp.as_deref(), Some("9222")); @@ -1454,6 +1480,33 @@ mod tests { assert!(flags.cli_allow_file_access); } + #[test] + fn test_hide_scrollbars_default_true() { + let guard = EnvGuard::new(&["AGENT_BROWSER_HIDE_SCROLLBARS"]); + guard.remove("AGENT_BROWSER_HIDE_SCROLLBARS"); + let flags = parse_flags(&args("open example.com")); + assert!(flags.hide_scrollbars); + assert!(!flags.cli_hide_scrollbars); + } + + #[test] + fn test_hide_scrollbars_false() { + let guard = EnvGuard::new(&["AGENT_BROWSER_HIDE_SCROLLBARS"]); + guard.remove("AGENT_BROWSER_HIDE_SCROLLBARS"); + let flags = parse_flags(&args("--hide-scrollbars false open")); + assert!(!flags.hide_scrollbars); + assert!(flags.cli_hide_scrollbars); + } + + #[test] + fn test_hide_scrollbars_bare_defaults_true() { + let guard = EnvGuard::new(&["AGENT_BROWSER_HIDE_SCROLLBARS"]); + guard.remove("AGENT_BROWSER_HIDE_SCROLLBARS"); + let flags = parse_flags(&args("--hide-scrollbars open")); + assert!(flags.hide_scrollbars); + assert!(flags.cli_hide_scrollbars); + } + #[test] fn test_auto_connect_false() { let flags = parse_flags(&args("--auto-connect false open")); @@ -1462,7 +1515,9 @@ mod tests { #[test] fn test_clean_args_removes_bool_flag_with_value() { - let cleaned = clean_args(&args("--headed false --debug true open example.com")); + let cleaned = clean_args(&args( + "--headed false --debug true --hide-scrollbars false open example.com", + )); assert_eq!(cleaned, vec!["open", "example.com"]); } diff --git a/cli/src/main.rs b/cli/src/main.rs index cd41560..7fc0e2b 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -60,6 +60,23 @@ fn print_json_error_with_type(message: impl AsRef, error_type: &str) { })); } +fn should_send_hide_scrollbars_launch_option( + cli_hide_scrollbars: bool, + hide_scrollbars: bool, +) -> bool { + cli_hide_scrollbars || !hide_scrollbars +} + +fn apply_hide_scrollbars_launch_option( + launch_cmd: &mut serde_json::Value, + cli_hide_scrollbars: bool, + hide_scrollbars: bool, +) { + if should_send_hide_scrollbars_launch_option(cli_hide_scrollbars, hide_scrollbars) { + launch_cmd["hideScrollbars"] = json!(hide_scrollbars); + } +} + struct ParsedProxy { server: String, username: Option, @@ -741,6 +758,7 @@ fn main() { proxy_password: proxy_password.as_deref(), ignore_https_errors: flags.ignore_https_errors, allow_file_access: flags.allow_file_access, + hide_scrollbars: flags.hide_scrollbars, profile: flags.profile.as_deref(), state: flags.state.as_deref(), provider: flags.provider.as_deref(), @@ -814,6 +832,7 @@ fn main() { }, flags.ignore_https_errors.then_some("--ignore-https-errors"), flags.cli_allow_file_access.then_some("--allow-file-access"), + flags.cli_hide_scrollbars.then_some("--hide-scrollbars"), flags.cli_download_path.then_some("--download-path"), flags.cli_headed.then_some("--headed"), ] @@ -1062,6 +1081,10 @@ fn main() { || flags.args.is_some() || flags.user_agent.is_some() || flags.allow_file_access + || should_send_hide_scrollbars_launch_option( + flags.cli_hide_scrollbars, + flags.hide_scrollbars, + ) || flags.color_scheme.is_some() || flags.download_path.is_some() || flags.engine.is_some() @@ -1136,6 +1159,12 @@ fn main() { launch_cmd["allowFileAccess"] = json!(true); } + apply_hide_scrollbars_launch_option( + &mut launch_cmd, + flags.cli_hide_scrollbars, + flags.hide_scrollbars, + ); + if let Some(ref cs) = flags.color_scheme { launch_cmd["colorScheme"] = json!(cs); } @@ -1488,4 +1517,23 @@ mod tests { "Daemon process exited during startup:\nline \"quoted\"\u{001b}[2mansi\u{001b}[22m" ); } + + #[test] + fn test_hide_scrollbars_launch_option_serialization() { + assert!(!should_send_hide_scrollbars_launch_option(false, true)); + assert!(should_send_hide_scrollbars_launch_option(false, false)); + assert!(should_send_hide_scrollbars_launch_option(true, true)); + + let mut default_cmd = json!({ "action": "launch" }); + apply_hide_scrollbars_launch_option(&mut default_cmd, false, true); + assert!(default_cmd.get("hideScrollbars").is_none()); + + let mut config_false_cmd = json!({ "action": "launch" }); + apply_hide_scrollbars_launch_option(&mut config_false_cmd, false, false); + assert_eq!(config_false_cmd["hideScrollbars"], false); + + let mut cli_true_cmd = json!({ "action": "launch" }); + apply_hide_scrollbars_launch_option(&mut cli_true_cmd, true, true); + assert_eq!(cli_true_cmd["hideScrollbars"], true); + } } diff --git a/cli/src/native/actions.rs b/cli/src/native/actions.rs index 60014ba..ffa836f 100644 --- a/cli/src/native/actions.rs +++ b/cli/src/native/actions.rs @@ -197,6 +197,7 @@ fn launch_hash(opts: &LaunchOptions) -> u64 { opts.proxy_password.hash(&mut h); opts.user_agent.hash(&mut h); opts.allow_file_access.hash(&mut h); + opts.hide_scrollbars.hash(&mut h); h.finish() } @@ -1876,11 +1877,24 @@ fn launch_options_from_env() -> LaunchOptions { .unwrap_or(false), color_scheme: env::var("AGENT_BROWSER_COLOR_SCHEME").ok(), download_path: env::var("AGENT_BROWSER_DOWNLOAD_PATH").ok(), + hide_scrollbars: hide_scrollbars_from_env(), viewport_size: None, use_real_keychain: false, } } +fn hide_scrollbars_from_env() -> bool { + env::var("AGENT_BROWSER_HIDE_SCROLLBARS") + .map(|v| !matches!(v.to_ascii_lowercase().as_str(), "0" | "false" | "no" | "")) + .unwrap_or(true) +} + +fn hide_scrollbars_from_launch_cmd(cmd: &Value) -> bool { + cmd.get("hideScrollbars") + .and_then(|v| v.as_bool()) + .unwrap_or_else(hide_scrollbars_from_env) +} + async fn try_auto_restore_state(state: &mut DaemonState) { let session_name = match state.session_name.as_deref() { Some(n) if !n.is_empty() => n.to_string(), @@ -2044,6 +2058,7 @@ async fn handle_launch(cmd: &Value, state: &mut DaemonState) -> Result, pub download_path: Option, + /// Hide native scrollbars in headless Chromium screenshots by launching + /// Chrome with `--hide-scrollbars`. + pub hide_scrollbars: bool, /// Initial viewport dimensions used for `--window-size` so the content /// area matches the desired viewport from the start. pub viewport_size: Option<(u32, u32)>, @@ -130,6 +133,7 @@ impl Default for LaunchOptions { ignore_https_errors: false, color_scheme: None, download_path: None, + hide_scrollbars: true, viewport_size: None, use_real_keychain: false, } @@ -178,6 +182,13 @@ fn build_chrome_args(options: &LaunchOptions) -> Result { // injected in headless mode). Skip --headless when extensions are loaded. if options.headless && !has_extensions { args.push("--headless=new".to_string()); + // Linux paints native scrollbars into viewport screenshots unless + // Chrome is launched with this flag. `--hide-scrollbars` is + // presence-based, so agent-browser exposes --hide-scrollbars false + // as the public opt-out instead of forwarding a fake inverse switch. + if options.hide_scrollbars { + args.push("--hide-scrollbars".to_string()); + } // Enable SwiftShader software rendering in headless mode. This // prevents silent crashes in environments where GPU drivers are // missing or restricted (VMs, containers, some cloud machines) @@ -1360,6 +1371,7 @@ mod tests { }; let result = build_chrome_args(&opts).unwrap(); assert!(result.args.iter().any(|a| a == "--headless=new")); + assert!(result.args.iter().any(|a| a == "--hide-scrollbars")); assert!(result .args .iter() @@ -1380,6 +1392,7 @@ mod tests { }; let result = build_chrome_args(&opts).unwrap(); assert!(!result.args.iter().any(|a| a.contains("--headless"))); + assert!(!result.args.iter().any(|a| a == "--hide-scrollbars")); assert!(!result .args .iter() @@ -1434,6 +1447,23 @@ mod tests { } } + #[test] + fn test_build_args_hide_scrollbars_false_suppresses_default_hide_scrollbars() { + let opts = LaunchOptions { + headless: true, + hide_scrollbars: false, + ..Default::default() + }; + let result = build_chrome_args(&opts).unwrap(); + assert!( + !result.args.iter().any(|a| a == "--hide-scrollbars"), + "--hide-scrollbars false should suppress agent-browser's default hide switch" + ); + if let Some(ref dir) = result.temp_user_data_dir { + let _ = std::fs::remove_dir_all(dir); + } + } + #[test] fn test_build_args_start_maximized_suppresses_default_window_size() { let opts = LaunchOptions { @@ -1474,6 +1504,10 @@ mod tests { !result.args.iter().any(|a| a.contains("--headless")), "headless flag should be omitted when extensions are present" ); + assert!( + !result.args.iter().any(|a| a == "--hide-scrollbars"), + "scrollbars should remain visible when extensions force headed mode" + ); assert!( !result.args.iter().any(|a| a.contains("--window-size")), "window-size should be omitted when extensions force headed mode" diff --git a/cli/src/output.rs b/cli/src/output.rs index 1dce934..0c8320b 100644 --- a/cli/src/output.rs +++ b/cli/src/output.rs @@ -1583,6 +1583,8 @@ 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. +Headless Chromium screenshots hide native scrollbars for consistent image output. +Pass --hide-scrollbars false when launching to keep native scrollbars visible. Options: --full, -f Capture full page (not just viewport) @@ -3098,6 +3100,8 @@ Options: e.g., --proxy-bypass "localhost,*.internal.com" --ignore-https-errors Ignore HTTPS certificate errors --allow-file-access Allow file:// URLs to access local files (Chromium only) + --hide-scrollbars Hide native scrollbars in headless Chromium screenshots (default: true) + Use --hide-scrollbars false to keep scrollbars visible -p, --provider Browser provider: ios, browserbase, kernel, browseruse, browserless, agentcore --device iOS device name (e.g., "iPhone 15 Pro") --json JSON output @@ -3137,11 +3141,12 @@ Configuration: Boolean flags accept an optional true/false value to override config: --headed (same as --headed true) --headed false (disables "headed": true from config) + --hide-scrollbars false (keeps native scrollbars visible in headless Chromium screenshots) Extensions from user and project configs are merged (not replaced). Example agent-browser.json: - {{"headed": true, "proxy": "http://localhost:8080", "profile": "./browser-data"}} + {{"headed": true, "hideScrollbars": false, "proxy": "http://localhost:8080"}} Environment: AGENT_BROWSER_CONFIG Path to config file (or use --config) @@ -3161,6 +3166,7 @@ Environment: AGENT_BROWSER_PROVIDER Browser provider (ios, browserbase, kernel, browseruse, browserless, agentcore) AGENT_BROWSER_AUTO_CONNECT Auto-discover and connect to running Chrome AGENT_BROWSER_ALLOW_FILE_ACCESS Allow file:// URLs to access local files + AGENT_BROWSER_HIDE_SCROLLBARS Hide scrollbars in headless Chromium screenshots (default: true) AGENT_BROWSER_COLOR_SCHEME Color scheme preference (dark, light, no-preference) AGENT_BROWSER_DOWNLOAD_PATH Default download directory for browser downloads AGENT_BROWSER_DEFAULT_TIMEOUT Default action timeout in ms (default: 25000) diff --git a/skill-data/core/SKILL.md b/skill-data/core/SKILL.md index 113e408..613450c 100644 --- a/skill-data/core/SKILL.md +++ b/skill-data/core/SKILL.md @@ -243,6 +243,9 @@ agent-browser screenshot --full full.png # full scroll height agent-browser screenshot --annotate map.png # numbered labels + legend keyed to snapshot refs ``` +Headless Chromium screenshots hide native scrollbars for consistent image output. +Pass `--hide-scrollbars false` when launching to keep native scrollbars visible. + `--annotate` is designed for multimodal models: each label `[N]` maps to ref `@eN`. ### Handle multiple pages via tabs diff --git a/skill-data/core/references/commands.md b/skill-data/core/references/commands.md index 994fba5..e97b92f 100644 --- a/skill-data/core/references/commands.md +++ b/skill-data/core/references/commands.md @@ -103,6 +103,9 @@ agent-browser screenshot --full # Full page agent-browser pdf output.pdf # Save as PDF ``` +Headless Chromium screenshots hide native scrollbars for consistent image output. +Pass `--hide-scrollbars false` when launching to keep native scrollbars visible. + ## Video Recording ```bash @@ -309,6 +312,7 @@ agent-browser --headers ... # HTTP headers scoped to URL's origin agent-browser --executable-path

# Custom browser executable agent-browser --extension ... # Load browser extension (repeatable) agent-browser --ignore-https-errors # Ignore SSL certificate errors +agent-browser --hide-scrollbars false # Keep native scrollbars visible in headless Chromium screenshots agent-browser --help # Show help (-h) agent-browser --version # Show version (-V) agent-browser --help # Show detailed help for a command @@ -383,6 +387,7 @@ AGENT_BROWSER_EXECUTABLE_PATH="/path/chrome" # Custom browser path AGENT_BROWSER_EXTENSIONS="/ext1,/ext2" # Comma-separated extension paths AGENT_BROWSER_INIT_SCRIPTS="/a.js,/b.js" # Comma-separated init script paths AGENT_BROWSER_ENABLE="react-devtools" # Comma-separated built-in init script features +AGENT_BROWSER_HIDE_SCROLLBARS="false" # Keep native scrollbars visible in headless Chromium screenshots AGENT_BROWSER_PROVIDER="browserbase" # Cloud browser provider AGENT_BROWSER_STREAM_PORT="9223" # Override WebSocket streaming port (default: OS-assigned) AGENT_BROWSER_HOME="/path/to/agent-browser" # Custom install location