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

> Understand ego-browser keyboard input actions fill, press, and pressSequentially. Learn how CDP wrappers ensure reliable input delivery for your automation.

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

---

**`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](https://github.com/citrolabs/ego-lite) 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`](https://github.com/citrolabs/ego-lite/blob/main/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`](https://github.com/citrolabs/ego-lite/blob/main/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`](https://github.com/citrolabs/ego-lite/blob/main/element-ops.ts)** — provides `withHandle` and `resolveAndCall` for element resolution
- **[`waits.ts`](https://github.com/citrolabs/ego-lite/blob/main/waits.ts)** — supplies `waitForSelector` for timeout handling
- **[`state.ts`](https://github.com/citrolabs/ego-lite/blob/main/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.

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

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

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

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

```typescript
// 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 `installKeyProbe` → `finishKeyProbe` fallback routine

```javascript
// 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:**

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

```javascript
// 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

```javascript
// 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

```javascript
// 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.