@@ -4,10 +4,9 @@ import React, { useState, useMemo, useCallback } from 'react'
44import { useTheme } from '../hooks/use-theme'
55import { useProviderStore } from '../state/provider-store'
66import { BORDER_CHARS } from '../utils/ui-constants'
7- import { PROVIDER_DEFINITIONS } from '@levelcode/common/providers/provider-registry'
7+ import { getProviderDefinition } from '@levelcode/common/providers/provider-registry'
88
99import type { KeyEvent } from '@opentui/core'
10- import type { ModelCatalogEntry } from '@levelcode/common/providers/provider-types'
1110
1211interface ModelPickerProps {
1312 onClose : ( ) => void
@@ -18,53 +17,34 @@ export const ModelPicker: React.FC<ModelPickerProps> = ({ onClose }) => {
1817
1918 const [ searchQuery , setSearchQuery ] = useState ( '' )
2019 const [ selectedIndex , setSelectedIndex ] = useState ( 0 )
21- const [ selectedProviderId , setSelectedProviderId ] = useState < string | null > ( null )
2220
2321 const catalogModels = useProviderStore ( ( state ) => state . catalogModels )
2422 const activeModel = useProviderStore ( ( state ) => state . config . activeModel )
2523 const activeProvider = useProviderStore ( ( state ) => state . config . activeProvider )
26- const configuredProviders = useProviderStore (
27- ( state ) => Object . keys ( state . config . providers ) ,
28- )
2924
3025 const filteredModels = useMemo ( ( ) => {
31- let models = catalogModels
32-
33- // Filter by selected provider
34- if ( selectedProviderId ) {
35- models = models . filter ( ( m ) => m . providerId === selectedProviderId )
36- }
37-
38- // Filter by search query
39- if ( searchQuery . trim ( ) ) {
40- const query = searchQuery . toLowerCase ( )
41- models = models . filter (
42- ( m ) =>
43- m . name . toLowerCase ( ) . includes ( query ) ||
44- m . id . toLowerCase ( ) . includes ( query ) ,
45- )
46- }
47-
48- return models
49- } , [ catalogModels , selectedProviderId , searchQuery ] )
50-
51- const formatCost = ( model : ModelCatalogEntry ) : string => {
52- if ( ! model . cost ) return '-'
53- return `$${ model . cost . input . toFixed ( 2 ) } /$${ model . cost . output . toFixed ( 2 ) } `
54- }
55-
56- const formatContext = ( model : ModelCatalogEntry ) : string => {
57- if ( ! model . limit ?. context ) return '-'
58- const ctx = model . limit . context
59- if ( ctx >= 1_000_000 ) return `${ ( ctx / 1_000_000 ) . toFixed ( 1 ) } M`
60- if ( ctx >= 1_000 ) return `${ Math . round ( ctx / 1_000 ) } K`
61- return String ( ctx )
62- }
63-
64- const getProviderName = ( providerId : string ) : string => {
65- const def = PROVIDER_DEFINITIONS [ providerId ]
66- return def ?. name ?? providerId
67- }
26+ if ( ! searchQuery . trim ( ) ) return catalogModels
27+ const query = searchQuery . toLowerCase ( )
28+ return catalogModels . filter (
29+ ( m ) =>
30+ m . name . toLowerCase ( ) . includes ( query ) ||
31+ m . id . toLowerCase ( ) . includes ( query ) ,
32+ )
33+ } , [ catalogModels , searchQuery ] )
34+
35+ // Clamp selectedIndex
36+ const clampedIndex = Math . min ( selectedIndex , Math . max ( 0 , filteredModels . length - 1 ) )
37+
38+ // Visible window
39+ const maxVisible = 10
40+ const scrollOffset = Math . max (
41+ 0 ,
42+ Math . min ( clampedIndex - Math . floor ( maxVisible / 2 ) , filteredModels . length - maxVisible ) ,
43+ )
44+ const visibleModels = filteredModels . slice (
45+ Math . max ( 0 , scrollOffset ) ,
46+ Math . max ( 0 , scrollOffset ) + maxVisible ,
47+ )
6848
6949 useKeyboard (
7050 useCallback (
@@ -73,82 +53,39 @@ export const ModelPicker: React.FC<ModelPickerProps> = ({ onClose }) => {
7353 onClose ( )
7454 return
7555 }
76-
7756 if ( key . name === 'up' ) {
7857 setSelectedIndex ( ( prev ) => Math . max ( 0 , prev - 1 ) )
7958 return
8059 }
8160 if ( key . name === 'down' ) {
82- setSelectedIndex ( ( prev ) =>
83- Math . min ( filteredModels . length - 1 , prev + 1 ) ,
84- )
61+ setSelectedIndex ( ( prev ) => Math . min ( filteredModels . length - 1 , prev + 1 ) )
8562 return
8663 }
87-
8864 if ( key . name === 'return' || key . name === 'enter' ) {
89- const model = filteredModels [ selectedIndex ]
65+ const model = filteredModels [ clampedIndex ]
9066 if ( model ) {
91- useProviderStore
92- . getState ( )
93- . setActiveModel ( model . providerId , model . id )
94- onClose ( )
95- }
96- return
97- }
98-
99- // Tab cycles through provider filters
100- if ( key . name === 'tab' ) {
101- if ( ! selectedProviderId ) {
102- setSelectedProviderId ( configuredProviders [ 0 ] ?? null )
103- } else {
104- const currentIdx = configuredProviders . indexOf ( selectedProviderId )
105- const nextIdx = currentIdx + 1
106- if ( nextIdx >= configuredProviders . length ) {
107- setSelectedProviderId ( null ) // cycle back to "all"
108- } else {
109- setSelectedProviderId ( configuredProviders [ nextIdx ] ?? null )
110- }
67+ useProviderStore . getState ( ) . setActiveModel ( model . providerId , model . id )
11168 }
112- setSelectedIndex ( 0 )
69+ onClose ( )
11370 return
11471 }
115-
116- // Backspace in search
11772 if ( key . name === 'backspace' || key . name === 'delete' ) {
11873 setSearchQuery ( ( prev ) => prev . slice ( 0 , - 1 ) )
11974 setSelectedIndex ( 0 )
12075 return
12176 }
122-
123- // Printable character input for search
12477 if ( key . sequence && key . sequence . length === 1 && ! key . ctrl && ! key . meta ) {
12578 setSearchQuery ( ( prev ) => prev + key . sequence )
12679 setSelectedIndex ( 0 )
12780 return
12881 }
12982 } ,
130- [ filteredModels , selectedIndex , selectedProviderId , configuredProviders , onClose ] ,
83+ [ filteredModels , clampedIndex , onClose ] ,
13184 ) ,
13285 )
13386
134- // Determine visible range for scrolling
135- const maxVisible = 12
136- const scrollOffset = Math . max (
137- 0 ,
138- Math . min (
139- selectedIndex - Math . floor ( maxVisible / 2 ) ,
140- filteredModels . length - maxVisible ,
141- ) ,
142- )
143- const visibleModels = filteredModels . slice (
144- scrollOffset ,
145- scrollOffset + maxVisible ,
146- )
147-
14887 return (
14988 < box
150- title = " Model Picker "
151- titleAlignment = "center"
15289 style = { {
15390 width : '100%' ,
15491 borderStyle : 'single' ,
@@ -159,83 +96,42 @@ export const ModelPicker: React.FC<ModelPickerProps> = ({ onClose }) => {
15996 flexDirection : 'column' ,
16097 } }
16198 >
162- { /* Search bar */ }
163- < box style = { { flexDirection : 'row' , height : 1 } } >
164- < text style = { { fg : theme . primary } } > Search: </ text >
165- < text style = { { fg : theme . foreground } } >
166- { searchQuery || '' }
167- </ text >
168- < text style = { { fg : theme . muted } } >
169- { searchQuery ? '' : '(type to filter)' }
170- </ text >
171- </ box >
172-
173- { /* Provider filter indicator */ }
174- < box style = { { flexDirection : 'row' , height : 1 } } >
175- < text style = { { fg : theme . muted } } >
176- Filter:{ ' ' }
177- </ text >
178- < text
179- style = { {
180- fg : selectedProviderId ? theme . info : theme . muted ,
181- } }
182- >
183- { selectedProviderId
184- ? getProviderName ( selectedProviderId )
185- : 'All providers' }
186- </ text >
187- < text style = { { fg : theme . muted } } >
188- { ' ' } ({ filteredModels . length } models)
189- </ text >
190- </ box >
191-
192- { /* Column header */ }
99+ < text style = { { fg : theme . primary } } >
100+ { 'Model Picker' }
101+ </ text >
193102 < text style = { { fg : theme . muted } } >
194- { ' ' } { 'Name' . padEnd ( 32 ) } { 'Provider' . padEnd ( 16 ) } { 'Cost' . padEnd ( 16 ) } { 'Context' }
103+ { 'Search: ' } { searchQuery || '(type to filter)' }
104+ </ text >
105+ < text style = { { fg : theme . muted } } >
106+ { filteredModels . length } { ' models available' }
195107 </ text >
196-
197- { /* Model list */ }
198108 { visibleModels . map ( ( model , visibleIdx ) => {
199- const realIndex = scrollOffset + visibleIdx
200- const isSelected = realIndex === selectedIndex
201- const isActive =
202- model . id === activeModel && model . providerId === activeProvider
109+ const realIndex = Math . max ( 0 , scrollOffset ) + visibleIdx
110+ const isSelected = realIndex === clampedIndex
111+ const isActive = model . id === activeModel && model . providerId === activeProvider
112+ const providerName = getProviderDefinition ( model . providerId ) ?. name ?? model . providerId
203113
204114 return (
205- < box key = { `${ model . providerId } -${ model . id } ` } style = { { flexDirection : 'row' , height : 1 } } >
206- < text
207- style = { {
208- fg : isActive
209- ? theme . success
210- : isSelected
211- ? theme . primary
212- : theme . foreground ,
213- bg : isSelected ? theme . surface : undefined ,
214- } }
215- >
216- { isSelected ? '> ' : ' ' }
217- { model . name . slice ( 0 , 30 ) . padEnd ( 30 ) }
218- { ' ' }
219- { getProviderName ( model . providerId ) . slice ( 0 , 14 ) . padEnd ( 14 ) }
220- { ' ' }
221- { formatCost ( model ) . padEnd ( 14 ) }
222- { ' ' }
223- { formatContext ( model ) }
224- { isActive ? ' *' : '' }
225- </ text >
226- </ box >
115+ < text
116+ key = { `${ model . providerId } -${ model . id } -${ visibleIdx } ` }
117+ style = { {
118+ fg : isActive ? theme . success : isSelected ? theme . primary : theme . foreground ,
119+ } }
120+ >
121+ { isSelected ? '> ' : ' ' }
122+ { model . name . slice ( 0 , 30 ) . padEnd ( 32 ) }
123+ { providerName . slice ( 0 , 14 ) . padEnd ( 16 ) }
124+ { isActive ? '*' : '' }
125+ </ text >
227126 )
228127 } ) }
229-
230128 { filteredModels . length === 0 && (
231129 < text style = { { fg : theme . muted } } >
232- { ' ' } No models found.
130+ { ' No models found. Run /provider:add first.' }
233131 </ text >
234132 ) }
235-
236- { /* Help text */ }
237133 < text style = { { fg : theme . muted } } >
238- { '\u2191\u2193 navigate \u00B7 Enter select \u00B7 Tab filter provider \u00B7 Esc close' }
134+ { 'Up/Down navigate | Enter select | Esc close' }
239135 </ text >
240136 </ box >
241137 )
0 commit comments