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.
Navigation Awareness in nav.ts
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.enablefails. At lines 12‑15, the runtime catches initialization errors—common on internal chrome pages likechrome://URLs—and continues without dialog events. In this mode,pendingDialog()always returnsnull, and helpers fall back to default behavior.
Complete Dialog Tracking Flow
Page.enableactivates CDP domain (best-effort)- Page triggers
alert,confirm,prompt, orbeforeunload - Chrome emits
Page.javascriptDialogOpeningvia CDP handleMessagestores dialog data inpendingDialogskeyed by session ID- Helper calls
pendingDialog()and receives active dialog info - User or code dismisses dialog; Chrome emits
Page.javascriptDialogClosed handleMessagedeletes 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}`);
}
Navigation with Dialog Reporting
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.javascriptDialogOpeningandPage.javascriptDialogClosed - Dialog state lives in
pendingDialogs, a session-keyed Map insrc/browser-runtime.ts pendingDialog()provides synchronous, read-only access to active dialog dataobserve.tsuses dialog detection to switch screenshot modesnav.tsreturns 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →