Browse Source

Fix input box revert issue and configuration loss during profile switch #955

System233 1 year ago
parent
commit
2943d655cf

+ 90 - 63
src/core/config/ConfigManager.ts

@@ -33,29 +33,38 @@ export class ConfigManager {
 		return Math.random().toString(36).substring(2, 15)
 	}
 
+	// Synchronize readConfig/writeConfig operations to avoid data loss.
+	private _lock = Promise.resolve()
+	private lock<T>(cb: () => Promise<T>) {
+		const next = this._lock.then(cb)
+		this._lock = next.catch(() => {}) as Promise<void>
+		return next
+	}
 	/**
 	 * Initialize config if it doesn't exist
 	 */
 	async initConfig(): Promise<void> {
 		try {
-			const config = await this.readConfig()
-			if (!config) {
-				await this.writeConfig(this.defaultConfig)
-				return
-			}
+			return await this.lock(async () => {
+				const config = await this.readConfig()
+				if (!config) {
+					await this.writeConfig(this.defaultConfig)
+					return
+				}
 
-			// Migrate: ensure all configs have IDs
-			let needsMigration = false
-			for (const [name, apiConfig] of Object.entries(config.apiConfigs)) {
-				if (!apiConfig.id) {
-					apiConfig.id = this.generateId()
-					needsMigration = true
+				// Migrate: ensure all configs have IDs
+				let needsMigration = false
+				for (const [name, apiConfig] of Object.entries(config.apiConfigs)) {
+					if (!apiConfig.id) {
+						apiConfig.id = this.generateId()
+						needsMigration = true
+					}
 				}
-			}
 
-			if (needsMigration) {
-				await this.writeConfig(config)
-			}
+				if (needsMigration) {
+					await this.writeConfig(config)
+				}
+			})
 		} catch (error) {
 			throw new Error(`Failed to initialize config: ${error}`)
 		}
@@ -66,12 +75,14 @@ export class ConfigManager {
 	 */
 	async listConfig(): Promise<ApiConfigMeta[]> {
 		try {
-			const config = await this.readConfig()
-			return Object.entries(config.apiConfigs).map(([name, apiConfig]) => ({
-				name,
-				id: apiConfig.id || "",
-				apiProvider: apiConfig.apiProvider,
-			}))
+			return await this.lock(async () => {
+				const config = await this.readConfig()
+				return Object.entries(config.apiConfigs).map(([name, apiConfig]) => ({
+					name,
+					id: apiConfig.id || "",
+					apiProvider: apiConfig.apiProvider,
+				}))
+			})
 		} catch (error) {
 			throw new Error(`Failed to list configs: ${error}`)
 		}
@@ -82,13 +93,15 @@ export class ConfigManager {
 	 */
 	async saveConfig(name: string, config: ApiConfiguration): Promise<void> {
 		try {
-			const currentConfig = await this.readConfig()
-			const existingConfig = currentConfig.apiConfigs[name]
-			currentConfig.apiConfigs[name] = {
-				...config,
-				id: existingConfig?.id || this.generateId(),
-			}
-			await this.writeConfig(currentConfig)
+			return await this.lock(async () => {
+				const currentConfig = await this.readConfig()
+				const existingConfig = currentConfig.apiConfigs[name]
+				currentConfig.apiConfigs[name] = {
+					...config,
+					id: existingConfig?.id || this.generateId(),
+				}
+				await this.writeConfig(currentConfig)
+			})
 		} catch (error) {
 			throw new Error(`Failed to save config: ${error}`)
 		}
@@ -99,17 +112,19 @@ export class ConfigManager {
 	 */
 	async loadConfig(name: string): Promise<ApiConfiguration> {
 		try {
-			const config = await this.readConfig()
-			const apiConfig = config.apiConfigs[name]
+			return await this.lock(async () => {
+				const config = await this.readConfig()
+				const apiConfig = config.apiConfigs[name]
 
-			if (!apiConfig) {
-				throw new Error(`Config '${name}' not found`)
-			}
+				if (!apiConfig) {
+					throw new Error(`Config '${name}' not found`)
+				}
 
-			config.currentApiConfigName = name
-			await this.writeConfig(config)
+				config.currentApiConfigName = name
+				await this.writeConfig(config)
 
-			return apiConfig
+				return apiConfig
+			})
 		} catch (error) {
 			throw new Error(`Failed to load config: ${error}`)
 		}
@@ -120,18 +135,20 @@ export class ConfigManager {
 	 */
 	async deleteConfig(name: string): Promise<void> {
 		try {
-			const currentConfig = await this.readConfig()
-			if (!currentConfig.apiConfigs[name]) {
-				throw new Error(`Config '${name}' not found`)
-			}
+			return await this.lock(async () => {
+				const currentConfig = await this.readConfig()
+				if (!currentConfig.apiConfigs[name]) {
+					throw new Error(`Config '${name}' not found`)
+				}
 
-			// Don't allow deleting the default config
-			if (Object.keys(currentConfig.apiConfigs).length === 1) {
-				throw new Error(`Cannot delete the last remaining configuration.`)
-			}
+				// Don't allow deleting the default config
+				if (Object.keys(currentConfig.apiConfigs).length === 1) {
+					throw new Error(`Cannot delete the last remaining configuration.`)
+				}
 
-			delete currentConfig.apiConfigs[name]
-			await this.writeConfig(currentConfig)
+				delete currentConfig.apiConfigs[name]
+				await this.writeConfig(currentConfig)
+			})
 		} catch (error) {
 			throw new Error(`Failed to delete config: ${error}`)
 		}
@@ -142,13 +159,15 @@ export class ConfigManager {
 	 */
 	async setCurrentConfig(name: string): Promise<void> {
 		try {
-			const currentConfig = await this.readConfig()
-			if (!currentConfig.apiConfigs[name]) {
-				throw new Error(`Config '${name}' not found`)
-			}
+			return await this.lock(async () => {
+				const currentConfig = await this.readConfig()
+				if (!currentConfig.apiConfigs[name]) {
+					throw new Error(`Config '${name}' not found`)
+				}
 
-			currentConfig.currentApiConfigName = name
-			await this.writeConfig(currentConfig)
+				currentConfig.currentApiConfigName = name
+				await this.writeConfig(currentConfig)
+			})
 		} catch (error) {
 			throw new Error(`Failed to set current config: ${error}`)
 		}
@@ -159,8 +178,10 @@ export class ConfigManager {
 	 */
 	async hasConfig(name: string): Promise<boolean> {
 		try {
-			const config = await this.readConfig()
-			return name in config.apiConfigs
+			return await this.lock(async () => {
+				const config = await this.readConfig()
+				return name in config.apiConfigs
+			})
 		} catch (error) {
 			throw new Error(`Failed to check config existence: ${error}`)
 		}
@@ -171,12 +192,14 @@ export class ConfigManager {
 	 */
 	async setModeConfig(mode: Mode, configId: string): Promise<void> {
 		try {
-			const currentConfig = await this.readConfig()
-			if (!currentConfig.modeApiConfigs) {
-				currentConfig.modeApiConfigs = {}
-			}
-			currentConfig.modeApiConfigs[mode] = configId
-			await this.writeConfig(currentConfig)
+			return await this.lock(async () => {
+				const currentConfig = await this.readConfig()
+				if (!currentConfig.modeApiConfigs) {
+					currentConfig.modeApiConfigs = {}
+				}
+				currentConfig.modeApiConfigs[mode] = configId
+				await this.writeConfig(currentConfig)
+			})
 		} catch (error) {
 			throw new Error(`Failed to set mode config: ${error}`)
 		}
@@ -187,8 +210,10 @@ export class ConfigManager {
 	 */
 	async getModeConfigId(mode: Mode): Promise<string | undefined> {
 		try {
-			const config = await this.readConfig()
-			return config.modeApiConfigs?.[mode]
+			return await this.lock(async () => {
+				const config = await this.readConfig()
+				return config.modeApiConfigs?.[mode]
+			})
 		} catch (error) {
 			throw new Error(`Failed to get mode config: ${error}`)
 		}
@@ -205,7 +230,9 @@ export class ConfigManager {
 	 * Reset all configuration by deleting the stored config from secrets
 	 */
 	public async resetAllConfigs(): Promise<void> {
-		await this.context.secrets.delete(this.getConfigKey())
+		return await this.lock(async () => {
+			await this.context.secrets.delete(this.getConfigKey())
+		})
 	}
 
 	private async readConfig(): Promise<ApiConfigData> {

+ 63 - 47
src/core/webview/ClineProvider.ts

@@ -1317,6 +1317,20 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 						}
 						break
 					}
+					case "saveApiConfiguration":
+						if (message.text && message.apiConfiguration) {
+							try {
+								await this.configManager.saveConfig(message.text, message.apiConfiguration)
+								const listApiConfig = await this.configManager.listConfig()
+								await this.updateGlobalState("listApiConfigMeta", listApiConfig)
+							} catch (error) {
+								this.outputChannel.appendLine(
+									`Error save api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
+								)
+								vscode.window.showErrorMessage("Failed to save api configuration")
+							}
+						}
+						break
 					case "upsertApiConfiguration":
 						if (message.text && message.apiConfiguration) {
 							try {
@@ -1361,9 +1375,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 								await this.postStateToWebview()
 							} catch (error) {
 								this.outputChannel.appendLine(
-									`Error create new api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
+									`Error rename api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
 								)
-								vscode.window.showErrorMessage("Failed to create api configuration")
+								vscode.window.showErrorMessage("Failed to rename api configuration")
 							}
 						}
 						break
@@ -1647,51 +1661,53 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			requestyModelInfo,
 			modelTemperature,
 		} = apiConfiguration
-		await this.updateGlobalState("apiProvider", apiProvider)
-		await this.updateGlobalState("apiModelId", apiModelId)
-		await this.storeSecret("apiKey", apiKey)
-		await this.updateGlobalState("glamaModelId", glamaModelId)
-		await this.updateGlobalState("glamaModelInfo", glamaModelInfo)
-		await this.storeSecret("glamaApiKey", glamaApiKey)
-		await this.storeSecret("openRouterApiKey", openRouterApiKey)
-		await this.storeSecret("awsAccessKey", awsAccessKey)
-		await this.storeSecret("awsSecretKey", awsSecretKey)
-		await this.storeSecret("awsSessionToken", awsSessionToken)
-		await this.updateGlobalState("awsRegion", awsRegion)
-		await this.updateGlobalState("awsUseCrossRegionInference", awsUseCrossRegionInference)
-		await this.updateGlobalState("awsProfile", awsProfile)
-		await this.updateGlobalState("awsUseProfile", awsUseProfile)
-		await this.updateGlobalState("vertexProjectId", vertexProjectId)
-		await this.updateGlobalState("vertexRegion", vertexRegion)
-		await this.updateGlobalState("openAiBaseUrl", openAiBaseUrl)
-		await this.storeSecret("openAiApiKey", openAiApiKey)
-		await this.updateGlobalState("openAiModelId", openAiModelId)
-		await this.updateGlobalState("openAiCustomModelInfo", openAiCustomModelInfo)
-		await this.updateGlobalState("openAiUseAzure", openAiUseAzure)
-		await this.updateGlobalState("ollamaModelId", ollamaModelId)
-		await this.updateGlobalState("ollamaBaseUrl", ollamaBaseUrl)
-		await this.updateGlobalState("lmStudioModelId", lmStudioModelId)
-		await this.updateGlobalState("lmStudioBaseUrl", lmStudioBaseUrl)
-		await this.updateGlobalState("anthropicBaseUrl", anthropicBaseUrl)
-		await this.storeSecret("geminiApiKey", geminiApiKey)
-		await this.storeSecret("openAiNativeApiKey", openAiNativeApiKey)
-		await this.storeSecret("deepSeekApiKey", deepSeekApiKey)
-		await this.updateGlobalState("azureApiVersion", azureApiVersion)
-		await this.updateGlobalState("openAiStreamingEnabled", openAiStreamingEnabled)
-		await this.updateGlobalState("openRouterModelId", openRouterModelId)
-		await this.updateGlobalState("openRouterModelInfo", openRouterModelInfo)
-		await this.updateGlobalState("openRouterBaseUrl", openRouterBaseUrl)
-		await this.updateGlobalState("openRouterUseMiddleOutTransform", openRouterUseMiddleOutTransform)
-		await this.updateGlobalState("vsCodeLmModelSelector", vsCodeLmModelSelector)
-		await this.storeSecret("mistralApiKey", mistralApiKey)
-		await this.updateGlobalState("mistralCodestralUrl", mistralCodestralUrl)
-		await this.storeSecret("unboundApiKey", unboundApiKey)
-		await this.updateGlobalState("unboundModelId", unboundModelId)
-		await this.updateGlobalState("unboundModelInfo", unboundModelInfo)
-		await this.storeSecret("requestyApiKey", requestyApiKey)
-		await this.updateGlobalState("requestyModelId", requestyModelId)
-		await this.updateGlobalState("requestyModelInfo", requestyModelInfo)
-		await this.updateGlobalState("modelTemperature", modelTemperature)
+		await Promise.all([
+			this.updateGlobalState("apiProvider", apiProvider),
+			this.updateGlobalState("apiModelId", apiModelId),
+			this.storeSecret("apiKey", apiKey),
+			this.updateGlobalState("glamaModelId", glamaModelId),
+			this.updateGlobalState("glamaModelInfo", glamaModelInfo),
+			this.storeSecret("glamaApiKey", glamaApiKey),
+			this.storeSecret("openRouterApiKey", openRouterApiKey),
+			this.storeSecret("awsAccessKey", awsAccessKey),
+			this.storeSecret("awsSecretKey", awsSecretKey),
+			this.storeSecret("awsSessionToken", awsSessionToken),
+			this.updateGlobalState("awsRegion", awsRegion),
+			this.updateGlobalState("awsUseCrossRegionInference", awsUseCrossRegionInference),
+			this.updateGlobalState("awsProfile", awsProfile),
+			this.updateGlobalState("awsUseProfile", awsUseProfile),
+			this.updateGlobalState("vertexProjectId", vertexProjectId),
+			this.updateGlobalState("vertexRegion", vertexRegion),
+			this.updateGlobalState("openAiBaseUrl", openAiBaseUrl),
+			this.storeSecret("openAiApiKey", openAiApiKey),
+			this.updateGlobalState("openAiModelId", openAiModelId),
+			this.updateGlobalState("openAiCustomModelInfo", openAiCustomModelInfo),
+			this.updateGlobalState("openAiUseAzure", openAiUseAzure),
+			this.updateGlobalState("ollamaModelId", ollamaModelId),
+			this.updateGlobalState("ollamaBaseUrl", ollamaBaseUrl),
+			this.updateGlobalState("lmStudioModelId", lmStudioModelId),
+			this.updateGlobalState("lmStudioBaseUrl", lmStudioBaseUrl),
+			this.updateGlobalState("anthropicBaseUrl", anthropicBaseUrl),
+			this.storeSecret("geminiApiKey", geminiApiKey),
+			this.storeSecret("openAiNativeApiKey", openAiNativeApiKey),
+			this.storeSecret("deepSeekApiKey", deepSeekApiKey),
+			this.updateGlobalState("azureApiVersion", azureApiVersion),
+			this.updateGlobalState("openAiStreamingEnabled", openAiStreamingEnabled),
+			this.updateGlobalState("openRouterModelId", openRouterModelId),
+			this.updateGlobalState("openRouterModelInfo", openRouterModelInfo),
+			this.updateGlobalState("openRouterBaseUrl", openRouterBaseUrl),
+			this.updateGlobalState("openRouterUseMiddleOutTransform", openRouterUseMiddleOutTransform),
+			this.updateGlobalState("vsCodeLmModelSelector", vsCodeLmModelSelector),
+			this.storeSecret("mistralApiKey", mistralApiKey),
+			this.updateGlobalState("mistralCodestralUrl", mistralCodestralUrl),
+			this.storeSecret("unboundApiKey", unboundApiKey),
+			this.updateGlobalState("unboundModelId", unboundModelId),
+			this.updateGlobalState("unboundModelInfo", unboundModelInfo),
+			this.storeSecret("requestyApiKey", requestyApiKey),
+			this.updateGlobalState("requestyModelId", requestyModelId),
+			this.updateGlobalState("requestyModelInfo", requestyModelInfo),
+			this.updateGlobalState("modelTemperature", modelTemperature),
+		])
 		if (this.cline) {
 			this.cline.api = buildApiHandler(apiConfiguration)
 		}

+ 33 - 0
src/core/webview/__tests__/ClineProvider.test.ts

@@ -1405,5 +1405,38 @@ describe("ClineProvider", () => {
 			])
 			expect(mockContext.globalState.update).toHaveBeenCalledWith("currentApiConfigName", "test-config")
 		})
+
+		test("handles successful saveApiConfiguration", async () => {
+			provider.resolveWebviewView(mockWebviewView)
+			const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
+
+			// Mock ConfigManager methods
+			provider.configManager = {
+				saveConfig: jest.fn().mockResolvedValue(undefined),
+				listConfig: jest
+					.fn()
+					.mockResolvedValue([{ name: "test-config", id: "test-id", apiProvider: "anthropic" }]),
+			} as any
+
+			const testApiConfig = {
+				apiProvider: "anthropic" as const,
+				apiKey: "test-key",
+			}
+
+			// Trigger upsertApiConfiguration
+			await messageHandler({
+				type: "saveApiConfiguration",
+				text: "test-config",
+				apiConfiguration: testApiConfig,
+			})
+
+			// Verify config was saved
+			expect(provider.configManager.saveConfig).toHaveBeenCalledWith("test-config", testApiConfig)
+
+			// Verify state updates
+			expect(mockContext.globalState.update).toHaveBeenCalledWith("listApiConfigMeta", [
+				{ name: "test-config", id: "test-id", apiProvider: "anthropic" },
+			])
+		})
 	})
 })

+ 1 - 0
src/shared/WebviewMessage.ts

@@ -12,6 +12,7 @@ export interface WebviewMessage {
 	type:
 		| "apiConfiguration"
 		| "currentApiConfigName"
+		| "saveApiConfiguration"
 		| "upsertApiConfiguration"
 		| "deleteApiConfiguration"
 		| "loadApiConfiguration"

+ 21 - 28
webview-ui/src/components/settings/ApiOptions.tsx

@@ -1,5 +1,5 @@
-import { memo, useCallback, useEffect, useMemo, useState } from "react"
-import { useEvent, useInterval } from "react-use"
+import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"
+import { useEvent } from "react-use"
 import { Checkbox, Dropdown, Pane, type DropdownOption } from "vscrui"
 import { VSCodeLink, VSCodeRadio, VSCodeRadioGroup, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
 import { TemperatureControl } from "./TemperatureControl"
@@ -65,7 +65,8 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage, fromWelcomeView }: A
 		return normalizeApiConfiguration(apiConfiguration)
 	}, [apiConfiguration])
 
-	// Poll ollama/lmstudio models
+	const requestLocalModelsTimeoutRef = useRef<NodeJS.Timeout | null>(null)
+	// Pull ollama/lmstudio models
 	const requestLocalModels = useCallback(() => {
 		if (selectedProvider === "ollama") {
 			vscode.postMessage({ type: "requestOllamaModels", text: apiConfiguration?.ollamaBaseUrl })
@@ -75,34 +76,29 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage, fromWelcomeView }: A
 			vscode.postMessage({ type: "requestVsCodeLmModels" })
 		}
 	}, [selectedProvider, apiConfiguration?.ollamaBaseUrl, apiConfiguration?.lmStudioBaseUrl])
+	// Debounced model updates, only executed 250ms after the user stops typing
 	useEffect(() => {
-		if (selectedProvider === "ollama" || selectedProvider === "lmstudio" || selectedProvider === "vscode-lm") {
-			requestLocalModels()
+		if (requestLocalModelsTimeoutRef.current) {
+			clearTimeout(requestLocalModelsTimeoutRef.current)
 		}
-	}, [selectedProvider, requestLocalModels])
-	useInterval(
-		requestLocalModels,
-		selectedProvider === "ollama" || selectedProvider === "lmstudio" || selectedProvider === "vscode-lm"
-			? 2000
-			: null,
-	)
+		requestLocalModelsTimeoutRef.current = setTimeout(requestLocalModels, 250)
+		return () => {
+			if (requestLocalModelsTimeoutRef.current) {
+				clearTimeout(requestLocalModelsTimeoutRef.current)
+			}
+		}
+	}, [requestLocalModels])
 	const handleMessage = useCallback((event: MessageEvent) => {
 		const message: ExtensionMessage = event.data
 		if (message.type === "ollamaModels" && Array.isArray(message.ollamaModels)) {
 			const newModels = message.ollamaModels
-			setOllamaModels((prevModels) => {
-				return JSON.stringify(prevModels) === JSON.stringify(newModels) ? prevModels : newModels
-			})
+			setOllamaModels(newModels)
 		} else if (message.type === "lmStudioModels" && Array.isArray(message.lmStudioModels)) {
 			const newModels = message.lmStudioModels
-			setLmStudioModels((prevModels) => {
-				return JSON.stringify(prevModels) === JSON.stringify(newModels) ? prevModels : newModels
-			})
+			setLmStudioModels(newModels)
 		} else if (message.type === "vsCodeLmModels" && Array.isArray(message.vsCodeLmModels)) {
 			const newModels = message.vsCodeLmModels
-			setVsCodeLmModels((prevModels) => {
-				return JSON.stringify(prevModels) === JSON.stringify(newModels) ? prevModels : newModels
-			})
+			setVsCodeLmModels(newModels)
 		}
 	}, [])
 	useEvent("message", handleMessage)
@@ -142,10 +138,7 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage, fromWelcomeView }: A
 					id="api-provider"
 					value={selectedProvider}
 					onChange={(value: unknown) => {
-						handleInputChange(
-							"apiProvider",
-							true,
-						)({
+						handleInputChange("apiProvider")({
 							target: {
 								value: (value as DropdownOption).value,
 							},
@@ -261,7 +254,7 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage, fromWelcomeView }: A
 						value={apiConfiguration?.requestyApiKey || ""}
 						style={{ width: "100%" }}
 						type="password"
-						onBlur={handleInputChange("requestyApiKey")}
+						onInput={handleInputChange("requestyApiKey")}
 						placeholder="Enter API Key...">
 						<span style={{ fontWeight: 500 }}>Requesty API Key</span>
 					</VSCodeTextField>
@@ -341,7 +334,7 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage, fromWelcomeView }: A
 								value={apiConfiguration?.mistralCodestralUrl || ""}
 								style={{ width: "100%", marginTop: "10px" }}
 								type="url"
-								onBlur={handleInputChange("mistralCodestralUrl")}
+								onInput={handleInputChange("mistralCodestralUrl")}
 								placeholder="Default: https://codestral.mistral.ai">
 								<span style={{ fontWeight: 500 }}>Codestral Base URL (Optional)</span>
 							</VSCodeTextField>
@@ -408,7 +401,7 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage, fromWelcomeView }: A
 									value={apiConfiguration?.openRouterBaseUrl || ""}
 									style={{ width: "100%", marginTop: 3 }}
 									type="url"
-									onBlur={handleInputChange("openRouterBaseUrl")}
+									onInput={handleInputChange("openRouterBaseUrl")}
 									placeholder="Default: https://openrouter.ai/api/v1"
 								/>
 							)}

+ 5 - 4
webview-ui/src/components/settings/SettingsView.tsx

@@ -77,10 +77,6 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
 		setApiErrorMessage(apiValidationResult)
 		setModelIdErrorMessage(modelIdValidationResult)
 		if (!apiValidationResult && !modelIdValidationResult) {
-			vscode.postMessage({
-				type: "apiConfiguration",
-				apiConfiguration,
-			})
 			vscode.postMessage({ type: "alwaysAllowReadOnly", bool: alwaysAllowReadOnly })
 			vscode.postMessage({ type: "alwaysAllowWrite", bool: alwaysAllowWrite })
 			vscode.postMessage({ type: "alwaysAllowExecute", bool: alwaysAllowExecute })
@@ -195,6 +191,11 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
 							currentApiConfigName={currentApiConfigName}
 							listApiConfigMeta={listApiConfigMeta}
 							onSelectConfig={(configName: string) => {
+								vscode.postMessage({
+									type: "saveApiConfiguration",
+									text: currentApiConfigName,
+									apiConfiguration,
+								})
 								vscode.postMessage({
 									type: "loadApiConfiguration",
 									text: configName,

+ 6 - 2
webview-ui/src/components/welcome/WelcomeView.tsx

@@ -6,7 +6,7 @@ import { vscode } from "../../utils/vscode"
 import ApiOptions from "../settings/ApiOptions"
 
 const WelcomeView = () => {
-	const { apiConfiguration } = useExtensionState()
+	const { apiConfiguration, currentApiConfigName } = useExtensionState()
 
 	const [errorMessage, setErrorMessage] = useState<string | undefined>(undefined)
 
@@ -17,7 +17,11 @@ const WelcomeView = () => {
 			return
 		}
 		setErrorMessage(undefined)
-		vscode.postMessage({ type: "apiConfiguration", apiConfiguration })
+		vscode.postMessage({
+			type: "upsertApiConfiguration",
+			text: currentApiConfigName,
+			apiConfiguration,
+		})
 	}
 
 	return (

+ 4 - 18
webview-ui/src/context/ExtensionStateContext.tsx

@@ -162,26 +162,12 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
 	const handleInputChange = useCallback(
 		// Returns a function that handles an input change event for a specific API configuration field.
 		// The optional "softUpdate" flag determines whether to immediately update local state or send an external update.
-		(field: keyof ApiConfiguration, softUpdate?: boolean) => (event: any) => {
+		(field: keyof ApiConfiguration) => (event: any) => {
 			// Use the functional form of setState to ensure the latest state is used in the update logic.
 			setState((currentState) => {
-				if (softUpdate) {
-					// Return a new state object with the updated apiConfiguration.
-					// This will trigger a re-render with the new configuration value.
-					return {
-						...currentState,
-						apiConfiguration: { ...currentState.apiConfiguration, [field]: event.target.value },
-					}
-				} else {
-					// For non-soft updates, send a message to the VS Code extension with the updated config.
-					// This side effect communicates the change without updating local React state.
-					vscode.postMessage({
-						type: "upsertApiConfiguration",
-						text: currentState.currentApiConfigName,
-						apiConfiguration: { ...currentState.apiConfiguration, [field]: event.target.value },
-					})
-					// Return the unchanged state as no local state update is intended in this branch.
-					return currentState
+				return {
+					...currentState,
+					apiConfiguration: { ...currentState.apiConfiguration, [field]: event.target.value },
 				}
 			})
 		},