mirror of
https://github.com/stablyai/orca.git
synced 2026-10-21 00:03:46 +00:00
feat(shortcuts): double-tap modifier keybindings (#5516)
* docs: design for double-tap modifier keybindings * feat(keybindings): parse and normalize DoubleTap+<Mod> grammar * test(keybindings): cover DoubleTap+Ctrl positive case * feat(keybindings): match DoubleTap bindings against synthetic input * test(keybindings): cover Ctrl-on-darwin double-tap miss; document input invariant * feat(keybindings): format DoubleTap bindings as a doubled glyph * test(keybindings): cover DoubleTap+Ctrl glyph on mac; note title label semantics * feat(keybindings): add pure ModifierDoubleTapDetector state machine * test(keybindings): cover missed-keyup double-tap edge; clarify keyUp guard * feat(keybindings): capture double-tap gestures into DoubleTap+<Mod> * test(keybindings): cover Ctrl/linux and Cmd/linux double-tap canonicalization * feat(keybindings): resolve double-tap input in window shortcut policy * feat(shortcuts): detect double-tap modifiers in main before-input-event * docs(shortcuts): note dictation guards live in the renderer * feat(shortcuts): dispatch double-tap modifiers in the renderer window handler * fix(shortcuts): clear armed double-tap state on dangling modifier keyup * feat(settings): record double-tap modifier shortcuts in the editor * i18n(shortcuts): localize ShortcutKeyCombo double-tap tooltip * Fix double-tap shortcut review gaps Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Devin345458 <devin@appleidimagination.com> Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com> Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
co-authored by
Orca
Devin345458
Jinwoo-H
parent
0eec0ad74a
commit
1368ba84b8
@@ -665,6 +665,100 @@ describe('createMainWindow', () => {
|
||||
expect(webContents.send).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('only intercepts double-tap dictation when enabled toggle mode can handle it', () => {
|
||||
const windowHandlers: Record<string, (...args: any[]) => void> = {}
|
||||
const webContents = {
|
||||
on: vi.fn((event, handler) => {
|
||||
windowHandlers[event] = handler
|
||||
}),
|
||||
setZoomLevel: vi.fn(),
|
||||
setBackgroundThrottling: vi.fn(),
|
||||
invalidate: vi.fn(),
|
||||
setWindowOpenHandler: vi.fn(),
|
||||
send: vi.fn(),
|
||||
isDevToolsOpened: vi.fn(),
|
||||
openDevTools: vi.fn(),
|
||||
closeDevTools: vi.fn()
|
||||
}
|
||||
const browserWindowInstance = {
|
||||
webContents,
|
||||
on: vi.fn(),
|
||||
isDestroyed: vi.fn(() => false),
|
||||
isMaximized: vi.fn(() => true),
|
||||
isFullScreen: vi.fn(() => false),
|
||||
getSize: vi.fn(() => [1200, 800]),
|
||||
setSize: vi.fn(),
|
||||
maximize: vi.fn(),
|
||||
show: vi.fn(),
|
||||
loadFile: vi.fn(),
|
||||
loadURL: vi.fn()
|
||||
}
|
||||
browserWindowMock.mockImplementation(function () {
|
||||
return browserWindowInstance
|
||||
})
|
||||
|
||||
const voice: { enabled: boolean; sttModel: string; dictationMode: 'toggle' | 'hold' } = {
|
||||
enabled: false,
|
||||
sttModel: '',
|
||||
dictationMode: 'toggle'
|
||||
}
|
||||
createMainWindow(
|
||||
{
|
||||
getUI: () => ({}),
|
||||
getSettings: () => ({ windowBackgroundBlur: false, voice }) as never,
|
||||
updateUI: vi.fn()
|
||||
} as never,
|
||||
{
|
||||
getKeybindings: () => ({ 'voice.dictation': ['DoubleTap+Shift'] })
|
||||
}
|
||||
)
|
||||
|
||||
const triggerDoubleTapShift = (): ReturnType<typeof vi.fn> => {
|
||||
const modifierInput = {
|
||||
code: 'ShiftLeft',
|
||||
key: 'Shift',
|
||||
shift: true,
|
||||
meta: false,
|
||||
control: false,
|
||||
alt: false
|
||||
}
|
||||
windowHandlers['before-input-event'](
|
||||
{ preventDefault: vi.fn() } as never,
|
||||
{ ...modifierInput, type: 'keyDown' } as never
|
||||
)
|
||||
windowHandlers['before-input-event'](
|
||||
{ preventDefault: vi.fn() } as never,
|
||||
{ ...modifierInput, type: 'keyUp' } as never
|
||||
)
|
||||
const preventDefault = vi.fn()
|
||||
windowHandlers['before-input-event'](
|
||||
{ preventDefault } as never,
|
||||
{ ...modifierInput, type: 'keyDown' } as never
|
||||
)
|
||||
windowHandlers['before-input-event'](
|
||||
{ preventDefault: vi.fn() } as never,
|
||||
{ ...modifierInput, type: 'keyUp' } as never
|
||||
)
|
||||
return preventDefault
|
||||
}
|
||||
|
||||
const disabledPreventDefault = triggerDoubleTapShift()
|
||||
expect(disabledPreventDefault).not.toHaveBeenCalled()
|
||||
expect(webContents.send).not.toHaveBeenCalledWith('ui:dictationKeyDown')
|
||||
|
||||
voice.enabled = true
|
||||
voice.sttModel = 'test-model'
|
||||
voice.dictationMode = 'hold'
|
||||
const holdPreventDefault = triggerDoubleTapShift()
|
||||
expect(holdPreventDefault).not.toHaveBeenCalled()
|
||||
expect(webContents.send).not.toHaveBeenCalledWith('ui:dictationKeyDown')
|
||||
|
||||
voice.dictationMode = 'toggle'
|
||||
const togglePreventDefault = triggerDoubleTapShift()
|
||||
expect(togglePreventDefault).toHaveBeenCalledTimes(1)
|
||||
expect(webContents.send).toHaveBeenCalledWith('ui:dictationKeyDown')
|
||||
})
|
||||
|
||||
it('forwards ctrl/cmd+j to the worktree palette toggle event', () => {
|
||||
const windowHandlers: Record<string, (...args: any[]) => void> = {}
|
||||
const webContents = {
|
||||
@@ -792,6 +886,85 @@ describe('createMainWindow', () => {
|
||||
expect(webContents.send).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('allows double-tap shortcuts while terminal input is focused with Terminal-first policy', () => {
|
||||
const windowHandlers: Record<string, (...args: any[]) => void> = {}
|
||||
const webContents = {
|
||||
on: vi.fn((event, handler) => {
|
||||
windowHandlers[event] = handler
|
||||
}),
|
||||
setZoomLevel: vi.fn(),
|
||||
setBackgroundThrottling: vi.fn(),
|
||||
invalidate: vi.fn(),
|
||||
setWindowOpenHandler: vi.fn(),
|
||||
send: vi.fn(),
|
||||
isDevToolsOpened: vi.fn(),
|
||||
openDevTools: vi.fn(),
|
||||
closeDevTools: vi.fn()
|
||||
}
|
||||
const browserWindowInstance = {
|
||||
webContents,
|
||||
on: vi.fn(),
|
||||
isDestroyed: vi.fn(() => false),
|
||||
isMaximized: vi.fn(() => true),
|
||||
isFullScreen: vi.fn(() => false),
|
||||
getSize: vi.fn(() => [1200, 800]),
|
||||
setSize: vi.fn(),
|
||||
maximize: vi.fn(),
|
||||
show: vi.fn(),
|
||||
loadFile: vi.fn(),
|
||||
loadURL: vi.fn()
|
||||
}
|
||||
browserWindowMock.mockImplementation(function () {
|
||||
return browserWindowInstance
|
||||
})
|
||||
|
||||
createMainWindow(
|
||||
{
|
||||
getUI: () => ({}),
|
||||
getSettings: () => ({ terminalShortcutPolicy: 'terminal-first' })
|
||||
} as never,
|
||||
{
|
||||
getKeybindings: () => ({ 'worktree.quickOpen': ['DoubleTap+Shift'] })
|
||||
}
|
||||
)
|
||||
|
||||
const setFocusedListener = vi
|
||||
.mocked(ipcMain.on)
|
||||
.mock.calls.find(([channel]) => channel === 'ui:setTerminalInputFocused')?.[1]
|
||||
expect(setFocusedListener).toBeTypeOf('function')
|
||||
setFocusedListener?.({ sender: webContents } as never, true)
|
||||
|
||||
const modifierInput = {
|
||||
code: 'ShiftLeft',
|
||||
key: 'Shift',
|
||||
shift: true,
|
||||
meta: false,
|
||||
control: false,
|
||||
alt: false
|
||||
}
|
||||
const firstDownPreventDefault = vi.fn()
|
||||
windowHandlers['before-input-event'](
|
||||
{ preventDefault: firstDownPreventDefault } as never,
|
||||
{ ...modifierInput, type: 'keyDown' } as never
|
||||
)
|
||||
const firstUpPreventDefault = vi.fn()
|
||||
windowHandlers['before-input-event'](
|
||||
{ preventDefault: firstUpPreventDefault } as never,
|
||||
{ ...modifierInput, type: 'keyUp' } as never
|
||||
)
|
||||
const secondDownPreventDefault = vi.fn()
|
||||
windowHandlers['before-input-event'](
|
||||
{ preventDefault: secondDownPreventDefault } as never,
|
||||
{ ...modifierInput, type: 'keyDown' } as never
|
||||
)
|
||||
|
||||
expect(firstDownPreventDefault).not.toHaveBeenCalled()
|
||||
expect(firstUpPreventDefault).not.toHaveBeenCalled()
|
||||
expect(secondDownPreventDefault).toHaveBeenCalledTimes(1)
|
||||
expect(webContents.send).toHaveBeenCalledTimes(1)
|
||||
expect(webContents.send).toHaveBeenCalledWith('ui:openQuickOpen')
|
||||
})
|
||||
|
||||
it('notifies before Orca-first captures a risky terminal-focused shortcut', () => {
|
||||
const windowHandlers: Record<string, (...args: any[]) => void> = {}
|
||||
const webContents = {
|
||||
@@ -857,6 +1030,83 @@ describe('createMainWindow', () => {
|
||||
expect(webContents.send).toHaveBeenNthCalledWith(2, 'ui:toggleWorktreePalette')
|
||||
})
|
||||
|
||||
it('notifies before Orca-first captures a terminal-focused double-tap shortcut', () => {
|
||||
const windowHandlers: Record<string, (...args: any[]) => void> = {}
|
||||
const webContents = {
|
||||
on: vi.fn((event, handler) => {
|
||||
windowHandlers[event] = handler
|
||||
}),
|
||||
setZoomLevel: vi.fn(),
|
||||
setBackgroundThrottling: vi.fn(),
|
||||
invalidate: vi.fn(),
|
||||
setWindowOpenHandler: vi.fn(),
|
||||
send: vi.fn(),
|
||||
isDevToolsOpened: vi.fn(),
|
||||
openDevTools: vi.fn(),
|
||||
closeDevTools: vi.fn()
|
||||
}
|
||||
const browserWindowInstance = {
|
||||
webContents,
|
||||
on: vi.fn(),
|
||||
isDestroyed: vi.fn(() => false),
|
||||
isMaximized: vi.fn(() => true),
|
||||
isFullScreen: vi.fn(() => false),
|
||||
getSize: vi.fn(() => [1200, 800]),
|
||||
setSize: vi.fn(),
|
||||
maximize: vi.fn(),
|
||||
show: vi.fn(),
|
||||
loadFile: vi.fn(),
|
||||
loadURL: vi.fn()
|
||||
}
|
||||
browserWindowMock.mockImplementation(function () {
|
||||
return browserWindowInstance
|
||||
})
|
||||
|
||||
createMainWindow(
|
||||
{
|
||||
getUI: () => ({}),
|
||||
getSettings: () => ({ terminalShortcutPolicy: 'orca-first' })
|
||||
} as never,
|
||||
{
|
||||
getKeybindings: () => ({ 'worktree.quickOpen': ['DoubleTap+Shift'] })
|
||||
}
|
||||
)
|
||||
|
||||
const setFocusedListener = vi
|
||||
.mocked(ipcMain.on)
|
||||
.mock.calls.find(([channel]) => channel === 'ui:setTerminalInputFocused')?.[1]
|
||||
expect(setFocusedListener).toBeTypeOf('function')
|
||||
setFocusedListener?.({ sender: webContents } as never, true)
|
||||
|
||||
const modifierInput = {
|
||||
code: 'ShiftLeft',
|
||||
key: 'Shift',
|
||||
shift: true,
|
||||
meta: false,
|
||||
control: false,
|
||||
alt: false
|
||||
}
|
||||
windowHandlers['before-input-event'](
|
||||
{ preventDefault: vi.fn() } as never,
|
||||
{ ...modifierInput, type: 'keyDown' } as never
|
||||
)
|
||||
windowHandlers['before-input-event'](
|
||||
{ preventDefault: vi.fn() } as never,
|
||||
{ ...modifierInput, type: 'keyUp' } as never
|
||||
)
|
||||
const preventDefault = vi.fn()
|
||||
windowHandlers['before-input-event'](
|
||||
{ preventDefault } as never,
|
||||
{ ...modifierInput, type: 'keyDown' } as never
|
||||
)
|
||||
|
||||
expect(preventDefault).toHaveBeenCalledTimes(1)
|
||||
expect(webContents.send).toHaveBeenNthCalledWith(1, 'ui:terminalShortcutCaptured', {
|
||||
actionId: 'worktree.quickOpen'
|
||||
})
|
||||
expect(webContents.send).toHaveBeenNthCalledWith(2, 'ui:openQuickOpen')
|
||||
})
|
||||
|
||||
it('forwards the configured workspace delete shortcut while terminal input is focused', () => {
|
||||
const windowHandlers: Record<string, (...args: any[]) => void> = {}
|
||||
const webContents = {
|
||||
|
||||
Reference in New Issue
Block a user