diff --git a/apps/desktop/src/lib/keybinds/combo.ts b/apps/desktop/src/lib/keybinds/combo.ts index 7f6f757dce..97f1a51d22 100644 --- a/apps/desktop/src/lib/keybinds/combo.ts +++ b/apps/desktop/src/lib/keybinds/combo.ts @@ -50,6 +50,9 @@ const MODIFIER_CODES = new Set([ 'ShiftRight' ]) +// Modifier names as reported by `event.key` on a bare modifier keydown. +const MODIFIER_KEYS = new Set(['Alt', 'Control', 'Meta', 'Shift']) + function baseKeyFromCode(code: string): string | null { if (code.startsWith('Key')) { return code.slice(3).toLowerCase() @@ -101,10 +104,28 @@ function baseKeyFromEventKey(key: string, shiftKey: boolean): string | null { // Returns the canonical combo for a keydown, or null while only modifiers are // held (so capture mode keeps waiting for a real key). export function comboFromEvent(event: KeyboardEvent): string | null { + // IME composition (Chinese/Japanese/Korean input): the keydown events + // during composition carry preedit keystrokes and the commit keypress + // (Enter/Space/Shift for candidate selection). Treating them as combos + // fires unrelated keybinds — e.g. typing 你 with a Chinese IME sent a + // keydown that dispatched `session.new` and silently opened a new session. + // Bail out entirely while composing. + if (event.isComposing || event.key === 'Process') { + return null + } + if (MODIFIER_CODES.has(event.code)) { return null } + // A keydown whose `key` is a modifier name but whose `code` is a regular + // key is not a real modifier chord — legacy IMEs that synthesize keystrokes + // (Q9 2002 sends key="Control" with code="KeyW") produce these, and they + // would canonicalize to phantom combos (Ctrl+W → close active tab). Ignore. + if (MODIFIER_KEYS.has(event.key)) { + return null + } + const base = baseKeyFromEventKey(event.key, event.shiftKey) ?? baseKeyFromCode(event.code) if (!base) {