Skip to content

Commit 49ef7ba

Browse files
committed
Fix input box revert issue and configuration loss during profile switch #955
1 parent 4a55e14 commit 49ef7ba

File tree

6 files changed

+125
-80
lines changed

6 files changed

+125
-80
lines changed

src/__mocks__/get-folder-size.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1-
module.exports = async function getFolderSize() {
1+
async function core() {
22
return {
33
size: 1000,
44
errors: [],
55
}
66
}
7+
core.loose = core
8+
core.strict = core
9+
module.exports = core

src/core/webview/ClineProvider.ts

Lines changed: 62 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1310,6 +1310,20 @@ export class ClineProvider implements vscode.WebviewViewProvider {
13101310
}
13111311
break
13121312
}
1313+
case "saveApiConfiguration":
1314+
if (message.text && message.apiConfiguration) {
1315+
try {
1316+
await this.configManager.saveConfig(message.text, message.apiConfiguration)
1317+
const listApiConfig = await this.configManager.listConfig()
1318+
await this.updateGlobalState("listApiConfigMeta", listApiConfig)
1319+
} catch (error) {
1320+
this.outputChannel.appendLine(
1321+
`Error save api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
1322+
)
1323+
vscode.window.showErrorMessage("Failed to save api configuration")
1324+
}
1325+
}
1326+
break
13131327
case "upsertApiConfiguration":
13141328
if (message.text && message.apiConfiguration) {
13151329
try {
@@ -1354,9 +1368,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
13541368
await this.postStateToWebview()
13551369
} catch (error) {
13561370
this.outputChannel.appendLine(
1357-
`Error create new api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
1371+
`Error rename api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
13581372
)
1359-
vscode.window.showErrorMessage("Failed to create api configuration")
1373+
vscode.window.showErrorMessage("Failed to rename api configuration")
13601374
}
13611375
}
13621376
break
@@ -1639,50 +1653,52 @@ export class ClineProvider implements vscode.WebviewViewProvider {
16391653
requestyModelInfo,
16401654
modelTemperature,
16411655
} = apiConfiguration
1642-
await this.updateGlobalState("apiProvider", apiProvider)
1643-
await this.updateGlobalState("apiModelId", apiModelId)
1644-
await this.storeSecret("apiKey", apiKey)
1645-
await this.updateGlobalState("glamaModelId", glamaModelId)
1646-
await this.updateGlobalState("glamaModelInfo", glamaModelInfo)
1647-
await this.storeSecret("glamaApiKey", glamaApiKey)
1648-
await this.storeSecret("openRouterApiKey", openRouterApiKey)
1649-
await this.storeSecret("awsAccessKey", awsAccessKey)
1650-
await this.storeSecret("awsSecretKey", awsSecretKey)
1651-
await this.storeSecret("awsSessionToken", awsSessionToken)
1652-
await this.updateGlobalState("awsRegion", awsRegion)
1653-
await this.updateGlobalState("awsUseCrossRegionInference", awsUseCrossRegionInference)
1654-
await this.updateGlobalState("awsProfile", awsProfile)
1655-
await this.updateGlobalState("awsUseProfile", awsUseProfile)
1656-
await this.updateGlobalState("vertexProjectId", vertexProjectId)
1657-
await this.updateGlobalState("vertexRegion", vertexRegion)
1658-
await this.updateGlobalState("openAiBaseUrl", openAiBaseUrl)
1659-
await this.storeSecret("openAiApiKey", openAiApiKey)
1660-
await this.updateGlobalState("openAiModelId", openAiModelId)
1661-
await this.updateGlobalState("openAiCustomModelInfo", openAiCustomModelInfo)
1662-
await this.updateGlobalState("openAiUseAzure", openAiUseAzure)
1663-
await this.updateGlobalState("ollamaModelId", ollamaModelId)
1664-
await this.updateGlobalState("ollamaBaseUrl", ollamaBaseUrl)
1665-
await this.updateGlobalState("lmStudioModelId", lmStudioModelId)
1666-
await this.updateGlobalState("lmStudioBaseUrl", lmStudioBaseUrl)
1667-
await this.updateGlobalState("anthropicBaseUrl", anthropicBaseUrl)
1668-
await this.storeSecret("geminiApiKey", geminiApiKey)
1669-
await this.storeSecret("openAiNativeApiKey", openAiNativeApiKey)
1670-
await this.storeSecret("deepSeekApiKey", deepSeekApiKey)
1671-
await this.updateGlobalState("azureApiVersion", azureApiVersion)
1672-
await this.updateGlobalState("openAiStreamingEnabled", openAiStreamingEnabled)
1673-
await this.updateGlobalState("openRouterModelId", openRouterModelId)
1674-
await this.updateGlobalState("openRouterModelInfo", openRouterModelInfo)
1675-
await this.updateGlobalState("openRouterBaseUrl", openRouterBaseUrl)
1676-
await this.updateGlobalState("openRouterUseMiddleOutTransform", openRouterUseMiddleOutTransform)
1677-
await this.updateGlobalState("vsCodeLmModelSelector", vsCodeLmModelSelector)
1678-
await this.storeSecret("mistralApiKey", mistralApiKey)
1679-
await this.storeSecret("unboundApiKey", unboundApiKey)
1680-
await this.updateGlobalState("unboundModelId", unboundModelId)
1681-
await this.updateGlobalState("unboundModelInfo", unboundModelInfo)
1682-
await this.storeSecret("requestyApiKey", requestyApiKey)
1683-
await this.updateGlobalState("requestyModelId", requestyModelId)
1684-
await this.updateGlobalState("requestyModelInfo", requestyModelInfo)
1685-
await this.updateGlobalState("modelTemperature", modelTemperature)
1656+
await Promise.all([
1657+
this.updateGlobalState("apiProvider", apiProvider),
1658+
this.updateGlobalState("apiModelId", apiModelId),
1659+
this.storeSecret("apiKey", apiKey),
1660+
this.updateGlobalState("glamaModelId", glamaModelId),
1661+
this.updateGlobalState("glamaModelInfo", glamaModelInfo),
1662+
this.storeSecret("glamaApiKey", glamaApiKey),
1663+
this.storeSecret("openRouterApiKey", openRouterApiKey),
1664+
this.storeSecret("awsAccessKey", awsAccessKey),
1665+
this.storeSecret("awsSecretKey", awsSecretKey),
1666+
this.storeSecret("awsSessionToken", awsSessionToken),
1667+
this.updateGlobalState("awsRegion", awsRegion),
1668+
this.updateGlobalState("awsUseCrossRegionInference", awsUseCrossRegionInference),
1669+
this.updateGlobalState("awsProfile", awsProfile),
1670+
this.updateGlobalState("awsUseProfile", awsUseProfile),
1671+
this.updateGlobalState("vertexProjectId", vertexProjectId),
1672+
this.updateGlobalState("vertexRegion", vertexRegion),
1673+
this.updateGlobalState("openAiBaseUrl", openAiBaseUrl),
1674+
this.storeSecret("openAiApiKey", openAiApiKey),
1675+
this.updateGlobalState("openAiModelId", openAiModelId),
1676+
this.updateGlobalState("openAiCustomModelInfo", openAiCustomModelInfo),
1677+
this.updateGlobalState("openAiUseAzure", openAiUseAzure),
1678+
this.updateGlobalState("ollamaModelId", ollamaModelId),
1679+
this.updateGlobalState("ollamaBaseUrl", ollamaBaseUrl),
1680+
this.updateGlobalState("lmStudioModelId", lmStudioModelId),
1681+
this.updateGlobalState("lmStudioBaseUrl", lmStudioBaseUrl),
1682+
this.updateGlobalState("anthropicBaseUrl", anthropicBaseUrl),
1683+
this.storeSecret("geminiApiKey", geminiApiKey),
1684+
this.storeSecret("openAiNativeApiKey", openAiNativeApiKey),
1685+
this.storeSecret("deepSeekApiKey", deepSeekApiKey),
1686+
this.updateGlobalState("azureApiVersion", azureApiVersion),
1687+
this.updateGlobalState("openAiStreamingEnabled", openAiStreamingEnabled),
1688+
this.updateGlobalState("openRouterModelId", openRouterModelId),
1689+
this.updateGlobalState("openRouterModelInfo", openRouterModelInfo),
1690+
this.updateGlobalState("openRouterBaseUrl", openRouterBaseUrl),
1691+
this.updateGlobalState("openRouterUseMiddleOutTransform", openRouterUseMiddleOutTransform),
1692+
this.updateGlobalState("vsCodeLmModelSelector", vsCodeLmModelSelector),
1693+
this.storeSecret("mistralApiKey", mistralApiKey),
1694+
this.storeSecret("unboundApiKey", unboundApiKey),
1695+
this.updateGlobalState("unboundModelId", unboundModelId),
1696+
this.updateGlobalState("unboundModelInfo", unboundModelInfo),
1697+
this.storeSecret("requestyApiKey", requestyApiKey),
1698+
this.updateGlobalState("requestyModelId", requestyModelId),
1699+
this.updateGlobalState("requestyModelInfo", requestyModelInfo),
1700+
this.updateGlobalState("modelTemperature", modelTemperature),
1701+
])
16861702
if (this.cline) {
16871703
this.cline.api = buildApiHandler(apiConfiguration)
16881704
}

src/core/webview/__tests__/ClineProvider.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1400,5 +1400,38 @@ describe("ClineProvider", () => {
14001400
])
14011401
expect(mockContext.globalState.update).toHaveBeenCalledWith("currentApiConfigName", "test-config")
14021402
})
1403+
1404+
test("handles successful saveApiConfiguration", async () => {
1405+
provider.resolveWebviewView(mockWebviewView)
1406+
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
1407+
1408+
// Mock ConfigManager methods
1409+
provider.configManager = {
1410+
saveConfig: jest.fn().mockResolvedValue(undefined),
1411+
listConfig: jest
1412+
.fn()
1413+
.mockResolvedValue([{ name: "test-config", id: "test-id", apiProvider: "anthropic" }]),
1414+
} as any
1415+
1416+
const testApiConfig = {
1417+
apiProvider: "anthropic" as const,
1418+
apiKey: "test-key",
1419+
}
1420+
1421+
// Trigger upsertApiConfiguration
1422+
await messageHandler({
1423+
type: "saveApiConfiguration",
1424+
text: "test-config",
1425+
apiConfiguration: testApiConfig,
1426+
})
1427+
1428+
// Verify config was saved
1429+
expect(provider.configManager.saveConfig).toHaveBeenCalledWith("test-config", testApiConfig)
1430+
1431+
// Verify state updates
1432+
expect(mockContext.globalState.update).toHaveBeenCalledWith("listApiConfigMeta", [
1433+
{ name: "test-config", id: "test-id", apiProvider: "anthropic" },
1434+
])
1435+
})
14031436
})
14041437
})

src/shared/WebviewMessage.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export interface WebviewMessage {
1212
type:
1313
| "apiConfiguration"
1414
| "currentApiConfigName"
15+
| "saveApiConfiguration"
1516
| "upsertApiConfiguration"
1617
| "deleteApiConfiguration"
1718
| "loadApiConfiguration"

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

Lines changed: 19 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { memo, useCallback, useEffect, useMemo, useState } from "react"
2-
import { useEvent, useInterval } from "react-use"
1+
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"
2+
import { useEvent } from "react-use"
33
import { Checkbox, Dropdown, Pane, type DropdownOption } from "vscrui"
44
import { VSCodeLink, VSCodeRadio, VSCodeRadioGroup, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
55
import { TemperatureControl } from "./TemperatureControl"
@@ -65,7 +65,8 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage, fromWelcomeView }: A
6565
return normalizeApiConfiguration(apiConfiguration)
6666
}, [apiConfiguration])
6767

68-
// Poll ollama/lmstudio models
68+
const requestLocalModelsTimeoutRef = useRef<NodeJS.Timeout | null>(null)
69+
// Pull ollama/lmstudio models
6970
const requestLocalModels = useCallback(() => {
7071
if (selectedProvider === "ollama") {
7172
vscode.postMessage({ type: "requestOllamaModels", text: apiConfiguration?.ollamaBaseUrl })
@@ -75,34 +76,29 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage, fromWelcomeView }: A
7576
vscode.postMessage({ type: "requestVsCodeLmModels" })
7677
}
7778
}, [selectedProvider, apiConfiguration?.ollamaBaseUrl, apiConfiguration?.lmStudioBaseUrl])
79+
// Debounced model updates, only executed 250ms after the user stops typing
7880
useEffect(() => {
79-
if (selectedProvider === "ollama" || selectedProvider === "lmstudio" || selectedProvider === "vscode-lm") {
80-
requestLocalModels()
81+
if (requestLocalModelsTimeoutRef.current) {
82+
clearTimeout(requestLocalModelsTimeoutRef.current)
8183
}
82-
}, [selectedProvider, requestLocalModels])
83-
useInterval(
84-
requestLocalModels,
85-
selectedProvider === "ollama" || selectedProvider === "lmstudio" || selectedProvider === "vscode-lm"
86-
? 2000
87-
: null,
88-
)
84+
requestLocalModelsTimeoutRef.current = setTimeout(requestLocalModels, 250)
85+
return () => {
86+
if (requestLocalModelsTimeoutRef.current) {
87+
clearTimeout(requestLocalModelsTimeoutRef.current)
88+
}
89+
}
90+
}, [requestLocalModels])
8991
const handleMessage = useCallback((event: MessageEvent) => {
9092
const message: ExtensionMessage = event.data
9193
if (message.type === "ollamaModels" && Array.isArray(message.ollamaModels)) {
9294
const newModels = message.ollamaModels
93-
setOllamaModels((prevModels) => {
94-
return JSON.stringify(prevModels) === JSON.stringify(newModels) ? prevModels : newModels
95-
})
95+
setOllamaModels(newModels)
9696
} else if (message.type === "lmStudioModels" && Array.isArray(message.lmStudioModels)) {
9797
const newModels = message.lmStudioModels
98-
setLmStudioModels((prevModels) => {
99-
return JSON.stringify(prevModels) === JSON.stringify(newModels) ? prevModels : newModels
100-
})
98+
setLmStudioModels(newModels)
10199
} else if (message.type === "vsCodeLmModels" && Array.isArray(message.vsCodeLmModels)) {
102100
const newModels = message.vsCodeLmModels
103-
setVsCodeLmModels((prevModels) => {
104-
return JSON.stringify(prevModels) === JSON.stringify(newModels) ? prevModels : newModels
105-
})
101+
setVsCodeLmModels(newModels)
106102
}
107103
}, [])
108104
useEvent("message", handleMessage)
@@ -1105,6 +1101,7 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage, fromWelcomeView }: A
11051101
value={apiConfiguration?.lmStudioBaseUrl || ""}
11061102
style={{ width: "100%" }}
11071103
type="url"
1104+
onInput={handleInputChange("lmStudioBaseUrl", true)}
11081105
onBlur={handleInputChange("lmStudioBaseUrl")}
11091106
placeholder={"Default: http://localhost:1234"}>
11101107
<span style={{ fontWeight: 500 }}>Base URL (optional)</span>
@@ -1264,6 +1261,7 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage, fromWelcomeView }: A
12641261
value={apiConfiguration?.ollamaBaseUrl || ""}
12651262
style={{ width: "100%" }}
12661263
type="url"
1264+
onInput={handleInputChange("ollamaBaseUrl", true)}
12671265
onBlur={handleInputChange("ollamaBaseUrl")}
12681266
placeholder={"Default: http://localhost:11434"}>
12691267
<span style={{ fontWeight: 500 }}>Base URL (optional)</span>

webview-ui/src/context/ExtensionStateContext.tsx

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -163,23 +163,17 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
163163
(field: keyof ApiConfiguration, softUpdate?: boolean) => (event: any) => {
164164
// Use the functional form of setState to ensure the latest state is used in the update logic.
165165
setState((currentState) => {
166-
if (softUpdate) {
167-
// Return a new state object with the updated apiConfiguration.
168-
// This will trigger a re-render with the new configuration value.
169-
return {
170-
...currentState,
171-
apiConfiguration: { ...currentState.apiConfiguration, [field]: event.target.value },
172-
}
173-
} else {
166+
if (!softUpdate) {
174167
// For non-soft updates, send a message to the VS Code extension with the updated config.
175-
// This side effect communicates the change without updating local React state.
176168
vscode.postMessage({
177-
type: "upsertApiConfiguration",
169+
type: "saveApiConfiguration",
178170
text: currentState.currentApiConfigName,
179171
apiConfiguration: { ...currentState.apiConfiguration, [field]: event.target.value },
180172
})
181-
// Return the unchanged state as no local state update is intended in this branch.
182-
return currentState
173+
}
174+
return {
175+
...currentState,
176+
apiConfiguration: { ...currentState.apiConfiguration, [field]: event.target.value },
183177
}
184178
})
185179
},

0 commit comments

Comments
 (0)