How Keyboard Input Actions Work in ego-browser: fill, press, and pressSequentially Explained

fill, press, and pressSequentially in ego-browser are Chrome DevTools Protocol (CDP) wrappers that handle key definition, modifier parsing, and fallback DOM event synthesis to guarantee reliable input delivery.

Keyboard automation in ego-browser follows a predictable pipeline: logical keys are decoded into virtual-key codes, modifiers are tracked across calls, and every dispatch is verified with a probe-and-fallback mechanism. This article examines the source code in package/ego-browser/src/driver/keyboard.ts to show exactly how each action functions.


Where Keyboard Actions Are Implemented

All keyboard helpers live in package/ego-browser/src/driver/keyboard.ts. The file exports three main user-facing functions—fill, press, and pressSequentially—plus internal utilities for key parsing, modifier management, and CDP event dispatching.

The implementation depends on supporting modules:

  • element-ops.ts — provides withHandle and resolveAndCall for element resolution
  • waits.ts — supplies waitForSelector for timeout handling
  • state.ts — contains default timeouts and the sleep utility

How Keys Are Defined and Translated

Before any event reaches the browser, key names must be converted into CDP-compatible payloads. The keyDefinition() function handles this transformation.

// Simplified logic from keyboard.ts lines 49-62
function keyDefinition(key: string) {
  // Returns: { vk, code, text } for CDP Input.dispatchKeyEvent
}

For printable characters, the regex PRINTABLE_CODE_RE maps values like "a" to "KeyA" and "7" to "Digit7". Non-printable keys (e.g., "Enter", "Tab") use predefined virtual-key codes without text content.

This translation layer ensures that Playwright-style key names work consistently across platforms.


Parsing Modifier Key Combinations

The parseKeyCombo() utility accepts strings like "Control+a" or "Shift+Tab" and decomposes them into a base key plus a modifier bitfield.

// From keyboard.ts lines 100-130
function parseKeyCombo(combo: string): {
  modifiers: number;  // Bitfield from MODIFIER_BITS
  key: string;        // Base key without modifiers
}

Unrecognized modifiers throw errors immediately. The MODIFIER_BITS constant defines standard values for Control, Shift, Alt, and Meta.


Tracking Active Modifiers State

Keyboard actions are stateful. The module maintains a Set called pressedModifiers that records which modifier keys are currently held down.

// keyboard.ts lines 98-106
const pressedModifiers = new Set<string>();

activeModifierBits() merges these into a single integer so subsequent calls preserve context. For example, holding Shift during one press call and pressing "Tab" in the next correctly produces a Shift+Tab event.


The Core Dispatch Pipeline: dispatchKeyEvent

Every key event flows through dispatchKeyEvent(), a thin wrapper around CDP's Input.dispatchKeyEvent method.

// keyboard.ts lines 30-37
async function dispatchKeyEvent(event: KeyEvent): Promise<void> {
  // Calls CDP with INPUT_DISPATCH_TIMEOUT_MS protection
}

A timeout guard prevents hanging calls when the browser becomes unresponsive.


Fallback Probing: Guaranteed Delivery

CDP dispatch can be ignored by certain pages (event listeners calling preventDefault(), shadow DOM boundaries, etc.). To handle this, ego-browser implements a probe-and-fallback system:

Stage Function Purpose
1. Install probe installKeyProbe() Injects a temporary page-side listener that marks keys as "seen"
2. Dispatch dispatchKeyEvent() Sends the CDP event
3. Verify & fallback finishKeyProbe() Checks probe status; if missed, synthesizes DOM KeyboardEvent
// Probe installation: keyboard.ts lines 39-45
function installKeyProbe(key: string): void

// Probe completion with fallback: lines 63-70
function finishKeyProbe(): Promise<void>

This guarantees that inputs reach the page even when CDP fails.


press: Single Key or Combination

The press() action sends one key event (with optional modifiers) and follows the complete pipeline.

Execution flow:

  1. Parse combo → base key + modifiers via parseKeyCombo()
  2. Compute effective modifier bits: activeModifierBits() | modifiers
  3. Build base event object with keyEventBase()
  4. Send keyDown with printable text and editing commands
  5. Wait INPUT_EVENT_DELAY_MS (tiny inter-event delay)
  6. Send matching keyUp
  7. Execute installKeyProbefinishKeyProbe fallback routine
