How Ego‑Browser Manages JavaScript Dialogs (alert, confirm, prompt): CDP Event Handling Explained

Ego‑Browser tracks JavaScript dialogs through CDP events Page.javascriptDialogOpening and Page.javascriptDialogClosed, storing active dialogs in a session-scoped Map that higher‑level helpers query to adapt their behavior.

The ego‑browser runtime, as implemented in citrolabs/ego-lite, provides first-class support for detecting and handling browser-side JavaScript dialogs. Rather than leaving dialogs as blocking UI elements that break automation workflows, the library surfaces them through a clean programmatic interface. This article explains the complete dialog management architecture, from low-level CDP message routing to practical usage in navigation and screenshot helpers.

CDP Event Routing and Dialog State Tracking

At the core of Ego‑Browser's dialog handling is the message router in browser-runtime.ts. The handleMessage function intercepts every Chrome DevTools Protocol message and routes dialog-specific events to stateful storage.

Storing Dialogs on javascriptDialogOpening

When Page.javascriptDialogOpening fires, the runtime extracts dialog parameters and stores them in an internal Map named pendingDialogs:

  • Key: the current CDP session ID
  • Value: an object containing type (alert, confirm, or prompt), message, defaultPrompt, and other CDP-provided fields
// The dialog object structure stored in pendingDialogs
{
  type: "confirm",           // "alert" | "confirm" | "prompt"
  message: "Proceed?",
  defaultPrompt: "",         // for prompt dialogs
  // ... additional CDP fields
}

Clearing Dialogs on javascriptDialogClosed

On Page.javascriptDialogClosed, the entry for that session ID is removed from pendingDialogs. The map is also cleared automatically when a session detaches or its target is destroyed, preventing stale state from accumulating.

Resilient Page Event Enablement

The Page.enable CDP command—required to receive dialog events—is wrapped in a try/catch block. Some internal Chrome pages reject this command; dialog tracking continues on a best-effort basis without propagating errors that would disrupt other runtime operations.

Session‑Scoped Dialog Access with pendingDialog()

The pendingDialog() function, exported from browser-runtime.ts, provides controlled read access to active dialog state:

import { pendingDialog } from "browser-runtime";

// Query dialog for the current session
const dlg = pendingDialog();              // → dialog object or null

// Query dialog for a specific session
const dlgForSession = pendingDialog(sessionId);

Key characteristics:

  • Returns a shallow copy of the stored dialog object to prevent external mutation
  • Defaults to state.sessionId when no argument is provided
  • Returns null when no dialog is pending for the specified session

Dialog Detection in Navigation: pageInfo()

The pageInfo function in driver/nav.ts integrates dialog awareness into page state inspection. Before gathering metrics like dimensions and scroll position, it calls pendingDialog():

import { pageInfo } from "driver/nav";

const info = await pageInfo();

if ("dialog" in info) {
  // A dialog blocks normal page interaction
  console.log("Active dialog:", info.dialog.type, info.dialog.message);
  // Handle or dismiss via external UI automation
} else {
  // Normal page metrics available
  console.log("Viewport:", info.w, info.h);
}

Design rationale: Returning { dialog } instead of page metrics allows calling code to branch immediately on dialog presence, avoiding brittle timeout-based detection.

Screenshot Fallback with Dialog Capture

The screenshot helper in driver/observe.ts demonstrates adaptive behavior based on dialog state. When pendingDialog() returns a non-null value, the helper falls back to raw screenshot mode:

import { screenshot } from "driver/observe";

// Automatically captures dialog if present, normal viewport otherwise
const path = await screenshot();
console.log(`Screenshot saved: ${path}`);

Implementation detail: The raw: true flag bypasses normal viewport framing, capturing the full visible surface including any modal dialog overlay. This prevents silent screenshot failures when a confirm() or prompt() blocks the main content area.

Complete Dialog Handling Workflow

Here is a unified example combining detection, inspection, and adaptive action:

import { pendingDialog } from "browser-runtime";
import { pageInfo } from "driver/nav";
import { screenshot } from "driver/observe";

async function inspectWithDialogHandling() {
  // 1. Check for dialog before navigation
  const preemptiveCheck = pendingDialog();
  if (preemptiveCheck) {
    console.warn(`Uncleared dialog: ${preemptiveCheck.message}`);
  }

  // 2. Navigate and get page state (dialog-aware)
  const info = await pageInfo();

  if ("dialog" in info) {
    // Dialog is blocking the page
    console.log(`Blocked by ${info.dialog.type}: "${info.dialog.message}"`);

    // Capture dialog appearance for debugging
    const dialogScreenshot = await screenshot();  // raw mode automatic
    return { blocked: true, dialog: info.dialog, screenshot: dialogScreenshot };
  }

  // 3. Normal flow: no dialog present
  console.log(`Page ready: ${info.w}x${info.h}`);
  const normalScreenshot = await screenshot();
  return { blocked: false, size: info, screenshot: normalScreenshot };
}

Key Files in the Dialog Architecture

File Role
browser-runtime.ts CDP message routing, pendingDialogs Map, pendingDialog() accessor, automatic cleanup on session end
driver/nav.ts pageInfo() function that short-circuits to {dialog} when dialogs are active
driver/observe.ts screenshot() helper that selects raw capture mode when pendingDialog() indicates an active dialog

Summary

  • CDP events drive state: Page.javascriptDialogOpening and Page.javascriptDialogClosed are the sole sources of dialog truth, processed in browser-runtime.ts
  • Session-scoped storage: the pendingDialogs Map isolates dialog state per CDP session with automatic cleanup
  • Explicit accessor: pendingDialog() exposes read-only dialog state to higher-level modules
  • Adaptive helpers: pageInfo() and screenshot() check dialog presence and modify behavior—returning dialog objects or switching capture modes—rather than failing silently

Frequently Asked Questions

How does ego‑browser distinguish between alert, confirm, and prompt dialogs?

The type field in the stored dialog object comes directly from the CDP Page.javascriptDialogOpening event. Ego‑Browser preserves this value without transformation, so calling code receives "alert", "confirm", or "prompt" exactly as Chrome reports it.

Can multiple dialogs be pending simultaneously across different sessions?

Yes. The pendingDialogs Map uses session ID as its key, so each CDP session maintains independent dialog state. The pendingDialog(sessionId) signature allows querying any session; the parameterless variant defaults to the currently active session.

What happens if Page.enable fails during initialization?

The enablement call is wrapped in a try/catch. If a page (such as chrome:// internals) rejects the command, dialog tracking is simply unavailable for that target. Other runtime functionality continues normally; no exception propagates to caller code.

Does ego‑browser provide methods to programmatically accept or dismiss dialogs?

No. The current implementation is detection-only. Dialogs must be handled through external UI automation or separate CDP Page.handleJavaScriptDialog calls issued outside the browser-runtime.ts module. The pendingDialog() accessor provides the information needed to decide when such intervention is required.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →