fix(desktop): ignore IME composition keydowns in keybind combo resolution

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.
This commit is contained in:
BAS Lam
2026-08-13 09:46:51 +08:00
committed by Teknium
parent 39e7607794
commit 9f2e6d05ab

View File

@@ -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) {