Skip to content

Commit 340f53e

Browse files
[Fix] Setup announcement shows the wrong origin and LM Studio misses models on first load (#97)
* fix: correct setup copy and lm studio model loading * fix: keep lm studio setup refreshes scoped * fix: preserve empty lm studio preview urls * fix: narrow lm studio preview base urls safely * Simplify LM Studio preview model handling --------- Co-authored-by: Roomote <roomote@roocode.com>
1 parent 350e172 commit 340f53e

7 files changed

Lines changed: 96 additions & 15 deletions

File tree

src/core/webview/__tests__/webviewMessageHandler.spec.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import type { Mock } from "vitest"
44

55
// Mock dependencies - must come before imports
66
vi.mock("../../../api/providers/fetchers/modelCache")
7+
vi.mock("../../../api/providers/fetchers/lmstudio", () => ({
8+
getLMStudioModels: vi.fn(),
9+
}))
710

811
vi.mock("../../../integrations/openai-codex/oauth", () => ({
912
openAiCodexOAuthManager: {
@@ -42,11 +45,13 @@ import type { ModelRecord } from "@roo-code/types"
4245
import { webviewMessageHandler } from "../webviewMessageHandler"
4346
import type { ClineProvider } from "../ClineProvider"
4447
import { getModels } from "../../../api/providers/fetchers/modelCache"
48+
import { getLMStudioModels } from "../../../api/providers/fetchers/lmstudio"
4549
import { getCommands } from "../../../services/command/commands"
4650
const { openAiCodexOAuthManager } = await import("../../../integrations/openai-codex/oauth")
4751
const { fetchOpenAiCodexRateLimitInfo } = await import("../../../integrations/openai-codex/rate-limits")
4852

4953
const mockGetModels = getModels as Mock<typeof getModels>
54+
const mockGetLMStudioModels = getLMStudioModels as Mock<typeof getLMStudioModels>
5055
const mockGetCommands = vi.mocked(getCommands)
5156
const mockGetAccessToken = vi.mocked(openAiCodexOAuthManager.getAccessToken)
5257
const mockGetAccountId = vi.mocked(openAiCodexOAuthManager.getAccountId)
@@ -166,6 +171,7 @@ import { resolveImageMentions } from "../../mentions/resolveImageMentions"
166171
describe("webviewMessageHandler - requestLmStudioModels", () => {
167172
beforeEach(() => {
168173
vi.clearAllMocks()
174+
mockGetLMStudioModels.mockReset()
169175
mockClineProvider.getState = vi.fn().mockResolvedValue({
170176
apiConfiguration: {
171177
lmStudioModelId: "model-1",
@@ -203,6 +209,30 @@ describe("webviewMessageHandler - requestLmStudioModels", () => {
203209
lmStudioModels: mockModels,
204210
})
205211
})
212+
213+
it("prefers the request payload base URL over persisted settings", async () => {
214+
mockGetLMStudioModels.mockResolvedValue({})
215+
216+
await webviewMessageHandler(mockClineProvider, {
217+
type: "requestLmStudioModels",
218+
values: { baseUrl: "http://127.0.0.1:4321" },
219+
})
220+
221+
expect(mockGetLMStudioModels).toHaveBeenCalledWith("http://127.0.0.1:4321")
222+
expect(mockGetModels).not.toHaveBeenCalled()
223+
})
224+
225+
it("treats an empty-string base URL as an explicit preview request", async () => {
226+
mockGetLMStudioModels.mockResolvedValue({})
227+
228+
await webviewMessageHandler(mockClineProvider, {
229+
type: "requestLmStudioModels",
230+
values: { baseUrl: "" },
231+
})
232+
233+
expect(mockGetLMStudioModels).toHaveBeenCalledWith("")
234+
expect(mockGetModels).not.toHaveBeenCalled()
235+
})
206236
})
207237

208238
describe("webviewMessageHandler - image mentions", () => {

src/core/webview/webviewMessageHandler.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ import { GetModelsOptions } from "../../shared/api"
7272
import { generateSystemPrompt } from "./generateSystemPrompt"
7373
import { resolveDefaultSaveUri, saveLastExportPath } from "../../utils/export"
7474
import { getCommand } from "../../utils/commands"
75+
import { getLMStudioModels } from "../../api/providers/fetchers/lmstudio"
7576

7677
const ALLOWED_VSCODE_SETTINGS = new Set(["terminal.integrated.inheritEnv"])
7778

@@ -1086,14 +1087,20 @@ export const webviewMessageHandler = async (
10861087
// Specific handler for LM Studio models only.
10871088
const { apiConfiguration: lmStudioApiConfig } = await provider.getState()
10881089
try {
1089-
const lmStudioOptions = {
1090-
provider: "lmstudio" as const,
1091-
baseUrl: lmStudioApiConfig.lmStudioBaseUrl,
1090+
const requestedBaseUrl = message.values?.baseUrl
1091+
const hasPreviewBaseUrl = typeof requestedBaseUrl === "string"
1092+
let lmStudioModels: ModelRecord
1093+
if (hasPreviewBaseUrl) {
1094+
lmStudioModels = await getLMStudioModels(requestedBaseUrl)
1095+
} else {
1096+
const lmStudioOptions = {
1097+
provider: "lmstudio" as const,
1098+
baseUrl: lmStudioApiConfig.lmStudioBaseUrl,
1099+
}
1100+
// Flush cache and refresh to ensure fresh models.
1101+
await flushModels(lmStudioOptions, true)
1102+
lmStudioModels = await getModels(lmStudioOptions)
10921103
}
1093-
// Flush cache and refresh to ensure fresh models.
1094-
await flushModels(lmStudioOptions, true)
1095-
1096-
const lmStudioModels = await getModels(lmStudioOptions)
10971104

10981105
if (Object.keys(lmStudioModels).length > 0) {
10991106
provider.postMessageToWebview({

webview-ui/src/components/settings/ApiOptions.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import { validateApiConfigurationExcludingModelErrors, getModelValidationError }
4747
import { useAppTranslation } from "@src/i18n/TranslationContext"
4848
import { useRouterModels } from "@src/components/ui/hooks/useRouterModels"
4949
import { useSelectedModel } from "@src/components/ui/hooks/useSelectedModel"
50+
import { requestLmStudioModels } from "@src/components/ui/hooks/useLmStudioModels"
5051
import { useExtensionState } from "@src/context/ExtensionStateContext"
5152
import {
5253
useOpenRouterModelProviders,
@@ -236,7 +237,7 @@ const ApiOptions = ({
236237
} else if (selectedProvider === "ollama") {
237238
vscode.postMessage({ type: "requestOllamaModels" })
238239
} else if (selectedProvider === "lmstudio") {
239-
vscode.postMessage({ type: "requestLmStudioModels" })
240+
requestLmStudioModels(apiConfiguration?.lmStudioBaseUrl)
240241
} else if (selectedProvider === "vscode-lm") {
241242
vscode.postMessage({ type: "requestVsCodeLmModels" })
242243
} else if (selectedProvider === "litellm" || selectedProvider === "poe") {

webview-ui/src/components/settings/providers/LMStudio.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useCallback, useState, useMemo, useEffect } from "react"
1+
import { useCallback, useState, useMemo, useEffect, useRef } from "react"
22
import { useEvent } from "react-use"
33
import { Trans } from "react-i18next"
44
import { Checkbox } from "vscrui"
@@ -7,8 +7,8 @@ import { VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
77
import type { ProviderSettings, ExtensionMessage, ModelRecord } from "@roo-code/types"
88

99
import { useAppTranslation } from "@src/i18n/TranslationContext"
10+
import { requestLmStudioModels } from "@src/components/ui/hooks/useLmStudioModels"
1011
import { useRouterModels } from "@src/components/ui/hooks/useRouterModels"
11-
import { vscode } from "@src/utils/vscode"
1212

1313
import { inputEventTransform } from "../transforms"
1414
import { ModelPicker } from "../ModelPicker"
@@ -23,6 +23,7 @@ export const LMStudio = ({ apiConfiguration, setApiConfigurationField }: LMStudi
2323

2424
const [lmStudioModels, setLmStudioModels] = useState<ModelRecord>({})
2525
const routerModels = useRouterModels()
26+
const initialBaseUrlRef = useRef(apiConfiguration?.lmStudioBaseUrl)
2627

2728
const handleInputChange = useCallback(
2829
<K extends keyof ProviderSettings, E>(
@@ -53,7 +54,7 @@ export const LMStudio = ({ apiConfiguration, setApiConfigurationField }: LMStudi
5354
// Refresh models on mount
5455
useEffect(() => {
5556
// Request fresh models - the handler now flushes cache automatically
56-
vscode.postMessage({ type: "requestLmStudioModels" })
57+
requestLmStudioModels(initialBaseUrlRef.current)
5758
}, [])
5859

5960
// Check if the selected model exists in the fetched models
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
vi.mock("@src/utils/vscode", () => ({
2+
vscode: {
3+
postMessage: vi.fn(),
4+
},
5+
}))
6+
7+
import { vscode } from "@src/utils/vscode"
8+
9+
import { requestLmStudioModels } from "../useLmStudioModels"
10+
11+
describe("requestLmStudioModels", () => {
12+
beforeEach(() => {
13+
vi.clearAllMocks()
14+
})
15+
16+
it("includes the current unsaved base URL when requesting models", () => {
17+
requestLmStudioModels("http://127.0.0.1:1234")
18+
19+
expect(vscode.postMessage).toHaveBeenCalledWith({
20+
type: "requestLmStudioModels",
21+
values: { baseUrl: "http://127.0.0.1:1234" },
22+
})
23+
})
24+
25+
it("preserves an empty base URL so the extension can fall back to the default", () => {
26+
requestLmStudioModels("")
27+
28+
expect(vscode.postMessage).toHaveBeenCalledWith({
29+
type: "requestLmStudioModels",
30+
values: { baseUrl: "" },
31+
})
32+
})
33+
})

webview-ui/src/components/ui/hooks/useLmStudioModels.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,13 @@ import { type ModelRecord, type ExtensionMessage } from "@roo-code/types"
44

55
import { vscode } from "@src/utils/vscode"
66

7-
const getLmStudioModels = async () =>
7+
export const requestLmStudioModels = (baseUrl?: string) =>
8+
vscode.postMessage({
9+
type: "requestLmStudioModels",
10+
values: typeof baseUrl === "string" ? { baseUrl } : undefined,
11+
})
12+
13+
const getLmStudioModels = async (baseUrl?: string) =>
814
new Promise<ModelRecord>((resolve, reject) => {
915
const cleanup = () => {
1016
window.removeEventListener("message", handler)
@@ -31,8 +37,11 @@ const getLmStudioModels = async () =>
3137
}
3238

3339
window.addEventListener("message", handler)
34-
vscode.postMessage({ type: "requestLmStudioModels" })
40+
requestLmStudioModels(baseUrl)
3541
})
3642

3743
export const useLmStudioModels = (modelId?: string) =>
38-
useQuery({ queryKey: ["lmStudioModels"], queryFn: () => (modelId ? getLmStudioModels() : {}) })
44+
useQuery({
45+
queryKey: ["lmStudioModels"],
46+
queryFn: () => (modelId ? getLmStudioModels() : {}),
47+
})

webview-ui/src/i18n/locales/en/chat.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -351,7 +351,7 @@
351351
"support": "Please support Zoo Code by starring us on <githubLink>GitHub</githubLink>.",
352352
"handoff": {
353353
"heading": "Roo Code is back! Now as a community-maintained plugin called Zoo Code!!",
354-
"description": "If you haven't been following, the Roo Code team recently announced they are sun setting the development of Roo Code and are archiving the work they have done. But fear not, the community has stepped up to continue the legacy of Roo Code with a new name and a new home! We are not just a single \"Roo\" anymore, we are a community, a \"Zoo\" if you will, Zoo Code is a community-maintained plugin that picks up where Zoo Code left off, and we're committed to keeping the spirit of Roo alive while also introducing new features and improvements. We want to give a huge shoutout to the entire Roo Code team for their incredible work and for creating such an amazing tool for developers. We're excited to continue building on their foundation and to see where the community takes Zoo Code in the future!",
354+
"description": "If you haven't been following, the Roo Code team recently announced they are sun setting the development of Roo Code and are archiving the work they have done. But fear not, the community has stepped up to continue the legacy of Roo Code with a new name and a new home! We are not just a single \"Roo\" anymore, we are a community, a \"Zoo\" if you will, Zoo Code is a community-maintained plugin that picks up where Roo Code left off, and we're committed to keeping the spirit of Roo alive while also introducing new features and improvements. We want to give a huge shoutout to the entire Roo Code team for their incredible work and for creating such an amazing tool for developers. We're excited to continue building on their foundation and to see where the community takes Zoo Code in the future!",
355355
"readMore": "See the new home page of Zoo Code and read the full announcement"
356356
},
357357
"release": {

0 commit comments

Comments
 (0)