Skip to content

Commit 868e2f1

Browse files
authored
Fix freebuff model tab navigation (#597)
1 parent 2ac2b09 commit 868e2f1

3 files changed

Lines changed: 102 additions & 10 deletions

File tree

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

Lines changed: 13 additions & 8 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'
@@ -32,6 +35,9 @@ const FREEBUFF_MODEL_SELECTOR_MODELS: readonly FreebuffModelOption[] = [
3235
...FREEBUFF_MODELS.filter((model) => model.id === DEFAULT_FREEBUFF_MODEL_ID),
3336
...FREEBUFF_MODELS.filter((model) => model.id !== DEFAULT_FREEBUFF_MODEL_ID),
3437
]
38+
const FREEBUFF_MODEL_SELECTOR_MODEL_IDS = FREEBUFF_MODEL_SELECTOR_MODELS.map(
39+
(model) => model.id,
40+
)
3541

3642
function formatSessionUnits(units: number): string {
3743
return Number.isInteger(units) ? String(units) : units.toFixed(1)
@@ -213,27 +219,26 @@ export const FreebuffModelSelector: React.FC = () => {
213219
(key: KeyEvent) => {
214220
if (pending) return
215221
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)
222+
const direction = freebuffModelNavigationDirectionForKey(key)
220223
const isCommit =
221224
name === 'return' || name === 'enter' || name === 'space'
222-
if (!isForward && !isBackward && !isCommit) return
223225
if (isCommit) {
224226
if (isJoinable(focusedId) && focusedId !== committedModelId) {
225227
key.preventDefault?.()
228+
key.stopPropagation?.()
226229
pick(focusedId)
227230
}
228231
return
229232
}
233+
if (!direction) return
230234
const targetId = nextFreebuffModelId({
231-
modelIds: FREEBUFF_MODEL_SELECTOR_MODELS.map((model) => model.id),
235+
modelIds: FREEBUFF_MODEL_SELECTOR_MODEL_IDS,
232236
focusedId,
233-
direction: isForward ? 'forward' : 'backward',
237+
direction,
234238
})
235239
if (targetId) {
236240
key.preventDefault?.()
241+
key.stopPropagation?.()
237242
setFocusedId(targetId)
238243
}
239244
},

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: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,18 @@
1+
export type FreebuffModelNavigationDirection = 'forward' | 'backward'
2+
3+
const FORWARD_KEY_NAMES = new Set(['right', 'down'])
4+
const BACKWARD_KEY_NAMES = new Set(['left', 'up'])
5+
const FORWARD_TAB_SEQUENCES = new Set(['\t', '\x1b[9u'])
6+
const BACKWARD_TAB_SEQUENCES = new Set([
7+
'\x1b[Z',
8+
'\x1b[9;2u',
9+
'\x1b[27;2;9~',
10+
])
11+
112
export function nextFreebuffModelId(params: {
213
modelIds: readonly string[]
314
focusedId: string
4-
direction: 'forward' | 'backward'
15+
direction: FreebuffModelNavigationDirection
516
}): string | null {
617
const { modelIds, focusedId, direction } = params
718
if (modelIds.length === 0) return null
@@ -12,3 +23,28 @@ export function nextFreebuffModelId(params: {
1223
const step = direction === 'forward' ? 1 : -1
1324
return modelIds[(currentIdx + step + modelIds.length) % modelIds.length]
1425
}
26+
27+
export function freebuffModelNavigationDirectionForKey(key: {
28+
name?: string
29+
shift?: boolean
30+
sequence?: string
31+
raw?: string
32+
}): FreebuffModelNavigationDirection | null {
33+
const name = (key.name ?? '').toLowerCase()
34+
const sequence = key.sequence ?? key.raw ?? ''
35+
36+
if (FORWARD_KEY_NAMES.has(name)) return 'forward'
37+
if (BACKWARD_KEY_NAMES.has(name)) return 'backward'
38+
39+
if (
40+
(name === 'tab' && Boolean(key.shift)) ||
41+
BACKWARD_TAB_SEQUENCES.has(sequence)
42+
) {
43+
return 'backward'
44+
}
45+
if (name === 'tab' || FORWARD_TAB_SEQUENCES.has(sequence)) {
46+
return 'forward'
47+
}
48+
49+
return null
50+
}

0 commit comments

Comments
 (0)