Merge remote-tracking branch 'upstream/main'

# Conflicts:
#	README.md
#	cli/src/connection.rs
#	cli/src/flags.rs
#	cli/src/main.rs
#	src/browser.ts
#	src/protocol.ts
This commit is contained in:
leeguooooo
2026-02-25 10:06:20 +09:00
22 changed files with 1467 additions and 59 deletions
+125 -6
View File
@@ -369,12 +369,48 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result<Value, ParseError
// === Scroll ===
"scroll" => {
let dir = rest.first().unwrap_or(&"down");
let amount = rest
.get(1)
.and_then(|s| s.parse::<i32>().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 <sel>]",
});
}
}
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::<i32>() {
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 {
@@ -1988,8 +2024,10 @@ mod tests {
cli_proxy_bypass: false,
cli_allow_file_access: false,
cli_annotate: false,
cli_download_path: false,
annotate: false,
color_scheme: None,
download_path: None,
}
}
@@ -3565,4 +3603,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 { .. }
));
}
}
+7
View File
@@ -220,6 +220,7 @@ pub fn ensure_daemon(
device: Option<&str>,
session_name: Option<&str>,
debug: bool,
download_path: Option<&str>,
) -> Result<DaemonResult, String> {
// Check if daemon is running AND responsive
if is_daemon_running(session) && daemon_ready(session) {
@@ -364,6 +365,9 @@ pub fn ensure_daemon(
if debug {
cmd.env("AGENT_BROWSER_DEBUG", "1");
}
if let Some(dp) = download_path {
cmd.env("AGENT_BROWSER_DOWNLOAD_PATH", dp);
}
// Create new process group and session to fully detach
unsafe {
@@ -448,6 +452,9 @@ pub fn ensure_daemon(
if debug {
cmd.env("AGENT_BROWSER_DEBUG", "1");
}
if let Some(dp) = download_path {
cmd.env("AGENT_BROWSER_DOWNLOAD_PATH", dp);
}
// CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS
const CREATE_NEW_PROCESS_GROUP: u32 = 0x00000200;
+29
View File
@@ -33,6 +33,7 @@ pub struct Config {
pub headers: Option<String>,
pub annotate: Option<bool>,
pub color_scheme: Option<String>,
pub download_path: Option<String>,
}
impl Config {
@@ -66,6 +67,7 @@ impl Config {
headers: other.headers.or(self.headers),
annotate: other.annotate.or(self.annotate),
color_scheme: other.color_scheme.or(self.color_scheme),
download_path: other.download_path.or(self.download_path),
}
}
}
@@ -131,6 +133,7 @@ fn extract_config_path(args: &[String]) -> Option<Option<String>> {
"--session-name",
"--color-scheme",
"--channel",
"--download-path",
];
let mut i = 0;
while i < args.len() {
@@ -200,6 +203,7 @@ pub struct Flags {
pub session_name: Option<String>,
pub annotate: bool,
pub color_scheme: Option<String>,
pub download_path: Option<String>,
// Track which launch-time options were explicitly passed via CLI
// (as opposed to being set only via environment variables)
@@ -212,6 +216,7 @@ pub struct Flags {
pub cli_proxy_bypass: bool,
pub cli_allow_file_access: bool,
pub cli_annotate: bool,
pub cli_download_path: bool,
}
pub fn parse_flags(args: &[String]) -> Flags {
@@ -278,6 +283,8 @@ pub fn parse_flags(args: &[String]) -> Flags {
color_scheme: env::var("AGENT_BROWSER_COLOR_SCHEME")
.ok()
.or(config.color_scheme),
download_path: env::var("AGENT_BROWSER_DOWNLOAD_PATH").ok()
.or(config.download_path),
cli_executable_path: false,
cli_extensions: false,
cli_state: false,
@@ -287,6 +294,7 @@ pub fn parse_flags(args: &[String]) -> Flags {
cli_proxy_bypass: false,
cli_allow_file_access: false,
cli_annotate: false,
cli_download_path: false,
};
let mut i = 0;
@@ -441,6 +449,13 @@ pub fn parse_flags(args: &[String]) -> Flags {
i += 1;
}
}
"--download-path" => {
if let Some(s) = args.get(i + 1) {
flags.download_path = Some(s.clone());
flags.cli_download_path = true;
i += 1;
}
}
"--config" => {
// Already handled by load_config(); skip the value
i += 1;
@@ -484,6 +499,7 @@ pub fn clean_args(args: &[String]) -> Vec<String> {
"--device",
"--session-name",
"--color-scheme",
"--download-path",
"--config",
];
@@ -668,6 +684,19 @@ mod tests {
assert!(!flags.cli_annotate);
}
#[test]
fn test_cli_download_path_tracking() {
let flags = parse_flags(&args("--download-path /tmp/dl snapshot"));
assert!(flags.cli_download_path);
assert_eq!(flags.download_path, Some("/tmp/dl".to_string()));
}
#[test]
fn test_cli_download_path_not_set_without_flag() {
let flags = parse_flags(&args("snapshot"));
assert!(!flags.cli_download_path);
}
#[test]
fn test_cli_multiple_flags_tracking() {
let flags = parse_flags(&args(
+31 -23
View File
@@ -267,6 +267,7 @@ fn main() {
flags.device.as_deref(),
flags.session_name.as_deref(),
flags.debug,
flags.download_path.as_deref(),
) {
Ok(result) => result,
Err(e) => {
@@ -317,6 +318,7 @@ fn main() {
},
flags.ignore_https_errors.then_some("--ignore-https-errors"),
flags.cli_allow_file_access.then_some("--allow-file-access"),
flags.cli_download_path.then_some("--download-path"),
]
.into_iter()
.flatten()
@@ -398,6 +400,10 @@ fn main() {
launch_cmd["colorScheme"] = json!(cs);
}
if let Some(ref dp) = flags.download_path {
launch_cmd["downloadPath"] = json!(dp);
}
let err = match send_command(launch_cmd, &flags.session) {
Ok(resp) if resp.success => None,
Ok(resp) => Some(
@@ -484,29 +490,26 @@ fn main() {
launch_cmd["colorScheme"] = json!(cs);
}
match send_command(launch_cmd, &flags.session) {
Ok(resp) => {
if !resp.success {
let msg = resp
.error
.unwrap_or_else(|| "CDP connection failed".to_string());
if flags.json {
println!(r#"{{"success":false,"error":"{}"}}"#, msg);
} else {
eprintln!("{} {}", color::error_indicator(), msg);
}
exit(1);
}
}
Err(e) => {
if flags.json {
println!(r#"{{"success":false,"error":"{}"}}"#, e);
} else {
eprintln!("{} {}", color::error_indicator(), e);
}
exit(1);
if let Some(ref dp) = flags.download_path {
launch_cmd["downloadPath"] = json!(dp);
}
let err = match send_command(launch_cmd, &flags.session) {
Ok(resp) if resp.success => None,
Ok(resp) => Some(
resp.error
.unwrap_or_else(|| "CDP connection failed".to_string()),
),
Err(e) => Some(e.to_string()),
};
if let Some(msg) = err {
if flags.json {
println!(r#"{{"success":false,"error":"{}"}}"#, msg);
} else {
eprintln!("{} {}", color::error_indicator(), msg);
}
exit(1);
}
}
@@ -599,7 +602,8 @@ fn main() {
|| flags.ignore_https_errors
|| flags.allow_file_access
|| flags.debug
|| flags.color_scheme.is_some())
|| flags.color_scheme.is_some()
|| flags.download_path.is_some())
&& flags.cdp.is_none()
&& flags.provider.is_none()
&& !launched_via_default_cdp
@@ -661,6 +665,10 @@ fn main() {
launch_cmd["colorScheme"] = json!(cs);
}
if let Some(ref dp) = flags.download_path {
launch_cmd["downloadPath"] = json!(dp);
}
match send_command(launch_cmd, &flags.session) {
Ok(resp) => {
if !resp.success {
+8 -2
View File
@@ -995,14 +995,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 <sel> CSS selector for a scrollable container
Global Options:
--json Output as JSON
--session <name> Use specific session
@@ -1012,6 +1015,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" => {
@@ -2130,6 +2134,7 @@ Options:
--auto-connect Auto-discover and connect to running Chrome
Project default: require existing browser at localhost:9333 (no auto local fallback)
--color-scheme <scheme> Color scheme: dark, light, no-preference (or AGENT_BROWSER_COLOR_SCHEME)
--download-path <path> Default download directory (or AGENT_BROWSER_DOWNLOAD_PATH)
--session-name <name> Auto-save/restore session state (cookies, localStorage)
--config <path> Use a custom config file (or AGENT_BROWSER_CONFIG env)
--debug Debug output
@@ -2180,6 +2185,7 @@ Environment:
AGENT_BROWSER_LOCALE Override auto-detected locale (e.g., zh-TW, ja-JP)
AGENT_BROWSER_TIMEZONE Override auto-detected timezone (e.g., Asia/Taipei)
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 Playwright timeout in ms (default: 25000)
AGENT_BROWSER_SESSION_NAME Auto-save/load state persistence name
AGENT_BROWSER_STATE_EXPIRE_DAYS Auto-delete saved states older than N days (default: 30)