fix(desktop): restore mod-chord keybinds while typing in inputs

#86586 replaced the combo-based input gate (any Cmd/Ctrl chord fires
while typing) with an action allowlist that dropped session.new and
every other mod-chord not explicitly listed. ⌘N/⌘T/⌘⇧N and friends
became dead keys whenever focus was in the composer.

Restore the pre-regression rule: primary-modifier chords stay global
even in text fields; the allowlist now gates only bare/Shift/Alt combos,
so rebound letter keys can never hijack typing. Text-navigation chords
(Ctrl+Arrow/PgUp/PgDn) still stay with the input.
This commit is contained in:
David Metcalfe
2026-08-15 10:00:10 -07:00
committed by Teknium
parent d6f18cd7db
commit a11fb1cd89
2 changed files with 35 additions and 7 deletions
+24 -4
View File
@@ -137,16 +137,36 @@ describe('formatCombo — honest Control labels', () => {
})
describe('actionAllowedInInput', () => {
it('keeps only explicit text-entry-safe global actions active while typing', async () => {
it('keeps primary-modifier chords global while typing, gating bare/Shift combos to the allowlist', async () => {
const { actionAllowedInInput } = await loadCombo('MacIntel')
// Mod/Ctrl chords are deliberate two-key gestures — they fire even with
// focus in the composer (⌘N, ⌘T, ⌘⇧N, ⌘K, ⌃Tab, …), matching every browser
// and chat app and the pre-#86586 behavior.
expect(actionAllowedInInput('session.new', 'mod+n')).toBe(true)
expect(actionAllowedInInput('session.newTab', 'mod+t')).toBe(true)
expect(actionAllowedInInput('session.newWindow', 'mod+shift+n')).toBe(true)
expect(actionAllowedInInput('session.next', 'ctrl+tab')).toBe(true)
expect(actionAllowedInInput('session.prev', 'ctrl+shift+tab')).toBe(true)
expect(actionAllowedInInput('nav.commandPalette', 'mod+k')).toBe(true)
expect(actionAllowedInInput('view.findInPage', 'mod+f')).toBe(true)
expect(actionAllowedInInput('nav.skills', 'mod+k')).toBe(false)
expect(actionAllowedInInput('view.showTerminal', 'ctrl+`')).toBe(false)
expect(actionAllowedInInput('profile.next', 'mod+shift+]')).toBe(false)
expect(actionAllowedInInput('nav.skills', 'mod+k')).toBe(true)
expect(actionAllowedInInput('view.showTerminal', 'ctrl+`')).toBe(true)
expect(actionAllowedInInput('profile.next', 'mod+shift+]')).toBe(true)
// A global action rebound onto a text-editing chord (Ctrl+A select-all,
// Ctrl+E line-end, …) fires deliberately while typing — mod-chords are
// two-key gestures, the pre-#86586 semantic. Unbound chords (the shipped
// default for a/e/u/backspace) never reach this function: the dispatcher
// returns before the gate when no action is bound, so the input keeps its
// native editing behavior out of the box. An editing-chord exclusion set
// would have to dodge shipped defaults (⌘K, ⌘W, ⌘D, ⌘F, ⌘B) or re-break them.
expect(actionAllowedInInput('session.new', 'ctrl+a')).toBe(true)
// Bare/Shift-only combos must never hijack typing: a rebound ⌘N → 'n'
// (or the pre-#76185 'shift+n') must not fire while the user types N.
expect(actionAllowedInInput('session.new', 'n')).toBe(false)
expect(actionAllowedInInput('session.new', 'shift+n')).toBe(false)
})
it('leaves text navigation chords with the focused input even when rebound to an allowed action', async () => {
+11 -3
View File
@@ -260,9 +260,13 @@ const INPUT_SAFE_ACTIONS = new Set([
const TEXT_NAVIGATION_KEYS = new Set(['up', 'down', 'left', 'right', 'home', 'end', 'pageup', 'pagedown'])
// Only explicit text-entry-safe actions fire while typing. Editing/navigation
// chords such as Ctrl+Arrow/PageUp must stay with the input even if a user
// rebinds them to a global navigation action.
// Only explicit text-entry-safe actions fire while typing. A primary-modifier
// chord (Cmd/Ctrl) is a deliberate two-key gesture that every browser and chat
// app fires even with focus in a text field (⌘N, ⌘T, ⌘K, ⌃Tab…), so those stay
// global — restoring the pre-#86586 behavior. Editing/navigation chords such
// as Ctrl+Arrow/PageUp must stay with the input even if a user rebinds them to
// a global navigation action, and bare/Shift-only combos (typed letters) are
// gated by the allowlist so they never hijack normal typing.
export function actionAllowedInInput(actionId: string, combo: string): boolean {
const base = combo.split('+').pop()
@@ -270,5 +274,9 @@ export function actionAllowedInInput(actionId: string, combo: string): boolean {
return false
}
if (/^(?:mod|ctrl)(?:\+|$)/.test(combo)) {
return true
}
return INPUT_SAFE_ACTIONS.has(actionId)
}