1- import { useState , useMemo , useCallback , useRef } from "react"
2- import { useTranslation } from "react-i18next"
1+ import { useState , useMemo , useCallback } from "react"
32import { Fzf } from "fzf"
43
54import { cn } from "@/lib/utils"
@@ -10,6 +9,84 @@ import { Button } from "@/components/ui"
109import { useExtensionState } from "@/context/ExtensionStateContext"
1110
1211import { IconButton } from "./IconButton"
12+ import { useAppTranslation } from "@/i18n/TranslationContext"
13+
14+ interface ConfigItemProps {
15+ config : { id : string ; name : string ; modelId ?: string }
16+ isPinned : boolean
17+ index : number
18+ value : string
19+ onSelect : ( configId : string ) => void
20+ togglePinnedApiConfig : ( id : string ) => void
21+ }
22+
23+ const ConfigItem = ( { config, isPinned, index, value, onSelect, togglePinnedApiConfig } : ConfigItemProps ) => {
24+ const { t } = useAppTranslation ( )
25+ const isCurrentConfig = config . id === value
26+
27+ return (
28+ < div
29+ key = { config . id }
30+ data-config-item
31+ data-config-item-index = { index }
32+ role = "option"
33+ aria-selected = { isCurrentConfig }
34+ aria-label = { `${ config . name } ${ config . modelId ? ` - ${ config . modelId } ` : "" } ` }
35+ onClick = { ( ) => onSelect ( config . id ) }
36+ onKeyDown = { ( e ) => {
37+ if ( e . key === "Enter" || e . key === " " ) {
38+ e . preventDefault ( )
39+ onSelect ( config . id )
40+ }
41+ } }
42+ className = { cn (
43+ "px-3 py-1.5 text-sm flex items-center group relative" ,
44+ "cursor-pointer hover:bg-vscode-list-hoverBackground" ,
45+ isCurrentConfig &&
46+ "bg-vscode-list-activeSelectionBackground text-vscode-list-activeSelectionForeground" ,
47+ ) } >
48+ < div className = "flex-1 min-w-0 flex items-center gap-1 overflow-hidden" >
49+ < span className = "flex-shrink-0" > { config . name } </ span >
50+ { config . modelId && (
51+ < >
52+ < span
53+ className = "text-vscode-descriptionForeground opacity-70 min-w-0 overflow-hidden"
54+ style = { { direction : "rtl" , textOverflow : "ellipsis" , whiteSpace : "nowrap" } } >
55+ { config . modelId }
56+ </ span >
57+ </ >
58+ ) }
59+ </ div >
60+
61+ < div className = "flex items-center gap-1" >
62+ { isCurrentConfig && (
63+ < div className = "size-5 p-1 flex items-center justify-center" >
64+ < span className = "codicon codicon-check text-xs" />
65+ </ div >
66+ ) }
67+ < StandardTooltip
68+ content = { isPinned ? t ( "chat:apiConfigSelector.unpin" ) : t ( "chat:apiConfigSelector.pin" ) } >
69+ < Button
70+ variant = "ghost"
71+ size = "icon"
72+ tabIndex = { - 1 }
73+ aria-label = { isPinned ? t ( "chat:apiConfigSelector.unpin" ) : t ( "chat:apiConfigSelector.pin" ) }
74+ onClick = { ( e ) => {
75+ e . stopPropagation ( )
76+ togglePinnedApiConfig ( config . id )
77+ vscode . postMessage ( { type : "toggleApiConfigPin" , text : config . id } )
78+ } }
79+ className = { cn ( "size-5 flex items-center justify-center" , {
80+ "opacity-0 group-hover:opacity-100" : ! isPinned && ! isCurrentConfig ,
81+ "bg-accent opacity-100" : isPinned ,
82+ } ) } >
83+ < span className = "codicon codicon-pin text-xs opacity-50" />
84+ </ Button >
85+ </ StandardTooltip >
86+ </ div >
87+ </ div >
88+ )
89+ }
1390
1491type SortMode = "alphabetical" | "custom"
1592
@@ -36,14 +113,13 @@ export const ApiConfigSelector = ({
36113 pinnedApiConfigs,
37114 togglePinnedApiConfig,
38115} : ApiConfigSelectorProps ) => {
39- const { t } = useTranslation ( )
116+ const { t } = useAppTranslation ( )
40117 const { apiConfigsCustomOrder : customOrder = [ ] } = useExtensionState ( )
41118 const [ open , setOpen ] = useState ( false )
42119 const [ searchValue , setSearchValue ] = useState ( "" )
43120 const [ sortMode , setSortMode ] = useState < SortMode > ( "alphabetical" )
44121
45122 const portalContainer = useRooPortal ( "roo-portal" )
46- const scrollContainerRef = useRef < HTMLDivElement | null > ( null )
47123
48124 // Sort configs based on sort mode
49125 const sortedConfigs = useMemo ( ( ) => {
@@ -75,13 +151,8 @@ export const ApiConfigSelector = ({
75151 return sortedConfigs
76152 }
77153
78- const searchableItems = sortedConfigs . map ( ( config ) => ( {
79- original : config ,
80- searchStr : config . name ,
81- } ) )
82-
83- const fzf = new Fzf ( searchableItems , { selector : ( item ) => item . searchStr } )
84- const matchingItems = fzf . find ( searchValue ) . map ( ( result ) => result . item . original )
154+ const fzf = new Fzf ( sortedConfigs , { selector : ( item ) => item . name } )
155+ const matchingItems = fzf . find ( searchValue ) . map ( ( result ) => result . item )
85156 return matchingItems
86157 } , [ sortedConfigs , searchValue ] )
87158
@@ -106,88 +177,12 @@ export const ApiConfigSelector = ({
106177 setOpen ( false )
107178 } , [ ] )
108179
109- const renderConfigItem = useCallback (
110- ( config : { id : string ; name : string ; modelId ?: string } , isPinned : boolean , index : number ) => {
111- const isCurrentConfig = config . id === value
112-
113- return (
114- < div
115- key = { config . id }
116- data-config-item
117- data-config-item-index = { index }
118- role = "option"
119- aria-selected = { isCurrentConfig }
120- aria-label = { `${ config . name } ${ config . modelId ? ` - ${ config . modelId } ` : "" } ` }
121- onClick = { ( ) => handleSelect ( config . id ) }
122- onKeyDown = { ( e ) => {
123- if ( e . key === "Enter" || e . key === " " ) {
124- e . preventDefault ( )
125- handleSelect ( config . id )
126- }
127- } }
128- tabIndex = { 0 }
129- className = { cn (
130- "px-3 py-1.5 text-sm flex items-center group relative" ,
131- "cursor-pointer hover:bg-vscode-list-hoverBackground" ,
132- isCurrentConfig &&
133- "bg-vscode-list-activeSelectionBackground text-vscode-list-activeSelectionForeground" ,
134- ) } >
135- < div className = "flex-1 min-w-0 flex items-center gap-1 overflow-hidden" >
136- < span className = "flex-shrink-0" > { config . name } </ span >
137- { config . modelId && (
138- < >
139- < span
140- className = "text-vscode-descriptionForeground opacity-70 min-w-0 overflow-hidden"
141- style = { { direction : "rtl" , textOverflow : "ellipsis" , whiteSpace : "nowrap" } } >
142- { config . modelId }
143- </ span >
144- </ >
145- ) }
146- </ div >
147-
148- < div className = "flex items-center gap-1" >
149- { isCurrentConfig && (
150- < div className = "size-5 p-1 flex items-center justify-center" >
151- < span className = "codicon codicon-check text-xs" />
152- </ div >
153- ) }
154- < StandardTooltip
155- content = { isPinned ? t ( "chat:apiConfigSelector.unpin" ) : t ( "chat:apiConfigSelector.pin" ) } >
156- < Button
157- variant = "ghost"
158- size = "icon"
159- tabIndex = { - 1 }
160- aria-label = {
161- isPinned ? t ( "chat:apiConfigSelector.unpin" ) : t ( "chat:apiConfigSelector.pin" )
162- }
163- onClick = { ( e ) => {
164- e . stopPropagation ( )
165- togglePinnedApiConfig ( config . id )
166- vscode . postMessage ( { type : "toggleApiConfigPin" , text : config . id } )
167- } }
168- className = { cn ( "size-5 flex items-center justify-center" , {
169- "opacity-0 group-hover:opacity-100" : ! isPinned && ! isCurrentConfig ,
170- "bg-accent opacity-100" : isPinned ,
171- } ) } >
172- < span className = "codicon codicon-pin text-xs opacity-50" />
173- </ Button >
174- </ StandardTooltip >
175- </ div >
176- </ div >
177- )
178- } ,
179- [ value , handleSelect , togglePinnedApiConfig , t ] ,
180- )
181-
182180 return (
183181 < Popover open = { open } onOpenChange = { setOpen } data-testid = "api-config-selector-root" >
184182 < StandardTooltip content = { title } >
185183 < PopoverTrigger
186184 disabled = { disabled }
187185 data-testid = "dropdown-trigger"
188- aria-label = { title }
189- aria-expanded = { open }
190- aria-haspopup = "listbox"
191186 className = { cn (
192187 "min-w-0 inline-flex items-center relative whitespace-nowrap px-1.5 py-1 text-xs" ,
193188 "bg-transparent border border-[rgba(255,255,255,0.08)] rounded-md text-vscode-foreground" ,
@@ -222,15 +217,6 @@ export const ApiConfigSelector = ({
222217 < span
223218 className = "codicon codicon-close text-vscode-input-foreground opacity-50 hover:opacity-100 text-xs cursor-pointer"
224219 onClick = { ( ) => setSearchValue ( "" ) }
225- aria-label = "Clear search"
226- role = "button"
227- tabIndex = { 0 }
228- onKeyDown = { ( e ) => {
229- if ( e . key === "Enter" || e . key === " " ) {
230- e . preventDefault ( )
231- setSearchValue ( "" )
232- }
233- } }
234220 />
235221 </ div >
236222 ) }
@@ -245,7 +231,6 @@ export const ApiConfigSelector = ({
245231
246232 { /* Config list */ }
247233 < div
248- ref = { scrollContainerRef }
249234 className = "max-h-[300px] overflow-y-auto"
250235 role = "listbox"
251236 aria-label = { t ( "prompts:apiConfiguration.select" ) } >
@@ -256,17 +241,35 @@ export const ApiConfigSelector = ({
256241 ) : (
257242 < div className = "py-1" >
258243 { /* Pinned configs */ }
259- { pinnedConfigs . map ( ( config , index ) => renderConfigItem ( config , true , index ) ) }
244+ { pinnedConfigs . map ( ( config , index ) => (
245+ < ConfigItem
246+ key = { config . id }
247+ config = { config }
248+ isPinned
249+ index = { index }
250+ value = { value }
251+ onSelect = { handleSelect }
252+ togglePinnedApiConfig = { togglePinnedApiConfig }
253+ />
254+ ) ) }
260255
261256 { /* Separator between pinned and unpinned */ }
262257 { pinnedConfigs . length > 0 && unpinnedConfigs . length > 0 && (
263258 < div className = "mx-1 my-1 h-px bg-vscode-dropdown-foreground/10" />
264259 ) }
265260
266261 { /* Unpinned configs */ }
267- { unpinnedConfigs . map ( ( config , index ) =>
268- renderConfigItem ( config , false , pinnedConfigs . length + index ) ,
269- ) }
262+ { unpinnedConfigs . map ( ( config , index ) => (
263+ < ConfigItem
264+ key = { config . id }
265+ config = { config }
266+ isPinned = { false }
267+ index = { pinnedConfigs . length + index }
268+ value = { value }
269+ onSelect = { handleSelect }
270+ togglePinnedApiConfig = { togglePinnedApiConfig }
271+ />
272+ ) ) }
270273 </ div >
271274 ) }
272275 </ div >
0 commit comments