Browse Source

Merge pull request #297 from RooVetGit/api_config

Save different API configurations to quickly switch between providers and settings
Matt Rubens 1 year ago
parent
commit
23fd205766

+ 5 - 0
.changeset/shiny-seahorses-peel.md

@@ -0,0 +1,5 @@
+---
+"roo-cline": patch
+---
+
+Save different API configurations to quickly switch between providers and settings (thanks @samhvw8!)

+ 1 - 0
README.md

@@ -7,6 +7,7 @@ A fork of Cline, an autonomous coding agent, with some additional experimental f
 - Drag and drop images into chats
 - Drag and drop images into chats
 - Delete messages from chats
 - Delete messages from chats
 - @-mention Git commits to include their context in the chat
 - @-mention Git commits to include their context in the chat
+- Save different API configurations to quickly switch between providers and settings
 - "Enhance prompt" button (OpenRouter models only for now)
 - "Enhance prompt" button (OpenRouter models only for now)
 - Sound effects for feedback
 - Sound effects for feedback
 - Option to use browsers of different sizes and adjust screenshot quality
 - Option to use browsers of different sizes and adjust screenshot quality

+ 24 - 2
src/core/Cline.ts

@@ -799,8 +799,30 @@ export class Cline {
 			}
 			}
 		}
 		}
 
 
-		// Convert to Anthropic.MessageParam by spreading only the API-required properties
-		const cleanConversationHistory = this.apiConversationHistory.map(({ role, content }) => ({ role, content }))
+		// Clean conversation history by:
+		// 1. Converting to Anthropic.MessageParam by spreading only the API-required properties
+		// 2. Converting image blocks to text descriptions if model doesn't support images
+		const cleanConversationHistory = this.apiConversationHistory.map(({ role, content }) => {
+			// Handle array content (could contain image blocks)
+			if (Array.isArray(content)) {
+				if (!this.api.getModel().info.supportsImages) {
+					// Convert image blocks to text descriptions
+					content = content.map(block => {
+						if (block.type === 'image') {
+							// Convert image blocks to text descriptions
+							// Note: We can't access the actual image content/url due to API limitations,
+							// but we can indicate that an image was present in the conversation
+							return {
+								type: 'text',
+								text: '[Referenced image in conversation]'
+							};
+						}
+						return block;
+					});
+				}
+			}
+			return { role, content }
+		})
 		const stream = this.api.createMessage(systemPrompt, cleanConversationHistory)
 		const stream = this.api.createMessage(systemPrompt, cleanConversationHistory)
 		const iterator = stream[Symbol.asyncIterator]()
 		const iterator = stream[Symbol.asyncIterator]()
 
 

+ 129 - 1
src/core/__tests__/Cline.test.ts

@@ -1,7 +1,8 @@
 import { Cline } from '../Cline';
 import { Cline } from '../Cline';
 import { ClineProvider } from '../webview/ClineProvider';
 import { ClineProvider } from '../webview/ClineProvider';
-import { ApiConfiguration } from '../../shared/api';
+import { ApiConfiguration, ModelInfo } from '../../shared/api';
 import { ApiStreamChunk } from '../../api/transform/stream';
 import { ApiStreamChunk } from '../../api/transform/stream';
+import { Anthropic } from '@anthropic-ai/sdk';
 import * as vscode from 'vscode';
 import * as vscode from 'vscode';
 
 
 // Mock all MCP-related modules
 // Mock all MCP-related modules
@@ -498,6 +499,133 @@ describe('Cline', () => {
                 expect(passedMessage).not.toHaveProperty('ts');
                 expect(passedMessage).not.toHaveProperty('ts');
                 expect(passedMessage).not.toHaveProperty('extraProp');
                 expect(passedMessage).not.toHaveProperty('extraProp');
             });
             });
+
+            it('should handle image blocks based on model capabilities', async () => {
+                // Create two configurations - one with image support, one without
+                const configWithImages = {
+                    ...mockApiConfig,
+                    apiModelId: 'claude-3-sonnet'
+                };
+                const configWithoutImages = {
+                    ...mockApiConfig,
+                    apiModelId: 'gpt-3.5-turbo'
+                };
+
+                // Create test conversation history with mixed content
+                const conversationHistory: (Anthropic.MessageParam & { ts?: number })[] = [
+                    {
+                        role: 'user' as const,
+                        content: [
+                            {
+                                type: 'text' as const,
+                                text: 'Here is an image'
+                            } satisfies Anthropic.TextBlockParam,
+                            {
+                                type: 'image' as const,
+                                source: {
+                                    type: 'base64' as const,
+                                    media_type: 'image/jpeg',
+                                    data: 'base64data'
+                                }
+                            } satisfies Anthropic.ImageBlockParam
+                        ]
+                    },
+                    {
+                        role: 'assistant' as const,
+                        content: [{
+                            type: 'text' as const,
+                            text: 'I see the image'
+                        } satisfies Anthropic.TextBlockParam]
+                    }
+                ];
+
+                // Test with model that supports images
+                const clineWithImages = new Cline(
+                    mockProvider,
+                    configWithImages,
+                    undefined,
+                    false,
+                    undefined,
+                    'test task'
+                );
+                // Mock the model info to indicate image support
+                jest.spyOn(clineWithImages.api, 'getModel').mockReturnValue({
+                    id: 'claude-3-sonnet',
+                    info: {
+                        supportsImages: true,
+                        supportsPromptCache: true,
+                        supportsComputerUse: true,
+                        contextWindow: 200000,
+                        maxTokens: 4096,
+                        inputPrice: 0.25,
+                        outputPrice: 0.75
+                    } as ModelInfo
+                });
+                clineWithImages.apiConversationHistory = conversationHistory;
+
+                // Test with model that doesn't support images
+                const clineWithoutImages = new Cline(
+                    mockProvider,
+                    configWithoutImages,
+                    undefined,
+                    false,
+                    undefined,
+                    'test task'
+                );
+                // Mock the model info to indicate no image support
+                jest.spyOn(clineWithoutImages.api, 'getModel').mockReturnValue({
+                    id: 'gpt-3.5-turbo',
+                    info: {
+                        supportsImages: false,
+                        supportsPromptCache: false,
+                        supportsComputerUse: false,
+                        contextWindow: 16000,
+                        maxTokens: 2048,
+                        inputPrice: 0.1,
+                        outputPrice: 0.2
+                    } as ModelInfo
+                });
+                clineWithoutImages.apiConversationHistory = conversationHistory;
+
+                // Create message spy for both instances
+                const createMessageSpyWithImages = jest.fn();
+                const createMessageSpyWithoutImages = jest.fn();
+                const mockStream = {
+                    async *[Symbol.asyncIterator]() {
+                        yield { type: 'text', text: '' };
+                    }
+                } as AsyncGenerator<ApiStreamChunk>;
+
+                jest.spyOn(clineWithImages.api, 'createMessage').mockImplementation((...args) => {
+                    createMessageSpyWithImages(...args);
+                    return mockStream;
+                });
+                jest.spyOn(clineWithoutImages.api, 'createMessage').mockImplementation((...args) => {
+                    createMessageSpyWithoutImages(...args);
+                    return mockStream;
+                });
+
+                // Trigger API requests for both instances
+                await clineWithImages.recursivelyMakeClineRequests([{ type: 'text', text: 'test' }]);
+                await clineWithoutImages.recursivelyMakeClineRequests([{ type: 'text', text: 'test' }]);
+
+                // Verify model with image support preserves image blocks
+                const callsWithImages = createMessageSpyWithImages.mock.calls;
+                const historyWithImages = callsWithImages[0][1][0];
+                expect(historyWithImages.content).toHaveLength(2);
+                expect(historyWithImages.content[0]).toEqual({ type: 'text', text: 'Here is an image' });
+                expect(historyWithImages.content[1]).toHaveProperty('type', 'image');
+
+                // Verify model without image support converts image blocks to text
+                const callsWithoutImages = createMessageSpyWithoutImages.mock.calls;
+                const historyWithoutImages = callsWithoutImages[0][1][0];
+                expect(historyWithoutImages.content).toHaveLength(2);
+                expect(historyWithoutImages.content[0]).toEqual({ type: 'text', text: 'Here is an image' });
+                expect(historyWithoutImages.content[1]).toEqual({
+                    type: 'text',
+                    text: '[Referenced image in conversation]'
+                });
+            });
         });
         });
     });
     });
 });
 });

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

