Просмотр исходного кода

feat: config manager using secret store

sam hoang 1 год назад
Родитель
Сommit
352f34d8ce

+ 153 - 0
src/core/config/ConfigManager.ts

@@ -0,0 +1,153 @@
+import { ExtensionContext } from 'vscode'
+import { ApiConfiguration } from '../../shared/api'
+import { ApiConfigMeta } from '../../shared/ExtensionMessage'
+
+export interface ApiConfigData {
+  currentApiConfigName: string
+  apiConfigs: {
+    [key: string]: ApiConfiguration
+  }
+}
+
+export class ConfigManager {
+  private readonly defaultConfig: ApiConfigData = {
+    currentApiConfigName: 'default',
+    apiConfigs: {
+      default: {}
+    }
+  }
+  private readonly SCOPE_PREFIX = "cline_config_"
+  private readonly context: ExtensionContext
+
+  constructor(context: ExtensionContext) {
+    this.context = context
+  }
+
+  /**
+   * Initialize config if it doesn't exist
+   */
+  async initConfig(): Promise<void> {
+    try {
+      const config = await this.readConfig()
+      console.log("config", config)
+      if (!config) {
+        await this.writeConfig(this.defaultConfig)
+      }
+    } catch (error) {
+      throw new Error(`Failed to initialize config: ${error}`)
+    }
+  }
+
+  /**
+   * List all available configs with metadata
+   */
+  async ListConfig(): Promise<ApiConfigMeta[]> {
+    try {
+      const config = await this.readConfig()
+      return Object.entries(config.apiConfigs).map(([name, apiConfig]) => ({
+        name,
+        apiProvider: apiConfig.apiProvider,
+      }))
+    } catch (error) {
+      throw new Error(`Failed to list configs: ${error}`)
+    }
+  }
+
+  /**
+   * Save a config with the given name
+   */
+  async SaveConfig(name: string, config: ApiConfiguration): Promise<void> {
+    try {
+      const currentConfig = await this.readConfig()
+      currentConfig.apiConfigs[name] = config
+      await this.writeConfig(currentConfig)
+    } catch (error) {
+      throw new Error(`Failed to save config: ${error}`)
+    }
+  }
+
+  /**
+   * Load a config by name
+   */
+  async LoadConfig(name: string): Promise<ApiConfiguration> {
+    try {
+      const config = await this.readConfig()
+      const apiConfig = config.apiConfigs[name]
+      
+      if (!apiConfig) {
+        throw new Error(`Config '${name}' not found`)
+      }
+      
+      config.currentApiConfigName = name;
+      await this.writeConfig(config)
+      
+      return apiConfig
+    } catch (error) {
+      throw new Error(`Failed to load config: ${error}`)
+    }
+  }
+
+  /**
+   * Delete a config by name
+   */
+  async DeleteConfig(name: string): Promise<void> {
+    try {
+      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.`)
+      }
+
+      delete currentConfig.apiConfigs[name]
+      await this.writeConfig(currentConfig)
+    } catch (error) {
+      throw new Error(`Failed to delete config: ${error}`)
+    }
+  }
+
+  /**
+   * Set the current active API configuration
+   */
+  async SetCurrentConfig(name: string): Promise<void> {
+    try {
+      const currentConfig = await this.readConfig()
+      if (!currentConfig.apiConfigs[name]) {
+        throw new Error(`Config '${name}' not found`)
+      }
+
+      currentConfig.currentApiConfigName = name
+      await this.writeConfig(currentConfig)
+    } catch (error) {
+      throw new Error(`Failed to set current config: ${error}`)
+    }
+  }
+
+  private async readConfig(): Promise<ApiConfigData> {
+    try {
+      const configKey = `${this.SCOPE_PREFIX}api_config`
+      const content = await this.context.secrets.get(configKey)
+      
+      if (!content) {
+        return this.defaultConfig
+      }
+
+      return JSON.parse(content)
+    } catch (error) {
+      throw new Error(`Failed to read config from secrets: ${error}`)
+    }
+  }
+
+  private async writeConfig(config: ApiConfigData): Promise<void> {
+    try {
+      const configKey = `${this.SCOPE_PREFIX}api_config`
+      const content = JSON.stringify(config, null, 2)
+      await this.context.secrets.store(configKey, content)
+    } catch (error) {
+      throw new Error(`Failed to write config to secrets: ${error}`)
+    }
+  }
+}

+ 348 - 0
src/core/config/__tests__/ConfigManager.test.ts

@@ -0,0 +1,348 @@
+import { ExtensionContext } from 'vscode'
+import { ConfigManager } from '../ConfigManager'
+import { ApiConfiguration } from '../../../shared/api'
+import { ApiConfigData } from '../ConfigManager'
+
+// Mock VSCode ExtensionContext
+const mockSecrets = {
+  get: jest.fn(),
+  store: jest.fn(),
+  delete: jest.fn()
+}
+
+const mockContext = {
+  secrets: mockSecrets
+} as unknown as ExtensionContext
+
+describe('ConfigManager', () => {
+  let configManager: ConfigManager
+
+  beforeEach(() => {
+    jest.clearAllMocks()
+    configManager = new ConfigManager(mockContext)
+  })
+
+  describe('initConfig', () => {
+    it('should not write to storage when secrets.get returns null', async () => {
+      // Mock readConfig to return null
+      mockSecrets.get.mockResolvedValueOnce(null)
+
+      await configManager.initConfig()
+
+      // Should not write to storage because readConfig returns defaultConfig
+      expect(mockSecrets.store).not.toHaveBeenCalled()
+    })
+
+    it('should not initialize config if it exists', async () => {
+      mockSecrets.get.mockResolvedValue(JSON.stringify({
+        currentApiConfigName: 'default',
+        apiConfigs: {
+          default: {}
+        }
+      }))
+
+      await configManager.initConfig()
+
+      expect(mockSecrets.store).not.toHaveBeenCalled()
+    })
+
+    it('should throw error if secrets storage fails', async () => {
+      mockSecrets.get.mockRejectedValue(new Error('Storage failed'))
+
+      await expect(configManager.initConfig()).rejects.toThrow(
+        'Failed to initialize config: Error: Failed to read config from secrets: Error: Storage failed'
+      )
+    })
+  })
+
+  describe('ListConfig', () => {
+    it('should list all available configs', async () => {
+      const existingConfig: ApiConfigData = {
+        currentApiConfigName: 'default',
+        apiConfigs: {
+          default: {},
+          test: {
+            apiProvider: 'anthropic'
+          }
+        }
+      }
+
+      mockSecrets.get.mockResolvedValue(JSON.stringify(existingConfig))
+
+      const configs = await configManager.ListConfig()
+      expect(configs).toEqual([
+        { name: 'default', apiProvider: undefined },
+        { name: 'test', apiProvider: 'anthropic' }
+      ])
+    })
+
+    it('should handle empty config file', async () => {
+      const emptyConfig: ApiConfigData = {
+        currentApiConfigName: 'default',
+        apiConfigs: {}
+      }
+
+      mockSecrets.get.mockResolvedValue(JSON.stringify(emptyConfig))
+
+      const configs = await configManager.ListConfig()
+      expect(configs).toEqual([])
+    })
+
+    it('should throw error if reading from secrets fails', async () => {
+      mockSecrets.get.mockRejectedValue(new Error('Read failed'))
+
+      await expect(configManager.ListConfig()).rejects.toThrow(
+        'Failed to list configs: Error: Failed to read config from secrets: Error: Read failed'
+      )
+    })
+  })
+
+  describe('SaveConfig', () => {
+    it('should save new config', async () => {
+      mockSecrets.get.mockResolvedValue(JSON.stringify({
+        currentApiConfigName: 'default',
+        apiConfigs: {
+          default: {}
+        }
+      }))
+
+      const newConfig: ApiConfiguration = {
+        apiProvider: 'anthropic',
+        apiKey: 'test-key'
+      }
+
+      await configManager.SaveConfig('test', newConfig)
+
+      const expectedConfig = {
+        currentApiConfigName: 'default',
+        apiConfigs: {
+          default: {},
+          test: newConfig
+        }
+      }
+
+      expect(mockSecrets.store).toHaveBeenCalledWith(
+        'cline_config_api_config',
+        JSON.stringify(expectedConfig, null, 2)
+      )
+    })
+
+    it('should update existing config', async () => {
+      const existingConfig: ApiConfigData = {
+        currentApiConfigName: 'default',
+        apiConfigs: {
+          test: {
+            apiProvider: 'anthropic',
+            apiKey: 'old-key'
+          }
+        }
+      }
+
+      mockSecrets.get.mockResolvedValue(JSON.stringify(existingConfig))
+
+      const updatedConfig: ApiConfiguration = {
+        apiProvider: 'anthropic',
+        apiKey: 'new-key'
+      }
+
+      await configManager.SaveConfig('test', updatedConfig)
+
+      const expectedConfig = {
+        currentApiConfigName: 'default',
+        apiConfigs: {
+          test: updatedConfig
+        }
+      }
+
+      expect(mockSecrets.store).toHaveBeenCalledWith(
+        'cline_config_api_config',
+        JSON.stringify(expectedConfig, null, 2)
+      )
+    })
+
+    it('should throw error if secrets storage fails', async () => {
+      mockSecrets.get.mockResolvedValue(JSON.stringify({
+        currentApiConfigName: 'default',
+        apiConfigs: { default: {} }
+      }))
+      mockSecrets.store.mockRejectedValueOnce(new Error('Storage failed'))
+
+      await expect(configManager.SaveConfig('test', {})).rejects.toThrow(
+        'Failed to save config: Error: Failed to write config to secrets: Error: Storage failed'
+      )
+    })
+  })
+
+  describe('DeleteConfig', () => {
+    it('should delete existing config', async () => {
+      const existingConfig: ApiConfigData = {
+        currentApiConfigName: 'default',
+        apiConfigs: {
+          default: {},
+          test: {
+            apiProvider: 'anthropic'
+          }
+        }
+      }
+
+      mockSecrets.get.mockResolvedValue(JSON.stringify(existingConfig))
+
+      await configManager.DeleteConfig('test')
+
+      const expectedConfig = {
+        currentApiConfigName: 'default',
+        apiConfigs: {
+          default: {}
+        }
+      }
+
+      expect(mockSecrets.store).toHaveBeenCalledWith(
+        'cline_config_api_config',
+        JSON.stringify(expectedConfig, null, 2)
+      )
+    })
+
+    it('should throw error when trying to delete non-existent config', async () => {
+      mockSecrets.get.mockResolvedValue(JSON.stringify({
+        currentApiConfigName: 'default',
+        apiConfigs: { default: {} }
+      }))
+
+      await expect(configManager.DeleteConfig('nonexistent')).rejects.toThrow(
+        "Config 'nonexistent' not found"
+      )
+    })
+
+    it('should throw error when trying to delete last remaining config', async () => {
+      mockSecrets.get.mockResolvedValue(JSON.stringify({
+        currentApiConfigName: 'default',
+        apiConfigs: { default: {} }
+      }))
+
+      await expect(configManager.DeleteConfig('default')).rejects.toThrow(
+        'Cannot delete the last remaining configuration.'
+      )
+    })
+  })
+
+  describe('LoadConfig', () => {
+    it('should load config and update current config name', async () => {
+      const existingConfig: ApiConfigData = {
+        currentApiConfigName: 'default',
+        apiConfigs: {
+          test: {
+            apiProvider: 'anthropic',
+            apiKey: 'test-key'
+          }
+        }
+      }
+
+      mockSecrets.get.mockResolvedValue(JSON.stringify(existingConfig))
+
+      const config = await configManager.LoadConfig('test')
+
+      expect(config).toEqual({
+        apiProvider: 'anthropic',
+        apiKey: 'test-key'
+      })
+
+      const expectedConfig = {
+        currentApiConfigName: 'test',
+        apiConfigs: {
+          test: {
+            apiProvider: 'anthropic',
+            apiKey: 'test-key'
+          }
+        }
+      }
+
+      expect(mockSecrets.store).toHaveBeenCalledWith(
+        'cline_config_api_config',
+        JSON.stringify(expectedConfig, null, 2)
+      )
+    })
+
+    it('should throw error when config does not exist', async () => {
+      mockSecrets.get.mockResolvedValue(JSON.stringify({
+        currentApiConfigName: 'default',
+        apiConfigs: { default: {} }
+      }))
+
+      await expect(configManager.LoadConfig('nonexistent')).rejects.toThrow(
+        "Config 'nonexistent' not found"
+      )
+    })
+
+    it('should throw error if secrets storage fails', async () => {
+      mockSecrets.get.mockResolvedValue(JSON.stringify({
+        currentApiConfigName: 'default',
+        apiConfigs: {
+          test: { apiProvider: 'anthropic' }
+        }
+      }))
+      mockSecrets.store.mockRejectedValueOnce(new Error('Storage failed'))
+
+      await expect(configManager.LoadConfig('test')).rejects.toThrow(
+        'Failed to load config: Error: Failed to write config to secrets: Error: Storage failed'
+      )
+    })
+  })
+
+  describe('SetCurrentConfig', () => {
+    it('should set current config', async () => {
+      const existingConfig: ApiConfigData = {
+        currentApiConfigName: 'default',
+        apiConfigs: {
+          default: {},
+          test: {
+            apiProvider: 'anthropic'
+          }
+        }
+      }
+
+      mockSecrets.get.mockResolvedValue(JSON.stringify(existingConfig))
+
+      await configManager.SetCurrentConfig('test')
+
+      const expectedConfig = {
+        currentApiConfigName: 'test',
+        apiConfigs: {
+          default: {},
+          test: {
+            apiProvider: 'anthropic'
+          }
+        }
+      }
+
+      expect(mockSecrets.store).toHaveBeenCalledWith(
+        'cline_config_api_config',
+        JSON.stringify(expectedConfig, null, 2)
+      )
+    })
+
+    it('should throw error when config does not exist', async () => {
+      mockSecrets.get.mockResolvedValue(JSON.stringify({
+        currentApiConfigName: 'default',
+        apiConfigs: { default: {} }
+      }))
+
+      await expect(configManager.SetCurrentConfig('nonexistent')).rejects.toThrow(
+        "Config 'nonexistent' not found"
+      )
+    })
+
+    it('should throw error if secrets storage fails', async () => {
+      mockSecrets.get.mockResolvedValue(JSON.stringify({
+        currentApiConfigName: 'default',
+        apiConfigs: {
+          test: { apiProvider: 'anthropic' }
+        }
+      }))
+      mockSecrets.store.mockRejectedValueOnce(new Error('Storage failed'))
+
+      await expect(configManager.SetCurrentConfig('test')).rejects.toThrow(
+        'Failed to set current config: Error: Failed to write config to secrets: Error: Storage failed'
+      )
+    })
+  })
+})

+ 232 - 76
src/core/webview/ClineProvider.ts

@@ -12,9 +12,9 @@ import { selectImages } from "../../integrations/misc/process-images"
 import { getTheme } from "../../integrations/theme/getTheme"
 import WorkspaceTracker from "../../integrations/workspace/WorkspaceTracker"
 import { McpHub } from "../../services/mcp/McpHub"
-import { ApiProvider, ModelInfo } from "../../shared/api"
+import { ApiConfiguration, ApiProvider, ModelInfo } from "../../shared/api"
 import { findLast } from "../../shared/array"
-import { ExtensionMessage } from "../../shared/ExtensionMessage"
+import { ApiConfigMeta, ExtensionMessage } from "../../shared/ExtensionMessage"
 import { HistoryItem } from "../../shared/HistoryItem"
 import { WebviewMessage } from "../../shared/WebviewMessage"
 import { fileExistsAtPath } from "../../utils/fs"
@@ -23,8 +23,10 @@ import { openMention } from "../mentions"
 import { getNonce } from "./getNonce"
 import { getUri } from "./getUri"
 import { playSound, setSoundEnabled, setSoundVolume } from "../../utils/sound"
+import { checkExistKey } from "../../shared/checkExistApiConfig"
 import { enhancePrompt } from "../../utils/enhance-prompt"
 import { getCommitInfo, searchCommits, getWorkingState } from "../../utils/git"
+import { ConfigManager } from "../config/ConfigManager"
 
 /*
 https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
@@ -43,6 +45,7 @@ type SecretKey =
 	| "geminiApiKey"
 	| "openAiNativeApiKey"
 	| "deepSeekApiKey"
+	| "apiConfigPassword"
 type GlobalStateKey =
 	| "apiProvider"
 	| "apiModelId"
@@ -85,6 +88,9 @@ type GlobalStateKey =
 	| "mcpEnabled"
 	| "alwaysApproveResubmit"
 	| "requestDelaySeconds"
+	| "currentApiConfigName"
+	| "listApiConfigMeta"
+
 export const GlobalFileNames = {
 	apiConversationHistory: "api_conversation_history.json",
 	uiMessages: "ui_messages.json",
@@ -103,6 +109,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 	private workspaceTracker?: WorkspaceTracker
 	mcpHub?: McpHub
 	private latestAnnouncementId = "dec-10-2024" // update to some unique identifier when we add a new announcement
+	configManager: ConfigManager
 
 	constructor(
 		readonly context: vscode.ExtensionContext,
@@ -112,6 +119,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 		ClineProvider.activeInstances.add(this)
 		this.workspaceTracker = new WorkspaceTracker(this)
 		this.mcpHub = new McpHub(this)
+		this.configManager = new ConfigManager(this.context)
 	}
 
 	/*
@@ -235,7 +243,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			diffEnabled,
 			fuzzyMatchThreshold
 		} = await this.getState()
-		
+
 		this.cline = new Cline(
 			this,
 			apiConfiguration,
@@ -255,7 +263,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			diffEnabled,
 			fuzzyMatchThreshold
 		} = await this.getState()
-		
+
 		this.cline = new Cline(
 			this,
 			apiConfiguration,
@@ -321,15 +329,15 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 
 		// Use a nonce to only allow a specific script to be run.
 		/*
-        content security policy of your webview to only allow scripts that have a specific nonce
-        create a content security policy meta tag so that only loading scripts with a nonce is allowed
-        As your extension grows you will likely want to add custom styles, fonts, and/or images to your webview. If you do, you will need to update the content security policy meta tag to explicity allow for these resources. E.g.
-                <meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src ${webview.cspSource}; font-src ${webview.cspSource}; img-src ${webview.cspSource} https:; script-src 'nonce-${nonce}';">
+		content security policy of your webview to only allow scripts that have a specific nonce
+		create a content security policy meta tag so that only loading scripts with a nonce is allowed
+		As your extension grows you will likely want to add custom styles, fonts, and/or images to your webview. If you do, you will need to update the content security policy meta tag to explicity allow for these resources. E.g.
+				<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src ${webview.cspSource}; font-src ${webview.cspSource}; img-src ${webview.cspSource} https:; script-src 'nonce-${nonce}';">
 		- 'unsafe-inline' is required for styles due to vscode-webview-toolkit's dynamic style injection
 		- since we pass base64 images to the webview, we need to specify img-src ${webview.cspSource} data:;
 
-        in meta tag we add nonce attribute: A cryptographic nonce (only used once) to allow scripts. The server must generate a unique nonce value each time it transmits a policy. It is critical to provide a nonce that cannot be guessed as bypassing a resource's policy is otherwise trivial.
-        */
+		in meta tag we add nonce attribute: A cryptographic nonce (only used once) to allow scripts. The server must generate a unique nonce value each time it transmits a policy. It is critical to provide a nonce that cannot be guessed as bypassing a resource's policy is otherwise trivial.
+		*/
 		const nonce = getNonce()
 
 		// Tip: Install the es6-string-html VS Code extension to enable code highlighting below
@@ -410,6 +418,33 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 								}
 							}
 						})
+
+
+						this.configManager.ListConfig().then(async (listApiConfig) => {
+
+							if (!listApiConfig) {
+								return
+							}
+
+							if (listApiConfig.length === 1) {
+								// check if first time init then sync with exist config
+								if (!checkExistKey(listApiConfig[0]) && listApiConfig[0].name === "default") {
+									const {
+										apiConfiguration,
+									} = await this.getState()
+									await this.configManager.SaveConfig("default", apiConfiguration)
+									listApiConfig[0].apiProvider = apiConfiguration.apiProvider
+								}
+							}
+
+							await Promise.all(
+								[
+									await this.updateGlobalState("listApiConfigMeta", listApiConfig),
+									await this.postMessageToWebview({ type: "listApiConfig", listApiConfig })
+								]
+							)
+						}).catch(console.error);
+
 						break
 					case "newTask":
 						// Code that should run in response to the hello message command
@@ -424,70 +459,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 						break
 					case "apiConfiguration":
 						if (message.apiConfiguration) {
-							const {
-								apiProvider,
-								apiModelId,
-								apiKey,
-								glamaModelId,
-								glamaModelInfo,
-								glamaApiKey,
-								openRouterApiKey,
-								awsAccessKey,
-								awsSecretKey,
-								awsSessionToken,
-								awsRegion,
-								awsUseCrossRegionInference,
-								vertexProjectId,
-								vertexRegion,
-								openAiBaseUrl,
-								openAiApiKey,
-								openAiModelId,
-								ollamaModelId,
-								ollamaBaseUrl,
-								lmStudioModelId,
-								lmStudioBaseUrl,
-								anthropicBaseUrl,
-								geminiApiKey,
-								openAiNativeApiKey,
-								azureApiVersion,
-								openAiStreamingEnabled,
-								openRouterModelId,
-								openRouterModelInfo,
-								openRouterUseMiddleOutTransform,
-							} = message.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("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("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", message.apiConfiguration.deepSeekApiKey)
-							await this.updateGlobalState("azureApiVersion", azureApiVersion)
-							await this.updateGlobalState("openAiStreamingEnabled", openAiStreamingEnabled)
-							await this.updateGlobalState("openRouterModelId", openRouterModelId)
-							await this.updateGlobalState("openRouterModelInfo", openRouterModelInfo)
-							await this.updateGlobalState("openRouterUseMiddleOutTransform", openRouterUseMiddleOutTransform)
-							if (this.cline) {
-								this.cline.api = buildApiHandler(message.apiConfiguration)
-							}
+							await this.updateApiConfiguration(message.apiConfiguration)
 						}
 						await this.postStateToWebview()
 						break
@@ -566,7 +538,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 						if (message?.values?.baseUrl && message?.values?.apiKey) {
 							const openAiModels = await this.getOpenAiModels(message?.values?.baseUrl, message?.values?.apiKey)
 							this.postMessageToWebview({ type: "openAiModels", openAiModels })
-						}	
+						}
 						break
 					case "openImage":
 						openImage(message.text!)
@@ -805,6 +777,106 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 						}
 						break
 					}
+					case "upsertApiConfiguration":
+						if (message.text && message.apiConfiguration) {
+							try {
+								await this.configManager.SaveConfig(message.text, message.apiConfiguration);
+
+								let listApiConfig = await this.configManager.ListConfig();
+
+								await Promise.all([
+									this.updateGlobalState("currentApiConfigName", message.text),
+									this.updateGlobalState("listApiConfigMeta", listApiConfig),
+								])
+
+								this.postStateToWebview()
+							} catch (error) {
+								console.error("Error create new api configuration:", error)
+								vscode.window.showErrorMessage("Failed to create api configuration")
+							}
+						}
+						break
+					case "renameApiConfiguration":
+						if (message.values && message.apiConfiguration) {
+							try {
+
+								const {oldName, newName} = message.values
+
+								await this.configManager.SaveConfig(newName, message.apiConfiguration);
+
+								await this.configManager.DeleteConfig(oldName)
+
+								let listApiConfig = await this.configManager.ListConfig();
+
+								await Promise.all([
+									this.updateGlobalState("currentApiConfigName", newName),
+									this.updateGlobalState("listApiConfigMeta", listApiConfig),
+								])
+
+								this.postStateToWebview()
+							} catch (error) {
+								console.error("Error create new api configuration:", error)
+								vscode.window.showErrorMessage("Failed to create api configuration")
+							}
+						}
+						break
+					case "loadApiConfiguration":
+						if (message.text) {
+							try {
+								const apiConfig = await this.configManager.LoadConfig(message.text);
+
+								await Promise.all([
+									this.updateGlobalState("currentApiConfigName", message.text),
+									this.updateApiConfiguration(apiConfig),
+								])
+
+								await this.postStateToWebview()
+							} catch (error) {
+								console.error("Error load api configuration:", error)
+								vscode.window.showErrorMessage("Failed to load api configuration")
+							}
+						}
+						break
+					case "deleteApiConfiguration":
+						if (message.text) {
+							try {
+								await this.configManager.DeleteConfig(message.text);
+								let currentApiConfigName = (await this.getGlobalState("currentApiConfigName") as string) ?? "default"
+
+								if (message.text === currentApiConfigName) {
+									await this.updateGlobalState("currentApiConfigName", "default")
+								}
+
+								let listApiConfig = await this.configManager.ListConfig();
+								await this.updateGlobalState("listApiConfigMeta", listApiConfig)
+								this.postMessageToWebview({ type: "listApiConfig", listApiConfig })
+
+							} catch (error) {
+								console.error("Error delete api configuration:", error)
+								vscode.window.showErrorMessage("Failed to delete api configuration")
+							}
+						}
+						break
+					case "getListApiConfiguration":
+						try {
+							let listApiConfig = await this.configManager.ListConfig();
+							await this.updateGlobalState("listApiConfigMeta", listApiConfig)
+							this.postMessageToWebview({ type: "listApiConfig", listApiConfig })
+						} catch (error) {
+							console.error("Error get list api configuration:", error)
+							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,
@@ -812,6 +884,74 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 		)
 	}
 
+	private async updateApiConfiguration(apiConfiguration: ApiConfiguration) {
+		const {
+			apiProvider,
+			apiModelId,
+			apiKey,
+			glamaModelId,
+			glamaModelInfo,
+			glamaApiKey,
+			openRouterApiKey,
+			awsAccessKey,
+			awsSecretKey,
+			awsSessionToken,
+			awsRegion,
+			awsUseCrossRegionInference,
+			vertexProjectId,
+			vertexRegion,
+			openAiBaseUrl,
+			openAiApiKey,
+			openAiModelId,
+			ollamaModelId,
+			ollamaBaseUrl,
+			lmStudioModelId,
+			lmStudioBaseUrl,
+			anthropicBaseUrl,
+			geminiApiKey,
+			openAiNativeApiKey,
+			deepSeekApiKey,
+			azureApiVersion,
+			openAiStreamingEnabled,
+			openRouterModelId,
+			openRouterModelInfo,
+			openRouterUseMiddleOutTransform,
+		} = 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("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("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("openRouterUseMiddleOutTransform", openRouterUseMiddleOutTransform)
+		if (this.cline) {
+			this.cline.api = buildApiHandler(apiConfiguration)
+		} 
+	}
+
 	async updateCustomInstructions(instructions?: string) {
 		// User may be clearing the field
 		await this.updateGlobalState("customInstructions", instructions || undefined)
@@ -1256,8 +1396,11 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			mcpEnabled,
 			alwaysApproveResubmit,
 			requestDelaySeconds,
+			currentApiConfigName,
+			listApiConfigMeta,
+			apiKeyPassword
 		} = await this.getState()
-		
+
 		const allowedCommands = vscode.workspace
 			.getConfiguration('roo-cline')
 			.get<string[]>('allowedCommands') || []
@@ -1290,6 +1433,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			mcpEnabled: mcpEnabled ?? true,
 			alwaysApproveResubmit: alwaysApproveResubmit ?? false,
 			requestDelaySeconds: requestDelaySeconds ?? 5,
+			currentApiConfigName: currentApiConfigName ?? "default",
+			listApiConfigMeta: listApiConfigMeta ?? [],
+			apiKeyPassword: apiKeyPassword ?? ""
 		}
 	}
 
@@ -1397,6 +1543,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			mcpEnabled,
 			alwaysApproveResubmit,
 			requestDelaySeconds,
+			currentApiConfigName,
+			listApiConfigMeta,
+			apiKeyPassword,
 		] = await Promise.all([
 			this.getGlobalState("apiProvider") as Promise<ApiProvider | undefined>,
 			this.getGlobalState("apiModelId") as Promise<string | undefined>,
@@ -1449,6 +1598,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			this.getGlobalState("mcpEnabled") as Promise<boolean | undefined>,
 			this.getGlobalState("alwaysApproveResubmit") as Promise<boolean | undefined>,
 			this.getGlobalState("requestDelaySeconds") as Promise<number | undefined>,
+			this.getGlobalState("currentApiConfigName") as Promise<string | undefined>,
+			this.getGlobalState("listApiConfigMeta") as Promise<ApiConfigMeta[] | undefined>,
+			this.getSecret("apiConfigPassword") as Promise<string | undefined>,
 		])
 
 		let apiProvider: ApiProvider
@@ -1545,6 +1697,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			mcpEnabled: mcpEnabled ?? true,
 			alwaysApproveResubmit: alwaysApproveResubmit ?? false,
 			requestDelaySeconds: requestDelaySeconds ?? 5,
+			currentApiConfigName: currentApiConfigName ?? "default",
+			listApiConfigMeta: listApiConfigMeta ?? [],
+			apiKeyPassword: apiKeyPassword ?? ""
 		}
 	}
 
@@ -1622,6 +1777,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			"geminiApiKey",
 			"openAiNativeApiKey",
 			"deepSeekApiKey",
+			"apiConfigPassword"
 		]
 		for (const key of secretKeys) {
 			await this.storeSecret(key, undefined)

+ 10 - 1
src/shared/ExtensionMessage.ts

@@ -1,6 +1,6 @@
 // type that represents json data that is sent from extension to webview, called ExtensionMessage and has 'type' enum which can be 'plusButtonClicked' or 'settingsButtonClicked' or 'hello'
 
-import { ApiConfiguration, ModelInfo } from "./api"
+import { ApiConfiguration, ApiProvider, ModelInfo } from "./api"
 import { HistoryItem } from "./HistoryItem"
 import { McpServer } from "./mcp"
 import { GitCommit } from "../utils/git"
@@ -23,6 +23,7 @@ export interface ExtensionMessage {
 		| "mcpServers"
 		| "enhancedPrompt"
 		| "commitSearchResults"
+		| "listApiConfig"
 	text?: string
 	action?:
 		| "chatButtonClicked"
@@ -42,6 +43,12 @@ export interface ExtensionMessage {
 	openAiModels?: string[]
 	mcpServers?: McpServer[]
 	commits?: GitCommit[]
+	listApiConfig?: ApiConfigMeta[]
+}
+
+export interface ApiConfigMeta {
+	name: string
+	apiProvider?: ApiProvider
 }
 
 export interface ExtensionState {
@@ -50,6 +57,8 @@ export interface ExtensionState {
 	taskHistory: HistoryItem[]
 	shouldShowAnnouncement: boolean
 	apiConfiguration?: ApiConfiguration
+	currentApiConfigName?: string
+	listApiConfigMeta?: ApiConfigMeta[]
 	customInstructions?: string
 	alwaysAllowReadOnly?: boolean
 	alwaysAllowWrite?: boolean

+ 7 - 0
src/shared/WebviewMessage.ts

@@ -5,6 +5,12 @@ export type AudioType = "notification" | "celebration" | "progress_loop"
 export interface WebviewMessage {
 	type:
 		| "apiConfiguration"
+		| "currentApiConfigName"
+		| "upsertApiConfiguration"
+		| "deleteApiConfiguration"
+		| "loadApiConfiguration"
+		| "renameApiConfiguration"
+		| "getListApiConfiguration"
 		| "customInstructions"
 		| "allowedCommands"
 		| "alwaysAllowReadOnly"
@@ -54,6 +60,7 @@ export interface WebviewMessage {
 		| "searchCommits"
 		| "alwaysApproveResubmit"
 		| "requestDelaySeconds"
+		| "setApiConfigPassword"
 	text?: string
 	disabled?: boolean
 	askResponse?: ClineAskResponse

+ 19 - 0
src/shared/checkExistApiConfig.ts

@@ -0,0 +1,19 @@
+import { ApiConfiguration } from "../shared/api";
+
+export function checkExistKey(config: ApiConfiguration | undefined) {
+	return config
+		? [
+			config.apiKey,
+			config.glamaApiKey,
+			config.openRouterApiKey,
+			config.awsRegion,
+			config.vertexProjectId,
+			config.openAiApiKey,
+			config.ollamaModelId,
+			config.lmStudioModelId,
+			config.geminiApiKey,
+			config.openAiNativeApiKey,
+			config.deepSeekApiKey
+		].some((key) => key !== undefined)
+		: false;
+}

+ 165 - 0
webview-ui/src/components/settings/ApiConfigManager.tsx

@@ -0,0 +1,165 @@
+import { VSCodeButton, VSCodeDivider, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
+import { memo, useState } from "react"
+import { ApiConfigMeta } from "../../../../src/shared/ExtensionMessage"
+
+interface ApiConfigManagerProps {
+    currentApiConfigName?: string
+    listApiConfigMeta?: ApiConfigMeta[]
+    onSelectConfig: (configName: string) => void
+    onDeleteConfig: (configName: string) => void
+    onRenameConfig: (oldName: string, newName: string) => void
+    onUpsertConfig: (configName: string) => void
+    // setDraftNewConfig: (mode: boolean) => void
+}
+
+const ApiConfigManager = ({
+    currentApiConfigName,
+    listApiConfigMeta,
+    onSelectConfig,
+    onDeleteConfig,
+    onRenameConfig,
+    onUpsertConfig,
+    // setDraftNewConfig,
+}: ApiConfigManagerProps) => {
+    const [isNewMode, setIsNewMode] = useState(false);
+    const [isRenameMode, setIsRenameMode] = useState(false);
+    const [newConfigName, setNewConfigName] = useState("");
+    const [renamedConfigName, setRenamedConfigName] = useState("");
+
+    const handleNewConfig = () => {
+        setIsNewMode(true);
+        setNewConfigName("");
+        // setDraftNewConfig(true)
+    };
+
+    const handleSaveNewConfig = () => {
+        if (newConfigName.trim()) {
+            onUpsertConfig(newConfigName.trim());
+            setIsNewMode(false);
+            setNewConfigName("");
+            // setDraftNewConfig(false)
+        }
+    };
+
+    const handleCancelNewConfig = () => {
+        setIsNewMode(false);
+        setNewConfigName("");
+        // setDraftNewConfig(false)
+    };
+
+    const handleStartRename = () => {
+        setIsRenameMode(true);
+        setRenamedConfigName(currentApiConfigName || "");
+    };
+
+    const handleSaveRename = () => {
+        if (renamedConfigName.trim() && currentApiConfigName) {
+            onRenameConfig(currentApiConfigName, renamedConfigName.trim());
+            setIsRenameMode(false);
+            setRenamedConfigName("");
+        }
+    };
+
+    const handleCancelRename = () => {
+        setIsRenameMode(false);
+        setRenamedConfigName("");
+    };
+
+    return (
+        <div>
+            <label style={{ fontWeight: "500", display: "block", marginBottom: 5 }}>
+                API Configuration
+            </label>
+            <div style={{ display: "flex", gap: "8px", alignItems: "center" }}>
+                {isNewMode ? (
+                    <>
+                        <VSCodeTextField
+                            value={newConfigName}
+                            onInput={(e: any) => setNewConfigName(e.target.value)}
+                            placeholder="Enter configuration name"
+                            style={{ flexGrow: 1 }}
+                        />
+                        <VSCodeButton
+                            appearance="secondary"
+                            disabled={!newConfigName.trim()}
+                            onClick={handleSaveNewConfig}
+                        >
+                            <span className="codicon codicon-check" /> Save
+                        </VSCodeButton>
+                        <VSCodeButton
+                            appearance="secondary"
+                            onClick={handleCancelNewConfig}
+                        >
+                            <span className="codicon codicon-close" /> Cancel
+                        </VSCodeButton>
+                    </>
+                ) : isRenameMode ? (
+                    <>
+                        <VSCodeTextField
+                            value={renamedConfigName}
+                            onInput={(e: any) => setRenamedConfigName(e.target.value)}
+                            placeholder="Enter new name"
+                            style={{ flexGrow: 1 }}
+                        />
+                        <VSCodeButton
+                            appearance="secondary"
+                            disabled={!renamedConfigName.trim()}
+                            onClick={handleSaveRename}
+                        >
+                            <span className="codicon codicon-check" /> Save
+                        </VSCodeButton>
+                        <VSCodeButton
+                            appearance="secondary"
+                            onClick={handleCancelRename}
+                        >
+                            <span className="codicon codicon-close" /> Cancel
+                        </VSCodeButton>
+                    </>
+                ) : (
+                    <>
+                        <select
+                            value={currentApiConfigName}
+                            onChange={(e) => onSelectConfig(e.target.value)}
+                            style={{
+                                flexGrow: 1,
+                                padding: "4px 8px",
+                                backgroundColor: "var(--vscode-input-background)",
+                                color: "var(--vscode-input-foreground)",
+                                border: "1px solid var(--vscode-input-border)",
+                                borderRadius: "2px",
+                                height: "28px"
+                            }}>
+                            {listApiConfigMeta?.map((config) => (
+                                <option key={config.name} value={config.name}>{config.name} {config.apiProvider ? `(${config.apiProvider})` : ""}
+                                </option>
+                            ))}
+                        </select>
+                        <VSCodeButton
+                            appearance="secondary"
+                            onClick={handleNewConfig}
+                        >
+                            <span className="codicon codicon-add" /> New
+                        </VSCodeButton>
+                        <VSCodeButton
+                            appearance="secondary"
+                            disabled={!currentApiConfigName}
+                            onClick={handleStartRename}
+                        >
+                            <span className="codicon codicon-edit" /> Rename
+                        </VSCodeButton>
+                        <VSCodeButton
+                            appearance="secondary"
+                            disabled={!currentApiConfigName}
+                            onClick={() => onDeleteConfig(currentApiConfigName!)}
+                        >
+                            <span className="codicon codicon-trash" /> Delete
+                        </VSCodeButton>
+                    </>
+                )}
+            </div>
+            <VSCodeDivider style={{ margin: "15px 0" }} />
+        </div>
+    )
+}
+
+export default memo(ApiConfigManager)

+ 49 - 0
webview-ui/src/components/settings/SettingsView.tsx

@@ -5,6 +5,7 @@ import { validateApiConfiguration, validateModelId } from "../../utils/validate"
 import { vscode } from "../../utils/vscode"
 import ApiOptions from "./ApiOptions"
 import McpEnabledToggle from "../mcp/McpEnabledToggle"
+import ApiConfigManager from "./ApiConfigManager"
 
 const IS_DEV = false // FIXME: use flags when packaging
 
@@ -55,10 +56,15 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
 		setAlwaysApproveResubmit,
 		requestDelaySeconds,
 		setRequestDelaySeconds,
+		currentApiConfigName,
+		listApiConfigMeta,
 	} = useExtensionState()
 	const [apiErrorMessage, setApiErrorMessage] = useState<string | undefined>(undefined)
 	const [modelIdErrorMessage, setModelIdErrorMessage] = useState<string | undefined>(undefined)
 	const [commandInput, setCommandInput] = useState("")
+	// const [draftNewMode, setDraftNewMode] = useState(false) 
+
+
 	const handleSubmit = () => {
 		const apiValidationResult = validateApiConfiguration(apiConfiguration)
 		const modelIdValidationResult = validateModelId(apiConfiguration, glamaModels, openRouterModels)
@@ -89,6 +95,13 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
 			vscode.postMessage({ type: "mcpEnabled", bool: mcpEnabled })
 			vscode.postMessage({ type: "alwaysApproveResubmit", bool: alwaysApproveResubmit })
 			vscode.postMessage({ type: "requestDelaySeconds", value: requestDelaySeconds })
+			vscode.postMessage({ type: "currentApiConfigName", text: currentApiConfigName })
+			vscode.postMessage({
+				type: "upsertApiConfiguration",
+				text: currentApiConfigName,
+				apiConfiguration
+			})
+
 			onDone()
 		}
 	}
@@ -150,6 +163,42 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
 			</div>
 			<div
 				style={{ flexGrow: 1, overflowY: "scroll", paddingRight: 8, display: "flex", flexDirection: "column" }}>
+				<div style={{ marginBottom: 5 }}>
+					<ApiConfigManager
+						currentApiConfigName={currentApiConfigName}
+						listApiConfigMeta={listApiConfigMeta}
+						onSelectConfig={(configName: string) => {
+							vscode.postMessage({
+								type: "loadApiConfiguration",
+								text: configName
+							})
+						}}
+						onDeleteConfig={(configName: string) => {
+							vscode.postMessage({
+								type: "deleteApiConfiguration",
+								text: configName
+							})
+						}}
+						onRenameConfig={(oldName: string, newName: string) => {
+							vscode.postMessage({
+								type: "renameApiConfiguration",
+								values: {oldName, newName},
+								apiConfiguration
+							})
+						}}
+						onUpsertConfig={(configName: string) => {
+							vscode.postMessage({
+								type: "upsertApiConfiguration",
+								text: configName,
+								apiConfiguration
+							})
+						}}
+						// setDraftNewConfig={(mode: boolean) => {
+						// 	setDraftNewMode(mode)
+						// }}
+					/>
+				</div>
+
 				<div style={{ marginBottom: 5 }}>
 					<h3 style={{ color: "var(--vscode-foreground)", margin: 0, marginBottom: 15 }}>Provider Settings</h3>
 					<ApiOptions

+ 22 - 0
webview-ui/src/components/settings/__tests__/SettingsView.test.tsx

@@ -11,6 +11,16 @@ jest.mock('../../../utils/vscode', () => ({
   },
 }))
 
+// Mock ApiConfigManager component
+jest.mock('../ApiConfigManager', () => ({
+  __esModule: true,
+  default: ({ currentApiConfigName, listApiConfigMeta, onSelectConfig, onDeleteConfig, onRenameConfig, onUpsertConfig }: any) => (
+    <div data-testid="api-config-management">
+      <span>Current config: {currentApiConfigName}</span>
+    </div>
+  )
+}))
+
 // Mock VSCode components
 jest.mock('@vscode/webview-ui-toolkit/react', () => ({
   VSCodeButton: ({ children, onClick, appearance }: any) => (
@@ -185,6 +195,18 @@ describe('SettingsView - Sound Settings', () => {
   })
 })
 
+describe('SettingsView - API Configuration', () => {
+  beforeEach(() => {
+    jest.clearAllMocks()
+  })
+
+  it('renders ApiConfigManagement with correct props', () => {
+    renderSettingsView()
+    
+    expect(screen.getByTestId('api-config-management')).toBeInTheDocument()
+  })
+})
+
 describe('SettingsView - Allowed Commands', () => {
   beforeEach(() => {
     jest.clearAllMocks()

+ 21 - 19
webview-ui/src/context/ExtensionStateContext.tsx

@@ -1,6 +1,6 @@
 import React, { createContext, useCallback, useContext, useEffect, useState } from "react"
 import { useEvent } from "react-use"
-import { ExtensionMessage, ExtensionState } from "../../../src/shared/ExtensionMessage"
+import { ApiConfigMeta, ExtensionMessage, ExtensionState } from "../../../src/shared/ExtensionMessage"
 import {
 	ApiConfiguration,
 	ModelInfo,
@@ -13,6 +13,9 @@ import { vscode } from "../utils/vscode"
 import { convertTextMateToHljs } from "../utils/textMateToHljs"
 import { findLastIndex } from "../../../src/shared/array"
 import { McpServer } from "../../../src/shared/mcp"
+import {
+	checkExistKey
+} from "../../../src/shared/checkExistApiConfig"
 
 export interface ExtensionStateContextType extends ExtensionState {
 	didHydrateState: boolean
@@ -50,6 +53,8 @@ export interface ExtensionStateContextType extends ExtensionState {
 	setAlwaysApproveResubmit: (value: boolean) => void
 	requestDelaySeconds: number
 	setRequestDelaySeconds: (value: number) => void
+	setCurrentApiConfigName: (value: string) => void
+	setListApiConfigMeta: (value: ApiConfigMeta[]) => void
 }
 
 export const ExtensionStateContext = createContext<ExtensionStateContextType | undefined>(undefined)
@@ -72,7 +77,9 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
 		terminalOutputLineLimit: 500,
 		mcpEnabled: true,
 		alwaysApproveResubmit: false,
-		requestDelaySeconds: 5
+		requestDelaySeconds: 5,
+		currentApiConfigName: 'default',
+		listApiConfigMeta: [],
 	})
 	const [didHydrateState, setDidHydrateState] = useState(false)
 	const [showWelcome, setShowWelcome] = useState(false)
@@ -88,27 +95,16 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
 	const [openAiModels, setOpenAiModels] = useState<string[]>([])
 	const [mcpServers, setMcpServers] = useState<McpServer[]>([])
 
+
+	const setListApiConfigMeta = useCallback((value: ApiConfigMeta[]) => setState((prevState) => ({ ...prevState, listApiConfigMeta: value })), [setState])
+
 	const handleMessage = useCallback((event: MessageEvent) => {
 		const message: ExtensionMessage = event.data
 		switch (message.type) {
 			case "state": {
 				setState(message.state!)
 				const config = message.state?.apiConfiguration
-				const hasKey = config
-					? [
-							config.apiKey,
-							config.glamaApiKey,
-							config.openRouterApiKey,
-							config.awsRegion,
-							config.vertexProjectId,
-							config.openAiApiKey,
-							config.ollamaModelId,
-							config.lmStudioModelId,
-							config.geminiApiKey,
-							config.openAiNativeApiKey,
-							config.deepSeekApiKey,
-						].some((key) => key !== undefined)
-					: false
+				const hasKey = checkExistKey(config)
 				setShowWelcome(!hasKey)
 				setDidHydrateState(true)
 				break
@@ -162,8 +158,12 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
 				setMcpServers(message.mcpServers ?? [])
 				break
 			}
+			case "listApiConfig": {
+				setListApiConfigMeta(message.listApiConfig ?? [])
+				break
+			}
 		}
-	}, [])
+	}, [setListApiConfigMeta])
 
 	useEvent("message", handleMessage)
 
@@ -208,7 +208,9 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
 		setTerminalOutputLineLimit: (value) => setState((prevState) => ({ ...prevState, terminalOutputLineLimit: value })),
 		setMcpEnabled: (value) => setState((prevState) => ({ ...prevState, mcpEnabled: value })),
 		setAlwaysApproveResubmit: (value) => setState((prevState) => ({ ...prevState, alwaysApproveResubmit: value })),
-		setRequestDelaySeconds: (value) => setState((prevState) => ({ ...prevState, requestDelaySeconds: value }))
+		setRequestDelaySeconds: (value) => setState((prevState) => ({ ...prevState, requestDelaySeconds: value })),
+		setCurrentApiConfigName: (value) => setState((prevState) => ({ ...prevState, currentApiConfigName: value })),
+		setListApiConfigMeta
 	}
 
 	return <ExtensionStateContext.Provider value={contextValue}>{children}</ExtensionStateContext.Provider>