1- import { useState , useMemo , useCallback , useEffect } from "react"
1+ import { useState , useMemo , useCallback } from "react"
22import { Fzf } from "fzf"
33
44import { cn } from "@/lib/utils"
@@ -10,68 +10,14 @@ import { Button } from "@/components/ui"
1010
1111import { IconButton } from "./IconButton"
1212
13- type ApiConfigMeta = {
14- id : string
15- name : string
16- apiProvider ?: string
17- modelId ?: string
18- }
19-
20- type ProviderGroup = {
21- key : string
22- label : string
23- configs : ApiConfigMeta [ ]
24- }
25-
26- const PROVIDER_LABELS : Record < string , string > = {
27- anthropic : "Anthropic" ,
28- bedrock : "Amazon Bedrock" ,
29- deepseek : "DeepSeek" ,
30- gemini : "Google Gemini" ,
31- "gemini-cli" : "Gemini CLI" ,
32- vertex : "Vertex AI" ,
33- openai : "OpenAI Compatible" ,
34- "openai-native" : "OpenAI" ,
35- "openai-codex" : "OpenAI Codex" ,
36- openrouter : "OpenRouter" ,
37- ollama : "Ollama" ,
38- lmstudio : "LM Studio" ,
39- mistral : "Mistral" ,
40- moonshot : "Moonshot" ,
41- minimax : "MiniMax" ,
42- requesty : "Requesty" ,
43- unbound : "Unbound" ,
44- poe : "Poe" ,
45- xai : "xAI" ,
46- baseten : "Baseten" ,
47- litellm : "LiteLLM" ,
48- sambanova : "SambaNova" ,
49- zai : "Z.ai" ,
50- fireworks : "Fireworks AI" ,
51- "qwen-code" : "Qwen Code" ,
52- roo : "Roo" ,
53- "vscode-lm" : "VS Code LM" ,
54- "vercel-ai-gateway" : "Vercel AI Gateway" ,
55- }
56-
57- const getProviderKey = ( config ?: ApiConfigMeta ) => config ?. apiProvider ?? "unknown"
58-
59- const getProviderLabel = ( apiProvider ?: string ) => {
60- if ( ! apiProvider ) {
61- return "Other"
62- }
63-
64- return PROVIDER_LABELS [ apiProvider ] ?? apiProvider
65- }
66-
6713interface ApiConfigSelectorProps {
6814 value : string
6915 displayName : string
7016 disabled ?: boolean
7117 title : string
7218 onChange : ( value : string ) => void
7319 triggerClassName ?: string
74- listApiConfigMeta : ApiConfigMeta [ ]
20+ listApiConfigMeta : Array < { id : string ; name : string ; modelId ?: string } >
7521 pinnedApiConfigs ?: Record < string , boolean >
7622 togglePinnedApiConfig : ( id : string ) => void
7723 lockApiConfigAcrossModes : boolean
@@ -94,17 +40,14 @@ export const ApiConfigSelector = ({
9440 const { t } = useAppTranslation ( )
9541 const [ open , setOpen ] = useState ( false )
9642 const [ searchValue , setSearchValue ] = useState ( "" )
97- const [ activeProviderKey , setActiveProviderKey ] = useState < string > ( "" )
9843 const portalContainer = useRooPortal ( "roo-portal" )
9944
10045 // Create searchable items for fuzzy search.
10146 const searchableItems = useMemo (
10247 ( ) =>
10348 listApiConfigMeta . map ( ( config ) => ( {
10449 original : config ,
105- searchStr : [ config . name , config . modelId , config . apiProvider , getProviderLabel ( config . apiProvider ) ]
106- . filter ( Boolean )
107- . join ( " " ) ,
50+ searchStr : config . name ,
10851 } ) ) ,
10952 [ listApiConfigMeta ] ,
11053 )
@@ -125,129 +68,54 @@ export const ApiConfigSelector = ({
12568 return matchingItems
12669 } , [ listApiConfigMeta , searchValue , fzfInstance ] )
12770
128- const providerGroups = useMemo < ProviderGroup [ ] > ( ( ) => {
129- const groups = new Map < string , ProviderGroup > ( )
130-
131- for ( const config of filteredConfigs ) {
132- const key = getProviderKey ( config )
133-
134- if ( ! groups . has ( key ) ) {
135- groups . set ( key , {
136- key,
137- label : getProviderLabel ( config . apiProvider ) ,
138- configs : [ ] ,
139- } )
140- }
141-
142- groups . get ( key ) ! . configs . push ( config )
143- }
144-
145- return Array . from ( groups . values ( ) ) . map ( ( group ) => ( {
146- ...group ,
147- configs : [ ...group . configs ] . sort ( ( a , b ) => {
148- const pinnedDelta = Number ( ! ! pinnedApiConfigs ?. [ b . id ] ) - Number ( ! ! pinnedApiConfigs ?. [ a . id ] )
149-
150- return pinnedDelta
151- } ) ,
152- } ) )
71+ // Separate pinned and unpinned configs.
72+ const { pinnedConfigs, unpinnedConfigs } = useMemo ( ( ) => {
73+ const pinned = filteredConfigs . filter ( ( config ) => pinnedApiConfigs ?. [ config . id ] )
74+ const unpinned = filteredConfigs . filter ( ( config ) => ! pinnedApiConfigs ?. [ config . id ] )
75+ return { pinnedConfigs : pinned , unpinnedConfigs : unpinned }
15376 } , [ filteredConfigs , pinnedApiConfigs ] )
15477
155- const currentConfig = useMemo (
156- ( ) => listApiConfigMeta . find ( ( config ) => config . id === value ) ,
157- [ listApiConfigMeta , value ] ,
158- )
159-
160- const currentProviderKey = getProviderKey ( currentConfig )
161- const preferredProviderKey = providerGroups . some ( ( group ) => group . key === currentProviderKey )
162- ? currentProviderKey
163- : ( providerGroups [ 0 ] ?. key ?? "" )
164-
165- useEffect ( ( ) => {
166- if ( ! providerGroups . length ) {
167- setActiveProviderKey ( "" )
168- return
169- }
170-
171- if ( ! providerGroups . some ( ( group ) => group . key === activeProviderKey ) ) {
172- setActiveProviderKey ( preferredProviderKey )
173- }
174- } , [ activeProviderKey , preferredProviderKey , providerGroups ] )
175-
176- const activeProviderGroup = providerGroups . find ( ( group ) => group . key === activeProviderKey ) ?? providerGroups [ 0 ]
177-
17878 const handleSelect = useCallback (
17979 ( configId : string ) => {
180- const selectedConfig = listApiConfigMeta . find ( ( config ) => config . id === configId )
181-
182- if ( selectedConfig ) {
183- setActiveProviderKey ( getProviderKey ( selectedConfig ) )
184- }
185-
18680 onChange ( configId )
18781 setOpen ( false )
18882 setSearchValue ( "" )
18983 } ,
190- [ listApiConfigMeta , onChange ] ,
84+ [ onChange ] ,
19185 )
19286
19387 const handleEditClick = useCallback ( ( ) => {
19488 vscode . postMessage ( { type : "switchTab" , tab : "settings" } )
19589 setOpen ( false )
19690 } , [ ] )
19791
198- const renderProviderItem = useCallback (
199- ( group : ProviderGroup ) => {
200- const isActive = group . key === activeProviderGroup ?. key
201- const currentCount = group . configs . filter ( ( config ) => config . id === value ) . length
202- const pinnedCount = group . configs . filter ( ( config ) => pinnedApiConfigs ?. [ config . id ] ) . length
203-
204- return (
205- < button
206- key = { group . key }
207- type = "button"
208- onClick = { ( ) => setActiveProviderKey ( group . key ) }
209- className = { cn (
210- "w-full px-2 py-1.5 text-left text-sm cursor-pointer flex items-center gap-2" ,
211- "hover:bg-vscode-list-hoverBackground" ,
212- isActive &&
213- "bg-vscode-list-activeSelectionBackground text-vscode-list-activeSelectionForeground" ,
214- ) } >
215- < span className = "truncate flex-1 min-w-0" > { group . label } </ span >
216- < div className = "flex items-center gap-1 flex-shrink-0" >
217- { pinnedCount > 0 && < span className = "codicon codicon-pin text-[10px] opacity-60" /> }
218- { currentCount > 0 && < span className = "codicon codicon-check text-xs" /> }
219- < span className = "text-[10px] opacity-70" > { group . configs . length } </ span >
220- </ div >
221- </ button >
222- )
223- } ,
224- [ activeProviderGroup ?. key , pinnedApiConfigs , value ] ,
225- )
226-
227- const renderModelItem = useCallback (
228- ( config : ApiConfigMeta ) => {
92+ const renderConfigItem = useCallback (
93+ ( config : { id : string ; name : string ; modelId ?: string } , isPinned : boolean ) => {
22994 const isCurrentConfig = config . id === value
230- const isPinned = ! ! pinnedApiConfigs ?. [ config . id ]
23195
23296 return (
23397 < div
23498 key = { config . id }
23599 onClick = { ( ) => handleSelect ( config . id ) }
236100 className = { cn (
237- "px-3 py-1.5 text-sm cursor-pointer flex items-center group gap-2 " ,
101+ "px-3 py-1.5 text-sm cursor-pointer flex items-center group" ,
238102 "hover:bg-vscode-list-hoverBackground" ,
239103 isCurrentConfig &&
240104 "bg-vscode-list-activeSelectionBackground text-vscode-list-activeSelectionForeground" ,
241105 ) } >
242- < div className = "flex-1 min-w-0 flex flex-col overflow-hidden leading-tight " >
243- < span className = "truncate " > { config . modelId || config . name } </ span >
106+ < div className = "flex-1 min-w-0 flex items-center gap-1 overflow-hidden " >
107+ < span className = "flex-shrink-0 " > { config . name } </ span >
244108 { config . modelId && (
245- < span className = "text-xs text-vscode-descriptionForeground opacity-80 truncate" >
246- { config . name }
247- </ span >
109+ < >
110+ < span
111+ className = "text-vscode-descriptionForeground opacity-70 min-w-0 overflow-hidden"
112+ style = { { direction : "rtl" , textOverflow : "ellipsis" , whiteSpace : "nowrap" } } >
113+ { config . modelId }
114+ </ span >
115+ </ >
248116 ) }
249117 </ div >
250- < div className = "flex items-center gap-1 flex-shrink-0 " >
118+ < div className = "flex items-center gap-1" >
251119 { isCurrentConfig && (
252120 < div className = "size-5 p-1 flex items-center justify-center" >
253121 < span className = "codicon codicon-check text-xs" />
@@ -274,7 +142,7 @@ export const ApiConfigSelector = ({
274142 </ div >
275143 )
276144 } ,
277- [ value , pinnedApiConfigs , handleSelect , t , togglePinnedApiConfig ] ,
145+ [ value , handleSelect , t , togglePinnedApiConfig ] ,
278146 )
279147
280148 return (
@@ -299,7 +167,7 @@ export const ApiConfigSelector = ({
299167 align = "start"
300168 sideOffset = { 4 }
301169 container = { portalContainer }
302- className = "p-0 overflow-hidden w-[520px] max-w-[calc(100vw-24px) ]" >
170+ className = "p-0 overflow-hidden w-[300px ]" >
303171 < div className = "flex flex-col w-full" >
304172 { /* Search input or info blurb */ }
305173 { listApiConfigMeta . length > 6 ? (
@@ -329,29 +197,29 @@ export const ApiConfigSelector = ({
329197 </ div >
330198 ) }
331199
332- { /* Provider/model picker */ }
200+ { /* Config list - single scroll container */ }
333201 { filteredConfigs . length === 0 && searchValue ? (
334202 < div className = "py-2 px-3 text-sm text-vscode-foreground/70" > { t ( "common:ui.no_results" ) } </ div >
335203 ) : (
336- < div className = "grid grid-cols-[170px_minmax(0,1fr)] max-h-[320px] overflow-hidden" >
337- < div
338- className = "border-r border-vscode-dropdown-border overflow-y-auto"
339- data-testid = "api-provider-column"
340- aria-label = { t ( "settings:providers.apiProvider" ) } >
341- < div className = "sticky top-0 z-10 bg-vscode-dropdown-background px-2 py-1 text-[10px] uppercase tracking-wide text-vscode-descriptionForeground border-b border-vscode-dropdown-border" >
342- { t ( "settings:providers.apiProvider" ) }
204+ < div className = "max-h-[300px] overflow-y-auto" >
205+ { /* Pinned configs - sticky header */ }
206+ { pinnedConfigs . length > 0 && (
207+ < div
208+ className = { cn (
209+ "sticky top-0 z-10 bg-vscode-dropdown-background py-1" ,
210+ unpinnedConfigs . length > 0 && "border-b border-vscode-dropdown-foreground/10" ,
211+ ) }
212+ aria-label = "Pinned configurations" >
213+ { pinnedConfigs . map ( ( config ) => renderConfigItem ( config , true ) ) }
343214 </ div >
344- < div className = "py-1" > { providerGroups . map ( renderProviderItem ) } </ div >
345- </ div >
346- < div
347- className = "overflow-y-auto"
348- data-testid = "api-model-column"
349- aria-label = { t ( "settings:providers.model" ) } >
350- < div className = "sticky top-0 z-10 bg-vscode-dropdown-background px-3 py-1 text-[10px] uppercase tracking-wide text-vscode-descriptionForeground border-b border-vscode-dropdown-border" >
351- { activeProviderGroup ?. label ?? t ( "settings:providers.model" ) }
215+ ) }
216+
217+ { /* Unpinned configs */ }
218+ { unpinnedConfigs . length > 0 && (
219+ < div className = "py-1" aria-label = "All configurations" >
220+ { unpinnedConfigs . map ( ( config ) => renderConfigItem ( config , false ) ) }
352221 </ div >
353- < div className = "py-1" > { activeProviderGroup ?. configs . map ( renderModelItem ) } </ div >
354- </ div >
222+ ) }
355223 </ div >
356224 ) }
357225
0 commit comments