mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-06 12:52:11 +00:00
Bind session.next/prev to Control+Tab / Control+Shift+Tab with a distinct `ctrl` modifier token (literal Control on macOS — not Cmd, which the OS reserves). Add ^1…^9 positional jumps mirroring profile ⌘1…⌘9. Mac-style interaction: - Quick ^Tab tap jumps on keydown with no HUD (even if Ctrl stays down) - Hold Tab ~220ms, or tap Tab again while Ctrl is held → compact HUD - Ctrl↑ commits the highlight; Esc cancels; rows clickable (^+click safe) - Recency-ordered list snapshotted on open; cycles by stored session id Includes combo.test.ts + session-switcher.test.ts.
115 lines
2.6 KiB
TypeScript
115 lines
2.6 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
import type { SessionInfo } from '@/types/hermes'
|
|
|
|
import { $selectedStoredSessionId, $sessions } from './session'
|
|
import {
|
|
$switcherIndex,
|
|
$switcherOpen,
|
|
$switcherSessions,
|
|
closeSwitcher,
|
|
commitOnCtrlUp,
|
|
onSwitcherTabDown,
|
|
onSwitcherTabUp,
|
|
openOrAdvanceSwitcher,
|
|
slotSessionId,
|
|
SWITCHER_REVEAL_MS
|
|
} from './session-switcher'
|
|
|
|
const session = (id: string): SessionInfo => ({ id }) as SessionInfo
|
|
|
|
const seed = (ids: string[], selected: null | string) => {
|
|
$sessions.set(ids.map(session))
|
|
$selectedStoredSessionId.set(selected)
|
|
}
|
|
|
|
const tabTap = (direction: 1 | -1 = 1) => {
|
|
onSwitcherTabDown()
|
|
const target = openOrAdvanceSwitcher(direction)
|
|
onSwitcherTabUp()
|
|
|
|
return target
|
|
}
|
|
|
|
beforeEach(() => {
|
|
vi.useRealTimers()
|
|
closeSwitcher()
|
|
$switcherSessions.set([])
|
|
$switcherIndex.set(0)
|
|
})
|
|
|
|
afterEach(() => {
|
|
seed([], null)
|
|
})
|
|
|
|
describe('openOrAdvanceSwitcher', () => {
|
|
it('does nothing with fewer than two sessions', () => {
|
|
seed(['a'], 'a')
|
|
onSwitcherTabDown()
|
|
|
|
expect(openOrAdvanceSwitcher(1)).toBeNull()
|
|
})
|
|
|
|
it('jumps immediately on a quick Tab tap without opening the HUD', () => {
|
|
seed(['a', 'b', 'c'], 'a')
|
|
|
|
expect(tabTap()).toBe('b')
|
|
expect($switcherOpen.get()).toBe(false)
|
|
expect(commitOnCtrlUp()).toBeNull()
|
|
})
|
|
|
|
it('does not open the HUD when Ctrl stays down but Tab was released quickly', () => {
|
|
vi.useFakeTimers()
|
|
seed(['a', 'b', 'c'], 'a')
|
|
|
|
tabTap()
|
|
vi.advanceTimersByTime(SWITCHER_REVEAL_MS)
|
|
|
|
expect($switcherOpen.get()).toBe(false)
|
|
})
|
|
|
|
it('opens the HUD when Tab stays held past the reveal delay', () => {
|
|
vi.useFakeTimers()
|
|
seed(['a', 'b', 'c'], 'a')
|
|
|
|
onSwitcherTabDown()
|
|
openOrAdvanceSwitcher(1)
|
|
vi.advanceTimersByTime(SWITCHER_REVEAL_MS)
|
|
|
|
expect($switcherOpen.get()).toBe(true)
|
|
onSwitcherTabUp()
|
|
})
|
|
|
|
it('opens on a second Tab while Ctrl is still down', () => {
|
|
seed(['a', 'b', 'c'], 'a')
|
|
|
|
expect(tabTap()).toBe('b')
|
|
onSwitcherTabDown()
|
|
openOrAdvanceSwitcher(1)
|
|
onSwitcherTabUp()
|
|
|
|
expect($switcherOpen.get()).toBe(true)
|
|
expect($switcherIndex.get()).toBe(2)
|
|
})
|
|
|
|
it('commits the HUD highlight on Ctrl up', () => {
|
|
seed(['a', 'b', 'c'], 'a')
|
|
|
|
expect(tabTap()).toBe('b')
|
|
onSwitcherTabDown()
|
|
openOrAdvanceSwitcher(1)
|
|
onSwitcherTabUp()
|
|
|
|
expect(commitOnCtrlUp()).toBe('c')
|
|
})
|
|
})
|
|
|
|
describe('slotSessionId', () => {
|
|
it('reads the armed snapshot while browsing is pending', () => {
|
|
seed(['a', 'b', 'c'], 'a')
|
|
tabTap()
|
|
$sessions.set([session('x')])
|
|
|
|
expect(slotSessionId(2)).toBe('b')
|
|
})
|
|
})
|