From 32ffd8f3c45f6b52490e106006e08136571c26aa Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Tue, 24 Mar 2026 11:38:23 -0500 Subject: [PATCH] feat: add dialog detection and document dialog commands (#999) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #992 When a JavaScript dialog (alert/confirm/prompt) blocks the page, agents had no way to detect it — all commands just timed out with generic errors. - Add `dialog status` command to check for pending dialogs - Track dialog state via CDP Page.javascriptDialogOpening/Closed events - Auto-inject `warning` field into all command responses when a dialog is pending, so agents can distinguish dialog-blocked timeouts from other issues - Document dialog commands in SKILL.md (was missing entirely), README.md, docs site, and --help output Co-authored-by: ctate <366502+ctate@users.noreply.github.com> --- README.md | 3 ++ cli/src/commands.rs | 5 ++- cli/src/connection.rs | 2 + cli/src/main.rs | 2 + cli/src/native/actions.rs | 78 +++++++++++++++++++++++++++++++--- cli/src/output.rs | 47 +++++++++++++++++++- docs/src/app/commands/page.mdx | 3 ++ skills/agent-browser/SKILL.md | 26 ++++++++++++ 8 files changed, 155 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index fb6ebfa..3e54385 100644 --- a/README.md +++ b/README.md @@ -300,8 +300,11 @@ agent-browser frame main # Back to main frame ```bash agent-browser dialog accept [text] # Accept (with optional prompt text) agent-browser dialog dismiss # Dismiss +agent-browser dialog status # Check if a dialog is currently open ``` +When a JavaScript dialog is pending, all command responses include a `warning` field with the dialog type and message. + ### Diff ```bash diff --git a/cli/src/commands.rs b/cli/src/commands.rs index d63bdac..cf95c1f 100644 --- a/cli/src/commands.rs +++ b/cli/src/commands.rs @@ -987,7 +987,7 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result { - const VALID: &[&str] = &["accept", "dismiss"]; + const VALID: &[&str] = &["accept", "dismiss", "status"]; match rest.first().copied() { Some("accept") => { let mut cmd = json!({ "id": id, "action": "dialog", "response": "accept" }); @@ -1003,13 +1003,14 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result Ok(json!({ "id": id, "action": "dialog", "response": "status" })), Some(sub) => Err(ParseError::UnknownSubcommand { subcommand: sub.to_string(), valid_options: VALID, }), None => Err(ParseError::MissingArguments { context: "dialog".to_string(), - usage: "dialog [text]", + usage: "dialog [text]", }), } } diff --git a/cli/src/connection.rs b/cli/src/connection.rs index 9665140..2665d38 100644 --- a/cli/src/connection.rs +++ b/cli/src/connection.rs @@ -26,6 +26,8 @@ pub struct Response { pub success: bool, pub data: Option, pub error: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub warning: Option, } #[allow(dead_code)] diff --git a/cli/src/main.rs b/cli/src/main.rs index eb3a2db..a059dea 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -313,11 +313,13 @@ fn main() { success: true, data: Some(data), error: None, + warning: None, }, Err(e) => connection::Response { success: false, data: None, error: Some(e), + warning: None, }, }; let output_opts = OutputOptions::from_flags(&flags); diff --git a/cli/src/native/actions.rs b/cli/src/native/actions.rs index 7ccd491..114ca74 100644 --- a/cli/src/native/actions.rs +++ b/cli/src/native/actions.rs @@ -14,8 +14,8 @@ use super::cdp::chrome::LaunchOptions; use super::cdp::client::CdpClient; use super::cdp::types::{ AttachToTargetParams, AttachToTargetResult, CdpEvent, ConsoleApiCalledEvent, - CreateTargetResult, DispatchMouseEventParams, ExceptionThrownEvent, TargetCreatedEvent, - TargetDestroyedEvent, TargetInfoChangedEvent, + CreateTargetResult, DispatchMouseEventParams, ExceptionThrownEvent, + JavascriptDialogOpeningEvent, TargetCreatedEvent, TargetDestroyedEvent, TargetInfoChangedEvent, }; use super::cookies; use super::diff; @@ -136,6 +136,14 @@ pub enum BackendType { WebDriver, } +#[derive(Debug, Clone, Default)] +pub struct PendingDialog { + pub dialog_type: String, + pub message: String, + pub url: String, + pub default_prompt: Option, +} + #[derive(Debug, Clone, Copy, Default)] pub struct MouseState { pub x: f64, @@ -192,6 +200,8 @@ pub struct DaemonState { /// without deadlocking navigation/evaluate. fetch_handler_task: Option>, pub mouse_state: MouseState, + /// Tracks the currently open JavaScript dialog (alert/confirm/prompt), if any. + pub pending_dialog: Option, /// 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. @@ -234,6 +244,7 @@ impl DaemonState { origin_headers: Arc::new(RwLock::new(HashMap::new())), fetch_handler_task: None, mouse_state: MouseState::default(), + pending_dialog: None, stream_client: None, stream_server: None, } @@ -718,6 +729,23 @@ impl DaemonState { } } } + "Page.javascriptDialogOpening" => { + if let Ok(dialog_event) = + serde_json::from_value::( + 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, + }); + } + } + "Page.javascriptDialogClosed" => { + self.pending_dialog = None; + } // Fetch.requestPaused is handled by the background // fetch_handler_task — no need to collect here. _ => {} @@ -1114,10 +1142,27 @@ pub async fn execute_command(cmd: &Value, state: &mut DaemonState) -> Value { _ => Err(format!("Not yet implemented: {}", action)), }; - match result { + let mut resp = match result { Ok(data) => success_response(&id, data), Err(e) => error_response(&id, &super::browser::to_ai_friendly_error(&e)), + }; + + // Auto-report pending JavaScript dialog so agents know why commands may hang + if action != "dialog" { + if let Some(ref dialog) = state.pending_dialog { + if let Some(obj) = resp.as_object_mut() { + obj.insert( + "warning".to_string(), + json!(format!( + "A JavaScript {} dialog is blocking the page: \"{}\" — use `dialog accept` or `dialog dismiss` to resolve it", + dialog.dialog_type, dialog.message + )), + ); + } + } } + + resp } // --------------------------------------------------------------------------- @@ -3805,17 +3850,36 @@ async fn handle_permissions(cmd: &Value, state: &DaemonState) -> Result Result { +async fn handle_dialog(cmd: &Value, state: &mut DaemonState) -> Result { + let response = cmd.get("response").and_then(|v| v.as_str()); + + // dialog status — return pending dialog info + if response == Some("status") { + return Ok(match &state.pending_dialog { + Some(dialog) => { + let mut obj = json!({ + "hasDialog": true, + "type": dialog.dialog_type, + "message": dialog.message, + }); + if let Some(ref prompt) = dialog.default_prompt { + obj["defaultPrompt"] = json!(prompt); + } + obj + } + None => json!({ "hasDialog": false }), + }); + } + let mgr = state.browser.as_ref().ok_or("Browser not launched")?; - let accept = cmd - .get("response") - .and_then(|v| v.as_str()) + let accept = response .map(|r| r == "accept") .or_else(|| cmd.get("accept").and_then(|v| v.as_bool())) .unwrap_or(true); let prompt_text = cmd.get("promptText").and_then(|v| v.as_str()); mgr.handle_dialog(accept, prompt_text).await?; + state.pending_dialog = None; Ok(json!({ "handled": true, "accepted": accept })) } diff --git a/cli/src/output.rs b/cli/src/output.rs index 06875d5..34f6e08 100644 --- a/cli/src/output.rs +++ b/cli/src/output.rs @@ -122,6 +122,7 @@ pub fn print_response_with_opts(resp: &Response, action: Option<&str>, opts: &Ou } else { println!("{}", serde_json::to_string(resp).unwrap_or_default()); } + // JSON mode includes the warning field in the JSON payload already return; } @@ -131,10 +132,42 @@ pub fn print_response_with_opts(resp: &Response, action: Option<&str>, opts: &Ou color::error_indicator(), resp.error.as_deref().unwrap_or("Unknown error") ); + // Still print dialog warning after errors, since a pending dialog + // is the most common cause of commands timing out + if let Some(ref warning) = resp.warning { + eprintln!("{} {}", color::warning_indicator(), warning); + } return; } if let Some(data) = &resp.data { + // Dialog status response + if action == Some("dialog") { + if let Some(has_dialog) = data.get("hasDialog").and_then(|v| v.as_bool()) { + if has_dialog { + let dtype = data + .get("type") + .and_then(|v| v.as_str()) + .unwrap_or("unknown"); + let message = data.get("message").and_then(|v| v.as_str()).unwrap_or(""); + println!( + "{} JavaScript {} dialog is open: \"{}\"", + color::warning_indicator(), + dtype, + message + ); + if let Some(default_prompt) = data.get("defaultPrompt").and_then(|v| v.as_str()) + { + println!(" Default prompt text: \"{}\"", default_prompt); + } + println!(" Use `dialog accept [text]` or `dialog dismiss` to resolve it"); + } else { + println!("{} No dialog is currently open", color::success_indicator()); + } + print_warning(resp); + return; + } + } if action == Some("storage_get") { if let Some(output) = format_storage_text(data) { println!("{}", output); @@ -888,6 +921,14 @@ pub fn print_response_with_opts(resp: &Response, action: Option<&str>, opts: &Ou // Default success println!("{} Done", color::success_indicator()); } + + print_warning(resp); +} + +fn print_warning(resp: &Response) { + if let Some(ref warning) = resp.warning { + eprintln!("{} {}", color::warning_indicator(), warning); + } } /// Print command-specific help. Returns true if help was printed, false if command unknown. @@ -1995,13 +2036,14 @@ Examples: r##" agent-browser dialog - Handle browser dialogs -Usage: agent-browser dialog [text] +Usage: agent-browser dialog [text] -Respond to browser dialogs (alert, confirm, prompt). +Respond to or check for browser dialogs (alert, confirm, prompt). Operations: accept [text] Accept dialog, optionally with prompt text dismiss Dismiss/cancel dialog + status Check if a dialog is currently open Global Options: --json Output as JSON @@ -2011,6 +2053,7 @@ Examples: agent-browser dialog accept agent-browser dialog accept "my input" agent-browser dialog dismiss + agent-browser dialog status "## } diff --git a/docs/src/app/commands/page.mdx b/docs/src/app/commands/page.mdx index 9258655..cbc6e34 100644 --- a/docs/src/app/commands/page.mdx +++ b/docs/src/app/commands/page.mdx @@ -223,8 +223,11 @@ The `frame` command accepts element refs (`@e3`), CSS selectors (`"#my-iframe"`) ```bash agent-browser dialog accept [text] # Accept dialog (with optional prompt text) agent-browser dialog dismiss # Dismiss dialog +agent-browser dialog status # Check if a dialog is currently open ``` +When a JavaScript dialog (`alert`, `confirm`, `prompt`) is pending, all command responses include a `warning` field with the dialog type and message. + ## Debug ```bash diff --git a/skills/agent-browser/SKILL.md b/skills/agent-browser/SKILL.md index c4bfa93..ba9cb20 100644 --- a/skills/agent-browser/SKILL.md +++ b/skills/agent-browser/SKILL.md @@ -177,6 +177,12 @@ 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) +agent-browser dialog accept # Accept dialog +agent-browser dialog accept "my input" # Accept prompt dialog with text +agent-browser dialog dismiss # Dismiss/cancel dialog +agent-browser dialog status # Check if a dialog is currently open + # Diff (compare page states) agent-browser diff snapshot # Compare current vs last snapshot agent-browser diff snapshot --baseline before.txt # Compare current vs saved file @@ -522,6 +528,26 @@ agent-browser wait 5000 When dealing with consistently slow websites, use `wait --load networkidle` after `open` to ensure the page is fully loaded before taking a snapshot. If a specific element is slow to render, wait for it directly with `wait ` or `wait @ref`. +## JavaScript Dialogs (alert / confirm / prompt) + +When a page opens a JavaScript dialog (`alert()`, `confirm()`, or `prompt()`), it blocks all other browser commands (snapshot, screenshot, click, etc.) until the dialog is dismissed. If commands start timing out unexpectedly, check for a pending dialog: + +```bash +# Check if a dialog is blocking +agent-browser dialog status + +# Accept the dialog (dismiss the alert / click OK) +agent-browser dialog accept + +# Accept a prompt dialog with input text +agent-browser dialog accept "my input" + +# Dismiss the dialog (click Cancel) +agent-browser dialog dismiss +``` + +When a dialog is pending, all command responses include a `warning` field indicating the dialog type and message. In `--json` mode this appears as a `"warning"` key in the response object. + ## Session Management and Cleanup When running multiple agents or automations concurrently, always use named sessions to avoid conflicts: