Skip to content

fix(app): drop duplicated keydown events at the document boundary - #56376

Closed
NoJuanNobody wants to merge 2 commits into
anomalyco:devfrom
NoJuanNobody:desktop-duplicate-keys
Closed

NoJuanNobody wants to merge 2 commits into
anomalyco:devfrom
NoJuanNobody:desktop-duplicate-keys

Conversation

@NoJuanNobody

@NoJuanNobody NoJuanNobody commented Oct 17, 2026 •

Copy link
Copy Markdown

Companion to #56372, rooted in the same reported behavior as #4920: one physical keypress can surface as two identical key events a few milliseconds apart, and in the desktop/web app the visible victim is text insertion in the composer and any editable field.

Change

  • packages/app/src/shell/key-dedupe.ts — a document-level, capture-phase keydown guard mounted once from the router root (AppInterface), the DOM counterpart of the TUI's raw-stdin guard:
    • Drops an identical keydown (same key and modifiers) within 6 ms of the original — below OS auto-repeat minimums and faster than any human double press. OS auto-repeat is additionally respected via event.repeat, so held keys are never touched.
    • Only covers printable characters plus the editing keys (Backspace, Delete, Enter, Tab, Escape): the event kinds whose duplication produces visible double text. IME composition events (isComposing, Process) are never touched, so non-Latin input methods are unaffected.
    • The duplicate is dropped via preventDefault + stopImmediatePropagation in the capture phase, so no text insertion or other listener sees it.

DOM keydown events carry no paste-burst path unlike the TUI's raw stdin (pastes do not emit per-character keydowns), so this guard needs no same-read discrimination.

Testing

  • packages/app/src/shell/key-dedupe.test.ts covers the duplicate drop, editing-key behavior, window expiry, auto-repeat/IME/non-editing-key pass-through, and chained editing-key drops — all passing.
  • bun run check passes.

Added by the opencode bot (AI-generated).

Related: #4920

@github-actions github-actions Bot added the needs:compliance This means the issue will auto-close after 2 hours. label Oct 17, 2026
@github-actions

Copy link
Copy Markdown
Contributor

This PR doesn't fully meet our contributing guidelines and PR template.

What needs to be fixed:

  • PR description is missing required template sections. Please use the PR template.

Please edit this PR description to address the above within 72 hours, or it will be automatically closed.

If you believe this was flagged incorrectly, please let a maintainer know.

@github-actions

Copy link
Copy Markdown
Contributor

The following comment was made by an LLM, it may be inaccurate:

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes. As written, the guard doesn't work in the web or desktop build, and I couldn't find evidence that the problem it targets happens in the DOM.

How I checked: the new unit tests pass (4/4) and tsgo -b passes. bun run build in packages/app warns that perf_hooks was externalized for browser compatibility. In the bundled output the timing call becomes ({}).performance.now(), so every printable or editing keydown throws a TypeError before the guard can act. Nothing is ever filtered, and each keystroke produces an uncaught error (including in Sentry). The tests pass only because Bun provides perf_hooks.

Public API: unchanged.

Concerns:

  • Broken in the browser: the perf_hooks import (inline).
  • No reproduction: #4920 is a terminal UI (TUI) issue, and I found no report of doubled keydowns in the app. A document-wide filter that runs before every other key handler is a large hammer without a reproduction. Please share one (browser or OS, Electron version, and an event log showing two keydowns for one press). Without it, this risks fixing a problem that doesn't occur in the app.
  • Swallowing real input: the 6 ms window also drops scripted or synthetic keystrokes (Playwright, accessibility tools, remote desktop) (inline).
  • Timing-based tests: they rely on real Bun.sleep(10) waits around a 6 ms window, which can be flaky on slow CI. packages/app/AGENTS.md asks for setSystemTime from bun:test instead.

Related: #4920

@@ -0,0 +1,38 @@
import { performance } from "perf_hooks"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

perf_hooks is a Node module. Vite replaces it with an empty module in the app build (bun run build warns about this), so performance is undefined at runtime. In the browser, line 28 throws TypeError: Cannot read properties of undefined (reading 'now') on every printable or editing keydown, before preventDefault can run, so the guard never filters anything and every keystroke raises an uncaught error. Use the global performance (or event.timeStamp) and drop the import.

if (key.length !== 1 && !EDITING_KEYS.has(key)) return

const signature = `${key}\u0000${event.metaKey}\u0000${event.ctrlKey}\u0000${event.altKey}\u0000${event.shiftKey}`
const at = performance.now()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This listener runs at the document level, before every other handler, so it drops any identical printable or editing key within 6 ms with no other check. That includes keystrokes from automation and accessibility tools, remote-desktop input, and dispatched KeyboardEvents, which can legitimately arrive that fast. Since there's no evidence yet of real OS or Electron duplicates in the DOM, the downside currently outweighs the benefit. A reproduction would show whether this belongs in the app at all, and if so, what narrower condition (e.g. isTrusted, matching timeStamp) actually identifies a duplicate.

keydown(guard, "b")
keydown(guard, "a")
expect(keydown(guard, "Backspace").defaultPrevented).toBe(false)
await Bun.sleep(10)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests time a 6 ms window with real Bun.sleep(10) waits, which is timing-sensitive and can flake on slow CI. packages/app/AGENTS.md asks to control time with setSystemTime from bun:test instead. Also, because Bun provides perf_hooks, these tests can't catch the browser breakage above.

@github-actions

Copy link
Copy Markdown
Contributor

This pull request has been automatically closed because it was not updated to meet our contributing guidelines within the 72-hour window.

Feel free to open a new pull request that follows our guidelines.

@github-actions github-actions Bot removed the needs:compliance This means the issue will auto-close after 2 hours. label Oct 20, 2026
@github-actions github-actions Bot closed this Oct 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant