Skip to content

Commit ee91d45

Browse files
committed
Fix freebuff model tab navigation
1 parent 7562031 commit ee91d45

3 files changed

Lines changed: 91 additions & 9 deletions

File tree

cli/src/components/freebuff-model-selector.tsx

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@ import { useFreebuffModelStore } from '../state/freebuff-model-store'
1919
import { useFreebuffSessionStore } from '../state/freebuff-session-store'
2020
import { useTerminalDimensions } from '../hooks/use-terminal-dimensions'
2121
import { useTheme } from '../hooks/use-theme'
22-
import { nextFreebuffModelId } from '../utils/freebuff-model-navigation'
22+
import {
23+
freebuffModelNavigationDirectionForKey,
24+
nextFreebuffModelId,
25+
} from '../utils/freebuff-model-navigation'
2326

2427
import type { FreebuffModelOption } from '@codebuff/common/constants/freebuff-models'
2528
import type { KeyEvent } from '@opentui/core'
@@ -213,27 +216,27 @@ export const FreebuffModelSelector: React.FC = () => {
213216
(key: KeyEvent) => {
214217
if (pending) return
215218
const name = key.name ?? ''
216-
const isForward =
217-
name === 'right' || name === 'down' || (name === 'tab' && !key.shift)
218-
const isBackward =
219-
name === 'left' || name === 'up' || (name === 'tab' && key.shift)
219+
const direction = freebuffModelNavigationDirectionForKey(key)
220220
const isCommit =
221221
name === 'return' || name === 'enter' || name === 'space'
222-
if (!isForward && !isBackward && !isCommit) return
222+
if (!direction && !isCommit) return
223223
if (isCommit) {
224224
if (isJoinable(focusedId) && focusedId !== committedModelId) {
225225
key.preventDefault?.()
226+
key.stopPropagation?.()
226227
pick(focusedId)
227228
}
228229
return
229230
}
231+
if (!direction) return
230232
const targetId = nextFreebuffModelId({
231233
modelIds: FREEBUFF_MODEL_SELECTOR_MODELS.map((model) => model.id),
232234
focusedId,
233-
direction: isForward ? 'forward' : 'backward',
235+
direction,
234236
})
235237
if (targetId) {
236238
key.preventDefault?.()
239+
key.stopPropagation?.()
237240
setFocusedId(targetId)
238241
}
239242
},

cli/src/utils/__tests__/freebuff-model-navigation.test.ts

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { describe, expect, test } from 'bun:test'
22

3-
import { nextFreebuffModelId } from '../freebuff-model-navigation'
3+
import {
4+
freebuffModelNavigationDirectionForKey,
5+
nextFreebuffModelId,
6+
} from '../freebuff-model-navigation'
47

58
describe('nextFreebuffModelId', () => {
69
test('moves to the next model when moving forward', () => {
@@ -49,3 +52,51 @@ describe('nextFreebuffModelId', () => {
4952
).toBeNull()
5053
})
5154
})
55+
56+
describe('freebuffModelNavigationDirectionForKey', () => {
57+
test('maps arrow keys to model navigation directions', () => {
58+
expect(freebuffModelNavigationDirectionForKey({ name: 'down' })).toBe(
59+
'forward',
60+
)
61+
expect(freebuffModelNavigationDirectionForKey({ name: 'right' })).toBe(
62+
'forward',
63+
)
64+
expect(freebuffModelNavigationDirectionForKey({ name: 'up' })).toBe(
65+
'backward',
66+
)
67+
expect(freebuffModelNavigationDirectionForKey({ name: 'left' })).toBe(
68+
'backward',
69+
)
70+
})
71+
72+
test('maps tab and shift-tab to model navigation directions', () => {
73+
expect(freebuffModelNavigationDirectionForKey({ name: 'tab' })).toBe(
74+
'forward',
75+
)
76+
expect(
77+
freebuffModelNavigationDirectionForKey({ name: 'tab', shift: true }),
78+
).toBe('backward')
79+
})
80+
81+
test('maps terminal tab sequences to model navigation directions', () => {
82+
expect(freebuffModelNavigationDirectionForKey({ sequence: '\t' })).toBe(
83+
'forward',
84+
)
85+
expect(
86+
freebuffModelNavigationDirectionForKey({ sequence: '\x1b[9u' }),
87+
).toBe('forward')
88+
expect(
89+
freebuffModelNavigationDirectionForKey({ sequence: '\x1b[Z' }),
90+
).toBe('backward')
91+
expect(
92+
freebuffModelNavigationDirectionForKey({ sequence: '\x1b[9;2u' }),
93+
).toBe('backward')
94+
expect(
95+
freebuffModelNavigationDirectionForKey({ sequence: '\x1b[27;2;9~' }),
96+
).toBe('backward')
97+
})
98+
99+
test('ignores non-navigation keys', () => {
100+
expect(freebuffModelNavigationDirectionForKey({ name: 'enter' })).toBeNull()
101+
})
102+
})

cli/src/utils/freebuff-model-navigation.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
export type FreebuffModelNavigationDirection = 'forward' | 'backward'
2+
13
export function nextFreebuffModelId(params: {
24
modelIds: readonly string[]
35
focusedId: string
4-
direction: 'forward' | 'backward'
6+
direction: FreebuffModelNavigationDirection
57
}): string | null {
68
const { modelIds, focusedId, direction } = params
79
if (modelIds.length === 0) return null
@@ -12,3 +14,29 @@ export function nextFreebuffModelId(params: {
1214
const step = direction === 'forward' ? 1 : -1
1315
return modelIds[(currentIdx + step + modelIds.length) % modelIds.length]
1416
}
17+
18+
export function freebuffModelNavigationDirectionForKey(key: {
19+
name?: string
20+
shift?: boolean
21+
sequence?: string
22+
raw?: string
23+
}): FreebuffModelNavigationDirection | null {
24+
const name = (key.name ?? '').toLowerCase()
25+
const sequence = key.sequence ?? key.raw ?? ''
26+
27+
if (name === 'right' || name === 'down') return 'forward'
28+
if (name === 'left' || name === 'up') return 'backward'
29+
30+
const isShiftTab =
31+
(name === 'tab' && Boolean(key.shift)) ||
32+
sequence === '\x1b[Z' ||
33+
sequence === '\x1b[9;2u' ||
34+
sequence === '\x1b[27;2;9~'
35+
if (isShiftTab) return 'backward'
36+
37+
if (name === 'tab' || sequence === '\t' || sequence === '\x1b[9u') {
38+
return 'forward'
39+
}
40+
41+
return null
42+
}

0 commit comments

Comments
 (0)