Skip to content
This repository was archived by the owner on May 15, 2026. It is now read-only.

Commit 98dd3ae

Browse files
committed
Add status bar model switcher
1 parent 31be701 commit 98dd3ae

4 files changed

Lines changed: 229 additions & 0 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"roo-cline": patch
3+
---
4+
5+
Add a native VS Code status bar model switcher for the active API provider.

src/activate/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ export { registerCommands } from "./registerCommands"
33
export { registerCodeActions } from "./registerCodeActions"
44
export { registerTerminalActions } from "./registerTerminalActions"
55
export { CodeActionProvider } from "./CodeActionProvider"
6+
export { initializeProviderModelStatusBar } from "./providerModelStatusBar"
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
import * as vscode from "vscode"
2+
3+
import {
4+
MODELS_BY_PROVIDER,
5+
RooCodeEventName,
6+
getModelId,
7+
isProviderName,
8+
modelIdKeysByProvider,
9+
type ModelIdKey,
10+
type ProviderName,
11+
type ProviderSettings,
12+
} from "@roo-code/types"
13+
14+
import { getModels } from "../api/providers/fetchers/modelCache"
15+
import { buildApiHandler } from "../api"
16+
import { toRouterName, type GetModelsOptions } from "../shared/api"
17+
import type { ClineProvider } from "../core/webview/ClineProvider"
18+
19+
const COMMAND_ID = "roo-cline.switchModelFromStatusBar"
20+
21+
type ModelQuickPickItem = vscode.QuickPickItem & {
22+
modelId: string
23+
}
24+
25+
const providerLabels: Partial<Record<ProviderName, string>> = {
26+
...Object.fromEntries(Object.entries(MODELS_BY_PROVIDER).map(([id, meta]) => [id, meta.label])),
27+
openai: "OpenAI Compatible",
28+
"gemini-cli": "Gemini CLI",
29+
"fake-ai": "Fake AI",
30+
}
31+
32+
export function initializeProviderModelStatusBar({
33+
context,
34+
provider,
35+
outputChannel,
36+
}: {
37+
context: vscode.ExtensionContext
38+
provider: ClineProvider
39+
outputChannel: vscode.OutputChannel
40+
}) {
41+
const statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 100)
42+
statusBarItem.command = COMMAND_ID
43+
statusBarItem.name = "Roo Code Model"
44+
context.subscriptions.push(statusBarItem)
45+
46+
const updateStatusBar = async () => {
47+
try {
48+
const { apiConfiguration } = await provider.getState()
49+
const providerName = getProviderName(apiConfiguration)
50+
const modelId = getCurrentModelId(apiConfiguration)
51+
52+
statusBarItem.text = `$(symbol-misc) ${getProviderLabel(providerName)} | ${modelId || "Select model"}`
53+
statusBarItem.tooltip = "Switch Roo Code model"
54+
statusBarItem.show()
55+
} catch (error) {
56+
outputChannel.appendLine(`Failed to update Roo Code model status bar: ${String(error)}`)
57+
}
58+
}
59+
60+
context.subscriptions.push(
61+
vscode.commands.registerCommand(COMMAND_ID, async () => {
62+
await showProviderModelQuickPick({ provider, outputChannel, updateStatusBar })
63+
}),
64+
)
65+
66+
const onProviderProfileChanged = () => {
67+
void updateStatusBar()
68+
}
69+
70+
provider.on(RooCodeEventName.ProviderProfileChanged, onProviderProfileChanged)
71+
context.subscriptions.push({
72+
dispose: () => provider.off(RooCodeEventName.ProviderProfileChanged, onProviderProfileChanged),
73+
})
74+
75+
void updateStatusBar()
76+
}
77+
78+
async function showProviderModelQuickPick({
79+
provider,
80+
outputChannel,
81+
updateStatusBar,
82+
}: {
83+
provider: ClineProvider
84+
outputChannel: vscode.OutputChannel
85+
updateStatusBar: () => Promise<void>
86+
}) {
87+
const { apiConfiguration, currentApiConfigName = "default" } = await provider.getState()
88+
const providerName = getProviderName(apiConfiguration)
89+
const models = await getAvailableModelIds(apiConfiguration, outputChannel)
90+
91+
if (models.length === 0) {
92+
void vscode.window.showInformationMessage(`No models found for ${getProviderLabel(providerName)}.`)
93+
return
94+
}
95+
96+
const currentModelId = getCurrentModelId(apiConfiguration)
97+
const selected = await vscode.window.showQuickPick<ModelQuickPickItem>(
98+
models.map((modelId) => ({
99+
label: modelId,
100+
description: modelId === currentModelId ? "Current" : undefined,
101+
modelId,
102+
})),
103+
{
104+
placeHolder: `Select ${getProviderLabel(providerName)} model`,
105+
matchOnDescription: true,
106+
},
107+
)
108+
109+
if (!selected || selected.modelId === currentModelId) {
110+
return
111+
}
112+
113+
const modelIdKey = getModelIdKey(providerName)
114+
if (!modelIdKey) {
115+
void vscode.window.showWarningMessage(`Model switching is not supported for ${getProviderLabel(providerName)}.`)
116+
return
117+
}
118+
119+
await provider.upsertProviderProfile(currentApiConfigName, {
120+
...apiConfiguration,
121+
apiProvider: providerName,
122+
[modelIdKey]: selected.modelId,
123+
})
124+
125+
await updateStatusBar()
126+
}
127+
128+
async function getAvailableModelIds(
129+
apiConfiguration: ProviderSettings,
130+
outputChannel: vscode.OutputChannel,
131+
): Promise<string[]> {
132+
const providerName = getProviderName(apiConfiguration)
133+
const staticModels = MODELS_BY_PROVIDER[providerName as keyof typeof MODELS_BY_PROVIDER]?.models
134+
135+
if (staticModels?.length) {
136+
return staticModels
137+
}
138+
139+
if (providerName === "openai") {
140+
return getCurrentModelId(apiConfiguration) ? [getCurrentModelId(apiConfiguration)] : []
141+
}
142+
143+
try {
144+
const models = await getModels({
145+
provider: toRouterName(providerName),
146+
apiKey: getProviderApiKey(apiConfiguration),
147+
baseUrl: getProviderBaseUrl(apiConfiguration),
148+
} as GetModelsOptions)
149+
150+
return Object.keys(models)
151+
} catch (error) {
152+
outputChannel.appendLine(
153+
`Failed to load ${providerName} models for status bar picker: ${error instanceof Error ? error.message : String(error)}`,
154+
)
155+
void vscode.window.showErrorMessage(`Failed to load ${getProviderLabel(providerName)} models.`)
156+
return []
157+
}
158+
}
159+
160+
function getProviderName(apiConfiguration: ProviderSettings): ProviderName {
161+
const providerName = apiConfiguration.apiProvider
162+
return providerName && isProviderName(providerName) ? providerName : "anthropic"
163+
}
164+
165+
function getProviderLabel(providerName: ProviderName): string {
166+
return providerLabels[providerName] ?? providerName
167+
}
168+
169+
function getCurrentModelId(apiConfiguration: ProviderSettings): string {
170+
try {
171+
return buildApiHandler(apiConfiguration).getModel().id
172+
} catch {
173+
return getModelId(apiConfiguration) ?? ""
174+
}
175+
}
176+
177+
function getModelIdKey(providerName: ProviderName): ModelIdKey | undefined {
178+
if (providerName === "openai") {
179+
return "openAiModelId"
180+
}
181+
182+
return modelIdKeysByProvider[providerName as keyof typeof modelIdKeysByProvider]
183+
}
184+
185+
function getProviderApiKey(apiConfiguration: ProviderSettings): string | undefined {
186+
switch (apiConfiguration.apiProvider) {
187+
case "litellm":
188+
return apiConfiguration.litellmApiKey
189+
case "requesty":
190+
return apiConfiguration.requestyApiKey
191+
case "unbound":
192+
return apiConfiguration.unboundApiKey
193+
case "roo":
194+
return apiConfiguration.rooApiKey
195+
case "poe":
196+
return apiConfiguration.poeApiKey
197+
case "vercel-ai-gateway":
198+
return apiConfiguration.vercelAiGatewayApiKey
199+
default:
200+
return undefined
201+
}
202+
}
203+
204+
function getProviderBaseUrl(apiConfiguration: ProviderSettings): string | undefined {
205+
switch (apiConfiguration.apiProvider) {
206+
case "litellm":
207+
return apiConfiguration.litellmBaseUrl
208+
case "requesty":
209+
return apiConfiguration.requestyBaseUrl
210+
case "ollama":
211+
return apiConfiguration.ollamaBaseUrl
212+
case "lmstudio":
213+
return apiConfiguration.lmStudioBaseUrl
214+
case "roo":
215+
return process.env.ROO_CODE_PROVIDER_URL ?? "https://api.roocode.com/proxy"
216+
case "poe":
217+
return apiConfiguration.poeBaseUrl
218+
default:
219+
return undefined
220+
}
221+
}

src/extension.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import {
4646
registerCodeActions,
4747
registerTerminalActions,
4848
CodeActionProvider,
49+
initializeProviderModelStatusBar,
4950
} from "./activate"
5051
import { initializeI18n } from "./i18n"
5152
import { flushModels, initializeModelCacheRefresh, refreshModels } from "./api/providers/fetchers/modelCache"
@@ -316,6 +317,7 @@ export async function activate(context: vscode.ExtensionContext) {
316317
}
317318

318319
registerCommands({ context, outputChannel, provider })
320+
initializeProviderModelStatusBar({ context, outputChannel, provider })
319321

320322
/**
321323
* We use the text document content provider API to show the left side for diff

0 commit comments

Comments
 (0)