fix(shortcuts): allow ESC to close dialogs when terminal is focused#55
Conversation
matchesGlobalShortcut now also passes dialogSafe shortcuts through xterm.js when a dialog overlay is open, so ESC reaches the window-level handler that closes the dialog instead of being swallowed by the terminal. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Updates shortcut handling so dialogs can be closed via Escape even when an xterm.js terminal has focus (i.e., the terminal no longer swallows ESC when a dialog is open).
Changes:
- Extend
matchesGlobalShortcutto treatdialogSafeshortcuts as “bypass terminal input” when a.dialog-overlayis present. - Preserve existing behavior when no dialog is open (ESC continues to reach the terminal normally).
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| export function matchesGlobalShortcut(e: KeyboardEvent): boolean { | ||
| return shortcuts.some((s) => s.global && matches(e, s)); | ||
| const dialogOpen = document.querySelector('.dialog-overlay') !== null; | ||
| return shortcuts.some((s) => (s.global || (dialogOpen && s.dialogSafe)) && matches(e, s)); |
There was a problem hiding this comment.
matchesGlobalShortcut now performs a DOM query (document.querySelector('.dialog-overlay')) every time xterm dispatches a keydown, and the same selector check is duplicated again in initShortcuts(). Since this code runs on hot paths (terminal typing), consider extracting a shared isDialogOpen() helper and/or maintaining a cached dialog-open flag (updated when dialogs mount/unmount) to avoid repeated DOM queries per keystroke.
There was a problem hiding this comment.
Addressed the DRY concern in d946ba6 by extracting an isDialogOpen() helper used by both call sites.
Intentionally skipping the cached-flag suggestion — the "hot path" framing overstates the cost:
- Keydown handlers fire at human typing rates (~5–15 Hz), not at hot-loop frequencies.
querySelector('.dialog-overlay')is a class-indexed lookup in modern browsers — sub-microsecond, no layout/paint.- Caching would require hooking every dialog's mount/unmount to keep the flag in sync with the DOM, introducing a new source of truth and stale-state risk for work that isn't actually expensive.
If dialog detection ever needs to become reactive (e.g. a SolidJS signal), the new isDialogOpen() helper is the single place to change it.
Dedupes the dialog-overlay DOM query between matchesGlobalShortcut and initShortcuts so there's a single point of change if dialog detection ever moves to a reactive signal. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
One observation not covered by the existing review: Double-close on ESC is idempotent but worth noting
Both paths ultimately call the same toggle setter with This is pre-existing design tension between the Dialog's internal ESC handler and the app-level shortcut registration — not introduced by this PR. The fix itself is correct and the approach is sound. Everything else looks good:
|
|
Thank you very much! <3 |
Summary
matchesGlobalShortcutnow also passesdialogSafeshortcuts through xterm when a.dialog-overlayis present in the DOMTest plan
global: true)🤖 Generated with Claude Code