feat: add dialog detection and document dialog commands (#999)
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>
This commit is contained in:
@@ -300,8 +300,11 @@ agent-browser frame main # Back to main frame
|
|||||||
```bash
|
```bash
|
||||||
agent-browser dialog accept [text] # Accept (with optional prompt text)
|
agent-browser dialog accept [text] # Accept (with optional prompt text)
|
||||||
agent-browser dialog dismiss # Dismiss
|
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
|
### Diff
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
+3
-2
@@ -987,7 +987,7 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result<Value, ParseError
|
|||||||
|
|
||||||
// === Dialog ===
|
// === Dialog ===
|
||||||
"dialog" => {
|
"dialog" => {
|
||||||
const VALID: &[&str] = &["accept", "dismiss"];
|
const VALID: &[&str] = &["accept", "dismiss", "status"];
|
||||||
match rest.first().copied() {
|
match rest.first().copied() {
|
||||||
Some("accept") => {
|
Some("accept") => {
|
||||||
let mut cmd = json!({ "id": id, "action": "dialog", "response": "accept" });
|
let mut cmd = json!({ "id": id, "action": "dialog", "response": "accept" });
|
||||||
@@ -1003,13 +1003,14 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result<Value, ParseError
|
|||||||
}
|
}
|
||||||
Ok(cmd)
|
Ok(cmd)
|
||||||
}
|
}
|
||||||
|
Some("status") => Ok(json!({ "id": id, "action": "dialog", "response": "status" })),
|
||||||
Some(sub) => Err(ParseError::UnknownSubcommand {
|
Some(sub) => Err(ParseError::UnknownSubcommand {
|
||||||
subcommand: sub.to_string(),
|
subcommand: sub.to_string(),
|
||||||
valid_options: VALID,
|
valid_options: VALID,
|
||||||
}),
|
}),
|
||||||
None => Err(ParseError::MissingArguments {
|
None => Err(ParseError::MissingArguments {
|
||||||
context: "dialog".to_string(),
|
context: "dialog".to_string(),
|
||||||
usage: "dialog <accept|dismiss> [text]",
|
usage: "dialog <accept|dismiss|status> [text]",
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ pub struct Response {
|
|||||||
pub success: bool,
|
pub success: bool,
|
||||||
pub data: Option<Value>,
|
pub data: Option<Value>,
|
||||||
pub error: Option<String>,
|
pub error: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub warning: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
|
|||||||
@@ -313,11 +313,13 @@ fn main() {
|
|||||||
success: true,
|
success: true,
|
||||||
data: Some(data),
|
data: Some(data),
|
||||||
error: None,
|
error: None,
|
||||||
|
warning: None,
|
||||||
},
|
},
|
||||||
Err(e) => connection::Response {
|
Err(e) => connection::Response {
|
||||||
success: false,
|
success: false,
|
||||||
data: None,
|
data: None,
|
||||||
error: Some(e),
|
error: Some(e),
|
||||||
|
warning: None,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
let output_opts = OutputOptions::from_flags(&flags);
|
let output_opts = OutputOptions::from_flags(&flags);
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ use super::cdp::chrome::LaunchOptions;
|
|||||||
use super::cdp::client::CdpClient;
|
use super::cdp::client::CdpClient;
|
||||||
use super::cdp::types::{
|
use super::cdp::types::{
|
||||||
AttachToTargetParams, AttachToTargetResult, CdpEvent, ConsoleApiCalledEvent,
|
AttachToTargetParams, AttachToTargetResult, CdpEvent, ConsoleApiCalledEvent,
|
||||||
CreateTargetResult, DispatchMouseEventParams, ExceptionThrownEvent, TargetCreatedEvent,
|
CreateTargetResult, DispatchMouseEventParams, ExceptionThrownEvent,
|
||||||
TargetDestroyedEvent, TargetInfoChangedEvent,
|
JavascriptDialogOpeningEvent, TargetCreatedEvent, TargetDestroyedEvent, TargetInfoChangedEvent,
|
||||||
};
|
};
|
||||||
use super::cookies;
|
use super::cookies;
|
||||||
use super::diff;
|
use super::diff;
|
||||||
@@ -136,6 +136,14 @@ pub enum BackendType {
|
|||||||
WebDriver,
|
WebDriver,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct PendingDialog {
|
||||||
|
pub dialog_type: String,
|
||||||
|
pub message: String,
|
||||||
|
pub url: String,
|
||||||
|
pub default_prompt: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Default)]
|
#[derive(Debug, Clone, Copy, Default)]
|
||||||
pub struct MouseState {
|
pub struct MouseState {
|
||||||
pub x: f64,
|
pub x: f64,
|
||||||
@@ -192,6 +200,8 @@ pub struct DaemonState {
|
|||||||
/// without deadlocking navigation/evaluate.
|
/// without deadlocking navigation/evaluate.
|
||||||
fetch_handler_task: Option<tokio::task::JoinHandle<()>>,
|
fetch_handler_task: Option<tokio::task::JoinHandle<()>>,
|
||||||
pub mouse_state: MouseState,
|
pub mouse_state: MouseState,
|
||||||
|
/// Tracks the currently open JavaScript dialog (alert/confirm/prompt), if any.
|
||||||
|
pub pending_dialog: Option<PendingDialog>,
|
||||||
/// Shared slot for stream server to receive CDP client when browser launches.
|
/// Shared slot for stream server to receive CDP client when browser launches.
|
||||||
pub stream_client: Option<Arc<RwLock<Option<Arc<CdpClient>>>>>,
|
pub stream_client: Option<Arc<RwLock<Option<Arc<CdpClient>>>>>,
|
||||||
/// Stream server instance kept alive so the broadcast channel remains open.
|
/// 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())),
|
origin_headers: Arc::new(RwLock::new(HashMap::new())),
|
||||||
fetch_handler_task: None,
|
fetch_handler_task: None,
|
||||||
mouse_state: MouseState::default(),
|
mouse_state: MouseState::default(),
|
||||||
|
pending_dialog: None,
|
||||||
stream_client: None,
|
stream_client: None,
|
||||||
stream_server: None,
|
stream_server: None,
|
||||||
}
|
}
|
||||||
@@ -718,6 +729,23 @@ impl DaemonState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"Page.javascriptDialogOpening" => {
|
||||||
|
if let Ok(dialog_event) =
|
||||||
|
serde_json::from_value::<JavascriptDialogOpeningEvent>(
|
||||||
|
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.requestPaused is handled by the background
|
||||||
// fetch_handler_task — no need to collect here.
|
// 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)),
|
_ => Err(format!("Not yet implemented: {}", action)),
|
||||||
};
|
};
|
||||||
|
|
||||||
match result {
|
let mut resp = match result {
|
||||||
Ok(data) => success_response(&id, data),
|
Ok(data) => success_response(&id, data),
|
||||||
Err(e) => error_response(&id, &super::browser::to_ai_friendly_error(&e)),
|
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<Value, S
|
|||||||
Ok(json!({ "granted": permissions }))
|
Ok(json!({ "granted": permissions }))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_dialog(cmd: &Value, state: &DaemonState) -> Result<Value, String> {
|
async fn handle_dialog(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {
|
||||||
|
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 mgr = state.browser.as_ref().ok_or("Browser not launched")?;
|
||||||
let accept = cmd
|
let accept = response
|
||||||
.get("response")
|
|
||||||
.and_then(|v| v.as_str())
|
|
||||||
.map(|r| r == "accept")
|
.map(|r| r == "accept")
|
||||||
.or_else(|| cmd.get("accept").and_then(|v| v.as_bool()))
|
.or_else(|| cmd.get("accept").and_then(|v| v.as_bool()))
|
||||||
.unwrap_or(true);
|
.unwrap_or(true);
|
||||||
let prompt_text = cmd.get("promptText").and_then(|v| v.as_str());
|
let prompt_text = cmd.get("promptText").and_then(|v| v.as_str());
|
||||||
|
|
||||||
mgr.handle_dialog(accept, prompt_text).await?;
|
mgr.handle_dialog(accept, prompt_text).await?;
|
||||||
|
state.pending_dialog = None;
|
||||||
Ok(json!({ "handled": true, "accepted": accept }))
|
Ok(json!({ "handled": true, "accepted": accept }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+45
-2
@@ -122,6 +122,7 @@ pub fn print_response_with_opts(resp: &Response, action: Option<&str>, opts: &Ou
|
|||||||
} else {
|
} else {
|
||||||
println!("{}", serde_json::to_string(resp).unwrap_or_default());
|
println!("{}", serde_json::to_string(resp).unwrap_or_default());
|
||||||
}
|
}
|
||||||
|
// JSON mode includes the warning field in the JSON payload already
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,10 +132,42 @@ pub fn print_response_with_opts(resp: &Response, action: Option<&str>, opts: &Ou
|
|||||||
color::error_indicator(),
|
color::error_indicator(),
|
||||||
resp.error.as_deref().unwrap_or("Unknown error")
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(data) = &resp.data {
|
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 action == Some("storage_get") {
|
||||||
if let Some(output) = format_storage_text(data) {
|
if let Some(output) = format_storage_text(data) {
|
||||||
println!("{}", output);
|
println!("{}", output);
|
||||||
@@ -888,6 +921,14 @@ pub fn print_response_with_opts(resp: &Response, action: Option<&str>, opts: &Ou
|
|||||||
// Default success
|
// Default success
|
||||||
println!("{} Done", color::success_indicator());
|
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.
|
/// Print command-specific help. Returns true if help was printed, false if command unknown.
|
||||||
@@ -1995,13 +2036,14 @@ Examples:
|
|||||||
r##"
|
r##"
|
||||||
agent-browser dialog - Handle browser dialogs
|
agent-browser dialog - Handle browser dialogs
|
||||||
|
|
||||||
Usage: agent-browser dialog <response> [text]
|
Usage: agent-browser dialog <accept|dismiss|status> [text]
|
||||||
|
|
||||||
Respond to browser dialogs (alert, confirm, prompt).
|
Respond to or check for browser dialogs (alert, confirm, prompt).
|
||||||
|
|
||||||
Operations:
|
Operations:
|
||||||
accept [text] Accept dialog, optionally with prompt text
|
accept [text] Accept dialog, optionally with prompt text
|
||||||
dismiss Dismiss/cancel dialog
|
dismiss Dismiss/cancel dialog
|
||||||
|
status Check if a dialog is currently open
|
||||||
|
|
||||||
Global Options:
|
Global Options:
|
||||||
--json Output as JSON
|
--json Output as JSON
|
||||||
@@ -2011,6 +2053,7 @@ Examples:
|
|||||||
agent-browser dialog accept
|
agent-browser dialog accept
|
||||||
agent-browser dialog accept "my input"
|
agent-browser dialog accept "my input"
|
||||||
agent-browser dialog dismiss
|
agent-browser dialog dismiss
|
||||||
|
agent-browser dialog status
|
||||||
"##
|
"##
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -223,8 +223,11 @@ The `frame` command accepts element refs (`@e3`), CSS selectors (`"#my-iframe"`)
|
|||||||
```bash
|
```bash
|
||||||
agent-browser dialog accept [text] # Accept dialog (with optional prompt text)
|
agent-browser dialog accept [text] # Accept dialog (with optional prompt text)
|
||||||
agent-browser dialog dismiss # Dismiss dialog
|
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
|
## Debug
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -177,6 +177,12 @@ agent-browser clipboard write "Hello, World!" # Write text to clipboard
|
|||||||
agent-browser clipboard copy # Copy current selection
|
agent-browser clipboard copy # Copy current selection
|
||||||
agent-browser clipboard paste # Paste from clipboard
|
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)
|
# Diff (compare page states)
|
||||||
agent-browser diff snapshot # Compare current vs last snapshot
|
agent-browser diff snapshot # Compare current vs last snapshot
|
||||||
agent-browser diff snapshot --baseline before.txt # Compare current vs saved file
|
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 <selector>` or `wait @ref`.
|
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 <selector>` 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
|
## Session Management and Cleanup
|
||||||
|
|
||||||
When running multiple agents or automations concurrently, always use named sessions to avoid conflicts:
|
When running multiple agents or automations concurrently, always use named sessions to avoid conflicts:
|
||||||
|
|||||||
Reference in New Issue
Block a user