@@ -13,8 +13,8 @@ import { useTheme } from '../hooks/use-theme'
1313import type { KeyEvent } from '@opentui/core'
1414
1515/**
16- * Lets the user pick which model's queue they're in. Tapping (or pressing the
17- * row's number key) on a different model triggers a re-POST: the server moves
16+ * Lets the user pick which model's queue they're in. Tapping a different model
17+ * (or cycling to it via Tab / arrow keys) triggers a re-POST: the server moves
1818 * them to the back of the new model's queue.
1919 *
2020 * Each row shows a live "N ahead" count sourced from the server's
@@ -43,6 +43,19 @@ export const FreebuffModelSelector: React.FC = () => {
4343 return out
4444 } , [ session ] )
4545
46+ // Pad the trailing hint ("3 ahead", "No wait", tagline) to a fixed width so
47+ // buttons don't visibly resize when the queue depth ticks down (12 → 9) or
48+ // when the user's selection moves between queues.
49+ const hintWidth = useMemo (
50+ ( ) =>
51+ Math . max (
52+ 'No wait' . length ,
53+ '999 ahead' . length ,
54+ ...FREEBUFF_MODELS . map ( ( m ) => m . tagline . length ) ,
55+ ) ,
56+ [ ] ,
57+ )
58+
4659 const pick = useCallback (
4760 ( modelId : string ) => {
4861 if ( pending ) return
@@ -53,17 +66,23 @@ export const FreebuffModelSelector: React.FC = () => {
5366 [ pending , selectedModel ] ,
5467 )
5568
56- // Number-key shortcuts (1-9) so keyboard-only users can switch without
57- // hunting for a clickable region .
69+ // Tab / Shift+Tab and Left/Right arrow keys cycle through the model buttons.
70+ // Up/Down intentionally do nothing so they don't fight other vertical UI .
5871 useKeyboard (
5972 useCallback (
6073 ( key : KeyEvent ) => {
6174 if ( pending ) return
6275 const name = key . name ?? ''
63- if ( ! / ^ [ 1 - 9 ] $ / . test ( name ) ) return
64- const digit = Number ( name )
65- if ( digit > FREEBUFF_MODELS . length ) return
66- const target = FREEBUFF_MODELS [ digit - 1 ]
76+ const isForward = name === 'right' || ( name === 'tab' && ! key . shift )
77+ const isBackward = name === 'left' || ( name === 'tab' && key . shift )
78+ if ( ! isForward && ! isBackward ) return
79+ const currentIdx = FREEBUFF_MODELS . findIndex ( ( m ) => m . id === selectedModel )
80+ if ( currentIdx === - 1 ) return
81+ const len = FREEBUFF_MODELS . length
82+ const nextIdx = isForward
83+ ? ( currentIdx + 1 ) % len
84+ : ( currentIdx - 1 + len ) % len
85+ const target = FREEBUFF_MODELS [ nextIdx ]
6786 if ( target && target . id !== selectedModel ) {
6887 key . preventDefault ?.( )
6988 pick ( target . id )
@@ -81,18 +100,14 @@ export const FreebuffModelSelector: React.FC = () => {
81100 gap : 0 ,
82101 } }
83102 >
84- < text style = { { fg : theme . muted , marginBottom : 1 } } >
85- Model — tap or press 1-{ FREEBUFF_MODELS . length } to switch
86- </ text >
87103 < box
88104 style = { {
89105 flexDirection : 'row' ,
90106 gap : 2 ,
91107 } }
92108 >
93- { FREEBUFF_MODELS . map ( ( model , idx ) => {
109+ { FREEBUFF_MODELS . map ( ( model ) => {
94110 const isSelected = model . id === selectedModel
95- const isPending = pending === model . id
96111 const isHovered = hoveredId === model . id
97112 const indicator = isSelected ? '●' : '○'
98113 const indicatorColor = isSelected ? theme . primary : theme . muted
@@ -128,16 +143,13 @@ export const FreebuffModelSelector: React.FC = () => {
128143 >
129144 < text >
130145 < span fg = { indicatorColor } > { indicator } </ span >
131- < span fg = { theme . muted } > { idx + 1 } . </ span >
132146 < span
133147 fg = { labelColor }
134148 attributes = { isSelected ? TextAttributes . BOLD : TextAttributes . NONE }
135149 >
136150 { model . displayName }
137151 </ span >
138- < span fg = { theme . muted } > { hint } </ span >
139- { isPending && < span fg = { theme . muted } > switching…</ span > }
140-
152+ < span fg = { theme . muted } > { hint . padEnd ( hintWidth ) } </ span >
141153 </ text >
142154 </ Button >
143155 )
0 commit comments