@@ -0,0 +1,164 @@
+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 = "roo_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()
+      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}`)
+    }
+  }
+
+  /**
+   * Check if a config exists by name
+   */
+  async HasConfig(name: string): Promise<boolean> {
+    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<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}`)
+    }
+  }
+}

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

@@ -0,0 +1,384 @@
+import { ExtensionContext } from 'vscode'
+import { ConfigManager, ApiConfigData } from '../ConfigManager'
+import { ApiConfiguration } from '../../../shared/api'
+
+// 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(
+        'roo_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(
+        'roo_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(
+        'roo_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(
+        'roo_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(
+        'roo_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'
+      )
+    })
+  })
+
+  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'
+      )
+    })
+  })
+})

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

@@ -12,9 +12,9 @@ import { selectImages } from "../../integrations/misc/process-images"
 import { getTheme } from "../../integrations/theme/getTheme"
 import { getTheme } from "../../integrations/theme/getTheme"
 import WorkspaceTracker from "../../integrations/workspace/WorkspaceTracker"
 import WorkspaceTracker from "../../integrations/workspace/WorkspaceTracker"
 import { McpHub } from "../../services/mcp/McpHub"
 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 { findLast } from "../../shared/array"
-import { ExtensionMessage } from "../../shared/ExtensionMessage"
+import { ApiConfigMeta, ExtensionMessage } from "../../shared/ExtensionMessage"
 import { HistoryItem } from "../../shared/HistoryItem"
 import { HistoryItem } from "../../shared/HistoryItem"
 import { WebviewMessage } from "../../shared/WebviewMessage"
 import { WebviewMessage } from "../../shared/WebviewMessage"
 import { fileExistsAtPath } from "../../utils/fs"
 import { fileExistsAtPath } from "../../utils/fs"
@@ -23,8 +23,10 @@ import { openMention } from "../mentions"
 import { getNonce } from "./getNonce"
 import { getNonce } from "./getNonce"
 import { getUri } from "./getUri"
 import { getUri } from "./getUri"
 import { playSound, setSoundEnabled, setSoundVolume } from "../../utils/sound"
 import { playSound, setSoundEnabled, setSoundVolume } from "../../utils/sound"
+import { checkExistKey } from "../../shared/checkExistApiConfig"
 import { enhancePrompt } from "../../utils/enhance-prompt"
 import { enhancePrompt } from "../../utils/enhance-prompt"
 import { getCommitInfo, searchCommits, getWorkingState } from "../../utils/git"
 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
 https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
@@ -85,6 +87,9 @@ type GlobalStateKey =
 	| "mcpEnabled"
 	| "mcpEnabled"
 	| "alwaysApproveResubmit"
 	| "alwaysApproveResubmit"
 	| "requestDelaySeconds"
 	| "requestDelaySeconds"