// Simple key press
await press('Enter');

// Modifier combination
await press('Control+a');

The full implementation spans keyboard.ts lines 99-44.


pressSequentially: Character-by-Character Input

pressSequentially() types a string one character at a time, with optional per-character delays.

Two calling signatures:

// Focus-first variant
pressSequentially(selector: string, text: string, options?: { delay?: number })

// Direct typing variant
pressSequentially(text: string, options?: { delay?: number })

Internal process:

  1. If selector provided, call focusWithTimeout() to activate the element
  2. Iterate over each character in text
  3. For each character: await press(char)
  4. Respect options.delay via state.sleep() between keystrokes

See implementation at keyboard.ts lines 35-57.

// Type with 50ms between keystrokes
await pressSequentially('#comment', 'Nice post!', { delay: 50 });

// Focus and type immediately
await pressSequentially('#username', 'alice');

fill: Direct Value Injection

fill() sets an input's value directly using CDP's Input.insertText, bypassing per-character key events entirely.

Execution steps:

  1. Optional timeout → waitForSelector()
  2. Resolve element handle via withHandle()
  3. Focus element; if clearFirst (default true), select all existing text
  4. When clearing: page script removes value and fires input event with inputType: "deleteContentBackward"
  5. Call CDP Input.insertText with the new value
  6. Fire final input and change events to simulate user completion
// Default: clear existing value, then fill
await fill('#search-box', 'Hello World');

// Preserve existing content (append instead of replace)
await fill('#notes', 'Additional text', { clearFirst: false });

Core implementation: keyboard.ts lines 74-86 with CDP steps at lines 88-124.


Comparing the Three Keyboard Actions

Action Use Case CDP Method Event Type Clear Existing?
press Single keys, shortcuts dispatchKeyEvent keyDown/keyUp No
pressSequentially Mimic human typing dispatchKeyEvent × N Character-by-character Optional focus
fill Fast value insertion insertText input/change Yes (default)

Choose fill for speed and reliability on standard inputs. Use pressSequentially when event handlers depend on individual keystroke timing. Reserve press for navigation keys and modifier shortcuts.


Working Example: Complete Form Interaction

// Navigate to a page and complete a form
await fill('#email', 'user@example.com');
await fill('#password', 'secret123');
await press('Tab');                    // Move to submit button
await press('Enter');                  // Activate

// Alternative: slower, more human-like entry
await pressSequentially('#otp', '123456', { delay: 100 });

All helpers are available on globalThis.ego in agent scripts.


Summary

  • Key definition in keyDefinition() maps logical names to CDP-compatible codes using PRINTABLE_CODE_RE
  • Modifier parsing via parseKeyCombo() supports Playwright-style combinations with bitfield tracking
  • State management through pressedModifiers Set and activeModifierBits() maintains context across calls
  • Guaranteed delivery using installKeyProbe() and finishKeyProbe() with DOM fallback synthesis
  • press() dispatches individual keyDown/keyUp pairs through the full probe-enabled pipeline
  • pressSequentially() iterates characters with optional delays, calling press() per character
  • fill() uses Input.insertText for direct value injection, firing proper input and change events

Frequently Asked Questions

What happens if a page blocks CDP keyboard events?

finishKeyProbe() detects when the probe signal is missed and automatically synthesizes a native DOM KeyboardEvent as a fallback. This ensures the input reaches the page regardless of preventDefault() handlers or shadow DOM boundaries.

How do I hold a modifier key across multiple press calls?

Modifiers are tracked in the pressedModifiers Set. Call press() with a modifier key to add it, then issue subsequent presses. The activeModifierBits() function automatically includes held modifiers in each event's modifier bitfield.

Why would I use pressSequentially instead of fill?

Use pressSequentially when the page's JavaScript expects individual keystroke events—common in autocomplete fields, character-counting inputs, or security-sensitive forms that validate during typing. fill bypasses per-character events and may trigger fewer handlers.

Can I combine clearFirst: false with fill for appending text?

Yes. Pass { clearFirst: false } in the options parameter. The element receives focus and the new text is inserted via Input.insertText without clearing existing content. Note that this appends rather than replaces; precise cursor positioning requires additional handling.

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 →