How ego-lite Tracks JavaScript Dialogs in CDP Sessions: A Deep Dive into Chrome DevTools Protocol Event Handling

ego-lite monitors JavaScript dialogs by subscribing to the CDP events Page.javascriptDialogOpening and Page.javascriptDialogClosed, storing dialog state in a session-scoped Map that runtime helpers query before screenshots and navigation.

JavaScript dialogs—alert, confirm, prompt, and beforeunload—can block automation workflows and distort visual outputs. The ego-lite project implements a lightweight, session-isolated tracking system that lets automation code detect and respond to these dialogs without heavyweight browser instrumentation. This article examines the implementation in citrolabs/ego-lite, focusing on how dialog state propagates through the CDP runtime to driver-level helpers.

CDP Event Subscription and Dialog State Storage

The foundation of dialog tracking resides in src/browser-runtime.ts. This module manages the lifecycle of CDP sessions and transforms protocol events into queryable state.

The pendingDialogs Map

At line 24, ego-lite initializes a private Map that persists dialog data per session:

const pendingDialogs = new Map<string, DialogData>();

The key is the CDP session ID; the value holds the dialog type, message, and other parameters from the opening event. This structure ensures that dialog awareness survives across asynchronous operations and remains isolated when multiple browser pages operate concurrently.

Message Handling for Dialog Events

The handleMessage function (lines 66‑76) processes incoming CDP notifications:

function handleMessage(message: CDPMessage, sessionId?: string) {
  const targetSession = sessionId ?? currentSessionId;
  
  if (message.method === 'Page.javascriptDialogOpening') {
    pendingDialogs.set(targetSession, message.params);
    return;
  }
  
  if (message.method === 'Page.javascriptDialogClosed') {
    pendingDialogs.delete(targetSession);
    return;
  }
  
  // ... other message handling
}

When Page.javascriptDialogOpening fires, ego-lite extracts the session identifier—using an explicit parameter if provided, otherwise falling back to the current session context—and stores the dialog parameters. The matching Page.javascriptDialogClosed event removes the entry, guaranteeing that stale state cannot accumulate.

Querying Dialog State

The exported pendingDialog helper (lines 98‑103) provides read-only access to this state:

export function pendingDialog(sessionId?: string): DialogData | null {
  const target = sessionId ?? currentSessionId;
  return pendingDialogs.get(target) ?? null;
}

Calling code can optionally specify a session ID; omitting it queries the default session. The function returns null when no dialog is active, enabling straightforward conditional logic in downstream consumers.

Runtime Helpers That Consume Dialog State

ego-lite's driver modules integrate pendingDialog() to adapt behavior when dialogs block the page.

Screenshot Handling in observe.ts

Before capturing screenshots, src/driver/observe.ts checks for pending dialogs at lines 15‑16:

import { pendingDialog } from '../browser-runtime';

export async function screenshot(options?: ScreenshotOptions): Promise<Buffer> {
  const dialog = pendingDialog();
  
  if (dialog) {
    // Force raw screenshot to avoid dialog overlay in capture
    return captureRawScreenshot(options);
  }
  
  return captureScreenshot(options);
}

This prevents the dialog chrome from appearing in visual outputs, which is critical for pixel-perfect regression testing and agent-based visual analysis.

The navigation helper in src/driver/nav.ts surfaces dialog state to callers at lines 86‑88:

import { pendingDialog } from '../browser-runtime';

export async function nav(url: string): Promise<NavigationResult> {
  // ... navigation logic
  
  const dialog = pendingDialog();
  if (dialog) {
    return { success: false, dialog };
  }
  
  return { success: true, url: finalUrl };
}

When a beforeunload dialog interrupts navigation, the caller receives explicit notification instead of a hanging promise or opaque failure.

Session Isolation and Graceful Degradation

ego-lite's design prioritizes fault tolerance over strict correctness. The dialog tracking system operates as a best-effort feature with two resilience mechanisms:

  • Session scoping prevents dialog leaks between pages. Each CDP session maintains independent state, so a dialog on page A does not pollute operations on page B.
  • Silent disablement occurs when Page.enable fails. At lines 12‑15, the runtime catches initialization errors—common on internal chrome pages like chrome:// URLs—and continues without dialog events. In this mode, pendingDialog() always returns null, and helpers fall back to default behavior.

Complete Dialog Tracking Flow

  1. Page.enable activates CDP domain (best-effort)
  2. Page triggers alert, confirm, prompt, or beforeunload
  3. Chrome emits Page.javascriptDialogOpening via CDP
  4. handleMessage stores dialog data in pendingDialogs keyed by session ID
  5. Helper calls pendingDialog() and receives active dialog info
  6. User or code dismisses dialog; Chrome emits Page.javascriptDialogClosed
  7. handleMessage deletes the Map entry, clearing state

Usage Examples

Automatic Screenshot Adaptation

import { screenshot } from 'ego-browser';

// Automatically switches to raw mode if dialog detected
const image = await screenshot({ fullPage: true });

Explicit Dialog Detection

import { pendingDialog } from 'ego-browser';

const dlg = pendingDialog();
if (dlg) {
  console.log(`Blocked by ${dlg.type}: ${dlg.message}`);
}
import { nav } from 'ego-browser';

const result = await nav('https://example.com/exit');
if (result.dialog) {
  console.log('Navigation interrupted:', result.dialog.message);
}

Source Code Reference

File Lines Function
src/browser-runtime.ts 24 pendingDialogs Map declaration
src/browser-runtime.ts 66‑76 handleMessage CDP event router
src/browser-runtime.ts 98‑103 pendingDialog public API
src/driver/observe.ts 15‑16 Screenshot dialog check
src/driver/nav.ts 86‑88 Navigation dialog return

Summary

  • ego-lite tracks JavaScript dialogs via CDP events Page.javascriptDialogOpening and Page.javascriptDialogClosed
  • Dialog state lives in pendingDialogs, a session-keyed Map in src/browser-runtime.ts
  • pendingDialog() provides synchronous, read-only access to active dialog data
  • observe.ts uses dialog detection to switch screenshot modes
  • nav.ts returns dialog metadata when navigation is interrupted
  • The system gracefully degrades when CDP Page domain is unavailable

Frequently Asked Questions

What CDP events does ego-lite use for dialog tracking?

ego-lite subscribes to Page.javascriptDialogOpening to detect when a dialog appears and Page.javascriptDialogClosed to detect dismissal. These events fire for all native JavaScript dialogs including alert, confirm, prompt, and beforeunload.

How does ego-lite handle multiple concurrent browser sessions?

The pendingDialogs Map uses session ID as the key, ensuring that dialog state is strictly isolated per CDP session. The pendingDialog() helper accepts an optional session ID parameter; without it, the function queries the current default session context.

What happens if CDP dialog events are unavailable?

If Page.enable fails during initialization—common on internal chrome pages—the dialog tracking system becomes a no-op. handleMessage never receives the events, pendingDialogs remains empty, and pendingDialog() consistently returns null. Downstream helpers continue with default behavior.

Can I query dialog state without specifying a session ID?

Yes. The pendingDialog() function has an optional sessionId parameter; when omitted, it defaults to the current session context maintained by the runtime. This simplifies common single-session usage while preserving multi-session flexibility.

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 →