diff --git a/cli/src/commands.rs b/cli/src/commands.rs index 274f99767..682156221 100644 --- a/cli/src/commands.rs +++ b/cli/src/commands.rs @@ -2385,6 +2385,7 @@ mod tests { model: None, verbose: false, quiet: false, + background: false, } } diff --git a/cli/src/connection.rs b/cli/src/connection.rs index 5520294c3..43a191336 100644 --- a/cli/src/connection.rs +++ b/cli/src/connection.rs @@ -227,6 +227,7 @@ pub struct DaemonOptions<'a> { pub default_timeout: Option, pub cdp: Option<&'a str>, pub no_auto_dialog: bool, + pub background: bool, } fn apply_daemon_env(cmd: &mut Command, session: &str, opts: &DaemonOptions) { @@ -314,6 +315,9 @@ fn apply_daemon_env(cmd: &mut Command, session: &str, opts: &DaemonOptions) { if opts.no_auto_dialog { cmd.env("AGENT_BROWSER_NO_AUTO_DIALOG", "1"); } + if opts.background { + cmd.env("AGENT_BROWSER_BACKGROUND", "1"); + } } /// Check if the running daemon's version matches this CLI binary. diff --git a/cli/src/flags.rs b/cli/src/flags.rs index 6e517c915..35820d953 100644 --- a/cli/src/flags.rs +++ b/cli/src/flags.rs @@ -89,6 +89,7 @@ pub struct Config { pub idle_timeout: Option, pub no_auto_dialog: Option, pub model: Option, + pub background: Option, } impl Config { @@ -136,6 +137,7 @@ impl Config { idle_timeout: other.idle_timeout.or(self.idle_timeout), no_auto_dialog: other.no_auto_dialog.or(self.no_auto_dialog), model: other.model.or(self.model), + background: other.background.or(self.background), } } } @@ -308,6 +310,7 @@ pub struct Flags { pub model: Option, pub verbose: bool, pub quiet: bool, + pub background: bool, // Track which launch-time options were explicitly passed via CLI // (as opposed to being set only via environment variables) @@ -447,6 +450,8 @@ pub fn parse_flags(args: &[String]) -> Flags { model: env::var("AI_GATEWAY_MODEL").ok().or(config.model), verbose: false, quiet: false, + background: env_var_is_truthy("AGENT_BROWSER_BACKGROUND") + || config.background.unwrap_or(false), cli_executable_path: false, cli_extensions: false, cli_profile: false, @@ -728,6 +733,13 @@ pub fn parse_flags(args: &[String]) -> Flags { i += 1; } } + "--background" => { + let (val, consumed) = parse_bool_arg(args, i); + flags.background = val; + if consumed { + i += 1; + } + } "--model" => { if let Some(s) = args.get(i + 1) { flags.model = Some(s.clone()); @@ -767,6 +779,7 @@ pub fn clean_args(args: &[String]) -> Vec { "--content-boundaries", "--confirm-interactive", "--no-auto-dialog", + "--background", "-v", "--verbose", "-q", diff --git a/cli/src/main.rs b/cli/src/main.rs index afc8cb488..7e2c5cd87 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -844,6 +844,7 @@ fn main() { default_timeout: flags.default_timeout, cdp: flags.cdp.as_deref(), no_auto_dialog: flags.no_auto_dialog, + background: flags.background, }; let daemon_result = match ensure_daemon(&flags.session, &daemon_opts) { @@ -1287,6 +1288,10 @@ fn main() { let output_opts = OutputOptions::from_flags(&flags); + if flags.background { + cmd["background"] = json!(true); + } + match send_command(cmd.clone(), &flags.session) { Ok(resp) => { let success = resp.success; diff --git a/cli/src/native/actions.rs b/cli/src/native/actions.rs index d62341a77..327699084 100644 --- a/cli/src/native/actions.rs +++ b/cli/src/native/actions.rs @@ -238,6 +238,9 @@ pub struct DaemonState { /// When true, automatically dismiss `beforeunload` dialogs and accept `alert` /// dialogs so they never block the agent. Enabled by default. pub auto_dialog: bool, + /// When true, new tabs are created in the background and tab switches skip + /// Page.bringToFront so the user's current browser tab is not disturbed. + pub background: bool, /// Shared slot for stream server to receive CDP client when browser launches. pub stream_client: Option>>>>, /// Stream server instance kept alive so the broadcast channel remains open. @@ -293,6 +296,10 @@ impl DaemonState { env::var("AGENT_BROWSER_NO_AUTO_DIALOG").as_deref(), Ok("1" | "true" | "yes") ), + background: matches!( + env::var("AGENT_BROWSER_BACKGROUND").as_deref(), + Ok("1" | "true" | "yes") + ), stream_client: None, stream_server: None, launch_hash: None, @@ -1479,14 +1486,16 @@ pub async fn execute_command(cmd: &Value, state: &mut DaemonState) -> Value { /// Connect to a running Chrome via auto-discovery and open a fresh tab so /// subsequent navigations don't hijack the user's existing tabs. -async fn connect_auto_with_fresh_tab() -> Result { +async fn connect_auto_with_fresh_tab(background: bool) -> Result { let mut mgr = BrowserManager::connect_auto().await?; - mgr.tab_new(None).await?; - let session_id = mgr.active_session_id()?.to_string(); - let _ = mgr - .client - .send_command("Page.bringToFront", None, Some(&session_id)) - .await; + mgr.tab_new(None, background).await?; + if !background { + let session_id = mgr.active_session_id()?.to_string(); + let _ = mgr + .client + .send_command("Page.bringToFront", None, Some(&session_id)) + .await; + } Ok(mgr) } @@ -1528,7 +1537,7 @@ async fn auto_launch(state: &mut DaemonState) -> Result<(), String> { if env::var("AGENT_BROWSER_AUTO_CONNECT").is_ok() { state.reset_input_state(); - state.browser = Some(connect_auto_with_fresh_tab().await?); + state.browser = Some(connect_auto_with_fresh_tab(state.background).await?); state.subscribe_to_browser_events(); state.start_fetch_handler(); state.start_dialog_handler(); @@ -1818,7 +1827,7 @@ async fn handle_launch(cmd: &Value, state: &mut DaemonState) -> Result Result Result { @@ -2506,7 +2526,7 @@ async fn handle_click(cmd: &Value, state: &mut DaemonState) -> Result Result Result { @@ -3570,7 +3590,7 @@ async fn handle_tab_switch(cmd: &Value, state: &mut DaemonState) -> Result) -> Result { + pub async fn tab_new(&mut self, url: Option<&str>, background: bool) -> Result { let target_url = url.unwrap_or("about:blank"); let result: CreateTargetResult = self @@ -855,6 +857,7 @@ impl BrowserManager { "Target.createTarget", &CreateTargetParams { url: target_url.to_string(), + background: if background { Some(true) } else { None }, }, None, ) @@ -887,7 +890,7 @@ impl BrowserManager { Ok(json!({ "index": index, "url": target_url })) } - pub async fn tab_switch(&mut self, index: usize) -> Result { + pub async fn tab_switch(&mut self, index: usize, background: bool) -> Result { if index >= self.pages.len() { return Err(format!( "Tab index {} out of range (0-{})", @@ -900,11 +903,12 @@ impl BrowserManager { let session_id = self.pages[index].session_id.clone(); self.enable_domains(&session_id).await?; - // Bring tab to front - let _ = self - .client - .send_command("Page.bringToFront", None, Some(&session_id)) - .await; + if !background { + let _ = self + .client + .send_command("Page.bringToFront", None, Some(&session_id)) + .await; + } let url = self.get_url().await.unwrap_or_default(); let title = self.get_title().await.unwrap_or_default(); diff --git a/cli/src/native/cdp/types.rs b/cli/src/native/cdp/types.rs index 54eb184c9..fb06debd1 100644 --- a/cli/src/native/cdp/types.rs +++ b/cli/src/native/cdp/types.rs @@ -141,6 +141,8 @@ pub struct SetDiscoverTargetsParams { #[serde(rename_all = "camelCase")] pub struct CreateTargetParams { pub url: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub background: Option, } #[derive(Debug, Deserialize)] diff --git a/cli/src/native/state.rs b/cli/src/native/state.rs index babc8ef07..94c53fb5c 100644 --- a/cli/src/native/state.rs +++ b/cli/src/native/state.rs @@ -119,6 +119,7 @@ async fn collect_storage_via_temp_target( "Target.createTarget", &CreateTargetParams { url: "about:blank".to_string(), + background: None, }, None, ) diff --git a/cli/src/output.rs b/cli/src/output.rs index 338959b9c..944d6bf42 100644 --- a/cli/src/output.rs +++ b/cli/src/output.rs @@ -2934,6 +2934,7 @@ Options: --confirm-interactive Interactive confirmation prompts; auto-denies if stdin is not a TTY (or AGENT_BROWSER_CONFIRM_INTERACTIVE) --engine Browser engine: chrome (default), lightpanda (or AGENT_BROWSER_ENGINE) --no-auto-dialog Disable automatic dismissal of alert/beforeunload dialogs (or AGENT_BROWSER_NO_AUTO_DIALOG) + --background Create new tabs in the background and skip focus switching (or AGENT_BROWSER_BACKGROUND) --model AI model for chat (or AI_GATEWAY_MODEL env) -v, --verbose Show tool commands and their raw output -q, --quiet Show only AI text responses (hide tool calls)