From 9f2e6d05ab2a345cfc350998c9ea0a236fe6a6c8 Mon Sep 17 00:00:00 2001 From: BAS Lam Date: Thu, 13 Aug 2026 09:46:51 +0800 Subject: [PATCH] fix(desktop): ignore IME composition keydowns in keybind combo resolution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Chinese/Japanese/Korean IMEs emit keydown events during composition that 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 CJK IME could dispatch session.new and silently open a new session. Guard comboFromEvent(): - Bail out entirely while composing (event.isComposing or key === 'Process') - Ignore keydowns whose event.key is a bare modifier name but whose code is a regular key — legacy IMEs that synthesize keystrokes (Q9 2002 sends key="Control" with code="KeyW") would otherwise canonicalize into phantom combos like mod+w that close the active tab. Tested with Q9 (九方) legacy IME on Windows. --- apps/desktop/src/lib/keybinds/combo.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) 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) {