Skip to content

Commit faf5734

Browse files
authored
feat: show model ID in API configuration dropdown (#7423)
1 parent 46b6fdd commit faf5734

File tree

4 files changed

+113
-55
lines changed

4 files changed

+113
-55
lines changed

packages/types/src/provider-settings.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ export const providerSettingsEntrySchema = z.object({
8181
id: z.string(),
8282
name: z.string(),
8383
apiProvider: providerNamesSchema.optional(),
84+
modelId: z.string().optional(),
8485
})
8586

8687
export type ProviderSettingsEntry = z.infer<typeof providerSettingsEntrySchema>

src/core/config/ProviderSettingsManager.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
isSecretStateKey,
1010
ProviderSettingsEntry,
1111
DEFAULT_CONSECUTIVE_MISTAKE_LIMIT,
12+
getModelId,
1213
} from "@roo-code/types"
1314
import { TelemetryService } from "@roo-code/telemetry"
1415

@@ -274,6 +275,20 @@ export class ProviderSettingsManager {
274275
}
275276
}
276277

278+
/**
279+
* Clean model ID by removing prefix before "/"
280+
*/
281+
private cleanModelId(modelId: string | undefined): string | undefined {
282+
if (!modelId) return undefined
283+
284+
// Check for "/" and take the part after it
285+
if (modelId.includes("/")) {
286+
return modelId.split("/").pop()
287+
}
288+
289+
return modelId
290+
}
291+
277292
/**
278293
* List all available configs with metadata.
279294
*/
@@ -286,6 +301,7 @@ export class ProviderSettingsManager {
286301
name,
287302
id: apiConfig.id || "",
288303
apiProvider: apiConfig.apiProvider,
304+
modelId: this.cleanModelId(getModelId(apiConfig)),
289305
}))
290306
})
291307
} catch (error) {

webview-ui/src/components/chat/ApiConfigSelector.tsx

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ interface ApiConfigSelectorProps {
1818
title: string
1919
onChange: (value: string) => void
2020
triggerClassName?: string
21-
listApiConfigMeta: Array<{ id: string; name: string }>
21+
listApiConfigMeta: Array<{ id: string; name: string; modelId?: string }>
2222
pinnedApiConfigs?: Record<string, boolean>
2323
togglePinnedApiConfig: (id: string) => void
2424
}
@@ -87,7 +87,7 @@ export const ApiConfigSelector = ({
8787
}, [])
8888

8989
const renderConfigItem = useCallback(
90-
(config: { id: string; name: string }, isPinned: boolean) => {
90+
(config: { id: string; name: string; modelId?: string }, isPinned: boolean) => {
9191
const isCurrentConfig = config.id === value
9292

9393
return (
@@ -100,7 +100,19 @@ export const ApiConfigSelector = ({
100100
isCurrentConfig &&
101101
"bg-vscode-list-activeSelectionBackground text-vscode-list-activeSelectionForeground",
102102
)}>
103-
<span className="flex-1 truncate">{config.name}</span>
103+
<div className="flex-1 min-w-0 flex items-center gap-1 overflow-hidden">
104+
<span className="flex-shrink-0">{config.name}</span>
105+
{config.modelId && (
106+
<>
107+
<span className="text-vscode-descriptionForeground opacity-70 flex-shrink-0">·</span>
108+
<span
109+
className="text-vscode-descriptionForeground opacity-70 min-w-0 overflow-hidden"
110+
style={{ direction: "rtl", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
111+
{config.modelId}
112+
</span>
113+
</>
114+
)}
115+
</div>
104116
<div className="flex items-center gap-1">
105117
{isCurrentConfig && (
106118
<div className="size-5 p-1 flex items-center justify-center">

webview-ui/src/components/chat/__tests__/ApiConfigSelector.spec.tsx

Lines changed: 81 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,21 @@ vi.mock("@/components/ui/hooks/useRooPortal", () => ({
2121
useRooPortal: () => document.body,
2222
}))
2323

24+
// Mock the ExtensionStateContext
25+
vi.mock("@/context/ExtensionStateContext", () => ({
26+
useExtensionState: () => ({
27+
apiConfiguration: {
28+
apiProvider: "anthropic",
29+
apiModelId: "claude-3-opus-20240229",
30+
},
31+
}),
32+
}))
33+
34+
// Mock the getModelId function from @roo-code/types
35+
vi.mock("@roo-code/types", () => ({
36+
getModelId: (config: any) => config?.apiModelId || undefined,
37+
}))
38+
2439
// Mock Popover components to be testable
2540
vi.mock("@/components/ui", () => ({
2641
Popover: ({ children, open }: any) => (
@@ -52,9 +67,9 @@ describe("ApiConfigSelector", () => {
5267
title: "API Config",
5368
onChange: mockOnChange,
5469
listApiConfigMeta: [
55-
{ id: "config1", name: "Config 1" },
56-
{ id: "config2", name: "Config 2" },
57-
{ id: "config3", name: "Config 3" },
70+
{ id: "config1", name: "Config 1", modelId: "claude-3-opus-20240229" },
71+
{ id: "config2", name: "Config 2", modelId: "gpt-4" },
72+
{ id: "config3", name: "Config 3", modelId: "claude-3-sonnet-20240229" },
5873
],
5974
pinnedApiConfigs: { config1: true },
6075
togglePinnedApiConfig: mockTogglePinnedApiConfig,
@@ -120,13 +135,13 @@ describe("ApiConfigSelector", () => {
120135
const props = {
121136
...defaultProps,
122137
listApiConfigMeta: [
123-
{ id: "config1", name: "Config 1" },
124-
{ id: "config2", name: "Config 2" },
125-
{ id: "config3", name: "Config 3" },
126-
{ id: "config4", name: "Config 4" },
127-
{ id: "config5", name: "Config 5" },
128-
{ id: "config6", name: "Config 6" },
129-
{ id: "config7", name: "Config 7" },
138+
{ id: "config1", name: "Config 1", modelId: "claude-3-opus-20240229" },
139+
{ id: "config2", name: "Config 2", modelId: "gpt-4" },
140+
{ id: "config3", name: "Config 3", modelId: "claude-3-sonnet-20240229" },
141+
{ id: "config4", name: "Config 4", modelId: "gpt-3.5-turbo" },
142+
{ id: "config5", name: "Config 5", modelId: "claude-3-haiku-20240307" },
143+
{ id: "config6", name: "Config 6", modelId: "gpt-4-turbo" },
144+
{ id: "config7", name: "Config 7", modelId: "claude-2.1" },
130145
],
131146
}
132147
render(<ApiConfigSelector {...props} />)
@@ -154,13 +169,13 @@ describe("ApiConfigSelector", () => {
154169
const props = {
155170
...defaultProps,
156171
listApiConfigMeta: [
157-
{ id: "config1", name: "Config 1" },
158-
{ id: "config2", name: "Config 2" },
159-
{ id: "config3", name: "Config 3" },
160-
{ id: "config4", name: "Config 4" },
161-
{ id: "config5", name: "Config 5" },
162-
{ id: "config6", name: "Config 6" },
163-
{ id: "config7", name: "Config 7" },
172+
{ id: "config1", name: "Config 1", modelId: "claude-3-opus-20240229" },
173+
{ id: "config2", name: "Config 2", modelId: "gpt-4" },
174+
{ id: "config3", name: "Config 3", modelId: "claude-3-sonnet-20240229" },
175+
{ id: "config4", name: "Config 4", modelId: "gpt-3.5-turbo" },
176+
{ id: "config5", name: "Config 5", modelId: "claude-3-haiku-20240307" },
177+
{ id: "config6", name: "Config 6", modelId: "gpt-4-turbo" },
178+
{ id: "config7", name: "Config 7", modelId: "claude-2.1" },
164179
],
165180
}
166181
render(<ApiConfigSelector {...props} />)
@@ -184,13 +199,13 @@ describe("ApiConfigSelector", () => {
184199
const props = {
185200
...defaultProps,
186201
listApiConfigMeta: [
187-
{ id: "config1", name: "Config 1" },
188-
{ id: "config2", name: "Config 2" },
189-
{ id: "config3", name: "Config 3" },
190-
{ id: "config4", name: "Config 4" },
191-
{ id: "config5", name: "Config 5" },
192-
{ id: "config6", name: "Config 6" },
193-
{ id: "config7", name: "Config 7" },
202+
{ id: "config1", name: "Config 1", modelId: "claude-3-opus-20240229" },
203+
{ id: "config2", name: "Config 2", modelId: "gpt-4" },
204+
{ id: "config3", name: "Config 3", modelId: "claude-3-sonnet-20240229" },
205+
{ id: "config4", name: "Config 4", modelId: "gpt-3.5-turbo" },
206+
{ id: "config5", name: "Config 5", modelId: "claude-3-haiku-20240307" },
207+
{ id: "config6", name: "Config 6", modelId: "gpt-4-turbo" },
208+
{ id: "config7", name: "Config 7", modelId: "claude-2.1" },
194209
],
195210
}
196211
render(<ApiConfigSelector {...props} />)
@@ -210,13 +225,13 @@ describe("ApiConfigSelector", () => {
210225
const props = {
211226
...defaultProps,
212227
listApiConfigMeta: [
213-
{ id: "config1", name: "Config 1" },
214-
{ id: "config2", name: "Config 2" },
215-
{ id: "config3", name: "Config 3" },
216-
{ id: "config4", name: "Config 4" },
217-
{ id: "config5", name: "Config 5" },
218-
{ id: "config6", name: "Config 6" },
219-
{ id: "config7", name: "Config 7" },
228+
{ id: "config1", name: "Config 1", modelId: "claude-3-opus-20240229" },
229+
{ id: "config2", name: "Config 2", modelId: "gpt-4" },
230+
{ id: "config3", name: "Config 3", modelId: "claude-3-sonnet-20240229" },
231+
{ id: "config4", name: "Config 4", modelId: "gpt-3.5-turbo" },
232+
{ id: "config5", name: "Config 5", modelId: "claude-3-haiku-20240307" },
233+
{ id: "config6", name: "Config 6", modelId: "gpt-4-turbo" },
234+
{ id: "config7", name: "Config 7", modelId: "claude-2.1" },
220235
],
221236
}
222237
render(<ApiConfigSelector {...props} />)
@@ -263,7 +278,8 @@ describe("ApiConfigSelector", () => {
263278
const config1Elements = screen.getAllByText("Config 1")
264279
// Find the one that's in the dropdown content (not the trigger)
265280
const configInDropdown = config1Elements.find((el) => el.closest('[data-testid="popover-content"]'))
266-
const selectedConfigRow = configInDropdown?.closest("div")
281+
// Navigate up to find the parent row that contains both the text and the check icon
282+
const selectedConfigRow = configInDropdown?.closest(".group")
267283
const checkIcon = selectedConfigRow?.querySelector(".codicon-check")
268284
expect(checkIcon).toBeInTheDocument()
269285
})
@@ -280,13 +296,24 @@ describe("ApiConfigSelector", () => {
280296
fireEvent.click(trigger)
281297

282298
const content = screen.getByTestId("popover-content")
283-
const configTexts = content.querySelectorAll(".truncate")
299+
// Get all config items by looking for the group class
300+
const configRows = content.querySelectorAll(".group")
301+
302+
// Extract the config names from each row
303+
const configNames: string[] = []
304+
configRows.forEach((row) => {
305+
// Find the first span that's flex-shrink-0 (the profile name)
306+
const nameElement = row.querySelector(".flex-1 span.flex-shrink-0")
307+
if (nameElement?.textContent) {
308+
configNames.push(nameElement.textContent)
309+
}
310+
})
284311

285312
// Pinned configs should appear first
286-
expect(configTexts[0]).toHaveTextContent("Config 1")
287-
expect(configTexts[1]).toHaveTextContent("Config 3")
313+
expect(configNames[0]).toBe("Config 1")
314+
expect(configNames[1]).toBe("Config 3")
288315
// Unpinned config should appear after separator
289-
expect(configTexts[2]).toHaveTextContent("Config 2")
316+
expect(configNames[2]).toBe("Config 2")
290317
})
291318

292319
test("toggles pin status when pin button is clicked", () => {
@@ -296,8 +323,10 @@ describe("ApiConfigSelector", () => {
296323
fireEvent.click(trigger)
297324

298325
// Find the pin button for Config 2 (unpinned)
299-
const config2Row = screen.getByText("Config 2").closest("div")
300-
const pinButton = config2Row?.querySelector("button")
326+
const config2Row = screen.getByText("Config 2").closest(".group")
327+
// Find the button with the pin icon (it's the second button, first is the row itself)
328+
const buttons = config2Row?.querySelectorAll("button")
329+
const pinButton = Array.from(buttons || []).find((btn) => btn.querySelector(".codicon-pin"))
301330

302331
if (pinButton) {
303332
fireEvent.click(pinButton)
@@ -332,13 +361,13 @@ describe("ApiConfigSelector", () => {
332361
const props = {
333362
...defaultProps,
334363
listApiConfigMeta: [
335-
{ id: "config1", name: "Config 1" },
336-
{ id: "config2", name: "Config 2" },
337-
{ id: "config3", name: "Config 3" },
338-
{ id: "config4", name: "Config 4" },
339-
{ id: "config5", name: "Config 5" },
340-
{ id: "config6", name: "Config 6" },
341-
{ id: "config7", name: "Config 7" },
364+
{ id: "config1", name: "Config 1", modelId: "claude-3-opus-20240229" },
365+
{ id: "config2", name: "Config 2", modelId: "gpt-4" },
366+
{ id: "config3", name: "Config 3", modelId: "claude-3-sonnet-20240229" },
367+
{ id: "config4", name: "Config 4", modelId: "gpt-3.5-turbo" },
368+
{ id: "config5", name: "Config 5", modelId: "claude-3-haiku-20240307" },
369+
{ id: "config6", name: "Config 6", modelId: "gpt-4-turbo" },
370+
{ id: "config7", name: "Config 7", modelId: "claude-2.1" },
342371
],
343372
}
344373
render(<ApiConfigSelector {...props} />)
@@ -389,13 +418,13 @@ describe("ApiConfigSelector", () => {
389418
const props = {
390419
...defaultProps,
391420
listApiConfigMeta: [
392-
{ id: "config1", name: "Config 1" },
393-
{ id: "config2", name: "Config 2" },
394-
{ id: "config3", name: "Config 3" },
395-
{ id: "config4", name: "Config 4" },
396-
{ id: "config5", name: "Config 5" },
397-
{ id: "config6", name: "Config 6" },
398-
{ id: "config7", name: "Config 7" },
421+
{ id: "config1", name: "Config 1", modelId: "claude-3-opus-20240229" },
422+
{ id: "config2", name: "Config 2", modelId: "gpt-4" },
423+
{ id: "config3", name: "Config 3", modelId: "claude-3-sonnet-20240229" },
424+
{ id: "config4", name: "Config 4", modelId: "gpt-3.5-turbo" },
425+
{ id: "config5", name: "Config 5", modelId: "claude-3-haiku-20240307" },
426+
{ id: "config6", name: "Config 6", modelId: "gpt-4-turbo" },
427+
{ id: "config7", name: "Config 7", modelId: "claude-2.1" },
399428
],
400429
}
401430
render(<ApiConfigSelector {...props} />)

0 commit comments

Comments
 (0)