diff --git a/src/core/config/ConfigManager.ts b/src/core/config/ConfigManager.ts index 467eeb3d26..dd4262ee2e 100644 --- a/src/core/config/ConfigManager.ts +++ b/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(cb: () => Promise) { + const next = this._lock.then(cb) + this._lock = next.catch(() => {}) as Promise + return next + } /** * Initialize config if it doesn't exist */ async initConfig(): Promise { try { - 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 + return await this.lock(async () => { + const config = await this.readConfig() + if (!config) { + await this.writeConfig(this.defaultConfig) + return } - } - if (needsMigration) { - await this.writeConfig(config) - } + // 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) + } + }) } catch (error) { throw new Error(`Failed to initialize config: ${error}`) } @@ -66,12 +75,14 @@ export class ConfigManager { */ async listConfig(): Promise { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { - await this.context.secrets.delete(this.getConfigKey()) + return await this.lock(async () => { + await this.context.secrets.delete(this.getConfigKey()) + }) } private async readConfig(): Promise { diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 4375cf4da1..fd7798bcad 100644 --- a/src/core/webview/ClineProvider.ts +++ b/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) } diff --git a/src/core/webview/__tests__/ClineProvider.test.ts b/src/core/webview/__tests__/ClineProvider.test.ts index 0f6612caa9..0a8f73308f 100644 --- a/src/core/webview/__tests__/ClineProvider.test.ts +++ b/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" }, + ]) + }) }) }) diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index aadbe0167c..4dde38dfcd 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -12,6 +12,7 @@ export interface WebviewMessage { type: | "apiConfiguration" | "currentApiConfigName" + | "saveApiConfiguration" | "upsertApiConfiguration" | "deleteApiConfiguration" | "loadApiConfiguration" diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index 00f9455f3c..c4214939d5 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/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(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..."> Requesty API Key @@ -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"> Codestral Base URL (Optional) @@ -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" /> )} diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx index 829df5258f..bef57d1f5a 100644 --- a/webview-ui/src/components/settings/SettingsView.tsx +++ b/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, diff --git a/webview-ui/src/components/welcome/WelcomeView.tsx b/webview-ui/src/components/welcome/WelcomeView.tsx index db52612b90..54712c0e6a 100644 --- a/webview-ui/src/components/welcome/WelcomeView.tsx +++ b/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(undefined) @@ -17,7 +17,11 @@ const WelcomeView = () => { return } setErrorMessage(undefined) - vscode.postMessage({ type: "apiConfiguration", apiConfiguration }) + vscode.postMessage({ + type: "upsertApiConfiguration", + text: currentApiConfigName, + apiConfiguration, + }) } return ( diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index d7ccc7d56c..8f4fda94c3 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/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 }, } }) },