diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index eb404f7129c..b55dc8a8c3a 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -81,6 +81,7 @@ export const providerSettingsEntrySchema = z.object({ id: z.string(), name: z.string(), apiProvider: providerNamesSchema.optional(), + modelId: z.string().optional(), }) export type ProviderSettingsEntry = z.infer diff --git a/src/core/config/ProviderSettingsManager.ts b/src/core/config/ProviderSettingsManager.ts index 1d2e96b9c0c..21a7a060c1a 100644 --- a/src/core/config/ProviderSettingsManager.ts +++ b/src/core/config/ProviderSettingsManager.ts @@ -9,6 +9,7 @@ import { isSecretStateKey, ProviderSettingsEntry, DEFAULT_CONSECUTIVE_MISTAKE_LIMIT, + getModelId, } from "@roo-code/types" import { TelemetryService } from "@roo-code/telemetry" @@ -274,6 +275,20 @@ export class ProviderSettingsManager { } } + /** + * Clean model ID by removing prefix before "/" + */ + private cleanModelId(modelId: string | undefined): string | undefined { + if (!modelId) return undefined + + // Check for "/" and take the part after it + if (modelId.includes("/")) { + return modelId.split("/").pop() + } + + return modelId + } + /** * List all available configs with metadata. */ @@ -286,6 +301,7 @@ export class ProviderSettingsManager { name, id: apiConfig.id || "", apiProvider: apiConfig.apiProvider, + modelId: this.cleanModelId(getModelId(apiConfig)), })) }) } catch (error) { diff --git a/webview-ui/src/components/chat/ApiConfigSelector.tsx b/webview-ui/src/components/chat/ApiConfigSelector.tsx index bf5f9612943..cf9cd5ebff9 100644 --- a/webview-ui/src/components/chat/ApiConfigSelector.tsx +++ b/webview-ui/src/components/chat/ApiConfigSelector.tsx @@ -18,7 +18,7 @@ interface ApiConfigSelectorProps { title: string onChange: (value: string) => void triggerClassName?: string - listApiConfigMeta: Array<{ id: string; name: string }> + listApiConfigMeta: Array<{ id: string; name: string; modelId?: string }> pinnedApiConfigs?: Record togglePinnedApiConfig: (id: string) => void } @@ -87,7 +87,7 @@ export const ApiConfigSelector = ({ }, []) const renderConfigItem = useCallback( - (config: { id: string; name: string }, isPinned: boolean) => { + (config: { id: string; name: string; modelId?: string }, isPinned: boolean) => { const isCurrentConfig = config.id === value return ( @@ -100,7 +100,19 @@ export const ApiConfigSelector = ({ isCurrentConfig && "bg-vscode-list-activeSelectionBackground text-vscode-list-activeSelectionForeground", )}> - {config.name} +
+ {config.name} + {config.modelId && ( + <> + ยท + + {config.modelId} + + + )} +
{isCurrentConfig && (
diff --git a/webview-ui/src/components/chat/__tests__/ApiConfigSelector.spec.tsx b/webview-ui/src/components/chat/__tests__/ApiConfigSelector.spec.tsx index 75b68fa77a8..d5e9cff8481 100644 --- a/webview-ui/src/components/chat/__tests__/ApiConfigSelector.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/ApiConfigSelector.spec.tsx @@ -21,6 +21,21 @@ vi.mock("@/components/ui/hooks/useRooPortal", () => ({ useRooPortal: () => document.body, })) +// Mock the ExtensionStateContext +vi.mock("@/context/ExtensionStateContext", () => ({ + useExtensionState: () => ({ + apiConfiguration: { + apiProvider: "anthropic", + apiModelId: "claude-3-opus-20240229", + }, + }), +})) + +// Mock the getModelId function from @roo-code/types +vi.mock("@roo-code/types", () => ({ + getModelId: (config: any) => config?.apiModelId || undefined, +})) + // Mock Popover components to be testable vi.mock("@/components/ui", () => ({ Popover: ({ children, open }: any) => ( @@ -52,9 +67,9 @@ describe("ApiConfigSelector", () => { title: "API Config", onChange: mockOnChange, listApiConfigMeta: [ - { id: "config1", name: "Config 1" }, - { id: "config2", name: "Config 2" }, - { id: "config3", name: "Config 3" }, + { id: "config1", name: "Config 1", modelId: "claude-3-opus-20240229" }, + { id: "config2", name: "Config 2", modelId: "gpt-4" }, + { id: "config3", name: "Config 3", modelId: "claude-3-sonnet-20240229" }, ], pinnedApiConfigs: { config1: true }, togglePinnedApiConfig: mockTogglePinnedApiConfig, @@ -120,13 +135,13 @@ describe("ApiConfigSelector", () => { const props = { ...defaultProps, listApiConfigMeta: [ - { id: "config1", name: "Config 1" }, - { id: "config2", name: "Config 2" }, - { id: "config3", name: "Config 3" }, - { id: "config4", name: "Config 4" }, - { id: "config5", name: "Config 5" }, - { id: "config6", name: "Config 6" }, - { id: "config7", name: "Config 7" }, + { id: "config1", name: "Config 1", modelId: "claude-3-opus-20240229" }, + { id: "config2", name: "Config 2", modelId: "gpt-4" }, + { id: "config3", name: "Config 3", modelId: "claude-3-sonnet-20240229" }, + { id: "config4", name: "Config 4", modelId: "gpt-3.5-turbo" }, + { id: "config5", name: "Config 5", modelId: "claude-3-haiku-20240307" }, + { id: "config6", name: "Config 6", modelId: "gpt-4-turbo" }, + { id: "config7", name: "Config 7", modelId: "claude-2.1" }, ], } render() @@ -154,13 +169,13 @@ describe("ApiConfigSelector", () => { const props = { ...defaultProps, listApiConfigMeta: [ - { id: "config1", name: "Config 1" }, - { id: "config2", name: "Config 2" }, - { id: "config3", name: "Config 3" }, - { id: "config4", name: "Config 4" }, - { id: "config5", name: "Config 5" }, - { id: "config6", name: "Config 6" }, - { id: "config7", name: "Config 7" }, + { id: "config1", name: "Config 1", modelId: "claude-3-opus-20240229" }, + { id: "config2", name: "Config 2", modelId: "gpt-4" }, + { id: "config3", name: "Config 3", modelId: "claude-3-sonnet-20240229" }, + { id: "config4", name: "Config 4", modelId: "gpt-3.5-turbo" }, + { id: "config5", name: "Config 5", modelId: "claude-3-haiku-20240307" }, + { id: "config6", name: "Config 6", modelId: "gpt-4-turbo" }, + { id: "config7", name: "Config 7", modelId: "claude-2.1" }, ], } render() @@ -184,13 +199,13 @@ describe("ApiConfigSelector", () => { const props = { ...defaultProps, listApiConfigMeta: [ - { id: "config1", name: "Config 1" }, - { id: "config2", name: "Config 2" }, - { id: "config3", name: "Config 3" }, - { id: "config4", name: "Config 4" }, - { id: "config5", name: "Config 5" }, - { id: "config6", name: "Config 6" }, - { id: "config7", name: "Config 7" }, + { id: "config1", name: "Config 1", modelId: "claude-3-opus-20240229" }, + { id: "config2", name: "Config 2", modelId: "gpt-4" }, + { id: "config3", name: "Config 3", modelId: "claude-3-sonnet-20240229" }, + { id: "config4", name: "Config 4", modelId: "gpt-3.5-turbo" }, + { id: "config5", name: "Config 5", modelId: "claude-3-haiku-20240307" }, + { id: "config6", name: "Config 6", modelId: "gpt-4-turbo" }, + { id: "config7", name: "Config 7", modelId: "claude-2.1" }, ], } render() @@ -210,13 +225,13 @@ describe("ApiConfigSelector", () => { const props = { ...defaultProps, listApiConfigMeta: [ - { id: "config1", name: "Config 1" }, - { id: "config2", name: "Config 2" }, - { id: "config3", name: "Config 3" }, - { id: "config4", name: "Config 4" }, - { id: "config5", name: "Config 5" }, - { id: "config6", name: "Config 6" }, - { id: "config7", name: "Config 7" }, + { id: "config1", name: "Config 1", modelId: "claude-3-opus-20240229" }, + { id: "config2", name: "Config 2", modelId: "gpt-4" }, + { id: "config3", name: "Config 3", modelId: "claude-3-sonnet-20240229" }, + { id: "config4", name: "Config 4", modelId: "gpt-3.5-turbo" }, + { id: "config5", name: "Config 5", modelId: "claude-3-haiku-20240307" }, + { id: "config6", name: "Config 6", modelId: "gpt-4-turbo" }, + { id: "config7", name: "Config 7", modelId: "claude-2.1" }, ], } render() @@ -263,7 +278,8 @@ describe("ApiConfigSelector", () => { const config1Elements = screen.getAllByText("Config 1") // Find the one that's in the dropdown content (not the trigger) const configInDropdown = config1Elements.find((el) => el.closest('[data-testid="popover-content"]')) - const selectedConfigRow = configInDropdown?.closest("div") + // Navigate up to find the parent row that contains both the text and the check icon + const selectedConfigRow = configInDropdown?.closest(".group") const checkIcon = selectedConfigRow?.querySelector(".codicon-check") expect(checkIcon).toBeInTheDocument() }) @@ -280,13 +296,24 @@ describe("ApiConfigSelector", () => { fireEvent.click(trigger) const content = screen.getByTestId("popover-content") - const configTexts = content.querySelectorAll(".truncate") + // Get all config items by looking for the group class + const configRows = content.querySelectorAll(".group") + + // Extract the config names from each row + const configNames: string[] = [] + configRows.forEach((row) => { + // Find the first span that's flex-shrink-0 (the profile name) + const nameElement = row.querySelector(".flex-1 span.flex-shrink-0") + if (nameElement?.textContent) { + configNames.push(nameElement.textContent) + } + }) // Pinned configs should appear first - expect(configTexts[0]).toHaveTextContent("Config 1") - expect(configTexts[1]).toHaveTextContent("Config 3") + expect(configNames[0]).toBe("Config 1") + expect(configNames[1]).toBe("Config 3") // Unpinned config should appear after separator - expect(configTexts[2]).toHaveTextContent("Config 2") + expect(configNames[2]).toBe("Config 2") }) test("toggles pin status when pin button is clicked", () => { @@ -296,8 +323,10 @@ describe("ApiConfigSelector", () => { fireEvent.click(trigger) // Find the pin button for Config 2 (unpinned) - const config2Row = screen.getByText("Config 2").closest("div") - const pinButton = config2Row?.querySelector("button") + const config2Row = screen.getByText("Config 2").closest(".group") + // Find the button with the pin icon (it's the second button, first is the row itself) + const buttons = config2Row?.querySelectorAll("button") + const pinButton = Array.from(buttons || []).find((btn) => btn.querySelector(".codicon-pin")) if (pinButton) { fireEvent.click(pinButton) @@ -332,13 +361,13 @@ describe("ApiConfigSelector", () => { const props = { ...defaultProps, listApiConfigMeta: [ - { id: "config1", name: "Config 1" }, - { id: "config2", name: "Config 2" }, - { id: "config3", name: "Config 3" }, - { id: "config4", name: "Config 4" }, - { id: "config5", name: "Config 5" }, - { id: "config6", name: "Config 6" }, - { id: "config7", name: "Config 7" }, + { id: "config1", name: "Config 1", modelId: "claude-3-opus-20240229" }, + { id: "config2", name: "Config 2", modelId: "gpt-4" }, + { id: "config3", name: "Config 3", modelId: "claude-3-sonnet-20240229" }, + { id: "config4", name: "Config 4", modelId: "gpt-3.5-turbo" }, + { id: "config5", name: "Config 5", modelId: "claude-3-haiku-20240307" }, + { id: "config6", name: "Config 6", modelId: "gpt-4-turbo" }, + { id: "config7", name: "Config 7", modelId: "claude-2.1" }, ], } render() @@ -389,13 +418,13 @@ describe("ApiConfigSelector", () => { const props = { ...defaultProps, listApiConfigMeta: [ - { id: "config1", name: "Config 1" }, - { id: "config2", name: "Config 2" }, - { id: "config3", name: "Config 3" }, - { id: "config4", name: "Config 4" }, - { id: "config5", name: "Config 5" }, - { id: "config6", name: "Config 6" }, - { id: "config7", name: "Config 7" }, + { id: "config1", name: "Config 1", modelId: "claude-3-opus-20240229" }, + { id: "config2", name: "Config 2", modelId: "gpt-4" }, + { id: "config3", name: "Config 3", modelId: "claude-3-sonnet-20240229" }, + { id: "config4", name: "Config 4", modelId: "gpt-3.5-turbo" }, + { id: "config5", name: "Config 5", modelId: "claude-3-haiku-20240307" }, + { id: "config6", name: "Config 6", modelId: "gpt-4-turbo" }, + { id: "config7", name: "Config 7", modelId: "claude-2.1" }, ], } render()