code-yeongyu / code-yeongyu/web-terminal
Firefox: keystrokes typed before the terminal is ready open quick find, which then swallows the whole session
- Dominant language
- TypeScript
- Stars
- 22
- Forks
- 5
- PR merge metrics
- No merged PRs in 30d
Description
### Summary
On Firefox with *Search for text when you start typing* enabled, keys pressed between submitting the login form and ghostty's WASM VT finishing its load land on `document.body`. That opens Firefox's quick find, and the find bar keeps focus afterwards, so every subsequent keystroke goes to the search box instead of the PTY. The terminal renders correctly but accepts no input for the rest of the session.
### Environment
- web-terminal `3a73f62`, Bun `1.4.0-canary.1`
- Firefox 153 (reproduced with Playwright, `accessibility.typeaheadfind=true`)
- Ubuntu 24.04, served over a cloudflared quick tunnel
### Reproduction
1. Enable *Search for text when you start typing* in Firefox settings.
2. Open web-terminal and submit the password.
3. Start typing immediately, before the terminal canvas paints.
### Observed
`document.activeElement` stays `BODY` through the login round trip and the WASM load:
| condition | activeElement while typing | input reaches PTY |
|---|---|---|
| no asset delay | `BODY`, then `MAIN.terminal` | no |
| wasm/woff2 delayed 3s | `BODY` throughout | no |
| retyped after ready | `MAIN.terminal` | yes |
The window is not only a slow-link artifact. argon2id verification (64 MiB, t=3) keeps `/api/login` busy for a few hundred milliseconds on its own, and a tunnel widens it further. Chromium hides the bug because it has no quick find, so the lost keys simply vanish.
An earlier attempt that installed the guard inside `renderApp()` still failed 3 out of 3 runs, because the login round trip completes before that point.
### Cause
Nothing focusable exists between the login submit and `createTerminalApp` resolving, so no element absorbs keys during that window.
### Suggested fix
Route stray printable keys to the PTY rather than only suppressing them, and hold the ones typed before the terminal exists:
```ts
function routeStrayKeyToTerminal(event: KeyboardEvent): void {
if (event.ctrlKey || event.metaKey || event.altKey) return
if (event.key.length !== 1) return
if (isEditable(event.target)) return
if (event.key === " " && activatesOnSpace(event.target)) return
event.preventDefault()
if (activeTerminal === undefined) {
if (pendingKeys.length < PENDING_KEY_LIMIT) pendingKeys.push(event.key)
return
}
activeTerminal.terminal.textarea?.focus()
activeTerminal.sendKeys(event.key)
}
async function boot(): Promise {
document.addEventListener("keydown", routeStrayKeyToTerminal, true)
// ...
}
```
Pair it with `created.terminal.textarea?.focus()` once the terminal is constructed, and flush the buffer from whichever of terminal-ready and socket-connected lands second — `connection.sendInput()` drops silently unless the socket is `OPEN`.
An earlier revision of this issue proposed narrowing the guard to `document.body` and only calling `preventDefault()`. That version is wrong on both counts: it misses focus parked on chrome buttons, and it eats keys typed during the load instead of delivering them. See the follow-up comment for the measurements.
### Verification
Checked on both engines with Playwright:
| check | Firefox | Chromium |
|---|---|---|
| pre-ready keys neutralised, 3 consecutive runs with 3s asset delay | pass | — |
| typing and Enter after the terminal is ready | pass | pass |
| password field accepts real keystrokes | pass | pass |
| `Space` on a focused button still activates it | pass | pass |
| `Ctrl`-modified keys pass through | pass | pass |
`bun run typecheck` and `biome check` are clean. `bun test` is 101/102; the single failure, `SessionStore > kill terminates the session and reports exit`, fails identically on an unmodified checkout in this environment.
Happy to send a PR if the approach looks right.
Contributor guide
No contributing guide indexed for this repository
Research direction
Start at boot and createTerminalApp, then trace terminal construction, readiness, and socket connection order. Verify the key-routing behavior with the described Playwright cases, including delayed WASM/woff2 loading, editable fields, Space on buttons, modified keys, and post-ready typing; done means bun run typecheck and biome check remain clean.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- playwright, typescript, wasm
- Domain
- frontend, testing-qa
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 64/100