From fd3989959cbbbcdcc7d3bd312fe1599aaf5410c9 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Tue, 4 Aug 2026 05:50:09 -0700 Subject: [PATCH] fix: stop carried IME shortcuts at renderer owner --- src/renderer/src/App.tsx | 3 +++ .../native-chat/native-chat-shortcut.test.ts | 24 +++++++++++++++++-- .../native-chat/native-chat-shortcut.ts | 7 ++++-- .../use-native-chat-toggle-shortcut.ts | 4 ++-- .../ime-composition-keyboard-event.test.ts | 8 +++---- .../src/lib/ime-composition-keyboard-event.ts | 7 +++--- 6 files changed, 40 insertions(+), 13 deletions(-) diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 3ace7fc2f77..0106dd1b9e0 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -1952,6 +1952,9 @@ function App(): React.JSX.Element { const gesture = resolveImeModifierGesture(imeOwnedModifierGesture, e) imeOwnedModifierGesture = gesture.active if (gesture.owned || isImeOwnedKeyboardEvent(e)) { + if (gesture.preventDefault) { + e.preventDefault() + } doubleTapDetector.reset() return } diff --git a/src/renderer/src/components/native-chat/native-chat-shortcut.test.ts b/src/renderer/src/components/native-chat/native-chat-shortcut.test.ts index d3bc50bab39..0baaa6286ea 100644 --- a/src/renderer/src/components/native-chat/native-chat-shortcut.test.ts +++ b/src/renderer/src/components/native-chat/native-chat-shortcut.test.ts @@ -4,10 +4,21 @@ import { nativeChatToggleShortcutLabel } from './native-chat-shortcut' -type Combo = Pick +type Combo = Pick< + KeyboardEvent, + 'key' | 'metaKey' | 'ctrlKey' | 'shiftKey' | 'altKey' | 'defaultPrevented' +> function combo(overrides: Partial): Combo { - return { key: 'j', metaKey: false, ctrlKey: false, shiftKey: false, altKey: false, ...overrides } + return { + key: 'j', + metaKey: false, + ctrlKey: false, + shiftKey: false, + altKey: false, + defaultPrevented: false, + ...overrides + } } describe('nativeChatToggleShortcutLabel', () => { @@ -54,4 +65,13 @@ describe('matchesNativeChatToggleShortcut', () => { matchesNativeChatToggleShortcut(combo({ key: 'k', metaKey: true, shiftKey: true }), true) ).toBe(false) }) + + it('yields events already owned by the app dispatcher', () => { + expect( + matchesNativeChatToggleShortcut( + combo({ ctrlKey: true, shiftKey: true, defaultPrevented: true }), + false + ) + ).toBe(false) + }) }) diff --git a/src/renderer/src/components/native-chat/native-chat-shortcut.ts b/src/renderer/src/components/native-chat/native-chat-shortcut.ts index d24f50f3e1d..53dff8ac0d8 100644 --- a/src/renderer/src/components/native-chat/native-chat-shortcut.ts +++ b/src/renderer/src/components/native-chat/native-chat-shortcut.ts @@ -17,10 +17,13 @@ export function nativeChatToggleShortcutLabel(isMac: boolean): string { /** True when the event is the native-chat toggle chord for the given platform. * Pure so it can be unit-tested without a DOM. */ export function matchesNativeChatToggleShortcut( - e: Pick, + e: Pick< + KeyboardEvent, + 'key' | 'metaKey' | 'ctrlKey' | 'shiftKey' | 'altKey' | 'defaultPrevented' + >, isMac: boolean ): boolean { - if (e.altKey || !e.shiftKey) { + if (e.defaultPrevented || e.altKey || !e.shiftKey) { return false } // Primary modifier is Cmd on Mac, Ctrl on Linux/Windows — and must be the diff --git a/src/renderer/src/components/native-chat/use-native-chat-toggle-shortcut.ts b/src/renderer/src/components/native-chat/use-native-chat-toggle-shortcut.ts index 3096077e820..5b6cd3dcf52 100644 --- a/src/renderer/src/components/native-chat/use-native-chat-toggle-shortcut.ts +++ b/src/renderer/src/components/native-chat/use-native-chat-toggle-shortcut.ts @@ -98,9 +98,9 @@ export function useNativeChatToggleShortcut(worktreeId: string, isWorktreeActive e.stopPropagation() state.toggleTabViewMode(tab.id) } - window.addEventListener('keydown', onKeyDown, { capture: true }) + window.addEventListener('keydown', onKeyDown) return () => { - window.removeEventListener('keydown', onKeyDown, { capture: true }) + window.removeEventListener('keydown', onKeyDown) } }, [worktreeId, isWorktreeActive]) } diff --git a/src/renderer/src/lib/ime-composition-keyboard-event.test.ts b/src/renderer/src/lib/ime-composition-keyboard-event.test.ts index 4e478703a90..4f2097c4da9 100644 --- a/src/renderer/src/lib/ime-composition-keyboard-event.test.ts +++ b/src/renderer/src/lib/ime-composition-keyboard-event.test.ts @@ -50,7 +50,7 @@ describe('isImeCompositionKeyDown', () => { ctrlKey: true, isComposing: true }) - expect(gesture).toEqual({ active: true, owned: true }) + expect(gesture).toEqual({ active: true, owned: true, preventDefault: false }) gesture = resolveImeModifierGesture(gesture.active, { ctrlKey: true, @@ -62,18 +62,18 @@ describe('isImeCompositionKeyDown', () => { shiftKey: true, isComposing: false }) - expect(gesture).toEqual({ active: true, owned: true }) + expect(gesture).toEqual({ active: true, owned: true, preventDefault: true }) gesture = resolveImeModifierGesture(gesture.active, { isComposing: false }) - expect(gesture).toEqual({ active: false, owned: true }) + expect(gesture).toEqual({ active: false, owned: true, preventDefault: true }) expect( resolveImeModifierGesture(false, { ctrlKey: true, shiftKey: true, isComposing: false }) - ).toEqual({ active: false, owned: false }) + ).toEqual({ active: false, owned: false, preventDefault: false }) }) }) diff --git a/src/renderer/src/lib/ime-composition-keyboard-event.ts b/src/renderer/src/lib/ime-composition-keyboard-event.ts index c015b1b017f..716d7c4311e 100644 --- a/src/renderer/src/lib/ime-composition-keyboard-event.ts +++ b/src/renderer/src/lib/ime-composition-keyboard-event.ts @@ -27,10 +27,11 @@ export function isImeOwnedKeyboardEvent(event: object): boolean { export function resolveImeModifierGesture( active: boolean, event: ImeModifierGestureEvent -): { active: boolean; owned: boolean } { +): { active: boolean; owned: boolean; preventDefault: boolean } { const hasModifier = Boolean(event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) - const owned = active || (hasModifier && isImeOwnedKeyboardEvent(event)) - return { active: owned && hasModifier, owned } + const marked = isImeOwnedKeyboardEvent(event) + const owned = active || (hasModifier && marked) + return { active: owned && hasModifier, owned, preventDefault: active && !marked } } /**