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

> Learn how ego-lite tracks JavaScript dialogs in CDP sessions by subscribing to Page events and storing dialog state for seamless runtime helper queries. Explore the Chrome DevTools Protocol in action.

- Repository: [CitroLabs/ego-lite](https://github.com/citrolabs/ego-lite)
- Tags: deep-dive
- Published: 2026-08-28

---

**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`](https://github.com/citrolabs/ego-lite/blob/main/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:

```typescript
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:

```typescript
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:

```typescript
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`](https://github.com/citrolabs/ego-lite/blob/main/src/driver/observe.ts)** checks for pending dialogs at lines 15‑16:

```typescript
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`](https://github.com/citrolabs/ego-lite/blob/main/src/driver/nav.ts)** surfaces dialog state to callers at lines 86‑88:

```typescript
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

```javascript
import { screenshot } from 'ego-browser';

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

```

### Explicit Dialog Detection

```javascript
import { pendingDialog } from 'ego-browser';

const dlg = pendingDialog();
if (dlg) {
  console.log(`Blocked by ${dlg.type}: ${dlg.message}`);
}

```

### Navigation with Dialog Reporting

```javascript
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`](https://github.com/citrolabs/ego-lite/blob/main/src/browser-runtime.ts) | 24 | `pendingDialogs` Map declaration |
| [`src/browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/browser-runtime.ts) | 66‑76 | `handleMessage` CDP event router |
| [`src/browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/browser-runtime.ts) | 98‑103 | `pendingDialog` public API |
| [`src/driver/observe.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/driver/observe.ts) | 15‑16 | Screenshot dialog check |
| [`src/driver/nav.ts`](https://github.com/citrolabs/ego-lite/blob/main/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`](https://github.com/citrolabs/ego-lite/blob/main/src/browser-runtime.ts)
- **`pendingDialog()`** provides synchronous, read-only access to active dialog data
- **[`observe.ts`](https://github.com/citrolabs/ego-lite/blob/main/observe.ts)** uses dialog detection to switch screenshot modes
- **[`nav.ts`](https://github.com/citrolabs/ego-lite/blob/main/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.