mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-20 12:21:13 -05:00
feat: config manager using secret store
This commit is contained in:
153
src/core/config/ConfigManager.ts
Normal file
153
src/core/config/ConfigManager.ts
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
import { ExtensionContext } from 'vscode'
|
||||||
|
import { ApiConfiguration } from '../../shared/api'
|
||||||
|
import { ApiConfigMeta } from '../../shared/ExtensionMessage'
|
||||||
|
|
||||||
|
export interface ApiConfigData {
|
||||||
|
currentApiConfigName: string
|
||||||
|
apiConfigs: {
|
||||||
|
[key: string]: ApiConfiguration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ConfigManager {
|
||||||
|
private readonly defaultConfig: ApiConfigData = {
|
||||||
|
currentApiConfigName: 'default',
|
||||||
|
apiConfigs: {
|
||||||
|
default: {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private readonly SCOPE_PREFIX = "cline_config_"
|
||||||
|
private readonly context: ExtensionContext
|
||||||
|
|
||||||
|
constructor(context: ExtensionContext) {
|
||||||
|
this.context = context
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize config if it doesn't exist
|
||||||
|
*/
|
||||||
|
async initConfig(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const config = await this.readConfig()
|
||||||
|
console.log("config", config)
|
||||||
|
if (!config) {
|
||||||
|
await this.writeConfig(this.defaultConfig)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to initialize config: ${error}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List all available configs with metadata
|
||||||
|
*/
|
||||||
|
async ListConfig(): Promise<ApiConfigMeta[]> {
|
||||||
|
try {
|
||||||
|
const config = await this.readConfig()
|
||||||
|
return Object.entries(config.apiConfigs).map(([name, apiConfig]) => ({
|
||||||
|
name,
|
||||||
|
apiProvider: apiConfig.apiProvider,
|
||||||
|
}))
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to list configs: ${error}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save a config with the given name
|
||||||
|
*/
|
||||||
|
async SaveConfig(name: string, config: ApiConfiguration): Promise<void> {
|
||||||
|
try {
|
||||||
|
const currentConfig = await this.readConfig()
|
||||||
|
currentConfig.apiConfigs[name] = config
|
||||||
|
await this.writeConfig(currentConfig)
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to save config: ${error}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load a config by name
|
||||||
|
*/
|
||||||
|
async LoadConfig(name: string): Promise<ApiConfiguration> {
|
||||||
|
try {
|
||||||
|
const config = await this.readConfig()
|
||||||
|
const apiConfig = config.apiConfigs[name]
|
||||||
|
|
||||||
|
if (!apiConfig) {
|
||||||
|
throw new Error(`Config '${name}' not found`)
|
||||||
|
}
|
||||||
|
|
||||||
|
config.currentApiConfigName = name;
|
||||||
|
await this.writeConfig(config)
|
||||||
|
|
||||||
|
return apiConfig
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to load config: ${error}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a config by name
|
||||||
|
*/
|
||||||
|
async DeleteConfig(name: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
const currentConfig = await this.readConfig()
|
||||||
|
if (!currentConfig.apiConfigs[name]) {
|
||||||
|
throw new Error(`Config '${name}' not found`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't allow deleting the default config
|
||||||
|
if (Object.keys(currentConfig.apiConfigs).length === 1) {
|
||||||
|
throw new Error(`Cannot delete the last remaining configuration.`)
|
||||||
|
}
|
||||||
|
|
||||||
|
delete currentConfig.apiConfigs[name]
|
||||||
|
await this.writeConfig(currentConfig)
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to delete config: ${error}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the current active API configuration
|
||||||
|
*/
|
||||||
|
async SetCurrentConfig(name: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
const currentConfig = await this.readConfig()
|
||||||
|
if (!currentConfig.apiConfigs[name]) {
|
||||||
|
throw new Error(`Config '${name}' not found`)
|
||||||
|
}
|
||||||
|
|
||||||
|
currentConfig.currentApiConfigName = name
|
||||||
|
await this.writeConfig(currentConfig)
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to set current config: ${error}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async readConfig(): Promise<ApiConfigData> {
|
||||||
|
try {
|
||||||
|
const configKey = `${this.SCOPE_PREFIX}api_config`
|
||||||
|
const content = await this.context.secrets.get(configKey)
|
||||||
|
|
||||||
|
if (!content) {
|
||||||
|
return this.defaultConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.parse(content)
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to read config from secrets: ${error}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async writeConfig(config: ApiConfigData): Promise<void> {
|
||||||
|
try {
|
||||||
|
const configKey = `${this.SCOPE_PREFIX}api_config`
|
||||||
|
const content = JSON.stringify(config, null, 2)
|
||||||
|
await this.context.secrets.store(configKey, content)
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to write config to secrets: ${error}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
348
src/core/config/__tests__/ConfigManager.test.ts
Normal file
348
src/core/config/__tests__/ConfigManager.test.ts
Normal file
@@ -0,0 +1,348 @@
|
|||||||
|
import { ExtensionContext } from 'vscode'
|
||||||
|
import { ConfigManager } from '../ConfigManager'
|
||||||
|
import { ApiConfiguration } from '../../../shared/api'
|
||||||
|
import { ApiConfigData } from '../ConfigManager'
|
||||||
|
|
||||||
|
// Mock VSCode ExtensionContext
|
||||||
|
const mockSecrets = {
|
||||||
|
get: jest.fn(),
|
||||||
|
store: jest.fn(),
|
||||||
|
delete: jest.fn()
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockContext = {
|
||||||
|
secrets: mockSecrets
|
||||||
|
} as unknown as ExtensionContext
|
||||||
|
|
||||||
|
describe('ConfigManager', () => {
|
||||||
|
let configManager: ConfigManager
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
configManager = new ConfigManager(mockContext)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('initConfig', () => {
|
||||||
|
it('should not write to storage when secrets.get returns null', async () => {
|
||||||
|
// Mock readConfig to return null
|
||||||
|
mockSecrets.get.mockResolvedValueOnce(null)
|
||||||
|
|
||||||
|
await configManager.initConfig()
|
||||||
|
|
||||||
|
// Should not write to storage because readConfig returns defaultConfig
|
||||||
|
expect(mockSecrets.store).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not initialize config if it exists', async () => {
|
||||||
|
mockSecrets.get.mockResolvedValue(JSON.stringify({
|
||||||
|
currentApiConfigName: 'default',
|
||||||
|
apiConfigs: {
|
||||||
|
default: {}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
await configManager.initConfig()
|
||||||
|
|
||||||
|
expect(mockSecrets.store).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should throw error if secrets storage fails', async () => {
|
||||||
|
mockSecrets.get.mockRejectedValue(new Error('Storage failed'))
|
||||||
|
|
||||||
|
await expect(configManager.initConfig()).rejects.toThrow(
|
||||||
|
'Failed to initialize config: Error: Failed to read config from secrets: Error: Storage failed'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('ListConfig', () => {
|
||||||
|
it('should list all available configs', async () => {
|
||||||
|
const existingConfig: ApiConfigData = {
|
||||||
|
currentApiConfigName: 'default',
|
||||||
|
apiConfigs: {
|
||||||
|
default: {},
|
||||||
|
test: {
|
||||||
|
apiProvider: 'anthropic'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mockSecrets.get.mockResolvedValue(JSON.stringify(existingConfig))
|
||||||
|
|
||||||
|
const configs = await configManager.ListConfig()
|
||||||
|
expect(configs).toEqual([
|
||||||
|
{ name: 'default', apiProvider: undefined },
|
||||||
|
{ name: 'test', apiProvider: 'anthropic' }
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle empty config file', async () => {
|
||||||
|
const emptyConfig: ApiConfigData = {
|
||||||
|
currentApiConfigName: 'default',
|
||||||
|
apiConfigs: {}
|
||||||
|
}
|
||||||
|
|
||||||
|
mockSecrets.get.mockResolvedValue(JSON.stringify(emptyConfig))
|
||||||
|
|
||||||
|
const configs = await configManager.ListConfig()
|
||||||
|
expect(configs).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should throw error if reading from secrets fails', async () => {
|
||||||
|
mockSecrets.get.mockRejectedValue(new Error('Read failed'))
|
||||||
|
|
||||||
|
await expect(configManager.ListConfig()).rejects.toThrow(
|
||||||
|
'Failed to list configs: Error: Failed to read config from secrets: Error: Read failed'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('SaveConfig', () => {
|
||||||
|
it('should save new config', async () => {
|
||||||
|
mockSecrets.get.mockResolvedValue(JSON.stringify({
|
||||||
|
currentApiConfigName: 'default',
|
||||||
|
apiConfigs: {
|
||||||
|
default: {}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
const newConfig: ApiConfiguration = {
|
||||||
|
apiProvider: 'anthropic',
|
||||||
|
apiKey: 'test-key'
|
||||||
|
}
|
||||||
|
|
||||||
|
await configManager.SaveConfig('test', newConfig)
|
||||||
|
|
||||||
|
const expectedConfig = {
|
||||||
|
currentApiConfigName: 'default',
|
||||||
|
apiConfigs: {
|
||||||
|
default: {},
|
||||||
|
test: newConfig
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(mockSecrets.store).toHaveBeenCalledWith(
|
||||||
|
'cline_config_api_config',
|
||||||
|
JSON.stringify(expectedConfig, null, 2)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should update existing config', async () => {
|
||||||
|
const existingConfig: ApiConfigData = {
|
||||||
|
currentApiConfigName: 'default',
|
||||||
|
apiConfigs: {
|
||||||
|
test: {
|
||||||
|
apiProvider: 'anthropic',
|
||||||
|
apiKey: 'old-key'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mockSecrets.get.mockResolvedValue(JSON.stringify(existingConfig))
|
||||||
|
|
||||||
|
const updatedConfig: ApiConfiguration = {
|
||||||
|
apiProvider: 'anthropic',
|
||||||
|
apiKey: 'new-key'
|
||||||
|
}
|
||||||
|
|
||||||
|
await configManager.SaveConfig('test', updatedConfig)
|
||||||
|
|
||||||
|
const expectedConfig = {
|
||||||
|
currentApiConfigName: 'default',
|
||||||
|
apiConfigs: {
|
||||||
|
test: updatedConfig
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(mockSecrets.store).toHaveBeenCalledWith(
|
||||||
|
'cline_config_api_config',
|
||||||
|
JSON.stringify(expectedConfig, null, 2)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should throw error if secrets storage fails', async () => {
|
||||||
|
mockSecrets.get.mockResolvedValue(JSON.stringify({
|
||||||
|
currentApiConfigName: 'default',
|
||||||
|
apiConfigs: { default: {} }
|
||||||
|
}))
|
||||||
|
mockSecrets.store.mockRejectedValueOnce(new Error('Storage failed'))
|
||||||
|
|
||||||
|
await expect(configManager.SaveConfig('test', {})).rejects.toThrow(
|
||||||
|
'Failed to save config: Error: Failed to write config to secrets: Error: Storage failed'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('DeleteConfig', () => {
|
||||||
|
it('should delete existing config', async () => {
|
||||||
|
const existingConfig: ApiConfigData = {
|
||||||
|
currentApiConfigName: 'default',
|
||||||
|
apiConfigs: {
|
||||||
|
default: {},
|
||||||
|
test: {
|
||||||
|
apiProvider: 'anthropic'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mockSecrets.get.mockResolvedValue(JSON.stringify(existingConfig))
|
||||||
|
|
||||||
|
await configManager.DeleteConfig('test')
|
||||||
|
|
||||||
|
const expectedConfig = {
|
||||||
|
currentApiConfigName: 'default',
|
||||||
|
apiConfigs: {
|
||||||
|
default: {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(mockSecrets.store).toHaveBeenCalledWith(
|
||||||
|
'cline_config_api_config',
|
||||||
|
JSON.stringify(expectedConfig, null, 2)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should throw error when trying to delete non-existent config', async () => {
|
||||||
|
mockSecrets.get.mockResolvedValue(JSON.stringify({
|
||||||
|
currentApiConfigName: 'default',
|
||||||
|
apiConfigs: { default: {} }
|
||||||
|
}))
|
||||||
|
|
||||||
|
await expect(configManager.DeleteConfig('nonexistent')).rejects.toThrow(
|
||||||
|
"Config 'nonexistent' not found"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should throw error when trying to delete last remaining config', async () => {
|
||||||
|
mockSecrets.get.mockResolvedValue(JSON.stringify({
|
||||||
|
currentApiConfigName: 'default',
|
||||||
|
apiConfigs: { default: {} }
|
||||||
|
}))
|
||||||
|
|
||||||
|
await expect(configManager.DeleteConfig('default')).rejects.toThrow(
|
||||||
|
'Cannot delete the last remaining configuration.'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('LoadConfig', () => {
|
||||||
|
it('should load config and update current config name', async () => {
|
||||||
|
const existingConfig: ApiConfigData = {
|
||||||
|
currentApiConfigName: 'default',
|
||||||
|
apiConfigs: {
|
||||||
|
test: {
|
||||||
|
apiProvider: 'anthropic',
|
||||||
|
apiKey: 'test-key'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mockSecrets.get.mockResolvedValue(JSON.stringify(existingConfig))
|
||||||
|
|
||||||
|
const config = await configManager.LoadConfig('test')
|
||||||
|
|
||||||
|
expect(config).toEqual({
|
||||||
|
apiProvider: 'anthropic',
|
||||||
|
apiKey: 'test-key'
|
||||||
|
})
|
||||||
|
|
||||||
|
const expectedConfig = {
|
||||||
|
currentApiConfigName: 'test',
|
||||||
|
apiConfigs: {
|
||||||
|
test: {
|
||||||
|
apiProvider: 'anthropic',
|
||||||
|
apiKey: 'test-key'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(mockSecrets.store).toHaveBeenCalledWith(
|
||||||
|
'cline_config_api_config',
|
||||||
|
JSON.stringify(expectedConfig, null, 2)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should throw error when config does not exist', async () => {
|
||||||
|
mockSecrets.get.mockResolvedValue(JSON.stringify({
|
||||||
|
currentApiConfigName: 'default',
|
||||||
|
apiConfigs: { default: {} }
|
||||||
|
}))
|
||||||
|
|
||||||
|
await expect(configManager.LoadConfig('nonexistent')).rejects.toThrow(
|
||||||
|
"Config 'nonexistent' not found"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should throw error if secrets storage fails', async () => {
|
||||||
|
mockSecrets.get.mockResolvedValue(JSON.stringify({
|
||||||
|
currentApiConfigName: 'default',
|
||||||
|
apiConfigs: {
|
||||||
|
test: { apiProvider: 'anthropic' }
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
mockSecrets.store.mockRejectedValueOnce(new Error('Storage failed'))
|
||||||
|
|
||||||
|
await expect(configManager.LoadConfig('test')).rejects.toThrow(
|
||||||
|
'Failed to load config: Error: Failed to write config to secrets: Error: Storage failed'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('SetCurrentConfig', () => {
|
||||||
|
it('should set current config', async () => {
|
||||||
|
const existingConfig: ApiConfigData = {
|
||||||
|
currentApiConfigName: 'default',
|
||||||
|
apiConfigs: {
|
||||||
|
default: {},
|
||||||
|
test: {
|
||||||
|
apiProvider: 'anthropic'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mockSecrets.get.mockResolvedValue(JSON.stringify(existingConfig))
|
||||||
|
|
||||||
|
await configManager.SetCurrentConfig('test')
|
||||||
|
|
||||||
|
const expectedConfig = {
|
||||||
|
currentApiConfigName: 'test',
|
||||||
|
apiConfigs: {
|
||||||
|
default: {},
|
||||||
|
test: {
|
||||||
|
apiProvider: 'anthropic'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(mockSecrets.store).toHaveBeenCalledWith(
|
||||||
|
'cline_config_api_config',
|
||||||
|
JSON.stringify(expectedConfig, null, 2)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should throw error when config does not exist', async () => {
|
||||||
|
mockSecrets.get.mockResolvedValue(JSON.stringify({
|
||||||
|
currentApiConfigName: 'default',
|
||||||
|
apiConfigs: { default: {} }
|
||||||
|
}))
|
||||||
|
|
||||||
|
await expect(configManager.SetCurrentConfig('nonexistent')).rejects.toThrow(
|
||||||
|
"Config 'nonexistent' not found"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should throw error if secrets storage fails', async () => {
|
||||||
|
mockSecrets.get.mockResolvedValue(JSON.stringify({
|
||||||
|
currentApiConfigName: 'default',
|
||||||
|
apiConfigs: {
|
||||||
|
test: { apiProvider: 'anthropic' }
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
mockSecrets.store.mockRejectedValueOnce(new Error('Storage failed'))
|
||||||
|
|
||||||
|
await expect(configManager.SetCurrentConfig('test')).rejects.toThrow(
|
||||||
|
'Failed to set current config: Error: Failed to write config to secrets: Error: Storage failed'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -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
|
||||||
@@ -43,6 +45,7 @@ type SecretKey =
|
|||||||
| "geminiApiKey"
|
| "geminiApiKey"
|
||||||
| "openAiNativeApiKey"
|
| "openAiNativeApiKey"
|
||||||
| "deepSeekApiKey"
|
| "deepSeekApiKey"
|
||||||
|
| "apiConfigPassword"
|
||||||
type GlobalStateKey =
|
type GlobalStateKey =
|
||||||
| "apiProvider"
|
| "apiProvider"
|
||||||
| "apiModelId"
|
| "apiModelId"
|
||||||
@@ -85,6 +88,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 +109,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 +119,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)
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -321,15 +329,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
|
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
|
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.
|
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}';">
|
<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 +418,33 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
this.configManager.ListConfig().then(async (listApiConfig) => {
|
||||||
|
|
||||||
|
if (!listApiConfig) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (listApiConfig.length === 1) {
|
||||||
|
// check if first time init then sync with exist config
|
||||||
|
if (!checkExistKey(listApiConfig[0]) && listApiConfig[0].name === "default") {
|
||||||
|
const {
|
||||||
|
apiConfiguration,
|
||||||
|
} = await this.getState()
|
||||||
|
await this.configManager.SaveConfig("default", apiConfiguration)
|
||||||
|
listApiConfig[0].apiProvider = apiConfiguration.apiProvider
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
[
|
||||||
|
await this.updateGlobalState("listApiConfigMeta", listApiConfig),
|
||||||
|
await this.postMessageToWebview({ type: "listApiConfig", listApiConfig })
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}).catch(console.error);
|
||||||
|
|
||||||
break
|
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 +459,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
break
|
break
|
||||||
case "apiConfiguration":
|
case "apiConfiguration":
|
||||||
if (message.apiConfiguration) {
|
if (message.apiConfiguration) {
|
||||||
const {
|
await this.updateApiConfiguration(message.apiConfiguration)
|
||||||
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.postStateToWebview()
|
await this.postStateToWebview()
|
||||||
break
|
break
|
||||||
@@ -805,6 +777,106 @@ 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.updateGlobalState("currentApiConfigName", message.text),
|
||||||
|
this.updateGlobalState("listApiConfigMeta", listApiConfig),
|
||||||
|
])
|
||||||
|
|
||||||
|
this.postStateToWebview()
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error create new api configuration:", error)
|
||||||
|
vscode.window.showErrorMessage("Failed to create api configuration")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case "renameApiConfiguration":
|
||||||
|
if (message.values && message.apiConfiguration) {
|
||||||
|
try {
|
||||||
|
|
||||||
|
const {oldName, newName} = message.values
|
||||||
|
|
||||||
|
await this.configManager.SaveConfig(newName, message.apiConfiguration);
|
||||||
|
|
||||||
|
await this.configManager.DeleteConfig(oldName)
|
||||||
|
|
||||||
|
let listApiConfig = await this.configManager.ListConfig();
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
this.updateGlobalState("currentApiConfigName", newName),
|
||||||
|
this.updateGlobalState("listApiConfigMeta", listApiConfig),
|
||||||
|
])
|
||||||
|
|
||||||
|
this.postStateToWebview()
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error create new api configuration:", error)
|
||||||
|
vscode.window.showErrorMessage("Failed to create api configuration")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case "loadApiConfiguration":
|
||||||
|
if (message.text) {
|
||||||
|
try {
|
||||||
|
const apiConfig = await this.configManager.LoadConfig(message.text);
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
this.updateGlobalState("currentApiConfigName", message.text),
|
||||||
|
this.updateApiConfiguration(apiConfig),
|
||||||
|
])
|
||||||
|
|
||||||
|
await this.postStateToWebview()
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error load api configuration:", error)
|
||||||
|
vscode.window.showErrorMessage("Failed to load api configuration")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case "deleteApiConfiguration":
|
||||||
|
if (message.text) {
|
||||||
|
try {
|
||||||
|
await this.configManager.DeleteConfig(message.text);
|
||||||
|
let currentApiConfigName = (await this.getGlobalState("currentApiConfigName") as string) ?? "default"
|
||||||
|
|
||||||
|
if (message.text === currentApiConfigName) {
|
||||||
|
await this.updateGlobalState("currentApiConfigName", "default")
|
||||||
|
}
|
||||||
|
|
||||||
|
let listApiConfig = await this.configManager.ListConfig();
|
||||||
|
await this.updateGlobalState("listApiConfigMeta", listApiConfig)
|
||||||
|
this.postMessageToWebview({ type: "listApiConfig", listApiConfig })
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error delete api configuration:", error)
|
||||||
|
vscode.window.showErrorMessage("Failed to delete api configuration")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case "getListApiConfiguration":
|
||||||
|
try {
|
||||||
|
let listApiConfig = await this.configManager.ListConfig();
|
||||||
|
await this.updateGlobalState("listApiConfigMeta", listApiConfig)
|
||||||
|
this.postMessageToWebview({ type: "listApiConfig", listApiConfig })
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error get list api configuration:", error)
|
||||||
|
vscode.window.showErrorMessage("Failed to get list api configuration")
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case "setApiConfigPassword":
|
||||||
|
if (message.text) {
|
||||||
|
try {
|
||||||
|
await this.storeSecret("apiConfigPassword", message.text !== "" ? message.text : undefined)
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error set apiKey password:", error)
|
||||||
|
vscode.window.showErrorMessage("Failed to set apiKey password")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
null,
|
null,
|
||||||
@@ -812,6 +884,74 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async updateApiConfiguration(apiConfiguration: ApiConfiguration) {
|
||||||
|
const {
|
||||||
|
apiProvider,
|
||||||
|
apiModelId,
|
||||||
|
apiKey,
|
||||||
|
glamaModelId,
|
||||||
|
glamaModelInfo,
|
||||||
|
glamaApiKey,
|
||||||
|
openRouterApiKey,
|
||||||
|
awsAccessKey,
|
||||||
|
awsSecretKey,
|
||||||
|
awsSessionToken,
|
||||||
|
awsRegion,
|
||||||
|
awsUseCrossRegionInference,
|
||||||
|
vertexProjectId,
|
||||||
|
vertexRegion,
|
||||||
|
openAiBaseUrl,
|
||||||
|
openAiApiKey,
|
||||||
|
openAiModelId,
|
||||||
|
ollamaModelId,
|
||||||
|
ollamaBaseUrl,
|
||||||
|
lmStudioModelId,
|
||||||
|
lmStudioBaseUrl,
|
||||||
|
anthropicBaseUrl,
|
||||||
|
geminiApiKey,
|
||||||
|
openAiNativeApiKey,
|
||||||
|
deepSeekApiKey,
|
||||||
|
azureApiVersion,
|
||||||
|
openAiStreamingEnabled,
|
||||||
|
openRouterModelId,
|
||||||
|
openRouterModelInfo,
|
||||||
|
openRouterUseMiddleOutTransform,
|
||||||
|
} = apiConfiguration
|
||||||
|
await this.updateGlobalState("apiProvider", apiProvider)
|
||||||
|
await this.updateGlobalState("apiModelId", apiModelId)
|
||||||
|
await this.storeSecret("apiKey", apiKey)
|
||||||
|
await this.updateGlobalState("glamaModelId", glamaModelId)
|
||||||
|
await this.updateGlobalState("glamaModelInfo", glamaModelInfo)
|
||||||
|
await this.storeSecret("glamaApiKey", glamaApiKey)
|
||||||
|
await this.storeSecret("openRouterApiKey", openRouterApiKey)
|
||||||
|
await this.storeSecret("awsAccessKey", awsAccessKey)
|
||||||
|
await this.storeSecret("awsSecretKey", awsSecretKey)
|
||||||
|
await this.storeSecret("awsSessionToken", awsSessionToken)
|
||||||
|
await this.updateGlobalState("awsRegion", awsRegion)
|
||||||
|
await this.updateGlobalState("awsUseCrossRegionInference", awsUseCrossRegionInference)
|
||||||
|
await this.updateGlobalState("vertexProjectId", vertexProjectId)
|
||||||
|
await this.updateGlobalState("vertexRegion", vertexRegion)
|
||||||
|
await this.updateGlobalState("openAiBaseUrl", openAiBaseUrl)
|
||||||
|
await this.storeSecret("openAiApiKey", openAiApiKey)
|
||||||
|
await this.updateGlobalState("openAiModelId", openAiModelId)
|
||||||
|
await this.updateGlobalState("ollamaModelId", ollamaModelId)
|
||||||
|
await this.updateGlobalState("ollamaBaseUrl", ollamaBaseUrl)
|
||||||
|
await this.updateGlobalState("lmStudioModelId", lmStudioModelId)
|
||||||
|
await this.updateGlobalState("lmStudioBaseUrl", lmStudioBaseUrl)
|
||||||
|
await this.updateGlobalState("anthropicBaseUrl", anthropicBaseUrl)
|
||||||
|
await this.storeSecret("geminiApiKey", geminiApiKey)
|
||||||
|
await this.storeSecret("openAiNativeApiKey", openAiNativeApiKey)
|
||||||
|
await this.storeSecret("deepSeekApiKey", deepSeekApiKey)
|
||||||
|
await this.updateGlobalState("azureApiVersion", azureApiVersion)
|
||||||
|
await this.updateGlobalState("openAiStreamingEnabled", openAiStreamingEnabled)
|
||||||
|
await this.updateGlobalState("openRouterModelId", openRouterModelId)
|
||||||
|
await this.updateGlobalState("openRouterModelInfo", openRouterModelInfo)
|
||||||
|
await this.updateGlobalState("openRouterUseMiddleOutTransform", openRouterUseMiddleOutTransform)
|
||||||
|
if (this.cline) {
|
||||||
|
this.cline.api = buildApiHandler(apiConfiguration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async updateCustomInstructions(instructions?: string) {
|
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,6 +1396,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
mcpEnabled,
|
mcpEnabled,
|
||||||
alwaysApproveResubmit,
|
alwaysApproveResubmit,
|
||||||
requestDelaySeconds,
|
requestDelaySeconds,
|
||||||
|
currentApiConfigName,
|
||||||
|
listApiConfigMeta,
|
||||||
|
apiKeyPassword
|
||||||
} = await this.getState()
|
} = await this.getState()
|
||||||
|
|
||||||
const allowedCommands = vscode.workspace
|
const allowedCommands = vscode.workspace
|
||||||
@@ -1290,6 +1433,9 @@ 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 ?? [],
|
||||||
|
apiKeyPassword: apiKeyPassword ?? ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1397,6 +1543,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
mcpEnabled,
|
mcpEnabled,
|
||||||
alwaysApproveResubmit,
|
alwaysApproveResubmit,
|
||||||
requestDelaySeconds,
|
requestDelaySeconds,
|
||||||
|
currentApiConfigName,
|
||||||
|
listApiConfigMeta,
|
||||||
|
apiKeyPassword,
|
||||||
] = 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 +1598,9 @@ 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>,
|
||||||
|
this.getSecret("apiConfigPassword") as Promise<string | undefined>,
|
||||||
])
|
])
|
||||||
|
|
||||||
let apiProvider: ApiProvider
|
let apiProvider: ApiProvider
|
||||||
@@ -1545,6 +1697,9 @@ 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 ?? [],
|
||||||
|
apiKeyPassword: apiKeyPassword ?? ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1622,6 +1777,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
"geminiApiKey",
|
"geminiApiKey",
|
||||||
"openAiNativeApiKey",
|
"openAiNativeApiKey",
|
||||||
"deepSeekApiKey",
|
"deepSeekApiKey",
|
||||||
|
"apiConfigPassword"
|
||||||
]
|
]
|
||||||
for (const key of secretKeys) {
|
for (const key of secretKeys) {
|
||||||
await this.storeSecret(key, undefined)
|
await this.storeSecret(key, undefined)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
src/shared/checkExistApiConfig.ts
Normal file
19
src/shared/checkExistApiConfig.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { ApiConfiguration } from "../shared/api";
|
||||||
|
|
||||||
|
export function checkExistKey(config: ApiConfiguration | undefined) {
|
||||||
|
return config
|
||||||
|
? [
|
||||||
|
config.apiKey,
|
||||||
|
config.glamaApiKey,
|
||||||
|
config.openRouterApiKey,
|
||||||
|
config.awsRegion,
|
||||||
|
config.vertexProjectId,
|
||||||
|
config.openAiApiKey,
|
||||||
|
config.ollamaModelId,
|
||||||
|
config.lmStudioModelId,
|
||||||
|
config.geminiApiKey,
|
||||||
|
config.openAiNativeApiKey,
|
||||||
|
config.deepSeekApiKey
|
||||||
|
].some((key) => key !== undefined)
|
||||||
|
: false;
|
||||||
|
}
|
||||||
165
webview-ui/src/components/settings/ApiConfigManager.tsx
Normal file
165
webview-ui/src/components/settings/ApiConfigManager.tsx
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
import { VSCodeButton, VSCodeDivider, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
|
||||||
|
import { memo, useState } from "react"
|
||||||
|
import { ApiConfigMeta } from "../../../../src/shared/ExtensionMessage"
|
||||||
|
|
||||||
|
interface ApiConfigManagerProps {
|
||||||
|
currentApiConfigName?: string
|
||||||
|
listApiConfigMeta?: ApiConfigMeta[]
|
||||||
|
onSelectConfig: (configName: string) => void
|
||||||
|
onDeleteConfig: (configName: string) => void
|
||||||
|
onRenameConfig: (oldName: string, newName: string) => void
|
||||||
|
onUpsertConfig: (configName: string) => void
|
||||||
|
// setDraftNewConfig: (mode: boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const ApiConfigManager = ({
|
||||||
|
currentApiConfigName,
|
||||||
|
listApiConfigMeta,
|
||||||
|
onSelectConfig,
|
||||||
|
onDeleteConfig,
|
||||||
|
onRenameConfig,
|
||||||
|
onUpsertConfig,
|
||||||
|
// setDraftNewConfig,
|
||||||
|
}: ApiConfigManagerProps) => {
|
||||||
|
const [isNewMode, setIsNewMode] = useState(false);
|
||||||
|
const [isRenameMode, setIsRenameMode] = useState(false);
|
||||||
|
const [newConfigName, setNewConfigName] = useState("");
|
||||||
|
const [renamedConfigName, setRenamedConfigName] = useState("");
|
||||||
|
|
||||||
|
const handleNewConfig = () => {
|
||||||
|
setIsNewMode(true);
|
||||||
|
setNewConfigName("");
|
||||||
|
// setDraftNewConfig(true)
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveNewConfig = () => {
|
||||||
|
if (newConfigName.trim()) {
|
||||||
|
onUpsertConfig(newConfigName.trim());
|
||||||
|
setIsNewMode(false);
|
||||||
|
setNewConfigName("");
|
||||||
|
// setDraftNewConfig(false)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancelNewConfig = () => {
|
||||||
|
setIsNewMode(false);
|
||||||
|
setNewConfigName("");
|
||||||
|
// setDraftNewConfig(false)
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleStartRename = () => {
|
||||||
|
setIsRenameMode(true);
|
||||||
|
setRenamedConfigName(currentApiConfigName || "");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveRename = () => {
|
||||||
|
if (renamedConfigName.trim() && currentApiConfigName) {
|
||||||
|
onRenameConfig(currentApiConfigName, renamedConfigName.trim());
|
||||||
|
setIsRenameMode(false);
|
||||||
|
setRenamedConfigName("");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancelRename = () => {
|
||||||
|
setIsRenameMode(false);
|
||||||
|
setRenamedConfigName("");
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<label style={{ fontWeight: "500", display: "block", marginBottom: 5 }}>
|
||||||
|
API Configuration
|
||||||
|
</label>
|
||||||
|
<div style={{ display: "flex", gap: "8px", alignItems: "center" }}>
|
||||||
|
{isNewMode ? (
|
||||||
|
<>
|
||||||
|
<VSCodeTextField
|
||||||
|
value={newConfigName}
|
||||||
|
onInput={(e: any) => setNewConfigName(e.target.value)}
|
||||||
|
placeholder="Enter configuration name"
|
||||||
|
style={{ flexGrow: 1 }}
|
||||||
|
/>
|
||||||
|
<VSCodeButton
|
||||||
|
appearance="secondary"
|
||||||
|
disabled={!newConfigName.trim()}
|
||||||
|
onClick={handleSaveNewConfig}
|
||||||
|
>
|
||||||
|
<span className="codicon codicon-check" /> Save
|
||||||
|
</VSCodeButton>
|
||||||
|
<VSCodeButton
|
||||||
|
appearance="secondary"
|
||||||
|
onClick={handleCancelNewConfig}
|
||||||
|
>
|
||||||
|
<span className="codicon codicon-close" /> Cancel
|
||||||
|
</VSCodeButton>
|
||||||
|
</>
|
||||||
|
) : isRenameMode ? (
|
||||||
|
<>
|
||||||
|
<VSCodeTextField
|
||||||
|
value={renamedConfigName}
|
||||||
|
onInput={(e: any) => setRenamedConfigName(e.target.value)}
|
||||||
|
placeholder="Enter new name"
|
||||||
|
style={{ flexGrow: 1 }}
|
||||||
|
/>
|
||||||
|
<VSCodeButton
|
||||||
|
appearance="secondary"
|
||||||
|
disabled={!renamedConfigName.trim()}
|
||||||
|
onClick={handleSaveRename}
|
||||||
|
>
|
||||||
|
<span className="codicon codicon-check" /> Save
|
||||||
|
</VSCodeButton>
|
||||||
|
<VSCodeButton
|
||||||
|
appearance="secondary"
|
||||||
|
onClick={handleCancelRename}
|
||||||
|
>
|
||||||
|
<span className="codicon codicon-close" /> Cancel
|
||||||
|
</VSCodeButton>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<select
|
||||||
|
value={currentApiConfigName}
|
||||||
|
onChange={(e) => onSelectConfig(e.target.value)}
|
||||||
|
style={{
|
||||||
|
flexGrow: 1,
|
||||||
|
padding: "4px 8px",
|
||||||
|
backgroundColor: "var(--vscode-input-background)",
|
||||||
|
color: "var(--vscode-input-foreground)",
|
||||||
|
border: "1px solid var(--vscode-input-border)",
|
||||||
|
borderRadius: "2px",
|
||||||
|
height: "28px"
|
||||||
|
}}>
|
||||||
|
{listApiConfigMeta?.map((config) => (
|
||||||
|
<option key={config.name} value={config.name}>{config.name} {config.apiProvider ? `(${config.apiProvider})` : ""}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<VSCodeButton
|
||||||
|
appearance="secondary"
|
||||||
|
onClick={handleNewConfig}
|
||||||
|
>
|
||||||
|
<span className="codicon codicon-add" /> New
|
||||||
|
</VSCodeButton>
|
||||||
|
<VSCodeButton
|
||||||
|
appearance="secondary"
|
||||||
|
disabled={!currentApiConfigName}
|
||||||
|
onClick={handleStartRename}
|
||||||
|
>
|
||||||
|
<span className="codicon codicon-edit" /> Rename
|
||||||
|
</VSCodeButton>
|
||||||
|
<VSCodeButton
|
||||||
|
appearance="secondary"
|
||||||
|
disabled={!currentApiConfigName}
|
||||||
|
onClick={() => onDeleteConfig(currentApiConfigName!)}
|
||||||
|
>
|
||||||
|
<span className="codicon codicon-trash" /> Delete
|
||||||
|
</VSCodeButton>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<VSCodeDivider style={{ margin: "15px 0" }} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(ApiConfigManager)
|
||||||
@@ -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,15 @@ 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 [draftNewMode, setDraftNewMode] = useState(false)
|
||||||
|
|
||||||
|
|
||||||
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 +95,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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -150,6 +163,42 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
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 }}>
|
||||||
|
<ApiConfigManager
|
||||||
|
currentApiConfigName={currentApiConfigName}
|
||||||
|
listApiConfigMeta={listApiConfigMeta}
|
||||||
|
onSelectConfig={(configName: string) => {
|
||||||
|
vscode.postMessage({
|
||||||
|
type: "loadApiConfiguration",
|
||||||
|
text: configName
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
onDeleteConfig={(configName: string) => {
|
||||||
|
vscode.postMessage({
|
||||||
|
type: "deleteApiConfiguration",
|
||||||
|
text: configName
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
onRenameConfig={(oldName: string, newName: string) => {
|
||||||
|
vscode.postMessage({
|
||||||
|
type: "renameApiConfiguration",
|
||||||
|
values: {oldName, newName},
|
||||||
|
apiConfiguration
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
onUpsertConfig={(configName: string) => {
|
||||||
|
vscode.postMessage({
|
||||||
|
type: "upsertApiConfiguration",
|
||||||
|
text: configName,
|
||||||
|
apiConfiguration
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
// setDraftNewConfig={(mode: boolean) => {
|
||||||
|
// setDraftNewMode(mode)
|
||||||
|
// }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div style={{ marginBottom: 5 }}>
|
<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>
|
||||||
<ApiOptions
|
<ApiOptions
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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,8 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ExtensionStateContext = createContext<ExtensionStateContextType | undefined>(undefined)
|
export const ExtensionStateContext = createContext<ExtensionStateContextType | undefined>(undefined)
|
||||||
@@ -72,7 +77,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 +95,16 @@ 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 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
|
const hasKey = checkExistKey(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
|
|
||||||
setShowWelcome(!hasKey)
|
setShowWelcome(!hasKey)
|
||||||
setDidHydrateState(true)
|
setDidHydrateState(true)
|
||||||
break
|
break
|
||||||
@@ -162,8 +158,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 +208,9 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
return <ExtensionStateContext.Provider value={contextValue}>{children}</ExtensionStateContext.Provider>
|
return <ExtensionStateContext.Provider value={contextValue}>{children}</ExtensionStateContext.Provider>
|
||||||
|
|||||||
Reference in New Issue
Block a user