fix: stop carried IME shortcuts at renderer owner

This commit is contained in:
Neil
2026-08-04 05:50:09 -07:00
parent 6a99c6ef66
commit fd3989959c
6 changed files with 40 additions and 13 deletions
+3
View File
@@ -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
}
@@ -4,10 +4,21 @@ import {
nativeChatToggleShortcutLabel
} from './native-chat-shortcut'
type Combo = Pick<KeyboardEvent, 'key' | 'metaKey' | 'ctrlKey' | 'shiftKey' | 'altKey'>
type Combo = Pick<
KeyboardEvent,
'key' | 'metaKey' | 'ctrlKey' | 'shiftKey' | 'altKey' | 'defaultPrevented'
>
function combo(overrides: Partial<Combo>): 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)
})
})
@@ -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<KeyboardEvent, 'key' | 'metaKey' | 'ctrlKey' | 'shiftKey' | 'altKey'>,
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
@@ -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])
}
@@ -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 })
})
})
@@ -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 }
}
/**