hermes-agent/apps/desktop/electron/quick-entry.test.ts
teknium1 d6fdae6746 feat(desktop): quick-entry window — global hotkey capture to any session
Parity with Claude Desktop quick-entry-window / ChatGPT Quick Chat.
2026-07-26 17:48:19 -07:00

240 lines
9 KiB
TypeScript

import { describe, expect, it, vi } from 'vitest'
import {
createQuickEntryShortcut,
DEFAULT_QUICK_ENTRY_SHORTCUT,
type GlobalShortcutLike,
parseQuickEntryShortcut,
quickEntryWindowBounds,
sanitizeQuickEntrySettings
} from './quick-entry'
function fakeGlobalShortcut(options: { register?: boolean; taken?: string[] } = {}) {
const held = new Set(options.taken ?? [])
const globalShortcut: GlobalShortcutLike = {
isRegistered: vi.fn((accelerator: string) => held.has(accelerator)),
register: vi.fn((accelerator: string) => {
if (options.register === false) {
return false
}
held.add(accelerator)
return true
}),
unregister: vi.fn((accelerator: string) => void held.delete(accelerator))
}
return { globalShortcut, held }
}
describe('parseQuickEntryShortcut', () => {
it('normalizes casing, aliases, and modifier order', () => {
expect(parseQuickEntryShortcut('cmdorctrl+shift+space')).toEqual({
accelerator: 'CommandOrControl+Shift+Space',
ok: true
})
expect(parseQuickEntryShortcut(' Shift + CTRL + k ')).toEqual({ accelerator: 'Control+Shift+K', ok: true })
expect(parseQuickEntryShortcut('Alt+f5')).toEqual({ accelerator: 'Alt+F5', ok: true })
expect(parseQuickEntryShortcut('Meta+/')).toEqual({ accelerator: 'Super+/', ok: true })
})
it('collapses duplicate modifiers', () => {
expect(parseQuickEntryShortcut('Ctrl+Control+Shift+J')).toEqual({ accelerator: 'Control+Shift+J', ok: true })
})
it('requires a modifier so a global bind cannot swallow a bare key', () => {
expect(parseQuickEntryShortcut('K')).toEqual({ ok: false, reason: 'no-modifier' })
expect(parseQuickEntryShortcut('Space')).toEqual({ ok: false, reason: 'no-modifier' })
})
it('requires exactly one non-modifier key', () => {
expect(parseQuickEntryShortcut('Shift+Control')).toEqual({ ok: false, reason: 'no-key' })
expect(parseQuickEntryShortcut('Shift+A+B')).toEqual({ ok: false, reason: 'invalid-key' })
expect(parseQuickEntryShortcut('A+Shift')).toEqual({ ok: false, reason: 'invalid-modifier' })
})
it('rejects empty, junk, and the reserved Escape key', () => {
expect(parseQuickEntryShortcut('')).toEqual({ ok: false, reason: 'empty' })
expect(parseQuickEntryShortcut(' ')).toEqual({ ok: false, reason: 'empty' })
expect(parseQuickEntryShortcut(null)).toEqual({ ok: false, reason: 'empty' })
expect(parseQuickEntryShortcut('Ctrl+NotAKey')).toEqual({ ok: false, reason: 'invalid-key' })
// Escape hides the window; binding it globally would make it un-toggleable.
expect(parseQuickEntryShortcut('Ctrl+Escape')).toEqual({ ok: false, reason: 'reserved' })
})
it('accepts the shipped default unchanged', () => {
expect(parseQuickEntryShortcut(DEFAULT_QUICK_ENTRY_SHORTCUT)).toEqual({
accelerator: DEFAULT_QUICK_ENTRY_SHORTCUT,
ok: true
})
})
})
describe('sanitizeQuickEntrySettings', () => {
it('defaults to enabled with the default shortcut', () => {
expect(sanitizeQuickEntrySettings(undefined)).toEqual({ enabled: true, shortcut: DEFAULT_QUICK_ENTRY_SHORTCUT })
expect(sanitizeQuickEntrySettings('not an object')).toEqual({
enabled: true,
shortcut: DEFAULT_QUICK_ENTRY_SHORTCUT
})
})
it('keeps an explicit disable and normalizes a stored shortcut', () => {
expect(sanitizeQuickEntrySettings({ enabled: false, shortcut: 'alt+j' })).toEqual({
enabled: false,
shortcut: 'Alt+J'
})
})
it('falls back to the default when the stored shortcut is unusable', () => {
expect(sanitizeQuickEntrySettings({ enabled: true, shortcut: 'Q' })).toEqual({
enabled: true,
shortcut: DEFAULT_QUICK_ENTRY_SHORTCUT
})
})
it('treats a non-boolean enabled as off (only `true` opts in once present)', () => {
expect(sanitizeQuickEntrySettings({ enabled: 'yes' }).enabled).toBe(false)
})
})
describe('createQuickEntryShortcut', () => {
it('registers the normalized accelerator when enabled', () => {
const { globalShortcut } = fakeGlobalShortcut()
const onTrigger = vi.fn()
const controller = createQuickEntryShortcut(globalShortcut, onTrigger)
const state = controller.apply({ enabled: true, shortcut: 'cmdorctrl+shift+space' })
expect(state).toEqual({ error: null, registered: true, shortcut: 'CommandOrControl+Shift+Space' })
expect(globalShortcut.register).toHaveBeenCalledWith('CommandOrControl+Shift+Space', onTrigger)
expect(controller.current()).toEqual(state)
})
it('never registers while the setting is disabled', () => {
const { globalShortcut } = fakeGlobalShortcut()
const controller = createQuickEntryShortcut(globalShortcut, vi.fn())
const state = controller.apply({ enabled: false, shortcut: DEFAULT_QUICK_ENTRY_SHORTCUT })
expect(globalShortcut.register).not.toHaveBeenCalled()
expect(state).toEqual({ error: null, registered: false, shortcut: DEFAULT_QUICK_ENTRY_SHORTCUT })
})
it('releases the old accelerator before registering a new one', () => {
const { globalShortcut, held } = fakeGlobalShortcut()
const controller = createQuickEntryShortcut(globalShortcut, vi.fn())
controller.apply({ enabled: true, shortcut: 'Alt+J' })
controller.apply({ enabled: true, shortcut: 'Alt+K' })
expect(globalShortcut.unregister).toHaveBeenCalledWith('Alt+J')
expect(held.has('Alt+J')).toBe(false)
expect(held.has('Alt+K')).toBe(true)
})
it('turning the feature off releases the live accelerator', () => {
const { globalShortcut, held } = fakeGlobalShortcut()
const controller = createQuickEntryShortcut(globalShortcut, vi.fn())
controller.apply({ enabled: true, shortcut: 'Alt+J' })
const off = controller.apply({ enabled: false, shortcut: 'Alt+J' })
expect(globalShortcut.unregister).toHaveBeenCalledWith('Alt+J')
expect(held.size).toBe(0)
expect(off.registered).toBe(false)
expect(off.error).toBeNull()
})
it("surfaces 'taken' when another app already owns the chord", () => {
const { globalShortcut } = fakeGlobalShortcut({ taken: ['Alt+J'] })
const controller = createQuickEntryShortcut(globalShortcut, vi.fn())
const state = controller.apply({ enabled: true, shortcut: 'alt+j' })
expect(globalShortcut.register).not.toHaveBeenCalled()
expect(state).toEqual({ error: 'taken', registered: false, shortcut: 'Alt+J' })
})
it("surfaces 'taken' when the OS refuses the registration", () => {
const { globalShortcut } = fakeGlobalShortcut({ register: false })
const controller = createQuickEntryShortcut(globalShortcut, vi.fn())
expect(controller.apply({ enabled: true, shortcut: 'Alt+J' })).toEqual({
error: 'taken',
registered: false,
shortcut: 'Alt+J'
})
})
it("surfaces 'invalid' for an unusable shortcut without asking the OS", () => {
const { globalShortcut } = fakeGlobalShortcut()
const controller = createQuickEntryShortcut(globalShortcut, vi.fn())
expect(controller.apply({ enabled: true, shortcut: 'J' })).toEqual({
error: 'invalid',
registered: false,
shortcut: 'J'
})
expect(globalShortcut.register).not.toHaveBeenCalled()
})
it('survives a throwing globalShortcut', () => {
const globalShortcut: GlobalShortcutLike = {
isRegistered: () => false,
register: () => {
throw new Error('x11 grab failed')
},
unregister: () => {}
}
const controller = createQuickEntryShortcut(globalShortcut, vi.fn())
expect(controller.apply({ enabled: true, shortcut: 'Alt+J' }).error).toBe('taken')
})
it('dispose releases the accelerator and is idempotent', () => {
const { globalShortcut, held } = fakeGlobalShortcut()
const controller = createQuickEntryShortcut(globalShortcut, vi.fn())
controller.apply({ enabled: true, shortcut: 'Alt+J' })
controller.dispose()
controller.dispose()
expect(globalShortcut.unregister).toHaveBeenCalledTimes(1)
expect(held.size).toBe(0)
expect(controller.current().registered).toBe(false)
})
})
describe('quickEntryWindowBounds', () => {
it('centers horizontally and sits below the top edge of the work area', () => {
const bounds = quickEntryWindowBounds({ height: 1000, width: 1600, x: 0, y: 0 })
expect(bounds.width).toBe(640)
expect(bounds.x).toBe((1600 - 640) / 2)
expect(bounds.y).toBeGreaterThan(0)
expect(bounds.y + bounds.height).toBeLessThanOrEqual(1000)
})
it('respects a display origin offset (second monitor)', () => {
const bounds = quickEntryWindowBounds({ height: 900, width: 1440, x: 1600, y: -200 })
expect(bounds.x).toBe(1600 + (1440 - 640) / 2)
expect(bounds.y).toBeGreaterThanOrEqual(-200)
})
it('stays inside a tiny work area', () => {
const bounds = quickEntryWindowBounds({ height: 120, width: 320, x: 0, y: 0 })
expect(bounds.width).toBeLessThanOrEqual(320)
expect(bounds.height).toBeLessThanOrEqual(120)
expect(bounds.y + bounds.height).toBeLessThanOrEqual(120)
})
it('falls back to the origin without a work area', () => {
expect(quickEntryWindowBounds()).toEqual({ height: 168, width: 640, x: 0, y: 0 })
})
})