11import React , { forwardRef , useCallback , useEffect , useLayoutEffect , useMemo , useRef , useState } from "react"
22import { useEvent } from "react-use"
33import DynamicTextArea from "react-textarea-autosize"
4- import { VolumeX , Image , WandSparkles , SendHorizontal , X , ListEnd , Square } from "lucide-react"
4+ import { VolumeX , Image , WandSparkles , SendHorizontal , X , ListEnd , Square , Check , ChevronsUpDown } from "lucide-react"
55
6- import type { ExtensionMessage } from "@roo-code/types"
6+ import {
7+ isDynamicProvider ,
8+ isRetiredProvider ,
9+ modelIdKeysByProvider ,
10+ openAiModelInfoSaneDefaults ,
11+ type ExtensionMessage ,
12+ type ModelIdKey ,
13+ type ModelRecord ,
14+ type OrganizationAllowList ,
15+ type ProviderName ,
16+ type ProviderSettings ,
17+ } from "@roo-code/types"
718
819import { mentionRegex , mentionRegexGlobal , commandRegexGlobal , unescapeSpaces } from "@roo/context-mentions"
920import { WebviewMessage } from "@roo/WebviewMessage"
@@ -22,9 +33,25 @@ import {
2233} from "@src/utils/context-mentions"
2334import { cn } from "@src/lib/utils"
2435import { convertToMentionPath } from "@src/utils/path-mentions"
25- import { StandardTooltip } from "@src/components/ui"
36+ import {
37+ Command ,
38+ CommandEmpty ,
39+ CommandGroup ,
40+ CommandInput ,
41+ CommandItem ,
42+ CommandList ,
43+ Popover ,
44+ PopoverContent ,
45+ PopoverTrigger ,
46+ StandardTooltip ,
47+ } from "@src/components/ui"
48+ import { useRouterModels } from "@src/components/ui/hooks/useRouterModels"
49+ import { useLmStudioModels } from "@src/components/ui/hooks/useLmStudioModels"
50+ import { useOllamaModels } from "@src/components/ui/hooks/useOllamaModels"
2651
2752import Thumbnails from "../common/Thumbnails"
53+ import { MODELS_BY_PROVIDER as STATIC_MODELS_BY_PROVIDER } from "../settings/constants"
54+ import { filterModels } from "../settings/utils/organizationFilters"
2855import { ModeSelector } from "./ModeSelector"
2956import { ApiConfigSelector } from "./ApiConfigSelector"
3057import { AutoApproveDropdown } from "./AutoApproveDropdown"
@@ -34,6 +61,243 @@ import { IndexingStatusBadge } from "./IndexingStatusBadge"
3461import { usePromptHistory } from "./hooks/usePromptHistory"
3562import { CloudAccountSwitcher } from "../cloud/CloudAccountSwitcher"
3663
64+ const QUICK_MODEL_ID_KEYS : Partial < Record < ProviderName , ModelIdKey > > = {
65+ openai : "openAiModelId" ,
66+ openrouter : "openRouterModelId" ,
67+ requesty : "requestyModelId" ,
68+ unbound : "unboundModelId" ,
69+ litellm : "litellmModelId" ,
70+ "vercel-ai-gateway" : "vercelAiGatewayModelId" ,
71+ ollama : "ollamaModelId" ,
72+ lmstudio : "lmStudioModelId" ,
73+ "openai-native" : "apiModelId" ,
74+ }
75+
76+ interface CurrentModelSelectorProps {
77+ apiConfiguration ?: ProviderSettings
78+ currentApiConfigName ?: string
79+ currentModelId ?: string
80+ currentModelDisplayName ?: string
81+ disabled ?: boolean
82+ organizationAllowList ?: OrganizationAllowList
83+ setApiConfiguration : ( config : ProviderSettings ) => void
84+ }
85+
86+ const CurrentModelSelector = ( {
87+ apiConfiguration,
88+ currentApiConfigName,
89+ currentModelId,
90+ currentModelDisplayName,
91+ disabled,
92+ organizationAllowList,
93+ setApiConfiguration,
94+ } : CurrentModelSelectorProps ) => {
95+ const { t } = useAppTranslation ( )
96+ const [ open , setOpen ] = useState ( false )
97+ const [ searchValue , setSearchValue ] = useState ( "" )
98+ const [ openAiModels , setOpenAiModels ] = useState < ModelRecord | null > ( null )
99+
100+ const provider = apiConfiguration ?. apiProvider
101+ const activeProvider : ProviderName | undefined =
102+ provider && ! isRetiredProvider ( provider ) ? ( provider as ProviderName ) : undefined
103+ const dynamicProvider = activeProvider && isDynamicProvider ( activeProvider ) ? activeProvider : undefined
104+
105+ const routerModels = useRouterModels ( { provider : dynamicProvider , enabled : ! ! dynamicProvider } )
106+ const lmStudioModels = useLmStudioModels (
107+ activeProvider === "lmstudio" ? apiConfiguration ?. lmStudioModelId : undefined ,
108+ )
109+ const ollamaModels = useOllamaModels ( activeProvider === "ollama" ? apiConfiguration ?. ollamaModelId : undefined )
110+
111+ const onMessage = useCallback ( ( event : MessageEvent ) => {
112+ const message : ExtensionMessage = event . data
113+
114+ if ( message . type === "openAiModels" ) {
115+ setOpenAiModels (
116+ Object . fromEntries (
117+ ( message . openAiModels ?? [ ] ) . map ( ( modelId ) => [ modelId , openAiModelInfoSaneDefaults ] ) ,
118+ ) ,
119+ )
120+ }
121+ } , [ ] )
122+
123+ useEvent ( "message" , onMessage )
124+
125+ useEffect ( ( ) => {
126+ if ( open && activeProvider === "openai" ) {
127+ vscode . postMessage ( {
128+ type : "requestOpenAiModels" ,
129+ values : {
130+ baseUrl : apiConfiguration ?. openAiBaseUrl ,
131+ apiKey : apiConfiguration ?. openAiApiKey ,
132+ customHeaders : apiConfiguration ?. openAiHeaders ?? { } ,
133+ } ,
134+ } )
135+ }
136+ } , [
137+ activeProvider ,
138+ apiConfiguration ?. openAiApiKey ,
139+ apiConfiguration ?. openAiBaseUrl ,
140+ apiConfiguration ?. openAiHeaders ,
141+ open ,
142+ ] )
143+
144+ const models = useMemo < ModelRecord | null > ( ( ) => {
145+ if ( ! activeProvider ) {
146+ return null
147+ }
148+
149+ if ( activeProvider === "openai" ) {
150+ return openAiModels
151+ }
152+
153+ if ( activeProvider === "lmstudio" ) {
154+ return lmStudioModels . data ?? null
155+ }
156+
157+ if ( activeProvider === "ollama" ) {
158+ return ollamaModels . data ?? null
159+ }
160+
161+ if ( dynamicProvider ) {
162+ return routerModels . data ?. [ dynamicProvider ] ?? null
163+ }
164+
165+ return STATIC_MODELS_BY_PROVIDER [ activeProvider ] ?? null
166+ } , [ activeProvider , dynamicProvider , lmStudioModels . data , ollamaModels . data , openAiModels , routerModels . data ] )
167+
168+ const modelIds = useMemo ( ( ) => {
169+ const filteredModels = filterModels ( models , activeProvider , organizationAllowList )
170+
171+ return Object . entries ( filteredModels ?? { } )
172+ . filter ( ( [ modelId , modelInfo ] ) => modelId === currentModelId || ! modelInfo . deprecated )
173+ . map ( ( [ modelId ] ) => modelId )
174+ . sort ( ( a , b ) => a . localeCompare ( b ) )
175+ } , [ activeProvider , currentModelId , models , organizationAllowList ] )
176+
177+ const modelIdKey = useMemo < ModelIdKey | undefined > ( ( ) => {
178+ if ( ! activeProvider ) {
179+ return undefined
180+ }
181+
182+ return (
183+ QUICK_MODEL_ID_KEYS [ activeProvider ] ??
184+ modelIdKeysByProvider [ activeProvider as keyof typeof modelIdKeysByProvider ]
185+ )
186+ } , [ activeProvider ] )
187+
188+ const handleModelSelect = useCallback (
189+ ( modelId : string ) => {
190+ if ( ! apiConfiguration || ! currentApiConfigName || ! modelIdKey ) {
191+ return
192+ }
193+
194+ const updatedConfiguration = {
195+ ...apiConfiguration ,
196+ [ modelIdKey ] : modelId ,
197+ } as ProviderSettings
198+
199+ setOpen ( false )
200+ setSearchValue ( "" )
201+ setApiConfiguration ( updatedConfiguration )
202+ vscode . postMessage ( {
203+ type : "upsertApiConfiguration" ,
204+ text : currentApiConfigName ,
205+ apiConfiguration : updatedConfiguration ,
206+ } )
207+ } ,
208+ [ apiConfiguration , currentApiConfigName , modelIdKey , setApiConfiguration ] ,
209+ )
210+
211+ if ( ! currentModelDisplayName || ! currentModelId ) {
212+ return null
213+ }
214+
215+ const isLoading =
216+ ( activeProvider === "openai" && openAiModels === null ) ||
217+ ( ! ! dynamicProvider && routerModels . isLoading ) ||
218+ ( activeProvider === "lmstudio" && lmStudioModels . isLoading ) ||
219+ ( activeProvider === "ollama" && ollamaModels . isLoading )
220+
221+ const canSelectModel = ! disabled && ! ! modelIdKey && ( modelIds . length > 0 || activeProvider === "openai" )
222+
223+ return (
224+ < Popover open = { open } onOpenChange = { setOpen } >
225+ < PopoverTrigger asChild >
226+ < button
227+ type = "button"
228+ disabled = { ! canSelectModel }
229+ aria-label = { t ( "settings:modelPicker.label" ) }
230+ className = { cn (
231+ "flex h-5 max-w-[180px] min-w-0 flex-shrink items-center gap-1 rounded-sm border border-vscode-input-border/60 px-1.5 text-vscode-descriptionForeground" ,
232+ "bg-transparent text-xs leading-none" ,
233+ canSelectModel &&
234+ "cursor-pointer hover:bg-[rgba(255,255,255,0.03)] hover:text-vscode-foreground focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder" ,
235+ ! canSelectModel && "cursor-default opacity-80" ,
236+ ) }
237+ title = { currentModelId }
238+ data-testid = "current-model-indicator" >
239+ < span className = "truncate" > { currentModelDisplayName } </ span >
240+ { canSelectModel ? < ChevronsUpDown className = "size-3 flex-shrink-0 opacity-70" /> : null }
241+ </ button >
242+ </ PopoverTrigger >
243+ < PopoverContent align = "start" sideOffset = { 4 } className = "w-[280px] p-0" >
244+ < Command >
245+ < CommandInput
246+ value = { searchValue }
247+ onValueChange = { setSearchValue }
248+ placeholder = { t ( "settings:modelPicker.searchPlaceholder" ) }
249+ className = "h-8"
250+ />
251+ < CommandList className = "max-h-[260px]" >
252+ < CommandEmpty >
253+ < div className = "py-2 px-1 text-sm" >
254+ { isLoading ? "Loading..." : t ( "settings:modelPicker.noMatchFound" ) }
255+ </ div >
256+ </ CommandEmpty >
257+ < CommandGroup >
258+ { modelIds . map ( ( modelId ) => (
259+ < CommandItem
260+ key = { modelId }
261+ value = { modelId }
262+ onSelect = { handleModelSelect }
263+ data-testid = { `quick-model-option-${ modelId } ` } >
264+ < span className = "truncate" title = { modelId } >
265+ { formatModelDisplayName ( modelId ) }
266+ </ span >
267+ < Check
268+ className = { cn (
269+ "size-4 p-0.5 ml-auto flex-shrink-0" ,
270+ modelId === currentModelId ? "opacity-100" : "opacity-0" ,
271+ ) }
272+ />
273+ </ CommandItem >
274+ ) ) }
275+ </ CommandGroup >
276+ </ CommandList >
277+ </ Command >
278+ </ PopoverContent >
279+ </ Popover >
280+ )
281+ }
282+
283+ const formatModelDisplayName = ( modelId : string ) => {
284+ return modelId
285+ . split ( / [ - _ \s ] + / )
286+ . filter ( Boolean )
287+ . map ( ( part ) => {
288+ if ( part . toLowerCase ( ) === "deepseek" ) {
289+ return "DeepSeek"
290+ }
291+
292+ if ( / ^ v \d + $ / i. test ( part ) ) {
293+ return part . toUpperCase ( )
294+ }
295+
296+ return part . charAt ( 0 ) . toUpperCase ( ) + part . slice ( 1 )
297+ } )
298+ . join ( " " )
299+ }
300+
37301interface ChatTextAreaProps {
38302 inputValue : string
39303 setInputValue : ( value : string ) => void
@@ -99,7 +363,10 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
99363 cloudUserInfo,
100364 enterBehavior,
101365 lockApiConfigAcrossModes,
366+ apiConfiguration,
102367 currentModelId,
368+ organizationAllowList,
369+ setApiConfiguration,
103370 } = useExtensionState ( )
104371
105372 // Find the ID and display text for the currently selected API configuration.
@@ -116,21 +383,7 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
116383 return undefined
117384 }
118385
119- return currentModelId
120- . split ( / [ - _ \s ] + / )
121- . filter ( Boolean )
122- . map ( ( part ) => {
123- if ( part . toLowerCase ( ) === "deepseek" ) {
124- return "DeepSeek"
125- }
126-
127- if ( / ^ v \d + $ / i. test ( part ) ) {
128- return part . toUpperCase ( )
129- }
130-
131- return part . charAt ( 0 ) . toUpperCase ( ) + part . slice ( 1 )
132- } )
133- . join ( " " )
386+ return formatModelDisplayName ( currentModelId )
134387 } , [ currentModelId ] )
135388
136389 const [ gitCommits , setGitCommits ] = useState < any [ ] > ( [ ] )
@@ -1343,14 +1596,15 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
13431596 lockApiConfigAcrossModes = { ! ! lockApiConfigAcrossModes }
13441597 onToggleLockApiConfig = { handleToggleLockApiConfig }
13451598 />
1346- { currentModelDisplayName ? (
1347- < div
1348- className = "flex h-5 max-w-[160px] min-w-0 flex-shrink items-center rounded-sm border border-vscode-input-border/60 px-1.5 text-vscode-descriptionForeground"
1349- title = { currentModelId }
1350- data-testid = "current-model-indicator" >
1351- < span className = "truncate text-xs leading-none" > { currentModelDisplayName } </ span >
1352- </ div >
1353- ) : null }
1599+ < CurrentModelSelector
1600+ apiConfiguration = { apiConfiguration }
1601+ currentApiConfigName = { currentApiConfigName }
1602+ currentModelId = { currentModelId }
1603+ currentModelDisplayName = { currentModelDisplayName }
1604+ disabled = { selectApiConfigDisabled }
1605+ organizationAllowList = { organizationAllowList }
1606+ setApiConfiguration = { setApiConfiguration }
1607+ />
13541608 < AutoApproveDropdown triggerClassName = "min-w-[28px] text-ellipsis overflow-hidden flex-shrink" />
13551609 </ div >
13561610 < div
0 commit comments