+	| "currentApiConfigName"
+	| "listApiConfigMeta"
+
 export const GlobalFileNames = {
 export const GlobalFileNames = {
 	apiConversationHistory: "api_conversation_history.json",
 	apiConversationHistory: "api_conversation_history.json",
 	uiMessages: "ui_messages.json",
 	uiMessages: "ui_messages.json",
@@ -103,6 +108,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 	private workspaceTracker?: WorkspaceTracker
 	private workspaceTracker?: WorkspaceTracker
 	mcpHub?: McpHub
 	mcpHub?: McpHub
 	private latestAnnouncementId = "dec-10-2024" // update to some unique identifier when we add a new announcement
 	private latestAnnouncementId = "dec-10-2024" // update to some unique identifier when we add a new announcement
+	configManager: ConfigManager
 
 
 	constructor(
 	constructor(
 		readonly context: vscode.ExtensionContext,
 		readonly context: vscode.ExtensionContext,
@@ -112,6 +118,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 		ClineProvider.activeInstances.add(this)
 		ClineProvider.activeInstances.add(this)
 		this.workspaceTracker = new WorkspaceTracker(this)
 		this.workspaceTracker = new WorkspaceTracker(this)
 		this.mcpHub = new McpHub(this)
 		this.mcpHub = new McpHub(this)
+		this.configManager = new ConfigManager(this.context)
 	}
 	}
 
 
 	/*
 	/*
@@ -235,7 +242,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			diffEnabled,
 			diffEnabled,
 			fuzzyMatchThreshold
 			fuzzyMatchThreshold
 		} = await this.getState()
 		} = await this.getState()
-		
+
 		this.cline = new Cline(
 		this.cline = new Cline(
 			this,
 			this,
 			apiConfiguration,
 			apiConfiguration,
@@ -255,7 +262,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			diffEnabled,
 			diffEnabled,
 			fuzzyMatchThreshold
 			fuzzyMatchThreshold
 		} = await this.getState()
 		} = await this.getState()
-		
+
 		this.cline = new Cline(
 		this.cline = new Cline(
 			this,
 			this,
 			apiConfiguration,
 			apiConfiguration,
@@ -321,15 +328,15 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 
 
 		// Use a nonce to only allow a specific script to be run.
 		// 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
 		- '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:;
 		- 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()
 		const nonce = getNonce()
 
 
 		// Tip: Install the es6-string-html VS Code extension to enable code highlighting below
 		// Tip: Install the es6-string-html VS Code extension to enable code highlighting below
@@ -410,6 +417,55 @@ 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])) {
+									const {
+										apiConfiguration,
+									} = await this.getState()
+									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),
+									await this.postMessageToWebview({ type: "listApiConfig", listApiConfig })
+								]
+							)
+						}).catch(console.error);
+
 						break
 						break
 					case "newTask":
 					case "newTask":
 						// Code that should run in response to the hello message command
 						// Code that should run in response to the hello message command
@@ -424,70 +480,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 						break
 						break
 					case "apiConfiguration":
 					case "apiConfiguration":
 						if (message.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()
 						await this.postStateToWebview()
 						break
 						break
@@ -566,7 +559,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 						if (message?.values?.baseUrl && message?.values?.apiKey) {
 						if (message?.values?.baseUrl && message?.values?.apiKey) {
 							const openAiModels = await this.getOpenAiModels(message?.values?.baseUrl, message?.values?.apiKey)
 							const openAiModels = await this.getOpenAiModels(message?.values?.baseUrl, message?.values?.apiKey)
 							this.postMessageToWebview({ type: "openAiModels", openAiModels })
 							this.postMessageToWebview({ type: "openAiModels", openAiModels })
-						}	
+						}
 						break
 						break
 					case "openImage":
 					case "openImage":
 						openImage(message.text!)
 						openImage(message.text!)
@@ -805,6 +798,113 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 						}
 						}
 						break
 						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.updateApiConfiguration(message.apiConfiguration),
+									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) {
+
+							const answer = await vscode.window.showInformationMessage(
+								"Are you sure you want to delete this configuration profile?",
+								{ modal: true },
+								"Yes",
+							)
+
+							if (answer !== "Yes") {
+								break
+							}
+
+							try {
+								await this.configManager.DeleteConfig(message.text);
+								let listApiConfig = await this.configManager.ListConfig()
+								let currentApiConfigName = await this.getGlobalState("currentApiConfigName")
+
+								if (message.text === currentApiConfigName) {
+									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()
+									}
+								}
+							} 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
 				}
 				}
 			},
 			},
 			null,
 			null,
@@ -812,6 +912,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) {
 	async updateCustomInstructions(instructions?: string) {
 		// User may be clearing the field
 		// User may be clearing the field
 		await this.updateGlobalState("customInstructions", instructions || undefined)
 		await this.updateGlobalState("customInstructions", instructions || undefined)
@@ -1256,8 +1424,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			mcpEnabled,
 			mcpEnabled,
 			alwaysApproveResubmit,
 			alwaysApproveResubmit,
 			requestDelaySeconds,
 			requestDelaySeconds,
+			currentApiConfigName,
+			listApiConfigMeta,
 		} = await this.getState()
 		} = await this.getState()
-		
+
 		const allowedCommands = vscode.workspace
 		const allowedCommands = vscode.workspace
 			.getConfiguration('roo-cline')
 			.getConfiguration('roo-cline')
 			.get<string[]>('allowedCommands') || []
 			.get<string[]>('allowedCommands') || []
@@ -1290,6 +1460,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			mcpEnabled: mcpEnabled ?? true,
 			mcpEnabled: mcpEnabled ?? true,
 			alwaysApproveResubmit: alwaysApproveResubmit ?? false,
 			alwaysApproveResubmit: alwaysApproveResubmit ?? false,
 			requestDelaySeconds: requestDelaySeconds ?? 5,
 			requestDelaySeconds: requestDelaySeconds ?? 5,
+			currentApiConfigName: currentApiConfigName ?? "default",
+			listApiConfigMeta: listApiConfigMeta ?? [],
 		}
 		}
 	}
 	}
 
 
@@ -1397,6 +1569,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			mcpEnabled,
 			mcpEnabled,
 			alwaysApproveResubmit,
 			alwaysApproveResubmit,
 			requestDelaySeconds,
 			requestDelaySeconds,
+			currentApiConfigName,
+			listApiConfigMeta,
 		] = await Promise.all([
 		] = await Promise.all([
 			this.getGlobalState("apiProvider") as Promise<ApiProvider | undefined>,
 			this.getGlobalState("apiProvider") as Promise<ApiProvider | undefined>,
 			this.getGlobalState("apiModelId") as Promise<string | undefined>,
 			this.getGlobalState("apiModelId") as Promise<string | undefined>,
@@ -1449,6 +1623,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			this.getGlobalState("mcpEnabled") as Promise<boolean | undefined>,
 			this.getGlobalState("mcpEnabled") as Promise<boolean | undefined>,
 			this.getGlobalState("alwaysApproveResubmit") as Promise<boolean | undefined>,
 			this.getGlobalState("alwaysApproveResubmit") as Promise<boolean | undefined>,
 			this.getGlobalState("requestDelaySeconds") as Promise<number | undefined>,
 			this.getGlobalState("requestDelaySeconds") as Promise<number | undefined>,
+			this.getGlobalState("currentApiConfigName") as Promise<string | undefined>,
+			this.getGlobalState("listApiConfigMeta") as Promise<ApiConfigMeta[] | undefined>,
 		])
 		])
 
 
 		let apiProvider: ApiProvider
 		let apiProvider: ApiProvider
@@ -1545,6 +1721,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			mcpEnabled: mcpEnabled ?? true,
 			mcpEnabled: mcpEnabled ?? true,
 			alwaysApproveResubmit: alwaysApproveResubmit ?? false,
 			alwaysApproveResubmit: alwaysApproveResubmit ?? false,
 			requestDelaySeconds: requestDelaySeconds ?? 5,
 			requestDelaySeconds: requestDelaySeconds ?? 5,
+			currentApiConfigName: currentApiConfigName ?? "default",
+			listApiConfigMeta: listApiConfigMeta ?? [],
 		}
 		}
 	}
 	}
 
 

+ 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'
 // 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 { HistoryItem } from "./HistoryItem"
 import { McpServer } from "./mcp"
 import { McpServer } from "./mcp"
 import { GitCommit } from "../utils/git"
 import { GitCommit } from "../utils/git"
@@ -23,6 +23,7 @@ export interface ExtensionMessage {
 		| "mcpServers"
 		| "mcpServers"
 		| "enhancedPrompt"
 		| "enhancedPrompt"
 		| "commitSearchResults"
 		| "commitSearchResults"
+		| "listApiConfig"
 	text?: string
 	text?: string
 	action?:
 	action?:
 		| "chatButtonClicked"
 		| "chatButtonClicked"
@@ -42,6 +43,12 @@ export interface ExtensionMessage {
 	openAiModels?: string[]
 	openAiModels?: string[]
 	mcpServers?: McpServer[]
 	mcpServers?: McpServer[]
 	commits?: GitCommit[]
 	commits?: GitCommit[]
+	listApiConfig?: ApiConfigMeta[]
+}
+
+export interface ApiConfigMeta {
+	name: string
+	apiProvider?: ApiProvider
 }
 }
 
 
 export interface ExtensionState {
 export interface ExtensionState {
@@ -50,6 +57,8 @@ export interface ExtensionState {
 	taskHistory: HistoryItem[]
 	taskHistory: HistoryItem[]
 	shouldShowAnnouncement: boolean
 	shouldShowAnnouncement: boolean
 	apiConfiguration?: ApiConfiguration
 	apiConfiguration?: ApiConfiguration
+	currentApiConfigName?: string
+	listApiConfigMeta?: ApiConfigMeta[]
 	customInstructions?: string
 	customInstructions?: string
 	alwaysAllowReadOnly?: boolean
 	alwaysAllowReadOnly?: boolean
 	alwaysAllowWrite?: boolean
 	alwaysAllowWrite?: boolean

+ 7 - 0
src/shared/WebviewMessage.ts

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

+ 63 - 2
webview-ui/src/components/chat/ChatTextArea.tsx

@@ -44,9 +44,21 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 		},
 		},
 		ref,
 		ref,
 	) => {
 	) => {
-		const { filePaths, apiConfiguration } = useExtensionState()
+		const { filePaths, apiConfiguration, currentApiConfigName, listApiConfigMeta } = useExtensionState()
 		const [isTextAreaFocused, setIsTextAreaFocused] = useState(false)
 		const [isTextAreaFocused, setIsTextAreaFocused] = useState(false)
 		const [gitCommits, setGitCommits] = useState<any[]>([])
 		const [gitCommits, setGitCommits] = useState<any[]>([])
+		const [showDropdown, setShowDropdown] = useState(false)
+
+		// Close dropdown when clicking outside
+		useEffect(() => {
+			const handleClickOutside = (event: MouseEvent) => {
+				if (showDropdown) {
+					setShowDropdown(false)
+				}
+			}
+			document.addEventListener("mousedown", handleClickOutside)
+			return () => document.removeEventListener("mousedown", handleClickOutside)
+		}, [showDropdown])
 
 
 		// Handle enhanced prompt response
 		// Handle enhanced prompt response
 		useEffect(() => {
 		useEffect(() => {
@@ -649,13 +661,62 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 						style={{
 						style={{
 							position: "absolute",
 							position: "absolute",
 							paddingTop: 4,
 							paddingTop: 4,
-							bottom: 14,
+							bottom: 32,
 							left: 22,
 							left: 22,
 							right: 67,
 							right: 67,
 							zIndex: 2,
 							zIndex: 2,
 						}}
 						}}
 					/>
 					/>
 				)}
 				)}
+				{(listApiConfigMeta || []).length > 1 && (
+					<div
+						style={{
+							position: "absolute",
+							left: 25,
+							bottom: 14,
+							zIndex: 2
+						}}
+					>
+						<select
+							value={currentApiConfigName}
+							disabled={textAreaDisabled}
+							onChange={(e) => vscode.postMessage({
+								type: "loadApiConfiguration",
+								text: e.target.value
+							})}
+							style={{
+								fontSize: "11px",
+								cursor: textAreaDisabled ? "not-allowed" : "pointer",
+								backgroundColor: "transparent",
+								border: "none",
+								color: "var(--vscode-input-foreground)",
+								opacity: textAreaDisabled ? 0.5 : 0.6,
+								outline: "none",
+								paddingLeft: 14,
+								WebkitAppearance: "none",
+								MozAppearance: "none",
+								appearance: "none",
+								backgroundImage: "url(\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='rgba(255,255,255,0.5)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e\")",
+								backgroundRepeat: "no-repeat",
+								backgroundPosition: "left 0px center",
+								backgroundSize: "10px"
+							}}
+						>
+							{(listApiConfigMeta || [])?.map((config) => (
+								<option
+									key={config.name}
+									value={config.name}
+									style={{
+										backgroundColor: "var(--vscode-dropdown-background)",
+										color: "var(--vscode-dropdown-foreground)"
+									}}
+								>
+									{config.name}
+								</option>
+							))}
+						</select>
+					</div>
+				)}
 				<div className="button-row" style={{ position: "absolute", right: 20, display: "flex", alignItems: "center", height: 31, bottom: 8, zIndex: 2, justifyContent: "flex-end" }}>
 				<div className="button-row" style={{ position: "absolute", right: 20, display: "flex", alignItems: "center", height: 31, bottom: 8, zIndex: 2, justifyContent: "flex-end" }}>
 				  <span style={{ display: "flex", alignItems: "center", gap: 12 }}>
 				  <span style={{ display: "flex", alignItems: "center", gap: 12 }}>
 					{apiConfiguration?.apiProvider === "openrouter" && (
 					{apiConfiguration?.apiProvider === "openrouter" && (

+ 2 - 7
webview-ui/src/components/chat/ChatView.tsx

@@ -1,4 +1,4 @@
-import { VSCodeButton, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
+import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
 import debounce from "debounce"
 import debounce from "debounce"
 import { useCallback, useEffect, useMemo, useRef, useState } from "react"
 import { useCallback, useEffect, useMemo, useRef, useState } from "react"
 import { useDeepCompareEffect, useEvent, useMount } from "react-use"
 import { useDeepCompareEffect, useEvent, useMount } from "react-use"
@@ -868,12 +868,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
 					<div style={{ padding: "0 20px", flexShrink: 0 }}>
 					<div style={{ padding: "0 20px", flexShrink: 0 }}>
 						<h2>What can I do for you?</h2>
 						<h2>What can I do for you?</h2>
 						<p>
 						<p>
-							Thanks to{" "}
-							<VSCodeLink
-								href="https://www-cdn.anthropic.com/fed9cc193a14b84131812372d8d5857f8f304c52/Model_Card_Claude_3_Addendum.pdf"
-								style={{ display: "inline" }}>
-								Claude 3.5 Sonnet's agentic coding capabilities,
-							</VSCodeLink>{" "}
+							Thanks to the latest breakthroughs in agentic coding capabilities,
 							I can handle complex software development tasks step-by-step. With tools that let me create
 							I can handle complex software development tasks step-by-step. With tools that let me create
 							& edit files, explore complex projects, use the browser, and execute terminal commands
 							& edit files, explore complex projects, use the browser, and execute terminal commands
 							(after you grant permission), I can assist you in ways that go beyond code completion or
 							(after you grant permission), I can assist you in ways that go beyond code completion or

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

@@ -0,0 +1,225 @@
+import { VSCodeButton, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
+import { memo, useEffect, useRef, 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
+}
+
+const ApiConfigManager = ({
+    currentApiConfigName = "",
+    listApiConfigMeta = [],
+    onSelectConfig,
+    onDeleteConfig,
+    onRenameConfig,
+    onUpsertConfig,
+}: ApiConfigManagerProps) => {
+    const [editState, setEditState] = useState<'new' | 'rename' | null>(null);
+    const [inputValue, setInputValue] = useState("");
+    const inputRef = useRef<HTMLInputElement>();
+
+    // Focus input when entering edit mode
+    useEffect(() => {
+        if (editState) {
+            setTimeout(() => inputRef.current?.focus(), 0);
+        }
+    }, [editState]);
+
+    // Reset edit state when current profile changes
+    useEffect(() => {
+        setEditState(null);
+        setInputValue("");
+    }, [currentApiConfigName]);
+
+    const handleAdd = () => {
+        const newConfigName = currentApiConfigName + " (copy)";
+        onUpsertConfig(newConfigName);
+    };
+
+    const handleStartRename = () => {
+        setEditState('rename');
+        setInputValue(currentApiConfigName || "");
+    };
+
+    const handleCancel = () => {
+        setEditState(null);
+        setInputValue("");
+    };
+
+    const handleSave = () => {
+        const trimmedValue = inputValue.trim();
+        if (!trimmedValue) return;
+
+        if (editState === 'new') {
+            onUpsertConfig(trimmedValue);
+        } else if (editState === 'rename' && currentApiConfigName) {
+            onRenameConfig(currentApiConfigName, trimmedValue);
+        }
+
+        setEditState(null);
+        setInputValue("");
+    };
+
+    const handleDelete = () => {
+        if (!currentApiConfigName || !listApiConfigMeta || listApiConfigMeta.length <= 1) return;
+        
+        // Let the extension handle both deletion and selection
+        onDeleteConfig(currentApiConfigName);
+    };
+
+    const isOnlyProfile = listApiConfigMeta?.length === 1;
+
+    return (
+        <div style={{ marginBottom: 5 }}>
+            <div style={{ 
+                display: "flex", 
+                flexDirection: "column",
+                gap: "2px"
+            }}>
+                <label htmlFor="config-profile">
+                    <span style={{ fontWeight: "500" }}>Configuration Profile</span>
+                </label>
+
+                {editState ? (
+                    <div style={{ display: "flex", gap: "4px", alignItems: "center" }}>
+                        <VSCodeTextField
+                            ref={inputRef as any}
+                            value={inputValue}
+                            onInput={(e: any) => setInputValue(e.target.value)}
+                            placeholder={editState === 'new' ? "Enter profile name" : "Enter new name"}
+                            style={{ flexGrow: 1 }}
+                            onKeyDown={(e: any) => {
+                                if (e.key === 'Enter' && inputValue.trim()) {
+                                    handleSave();
+                                } else if (e.key === 'Escape') {
+                                    handleCancel();
+                                }
+                            }}
+                        />
+                        <VSCodeButton
+                            appearance="icon"
+                            disabled={!inputValue.trim()}
+                            onClick={handleSave}
+                            title="Save"
+                            style={{
+                                padding: 0,
+                                margin: 0,
+                                height: '28px',
+                                width: '28px',
+                                minWidth: '28px'
+                            }}
+                        >
+                            <span className="codicon codicon-check" />
+                        </VSCodeButton>
+                        <VSCodeButton
+                            appearance="icon"
+                            onClick={handleCancel}
+                            title="Cancel"
+                            style={{
+                                padding: 0,
+                                margin: 0,
+                                height: '28px',
+                                width: '28px',
+                                minWidth: '28px'
+                            }}
+                        >
+                            <span className="codicon codicon-close" />
+                        </VSCodeButton>
+                    </div>
+                ) : (
+                    <>
+                        <div style={{ display: "flex", gap: "4px", alignItems: "center" }}>
+                            <select
+                                id="config-profile"
+                                value={currentApiConfigName}
+                                onChange={(e) => onSelectConfig(e.target.value)}
+                                style={{
+                                    flexGrow: 1,
+                                    padding: "4px 8px",
+                                    paddingRight: "24px",
+                                    backgroundColor: "var(--vscode-dropdown-background)",
+                                    color: "var(--vscode-dropdown-foreground)",
+                                    border: "1px solid var(--vscode-dropdown-border)",
+                                    borderRadius: "2px",
+                                    height: "28px",
+                                    cursor: "pointer",
+                                    outline: "none"
+                                }}
+                            >
+                                {listApiConfigMeta?.map((config) => (
+                                    <option 
+                                        key={config.name} 
+                                        value={config.name}
+                                    >
+                                        {config.name}
+                                    </option>
+                                ))}
+                            </select>
+                            <VSCodeButton
+                                appearance="icon"
+                                onClick={handleAdd}
+                                title="Add profile"
+                                style={{
+                                    padding: 0,
+                                    margin: 0,
+                                    height: '28px',
+                                    width: '28px',
+                                    minWidth: '28px'
+                                }}
+                            >
+                                <span className="codicon codicon-add" />
+                            </VSCodeButton>
+                            {currentApiConfigName && (
+                                <>
+                                    <VSCodeButton
+                                        appearance="icon"
+                                        onClick={handleStartRename}
+                                        title="Rename profile"
+                                        style={{
+                                            padding: 0,
+                                            margin: 0,
+                                            height: '28px',
+                                            width: '28px',
+                                            minWidth: '28px'
+                                        }}
+                                    >
+                                        <span className="codicon codicon-edit" />
+                                    </VSCodeButton>
+                                    <VSCodeButton
+                                        appearance="icon"
+                                        onClick={handleDelete}
+                                        title={isOnlyProfile ? "Cannot delete the only profile" : "Delete profile"}
+                                        disabled={isOnlyProfile}
+                                        style={{
+                                            padding: 0,
+                                            margin: 0,
+                                            height: '28px',
+                                            width: '28px',
+                                            minWidth: '28px'
+                                        }}
+                                    >
+                                        <span className="codicon codicon-trash" />
+                                    </VSCodeButton>
+                                </>
+                            )}
+                        </div>
+                        <p style={{
+                            fontSize: "12px",
+                            margin: "5px 0 12px",
+                            color: "var(--vscode-descriptionForeground)"
+                        }}>
+                            Save different API configurations to quickly switch between providers and settings
+                        </p>
+                    </>
+                )}
+            </div>
+        </div>
+    )
+}
+
+export default memo(ApiConfigManager)

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

@@ -43,13 +43,12 @@ import OpenAiModelPicker from "./OpenAiModelPicker"
 import GlamaModelPicker from "./GlamaModelPicker"
 import GlamaModelPicker from "./GlamaModelPicker"
 
 
 interface ApiOptionsProps {
 interface ApiOptionsProps {
-	showModelOptions: boolean
 	apiErrorMessage?: string
 	apiErrorMessage?: string
 	modelIdErrorMessage?: string
 	modelIdErrorMessage?: string
 }
 }
 
 
-const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) => {
-	const { apiConfiguration, setApiConfiguration, uriScheme } = useExtensionState()
+const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) => {
+	const { apiConfiguration, setApiConfiguration, uriScheme, onUpdateApiConfig } = useExtensionState()
 	const [ollamaModels, setOllamaModels] = useState<string[]>([])
 	const [ollamaModels, setOllamaModels] = useState<string[]>([])
 	const [lmStudioModels, setLmStudioModels] = useState<string[]>([])
 	const [lmStudioModels, setLmStudioModels] = useState<string[]>([])
 	const [anthropicBaseUrlSelected, setAnthropicBaseUrlSelected] = useState(!!apiConfiguration?.anthropicBaseUrl)
 	const [anthropicBaseUrlSelected, setAnthropicBaseUrlSelected] = useState(!!apiConfiguration?.anthropicBaseUrl)
@@ -57,7 +56,9 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage }:
 	const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false)
 	const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false)
 
 
 	const handleInputChange = (field: keyof ApiConfiguration) => (event: any) => {
 	const handleInputChange = (field: keyof ApiConfiguration) => (event: any) => {
-		setApiConfiguration({ ...apiConfiguration, [field]: event.target.value })
+		const apiConfig = { ...apiConfiguration, [field]: event.target.value }
+		onUpdateApiConfig(apiConfig)
+		setApiConfiguration(apiConfig)
 	}
 	}
 
 
 	const { selectedProvider, selectedModelId, selectedModelInfo } = useMemo(() => {
 	const { selectedProvider, selectedModelId, selectedModelInfo } = useMemo(() => {
@@ -693,16 +694,15 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage }:
 				</p>
 				</p>
 			)}
 			)}
 
 
-			{selectedProvider === "glama" && showModelOptions && <GlamaModelPicker />}
+			{selectedProvider === "glama" && <GlamaModelPicker />}
 
 
-			{selectedProvider === "openrouter" && showModelOptions && <OpenRouterModelPicker />}
+			{selectedProvider === "openrouter" && <OpenRouterModelPicker />}
 
 
 			{selectedProvider !== "glama" &&
 			{selectedProvider !== "glama" &&
 				selectedProvider !== "openrouter" &&
 				selectedProvider !== "openrouter" &&
 				selectedProvider !== "openai" &&
 				selectedProvider !== "openai" &&
 				selectedProvider !== "ollama" &&
 				selectedProvider !== "ollama" &&
-				selectedProvider !== "lmstudio" &&
-				showModelOptions && (
+				selectedProvider !== "lmstudio" && (
 					<>
 					<>
 						<div className="dropdown-container">
 						<div className="dropdown-container">
 							<label htmlFor="model-id">
 							<label htmlFor="model-id">

+ 13 - 3
webview-ui/src/components/settings/GlamaModelPicker.tsx

@@ -11,7 +11,7 @@ import { highlight } from "../history/HistoryView"
 import { ModelInfoView, normalizeApiConfiguration } from "./ApiOptions"
 import { ModelInfoView, normalizeApiConfiguration } from "./ApiOptions"
 
 
 const GlamaModelPicker: React.FC = () => {
 const GlamaModelPicker: React.FC = () => {
-	const { apiConfiguration, setApiConfiguration, glamaModels } = useExtensionState()
+	const { apiConfiguration, setApiConfiguration, glamaModels, onUpdateApiConfig } = useExtensionState()
 	const [searchTerm, setSearchTerm] = useState(apiConfiguration?.glamaModelId || glamaDefaultModelId)
 	const [searchTerm, setSearchTerm] = useState(apiConfiguration?.glamaModelId || glamaDefaultModelId)
 	const [isDropdownVisible, setIsDropdownVisible] = useState(false)
 	const [isDropdownVisible, setIsDropdownVisible] = useState(false)
 	const [selectedIndex, setSelectedIndex] = useState(-1)
 	const [selectedIndex, setSelectedIndex] = useState(-1)
@@ -22,11 +22,14 @@ const GlamaModelPicker: React.FC = () => {
 
 
 	const handleModelChange = (newModelId: string) => {
 	const handleModelChange = (newModelId: string) => {
 		// could be setting invalid model id/undefined info but validation will catch it
 		// could be setting invalid model id/undefined info but validation will catch it
-		setApiConfiguration({
+		const apiConfig = {
 			...apiConfiguration,
 			...apiConfiguration,
 			glamaModelId: newModelId,
 			glamaModelId: newModelId,
 			glamaModelInfo: glamaModels[newModelId],
 			glamaModelInfo: glamaModels[newModelId],
-		})
+		}
+		setApiConfiguration(apiConfig)
+		onUpdateApiConfig(apiConfig)
+
 		setSearchTerm(newModelId)
 		setSearchTerm(newModelId)
 	}
 	}
 
 
@@ -34,6 +37,13 @@ const GlamaModelPicker: React.FC = () => {
 		return normalizeApiConfiguration(apiConfiguration)
 		return normalizeApiConfiguration(apiConfiguration)
 	}, [apiConfiguration])
 	}, [apiConfiguration])
 
 
+
+	useEffect(() => {
+		if (apiConfiguration?.glamaModelId && apiConfiguration?.glamaModelId !== searchTerm) {
+			setSearchTerm(apiConfiguration?.glamaModelId)
+		}
+	}, [apiConfiguration, searchTerm])
+
 	useMount(() => {
 	useMount(() => {
 		vscode.postMessage({ type: "refreshGlamaModels" })
 		vscode.postMessage({ type: "refreshGlamaModels" })
 	})
 	})

+ 12 - 3
webview-ui/src/components/settings/OpenAiModelPicker.tsx

@@ -8,7 +8,7 @@ import { vscode } from "../../utils/vscode"
 import { highlight } from "../history/HistoryView"
 import { highlight } from "../history/HistoryView"
 
 
 const OpenAiModelPicker: React.FC = () => {
 const OpenAiModelPicker: React.FC = () => {
-	const { apiConfiguration, setApiConfiguration, openAiModels } = useExtensionState()
+	const { apiConfiguration, setApiConfiguration, openAiModels, onUpdateApiConfig } = useExtensionState()
 	const [searchTerm, setSearchTerm] = useState(apiConfiguration?.openAiModelId || "")
 	const [searchTerm, setSearchTerm] = useState(apiConfiguration?.openAiModelId || "")
 	const [isDropdownVisible, setIsDropdownVisible] = useState(false)
 	const [isDropdownVisible, setIsDropdownVisible] = useState(false)
 	const [selectedIndex, setSelectedIndex] = useState(-1)
 	const [selectedIndex, setSelectedIndex] = useState(-1)
@@ -18,13 +18,22 @@ const OpenAiModelPicker: React.FC = () => {
 
 
 	const handleModelChange = (newModelId: string) => {
 	const handleModelChange = (newModelId: string) => {
 		// could be setting invalid model id/undefined info but validation will catch it
 		// could be setting invalid model id/undefined info but validation will catch it
-		setApiConfiguration({
+		const apiConfig = {
 			...apiConfiguration,
 			...apiConfiguration,
 			openAiModelId: newModelId,
 			openAiModelId: newModelId,
-		})
+		}
+		setApiConfiguration(apiConfig)
+		onUpdateApiConfig(apiConfig)
+
 		setSearchTerm(newModelId)
 		setSearchTerm(newModelId)
 	}
 	}
 
 
+	useEffect(() => {
+		if (apiConfiguration?.openAiModelId && apiConfiguration?.openAiModelId !== searchTerm) {
+			setSearchTerm(apiConfiguration?.openAiModelId)
+		}
+	}, [apiConfiguration, searchTerm])
+
 	useEffect(() => {
 	useEffect(() => {
 		if (!apiConfiguration?.openAiBaseUrl || !apiConfiguration?.openAiApiKey) {
 		if (!apiConfiguration?.openAiBaseUrl || !apiConfiguration?.openAiApiKey) {
 			return
 			return

+ 12 - 3
webview-ui/src/components/settings/OpenRouterModelPicker.tsx

@@ -11,7 +11,7 @@ import { highlight } from "../history/HistoryView"
 import { ModelInfoView, normalizeApiConfiguration } from "./ApiOptions"
 import { ModelInfoView, normalizeApiConfiguration } from "./ApiOptions"
 
 
 const OpenRouterModelPicker: React.FC = () => {
 const OpenRouterModelPicker: React.FC = () => {
-	const { apiConfiguration, setApiConfiguration, openRouterModels } = useExtensionState()
+	const { apiConfiguration, setApiConfiguration, openRouterModels, onUpdateApiConfig } = useExtensionState()
 	const [searchTerm, setSearchTerm] = useState(apiConfiguration?.openRouterModelId || openRouterDefaultModelId)
 	const [searchTerm, setSearchTerm] = useState(apiConfiguration?.openRouterModelId || openRouterDefaultModelId)
 	const [isDropdownVisible, setIsDropdownVisible] = useState(false)
 	const [isDropdownVisible, setIsDropdownVisible] = useState(false)
 	const [selectedIndex, setSelectedIndex] = useState(-1)
 	const [selectedIndex, setSelectedIndex] = useState(-1)
@@ -22,11 +22,14 @@ const OpenRouterModelPicker: React.FC = () => {
 
 
 	const handleModelChange = (newModelId: string) => {
 	const handleModelChange = (newModelId: string) => {
 		// could be setting invalid model id/undefined info but validation will catch it
 		// could be setting invalid model id/undefined info but validation will catch it
-		setApiConfiguration({
+		const apiConfig = {
 			...apiConfiguration,
 			...apiConfiguration,
 			openRouterModelId: newModelId,
 			openRouterModelId: newModelId,
 			openRouterModelInfo: openRouterModels[newModelId],
 			openRouterModelInfo: openRouterModels[newModelId],
-		})
+		}
+
+		setApiConfiguration(apiConfig)
+		onUpdateApiConfig(apiConfig)
 		setSearchTerm(newModelId)
 		setSearchTerm(newModelId)
 	}
 	}
 
 
@@ -34,6 +37,12 @@ const OpenRouterModelPicker: React.FC = () => {
 		return normalizeApiConfiguration(apiConfiguration)
 		return normalizeApiConfiguration(apiConfiguration)
 	}, [apiConfiguration])
 	}, [apiConfiguration])
 
 
+	useEffect(() => {
+		if (apiConfiguration?.openRouterModelId && apiConfiguration?.openRouterModelId !== searchTerm) {
+			setSearchTerm(apiConfiguration?.openRouterModelId)
+		}
+	}, [apiConfiguration, searchTerm])
+
 	useMount(() => {
 	useMount(() => {
 		vscode.postMessage({ type: "refreshOpenRouterModels" })
 		vscode.postMessage({ type: "refreshOpenRouterModels" })
 	})
 	})

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

@@ -5,6 +5,7 @@ import { validateApiConfiguration, validateModelId } from "../../utils/validate"
 import { vscode } from "../../utils/vscode"
 import { vscode } from "../../utils/vscode"
 import ApiOptions from "./ApiOptions"
 import ApiOptions from "./ApiOptions"
 import McpEnabledToggle from "../mcp/McpEnabledToggle"
 import McpEnabledToggle from "../mcp/McpEnabledToggle"
+import ApiConfigManager from "./ApiConfigManager"
 
 
 const IS_DEV = false // FIXME: use flags when packaging
 const IS_DEV = false // FIXME: use flags when packaging
 
 
@@ -55,10 +56,13 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
 		setAlwaysApproveResubmit,
 		setAlwaysApproveResubmit,
 		requestDelaySeconds,
 		requestDelaySeconds,
 		setRequestDelaySeconds,
 		setRequestDelaySeconds,
+		currentApiConfigName,
+		listApiConfigMeta,
 	} = useExtensionState()
 	} = useExtensionState()
 	const [apiErrorMessage, setApiErrorMessage] = useState<string | undefined>(undefined)
 	const [apiErrorMessage, setApiErrorMessage] = useState<string | undefined>(undefined)
 	const [modelIdErrorMessage, setModelIdErrorMessage] = useState<string | undefined>(undefined)
 	const [modelIdErrorMessage, setModelIdErrorMessage] = useState<string | undefined>(undefined)
 	const [commandInput, setCommandInput] = useState("")
 	const [commandInput, setCommandInput] = useState("")
+
 	const handleSubmit = () => {
 	const handleSubmit = () => {
 		const apiValidationResult = validateApiConfiguration(apiConfiguration)
 		const apiValidationResult = validateApiConfiguration(apiConfiguration)
 		const modelIdValidationResult = validateModelId(apiConfiguration, glamaModels, openRouterModels)
 		const modelIdValidationResult = validateModelId(apiConfiguration, glamaModels, openRouterModels)
@@ -89,6 +93,13 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
 			vscode.postMessage({ type: "mcpEnabled", bool: mcpEnabled })
 			vscode.postMessage({ type: "mcpEnabled", bool: mcpEnabled })
 			vscode.postMessage({ type: "alwaysApproveResubmit", bool: alwaysApproveResubmit })
 			vscode.postMessage({ type: "alwaysApproveResubmit", bool: alwaysApproveResubmit })
 			vscode.postMessage({ type: "requestDelaySeconds", value: requestDelaySeconds })
 			vscode.postMessage({ type: "requestDelaySeconds", value: requestDelaySeconds })
+			vscode.postMessage({ type: "currentApiConfigName", text: currentApiConfigName })
+			vscode.postMessage({
+				type: "upsertApiConfiguration",
+				text: currentApiConfigName,
+				apiConfiguration
+			})
+
 			onDone()
 			onDone()
 		}
 		}
 	}
 	}
@@ -152,8 +163,37 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
 				style={{ flexGrow: 1, overflowY: "scroll", paddingRight: 8, display: "flex", flexDirection: "column" }}>
 				style={{ flexGrow: 1, overflowY: "scroll", paddingRight: 8, display: "flex", flexDirection: "column" }}>
 				<div style={{ marginBottom: 5 }}>
 				<div style={{ marginBottom: 5 }}>
 					<h3 style={{ color: "var(--vscode-foreground)", margin: 0, marginBottom: 15 }}>Provider Settings</h3>
 					<h3 style={{ color: "var(--vscode-foreground)", margin: 0, marginBottom: 15 }}>Provider Settings</h3>
+					<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
+							})
+						}}
+					/>
 					<ApiOptions
 					<ApiOptions
-						showModelOptions={true}
 						apiErrorMessage={apiErrorMessage}
 						apiErrorMessage={apiErrorMessage}
 						modelIdErrorMessage={modelIdErrorMessage}
 						modelIdErrorMessage={modelIdErrorMessage}
 					/>
 					/>
@@ -405,10 +445,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
 					<div style={{ marginBottom: 5 }}>
 					<div style={{ marginBottom: 5 }}>
 						<VSCodeCheckbox
 						<VSCodeCheckbox
 							checked={alwaysAllowMcp}
 							checked={alwaysAllowMcp}
-							onChange={(e: any) => {
-								setAlwaysAllowMcp(e.target.checked)
-								vscode.postMessage({ type: "alwaysAllowMcp", bool: e.target.checked })
-							}}>
+							onChange={(e: any) => setAlwaysAllowMcp(e.target.checked)}>
 							<span style={{ fontWeight: "500" }}>Always approve MCP tools</span>
 							<span style={{ fontWeight: "500" }}>Always approve MCP tools</span>
 						</VSCodeCheckbox>
 						</VSCodeCheckbox>
 						<p style={{ fontSize: "12px", marginTop: "5px", color: "var(--vscode-descriptionForeground)" }}>
 						<p style={{ fontSize: "12px", marginTop: "5px", color: "var(--vscode-descriptionForeground)" }}>

+ 154 - 0
webview-ui/src/components/settings/__tests__/ApiConfigManager.test.tsx

@@ -0,0 +1,154 @@
+import { render, screen, fireEvent } from '@testing-library/react';
+import '@testing-library/jest-dom';
+import ApiConfigManager from '../ApiConfigManager';
+
+// Mock VSCode components
+jest.mock('@vscode/webview-ui-toolkit/react', () => ({
+  VSCodeButton: ({ children, onClick, title, disabled }: any) => (
+    <button onClick={onClick} title={title} disabled={disabled}>
+      {children}
+    </button>
+  ),
+  VSCodeTextField: ({ value, onInput, placeholder }: any) => (
+    <input
+      value={value}
+      onChange={e => onInput(e)}
+      placeholder={placeholder}
+      ref={undefined} // Explicitly set ref to undefined to avoid warning
+    />
+  ),
+}));
+
+describe('ApiConfigManager', () => {
+  const mockOnSelectConfig = jest.fn();
+  const mockOnDeleteConfig = jest.fn();
+  const mockOnRenameConfig = jest.fn();
+  const mockOnUpsertConfig = jest.fn();
+
+  const defaultProps = {
+    currentApiConfigName: 'Default Config',
+    listApiConfigMeta: [
+      { name: 'Default Config' },
+      { name: 'Another Config' }
+    ],
+    onSelectConfig: mockOnSelectConfig,
+    onDeleteConfig: mockOnDeleteConfig,
+    onRenameConfig: mockOnRenameConfig,
+    onUpsertConfig: mockOnUpsertConfig,
+  };
+
+  beforeEach(() => {
+    jest.clearAllMocks();
+  });
+
+  it('immediately creates a copy when clicking add button', () => {
+    render(<ApiConfigManager {...defaultProps} />);
+
+    // Find and click the add button
+    const addButton = screen.getByTitle('Add profile');
+    fireEvent.click(addButton);
+
+    // Verify that onUpsertConfig was called with the correct name
+    expect(mockOnUpsertConfig).toHaveBeenCalledTimes(1);
+    expect(mockOnUpsertConfig).toHaveBeenCalledWith('Default Config (copy)');
+  });
+
+  it('creates copy with correct name when current config has spaces', () => {
+    render(
+      <ApiConfigManager
+        {...defaultProps}
+        currentApiConfigName="My Test Config"
+      />
+    );
+
+    const addButton = screen.getByTitle('Add profile');
+    fireEvent.click(addButton);
+
+    expect(mockOnUpsertConfig).toHaveBeenCalledWith('My Test Config (copy)');
+  });
+
+  it('handles empty current config name gracefully', () => {
+    render(
+      <ApiConfigManager
+        {...defaultProps}
+        currentApiConfigName=""
+      />
+    );
+
+    const addButton = screen.getByTitle('Add profile');
+    fireEvent.click(addButton);
+
+    expect(mockOnUpsertConfig).toHaveBeenCalledWith(' (copy)');
+  });
+
+  it('allows renaming the current config', () => {
+    render(<ApiConfigManager {...defaultProps} />);
+    
+    // Start rename
+    const renameButton = screen.getByTitle('Rename profile');
+    fireEvent.click(renameButton);
+
+    // Find input and enter new name
+    const input = screen.getByDisplayValue('Default Config');
+    fireEvent.input(input, { target: { value: 'New Name' } });
+
+    // Save
+    const saveButton = screen.getByTitle('Save');
+    fireEvent.click(saveButton);
+
+    expect(mockOnRenameConfig).toHaveBeenCalledWith('Default Config', 'New Name');
+  });
+
+  it('allows selecting a different config', () => {
+    render(<ApiConfigManager {...defaultProps} />);
+    
+    const select = screen.getByRole('combobox');
+    fireEvent.change(select, { target: { value: 'Another Config' } });
+
+    expect(mockOnSelectConfig).toHaveBeenCalledWith('Another Config');
+  });
+
+  it('allows deleting the current config when not the only one', () => {
+    render(<ApiConfigManager {...defaultProps} />);
+    
+    const deleteButton = screen.getByTitle('Delete profile');
+    expect(deleteButton).not.toBeDisabled();
+    
+    fireEvent.click(deleteButton);
+    expect(mockOnDeleteConfig).toHaveBeenCalledWith('Default Config');
+  });
+
+  it('disables delete button when only one config exists', () => {
+    render(
+      <ApiConfigManager
+        {...defaultProps}
+        listApiConfigMeta={[{ name: 'Default Config' }]}
+      />
+    );
+    
+    const deleteButton = screen.getByTitle('Cannot delete the only profile');
+    expect(deleteButton).toHaveAttribute('disabled');
+  });
+
+  it('cancels rename operation when clicking cancel', () => {
+    render(<ApiConfigManager {...defaultProps} />);
+    
+    // Start rename
+    const renameButton = screen.getByTitle('Rename profile');
+    fireEvent.click(renameButton);
+
+    // Find input and enter new name
+    const input = screen.getByDisplayValue('Default Config');
+    fireEvent.input(input, { target: { value: 'New Name' } });
+
+    // Cancel
+    const cancelButton = screen.getByTitle('Cancel');
+    fireEvent.click(cancelButton);
+
+    // Verify rename was not called
+    expect(mockOnRenameConfig).not.toHaveBeenCalled();
+    
+    // Verify we're back to normal view
+    expect(screen.queryByDisplayValue('New Name')).not.toBeInTheDocument();
+  });
+});

+ 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
 // Mock VSCode components
 jest.mock('@vscode/webview-ui-toolkit/react', () => ({
 jest.mock('@vscode/webview-ui-toolkit/react', () => ({
   VSCodeButton: ({ children, onClick, appearance }: any) => (
   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', () => {
 describe('SettingsView - Allowed Commands', () => {
   beforeEach(() => {
   beforeEach(() => {
     jest.clearAllMocks()
     jest.clearAllMocks()

+ 4 - 9
webview-ui/src/components/welcome/WelcomeView.tsx

@@ -1,4 +1,4 @@
-import { VSCodeButton, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
+import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
 import { useEffect, useState } from "react"
 import { useEffect, useState } from "react"
 import { useExtensionState } from "../../context/ExtensionStateContext"
 import { useExtensionState } from "../../context/ExtensionStateContext"
 import { validateApiConfiguration } from "../../utils/validate"
 import { validateApiConfiguration } from "../../utils/validate"
@@ -24,21 +24,16 @@ const WelcomeView = () => {
 		<div style={{ position: "fixed", top: 0, left: 0, right: 0, bottom: 0, padding: "0 20px" }}>
 		<div style={{ position: "fixed", top: 0, left: 0, right: 0, bottom: 0, padding: "0 20px" }}>
 			<h2>Hi, I'm Cline</h2>
 			<h2>Hi, I'm Cline</h2>
 			<p>
 			<p>
-				I can do all kinds of tasks thanks to the latest breakthroughs in{" "}
-				<VSCodeLink
-					href="https://www-cdn.anthropic.com/fed9cc193a14b84131812372d8d5857f8f304c52/Model_Card_Claude_3_Addendum.pdf"
-					style={{ display: "inline" }}>
-					Claude 3.5 Sonnet's agentic coding capabilities
-				</VSCodeLink>{" "}
+				I can do all kinds of tasks thanks to the latest breakthroughs in agentic coding capabilities
 				and access to tools that let me create & edit files, explore complex projects, use the browser, and
 				and access to tools that let me create & edit files, explore complex projects, use the browser, and
 				execute terminal commands (with your permission, of course). I can even use MCP to create new tools and
 				execute terminal commands (with your permission, of course). I can even use MCP to create new tools and
 				extend my own capabilities.
 				extend my own capabilities.
 			</p>
 			</p>
 
 
-			<b>To get started, this extension needs an API provider for Claude 3.5 Sonnet.</b>
+			<b>To get started, this extension needs an API provider.</b>
 
 
 			<div style={{ marginTop: "10px" }}>
 			<div style={{ marginTop: "10px" }}>
-				<ApiOptions showModelOptions={false} />
+				<ApiOptions />
 				<VSCodeButton onClick={handleSubmit} disabled={disableLetsGoButton} style={{ marginTop: "3px" }}>
 				<VSCodeButton onClick={handleSubmit} disabled={disableLetsGoButton} style={{ marginTop: "3px" }}>
 					Let's go!
 					Let's go!
 				</VSCodeButton>
 				</VSCodeButton>

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

@@ -1,6 +1,6 @@
 import React, { createContext, useCallback, useContext, useEffect, useState } from "react"
 import React, { createContext, useCallback, useContext, useEffect, useState } from "react"
 import { useEvent } from "react-use"
 import { useEvent } from "react-use"
-import { ExtensionMessage, ExtensionState } from "../../../src/shared/ExtensionMessage"
+import { ApiConfigMeta, ExtensionMessage, ExtensionState } from "../../../src/shared/ExtensionMessage"
 import {
 import {
 	ApiConfiguration,
 	ApiConfiguration,
 	ModelInfo,
 	ModelInfo,
@@ -13,6 +13,9 @@ import { vscode } from "../utils/vscode"
 import { convertTextMateToHljs } from "../utils/textMateToHljs"
 import { convertTextMateToHljs } from "../utils/textMateToHljs"
 import { findLastIndex } from "../../../src/shared/array"
 import { findLastIndex } from "../../../src/shared/array"
 import { McpServer } from "../../../src/shared/mcp"
 import { McpServer } from "../../../src/shared/mcp"
+import {
+	checkExistKey
+} from "../../../src/shared/checkExistApiConfig"
 
 
 export interface ExtensionStateContextType extends ExtensionState {
 export interface ExtensionStateContextType extends ExtensionState {
 	didHydrateState: boolean
 	didHydrateState: boolean
@@ -50,6 +53,9 @@ export interface ExtensionStateContextType extends ExtensionState {
 	setAlwaysApproveResubmit: (value: boolean) => void
 	setAlwaysApproveResubmit: (value: boolean) => void
 	requestDelaySeconds: number
 	requestDelaySeconds: number
 	setRequestDelaySeconds: (value: number) => void
 	setRequestDelaySeconds: (value: number) => void
+	setCurrentApiConfigName: (value: string) => void
+	setListApiConfigMeta: (value: ApiConfigMeta[]) => void
+	onUpdateApiConfig: (apiConfig: ApiConfiguration) => void
 }
 }
 
 
 export const ExtensionStateContext = createContext<ExtensionStateContextType | undefined>(undefined)
 export const ExtensionStateContext = createContext<ExtensionStateContextType | undefined>(undefined)
@@ -72,7 +78,9 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
 		terminalOutputLineLimit: 500,
 		terminalOutputLineLimit: 500,
 		mcpEnabled: true,
 		mcpEnabled: true,
 		alwaysApproveResubmit: false,
 		alwaysApproveResubmit: false,
-		requestDelaySeconds: 5
+		requestDelaySeconds: 5,
+		currentApiConfigName: 'default',
+		listApiConfigMeta: [],
 	})
 	})
 	const [didHydrateState, setDidHydrateState] = useState(false)
 	const [didHydrateState, setDidHydrateState] = useState(false)
 	const [showWelcome, setShowWelcome] = useState(false)
 	const [showWelcome, setShowWelcome] = useState(false)
@@ -88,27 +96,24 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
 	const [openAiModels, setOpenAiModels] = useState<string[]>([])
 	const [openAiModels, setOpenAiModels] = useState<string[]>([])
 	const [mcpServers, setMcpServers] = useState<McpServer[]>([])
 	const [mcpServers, setMcpServers] = useState<McpServer[]>([])
 
 
+
+	const setListApiConfigMeta = useCallback((value: ApiConfigMeta[]) => setState((prevState) => ({ ...prevState, listApiConfigMeta: value })), [setState])
+
+	const onUpdateApiConfig = useCallback((apiConfig: ApiConfiguration) => {
+		vscode.postMessage({
+			type: "upsertApiConfiguration",
+			text: state.currentApiConfigName,
+			apiConfiguration: apiConfig,
+		})
+	}, [state])
+
 	const handleMessage = useCallback((event: MessageEvent) => {
 	const handleMessage = useCallback((event: MessageEvent) => {
 		const message: ExtensionMessage = event.data
 		const message: ExtensionMessage = event.data
 		switch (message.type) {
 		switch (message.type) {
 			case "state": {
 			case "state": {
 				setState(message.state!)
 				setState(message.state!)
 				const config = message.state?.apiConfiguration
 				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)
 				setShowWelcome(!hasKey)
 				setDidHydrateState(true)
 				setDidHydrateState(true)
 				break
 				break
@@ -162,8 +167,12 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
 				setMcpServers(message.mcpServers ?? [])
 				setMcpServers(message.mcpServers ?? [])
 				break
 				break
 			}
 			}
+			case "listApiConfig": {
+				setListApiConfigMeta(message.listApiConfig ?? [])
+				break
+			}
 		}
 		}
-	}, [])
+	}, [setListApiConfigMeta])
 
 
 	useEvent("message", handleMessage)
 	useEvent("message", handleMessage)
 
 
@@ -208,7 +217,10 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
 		setTerminalOutputLineLimit: (value) => setState((prevState) => ({ ...prevState, terminalOutputLineLimit: value })),
 		setTerminalOutputLineLimit: (value) => setState((prevState) => ({ ...prevState, terminalOutputLineLimit: value })),
 		setMcpEnabled: (value) => setState((prevState) => ({ ...prevState, mcpEnabled: value })),
 		setMcpEnabled: (value) => setState((prevState) => ({ ...prevState, mcpEnabled: value })),
 		setAlwaysApproveResubmit: (value) => setState((prevState) => ({ ...prevState, alwaysApproveResubmit: 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,
+		onUpdateApiConfig
 	}
 	}
 
 
 	return <ExtensionStateContext.Provider value={contextValue}>{children}</ExtensionStateContext.Provider>
 	return <ExtensionStateContext.Provider value={contextValue}>{children}</ExtensionStateContext.Provider>