diff --git a/src/core/config/ConfigManager.ts b/src/core/config/ConfigManager.ts index 960babe..562ecb3 100644 --- a/src/core/config/ConfigManager.ts +++ b/src/core/config/ConfigManager.ts @@ -125,6 +125,18 @@ export class ConfigManager { } } + /** + * Check if a config exists by name + */ + async HasConfig(name: string): Promise { + try { + const config = await this.readConfig() + return name in config.apiConfigs + } catch (error) { + throw new Error(`Failed to check config existence: ${error}`) + } + } + private async readConfig(): Promise { try { const configKey = `${this.SCOPE_PREFIX}api_config` diff --git a/src/core/config/__tests__/ConfigManager.test.ts b/src/core/config/__tests__/ConfigManager.test.ts index a6527ab..f185ede 100644 --- a/src/core/config/__tests__/ConfigManager.test.ts +++ b/src/core/config/__tests__/ConfigManager.test.ts @@ -1,7 +1,6 @@ import { ExtensionContext } from 'vscode' -import { ConfigManager } from '../ConfigManager' +import { ConfigManager, ApiConfigData } from '../ConfigManager' import { ApiConfiguration } from '../../../shared/api' -import { ApiConfigData } from '../ConfigManager' // Mock VSCode ExtensionContext const mockSecrets = { @@ -345,4 +344,41 @@ describe('ConfigManager', () => { ) }) }) + + describe('HasConfig', () => { + it('should return true for existing config', async () => { + const existingConfig: ApiConfigData = { + currentApiConfigName: 'default', + apiConfigs: { + default: {}, + test: { + apiProvider: 'anthropic' + } + } + } + + mockSecrets.get.mockResolvedValue(JSON.stringify(existingConfig)) + + const hasConfig = await configManager.HasConfig('test') + expect(hasConfig).toBe(true) + }) + + it('should return false for non-existent config', async () => { + mockSecrets.get.mockResolvedValue(JSON.stringify({ + currentApiConfigName: 'default', + apiConfigs: { default: {} } + })) + + const hasConfig = await configManager.HasConfig('nonexistent') + expect(hasConfig).toBe(false) + }) + + it('should throw error if secrets storage fails', async () => { + mockSecrets.get.mockRejectedValue(new Error('Storage failed')) + + await expect(configManager.HasConfig('test')).rejects.toThrow( + 'Failed to check config existence: Error: Failed to read config from secrets: Error: Storage failed' + ) + }) + }) }) \ No newline at end of file diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 97d3521..94b8ea5 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -45,7 +45,6 @@ type SecretKey = | "geminiApiKey" | "openAiNativeApiKey" | "deepSeekApiKey" - | "apiConfigPassword" type GlobalStateKey = | "apiProvider" | "apiModelId" @@ -428,15 +427,37 @@ export class ClineProvider implements vscode.WebviewViewProvider { if (listApiConfig.length === 1) { // check if first time init then sync with exist config - if (!checkExistKey(listApiConfig[0]) && listApiConfig[0].name === "default") { + if (!checkExistKey(listApiConfig[0])) { const { apiConfiguration, } = await this.getState() - await this.configManager.SaveConfig("default", apiConfiguration) + await this.configManager.SaveConfig(listApiConfig[0].name ?? "default", apiConfiguration) listApiConfig[0].apiProvider = apiConfiguration.apiProvider } } + let currentConfigName = await this.getGlobalState("currentApiConfigName") as string + + if (currentConfigName) { + if (!await this.configManager.HasConfig(currentConfigName)) { + // current config name not valid, get first config in list + await this.updateGlobalState("currentApiConfigName", listApiConfig?.[0]?.name) + if (listApiConfig?.[0]?.name) { + const apiConfig = await this.configManager.LoadConfig(listApiConfig?.[0]?.name); + + await Promise.all([ + this.updateGlobalState("listApiConfigMeta", listApiConfig), + this.postMessageToWebview({ type: "listApiConfig", listApiConfig }), + this.updateApiConfiguration(apiConfig), + ]) + await this.postStateToWebview() + return + } + + } + } + + await Promise.all( [ await this.updateGlobalState("listApiConfigMeta", listApiConfig), @@ -785,6 +806,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { let listApiConfig = await this.configManager.ListConfig(); await Promise.all([ + this.updateApiConfiguration(message.apiConfiguration), this.updateGlobalState("currentApiConfigName", message.text), this.updateGlobalState("listApiConfigMeta", listApiConfig), ]) @@ -800,7 +822,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { if (message.values && message.apiConfiguration) { try { - const {oldName, newName} = message.values + const { oldName, newName } = message.values await this.configManager.SaveConfig(newName, message.apiConfiguration); @@ -839,17 +861,37 @@ export class ClineProvider implements vscode.WebviewViewProvider { break case "deleteApiConfiguration": if (message.text) { + + const answer = await vscode.window.showInformationMessage( + "What would you like to delete this api config?", + { modal: true }, + "Yes", + "No", + ) + + if (answer === "No" || answer === undefined) { + break + } + try { await this.configManager.DeleteConfig(message.text); - let currentApiConfigName = (await this.getGlobalState("currentApiConfigName") as string) ?? "default" + let listApiConfig = await this.configManager.ListConfig() + let currentApiConfigName = await this.getGlobalState("currentApiConfigName") if (message.text === currentApiConfigName) { - await this.updateGlobalState("currentApiConfigName", "default") + await this.updateGlobalState("currentApiConfigName", listApiConfig?.[0]?.name) + if (listApiConfig?.[0]?.name) { + const apiConfig = await this.configManager.LoadConfig(listApiConfig?.[0]?.name); + + await Promise.all([ + this.updateGlobalState("listApiConfigMeta", listApiConfig), + this.updateApiConfiguration(apiConfig), + ]) + await this.postStateToWebview() + } } - let listApiConfig = await this.configManager.ListConfig(); - await this.updateGlobalState("listApiConfigMeta", listApiConfig) - this.postMessageToWebview({ type: "listApiConfig", listApiConfig }) + // this.postMessageToWebview({ type: "listApiConfig", listApiConfig }) } catch (error) { console.error("Error delete api configuration:", error) @@ -867,16 +909,6 @@ export class ClineProvider implements vscode.WebviewViewProvider { vscode.window.showErrorMessage("Failed to get list api configuration") } break - case "setApiConfigPassword": - if (message.text) { - try { - await this.storeSecret("apiConfigPassword", message.text !== "" ? message.text : undefined) - } catch (error) { - console.error("Error set apiKey password:", error) - vscode.window.showErrorMessage("Failed to set apiKey password") - } - } - break } }, null, @@ -1398,7 +1430,6 @@ export class ClineProvider implements vscode.WebviewViewProvider { requestDelaySeconds, currentApiConfigName, listApiConfigMeta, - apiKeyPassword } = await this.getState() const allowedCommands = vscode.workspace @@ -1435,7 +1466,6 @@ export class ClineProvider implements vscode.WebviewViewProvider { requestDelaySeconds: requestDelaySeconds ?? 5, currentApiConfigName: currentApiConfigName ?? "default", listApiConfigMeta: listApiConfigMeta ?? [], - apiKeyPassword: apiKeyPassword ?? "" } } @@ -1545,7 +1575,6 @@ export class ClineProvider implements vscode.WebviewViewProvider { requestDelaySeconds, currentApiConfigName, listApiConfigMeta, - apiKeyPassword, ] = await Promise.all([ this.getGlobalState("apiProvider") as Promise, this.getGlobalState("apiModelId") as Promise, @@ -1600,7 +1629,6 @@ export class ClineProvider implements vscode.WebviewViewProvider { this.getGlobalState("requestDelaySeconds") as Promise, this.getGlobalState("currentApiConfigName") as Promise, this.getGlobalState("listApiConfigMeta") as Promise, - this.getSecret("apiConfigPassword") as Promise, ]) let apiProvider: ApiProvider @@ -1699,7 +1727,6 @@ export class ClineProvider implements vscode.WebviewViewProvider { requestDelaySeconds: requestDelaySeconds ?? 5, currentApiConfigName: currentApiConfigName ?? "default", listApiConfigMeta: listApiConfigMeta ?? [], - apiKeyPassword: apiKeyPassword ?? "" } } @@ -1777,7 +1804,6 @@ export class ClineProvider implements vscode.WebviewViewProvider { "geminiApiKey", "openAiNativeApiKey", "deepSeekApiKey", - "apiConfigPassword" ] for (const key of secretKeys) { await this.storeSecret(key, undefined) diff --git a/webview-ui/src/components/settings/ApiConfigManager.tsx b/webview-ui/src/components/settings/ApiConfigManager.tsx index b6cf5dd..e69865d 100644 --- a/webview-ui/src/components/settings/ApiConfigManager.tsx +++ b/webview-ui/src/components/settings/ApiConfigManager.tsx @@ -12,8 +12,8 @@ interface ApiConfigManagerProps { } const ApiConfigManager = ({ - currentApiConfigName, - listApiConfigMeta, + currentApiConfigName = "", + listApiConfigMeta = [], onSelectConfig, onDeleteConfig, onRenameConfig, diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index f38a0a3..3ddb55f 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -46,9 +46,10 @@ interface ApiOptionsProps { showModelOptions: boolean apiErrorMessage?: string modelIdErrorMessage?: string + onSelectProvider: (apiProvider: any) => void } -const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) => { +const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, onSelectProvider }: ApiOptionsProps) => { const { apiConfiguration, setApiConfiguration, uriScheme } = useExtensionState() const [ollamaModels, setOllamaModels] = useState([]) const [lmStudioModels, setLmStudioModels] = useState([]) @@ -130,7 +131,10 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage }: { + onSelectProvider(event.target.value); + handleInputChange("apiProvider")(event); + }} style={{ minWidth: 130, position: "relative", zIndex: OPENROUTER_MODEL_PICKER_Z_INDEX + 1 }}> OpenRouter Anthropic diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx index 6a0883c..0567d92 100644 --- a/webview-ui/src/components/settings/SettingsView.tsx +++ b/webview-ui/src/components/settings/SettingsView.tsx @@ -183,7 +183,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => { onRenameConfig={(oldName: string, newName: string) => { vscode.postMessage({ type: "renameApiConfiguration", - values: {oldName, newName}, + values: { oldName, newName }, apiConfiguration }) }} @@ -199,6 +199,16 @@ const SettingsView = ({ onDone }: SettingsViewProps) => { showModelOptions={true} apiErrorMessage={apiErrorMessage} modelIdErrorMessage={modelIdErrorMessage} + onSelectProvider={(apiProvider: any) => { + vscode.postMessage({ + type: "upsertApiConfiguration", + text: currentApiConfigName, + apiConfiguration: { + ...apiConfiguration, + apiProvider: apiProvider, + } + }) + }} /> diff --git a/webview-ui/src/components/welcome/WelcomeView.tsx b/webview-ui/src/components/welcome/WelcomeView.tsx index ef15a4e..fdaedb9 100644 --- a/webview-ui/src/components/welcome/WelcomeView.tsx +++ b/webview-ui/src/components/welcome/WelcomeView.tsx @@ -38,7 +38,7 @@ const WelcomeView = () => { To get started, this extension needs an API provider for Claude 3.5 Sonnet.
- + {}} /> Let's go!