From 6dd53449e815fceca9441d0b281fe03997be877e Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 29 Mar 2026 12:00:27 -0600 Subject: [PATCH] Add auto-dismissal for alert and beforeunload dialogs (#1075) * Add auto-dismissal for alert and beforeunload dialogs This PR adds automatic handling of JavaScript dialogs to prevent the agent from blocking indefinitely when `alert()` or `beforeunload` dialogs appear on web pages. ## Summary Previously, when a website displayed native browser confirmation dialogs (like alerts or "Are you sure you want to leave?" prompts), agent-browser would hang waiting for manual intervention. This is a common issue since many websites use these dialogs for notifications or navigation warnings. ## Changes Made - **Auto-dismiss functionality**: Added a background task that automatically accepts `alert` and `beforeunload` dialogs while leaving `confirm` and `prompt` dialogs for explicit handling - **New flag**: Added `--no-auto-dialog` flag to disable automatic handling when needed - **Environment variable**: Added `AGENT_BROWSER_NO_AUTO_DIALOG` for configuration - **Documentation**: Updated README and docs with usage examples and configuration details - **Tests**: Added comprehensive test coverage for flag parsing and dialog handling logic ## Implementation Details - Only `alert` (notification-only) and `beforeunload` (navigation warning) dialogs are auto-handled for safety - `confirm` and `prompt` dialogs still require explicit `dialog accept/dismiss` commands to ensure agents make deliberate choices for destructive actions - The feature is enabled by default since these dialog types rarely require user decision-making - Uses Chrome DevTools Protocol's `Page.handleJavaScriptDialog` for reliable dialog dismissal Fixes #1070 * Log dialog type and message before auto-dismissal Without this, auto-dismissed alert/beforeunload dialogs are silently swallowed and the agent has no way to see what the dialog said. Adding an eprintln before the CDP call makes the dismissal visible in stderr for debugging. * Log dialog dismissal errors instead of silently discarding them - Remove premature "accepted" from log message since it fires before the CDP command executes - Replace `let _ =` with `if let Err(e)` to log failures when Page.handleJavaScriptDialog fails - Apply rustfmt to auto-dialog tests --------- Co-authored-by: ctate <366502+ctate@users.noreply.github.com> --- README.md | 3 + cli/src/commands.rs | 1 + cli/src/connection.rs | 4 + cli/src/flags.rs | 36 ++++ cli/src/main.rs | 1 + cli/src/native/actions.rs | 172 +++++++++++++++++++- cli/src/output.rs | 2 + docs/src/app/commands/page.mdx | 2 + docs/src/app/configuration/page.mdx | 2 + skills/agent-browser/SKILL.md | 5 +- skills/agent-browser/references/commands.md | 3 + 11 files changed, 224 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 19b10f3..9005e39 100644 --- a/README.md +++ b/README.md @@ -307,6 +307,8 @@ agent-browser dialog dismiss # Dismiss agent-browser dialog status # Check if a dialog is currently open ``` +By default, `alert` and `beforeunload` dialogs are automatically accepted so they never block the agent. `confirm` and `prompt` dialogs still require explicit handling. Use `--no-auto-dialog` (or `AGENT_BROWSER_NO_AUTO_DIALOG=1`) to disable automatic handling. + When a JavaScript dialog is pending, all command responses include a `warning` field with the dialog type and message. ### Diff @@ -594,6 +596,7 @@ This is useful for multimodal AI models that can reason about visual layout, unl | `--confirm-actions ` | Action categories requiring confirmation (or `AGENT_BROWSER_CONFIRM_ACTIONS` env) | | `--confirm-interactive` | Interactive confirmation prompts; auto-denies if stdin is not a TTY (or `AGENT_BROWSER_CONFIRM_INTERACTIVE` env) | | `--engine ` | Browser engine: `chrome` (default), `lightpanda` (or `AGENT_BROWSER_ENGINE` env) | +| `--no-auto-dialog` | Disable automatic dismissal of `alert`/`beforeunload` dialogs (or `AGENT_BROWSER_NO_AUTO_DIALOG` env) | | `--config ` | Use a custom config file (or `AGENT_BROWSER_CONFIG` env) | | `--debug` | Debug output | diff --git a/cli/src/commands.rs b/cli/src/commands.rs index a802c06..aa24ceb 100644 --- a/cli/src/commands.rs +++ b/cli/src/commands.rs @@ -2322,6 +2322,7 @@ mod tests { screenshot_quality: None, screenshot_format: None, idle_timeout: None, + no_auto_dialog: false, } } diff --git a/cli/src/connection.rs b/cli/src/connection.rs index f423164..d532943 100644 --- a/cli/src/connection.rs +++ b/cli/src/connection.rs @@ -219,6 +219,7 @@ pub struct DaemonOptions<'a> { pub auto_connect: bool, pub idle_timeout: Option<&'a str>, pub cdp: Option<&'a str>, + pub no_auto_dialog: bool, } fn apply_daemon_env(cmd: &mut Command, session: &str, opts: &DaemonOptions) { @@ -300,6 +301,9 @@ fn apply_daemon_env(cmd: &mut Command, session: &str, opts: &DaemonOptions) { if let Some(cdp) = opts.cdp { cmd.env("AGENT_BROWSER_CDP", cdp); } + if opts.no_auto_dialog { + cmd.env("AGENT_BROWSER_NO_AUTO_DIALOG", "1"); + } } pub fn ensure_daemon(session: &str, opts: &DaemonOptions) -> Result { diff --git a/cli/src/flags.rs b/cli/src/flags.rs index 501127a..806d4e8 100644 --- a/cli/src/flags.rs +++ b/cli/src/flags.rs @@ -87,6 +87,7 @@ pub struct Config { pub screenshot_quality: Option, pub screenshot_format: Option, pub idle_timeout: Option, + pub no_auto_dialog: Option, } impl Config { @@ -132,6 +133,7 @@ impl Config { screenshot_quality: other.screenshot_quality.or(self.screenshot_quality), screenshot_format: other.screenshot_format.or(self.screenshot_format), idle_timeout: other.idle_timeout.or(self.idle_timeout), + no_auto_dialog: other.no_auto_dialog.or(self.no_auto_dialog), } } } @@ -298,6 +300,7 @@ pub struct Flags { pub screenshot_quality: Option, pub screenshot_format: Option, pub idle_timeout: Option, // Canonical milliseconds string for AGENT_BROWSER_IDLE_TIMEOUT_MS + pub no_auto_dialog: bool, // Track which launch-time options were explicitly passed via CLI // (as opposed to being set only via environment variables) @@ -429,6 +432,8 @@ pub fn parse_flags(args: &[String]) -> Flags { "AGENT_BROWSER_IDLE_TIMEOUT_MS", ) .or(config.idle_timeout), + no_auto_dialog: env_var_is_truthy("AGENT_BROWSER_NO_AUTO_DIALOG") + || config.no_auto_dialog.unwrap_or(false), cli_executable_path: false, cli_extensions: false, cli_profile: false, @@ -703,6 +708,13 @@ pub fn parse_flags(args: &[String]) -> Flags { i += 1; } } + "--no-auto-dialog" => { + let (val, consumed) = parse_bool_arg(args, i); + flags.no_auto_dialog = val; + if consumed { + i += 1; + } + } "--config" => { // Already handled by load_config(); skip the value i += 1; @@ -729,6 +741,7 @@ pub fn clean_args(args: &[String]) -> Vec { "--annotate", "--content-boundaries", "--confirm-interactive", + "--no-auto-dialog", ]; // Global flags that always take a value (need to skip the next arg too) const GLOBAL_FLAGS_WITH_VALUE: &[&str] = &[ @@ -1375,4 +1388,27 @@ mod tests { let merged = user.merge(project); assert_eq!(merged.extensions, Some(vec!["/ext2".to_string()])); } + + #[test] + fn test_no_auto_dialog_flag() { + let flags = parse_flags(&args("open example.com --no-auto-dialog")); + assert!(flags.no_auto_dialog); + } + + #[test] + fn test_no_auto_dialog_default_false() { + let flags = parse_flags(&args("open example.com")); + assert!(!flags.no_auto_dialog); + } + + #[test] + fn test_clean_args_removes_no_auto_dialog() { + let input: Vec = vec![ + "open".to_string(), + "example.com".to_string(), + "--no-auto-dialog".to_string(), + ]; + let clean = clean_args(&input); + assert_eq!(clean, vec!["open", "example.com"]); + } } diff --git a/cli/src/main.rs b/cli/src/main.rs index e046b83..979f592 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -721,6 +721,7 @@ fn main() { auto_connect: flags.auto_connect, idle_timeout: flags.idle_timeout.as_deref(), cdp: flags.cdp.as_deref(), + no_auto_dialog: flags.no_auto_dialog, }; let daemon_result = match ensure_daemon(&flags.session, &daemon_opts) { diff --git a/cli/src/native/actions.rs b/cli/src/native/actions.rs index 781a6dc..7048fc3 100644 --- a/cli/src/native/actions.rs +++ b/cli/src/native/actions.rs @@ -205,9 +205,15 @@ pub struct DaemonState { /// handling domain filtering, route interception, and origin-scoped headers /// without deadlocking navigation/evaluate. fetch_handler_task: Option>, + /// Background task that auto-accepts `alert` and `beforeunload` dialogs + /// so they never block the agent. + dialog_handler_task: Option>, pub mouse_state: MouseState, /// Tracks the currently open JavaScript dialog (alert/confirm/prompt), if any. pub pending_dialog: Option, + /// When true, automatically dismiss `beforeunload` dialogs and accept `alert` + /// dialogs so they never block the agent. Enabled by default. + pub auto_dialog: 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. @@ -252,8 +258,13 @@ impl DaemonState { origin_headers: Arc::new(RwLock::new(HashMap::new())), proxy_credentials: Arc::new(RwLock::new(None)), fetch_handler_task: None, + dialog_handler_task: None, mouse_state: MouseState::default(), pending_dialog: None, + auto_dialog: !matches!( + env::var("AGENT_BROWSER_NO_AUTO_DIALOG").as_deref(), + Ok("1" | "true" | "yes") + ), stream_client: None, stream_server: None, engine: env::var("AGENT_BROWSER_ENGINE").unwrap_or_else(|_| "chrome".to_string()), @@ -398,6 +409,65 @@ impl DaemonState { })); } + /// Start the background task that auto-accepts `alert` and `beforeunload` + /// dialogs so they never block the agent. `confirm` and `prompt` dialogs + /// are left for the agent to handle explicitly. + fn start_dialog_handler(&mut self) { + if let Some(task) = self.dialog_handler_task.take() { + task.abort(); + } + + if !self.auto_dialog { + return; + } + + let Some(ref browser) = self.browser else { + return; + }; + + let client = browser.client.clone(); + let mut rx = browser.client.subscribe(); + + self.dialog_handler_task = Some(tokio::spawn(async move { + loop { + match rx.recv().await { + Ok(event) if event.method == "Page.javascriptDialogOpening" => { + let dialog_type = event + .params + .get("type") + .and_then(|v| v.as_str()) + .unwrap_or(""); + if matches!(dialog_type, "beforeunload" | "alert") { + let message = event + .params + .get("message") + .and_then(|v| v.as_str()) + .unwrap_or(""); + eprintln!("[auto-dismiss] {} dialog: {}", dialog_type, message); + let sid = event.session_id.clone().unwrap_or_default(); + if let Err(e) = client + .send_command( + "Page.handleJavaScriptDialog", + Some(json!({ "accept": true })), + Some(&sid), + ) + .await + { + eprintln!( + "[auto-dismiss] failed to dismiss {} dialog: {}", + dialog_type, e + ); + } + } + } + Ok(_) => continue, + Err(broadcast::error::RecvError::Lagged(_)) => continue, + Err(_) => break, + } + } + })); + } + /// Update the stream server's CDP client slot when browser is set or cleared. pub async fn update_stream_client(&self) { if let Some(ref slot) = self.stream_client { @@ -918,12 +988,22 @@ impl DaemonState { event.params.clone(), ) { - self.pending_dialog = Some(PendingDialog { - dialog_type: dialog_event.dialog_type, - message: dialog_event.message, - url: dialog_event.url, - default_prompt: dialog_event.default_prompt, - }); + // When auto_dialog is enabled, alert and beforeunload + // dialogs are handled by the background dialog_handler_task. + // Skip tracking them to avoid a stale warning. + let auto_handled = self.auto_dialog + && matches!( + dialog_event.dialog_type.as_str(), + "beforeunload" | "alert" + ); + if !auto_handled { + self.pending_dialog = Some(PendingDialog { + dialog_type: dialog_event.dialog_type, + message: dialog_event.message, + url: dialog_event.url, + default_prompt: dialog_event.default_prompt, + }); + } } } "Page.javascriptDialogClosed" => { @@ -961,6 +1041,9 @@ impl Drop for DaemonState { if let Some(task) = self.fetch_handler_task.take() { task.abort(); } + if let Some(task) = self.dialog_handler_task.take() { + task.abort(); + } } } @@ -1342,6 +1425,7 @@ async fn auto_launch(state: &mut DaemonState) -> Result<(), String> { state.browser = Some(mgr); state.subscribe_to_browser_events(); state.start_fetch_handler(); + state.start_dialog_handler(); state.update_stream_client().await; try_auto_restore_state(state).await; return Ok(()); @@ -1352,6 +1436,7 @@ async fn auto_launch(state: &mut DaemonState) -> Result<(), String> { state.browser = Some(connect_auto_with_fresh_tab().await?); state.subscribe_to_browser_events(); state.start_fetch_handler(); + state.start_dialog_handler(); state.update_stream_client().await; try_auto_restore_state(state).await; return Ok(()); @@ -1362,6 +1447,7 @@ async fn auto_launch(state: &mut DaemonState) -> Result<(), String> { state.browser = Some(mgr); state.subscribe_to_browser_events(); state.start_fetch_handler(); + state.start_dialog_handler(); state.update_stream_client().await; // Enable Fetch with handleAuthRequests for proxy authentication @@ -1505,6 +1591,7 @@ async fn handle_launch(cmd: &Value, state: &mut DaemonState) -> Result Result Result Result Result Categories requiring confirmation (or AGENT_BROWSER_CONFIRM_ACTIONS) --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) --config Use a custom config file (or AGENT_BROWSER_CONFIG env) --debug Debug output --version, -V Show version @@ -2881,6 +2882,7 @@ Environment: AGENT_BROWSER_ACTION_POLICY Path to action policy JSON file AGENT_BROWSER_CONFIRM_ACTIONS Action categories requiring confirmation AGENT_BROWSER_CONFIRM_INTERACTIVE Enable interactive confirmation prompts + AGENT_BROWSER_NO_AUTO_DIALOG Disable automatic dismissal of alert/beforeunload dialogs AGENT_BROWSER_ENGINE Browser engine: chrome (default), lightpanda HTTP_PROXY / HTTPS_PROXY Standard proxy env vars (fallback if AGENT_BROWSER_PROXY not set) ALL_PROXY SOCKS proxy (fallback for proxy) diff --git a/docs/src/app/commands/page.mdx b/docs/src/app/commands/page.mdx index a3e1d05..b6ea89e 100644 --- a/docs/src/app/commands/page.mdx +++ b/docs/src/app/commands/page.mdx @@ -230,6 +230,8 @@ agent-browser dialog dismiss # Dismiss dialog agent-browser dialog status # Check if a dialog is currently open ``` +By default, `alert` and `beforeunload` dialogs are automatically accepted so they never block the agent. `confirm` and `prompt` dialogs still require explicit handling. Use `--no-auto-dialog` (or `AGENT_BROWSER_NO_AUTO_DIALOG=1`) to disable automatic handling. + When a JavaScript dialog (`alert`, `confirm`, `prompt`) is pending, all command responses include a `warning` field with the dialog type and message. ## Streaming diff --git a/docs/src/app/configuration/page.mdx b/docs/src/app/configuration/page.mdx index 36ca990..334f402 100644 --- a/docs/src/app/configuration/page.mdx +++ b/docs/src/app/configuration/page.mdx @@ -77,6 +77,7 @@ Every CLI flag can be set in the config file using its camelCase equivalent: confirmActions--confirm-actionsstring confirmInteractive--confirm-interactiveboolean engine--enginestring (chrome, lightpanda) + noAutoDialog--no-auto-dialogboolean headers--headersstring (JSON) @@ -185,6 +186,7 @@ These environment variables configure additional daemon and runtime behavior: AGENT_BROWSER_CONFIRM_ACTIONSComma-separated action categories requiring confirmation.(none) AGENT_BROWSER_CONFIRM_INTERACTIVEEnable interactive confirmation prompts (auto-denies if stdin is not a TTY).(disabled) AGENT_BROWSER_ENGINEBrowser engine to use: chrome (default), lightpanda.chrome + AGENT_BROWSER_NO_AUTO_DIALOGDisable automatic dismissal of alert/beforeunload dialogs.(disabled) diff --git a/skills/agent-browser/SKILL.md b/skills/agent-browser/SKILL.md index a0979e6..00f97aa 100644 --- a/skills/agent-browser/SKILL.md +++ b/skills/agent-browser/SKILL.md @@ -184,7 +184,10 @@ agent-browser clipboard write "Hello, World!" # Write text to clipboard agent-browser clipboard copy # Copy current selection agent-browser clipboard paste # Paste from clipboard -# Dialogs (alert, confirm, prompt) +# Dialogs (alert, confirm, prompt, beforeunload) +# By default, alert and beforeunload dialogs are auto-accepted so they never block the agent. +# confirm and prompt dialogs still require explicit handling. +# Use --no-auto-dialog to disable automatic handling. agent-browser dialog accept # Accept dialog agent-browser dialog accept "my input" # Accept prompt dialog with text agent-browser dialog dismiss # Dismiss/cancel dialog diff --git a/skills/agent-browser/references/commands.md b/skills/agent-browser/references/commands.md index bae62c6..8fbfe36 100644 --- a/skills/agent-browser/references/commands.md +++ b/skills/agent-browser/references/commands.md @@ -209,9 +209,12 @@ The `frame` command accepts: ## Dialogs +By default, `alert` and `beforeunload` dialogs are automatically accepted so they never block the agent. `confirm` and `prompt` dialogs still require explicit handling. Use `--no-auto-dialog` to disable this behavior. + ```bash agent-browser dialog accept [text] # Accept dialog agent-browser dialog dismiss # Dismiss dialog +agent-browser dialog status # Check if a dialog is currently open ``` ## JavaScript