Wrangle our settings-related types and add support for settings import / export (#1997)

* Strongly type our settings use composition where possible

* Update src/core/contextProxy.ts

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

* Update src/core/contextProxy.ts

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

* Add comment, tweak function name

* Add changeset

* Clean up toolGroup types

* Comments tweak

* Remove unused type

* More type safety

* Improve import / export, add translations

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
This commit is contained in:
Chris Estreich 2025-03-26 16:29:01 -07:00 committed by GitHub
parent 55bbe88e39
commit a414c7d028
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
64 changed files with 2656 additions and 1520 deletions

View file

@ -0,0 +1,5 @@
---
"roo-cline": patch
---
Wrangle our settings-related types and add support for settings import / export

View file

@ -994,7 +994,7 @@ export class Cline extends EventEmitter<ClineEvents> {
}
}
const { terminalOutputLineLimit } = (await this.providerRef.deref()?.getState()) ?? {}
const { terminalOutputLineLimit = 500 } = (await this.providerRef.deref()?.getState()) ?? {}
process.on("line", (line) => {
if (!didContinue) {
@ -2339,7 +2339,7 @@ export class Cline extends EventEmitter<ClineEvents> {
}
// Get the maxReadFileLine setting
const { maxReadFileLine } = (await this.providerRef.deref()?.getState()) ?? {}
const { maxReadFileLine = 500 } = (await this.providerRef.deref()?.getState()) ?? {}
// Count total lines in the file
let totalLines = 0
@ -2480,13 +2480,14 @@ export class Cline extends EventEmitter<ClineEvents> {
this.consecutiveMistakeCount = 0
const absolutePath = path.resolve(this.cwd, relDirPath)
const [files, didHitLimit] = await listFiles(absolutePath, recursive, 200)
const { showRooIgnoredFiles } = (await this.providerRef.deref()?.getState()) ?? {}
const { showRooIgnoredFiles = true } =
(await this.providerRef.deref()?.getState()) ?? {}
const result = formatResponse.formatFilesList(
absolutePath,
files,
didHitLimit,
this.rooIgnoreController,
showRooIgnoredFiles ?? true,
showRooIgnoredFiles,
)
const completeMessage = JSON.stringify({
...sharedMessageProps,
@ -3759,7 +3760,8 @@ export class Cline extends EventEmitter<ClineEvents> {
async getEnvironmentDetails(includeFileDetails: boolean = false) {
let details = ""
const { terminalOutputLineLimit, maxWorkspaceFiles } = (await this.providerRef.deref()?.getState()) ?? {}
const { terminalOutputLineLimit = 500, maxWorkspaceFiles = 200 } =
(await this.providerRef.deref()?.getState()) ?? {}
// It could be useful for cline to know if the user went from one or no file to another between messages, so we always include this context
details += "\n\n# VSCode Visible Files"
@ -3767,7 +3769,7 @@ export class Cline extends EventEmitter<ClineEvents> {
?.map((editor) => editor.document?.uri?.fsPath)
.filter(Boolean)
.map((absolutePath) => path.relative(this.cwd, absolutePath))
.slice(0, maxWorkspaceFiles ?? 200)
.slice(0, maxWorkspaceFiles)
// Filter paths through rooIgnoreController
const allowedVisibleFiles = this.rooIgnoreController
@ -3979,7 +3981,7 @@ export class Cline extends EventEmitter<ClineEvents> {
} else {
const maxFiles = maxWorkspaceFiles ?? 200
const [files, didHitLimit] = await listFiles(this.cwd, true, maxFiles)
const { showRooIgnoredFiles } = (await this.providerRef.deref()?.getState()) ?? {}
const { showRooIgnoredFiles = true } = (await this.providerRef.deref()?.getState()) ?? {}
const result = formatResponse.formatFilesList(
this.cwd,
files,

View file

@ -1,294 +0,0 @@
import { ExtensionContext } from "vscode"
import { ApiConfiguration } from "../../shared/api"
import { Mode } from "../../shared/modes"
import { ApiConfigMeta } from "../../shared/ExtensionMessage"
export interface ApiConfigData {
currentApiConfigName: string
apiConfigs: {
[key: string]: ApiConfiguration
}
modeApiConfigs?: Partial<Record<Mode, string>>
}
export class ConfigManager {
private readonly defaultConfig: ApiConfigData = {
currentApiConfigName: "default",
apiConfigs: {
default: {
id: this.generateId(),
},
},
}
private readonly SCOPE_PREFIX = "roo_cline_config_"
private readonly context: ExtensionContext
constructor(context: ExtensionContext) {
this.context = context
this.initConfig().catch(console.error)
}
private generateId(): string {
return Math.random().toString(36).substring(2, 15)
}
// Synchronize readConfig/writeConfig operations to avoid data loss.
private _lock = Promise.resolve()
private lock<T>(cb: () => Promise<T>) {
const next = this._lock.then(cb)
this._lock = next.catch(() => {}) as Promise<void>
return next
}
/**
* Initialize config if it doesn't exist
*/
async initConfig(): Promise<void> {
try {
return await this.lock(async () => {
const config = await this.readConfig()
if (!config) {
await this.writeConfig(this.defaultConfig)
return
}
// Migrate: ensure all configs have IDs
let needsMigration = false
for (const [name, apiConfig] of Object.entries(config.apiConfigs)) {
if (!apiConfig.id) {
apiConfig.id = this.generateId()
needsMigration = true
}
}
if (needsMigration) {
await this.writeConfig(config)
}
})
} catch (error) {
throw new Error(`Failed to initialize config: ${error}`)
}
}
/**
* List all available configs with metadata
*/
async listConfig(): Promise<ApiConfigMeta[]> {
try {
return await this.lock(async () => {
const config = await this.readConfig()
return Object.entries(config.apiConfigs).map(([name, apiConfig]) => ({
name,
id: apiConfig.id || "",
apiProvider: apiConfig.apiProvider,
}))
})
} catch (error) {
throw new Error(`Failed to list configs: ${error}`)
}
}
/**
* Save a config with the given name.
* Preserves the ID from the input 'config' object if it exists,
* otherwise generates a new one (for creation scenarios).
*/
async saveConfig(name: string, config: ApiConfiguration): Promise<void> {
try {
return await this.lock(async () => {
const currentConfig = await this.readConfig()
// Preserve the existing ID if this is an update to an existing config
const existingId = currentConfig.apiConfigs[name]?.id
currentConfig.apiConfigs[name] = {
...config,
id: config.id || existingId || this.generateId(),
}
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 {
return await this.lock(async () => {
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}`)
}
}
/**
* Load a config by ID
*/
async loadConfigById(id: string): Promise<{ config: ApiConfiguration; name: string }> {
try {
return await this.lock(async () => {
const config = await this.readConfig()
// Find the config with the matching ID
const entry = Object.entries(config.apiConfigs).find(([_, apiConfig]) => apiConfig.id === id)
if (!entry) {
throw new Error(`Config with ID '${id}' not found`)
}
const [name, apiConfig] = entry
// Update current config name
config.currentApiConfigName = name
await this.writeConfig(config)
return { config: apiConfig, name }
})
} catch (error) {
throw new Error(`Failed to load config by ID: ${error}`)
}
}
/**
* Delete a config by name
*/
async deleteConfig(name: string): Promise<void> {
try {
return await this.lock(async () => {
const currentConfig = await this.readConfig()
if (!currentConfig.apiConfigs[name]) {
throw new Error(`Config '${name}' not found`)
}
// Don't allow deleting the default config
if (Object.keys(currentConfig.apiConfigs).length === 1) {
throw new Error(`Cannot delete the last remaining configuration.`)
}
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 {
return await this.lock(async () => {
const currentConfig = await this.readConfig()
if (!currentConfig.apiConfigs[name]) {
throw new Error(`Config '${name}' not found`)
}
currentConfig.currentApiConfigName = name
await this.writeConfig(currentConfig)
})
} 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 {
return await this.lock(async () => {
const config = await this.readConfig()
return name in config.apiConfigs
})
} catch (error) {
throw new Error(`Failed to check config existence: ${error}`)
}
}
/**
* Set the API config for a specific mode
*/
async setModeConfig(mode: Mode, configId: string): Promise<void> {
try {
return await this.lock(async () => {
const currentConfig = await this.readConfig()
if (!currentConfig.modeApiConfigs) {
currentConfig.modeApiConfigs = {}
}
currentConfig.modeApiConfigs[mode] = configId
await this.writeConfig(currentConfig)
})
} catch (error) {
throw new Error(`Failed to set mode config: ${error}`)
}
}
/**
* Get the API config ID for a specific mode
*/
async getModeConfigId(mode: Mode): Promise<string | undefined> {
try {
return await this.lock(async () => {
const config = await this.readConfig()
return config.modeApiConfigs?.[mode]
})
} catch (error) {
throw new Error(`Failed to get mode config: ${error}`)
}
}
/**
* Get the key used for storing config in secrets
*/
private getConfigKey(): string {
return `${this.SCOPE_PREFIX}api_config`
}
/**
* Reset all configuration by deleting the stored config from secrets
*/
public async resetAllConfigs(): Promise<void> {
return await this.lock(async () => {
await this.context.secrets.delete(this.getConfigKey())
})
}
private async readConfig(): Promise<ApiConfigData> {
try {
const content = await this.context.secrets.get(this.getConfigKey())
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 content = JSON.stringify(config, null, 2)
await this.context.secrets.store(this.getConfigKey(), content)
} catch (error) {
throw new Error(`Failed to write config to secrets: ${error}`)
}
}
}

View file

@ -0,0 +1,232 @@
import * as vscode from "vscode"
import { logger } from "../../utils/logging"
import type {
ProviderSettings,
RooCodeSettings,
RooCodeSettingsKey,
GlobalStateKey,
GlobalState,
SecretStateKey,
SecretState,
GlobalSettings,
} from "../../exports/roo-code"
import {
PROVIDER_SETTINGS_KEYS,
GLOBAL_STATE_KEYS,
SECRET_STATE_KEYS,
isSecretStateKey,
isPassThroughStateKey,
globalSettingsSchema,
providerSettingsSchema,
} from "../../shared/globalState"
const globalSettingsExportSchema = globalSettingsSchema.omit({
taskHistory: true,
listApiConfigMeta: true,
currentApiConfigName: true,
})
export class ContextProxy {
private readonly originalContext: vscode.ExtensionContext
private stateCache: GlobalState
private secretCache: SecretState
private _isInitialized = false
constructor(context: vscode.ExtensionContext) {
this.originalContext = context
this.stateCache = {}
this.secretCache = {}
this._isInitialized = false
}
public get isInitialized() {
return this._isInitialized
}
public async initialize() {
for (const key of GLOBAL_STATE_KEYS) {
try {
this.stateCache[key] = this.originalContext.globalState.get(key)
} catch (error) {
logger.error(`Error loading global ${key}: ${error instanceof Error ? error.message : String(error)}`)
}
}
const promises = SECRET_STATE_KEYS.map(async (key) => {
try {
this.secretCache[key] = await this.originalContext.secrets.get(key)
} catch (error) {
logger.error(`Error loading secret ${key}: ${error instanceof Error ? error.message : String(error)}`)
}
})
await Promise.all(promises)
this._isInitialized = true
}
public get extensionUri() {
return this.originalContext.extensionUri
}
public get extensionPath() {
return this.originalContext.extensionPath
}
public get globalStorageUri() {
return this.originalContext.globalStorageUri
}
public get logUri() {
return this.originalContext.logUri
}
public get extension() {
return this.originalContext.extension
}
public get extensionMode() {
return this.originalContext.extensionMode
}
/**
* ExtensionContext.globalState
* https://code.visualstudio.com/api/references/vscode-api#ExtensionContext.globalState
*/
getGlobalState<K extends GlobalStateKey>(key: K): GlobalState[K]
getGlobalState<K extends GlobalStateKey>(key: K, defaultValue: GlobalState[K]): GlobalState[K]
getGlobalState<K extends GlobalStateKey>(key: K, defaultValue?: GlobalState[K]): GlobalState[K] {
if (isPassThroughStateKey(key)) {
const value = this.originalContext.globalState.get<GlobalState[K]>(key)
return value === undefined || value === null ? defaultValue : value
}
const value = this.stateCache[key]
return value !== undefined ? value : defaultValue
}
updateGlobalState<K extends GlobalStateKey>(key: K, value: GlobalState[K]) {
if (isPassThroughStateKey(key)) {
return this.originalContext.globalState.update(key, value)
}
this.stateCache[key] = value
return this.originalContext.globalState.update(key, value)
}
private getAllGlobalState(): GlobalState {
return Object.fromEntries(GLOBAL_STATE_KEYS.map((key) => [key, this.getGlobalState(key)]))
}
/**
* ExtensionContext.secrets
* https://code.visualstudio.com/api/references/vscode-api#ExtensionContext.secrets
*/
getSecret(key: SecretStateKey) {
return this.secretCache[key]
}
storeSecret(key: SecretStateKey, value?: string) {
// Update cache.
this.secretCache[key] = value
// Write directly to context.
return value === undefined
? this.originalContext.secrets.delete(key)
: this.originalContext.secrets.store(key, value)
}
private getAllSecretState(): SecretState {
return Object.fromEntries(SECRET_STATE_KEYS.map((key) => [key, this.getSecret(key)]))
}
/**
* GlobalSettings
*/
public getGlobalSettings(): GlobalSettings {
return globalSettingsSchema.parse({ ...this.stateCache })
}
/**
* ProviderSettings
*/
public getProviderSettings(): ProviderSettings {
return providerSettingsSchema.parse(this.getValues())
}
public async setProviderSettings(values: ProviderSettings) {
// Explicitly clear out any old API configuration values before that
// might not be present in the new configuration.
// If a value is not present in the new configuration, then it is assumed
// that the setting's value should be `undefined` and therefore we
// need to remove it from the state cache if it exists.
await this.setValues({
...PROVIDER_SETTINGS_KEYS.filter((key) => !isSecretStateKey(key))
.filter((key) => !!this.stateCache[key])
.reduce((acc, key) => ({ ...acc, [key]: undefined }), {} as ProviderSettings),
...values,
})
}
/**
* RooCodeSettings
*/
public setValue<K extends RooCodeSettingsKey>(key: K, value: RooCodeSettings[K]) {
return isSecretStateKey(key) ? this.storeSecret(key, value as string) : this.updateGlobalState(key, value)
}
public getValue<K extends RooCodeSettingsKey>(key: K): RooCodeSettings[K] {
return isSecretStateKey(key)
? (this.getSecret(key) as RooCodeSettings[K])
: (this.getGlobalState(key) as RooCodeSettings[K])
}
public getValues(): RooCodeSettings {
return { ...this.getAllGlobalState(), ...this.getAllSecretState() }
}
public async setValues(values: RooCodeSettings) {
const entries = Object.entries(values) as [RooCodeSettingsKey, unknown][]
await Promise.all(entries.map(([key, value]) => this.setValue(key, value)))
}
/**
* Import / Export
*/
public async export(): Promise<GlobalSettings | undefined> {
try {
const globalSettings = globalSettingsExportSchema.parse(this.getValues())
return Object.fromEntries(Object.entries(globalSettings).filter(([_, value]) => value !== undefined))
} catch (error) {
console.log(error.message)
return undefined
}
}
/**
* Resets all global state, secrets, and in-memory caches.
* This clears all data from both the in-memory caches and the VSCode storage.
* @returns A promise that resolves when all reset operations are complete
*/
public async resetAllState() {
// Clear in-memory caches
this.stateCache = {}
this.secretCache = {}
await Promise.all([
...GLOBAL_STATE_KEYS.map((key) => this.originalContext.globalState.update(key, undefined)),
...SECRET_STATE_KEYS.map((key) => this.originalContext.secrets.delete(key)),
])
await this.initialize()
}
}

View file

@ -0,0 +1,293 @@
import { ExtensionContext } from "vscode"
import { z } from "zod"
import { providerSettingsSchema } from "../../shared/globalState"
import { Mode } from "../../shared/modes"
import { ApiConfigMeta } from "../../shared/ExtensionMessage"
const providerSettingsWithIdSchema = providerSettingsSchema.extend({ id: z.string().optional() })
type ProviderSettingsWithId = z.infer<typeof providerSettingsWithIdSchema>
export const providerProfilesSchema = z.object({
currentApiConfigName: z.string(),
apiConfigs: z.record(z.string(), providerSettingsWithIdSchema),
modeApiConfigs: z.record(z.string(), z.string()).optional(),
})
export type ProviderProfiles = z.infer<typeof providerProfilesSchema>
const providerProfilesExportSchema = providerProfilesSchema.extend({
apiConfigs: z.record(
z.string(),
providerSettingsWithIdSchema.omit({
glamaModelInfo: true,
openRouterModelInfo: true,
unboundModelInfo: true,
requestyModelInfo: true,
}),
),
})
export class ProviderSettingsManager {
private static readonly SCOPE_PREFIX = "roo_cline_config_"
private readonly defaultProviderProfiles: ProviderProfiles = {
currentApiConfigName: "default",
apiConfigs: { default: { id: this.generateId() } },
}
private readonly context: ExtensionContext
constructor(context: ExtensionContext) {
this.context = context
// TODO: We really shouldn't have async methods in the constructor.
this.initialize().catch(console.error)
}
private generateId() {
return Math.random().toString(36).substring(2, 15)
}
// Synchronize readConfig/writeConfig operations to avoid data loss.
private _lock = Promise.resolve()
private lock<T>(cb: () => Promise<T>) {
const next = this._lock.then(cb)
this._lock = next.catch(() => {}) as Promise<void>
return next
}
/**
* Initialize config if it doesn't exist.
*/
public async initialize() {
try {
return await this.lock(async () => {
const providerProfiles = await this.load()
if (!providerProfiles) {
await this.store(this.defaultProviderProfiles)
return
}
let isDirty = false
// Ensure all configs have IDs.
for (const [name, apiConfig] of Object.entries(providerProfiles.apiConfigs)) {
if (!apiConfig.id) {
apiConfig.id = this.generateId()
isDirty = true
}
}
if (isDirty) {
await this.store(providerProfiles)
}
})
} catch (error) {
throw new Error(`Failed to initialize config: ${error}`)
}
}
/**
* List all available configs with metadata.
*/
public async listConfig(): Promise<ApiConfigMeta[]> {
try {
return await this.lock(async () => {
const providerProfiles = await this.load()
return Object.entries(providerProfiles.apiConfigs).map(([name, apiConfig]) => ({
name,
id: apiConfig.id || "",
apiProvider: apiConfig.apiProvider,
}))
})
} catch (error) {
throw new Error(`Failed to list configs: ${error}`)
}
}
/**
* Save a config with the given name.
* Preserves the ID from the input 'config' object if it exists,
* otherwise generates a new one (for creation scenarios).
*/
public async saveConfig(name: string, config: ProviderSettingsWithId) {
try {
return await this.lock(async () => {
const providerProfiles = await this.load()
// Preserve the existing ID if this is an update to an existing config.
const existingId = providerProfiles.apiConfigs[name]?.id
providerProfiles.apiConfigs[name] = { ...config, id: config.id || existingId || this.generateId() }
await this.store(providerProfiles)
})
} catch (error) {
throw new Error(`Failed to save config: ${error}`)
}
}
/**
* Load a config by name and set it as the current config.
*/
public async loadConfig(name: string) {
try {
return await this.lock(async () => {
const providerProfiles = await this.load()
const providerSettings = providerProfiles.apiConfigs[name]
if (!providerSettings) {
throw new Error(`Config '${name}' not found`)
}
providerProfiles.currentApiConfigName = name
await this.store(providerProfiles)
return providerSettings
})
} catch (error) {
throw new Error(`Failed to load config: ${error}`)
}
}
/**
* Load a config by ID and set it as the current config.
*/
public async loadConfigById(id: string) {
try {
return await this.lock(async () => {
const providerProfiles = await this.load()
const providerSettings = Object.entries(providerProfiles.apiConfigs).find(
([_, apiConfig]) => apiConfig.id === id,
)
if (!providerSettings) {
throw new Error(`Config with ID '${id}' not found`)
}
const [name, apiConfig] = providerSettings
providerProfiles.currentApiConfigName = name
await this.store(providerProfiles)
return { config: apiConfig, name }
})
} catch (error) {
throw new Error(`Failed to load config by ID: ${error}`)
}
}
/**
* Delete a config by name.
*/
public async deleteConfig(name: string) {
try {
return await this.lock(async () => {
const providerProfiles = await this.load()
if (!providerProfiles.apiConfigs[name]) {
throw new Error(`Config '${name}' not found`)
}
if (Object.keys(providerProfiles.apiConfigs).length === 1) {
throw new Error(`Cannot delete the last remaining configuration`)
}
delete providerProfiles.apiConfigs[name]
await this.store(providerProfiles)
})
} catch (error) {
throw new Error(`Failed to delete config: ${error}`)
}
}
/**
* Check if a config exists by name.
*/
public async hasConfig(name: string) {
try {
return await this.lock(async () => {
const providerProfiles = await this.load()
return name in providerProfiles.apiConfigs
})
} catch (error) {
throw new Error(`Failed to check config existence: ${error}`)
}
}
/**
* Set the API config for a specific mode.
*/
public async setModeConfig(mode: Mode, configId: string) {
try {
return await this.lock(async () => {
const providerProfiles = await this.load()
const { modeApiConfigs = {} } = providerProfiles
modeApiConfigs[mode] = configId
await this.store(providerProfiles)
})
} catch (error) {
throw new Error(`Failed to set mode config: ${error}`)
}
}
/**
* Get the API config ID for a specific mode.
*/
public async getModeConfigId(mode: Mode) {
try {
return await this.lock(async () => {
const { modeApiConfigs } = await this.load()
return modeApiConfigs?.[mode]
})
} catch (error) {
throw new Error(`Failed to get mode config: ${error}`)
}
}
public async export() {
try {
return await this.lock(async () => providerProfilesExportSchema.parse(await this.load()))
} catch (error) {
throw new Error(`Failed to export provider profiles: ${error}`)
}
}
public async import(providerProfiles: ProviderProfiles) {
try {
return await this.lock(() => this.store(providerProfiles))
} catch (error) {
throw new Error(`Failed to import provider profiles: ${error}`)
}
}
/**
* Reset provider profiles by deleting them from secrets.
*/
public async resetAllConfigs() {
return await this.lock(async () => {
await this.context.secrets.delete(this.secretsKey)
})
}
private get secretsKey() {
return `${ProviderSettingsManager.SCOPE_PREFIX}api_config`
}
private async load(): Promise<ProviderProfiles> {
try {
const content = await this.context.secrets.get(this.secretsKey)
return content ? providerProfilesSchema.parse(JSON.parse(content)) : this.defaultProviderProfiles
} catch (error) {
throw new Error(`Failed to read provider profiles from secrets: ${error}`)
}
}
private async store(providerProfiles: ProviderProfiles) {
try {
await this.context.secrets.store(this.secretsKey, JSON.stringify(providerProfiles, null, 2))
} catch (error) {
throw new Error(`Failed to write provider profiles to secrets: ${error}`)
}
}
}

View file

@ -1,10 +1,12 @@
// npx jest src/core/__tests__/contextProxy.test.ts
// npx jest src/core/config/__tests__/ContextProxy.test.ts
import fs from "fs/promises"
import * as vscode from "vscode"
import { ContextProxy } from "../contextProxy"
import { ContextProxy } from "../ContextProxy"
import { logger } from "../../utils/logging"
import { GLOBAL_STATE_KEYS, SECRET_KEYS, ConfigurationKey, GlobalStateKey } from "../../shared/globalState"
import { logger } from "../../../utils/logging"
import { GLOBAL_STATE_KEYS, SECRET_STATE_KEYS } from "../../../shared/globalState"
jest.mock("vscode", () => ({
Uri: {
@ -77,8 +79,8 @@ describe("ContextProxy", () => {
})
it("should initialize secret cache with all secret keys", () => {
expect(mockSecrets.get).toHaveBeenCalledTimes(SECRET_KEYS.length)
for (const key of SECRET_KEYS) {
expect(mockSecrets.get).toHaveBeenCalledTimes(SECRET_STATE_KEYS.length)
for (const key of SECRET_STATE_KEYS) {
expect(mockSecrets.get).toHaveBeenCalledWith(key)
}
})
@ -87,11 +89,11 @@ describe("ContextProxy", () => {
describe("getGlobalState", () => {
it("should return value from cache when it exists", async () => {
// Manually set a value in the cache
await proxy.updateGlobalState("apiProvider", "cached-value")
await proxy.updateGlobalState("apiProvider", "deepseek")
// Should return the cached value
const result = proxy.getGlobalState("apiProvider")
expect(result).toBe("cached-value")
expect(result).toBe("deepseek")
// Original context should be called once during updateGlobalState
expect(mockGlobalState.get).toHaveBeenCalledTimes(GLOBAL_STATE_KEYS.length) // Only from initialization
@ -99,8 +101,8 @@ describe("ContextProxy", () => {
it("should handle default values correctly", async () => {
// No value in cache
const result = proxy.getGlobalState("apiProvider", "default-value")
expect(result).toBe("default-value")
const result = proxy.getGlobalState("apiProvider", "deepseek")
expect(result).toBe("deepseek")
})
it("should bypass cache for pass-through state keys", async () => {
@ -108,7 +110,7 @@ describe("ContextProxy", () => {
mockGlobalState.get.mockReturnValue("pass-through-value")
// Use a pass-through key (taskHistory)
const result = proxy.getGlobalState("taskHistory" as GlobalStateKey)
const result = proxy.getGlobalState("taskHistory")
// Should get value directly from original context
expect(result).toBe("pass-through-value")
@ -120,37 +122,61 @@ describe("ContextProxy", () => {
mockGlobalState.get.mockReturnValue(undefined)
// Use a pass-through key with default value
const result = proxy.getGlobalState("taskHistory" as GlobalStateKey, "default-value")
const historyItems = [
{
id: "1",
number: 1,
ts: 1,
task: "test",
tokensIn: 1,
tokensOut: 1,
totalCost: 1,
},
]
const result = proxy.getGlobalState("taskHistory", historyItems)
// Should return default value when original context returns undefined
expect(result).toBe("default-value")
expect(result).toBe(historyItems)
})
})
describe("updateGlobalState", () => {
it("should update state directly in original context", async () => {
await proxy.updateGlobalState("apiProvider", "new-value")
await proxy.updateGlobalState("apiProvider", "deepseek")
// Should have called original context
expect(mockGlobalState.update).toHaveBeenCalledWith("apiProvider", "new-value")
expect(mockGlobalState.update).toHaveBeenCalledWith("apiProvider", "deepseek")
// Should have stored the value in cache
const storedValue = await proxy.getGlobalState("apiProvider")
expect(storedValue).toBe("new-value")
expect(storedValue).toBe("deepseek")
})
it("should bypass cache for pass-through state keys", async () => {
await proxy.updateGlobalState("taskHistory" as GlobalStateKey, "new-value")
const historyItems = [
{
id: "1",
number: 1,
ts: 1,
task: "test",
tokensIn: 1,
tokensOut: 1,
totalCost: 1,
},
]
await proxy.updateGlobalState("taskHistory", historyItems)
// Should update original context
expect(mockGlobalState.update).toHaveBeenCalledWith("taskHistory", "new-value")
expect(mockGlobalState.update).toHaveBeenCalledWith("taskHistory", historyItems)
// Setup mock for subsequent get
mockGlobalState.get.mockReturnValue("new-value")
mockGlobalState.get.mockReturnValue(historyItems)
// Should get fresh value from original context
const storedValue = proxy.getGlobalState("taskHistory" as GlobalStateKey)
expect(storedValue).toBe("new-value")
const storedValue = proxy.getGlobalState("taskHistory")
expect(storedValue).toBe(historyItems)
expect(mockGlobalState.get).toHaveBeenCalledWith("taskHistory")
})
})
@ -220,27 +246,6 @@ describe("ContextProxy", () => {
const storedValue = proxy.getGlobalState("apiModelId")
expect(storedValue).toBe("gpt-4")
})
it("should handle unknown keys as global state with warning", async () => {
// Spy on the logger
const warnSpy = jest.spyOn(logger, "warn")
// Spy on updateGlobalState
const updateGlobalStateSpy = jest.spyOn(proxy, "updateGlobalState")
// Test with an unknown key
await proxy.setValue("unknownKey" as ConfigurationKey, "some-value")
// Should have logged a warning
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("Unknown key: unknownKey"))
// Should have called updateGlobalState
expect(updateGlobalStateSpy).toHaveBeenCalledWith("unknownKey", "some-value")
// Should have stored the value in state cache
const storedValue = proxy.getGlobalState("unknownKey" as GlobalStateKey)
expect(storedValue).toBe("some-value")
})
})
describe("setValues", () => {
@ -288,7 +293,7 @@ describe("ContextProxy", () => {
})
})
describe("setApiConfiguration", () => {
describe("setProviderSettings", () => {
it("should clear old API configuration values and set new ones", async () => {
// Set up initial API configuration values
await proxy.updateGlobalState("apiModelId", "old-model")
@ -298,8 +303,8 @@ describe("ContextProxy", () => {
// Spy on setValues
const setValuesSpy = jest.spyOn(proxy, "setValues")
// Call setApiConfiguration with new configuration
await proxy.setApiConfiguration({
// Call setProviderSettings with new configuration
await proxy.setProviderSettings({
apiModelId: "new-model",
apiProvider: "anthropic",
// Note: openAiBaseUrl is not included in the new config
@ -332,8 +337,8 @@ describe("ContextProxy", () => {
// Spy on setValues
const setValuesSpy = jest.spyOn(proxy, "setValues")
// Call setApiConfiguration with empty configuration
await proxy.setApiConfiguration({})
// Call setProviderSettings with empty configuration
await proxy.setProviderSettings({})
// Verify setValues was called with undefined for all existing API config keys
expect(setValuesSpy).toHaveBeenCalledWith(
@ -397,12 +402,12 @@ describe("ContextProxy", () => {
await proxy.resetAllState()
// Should have called delete for each key
for (const key of SECRET_KEYS) {
for (const key of SECRET_STATE_KEYS) {
expect(mockSecrets.delete).toHaveBeenCalledWith(key)
}
// Total calls should equal the number of secret keys
expect(mockSecrets.delete).toHaveBeenCalledTimes(SECRET_KEYS.length)
expect(mockSecrets.delete).toHaveBeenCalledTimes(SECRET_STATE_KEYS.length)
})
it("should reinitialize caches after reset", async () => {

View file

@ -1,6 +1,9 @@
// npx jest src/core/config/__tests__/ProviderSettingsManager.test.ts
import { ExtensionContext } from "vscode"
import { ConfigManager, ApiConfigData } from "../ConfigManager"
import { ApiConfiguration } from "../../../shared/api"
import { ProviderSettings } from "../../../exports/roo-code"
import { ProviderSettingsManager, ProviderProfiles } from "../ProviderSettingsManager"
// Mock VSCode ExtensionContext
const mockSecrets = {
@ -13,20 +16,20 @@ const mockContext = {
secrets: mockSecrets,
} as unknown as ExtensionContext
describe("ConfigManager", () => {
let configManager: ConfigManager
describe("ProviderSettingsManager", () => {
let providerSettingsManager: ProviderSettingsManager
beforeEach(() => {
jest.clearAllMocks()
configManager = new ConfigManager(mockContext)
providerSettingsManager = new ProviderSettingsManager(mockContext)
})
describe("initConfig", () => {
describe("initialize", () => {
it("should not write to storage when secrets.get returns null", async () => {
// Mock readConfig to return null
mockSecrets.get.mockResolvedValueOnce(null)
await configManager.initConfig()
await providerSettingsManager.initialize()
// Should not write to storage because readConfig returns defaultConfig
expect(mockSecrets.store).not.toHaveBeenCalled()
@ -45,7 +48,7 @@ describe("ConfigManager", () => {
}),
)
await configManager.initConfig()
await providerSettingsManager.initialize()
expect(mockSecrets.store).not.toHaveBeenCalled()
})
@ -66,7 +69,7 @@ describe("ConfigManager", () => {
}),
)
await configManager.initConfig()
await providerSettingsManager.initialize()
// Should have written the config with new IDs
expect(mockSecrets.store).toHaveBeenCalled()
@ -78,15 +81,15 @@ describe("ConfigManager", () => {
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",
await expect(providerSettingsManager.initialize()).rejects.toThrow(
"Failed to initialize config: Error: Failed to read provider profiles from secrets: Error: Storage failed",
)
})
})
describe("ListConfig", () => {
it("should list all available configs", async () => {
const existingConfig: ApiConfigData = {
const existingConfig: ProviderProfiles = {
currentApiConfigName: "default",
apiConfigs: {
default: {
@ -106,7 +109,7 @@ describe("ConfigManager", () => {
mockSecrets.get.mockResolvedValue(JSON.stringify(existingConfig))
const configs = await configManager.listConfig()
const configs = await providerSettingsManager.listConfig()
expect(configs).toEqual([
{ name: "default", id: "default", apiProvider: undefined },
{ name: "test", id: "test-id", apiProvider: "anthropic" },
@ -114,7 +117,7 @@ describe("ConfigManager", () => {
})
it("should handle empty config file", async () => {
const emptyConfig: ApiConfigData = {
const emptyConfig: ProviderProfiles = {
currentApiConfigName: "default",
apiConfigs: {},
modeApiConfigs: {
@ -126,15 +129,15 @@ describe("ConfigManager", () => {
mockSecrets.get.mockResolvedValue(JSON.stringify(emptyConfig))
const configs = await configManager.listConfig()
const configs = await providerSettingsManager.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",
await expect(providerSettingsManager.listConfig()).rejects.toThrow(
"Failed to list configs: Error: Failed to read provider profiles from secrets: Error: Read failed",
)
})
})
@ -155,12 +158,12 @@ describe("ConfigManager", () => {
}),
)
const newConfig: ApiConfiguration = {
const newConfig: ProviderSettings = {
apiProvider: "anthropic",
apiKey: "test-key",
}
await configManager.saveConfig("test", newConfig)
await providerSettingsManager.saveConfig("test", newConfig)
// Get the actual stored config to check the generated ID
const storedConfig = JSON.parse(mockSecrets.store.mock.calls[0][1])
@ -189,7 +192,7 @@ describe("ConfigManager", () => {
})
it("should update existing config", async () => {
const existingConfig: ApiConfigData = {
const existingConfig: ProviderProfiles = {
currentApiConfigName: "default",
apiConfigs: {
test: {
@ -202,12 +205,12 @@ describe("ConfigManager", () => {
mockSecrets.get.mockResolvedValue(JSON.stringify(existingConfig))
const updatedConfig: ApiConfiguration = {
const updatedConfig: ProviderSettings = {
apiProvider: "anthropic",
apiKey: "new-key",
}
await configManager.saveConfig("test", updatedConfig)
await providerSettingsManager.saveConfig("test", updatedConfig)
const expectedConfig = {
currentApiConfigName: "default",
@ -235,15 +238,15 @@ describe("ConfigManager", () => {
)
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",
await expect(providerSettingsManager.saveConfig("test", {})).rejects.toThrow(
"Failed to save config: Error: Failed to write provider profiles to secrets: Error: Storage failed",
)
})
})
describe("DeleteConfig", () => {
it("should delete existing config", async () => {
const existingConfig: ApiConfigData = {
const existingConfig: ProviderProfiles = {
currentApiConfigName: "default",
apiConfigs: {
default: {
@ -258,7 +261,7 @@ describe("ConfigManager", () => {
mockSecrets.get.mockResolvedValue(JSON.stringify(existingConfig))
await configManager.deleteConfig("test")
await providerSettingsManager.deleteConfig("test")
// Get the stored config to check the ID
const storedConfig = JSON.parse(mockSecrets.store.mock.calls[0][1])
@ -275,7 +278,9 @@ describe("ConfigManager", () => {
}),
)
await expect(configManager.deleteConfig("nonexistent")).rejects.toThrow("Config 'nonexistent' not found")
await expect(providerSettingsManager.deleteConfig("nonexistent")).rejects.toThrow(
"Config 'nonexistent' not found",
)
})
it("should throw error when trying to delete last remaining config", async () => {
@ -290,15 +295,15 @@ describe("ConfigManager", () => {
}),
)
await expect(configManager.deleteConfig("default")).rejects.toThrow(
"Cannot delete the last remaining configuration.",
await expect(providerSettingsManager.deleteConfig("default")).rejects.toThrow(
"Failed to delete config: Error: Cannot delete the last remaining configuration",
)
})
})
describe("LoadConfig", () => {
it("should load config and update current config name", async () => {
const existingConfig: ApiConfigData = {
const existingConfig: ProviderProfiles = {
currentApiConfigName: "default",
apiConfigs: {
test: {
@ -311,7 +316,7 @@ describe("ConfigManager", () => {
mockSecrets.get.mockResolvedValue(JSON.stringify(existingConfig))
const config = await configManager.loadConfig("test")
const config = await providerSettingsManager.loadConfig("test")
expect(config).toEqual({
apiProvider: "anthropic",
@ -342,7 +347,9 @@ describe("ConfigManager", () => {
}),
)
await expect(configManager.loadConfig("nonexistent")).rejects.toThrow("Config 'nonexistent' not found")
await expect(providerSettingsManager.loadConfig("nonexistent")).rejects.toThrow(
"Config 'nonexistent' not found",
)
})
it("should throw error if secrets storage fails", async () => {
@ -361,67 +368,8 @@ describe("ConfigManager", () => {
)
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: {
id: "default",
},
test: {
apiProvider: "anthropic",
id: "test-id",
},
},
}
mockSecrets.get.mockResolvedValue(JSON.stringify(existingConfig))
await configManager.setCurrentConfig("test")
// Get the stored config to check the structure
const storedConfig = JSON.parse(mockSecrets.store.mock.calls[0][1])
expect(storedConfig.currentApiConfigName).toBe("test")
expect(storedConfig.apiConfigs.default.id).toBe("default")
expect(storedConfig.apiConfigs.test).toEqual({
apiProvider: "anthropic",
id: "test-id",
})
})
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",
await expect(providerSettingsManager.loadConfig("test")).rejects.toThrow(
"Failed to load config: Error: Failed to write provider profiles to secrets: Error: Storage failed",
)
})
})
@ -441,7 +389,7 @@ describe("ConfigManager", () => {
}),
)
await configManager.resetAllConfigs()
await providerSettingsManager.resetAllConfigs()
// Should have called delete with the correct config key
expect(mockSecrets.delete).toHaveBeenCalledWith("roo_cline_config_api_config")
@ -450,7 +398,7 @@ describe("ConfigManager", () => {
describe("HasConfig", () => {
it("should return true for existing config", async () => {
const existingConfig: ApiConfigData = {
const existingConfig: ProviderProfiles = {
currentApiConfigName: "default",
apiConfigs: {
default: {
@ -465,7 +413,7 @@ describe("ConfigManager", () => {
mockSecrets.get.mockResolvedValue(JSON.stringify(existingConfig))
const hasConfig = await configManager.hasConfig("test")
const hasConfig = await providerSettingsManager.hasConfig("test")
expect(hasConfig).toBe(true)
})
@ -477,15 +425,15 @@ describe("ConfigManager", () => {
}),
)
const hasConfig = await configManager.hasConfig("nonexistent")
const hasConfig = await providerSettingsManager.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",
await expect(providerSettingsManager.hasConfig("test")).rejects.toThrow(
"Failed to check config existence: Error: Failed to read provider profiles from secrets: Error: Storage failed",
)
})
})

View file

@ -0,0 +1,391 @@
// npx jest src/core/config/__tests__/importExport.test.ts
import fs from "fs/promises"
import * as path from "path"
import os from "os"
import * as vscode from "vscode"
import { importSettings, exportSettings } from "../importExport"
import { ProviderSettingsManager } from "../ProviderSettingsManager"
import { ContextProxy } from "../ContextProxy"
import { ProviderName } from "../../../exports/roo-code"
// Mock VSCode modules
jest.mock("vscode", () => ({
window: {
showOpenDialog: jest.fn(),
showSaveDialog: jest.fn(),
},
Uri: {
file: jest.fn((filePath) => ({ fsPath: filePath })),
},
}))
// Mock fs/promises
jest.mock("fs/promises", () => ({
readFile: jest.fn(),
mkdir: jest.fn(),
writeFile: jest.fn(),
}))
// Mock os module
jest.mock("os", () => ({
homedir: jest.fn(() => "/mock/home"),
}))
describe("importExport", () => {
let mockProviderSettingsManager: jest.Mocked<ProviderSettingsManager>
let mockContextProxy: jest.Mocked<ContextProxy>
beforeEach(() => {
// Reset all mocks
jest.clearAllMocks()
// Setup providerSettingsManager mock
mockProviderSettingsManager = {
export: jest.fn(),
import: jest.fn(),
listConfig: jest.fn(),
} as unknown as jest.Mocked<ProviderSettingsManager>
// Setup contextProxy mock with properly typed export method
mockContextProxy = {
setValues: jest.fn(),
setValue: jest.fn(),
export: jest.fn().mockImplementation(() => Promise.resolve({})),
} as unknown as jest.Mocked<ContextProxy>
})
describe("importSettings", () => {
it("should return success: false when user cancels file selection", async () => {
// Mock user canceling file selection
;(vscode.window.showOpenDialog as jest.Mock).mockResolvedValue(undefined)
const result = await importSettings({
providerSettingsManager: mockProviderSettingsManager,
contextProxy: mockContextProxy,
})
expect(result).toEqual({ success: false })
expect(vscode.window.showOpenDialog).toHaveBeenCalledWith({
filters: { JSON: ["json"] },
canSelectMany: false,
})
expect(fs.readFile).not.toHaveBeenCalled()
expect(mockProviderSettingsManager.import).not.toHaveBeenCalled()
expect(mockContextProxy.setValues).not.toHaveBeenCalled()
})
it("should import settings successfully from a valid file", async () => {
// Mock successful file selection
;(vscode.window.showOpenDialog as jest.Mock).mockResolvedValue([{ fsPath: "/mock/path/settings.json" }])
// Valid settings content
const mockFileContent = JSON.stringify({
providerProfiles: {
currentApiConfigName: "test",
apiConfigs: {
test: {
apiProvider: "openai" as ProviderName,
apiKey: "test-key",
id: "test-id",
},
},
},
globalSettings: {
mode: "code",
autoApprovalEnabled: true,
},
})
// Mock reading file
;(fs.readFile as jest.Mock).mockResolvedValue(mockFileContent)
// Mock export returning previous provider profiles
const previousProviderProfiles = {
currentApiConfigName: "default",
apiConfigs: {
default: {
apiProvider: "anthropic" as ProviderName,
id: "default-id",
},
},
}
mockProviderSettingsManager.export.mockResolvedValue(previousProviderProfiles)
// Mock listConfig
mockProviderSettingsManager.listConfig.mockResolvedValue([
{ name: "test", id: "test-id", apiProvider: "openai" as ProviderName },
{ name: "default", id: "default-id", apiProvider: "anthropic" as ProviderName },
])
// Mock contextProxy.export
mockContextProxy.export.mockResolvedValue({
mode: "code",
})
const result = await importSettings({
providerSettingsManager: mockProviderSettingsManager,
contextProxy: mockContextProxy,
})
expect(result.success).toBe(true)
expect(fs.readFile).toHaveBeenCalledWith("/mock/path/settings.json", "utf-8")
expect(mockProviderSettingsManager.export).toHaveBeenCalled()
expect(mockProviderSettingsManager.import).toHaveBeenCalledWith({
...previousProviderProfiles,
currentApiConfigName: "test",
apiConfigs: {
test: {
apiProvider: "openai" as ProviderName,
apiKey: "test-key",
id: "test-id",
},
},
})
expect(mockContextProxy.setValues).toHaveBeenCalledWith({
mode: "code",
autoApprovalEnabled: true,
})
expect(mockContextProxy.setValue).toHaveBeenCalledWith("currentApiConfigName", "test")
expect(mockContextProxy.setValue).toHaveBeenCalledWith("listApiConfigMeta", [
{ name: "test", id: "test-id", apiProvider: "openai" as ProviderName },
{ name: "default", id: "default-id", apiProvider: "anthropic" as ProviderName },
])
})
it("should return success: false when file content is invalid", async () => {
// Mock successful file selection
;(vscode.window.showOpenDialog as jest.Mock).mockResolvedValue([{ fsPath: "/mock/path/settings.json" }])
// Invalid content (missing required fields)
const mockInvalidContent = JSON.stringify({
providerProfiles: {
apiConfigs: {},
},
globalSettings: {},
})
// Mock reading file
;(fs.readFile as jest.Mock).mockResolvedValue(mockInvalidContent)
const result = await importSettings({
providerSettingsManager: mockProviderSettingsManager,
contextProxy: mockContextProxy,
})
expect(result).toEqual({ success: false })
expect(fs.readFile).toHaveBeenCalledWith("/mock/path/settings.json", "utf-8")
expect(mockProviderSettingsManager.import).not.toHaveBeenCalled()
expect(mockContextProxy.setValues).not.toHaveBeenCalled()
})
it("should return success: false when file content is not valid JSON", async () => {
// Mock successful file selection
;(vscode.window.showOpenDialog as jest.Mock).mockResolvedValue([{ fsPath: "/mock/path/settings.json" }])
// Invalid JSON
const mockInvalidJson = "{ this is not valid JSON }"
// Mock reading file
;(fs.readFile as jest.Mock).mockResolvedValue(mockInvalidJson)
const result = await importSettings({
providerSettingsManager: mockProviderSettingsManager,
contextProxy: mockContextProxy,
})
expect(result).toEqual({ success: false })
expect(fs.readFile).toHaveBeenCalledWith("/mock/path/settings.json", "utf-8")
expect(mockProviderSettingsManager.import).not.toHaveBeenCalled()
expect(mockContextProxy.setValues).not.toHaveBeenCalled()
})
it("should return success: false when reading file fails", async () => {
// Mock successful file selection
;(vscode.window.showOpenDialog as jest.Mock).mockResolvedValue([{ fsPath: "/mock/path/settings.json" }])
// Mock file read error
;(fs.readFile as jest.Mock).mockRejectedValue(new Error("File read error"))
const result = await importSettings({
providerSettingsManager: mockProviderSettingsManager,
contextProxy: mockContextProxy,
})
expect(result).toEqual({ success: false })
expect(fs.readFile).toHaveBeenCalledWith("/mock/path/settings.json", "utf-8")
expect(mockProviderSettingsManager.import).not.toHaveBeenCalled()
expect(mockContextProxy.setValues).not.toHaveBeenCalled()
})
})
describe("exportSettings", () => {
it("should not export settings when user cancels file selection", async () => {
// Mock user canceling file selection
;(vscode.window.showSaveDialog as jest.Mock).mockResolvedValue(undefined)
await exportSettings({
providerSettingsManager: mockProviderSettingsManager,
contextProxy: mockContextProxy,
})
expect(vscode.window.showSaveDialog).toHaveBeenCalledWith({
filters: { JSON: ["json"] },
defaultUri: expect.anything(),
})
expect(mockProviderSettingsManager.export).not.toHaveBeenCalled()
expect(mockContextProxy.export).not.toHaveBeenCalled()
expect(fs.writeFile).not.toHaveBeenCalled()
})
it("should export settings to the selected file location", async () => {
// Mock successful file location selection
;(vscode.window.showSaveDialog as jest.Mock).mockResolvedValue({
fsPath: "/mock/path/roo-code-settings.json",
})
// Mock providerProfiles data
const mockProviderProfiles = {
currentApiConfigName: "test",
apiConfigs: {
test: {
apiProvider: "openai" as ProviderName,
id: "test-id",
},
},
}
mockProviderSettingsManager.export.mockResolvedValue(mockProviderProfiles)
// Mock globalSettings data
const mockGlobalSettings = {
mode: "code",
autoApprovalEnabled: true,
}
mockContextProxy.export.mockResolvedValue(mockGlobalSettings)
await exportSettings({
providerSettingsManager: mockProviderSettingsManager,
contextProxy: mockContextProxy,
})
expect(vscode.window.showSaveDialog).toHaveBeenCalledWith({
filters: { JSON: ["json"] },
defaultUri: expect.anything(),
})
expect(mockProviderSettingsManager.export).toHaveBeenCalled()
expect(mockContextProxy.export).toHaveBeenCalled()
expect(fs.mkdir).toHaveBeenCalledWith("/mock/path", { recursive: true })
expect(fs.writeFile).toHaveBeenCalledWith(
"/mock/path/roo-code-settings.json",
JSON.stringify(
{
providerProfiles: mockProviderProfiles,
globalSettings: mockGlobalSettings,
},
null,
2,
),
"utf-8",
)
})
it("should handle errors during the export process", async () => {
// Mock successful file location selection
;(vscode.window.showSaveDialog as jest.Mock).mockResolvedValue({
fsPath: "/mock/path/roo-code-settings.json",
})
// Mock provider profiles
mockProviderSettingsManager.export.mockResolvedValue({
currentApiConfigName: "test",
apiConfigs: {
test: {
apiProvider: "openai" as ProviderName,
id: "test-id",
},
},
})
// Mock global settings
mockContextProxy.export.mockResolvedValue({
mode: "code",
})
// Mock file write error
;(fs.writeFile as jest.Mock).mockRejectedValue(new Error("Write error"))
// The function catches errors internally and doesn't throw or return anything
await exportSettings({
providerSettingsManager: mockProviderSettingsManager,
contextProxy: mockContextProxy,
})
expect(vscode.window.showSaveDialog).toHaveBeenCalled()
expect(mockProviderSettingsManager.export).toHaveBeenCalled()
expect(mockContextProxy.export).toHaveBeenCalled()
expect(fs.mkdir).toHaveBeenCalledWith("/mock/path", { recursive: true })
expect(fs.writeFile).toHaveBeenCalled()
// The error is caught and the function exits silently
})
it("should handle errors during directory creation", async () => {
// Mock successful file location selection
;(vscode.window.showSaveDialog as jest.Mock).mockResolvedValue({
fsPath: "/mock/path/roo-code-settings.json",
})
// Mock provider profiles
mockProviderSettingsManager.export.mockResolvedValue({
currentApiConfigName: "test",
apiConfigs: {
test: {
apiProvider: "openai" as ProviderName,
id: "test-id",
},
},
})
// Mock global settings
mockContextProxy.export.mockResolvedValue({
mode: "code",
})
// Mock directory creation error
;(fs.mkdir as jest.Mock).mockRejectedValue(new Error("Directory creation error"))
// The function catches errors internally and doesn't throw or return anything
await exportSettings({
providerSettingsManager: mockProviderSettingsManager,
contextProxy: mockContextProxy,
})
expect(vscode.window.showSaveDialog).toHaveBeenCalled()
expect(mockProviderSettingsManager.export).toHaveBeenCalled()
expect(mockContextProxy.export).toHaveBeenCalled()
expect(fs.mkdir).toHaveBeenCalled()
expect(fs.writeFile).not.toHaveBeenCalled() // Should not be called since mkdir failed
})
it("should use the correct default save location", async () => {
// Mock user cancels to avoid full execution
;(vscode.window.showSaveDialog as jest.Mock).mockResolvedValue(undefined)
// Call the function
await exportSettings({
providerSettingsManager: mockProviderSettingsManager,
contextProxy: mockContextProxy,
})
// Verify the default save location
expect(vscode.window.showSaveDialog).toHaveBeenCalledWith({
filters: { JSON: ["json"] },
defaultUri: expect.anything(),
})
// Verify Uri.file was called with the correct path
expect(vscode.Uri.file).toHaveBeenCalledWith(path.join("/mock/home", "Documents", "roo-code-settings.json"))
})
})
})

View file

@ -0,0 +1,69 @@
import os from "os"
import * as path from "path"
import fs from "fs/promises"
import * as vscode from "vscode"
import { z } from "zod"
import { globalSettingsSchema } from "../../shared/globalState"
import { ProviderSettingsManager, providerProfilesSchema } from "./ProviderSettingsManager"
import { ContextProxy } from "./ContextProxy"
type ImportExportOptions = {
providerSettingsManager: ProviderSettingsManager
contextProxy: ContextProxy
}
export const importSettings = async ({ providerSettingsManager, contextProxy }: ImportExportOptions) => {
const uris = await vscode.window.showOpenDialog({
filters: { JSON: ["json"] },
canSelectMany: false,
})
if (!uris) {
return { success: false }
}
const schema = z.object({
providerProfiles: providerProfilesSchema,
globalSettings: globalSettingsSchema,
})
try {
const { providerProfiles, globalSettings } = schema.parse(
JSON.parse(await fs.readFile(uris[0].fsPath, "utf-8")),
)
const previousProviderProfiles = await providerSettingsManager.export()
await contextProxy.setValues(globalSettings)
await providerSettingsManager.import({ ...previousProviderProfiles, ...providerProfiles })
contextProxy.setValue("currentApiConfigName", providerProfiles.currentApiConfigName)
contextProxy.setValue("listApiConfigMeta", await providerSettingsManager.listConfig())
return { providerProfiles, globalSettings, success: true }
} catch (e) {
return { success: false }
}
}
export const exportSettings = async ({ providerSettingsManager, contextProxy }: ImportExportOptions) => {
const uri = await vscode.window.showSaveDialog({
filters: { JSON: ["json"] },
defaultUri: vscode.Uri.file(path.join(os.homedir(), "Documents", "roo-code-settings.json")),
})
if (!uri) {
return
}
try {
const providerProfiles = await providerSettingsManager.export()
const globalSettings = await contextProxy.export()
const dirname = path.dirname(uri.fsPath)
await fs.mkdir(dirname, { recursive: true })
await fs.writeFile(uri.fsPath, JSON.stringify({ providerProfiles, globalSettings }, null, 2), "utf-8")
} catch (e) {}
}

View file

@ -1,189 +0,0 @@
import * as vscode from "vscode"
import { logger } from "../utils/logging"
import {
GLOBAL_STATE_KEYS,
SECRET_KEYS,
GlobalStateKey,
SecretKey,
ConfigurationKey,
ConfigurationValues,
isSecretKey,
isGlobalStateKey,
isPassThroughStateKey,
} from "../shared/globalState"
import { API_CONFIG_KEYS, ApiConfiguration } from "../shared/api"
export class ContextProxy {
private readonly originalContext: vscode.ExtensionContext
private stateCache: Map<GlobalStateKey, any>
private secretCache: Map<SecretKey, string | undefined>
private _isInitialized = false
constructor(context: vscode.ExtensionContext) {
this.originalContext = context
this.stateCache = new Map()
this.secretCache = new Map()
this._isInitialized = false
}
public get isInitialized() {
return this._isInitialized
}
public async initialize() {
for (const key of GLOBAL_STATE_KEYS) {
try {
this.stateCache.set(key, this.originalContext.globalState.get(key))
} catch (error) {
logger.error(`Error loading global ${key}: ${error instanceof Error ? error.message : String(error)}`)
}
}
const promises = SECRET_KEYS.map(async (key) => {
try {
this.secretCache.set(key, await this.originalContext.secrets.get(key))
} catch (error) {
logger.error(`Error loading secret ${key}: ${error instanceof Error ? error.message : String(error)}`)
}
})
await Promise.all(promises)
this._isInitialized = true
}
get extensionUri() {
return this.originalContext.extensionUri
}
get extensionPath() {
return this.originalContext.extensionPath
}
get globalStorageUri() {
return this.originalContext.globalStorageUri
}
get logUri() {
return this.originalContext.logUri
}
get extension() {
return this.originalContext.extension
}
get extensionMode() {
return this.originalContext.extensionMode
}
getGlobalState<T>(key: GlobalStateKey): T | undefined
getGlobalState<T>(key: GlobalStateKey, defaultValue: T): T
getGlobalState<T>(key: GlobalStateKey, defaultValue?: T): T | undefined {
if (isPassThroughStateKey(key)) {
const value = this.originalContext.globalState.get(key)
return value === undefined || value === null ? defaultValue : (value as T)
}
const value = this.stateCache.get(key) as T | undefined
return value !== undefined ? value : (defaultValue as T | undefined)
}
updateGlobalState<T>(key: GlobalStateKey, value: T) {
if (isPassThroughStateKey(key)) {
return this.originalContext.globalState.update(key, value)
}
this.stateCache.set(key, value)
return this.originalContext.globalState.update(key, value)
}
getSecret(key: SecretKey) {
return this.secretCache.get(key)
}
storeSecret(key: SecretKey, value?: string) {
// Update cache.
this.secretCache.set(key, value)
// Write directly to context.
return value === undefined
? this.originalContext.secrets.delete(key)
: this.originalContext.secrets.store(key, value)
}
/**
* Set a value in either secrets or global state based on key type.
* If the key is in SECRET_KEYS, it will be stored as a secret.
* If the key is in GLOBAL_STATE_KEYS or unknown, it will be stored in global state.
* @param key The key to set
* @param value The value to set
* @returns A promise that resolves when the operation completes
*/
setValue(key: ConfigurationKey, value: any) {
if (isSecretKey(key)) {
return this.storeSecret(key, value)
}
if (isGlobalStateKey(key)) {
return this.updateGlobalState(key, value)
}
logger.warn(`Unknown key: ${key}. Storing as global state.`)
return this.updateGlobalState(key, value)
}
/**
* Set multiple values at once. Each key will be routed to either
* secrets or global state based on its type.
* @param values An object containing key-value pairs to set
* @returns A promise that resolves when all operations complete
*/
async setValues(values: Partial<ConfigurationValues>) {
const promises: Thenable<void>[] = []
for (const [key, value] of Object.entries(values)) {
promises.push(this.setValue(key as ConfigurationKey, value))
}
await Promise.all(promises)
}
async setApiConfiguration(apiConfiguration: ApiConfiguration) {
// Explicitly clear out any old API configuration values before that
// might not be present in the new configuration.
// If a value is not present in the new configuration, then it is assumed
// that the setting's value should be `undefined` and therefore we
// need to remove it from the state cache if it exists.
await this.setValues({
...API_CONFIG_KEYS.filter((key) => !!this.stateCache.get(key)).reduce(
(acc, key) => ({ ...acc, [key]: undefined }),
{} as Partial<ConfigurationValues>,
),
...apiConfiguration,
})
}
/**
* Resets all global state, secrets, and in-memory caches.
* This clears all data from both the in-memory caches and the VSCode storage.
* @returns A promise that resolves when all reset operations are complete
*/
async resetAllState() {
// Clear in-memory caches
this.stateCache.clear()
this.secretCache.clear()
// Reset all global state values to undefined.
const stateResetPromises = GLOBAL_STATE_KEYS.map((key) =>
this.originalContext.globalState.update(key, undefined),
)
// Delete all secrets.
const secretResetPromises = SECRET_KEYS.map((key) => this.originalContext.secrets.delete(key))
// Wait for all reset operations to complete.
await Promise.all([...stateResetPromises, ...secretResetPromises])
this.initialize()
}
}

View file

@ -1,7 +1,8 @@
import fs from "fs/promises"
import path from "path"
import * as vscode from "vscode"
import { LANGUAGES } from "../../../shared/language"
import { isLanguage } from "../../../shared/globalState"
async function safeReadFile(filePath: string): Promise<string> {
try {
@ -48,7 +49,7 @@ export async function addCustomInstructions(
// Add language preference if provided
if (options.language) {
const languageName = LANGUAGES[options.language] || options.language
const languageName = isLanguage(options.language) ? LANGUAGES[options.language] : options.language
sections.push(
`Language Preference:\nYou should always speak and think in the "${languageName}" (${options.language}) language unless the user gives you instructions below to do otherwise.`,
)

View file

@ -1,20 +1,29 @@
import os from "os"
import * as path from "path"
import fs from "fs/promises"
import EventEmitter from "events"
import { Anthropic } from "@anthropic-ai/sdk"
import delay from "delay"
import axios from "axios"
import EventEmitter from "events"
import fs from "fs/promises"
import os from "os"
import pWaitFor from "p-wait-for"
import * as path from "path"
import * as vscode from "vscode"
import {
CheckpointStorage,
GlobalState,
Language,
ProviderSettings,
RooCodeSettings,
GlobalStateKey,
SecretStateKey,
} from "../../exports/roo-code"
import { changeLanguage, t } from "../../i18n"
import { setPanel } from "../../activate/registerCommands"
import {
ApiConfiguration,
ApiProvider,
ModelInfo,
API_CONFIG_KEYS,
requestyDefaultModelId,
requestyDefaultModelInfo,
openRouterDefaultModelId,
@ -25,17 +34,10 @@ import {
import { findLast } from "../../shared/array"
import { supportPrompt } from "../../shared/support-prompt"
import { GlobalFileNames } from "../../shared/globalFileNames"
import {
SecretKey,
GlobalStateKey,
SECRET_KEYS,
GLOBAL_STATE_KEYS,
ConfigurationValues,
} from "../../shared/globalState"
import { HistoryItem } from "../../shared/HistoryItem"
import { ApiConfigMeta, ExtensionMessage } from "../../shared/ExtensionMessage"
import { checkoutDiffPayloadSchema, checkoutRestorePayloadSchema, WebviewMessage } from "../../shared/WebviewMessage"
import { Mode, PromptComponent, defaultModeSlug, ModeConfig, getModeBySlug, getGroupName } from "../../shared/modes"
import { Mode, PromptComponent, defaultModeSlug, getModeBySlug, getGroupName } from "../../shared/modes"
import { checkExistKey } from "../../shared/checkExistApiConfig"
import { EXPERIMENT_IDS, experiments as Experiments, experimentDefault, ExperimentId } from "../../shared/experiments"
import { formatLanguage } from "../../shared/language"
@ -58,9 +60,10 @@ import { singleCompletionHandler } from "../../utils/single-completion-handler"
import { searchCommits } from "../../utils/git"
import { getDiffStrategy } from "../diff/DiffStrategy"
import { SYSTEM_PROMPT } from "../prompts/system"
import { ConfigManager } from "../config/ConfigManager"
import { ContextProxy } from "../config/ContextProxy"
import { ProviderSettingsManager } from "../config/ProviderSettingsManager"
import { exportSettings, importSettings } from "../config/importExport"
import { CustomModesManager } from "../config/CustomModesManager"
import { ContextProxy } from "../contextProxy"
import { buildApiHandler } from "../../api"
import { getOpenRouterModels } from "../../api/providers/openrouter"
import { getGlamaModels } from "../../api/providers/glama"
@ -99,12 +102,11 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
private workspaceTracker?: WorkspaceTracker
protected mcpHub?: McpHub // Change from private to protected
private latestAnnouncementId = "mar-20-2025-3-10" // update to some unique identifier when we add a new announcement
private settingsImportedAt?: number
private contextProxy: ContextProxy
configManager: ConfigManager
customModesManager: CustomModesManager
get cwd() {
return getWorkspacePath()
}
public readonly providerSettingsManager: ProviderSettingsManager
public readonly customModesManager: CustomModesManager
constructor(
readonly context: vscode.ExtensionContext,
private readonly outputChannel: vscode.OutputChannel,
@ -116,11 +118,14 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
this.contextProxy = new ContextProxy(context)
ClineProvider.activeInstances.add(this)
// Register this provider with the telemetry service to enable it to add properties like mode and provider
// Register this provider with the telemetry service to enable it to add
// properties like mode and provider.
telemetryService.setProvider(this)
this.workspaceTracker = new WorkspaceTracker(this)
this.configManager = new ConfigManager(this.context)
this.providerSettingsManager = new ProviderSettingsManager(this.context)
this.customModesManager = new CustomModesManager(this.context, async () => {
await this.postStateToWebview()
})
@ -886,7 +891,7 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
}
})
this.configManager
this.providerSettingsManager
.listConfig()
.then(async (listApiConfig) => {
if (!listApiConfig) {
@ -894,25 +899,27 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
}
if (listApiConfig.length === 1) {
// check if first time init then sync with exist config
// Check if first time init then sync with exist config.
if (!checkExistKey(listApiConfig[0])) {
const { apiConfiguration } = await this.getState()
await this.configManager.saveConfig(
await this.providerSettingsManager.saveConfig(
listApiConfig[0].name ?? "default",
apiConfiguration,
)
listApiConfig[0].apiProvider = apiConfiguration.apiProvider
}
}
const currentConfigName = (await this.getGlobalState("currentApiConfigName")) as string
const currentConfigName = this.getGlobalState("currentApiConfigName")
if (currentConfigName) {
if (!(await this.configManager.hasConfig(currentConfigName))) {
if (!(await this.providerSettingsManager.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(
const apiConfig = await this.providerSettingsManager.loadConfig(
listApiConfig?.[0]?.name,
)
@ -1037,6 +1044,7 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
break
case "deleteMultipleTasksWithIds": {
const ids = message.ids
if (Array.isArray(ids)) {
// Process in batches of 20 (or another reasonable number)
const batchSize = 20
@ -1080,6 +1088,26 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
}
case "exportTaskWithId":
this.exportTaskWithId(message.text!)
break
case "importSettings":
const { success } = await importSettings({
providerSettingsManager: this.providerSettingsManager,
contextProxy: this.contextProxy,
})
if (success) {
this.settingsImportedAt = Date.now()
await this.postStateToWebview()
await vscode.window.showInformationMessage(t("common:info.settings_imported"))
}
break
case "exportSettings":
await exportSettings({
providerSettingsManager: this.providerSettingsManager,
contextProxy: this.contextProxy,
})
break
case "resetState":
await this.resetState()
@ -1338,7 +1366,7 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
case "checkpointStorage":
console.log(`[ClineProvider] checkpointStorage: ${message.text}`)
const checkpointStorage = message.text ?? "task"
await this.updateGlobalState("checkpointStorage", checkpointStorage)
await this.updateGlobalState("checkpointStorage", checkpointStorage as CheckpointStorage)
await this.postStateToWebview()
break
case "browserViewportSize":
@ -1485,13 +1513,8 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
return
}
const existingPrompts = (await this.getGlobalState("customSupportPrompts")) || {}
const updatedPrompts = {
...existingPrompts,
...message.values,
}
const existingPrompts = this.getGlobalState("customSupportPrompts") ?? {}
const updatedPrompts = { ...existingPrompts, ...message.values }
await this.updateGlobalState("customSupportPrompts", updatedPrompts)
await this.postStateToWebview()
} catch (error) {
@ -1507,15 +1530,9 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
return
}
const existingPrompts = ((await this.getGlobalState("customSupportPrompts")) ||
{}) as Record<string, any>
const updatedPrompts = {
...existingPrompts,
}
const existingPrompts = this.getGlobalState("customSupportPrompts") ?? {}
const updatedPrompts = { ...existingPrompts }
updatedPrompts[message.text] = undefined
await this.updateGlobalState("customSupportPrompts", updatedPrompts)
await this.postStateToWebview()
} catch (error) {
@ -1527,28 +1544,12 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
break
case "updatePrompt":
if (message.promptMode && message.customPrompt !== undefined) {
const existingPrompts = (await this.getGlobalState("customModePrompts")) || {}
const updatedPrompts = {
...existingPrompts,
[message.promptMode]: message.customPrompt,
}
const existingPrompts = this.getGlobalState("customModePrompts") ?? {}
const updatedPrompts = { ...existingPrompts, [message.promptMode]: message.customPrompt }
await this.updateGlobalState("customModePrompts", updatedPrompts)
// Get current state and explicitly include customModePrompts
const currentState = await this.getState()
const stateWithPrompts = {
...currentState,
customModePrompts: updatedPrompts,
}
// Post state with prompts
this.view?.webview.postMessage({
type: "state",
state: stateWithPrompts,
})
const stateWithPrompts = { ...currentState, customModePrompts: updatedPrompts }
this.view?.webview.postMessage({ type: "state", state: stateWithPrompts })
}
break
case "deleteMessage": {
@ -1664,7 +1665,7 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
break
case "language":
changeLanguage(message.text ?? "en")
await this.updateGlobalState("language", message.text)
await this.updateGlobalState("language", message.text as Language)
await this.postStateToWebview()
break
case "showRooIgnoredFiles":
@ -1677,13 +1678,9 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
break
case "toggleApiConfigPin":
if (message.text) {
const currentPinned = ((await this.getGlobalState("pinnedApiConfigs")) || {}) as Record<
string,
boolean
>
const currentPinned = this.getGlobalState("pinnedApiConfigs") ?? {}
const updatedPinned: Record<string, boolean> = { ...currentPinned }
// Toggle the pinned state
if (currentPinned[message.text]) {
delete updatedPinned[message.text]
} else {
@ -1719,7 +1716,7 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
(c: ApiConfigMeta) => c.id === enhancementApiConfigId,
)
if (config?.name) {
const loadedConfig = await this.configManager.loadConfig(config.name)
const loadedConfig = await this.providerSettingsManager.loadConfig(config.name)
if (loadedConfig.apiProvider) {
configToUse = loadedConfig
}
@ -1842,8 +1839,8 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
case "saveApiConfiguration":
if (message.text && message.apiConfiguration) {
try {
await this.configManager.saveConfig(message.text, message.apiConfiguration)
const listApiConfig = await this.configManager.listConfig()
await this.providerSettingsManager.saveConfig(message.text, message.apiConfiguration)
const listApiConfig = await this.providerSettingsManager.listConfig()
await this.updateGlobalState("listApiConfigMeta", listApiConfig)
} catch (error) {
this.outputChannel.appendLine(
@ -1868,7 +1865,7 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
}
// Load the old configuration to get its ID
const oldConfig = await this.configManager.loadConfig(oldName)
const oldConfig = await this.providerSettingsManager.loadConfig(oldName)
// Create a new configuration with the same ID
const newConfig = {
@ -1877,10 +1874,10 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
}
// Save with the new name but same ID
await this.configManager.saveConfig(newName, newConfig)
await this.configManager.deleteConfig(oldName)
await this.providerSettingsManager.saveConfig(newName, newConfig)
await this.providerSettingsManager.deleteConfig(oldName)
const listApiConfig = await this.configManager.listConfig()
const listApiConfig = await this.providerSettingsManager.listConfig()
// Update listApiConfigMeta first to ensure UI has latest data
await this.updateGlobalState("listApiConfigMeta", listApiConfig)
@ -1898,8 +1895,8 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
case "loadApiConfiguration":
if (message.text) {
try {
const apiConfig = await this.configManager.loadConfig(message.text)
const listApiConfig = await this.configManager.listConfig()
const apiConfig = await this.providerSettingsManager.loadConfig(message.text)
const listApiConfig = await this.providerSettingsManager.listConfig()
await Promise.all([
this.updateGlobalState("listApiConfigMeta", listApiConfig),
@ -1919,10 +1916,10 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
case "loadApiConfigurationById":
if (message.text) {
try {
const { config: apiConfig, name } = await this.configManager.loadConfigById(
const { config: apiConfig, name } = await this.providerSettingsManager.loadConfigById(
message.text,
)
const listApiConfig = await this.configManager.listConfig()
const listApiConfig = await this.providerSettingsManager.listConfig()
await Promise.all([
this.updateGlobalState("listApiConfigMeta", listApiConfig),
@ -1952,16 +1949,19 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
}
try {
await this.configManager.deleteConfig(message.text)
const listApiConfig = await this.configManager.listConfig()
await this.providerSettingsManager.deleteConfig(message.text)
const listApiConfig = await this.providerSettingsManager.listConfig()
// Update listApiConfigMeta first to ensure UI has latest data
await this.updateGlobalState("listApiConfigMeta", listApiConfig)
// If this was the current config, switch to first available
const currentApiConfigName = await this.getGlobalState("currentApiConfigName")
const currentApiConfigName = this.getGlobalState("currentApiConfigName")
if (message.text === currentApiConfigName && listApiConfig?.[0]?.name) {
const apiConfig = await this.configManager.loadConfig(listApiConfig[0].name)
const apiConfig = await this.providerSettingsManager.loadConfig(
listApiConfig[0].name,
)
await Promise.all([
this.updateGlobalState("currentApiConfigName", listApiConfig[0].name),
this.updateApiConfiguration(apiConfig),
@ -1979,7 +1979,7 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
break
case "getListApiConfiguration":
try {
const listApiConfig = await this.configManager.listConfig()
const listApiConfig = await this.providerSettingsManager.listConfig()
await this.updateGlobalState("listApiConfigMeta", listApiConfig)
this.postMessageToWebview({ type: "listApiConfig", listApiConfig })
} catch (error) {
@ -1995,9 +1995,9 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
}
const updatedExperiments = {
...((await this.getGlobalState("experiments")) ?? experimentDefault),
...(this.getGlobalState("experiments") ?? experimentDefault),
...message.values,
} as Record<ExperimentId, boolean>
}
await this.updateGlobalState("experiments", updatedExperiments)
@ -2169,8 +2169,8 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
await this.updateGlobalState("mode", newMode)
// Load the saved API config for the new mode if it exists
const savedConfigId = await this.configManager.getModeConfigId(newMode)
const listApiConfig = await this.configManager.listConfig()
const savedConfigId = await this.providerSettingsManager.getModeConfigId(newMode)
const listApiConfig = await this.providerSettingsManager.listConfig()
// Update listApiConfigMeta first to ensure UI has latest data
await this.updateGlobalState("listApiConfigMeta", listApiConfig)
@ -2179,7 +2179,7 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
if (savedConfigId) {
const config = listApiConfig?.find((c) => c.id === savedConfigId)
if (config?.name) {
const apiConfig = await this.configManager.loadConfig(config.name)
const apiConfig = await this.providerSettingsManager.loadConfig(config.name)
await Promise.all([
this.updateGlobalState("currentApiConfigName", config.name),
this.updateApiConfiguration(apiConfig),
@ -2187,11 +2187,12 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
}
} else {
// If no saved config for this mode, save current config as default
const currentApiConfigName = await this.getGlobalState("currentApiConfigName")
const currentApiConfigName = this.getGlobalState("currentApiConfigName")
if (currentApiConfigName) {
const config = listApiConfig?.find((c) => c.name === currentApiConfigName)
if (config?.id) {
await this.configManager.setModeConfig(newMode, config.id)
await this.providerSettingsManager.setModeConfig(newMode, config.id)
}
}
}
@ -2199,23 +2200,24 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
await this.postStateToWebview()
}
private async updateApiConfiguration(apiConfiguration: ApiConfiguration) {
private async updateApiConfiguration(providerSettings: ProviderSettings) {
// Update mode's default config.
const { mode } = await this.getState()
if (mode) {
const currentApiConfigName = await this.getGlobalState("currentApiConfigName")
const listApiConfig = await this.configManager.listConfig()
const currentApiConfigName = this.getGlobalState("currentApiConfigName")
const listApiConfig = await this.providerSettingsManager.listConfig()
const config = listApiConfig?.find((c) => c.name === currentApiConfigName)
if (config?.id) {
await this.configManager.setModeConfig(mode, config.id)
await this.providerSettingsManager.setModeConfig(mode, config.id)
}
}
await this.contextProxy.setApiConfiguration(apiConfiguration)
await this.contextProxy.setProviderSettings(providerSettings)
if (this.getCurrentCline()) {
this.getCurrentCline()!.api = buildApiHandler(apiConfiguration)
this.getCurrentCline()!.api = buildApiHandler(providerSettings)
}
}
@ -2407,8 +2409,8 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
async upsertApiConfiguration(configName: string, apiConfiguration: ApiConfiguration) {
try {
await this.configManager.saveConfig(configName, apiConfiguration)
const listApiConfig = await this.configManager.listConfig()
await this.providerSettingsManager.saveConfig(configName, apiConfiguration)
const listApiConfig = await this.providerSettingsManager.listConfig()
await Promise.all([
this.updateGlobalState("listApiConfigMeta", listApiConfig),
@ -2434,8 +2436,9 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
uiMessagesFilePath: string
apiConversationHistory: Anthropic.MessageParam[]
}> {
const history = ((await this.getGlobalState("taskHistory")) as HistoryItem[] | undefined) || []
const history = this.getGlobalState("taskHistory") ?? []
const historyItem = history.find((item) => item.id === id)
if (historyItem) {
const { getTaskDirectoryPath } = await import("../../shared/storagePathManager")
const globalStoragePath = this.contextProxy.globalStorageUri.fsPath
@ -2443,8 +2446,10 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
const apiConversationHistoryFilePath = path.join(taskDirPath, GlobalFileNames.apiConversationHistory)
const uiMessagesFilePath = path.join(taskDirPath, GlobalFileNames.uiMessages)
const fileExists = await fileExistsAtPath(apiConversationHistoryFilePath)
if (fileExists) {
const apiConversationHistory = JSON.parse(await fs.readFile(apiConversationHistoryFilePath, "utf8"))
return {
historyItem,
taskDirPath,
@ -2454,6 +2459,7 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
}
}
}
// if we tried to get a task that doesn't exist, remove it from state
// FIXME: this seems to happen sometimes when the json file doesnt save to disk for some reason
await this.deleteTaskFromState(id)
@ -2524,12 +2530,9 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
}
async deleteTaskFromState(id: string) {
// Remove the task from history
const taskHistory = ((await this.getGlobalState("taskHistory")) as HistoryItem[]) || []
const taskHistory = this.getGlobalState("taskHistory") ?? []
const updatedTaskHistory = taskHistory.filter((task) => task.id !== id)
await this.updateGlobalState("taskHistory", updatedTaskHistory)
// Notify the webview that the task has been deleted
await this.postStateToWebview()
}
@ -2575,6 +2578,7 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
rateLimitSeconds,
currentApiConfigName,
listApiConfigMeta,
pinnedApiConfigs,
mode,
customModePrompts,
customSupportPrompts,
@ -2641,6 +2645,7 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
rateLimitSeconds: rateLimitSeconds ?? 0,
currentApiConfigName: currentApiConfigName ?? "default",
listApiConfigMeta: listApiConfigMeta ?? [],
pinnedApiConfigs: pinnedApiConfigs ?? {},
mode: mode ?? defaultModeSlug,
customModePrompts: customModePrompts ?? {},
customSupportPrompts: customSupportPrompts ?? {},
@ -2660,111 +2665,35 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
language,
renderContext: this.renderContext,
maxReadFileLine: maxReadFileLine ?? 500,
pinnedApiConfigs:
((await this.getGlobalState("pinnedApiConfigs")) as Record<string, boolean>) ??
({} as Record<string, boolean>),
settingsImportedAt: this.settingsImportedAt,
}
}
// Caching mechanism to keep track of webview messages + API conversation history per provider instance
/*
Now that we use retainContextWhenHidden, we don't have to store a cache of cline messages in the user's state, but we could to reduce memory footprint in long conversations.
- We have to be careful of what state is shared between ClineProvider instances since there could be multiple instances of the extension running at once. For example when we cached cline messages using the same key, two instances of the extension could end up using the same key and overwriting each other's messages.
- Some state does need to be shared between the instances, i.e. the API key--however there doesn't seem to be a good way to notfy the other instances that the API key has changed.
We need to use a unique identifier for each ClineProvider instance's message cache since we could be running several instances of the extension outside of just the sidebar i.e. in editor panels.
// conversation history to send in API requests
/*
It seems that some API messages do not comply with vscode state requirements. Either the Anthropic library is manipulating these values somehow in the backend in a way thats creating cyclic references, or the API returns a function or a Symbol as part of the message content.
VSCode docs about state: "The value must be JSON-stringifyable ... value — A value. MUST not contain cyclic references."
For now we'll store the conversation history in memory, and if we need to store in state directly we'd need to do a manual conversion to ensure proper json stringification.
*/
// getApiConversationHistory(): Anthropic.MessageParam[] {
// // const history = (await this.getGlobalState(
// // this.getApiConversationHistoryStateKey()
// // )) as Anthropic.MessageParam[]
// // return history || []
// return this.apiConversationHistory
// }
// setApiConversationHistory(history: Anthropic.MessageParam[] | undefined) {
// // await this.updateGlobalState(this.getApiConversationHistoryStateKey(), history)
// this.apiConversationHistory = history || []
// }
// addMessageToApiConversationHistory(message: Anthropic.MessageParam): Anthropic.MessageParam[] {
// // const history = await this.getApiConversationHistory()
// // history.push(message)
// // await this.setApiConversationHistory(history)
// // return history
// this.apiConversationHistory.push(message)
// return this.apiConversationHistory
// }
/*
Storage
https://dev.to/kompotkot/how-to-use-secretstorage-in-your-vscode-extensions-2hco
https://www.eliostruyf.com/devhack-code-extension-storage-options/
*/
/**
* Storage
* https://dev.to/kompotkot/how-to-use-secretstorage-in-your-vscode-extensions-2hco
* https://www.eliostruyf.com/devhack-code-extension-storage-options/
*/
async getState() {
// Create an object to store all fetched values
const stateValues: Record<GlobalStateKey | SecretKey, any> = {} as Record<GlobalStateKey | SecretKey, any>
const secretValues: Record<SecretKey, any> = {} as Record<SecretKey, any>
const stateValues = this.contextProxy.getValues()
// Create promise arrays for global state and secrets
const statePromises = GLOBAL_STATE_KEYS.map((key) => this.getGlobalState(key))
const secretPromises = SECRET_KEYS.map((key) => this.getSecret(key))
const customModes = await this.customModesManager.getCustomModes()
// Add promise for custom modes which is handled separately
const customModesPromise = this.customModesManager.getCustomModes()
// Determine apiProvider with the same logic as before.
const apiProvider: ApiProvider = stateValues.apiProvider ? stateValues.apiProvider : "anthropic"
let idx = 0
const valuePromises = await Promise.all([...statePromises, ...secretPromises, customModesPromise])
// Populate stateValues and secretValues
GLOBAL_STATE_KEYS.forEach((key, _) => {
stateValues[key] = valuePromises[idx]
idx = idx + 1
})
SECRET_KEYS.forEach((key, index) => {
secretValues[key] = valuePromises[idx]
idx = idx + 1
})
let customModes = valuePromises[idx] as ModeConfig[] | undefined
// Determine apiProvider with the same logic as before
let apiProvider: ApiProvider
if (stateValues.apiProvider) {
apiProvider = stateValues.apiProvider
} else {
apiProvider = "anthropic"
}
// Build the apiConfiguration object combining state values and secrets
// Using the dynamic approach with API_CONFIG_KEYS
const apiConfiguration: ApiConfiguration = {
// Dynamically add all API-related keys from stateValues
...Object.fromEntries(API_CONFIG_KEYS.map((key) => [key, stateValues[key]])),
// Add all secrets
...secretValues,
}
// Build the apiConfiguration object combining state values and secrets.
const providerSettings = this.contextProxy.getProviderSettings()
// Ensure apiProvider is set properly if not already in state
if (!apiConfiguration.apiProvider) {
apiConfiguration.apiProvider = apiProvider
if (!providerSettings.apiProvider) {
providerSettings.apiProvider = apiProvider
}
// Return the same structure as before
return {
apiConfiguration,
apiConfiguration: providerSettings,
lastShownAnnouncementId: stateValues.lastShownAnnouncementId,
customInstructions: stateValues.customInstructions,
alwaysAllowReadOnly: stateValues.alwaysAllowReadOnly ?? false,
@ -2803,6 +2732,7 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
rateLimitSeconds: stateValues.rateLimitSeconds ?? 0,
currentApiConfigName: stateValues.currentApiConfigName ?? "default",
listApiConfigMeta: stateValues.listApiConfigMeta ?? [],
pinnedApiConfigs: stateValues.pinnedApiConfigs ?? {},
modeApiConfigs: stateValues.modeApiConfigs ?? ({} as Record<Mode, string>),
customModePrompts: stateValues.customModePrompts ?? {},
customSupportPrompts: stateValues.customSupportPrompts ?? {},
@ -2821,7 +2751,7 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
}
async updateTaskHistory(item: HistoryItem): Promise<HistoryItem[]> {
const history = ((await this.getGlobalState("taskHistory")) as HistoryItem[] | undefined) || []
const history = (this.getGlobalState("taskHistory") as HistoryItem[] | undefined) || []
const existingItemIndex = history.findIndex((h) => h.id === item.id)
if (existingItemIndex !== -1) {
@ -2829,36 +2759,43 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
} else {
history.push(item)
}
await this.updateGlobalState("taskHistory", history)
return history
}
// global
public async updateGlobalState(key: GlobalStateKey, value: any) {
await this.contextProxy.updateGlobalState(key, value)
public async updateGlobalState<K extends GlobalStateKey>(key: K, value: GlobalState[K]) {
await this.contextProxy.setValue(key, value)
}
public async getGlobalState(key: GlobalStateKey) {
return await this.contextProxy.getGlobalState(key)
public getGlobalState<K extends GlobalStateKey>(key: K) {
return this.contextProxy.getValue(key)
}
// secrets
public async storeSecret(key: SecretKey, value?: string) {
await this.contextProxy.storeSecret(key, value)
public async storeSecret(key: SecretStateKey, value?: string) {
await this.contextProxy.setValue(key, value)
}
private async getSecret(key: SecretKey) {
return await this.contextProxy.getSecret(key)
private getSecret(key: SecretStateKey) {
return this.contextProxy.getValue(key)
}
// global + secret
public async setValues(values: Partial<ConfigurationValues>) {
public async setValues(values: RooCodeSettings) {
await this.contextProxy.setValues(values)
}
// cwd
get cwd() {
return getWorkspacePath()
}
// dev
async resetState() {
@ -2873,7 +2810,7 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
}
await this.contextProxy.resetAllState()
await this.configManager.resetAllConfigs()
await this.providerSettingsManager.resetAllConfigs()
await this.customModesManager.resetCustomModes()
await this.removeClineFromStack()
await this.postStateToWebview()

View file

@ -5,18 +5,16 @@ import axios from "axios"
import { ClineProvider } from "../ClineProvider"
import { ExtensionMessage, ExtensionState } from "../../../shared/ExtensionMessage"
import { GlobalStateKey, SecretKey } from "../../../shared/globalState"
import { setSoundEnabled } from "../../../utils/sound"
import { setTtsEnabled } from "../../../utils/tts"
import { defaultModeSlug } from "../../../shared/modes"
import { experimentDefault } from "../../../shared/experiments"
import { Cline } from "../../Cline"
// Mock setup must come before imports
jest.mock("../../prompts/sections/custom-instructions")
// Mock ContextProxy
jest.mock("../../contextProxy", () => {
jest.mock("../../config/ContextProxy", () => {
return {
ContextProxy: jest.fn().mockImplementation((context) => ({
originalContext: context,
@ -649,8 +647,7 @@ describe("ClineProvider", () => {
await provider.resolveWebviewView(mockWebviewView)
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
// Mock ConfigManager methods
provider.configManager = {
;(provider as any).providerSettingsManager = {
getModeConfigId: jest.fn().mockResolvedValue("test-id"),
listConfig: jest.fn().mockResolvedValue([{ name: "test-config", id: "test-id", apiProvider: "anthropic" }]),
loadConfig: jest.fn().mockResolvedValue({ apiProvider: "anthropic" }),
@ -661,8 +658,8 @@ describe("ClineProvider", () => {
await messageHandler({ type: "mode", text: "architect" })
// Should load the saved config for architect mode
expect(provider.configManager.getModeConfigId).toHaveBeenCalledWith("architect")
expect(provider.configManager.loadConfig).toHaveBeenCalledWith("test-config")
expect(provider.providerSettingsManager.getModeConfigId).toHaveBeenCalledWith("architect")
expect(provider.providerSettingsManager.loadConfig).toHaveBeenCalledWith("test-config")
expect(mockContext.globalState.update).toHaveBeenCalledWith("currentApiConfigName", "test-config")
})
@ -670,8 +667,7 @@ describe("ClineProvider", () => {
await provider.resolveWebviewView(mockWebviewView)
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
// Mock ConfigManager methods
provider.configManager = {
;(provider as any).providerSettingsManager = {
getModeConfigId: jest.fn().mockResolvedValue(undefined),
listConfig: jest
.fn()
@ -691,14 +687,14 @@ describe("ClineProvider", () => {
await messageHandler({ type: "mode", text: "architect" })
// Should save current config as default for architect mode
expect(provider.configManager.setModeConfig).toHaveBeenCalledWith("architect", "current-id")
expect(provider.providerSettingsManager.setModeConfig).toHaveBeenCalledWith("architect", "current-id")
})
test("saves config as default for current mode when loading config", async () => {
await provider.resolveWebviewView(mockWebviewView)
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
provider.configManager = {
;(provider as any).providerSettingsManager = {
loadConfig: jest.fn().mockResolvedValue({ apiProvider: "anthropic", id: "new-id" }),
loadConfigById: jest
.fn()
@ -715,14 +711,14 @@ describe("ClineProvider", () => {
await messageHandler({ type: "loadApiConfiguration", text: "new-config" })
// Should save new config as default for architect mode
expect(provider.configManager.setModeConfig).toHaveBeenCalledWith("architect", "new-id")
expect(provider.providerSettingsManager.setModeConfig).toHaveBeenCalledWith("architect", "new-id")
})
test("load API configuration by ID works and updates mode config", async () => {
await provider.resolveWebviewView(mockWebviewView)
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
provider.configManager = {
;(provider as any).providerSettingsManager = {
loadConfigById: jest.fn().mockResolvedValue({
config: { apiProvider: "anthropic", id: "config-id-123" },
name: "config-by-id",
@ -741,10 +737,10 @@ describe("ClineProvider", () => {
await messageHandler({ type: "loadApiConfigurationById", text: "config-id-123" })
// Should save new config as default for architect mode
expect(provider.configManager.setModeConfig).toHaveBeenCalledWith("architect", "config-id-123")
expect(provider.providerSettingsManager.setModeConfig).toHaveBeenCalledWith("architect", "config-id-123")
// Ensure the loadConfigById method was called with the correct ID
expect(provider.configManager.loadConfigById).toHaveBeenCalledWith("config-id-123")
expect(provider.providerSettingsManager.loadConfigById).toHaveBeenCalledWith("config-id-123")
})
test("handles browserToolEnabled setting", async () => {
@ -973,7 +969,7 @@ describe("ClineProvider", () => {
await provider.resolveWebviewView(mockWebviewView)
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
provider.configManager = {
;(provider as any).providerSettingsManager = {
listConfig: jest.fn().mockResolvedValue([{ name: "test-config", id: "test-id", apiProvider: "anthropic" }]),
setModeConfig: jest.fn(),
} as any
@ -985,7 +981,7 @@ describe("ClineProvider", () => {
})
// Should save config as default for current mode
expect(provider.configManager.setModeConfig).toHaveBeenCalledWith("code", "test-id")
expect(provider.providerSettingsManager.setModeConfig).toHaveBeenCalledWith("code", "test-id")
})
test("file content includes line numbers", async () => {
@ -1619,8 +1615,7 @@ describe("ClineProvider", () => {
})
test("loads saved API config when switching modes", async () => {
// Mock ConfigManager methods
provider.configManager = {
;(provider as any).providerSettingsManager = {
getModeConfigId: jest.fn().mockResolvedValue("saved-config-id"),
listConfig: jest
.fn()
@ -1636,8 +1631,8 @@ describe("ClineProvider", () => {
expect(mockContext.globalState.update).toHaveBeenCalledWith("mode", "architect")
// Verify saved config was loaded
expect(provider.configManager.getModeConfigId).toHaveBeenCalledWith("architect")
expect(provider.configManager.loadConfig).toHaveBeenCalledWith("saved-config")
expect(provider.providerSettingsManager.getModeConfigId).toHaveBeenCalledWith("architect")
expect(provider.providerSettingsManager.loadConfig).toHaveBeenCalledWith("saved-config")
expect(mockContext.globalState.update).toHaveBeenCalledWith("currentApiConfigName", "saved-config")
// Verify state was posted to webview
@ -1645,8 +1640,7 @@ describe("ClineProvider", () => {
})
test("saves current config when switching to mode without config", async () => {
// Mock ConfigManager methods
provider.configManager = {
;(provider as any).providerSettingsManager = {
getModeConfigId: jest.fn().mockResolvedValue(undefined),
listConfig: jest
.fn()
@ -1667,7 +1661,7 @@ describe("ClineProvider", () => {
expect(mockContext.globalState.update).toHaveBeenCalledWith("mode", "architect")
// Verify current config was saved as default for new mode
expect(provider.configManager.setModeConfig).toHaveBeenCalledWith("architect", "current-id")
expect(provider.providerSettingsManager.setModeConfig).toHaveBeenCalledWith("architect", "current-id")
// Verify state was posted to webview
expect(mockPostMessage).toHaveBeenCalledWith(expect.objectContaining({ type: "state" }))
@ -1680,7 +1674,7 @@ describe("ClineProvider", () => {
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
// Mock CustomModesManager methods
provider.customModesManager = {
;(provider as any).customModesManager = {
updateCustomMode: jest.fn().mockResolvedValue(undefined),
getCustomModes: jest.fn().mockResolvedValue({
customModes: [
@ -1750,8 +1744,7 @@ describe("ClineProvider", () => {
provider.resolveWebviewView(mockWebviewView)
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
// Mock ConfigManager methods to simulate error
provider.configManager = {
;(provider as any).providerSettingsManager = {
setModeConfig: jest.fn().mockRejectedValue(new Error("Failed to update mode config")),
listConfig: jest
.fn()
@ -1785,8 +1778,7 @@ describe("ClineProvider", () => {
provider.resolveWebviewView(mockWebviewView)
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
// Mock ConfigManager methods
provider.configManager = {
;(provider as any).providerSettingsManager = {
saveConfig: jest.fn().mockResolvedValue(undefined),
listConfig: jest
.fn()
@ -1806,7 +1798,7 @@ describe("ClineProvider", () => {
})
// Verify config was saved
expect(provider.configManager.saveConfig).toHaveBeenCalledWith("test-config", testApiConfig)
expect(provider.providerSettingsManager.saveConfig).toHaveBeenCalledWith("test-config", testApiConfig)
// Verify state updates
expect(mockContext.globalState.update).toHaveBeenCalledWith("listApiConfigMeta", [
@ -1827,9 +1819,7 @@ describe("ClineProvider", () => {
;(buildApiHandler as jest.Mock).mockImplementationOnce(() => {
throw new Error("API handler error")
})
// Mock ConfigManager methods
provider.configManager = {
;(provider as any).providerSettingsManager = {
saveConfig: jest.fn().mockResolvedValue(undefined),
listConfig: jest
.fn()
@ -1870,8 +1860,7 @@ describe("ClineProvider", () => {
provider.resolveWebviewView(mockWebviewView)
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
// Mock ConfigManager methods
provider.configManager = {
;(provider as any).providerSettingsManager = {
saveConfig: jest.fn().mockResolvedValue(undefined),
listConfig: jest
.fn()
@ -1891,7 +1880,7 @@ describe("ClineProvider", () => {
})
// Verify config was saved
expect(provider.configManager.saveConfig).toHaveBeenCalledWith("test-config", testApiConfig)
expect(provider.providerSettingsManager.saveConfig).toHaveBeenCalledWith("test-config", testApiConfig)
// Verify state updates
expect(mockContext.globalState.update).toHaveBeenCalledWith("listApiConfigMeta", [
@ -2105,19 +2094,19 @@ describe("ContextProxy integration", () => {
})
test("updateGlobalState uses contextProxy", async () => {
await provider.updateGlobalState("currentApiConfigName" as GlobalStateKey, "testValue")
await provider.updateGlobalState("currentApiConfigName", "testValue")
expect(mockContextProxy.updateGlobalState).toHaveBeenCalledWith("currentApiConfigName", "testValue")
})
test("getGlobalState uses contextProxy", async () => {
mockContextProxy.getGlobalState.mockResolvedValueOnce("testValue")
const result = await provider.getGlobalState("currentApiConfigName" as GlobalStateKey)
const result = await provider.getGlobalState("currentApiConfigName")
expect(mockContextProxy.getGlobalState).toHaveBeenCalledWith("currentApiConfigName")
expect(result).toBe("testValue")
})
test("storeSecret uses contextProxy", async () => {
await provider.storeSecret("apiKey" as SecretKey, "test-secret")
await provider.storeSecret("apiKey", "test-secret")
expect(mockContextProxy.storeSecret).toHaveBeenCalledWith("apiKey", "test-secret")
})

View file

@ -3,7 +3,7 @@ import * as vscode from "vscode"
import { ClineProvider } from "../core/webview/ClineProvider"
import { RooCodeAPI, RooCodeEvents, ConfigurationValues, TokenUsage } from "./roo-code"
import { RooCodeAPI, RooCodeEvents, TokenUsage, RooCodeSettings } from "./roo-code"
import { MessageHistory } from "./message-history"
export class API extends EventEmitter<RooCodeEvents> implements RooCodeAPI {
@ -78,8 +78,7 @@ export class API extends EventEmitter<RooCodeEvents> implements RooCodeAPI {
await this.provider.postMessageToWebview({ type: "invoke", invoke: "secondaryButtonClick" })
}
// TODO: Change this to `setApiConfiguration`.
public async setConfiguration(values: Partial<ConfigurationValues>) {
public async setConfiguration(values: RooCodeSettings) {
await this.provider.setValues(values)
}

View file

@ -1,3 +1,5 @@
import * as vscode from "vscode"
import { EventEmitter } from "events"
export interface TokenUsage {
@ -150,7 +152,395 @@ export interface ClineMessage {
progressStatus?: ToolProgressStatus
}
export type SecretKey =
export interface ModelInfo {
maxTokens?: number
contextWindow: number
supportsImages?: boolean
supportsComputerUse?: boolean
supportsPromptCache: boolean // This value is hardcoded for now.
inputPrice?: number
outputPrice?: number
cacheWritesPrice?: number
cacheReadsPrice?: number
description?: string
reasoningEffort?: "low" | "medium" | "high"
thinking?: boolean
}
export interface ApiConfigMeta {
id: string
name: string
apiProvider?: ProviderName
}
export type HistoryItem = {
id: string
number: number
ts: number
task: string
tokensIn: number
tokensOut: number
cacheWrites?: number
cacheReads?: number
totalCost: number
size?: number
}
export type ExperimentId =
| "experimentalDiffStrategy"
| "search_and_replace"
| "insert_content"
| "powerSteering"
| "multi_search_and_replace"
export type CheckpointStorage = "task" | "workspace"
export type GroupOptions = {
fileRegex?: string // Regular expression pattern.
description?: string // Human-readable description of the pattern.
}
export type ToolGroup = "read" | "edit" | "browser" | "command" | "mcp" | "modes"
export type GroupEntry = ToolGroup | readonly [ToolGroup, GroupOptions]
export type ModeConfig = {
slug: string
name: string
roleDefinition: string
customInstructions?: string
groups: readonly GroupEntry[] // Now supports both simple strings and tuples with options
source?: "global" | "project" // Where this mode was loaded from
}
export type PromptComponent = {
roleDefinition?: string
customInstructions?: string
}
export type CustomModePrompts = {
[key: string]: PromptComponent | undefined
}
export type CustomSupportPrompts = {
[key: string]: string | undefined
}
export type TelemetrySetting = "unset" | "enabled" | "disabled"
export type Language =
| "ca"
| "de"
| "en"
| "es"
| "fr"
| "hi"
| "it"
| "ja"
| "ko"
| "pl"
| "pt-BR"
| "tr"
| "vi"
| "zh-CN"
| "zh-TW"
/**
* GlobalSettings
*
* These are settings that apply globally.
* They are all stored in the global state.
*/
export interface GlobalSettings {
currentApiConfigName?: string
listApiConfigMeta?: ApiConfigMeta[]
pinnedApiConfigs?: Record<string, boolean>
lastShownAnnouncementId?: string
customInstructions?: string
taskHistory?: HistoryItem[]
autoApprovalEnabled?: boolean
alwaysAllowReadOnly?: boolean
alwaysAllowReadOnlyOutsideWorkspace?: boolean
alwaysAllowWrite?: boolean
alwaysAllowWriteOutsideWorkspace?: boolean
writeDelayMs?: number
alwaysAllowBrowser?: boolean
alwaysApproveResubmit?: boolean
requestDelaySeconds?: number
alwaysAllowMcp?: boolean
alwaysAllowModeSwitch?: boolean
alwaysAllowSubtasks?: boolean
alwaysAllowExecute?: boolean
allowedCommands?: string[]
browserToolEnabled?: boolean
browserViewportSize?: string
screenshotQuality?: number
remoteBrowserEnabled?: boolean
remoteBrowserHost?: string
enableCheckpoints?: boolean
checkpointStorage?: CheckpointStorage
ttsEnabled?: boolean
ttsSpeed?: number
soundEnabled?: boolean
soundVolume?: number
maxOpenTabsContext?: number
maxWorkspaceFiles?: number
showRooIgnoredFiles?: boolean
maxReadFileLine?: number
terminalOutputLineLimit?: number
terminalShellIntegrationTimeout?: number
rateLimitSeconds?: number
diffEnabled?: boolean
fuzzyMatchThreshold?: number
experiments?: Record<ExperimentId, boolean> // Map of experiment IDs to their enabled state.
language?: Language
telemetrySetting?: TelemetrySetting
mcpEnabled?: boolean
enableMcpServerCreation?: boolean
mode?: string
modeApiConfigs?: Record<string, string>
customModes?: ModeConfig[]
customModePrompts?: CustomModePrompts
customSupportPrompts?: CustomSupportPrompts
enhancementApiConfigId?: string
}
export type GlobalSettingsKey = keyof GlobalSettings
/**
* ProviderSettings
*
* These are settings that apply on a per-provider basis.
* Non-sensitive values are stored in the global state.
* Sensitive values are stored in VSCode secrets.
*/
/**
* DiscriminatedProviderSettings
*
* NOTE: This is actually how our provider settings should be typed, but it
* will take a little elbow grease to move to this shape. For now we're just
* using it to generate the `ProviderName`.
*/
export type DiscriminatedProviderSettings =
| {
apiProvider: "anthropic"
apiKey?: string
anthropicBaseUrl?: string
apiModelId?: string
}
| {
apiProvider: "glama"
glamaApiKey?: string
glamaModelId?: string
}
| {
apiProvider: "openrouter"
openRouterApiKey?: string
openRouterModelId?: string
openRouterBaseUrl?: string
openRouterSpecificProvider?: string
openRouterUseMiddleOutTransform?: boolean
}
| {
apiProvider: "bedrock"
awsAccessKey?: string
awsSecretKey?: string
awsSessionToken?: string
awsRegion?: string
awsUseCrossRegionInference?: boolean
awsUsePromptCache?: boolean
awspromptCacheId?: string
awsProfile?: string
awsUseProfile?: boolean
awsCustomArn?: string
}
| {
apiProvider: "vertex"
vertexKeyFile?: string
vertexJsonCredentials?: string
vertexProjectId?: string
vertexRegion?: string
}
| {
apiProvider: "openai"
openAiApiKey?: string
openAiBaseUrl?: string
openAiR1FormatEnabled?: boolean
openAiModelId?: string
openAiUseAzure?: boolean
azureApiVersion?: string
openAiStreamingEnabled?: boolean
}
| {
apiProvider: "ollama"
ollamaModelId?: string
ollamaBaseUrl?: string
}
| {
apiProvider: "vscode-lm"
vsCodeLmModelSelector?: vscode.LanguageModelChatSelector
}
| {
apiProvider: "lmstudio"
lmStudioModelId?: string
lmStudioBaseUrl?: string
lmStudioDraftModelId?: string
lmStudioSpeculativeDecodingEnabled?: boolean
}
| {
apiProvider: "gemini"
googleGeminiBaseUrl?: string
}
| {
apiProvider: "openai-native"
openAiNativeApiKey?: string
}
| {
apiProvider: "mistral"
mistralApiKey?: string
mistralCodestralUrl?: string
}
| {
apiProvider: "deepseek"
deepSeekApiKey?: string
deepSeekBaseUrl?: string
}
| {
apiProvider: "unbound"
unboundApiKey?: string
unboundModelId?: string
}
| {
apiProvider: "requesty"
requestyApiKey?: string
requestyModelId?: string
}
| {
apiProvider: "human-relay"
}
| {
apiProvider: "fake-ai"
fakeAi?: unknown
}
export type ProviderName = DiscriminatedProviderSettings["apiProvider"]
export interface ProviderSettings {
apiProvider?: ProviderName
apiModelId?: string
// Anthropic
apiKey?: string // secret
anthropicBaseUrl?: string
// Glama
glamaApiKey?: string // secret
glamaModelId?: string
glamaModelInfo?: ModelInfo
// OpenRouter
openRouterApiKey?: string // secret
openRouterModelId?: string
openRouterModelInfo?: ModelInfo
openRouterBaseUrl?: string
openRouterSpecificProvider?: string
openRouterUseMiddleOutTransform?: boolean
// AWS Bedrock
awsAccessKey?: string // secret
awsSecretKey?: string // secret
awsSessionToken?: string // secret
awsRegion?: string
awsUseCrossRegionInference?: boolean
awsUsePromptCache?: boolean
awspromptCacheId?: string
awsProfile?: string
awsUseProfile?: boolean
awsCustomArn?: string
// Google Vertex
vertexKeyFile?: string
vertexJsonCredentials?: string
vertexProjectId?: string
vertexRegion?: string
// OpenAI
openAiApiKey?: string // secret
openAiBaseUrl?: string
openAiR1FormatEnabled?: boolean
openAiModelId?: string
openAiCustomModelInfo?: ModelInfo
openAiUseAzure?: boolean
azureApiVersion?: string
openAiStreamingEnabled?: boolean
// Ollama
ollamaModelId?: string
ollamaBaseUrl?: string
// VS Code LM
vsCodeLmModelSelector?: vscode.LanguageModelChatSelector
// LM Studio
lmStudioModelId?: string
lmStudioBaseUrl?: string
lmStudioDraftModelId?: string
lmStudioSpeculativeDecodingEnabled?: boolean
// Gemini
geminiApiKey?: string // secret
googleGeminiBaseUrl?: string
// OpenAI Native
openAiNativeApiKey?: string // secret
// Mistral
mistralApiKey?: string // secret
mistralCodestralUrl?: string // New option for Codestral URL.
// DeepSeek
deepSeekApiKey?: string // secret
deepSeekBaseUrl?: string
// Unbound
unboundApiKey?: string // secret
unboundModelId?: string
unboundModelInfo?: ModelInfo
// Requesty
requestyApiKey?: string
requestyModelId?: string
requestyModelInfo?: ModelInfo
// Claude 3.7 Sonnet Thinking
modelTemperature?: number | null
modelMaxTokens?: number
modelMaxThinkingTokens?: number
// Generic (For now though, OpenAI, DeekSeek, Mistral, and Requesty make reference to it.)
includeMaxTokens?: boolean
// Fake AI
fakeAi?: unknown
}
export type ProviderSettingsKey = keyof ProviderSettings
/**
* RooCodeSettings
*
* All settings, irrespective of scope and storage.
*/
export type RooCodeSettings = GlobalSettings & ProviderSettings
export type RooCodeSettingsKey = keyof RooCodeSettings
/**
* SecretState
*
* All settings that are stored in VSCode secrets.
*/
export type SecretState = Pick<
RooCodeSettings,
| "apiKey"
| "glamaApiKey"
| "openRouterApiKey"
@ -164,103 +554,16 @@ export type SecretKey =
| "mistralApiKey"
| "unboundApiKey"
| "requestyApiKey"
>
export type GlobalStateKey =
| "apiProvider"
| "apiModelId"
| "glamaModelId"
| "glamaModelInfo"
| "awsRegion"
| "awsUseCrossRegionInference"
| "awsProfile"
| "awsUseProfile"
| "awsCustomArn"
| "vertexKeyFile"
| "vertexJsonCredentials"
| "vertexProjectId"
| "vertexRegion"
| "lastShownAnnouncementId"
| "customInstructions"
| "alwaysAllowReadOnly"
| "alwaysAllowReadOnlyOutsideWorkspace"
| "alwaysAllowWrite"
| "alwaysAllowWriteOutsideWorkspace"
| "alwaysAllowExecute"
| "alwaysAllowBrowser"
| "alwaysAllowMcp"
| "alwaysAllowModeSwitch"
| "alwaysAllowSubtasks"
| "taskHistory"
| "openAiBaseUrl"
| "openAiModelId"
| "openAiCustomModelInfo"
| "openAiUseAzure"
| "ollamaModelId"
| "ollamaBaseUrl"
| "lmStudioModelId"
| "lmStudioBaseUrl"
| "anthropicBaseUrl"
| "modelMaxThinkingTokens"
| "azureApiVersion"
| "openAiStreamingEnabled"
| "openAiR1FormatEnabled"
| "openRouterModelId"
| "openRouterModelInfo"
| "openRouterBaseUrl"
| "openRouterSpecificProvider"
| "openRouterUseMiddleOutTransform"
| "googleGeminiBaseUrl"
| "allowedCommands"
| "ttsEnabled"
| "ttsSpeed"
| "soundEnabled"
| "soundVolume"
| "diffEnabled"
| "enableCheckpoints"
| "checkpointStorage"
| "browserViewportSize"
| "screenshotQuality"
| "remoteBrowserHost"
| "fuzzyMatchThreshold"
| "writeDelayMs"
| "terminalOutputLineLimit"
| "terminalShellIntegrationTimeout"
| "mcpEnabled"
| "enableMcpServerCreation"
| "alwaysApproveResubmit"
| "requestDelaySeconds"
| "rateLimitSeconds"
| "currentApiConfigName"
| "listApiConfigMeta"
| "vsCodeLmModelSelector"
| "mode"
| "modeApiConfigs"
| "customModePrompts"
| "customSupportPrompts"
| "enhancementApiConfigId"
| "experiments" // Map of experiment IDs to their enabled state
| "autoApprovalEnabled"
| "customModes" // Array of custom modes
| "unboundModelId"
| "requestyModelId"
| "requestyModelInfo"
| "unboundModelInfo"
| "modelTemperature"
| "modelMaxTokens"
| "mistralCodestralUrl"
| "maxOpenTabsContext"
| "maxWorkspaceFiles"
| "browserToolEnabled"
| "lmStudioSpeculativeDecodingEnabled"
| "lmStudioDraftModelId"
| "telemetrySetting"
| "showRooIgnoredFiles"
| "remoteBrowserEnabled"
| "language"
| "maxReadFileLine"
| "fakeAi"
| "pinnedApiConfigs" // Record of API config names that should be pinned to the top of the API provides dropdown
export type SecretStateKey = keyof SecretState
export type ConfigurationKey = GlobalStateKey | SecretKey
/**
* GlobalState
*
* All settings that are stored in the global state.
*/
export type ConfigurationValues = Record<ConfigurationKey, any>
export type GlobalState = Omit<RooCodeSettings, SecretStateKey>
export type GlobalStateKey = keyof GlobalState

View file

@ -69,7 +69,8 @@
"mcp_server_deleted": "Servidor MCP eliminat: {{serverName}}",
"mcp_server_not_found": "Servidor \"{{serverName}}\" no trobat a la configuració",
"custom_storage_path_set": "Ruta d'emmagatzematge personalitzada establerta: {{path}}",
"default_storage_path": "S'ha reprès l'ús de la ruta d'emmagatzematge predeterminada"
"default_storage_path": "S'ha reprès l'ús de la ruta d'emmagatzematge predeterminada",
"settings_imported": "Configuració importada correctament."
},
"answers": {
"yes": "Sí",

View file

@ -65,7 +65,8 @@
"mcp_server_deleted": "MCP-Server gelöscht: {{serverName}}",
"mcp_server_not_found": "Server \"{{serverName}}\" nicht in der Konfiguration gefunden",
"custom_storage_path_set": "Benutzerdefinierter Speicherpfad festgelegt: {{path}}",
"default_storage_path": "Auf Standardspeicherpfad zurückgesetzt"
"default_storage_path": "Auf Standardspeicherpfad zurückgesetzt",
"settings_imported": "Einstellungen erfolgreich importiert."
},
"answers": {
"yes": "Ja",

View file

@ -65,7 +65,8 @@
"mcp_server_deleted": "Deleted MCP server: {{serverName}}",
"mcp_server_not_found": "Server \"{{serverName}}\" not found in configuration",
"custom_storage_path_set": "Custom storage path set: {{path}}",
"default_storage_path": "Reverted to using default storage path"
"default_storage_path": "Reverted to using default storage path",
"settings_imported": "Settings imported successfully."
},
"answers": {
"yes": "Yes",

View file

@ -65,7 +65,8 @@
"mcp_server_deleted": "Servidor MCP eliminado: {{serverName}}",
"mcp_server_not_found": "Servidor \"{{serverName}}\" no encontrado en la configuración",
"custom_storage_path_set": "Ruta de almacenamiento personalizada establecida: {{path}}",
"default_storage_path": "Se ha vuelto a usar la ruta de almacenamiento predeterminada"
"default_storage_path": "Se ha vuelto a usar la ruta de almacenamiento predeterminada",
"settings_imported": "Configuración importada correctamente."
},
"answers": {
"yes": "Sí",

View file

@ -65,7 +65,8 @@
"mcp_server_deleted": "Serveur MCP supprimé : {{serverName}}",
"mcp_server_not_found": "Serveur \"{{serverName}}\" introuvable dans la configuration",
"custom_storage_path_set": "Chemin de stockage personnalisé défini : {{path}}",
"default_storage_path": "Retour au chemin de stockage par défaut"
"default_storage_path": "Retour au chemin de stockage par défaut",
"settings_imported": "Paramètres importés avec succès."
},
"answers": {
"yes": "Oui",

View file

@ -65,7 +65,8 @@
"mcp_server_deleted": "MCP सर्वर हटाया गया: {{serverName}}",
"mcp_server_not_found": "सर्वर \"{{serverName}}\" कॉन्फ़िगरेशन में नहीं मिला",
"custom_storage_path_set": "कस्टम स्टोरेज पाथ सेट किया गया: {{path}}",
"default_storage_path": "डिफ़ॉल्ट स्टोरेज पाथ का उपयोग पुनः शुरू किया गया"
"default_storage_path": "डिफ़ॉल्ट स्टोरेज पाथ का उपयोग पुनः शुरू किया गया",
"settings_imported": "सेटिंग्स सफलतापूर्वक इम्पोर्ट की गईं।"
},
"answers": {
"yes": "हां",

View file

@ -65,7 +65,8 @@
"mcp_server_deleted": "Server MCP eliminato: {{serverName}}",
"mcp_server_not_found": "Server \"{{serverName}}\" non trovato nella configurazione",
"custom_storage_path_set": "Percorso di archiviazione personalizzato impostato: {{path}}",
"default_storage_path": "Tornato al percorso di archiviazione predefinito"
"default_storage_path": "Tornato al percorso di archiviazione predefinito",
"settings_imported": "Impostazioni importate con successo."
},
"answers": {
"yes": "Sì",

View file

@ -65,7 +65,8 @@
"mcp_server_deleted": "MCPサーバーが削除されました{{serverName}}",
"mcp_server_not_found": "サーバー\"{{serverName}}\"が設定内に見つかりません",
"custom_storage_path_set": "カスタムストレージパスが設定されました:{{path}}",
"default_storage_path": "デフォルトのストレージパスに戻りました"
"default_storage_path": "デフォルトのストレージパスに戻りました",
"settings_imported": "設定が正常にインポートされました。"
},
"answers": {
"yes": "はい",

View file

@ -65,7 +65,8 @@
"mcp_server_deleted": "MCP 서버 삭제됨: {{serverName}}",
"mcp_server_not_found": "구성에서 서버 \"{{serverName}}\"을(를) 찾을 수 없습니다",
"custom_storage_path_set": "사용자 지정 저장 경로 설정됨: {{path}}",
"default_storage_path": "기본 저장 경로로 되돌아갔습니다"
"default_storage_path": "기본 저장 경로로 되돌아갔습니다",
"settings_imported": "설정이 성공적으로 가져와졌습니다."
},
"answers": {
"yes": "예",

View file

@ -65,7 +65,8 @@
"mcp_server_deleted": "Usunięto serwer MCP: {{serverName}}",
"mcp_server_not_found": "Serwer \"{{serverName}}\" nie znaleziony w konfiguracji",
"custom_storage_path_set": "Ustawiono niestandardową ścieżkę przechowywania: {{path}}",
"default_storage_path": "Wznowiono używanie domyślnej ścieżki przechowywania"
"default_storage_path": "Wznowiono używanie domyślnej ścieżki przechowywania",
"settings_imported": "Ustawienia zaimportowane pomyślnie."
},
"answers": {
"yes": "Tak",

View file

@ -69,7 +69,8 @@
"mcp_server_deleted": "Servidor MCP excluído: {{serverName}}",
"mcp_server_not_found": "Servidor \"{{serverName}}\" não encontrado na configuração",
"custom_storage_path_set": "Caminho de armazenamento personalizado definido: {{path}}",
"default_storage_path": "Retornado ao caminho de armazenamento padrão"
"default_storage_path": "Retornado ao caminho de armazenamento padrão",
"settings_imported": "Configurações importadas com sucesso."
},
"answers": {
"yes": "Sim",

View file

@ -65,7 +65,8 @@
"mcp_server_deleted": "MCP sunucusu silindi: {{serverName}}",
"mcp_server_not_found": "Yapılandırmada \"{{serverName}}\" sunucusu bulunamadı",
"custom_storage_path_set": "Özel depolama yolu ayarlandı: {{path}}",
"default_storage_path": "Varsayılan depolama yoluna geri dönüldü"
"default_storage_path": "Varsayılan depolama yoluna geri dönüldü",
"settings_imported": "Ayarlar başarıyla içe aktarıldı."
},
"answers": {
"yes": "Evet",

View file

@ -65,7 +65,8 @@
"mcp_server_deleted": "Đã xóa máy chủ MCP: {{serverName}}",
"mcp_server_not_found": "Không tìm thấy máy chủ \"{{serverName}}\" trong cấu hình",
"custom_storage_path_set": "Đã thiết lập đường dẫn lưu trữ tùy chỉnh: {{path}}",
"default_storage_path": "Đã quay lại sử dụng đường dẫn lưu trữ mặc định"
"default_storage_path": "Đã quay lại sử dụng đường dẫn lưu trữ mặc định",
"settings_imported": "Cài đặt đã được nhập thành công."
},
"answers": {
"yes": "Có",

View file

@ -65,7 +65,8 @@
"mcp_server_deleted": "已删除MCP服务器{{serverName}}",
"mcp_server_not_found": "在配置中未找到服务器\"{{serverName}}\"",
"custom_storage_path_set": "自定义存储路径已设置:{{path}}",
"default_storage_path": "已恢复使用默认存储路径"
"default_storage_path": "已恢复使用默认存储路径",
"settings_imported": "设置已成功导入。"
},
"answers": {
"yes": "是",

View file

@ -65,7 +65,8 @@
"mcp_server_deleted": "已刪除MCP服務器{{serverName}}",
"mcp_server_not_found": "在配置中未找到服務器\"{{serverName}}\"",
"custom_storage_path_set": "自定義存儲路徑已設置:{{path}}",
"default_storage_path": "已恢復使用默認存儲路徑"
"default_storage_path": "已恢復使用默認存儲路徑",
"settings_imported": "設置已成功導入。"
},
"answers": {
"yes": "是",

View file

@ -1,13 +1,14 @@
import { ApiConfiguration, ApiProvider, ModelInfo } from "./api"
import { ApiConfiguration, ModelInfo } from "./api"
import { HistoryItem } from "./HistoryItem"
import { McpServer } from "./mcp"
import { GitCommit } from "../utils/git"
import { Mode, CustomModePrompts, ModeConfig } from "./modes"
import { CustomSupportPrompts } from "./support-prompt"
import { Mode, ModeConfig } from "./modes"
import { ExperimentId } from "./experiments"
import { CheckpointStorage } from "./checkpoints"
import { TelemetrySetting } from "./TelemetrySetting"
import type { ClineMessage, ClineAsk, ClineSay } from "../exports/roo-code"
import type { GlobalSettings, ApiConfigMeta, ClineMessage, ClineAsk, ClineSay } from "../exports/roo-code"
export type { ApiConfigMeta }
export interface LanguageModelChatSelector {
vendor?: string
@ -103,74 +104,96 @@ export interface ExtensionMessage {
error?: string
}
export interface ApiConfigMeta {
id: string
name: string
apiProvider?: ApiProvider
}
export interface ExtensionState {
export type ExtensionState = Pick<
GlobalSettings,
| "currentApiConfigName"
| "listApiConfigMeta"
| "pinnedApiConfigs"
// | "lastShownAnnouncementId"
| "customInstructions"
// | "taskHistory" // Optional in GlobalSettings, required here.
| "autoApprovalEnabled"
| "alwaysAllowReadOnly"
| "alwaysAllowReadOnlyOutsideWorkspace"
| "alwaysAllowWrite"
| "alwaysAllowWriteOutsideWorkspace"
// | "writeDelayMs" // Optional in GlobalSettings, required here.
| "alwaysAllowBrowser"
| "alwaysApproveResubmit"
// | "requestDelaySeconds" // Optional in GlobalSettings, required here.
| "alwaysAllowMcp"
| "alwaysAllowModeSwitch"
| "alwaysAllowSubtasks"
| "alwaysAllowExecute"
| "allowedCommands"
| "browserToolEnabled"
| "browserViewportSize"
| "screenshotQuality"
| "remoteBrowserEnabled"
| "remoteBrowserHost"
// | "enableCheckpoints" // Optional in GlobalSettings, required here.
// | "checkpointStorage" // Optional in GlobalSettings, required here.
| "ttsEnabled"
| "ttsSpeed"
| "soundEnabled"
| "soundVolume"
// | "maxOpenTabsContext" // Optional in GlobalSettings, required here.
// | "maxWorkspaceFiles" // Optional in GlobalSettings, required here.
// | "showRooIgnoredFiles" // Optional in GlobalSettings, required here.
// | "maxReadFileLine" // Optional in GlobalSettings, required here.
| "terminalOutputLineLimit"
| "terminalShellIntegrationTimeout"
// | "rateLimitSeconds" // Optional in GlobalSettings, required here.
| "diffEnabled"
| "fuzzyMatchThreshold"
// | "experiments" // Optional in GlobalSettings, required here.
| "language"
// | "telemetrySetting" // Optional in GlobalSettings, required here.
// | "mcpEnabled" // Optional in GlobalSettings, required here.
// | "enableMcpServerCreation" // Optional in GlobalSettings, required here.
// | "mode" // Optional in GlobalSettings, required here.
| "modeApiConfigs"
// | "customModes" // Optional in GlobalSettings, required here.
| "customModePrompts"
| "customSupportPrompts"
| "enhancementApiConfigId"
> & {
version: string
clineMessages: ClineMessage[]
taskHistory: HistoryItem[]
shouldShowAnnouncement: boolean
apiConfiguration?: ApiConfiguration
currentApiConfigName?: string
listApiConfigMeta?: ApiConfigMeta[]
customInstructions?: string
customModePrompts?: CustomModePrompts
customSupportPrompts?: CustomSupportPrompts
alwaysAllowReadOnly?: boolean
alwaysAllowReadOnlyOutsideWorkspace?: boolean
alwaysAllowWrite?: boolean
alwaysAllowWriteOutsideWorkspace?: boolean
alwaysAllowExecute?: boolean
alwaysAllowBrowser?: boolean
alwaysAllowMcp?: boolean
alwaysApproveResubmit?: boolean
alwaysAllowModeSwitch?: boolean
alwaysAllowSubtasks?: boolean
browserToolEnabled?: boolean
requestDelaySeconds: number
rateLimitSeconds: number // Minimum time between successive requests (0 = disabled)
uriScheme?: string
currentTaskItem?: HistoryItem
allowedCommands?: string[]
soundEnabled?: boolean
ttsEnabled?: boolean
ttsSpeed?: number
soundVolume?: number
diffEnabled?: boolean
apiConfiguration?: ApiConfiguration
uriScheme?: string
shouldShowAnnouncement: boolean
taskHistory: HistoryItem[]
writeDelayMs: number
requestDelaySeconds: number
enableCheckpoints: boolean
checkpointStorage: CheckpointStorage
browserViewportSize?: string
screenshotQuality?: number
remoteBrowserHost?: string
remoteBrowserEnabled?: boolean
fuzzyMatchThreshold?: number
language?: string
writeDelayMs: number
terminalOutputLineLimit?: number
terminalShellIntegrationTimeout?: number
mcpEnabled: boolean
enableMcpServerCreation: boolean
mode: Mode
modeApiConfigs?: Record<Mode, string>
enhancementApiConfigId?: string
experiments: Record<ExperimentId, boolean> // Map of experiment IDs to their enabled state
autoApprovalEnabled?: boolean
customModes: ModeConfig[]
toolRequirements?: Record<string, boolean> // Map of tool names to their requirements (e.g. {"apply_diff": true} if diffEnabled)
maxOpenTabsContext: number // Maximum number of VSCode open tabs to include in context (0-500)
maxWorkspaceFiles: number // Maximum number of files to include in current working directory details (0-500)
showRooIgnoredFiles: boolean // Whether to show .rooignore'd files in listings
maxReadFileLine: number // Maximum number of lines to read from a file before truncating
rateLimitSeconds: number // Minimum time between successive requests (0 = disabled).
experiments: Record<ExperimentId, boolean> // Map of experiment IDs to their enabled state
mcpEnabled: boolean
enableMcpServerCreation: boolean
mode: Mode
customModes: ModeConfig[]
toolRequirements?: Record<string, boolean> // Map of tool names to their requirements (e.g. {"apply_diff": true} if diffEnabled)
cwd?: string // Current working directory
telemetrySetting: TelemetrySetting
telemetryKey?: string
machineId?: string
showRooIgnoredFiles: boolean // Whether to show .rooignore'd files in listings
renderContext: "sidebar" | "editor"
pinnedApiConfigs?: Record<string, boolean> // Map of API config names to pinned state
maxReadFileLine: number // Maximum number of lines to read from a file before truncating
settingsImportedAt?: number
}
export type { ClineMessage, ClineAsk, ClineSay }

View file

@ -1,12 +1,3 @@
export type HistoryItem = {
id: string
number: number
ts: number
task: string
tokensIn: number
tokensOut: number
cacheWrites?: number
cacheReads?: number
totalCost: number
size?: number
}
import type { HistoryItem } from "../exports/roo-code"
export type { HistoryItem }

View file

@ -37,6 +37,8 @@ export interface WebviewMessage {
| "showTaskWithId"
| "deleteTaskWithId"
| "exportTaskWithId"
| "importSettings"
| "exportSettings"
| "resetState"
| "requestOllamaModels"
| "requestLmStudioModels"

View file

@ -1,10 +1,11 @@
// npx jest src/shared/__tests__/language.test.ts
import { formatLanguage } from "../language"
describe("formatLanguage", () => {
it("should uppercase region code in locale string", () => {
expect(formatLanguage("en-us")).toBe("en-US")
expect(formatLanguage("fr-ca")).toBe("fr-CA")
expect(formatLanguage("de-de")).toBe("de-DE")
expect(formatLanguage("pt-br")).toBe("pt-BR")
expect(formatLanguage("zh-cn")).toBe("zh-CN")
})
it("should return original string if no region code present", () => {

View file

@ -366,7 +366,7 @@ describe("FileRestrictionError", () => {
})
it("applies custom mode overrides", async () => {
const customModes = [
const customModes: ModeConfig[] = [
{
slug: "debug",
name: "Custom Debug",

View file

@ -1,162 +1,10 @@
import * as vscode from "vscode"
import { ModelInfo, ProviderName, ProviderSettings } from "../exports/roo-code"
export type ApiProvider =
| "anthropic"
| "glama"
| "openrouter"
| "bedrock"
| "vertex"
| "openai"
| "ollama"
| "lmstudio"
| "gemini"
| "openai-native"
| "deepseek"
| "vscode-lm"
| "mistral"
| "unbound"
| "requesty"
| "human-relay"
| "fake-ai"
export type { ModelInfo, ProviderName as ApiProvider }
export interface ApiHandlerOptions {
apiModelId?: string
apiKey?: string // anthropic
anthropicBaseUrl?: string
vsCodeLmModelSelector?: vscode.LanguageModelChatSelector
glamaModelId?: string
glamaModelInfo?: ModelInfo
glamaApiKey?: string
openRouterApiKey?: string
openRouterModelId?: string
openRouterModelInfo?: ModelInfo
openRouterBaseUrl?: string
openRouterSpecificProvider?: string
awsAccessKey?: string
awsSecretKey?: string
awsSessionToken?: string
awsRegion?: string
awsUseCrossRegionInference?: boolean
awsUsePromptCache?: boolean
awspromptCacheId?: string
awsProfile?: string
awsUseProfile?: boolean
awsCustomArn?: string
vertexKeyFile?: string
vertexJsonCredentials?: string
vertexProjectId?: string
vertexRegion?: string
openAiBaseUrl?: string
openAiApiKey?: string
openAiR1FormatEnabled?: boolean
openAiModelId?: string
openAiCustomModelInfo?: ModelInfo
openAiUseAzure?: boolean
ollamaModelId?: string
ollamaBaseUrl?: string
lmStudioModelId?: string
lmStudioBaseUrl?: string
lmStudioDraftModelId?: string
lmStudioSpeculativeDecodingEnabled?: boolean
geminiApiKey?: string
googleGeminiBaseUrl?: string
openAiNativeApiKey?: string
mistralApiKey?: string
mistralCodestralUrl?: string // New option for Codestral URL
azureApiVersion?: string
openRouterUseMiddleOutTransform?: boolean
openAiStreamingEnabled?: boolean
deepSeekBaseUrl?: string
deepSeekApiKey?: string
includeMaxTokens?: boolean
unboundApiKey?: string
unboundModelId?: string
unboundModelInfo?: ModelInfo
requestyApiKey?: string
requestyModelId?: string
requestyModelInfo?: ModelInfo
modelTemperature?: number | null
modelMaxTokens?: number
modelMaxThinkingTokens?: number
fakeAi?: unknown
}
export type ApiHandlerOptions = Omit<ProviderSettings, "apiProvider" | "id">
export type ApiConfiguration = ApiHandlerOptions & {
apiProvider?: ApiProvider
id?: string // stable unique identifier
}
// Import GlobalStateKey type from globalState.ts
import { GlobalStateKey } from "./globalState"
// Define API configuration keys for dynamic object building.
// TODO: This needs actual type safety; a type error should be thrown if
// this is not an exhaustive list of all `GlobalStateKey` values.
export const API_CONFIG_KEYS: GlobalStateKey[] = [
"apiModelId",
"anthropicBaseUrl",
"vsCodeLmModelSelector",
"glamaModelId",
"glamaModelInfo",
"openRouterModelId",
"openRouterModelInfo",
"openRouterBaseUrl",
"openRouterSpecificProvider",
"awsRegion",
"awsUseCrossRegionInference",
// "awsUsePromptCache", // NOT exist on GlobalStateKey
// "awspromptCacheId", // NOT exist on GlobalStateKey
"awsProfile",
"awsUseProfile",
"awsCustomArn",
"vertexKeyFile",
"vertexJsonCredentials",
"vertexProjectId",
"vertexRegion",
"openAiBaseUrl",
"openAiModelId",
"openAiCustomModelInfo",
"openAiUseAzure",
"ollamaModelId",
"ollamaBaseUrl",
"lmStudioModelId",
"lmStudioBaseUrl",
"lmStudioDraftModelId",
"lmStudioSpeculativeDecodingEnabled",
"googleGeminiBaseUrl",
"mistralCodestralUrl",
"azureApiVersion",
"openRouterUseMiddleOutTransform",
"openAiStreamingEnabled",
"openAiR1FormatEnabled",
// "deepSeekBaseUrl", // not exist on GlobalStateKey
// "includeMaxTokens", // not exist on GlobalStateKey
"unboundModelId",
"unboundModelInfo",
"requestyModelId",
"requestyModelInfo",
"modelTemperature",
"modelMaxTokens",
"modelMaxThinkingTokens",
"fakeAi",
]
// Models
export interface ModelInfo {
maxTokens?: number
contextWindow: number
supportsImages?: boolean
supportsComputerUse?: boolean
supportsPromptCache: boolean // this value is hardcoded for now
inputPrice?: number
outputPrice?: number
cacheWritesPrice?: number
cacheReadsPrice?: number
description?: string
reasoningEffort?: "low" | "medium" | "high"
thinking?: boolean
}
export type ApiConfiguration = ProviderSettings
// Anthropic
// https://docs.anthropic.com/en/docs/about-claude/models

View file

@ -1,16 +1,18 @@
import { ApiConfiguration } from "../shared/api"
import { SECRET_KEYS } from "./globalState"
import { ProviderSettings } from "../exports/roo-code"
import { SECRET_STATE_KEYS } from "./globalState"
export function checkExistKey(config: ApiConfiguration | undefined) {
if (!config) return false
export function checkExistKey(config: ProviderSettings | undefined) {
if (!config) {
return false
}
// Special case for human-relay and fake-ai providers which don't need any configuration
// Special case for human-relay and fake-ai providers which don't need any configuration.
if (config.apiProvider === "human-relay" || config.apiProvider === "fake-ai") {
return true
}
// Check all secret keys from the centralized SECRET_KEYS array
const hasSecretKey = SECRET_KEYS.some((key) => config[key as keyof ApiConfiguration] !== undefined)
// Check all secret keys from the centralized SECRET_STATE_KEYS array.
const hasSecretKey = SECRET_STATE_KEYS.some((key) => config[key] !== undefined)
// Check additional non-secret configuration properties
const hasOtherConfig = [

View file

@ -1,4 +1,6 @@
export type CheckpointStorage = "task" | "workspace"
import { CheckpointStorage } from "../exports/roo-code"
export type { CheckpointStorage }
export const isCheckpointStorage = (value: string): value is CheckpointStorage => {
return value === "task" || value === "workspace"

View file

@ -1,36 +1,31 @@
import { ExperimentId } from "../exports/roo-code"
import { AssertEqual, Equals, Keys, Values } from "../utils/type-fu"
export type { ExperimentId }
export const EXPERIMENT_IDS = {
DIFF_STRATEGY: "experimentalDiffStrategy",
SEARCH_AND_REPLACE: "search_and_replace",
INSERT_BLOCK: "insert_content",
POWER_STEERING: "powerSteering",
MULTI_SEARCH_AND_REPLACE: "multi_search_and_replace",
} as const
} as const satisfies Record<string, ExperimentId>
export type ExperimentKey = keyof typeof EXPERIMENT_IDS
export type ExperimentId = valueof<typeof EXPERIMENT_IDS>
type _AssertExperimentIds = AssertEqual<Equals<ExperimentId, Values<typeof EXPERIMENT_IDS>>>
export interface ExperimentConfig {
type ExperimentKey = Keys<typeof EXPERIMENT_IDS>
interface ExperimentConfig {
enabled: boolean
}
type valueof<X> = X[keyof X]
export const experimentConfigsMap: Record<ExperimentKey, ExperimentConfig> = {
DIFF_STRATEGY: {
enabled: false,
},
SEARCH_AND_REPLACE: {
enabled: false,
},
INSERT_BLOCK: {
enabled: false,
},
POWER_STEERING: {
enabled: false,
},
MULTI_SEARCH_AND_REPLACE: {
enabled: false,
},
DIFF_STRATEGY: { enabled: false },
SEARCH_AND_REPLACE: { enabled: false },
INSERT_BLOCK: { enabled: false },
POWER_STEERING: { enabled: false },
MULTI_SEARCH_AND_REPLACE: { enabled: false },
}
export const experimentDefault = Object.fromEntries(
@ -41,12 +36,7 @@ export const experimentDefault = Object.fromEntries(
) as Record<ExperimentId, boolean>
export const experiments = {
get: (id: ExperimentKey): ExperimentConfig | undefined => {
return experimentConfigsMap[id]
},
isEnabled: (experimentsConfig: Record<ExperimentId, boolean>, id: ExperimentId): boolean => {
return experimentsConfig[id] ?? experimentDefault[id]
},
get: (id: ExperimentKey): ExperimentConfig | undefined => experimentConfigsMap[id],
isEnabled: (experimentsConfig: Record<ExperimentId, boolean>, id: ExperimentId) =>
experimentsConfig[id] ?? experimentDefault[id],
} as const
// No longer needed as we use translation keys directly in the UI

View file

@ -1,145 +1,695 @@
import type { SecretKey, GlobalStateKey, ConfigurationKey, ConfigurationValues } from "../exports/roo-code"
import { z } from "zod"
export type { SecretKey, GlobalStateKey, ConfigurationKey, ConfigurationValues }
import type {
ProviderName,
CheckpointStorage,
ToolGroup,
Language,
TelemetrySetting,
ProviderSettingsKey,
SecretStateKey,
GlobalStateKey,
ModelInfo,
ApiConfigMeta,
HistoryItem,
GroupEntry,
ModeConfig,
ExperimentId,
ProviderSettings,
GlobalSettings,
} from "../exports/roo-code"
import { Keys, AssertEqual, Equals } from "../utils/type-fu"
/**
* For convenience we'd like the `RooCodeAPI` to define `SecretKey` and `GlobalStateKey`,
* but since it is a type definition file we can't export constants without some
* annoyances. In order to achieve proper type safety without using constants as
* in the type definition we use this clever Check<>Exhaustiveness pattern.
* If you extend the `SecretKey` or `GlobalStateKey` types, you will need to
* update the `SECRET_KEYS` and `GLOBAL_STATE_KEYS` arrays to include the new
* keys or a type error will be thrown.
* ProviderName
*/
export const SECRET_KEYS = [
"apiKey",
"glamaApiKey",
"openRouterApiKey",
"awsAccessKey",
"awsSecretKey",
"awsSessionToken",
"openAiApiKey",
"geminiApiKey",
"openAiNativeApiKey",
"deepSeekApiKey",
"mistralApiKey",
"unboundApiKey",
"requestyApiKey",
] as const
const providerNames: Record<ProviderName, true> = {
anthropic: true,
glama: true,
openrouter: true,
bedrock: true,
vertex: true,
openai: true,
ollama: true,
lmstudio: true,
gemini: true,
"openai-native": true,
deepseek: true,
"vscode-lm": true,
mistral: true,
unbound: true,
requesty: true,
"human-relay": true,
"fake-ai": true,
}
type CheckSecretKeysExhaustiveness = Exclude<SecretKey, (typeof SECRET_KEYS)[number]> extends never ? true : false
const PROVIDER_NAMES = Object.keys(providerNames) as ProviderName[]
const _checkSecretKeysExhaustiveness: CheckSecretKeysExhaustiveness = true
const providerNamesEnum: [ProviderName, ...ProviderName[]] = [
PROVIDER_NAMES[0],
...PROVIDER_NAMES.slice(1).map((p) => p),
]
export const GLOBAL_STATE_KEYS = [
"apiProvider",
"apiModelId",
"glamaModelId",
"glamaModelInfo",
"awsRegion",
"awsUseCrossRegionInference",
"awsProfile",
"awsUseProfile",
"awsCustomArn",
"vertexKeyFile",
"vertexJsonCredentials",
"vertexProjectId",
"vertexRegion",
"lastShownAnnouncementId",
"customInstructions",
"alwaysAllowReadOnly",
"alwaysAllowReadOnlyOutsideWorkspace",
"alwaysAllowWrite",
"alwaysAllowWriteOutsideWorkspace",
"alwaysAllowExecute",
"alwaysAllowBrowser",
"alwaysAllowMcp",
"alwaysAllowModeSwitch",
"alwaysAllowSubtasks",
"taskHistory",
"openAiBaseUrl",
"openAiModelId",
"openAiCustomModelInfo",
"openAiUseAzure",
"ollamaModelId",
"ollamaBaseUrl",
"lmStudioModelId",
"lmStudioBaseUrl",
"anthropicBaseUrl",
"modelMaxThinkingTokens",
"azureApiVersion",
"openAiStreamingEnabled",
"openAiR1FormatEnabled",
"openRouterModelId",
"openRouterModelInfo",
"openRouterBaseUrl",
"openRouterSpecificProvider",
"openRouterUseMiddleOutTransform",
"googleGeminiBaseUrl",
"allowedCommands",
"soundEnabled",
"ttsEnabled",
"ttsSpeed",
"soundVolume",
"diffEnabled",
"enableCheckpoints",
"checkpointStorage",
"browserViewportSize",
"screenshotQuality",
"remoteBrowserHost",
"fuzzyMatchThreshold",
"writeDelayMs",
"terminalOutputLineLimit",
"terminalShellIntegrationTimeout",
"mcpEnabled",
"enableMcpServerCreation",
"alwaysApproveResubmit",
"requestDelaySeconds",
"rateLimitSeconds",
"currentApiConfigName",
"listApiConfigMeta",
"vsCodeLmModelSelector",
"mode",
"modeApiConfigs",
"customModePrompts",
"customSupportPrompts",
"enhancementApiConfigId",
"experiments", // Map of experiment IDs to their enabled state.
"autoApprovalEnabled",
"customModes", // Array of custom modes.
"unboundModelId",
"requestyModelId",
"requestyModelInfo",
"unboundModelInfo",
"modelTemperature",
"modelMaxTokens",
"mistralCodestralUrl",
"maxOpenTabsContext",
"browserToolEnabled",
"lmStudioSpeculativeDecodingEnabled",
"lmStudioDraftModelId",
"telemetrySetting",
"showRooIgnoredFiles",
"remoteBrowserEnabled",
"language",
"maxWorkspaceFiles",
"maxReadFileLine",
"fakeAi",
"pinnedApiConfigs",
] as const
/**
* CheckpointStorage
*/
export const PASS_THROUGH_STATE_KEYS = ["taskHistory"] as const
const checkpointStorages: Record<CheckpointStorage, true> = {
task: true,
workspace: true,
}
type CheckGlobalStateKeysExhaustiveness =
Exclude<GlobalStateKey, (typeof GLOBAL_STATE_KEYS)[number]> extends never ? true : false
const CHECKPOINT_STORAGES = Object.keys(checkpointStorages) as CheckpointStorage[]
const _checkGlobalStateKeysExhaustiveness: CheckGlobalStateKeysExhaustiveness = true
const checkpointStoragesEnum: [CheckpointStorage, ...CheckpointStorage[]] = [
CHECKPOINT_STORAGES[0],
...CHECKPOINT_STORAGES.slice(1).map((p) => p),
]
export const isSecretKey = (key: string): key is SecretKey => SECRET_KEYS.includes(key as SecretKey)
/**
* ToolGroup
*/
export const isGlobalStateKey = (key: string): key is GlobalStateKey =>
GLOBAL_STATE_KEYS.includes(key as GlobalStateKey)
const toolGroups: Record<ToolGroup, true> = {
read: true,
edit: true,
browser: true,
command: true,
mcp: true,
modes: true,
}
export const isPassThroughStateKey = (key: string): key is (typeof PASS_THROUGH_STATE_KEYS)[number] =>
PASS_THROUGH_STATE_KEYS.includes(key as (typeof PASS_THROUGH_STATE_KEYS)[number])
const TOOL_GROUPS = Object.keys(toolGroups) as ToolGroup[]
const toolGroupsEnum: [ToolGroup, ...ToolGroup[]] = [TOOL_GROUPS[0], ...TOOL_GROUPS.slice(1).map((p) => p)]
/**
* Language
*/
const languages: Record<Language, true> = {
ca: true,
de: true,
en: true,
es: true,
fr: true,
hi: true,
it: true,
ja: true,
ko: true,
pl: true,
"pt-BR": true,
tr: true,
vi: true,
"zh-CN": true,
"zh-TW": true,
}
const LANGUAGES = Object.keys(languages) as Language[]
const languagesEnum: [Language, ...Language[]] = [LANGUAGES[0], ...LANGUAGES.slice(1).map((p) => p)]
export const isLanguage = (key: string): key is Language => LANGUAGES.includes(key as Language)
/**
* TelemetrySetting
*/
const telemetrySettings: Record<TelemetrySetting, true> = {
unset: true,
enabled: true,
disabled: true,
}
const TELEMETRY_SETTINGS = Object.keys(telemetrySettings) as TelemetrySetting[]
const telemetrySettingsEnum: [TelemetrySetting, ...TelemetrySetting[]] = [
TELEMETRY_SETTINGS[0],
...TELEMETRY_SETTINGS.slice(1).map((p) => p),
]
/**
* ProviderSettingsKey
*/
const providerSettingsKeys: Record<ProviderSettingsKey, true> = {
apiProvider: true,
apiModelId: true,
// Anthropic
apiKey: true,
anthropicBaseUrl: true,
// Glama
glamaApiKey: true,
glamaModelId: true,
glamaModelInfo: true,
// OpenRouter
openRouterApiKey: true,
openRouterModelId: true,
openRouterModelInfo: true,
openRouterBaseUrl: true,
openRouterSpecificProvider: true,
openRouterUseMiddleOutTransform: true,
// AWS Bedrock
awsAccessKey: true,
awsSecretKey: true,
awsSessionToken: true,
awsRegion: true,
awsUseCrossRegionInference: true,
awsUsePromptCache: true,
awspromptCacheId: true,
awsProfile: true,
awsUseProfile: true,
awsCustomArn: true,
// Google Vertex
vertexKeyFile: true,
vertexJsonCredentials: true,
vertexProjectId: true,
vertexRegion: true,
// OpenAI
openAiApiKey: true,
openAiBaseUrl: true,
openAiR1FormatEnabled: true,
openAiModelId: true,
openAiCustomModelInfo: true,
openAiUseAzure: true,
azureApiVersion: true,
openAiStreamingEnabled: true,
// Ollama
ollamaModelId: true,
ollamaBaseUrl: true,
// VS Code LM
vsCodeLmModelSelector: true,
// LM Studio
lmStudioModelId: true,
lmStudioBaseUrl: true,
lmStudioDraftModelId: true,
lmStudioSpeculativeDecodingEnabled: true,
// Gemini
geminiApiKey: true,
googleGeminiBaseUrl: true,
// OpenAI Native
openAiNativeApiKey: true,
// Mistral
mistralApiKey: true,
mistralCodestralUrl: true,
// DeepSeek
deepSeekApiKey: true,
deepSeekBaseUrl: true,
includeMaxTokens: true,
// Unbound
unboundApiKey: true,
unboundModelId: true,
unboundModelInfo: true,
// Requesty
requestyApiKey: true,
requestyModelId: true,
requestyModelInfo: true,
// Claude 3.7 Sonnet Thinking
modelTemperature: true,
modelMaxTokens: true,
modelMaxThinkingTokens: true,
// Fake AI
fakeAi: true,
}
export const PROVIDER_SETTINGS_KEYS = Object.keys(providerSettingsKeys) as ProviderSettingsKey[]
/**
* SecretStateKey
*/
const secretStateKeys: Record<SecretStateKey, true> = {
apiKey: true,
glamaApiKey: true,
openRouterApiKey: true,
awsAccessKey: true,
awsSecretKey: true,
awsSessionToken: true,
openAiApiKey: true,
geminiApiKey: true,
openAiNativeApiKey: true,
deepSeekApiKey: true,
mistralApiKey: true,
unboundApiKey: true,
requestyApiKey: true,
}
export const SECRET_STATE_KEYS = Object.keys(secretStateKeys) as SecretStateKey[]
export const isSecretStateKey = (key: string): key is SecretStateKey =>
SECRET_STATE_KEYS.includes(key as SecretStateKey)
/**
* GlobalStateKey
*/
const globalStateKeys: Record<GlobalStateKey, true> = {
apiProvider: true,
apiModelId: true,
// Anthropic
// apiKey: true,
anthropicBaseUrl: true,
// Glama
// glamaApiKey: true,
glamaModelId: true,
glamaModelInfo: true,
// OpenRouter
// openRouterApiKey: true,
openRouterModelId: true,
openRouterModelInfo: true,
openRouterBaseUrl: true,
openRouterSpecificProvider: true,
openRouterUseMiddleOutTransform: true,
// AWS Bedrock
// awsAccessKey: true,
// awsSecretKey: true,
// awsSessionToken: true,
awsRegion: true,
awsUseCrossRegionInference: true,
awsUsePromptCache: true,
awspromptCacheId: true,
awsProfile: true,
awsUseProfile: true,
awsCustomArn: true,
// Google Vertex
vertexKeyFile: true,
vertexJsonCredentials: true,
vertexProjectId: true,
vertexRegion: true,
// OpenAI
// openAiApiKey: true,
openAiBaseUrl: true,
openAiR1FormatEnabled: true,
openAiModelId: true,
openAiCustomModelInfo: true,
openAiUseAzure: true,
azureApiVersion: true,
openAiStreamingEnabled: true,
// Ollama
ollamaModelId: true,
ollamaBaseUrl: true,
// VS Code LM
vsCodeLmModelSelector: true,
// LM Studio
lmStudioModelId: true,
lmStudioBaseUrl: true,
lmStudioDraftModelId: true,
lmStudioSpeculativeDecodingEnabled: true,
// Gemini
// geminiApiKey: true,
googleGeminiBaseUrl: true,
// OpenAI Native
// openAiNativeApiKey: true,
// Mistral
// mistralApiKey: true,
mistralCodestralUrl: true,
// DeepSeek
// deepSeekApiKey: true,
deepSeekBaseUrl: true,
includeMaxTokens: true,
// Unbound
// unboundApiKey: true,
unboundModelId: true,
unboundModelInfo: true,
// Requesty
// requestyApiKey: true,
requestyModelId: true,
requestyModelInfo: true,
// Claude 3.7 Sonnet Thinking
modelTemperature: true,
modelMaxTokens: true,
modelMaxThinkingTokens: true,
// Fake AI
fakeAi: true,
currentApiConfigName: true,
listApiConfigMeta: true,
pinnedApiConfigs: true,
lastShownAnnouncementId: true,
customInstructions: true,
taskHistory: true,
autoApprovalEnabled: true,
alwaysAllowReadOnly: true,
alwaysAllowReadOnlyOutsideWorkspace: true,
alwaysAllowWrite: true,
alwaysAllowWriteOutsideWorkspace: true,
writeDelayMs: true,
alwaysAllowBrowser: true,
alwaysApproveResubmit: true,
requestDelaySeconds: true,
alwaysAllowMcp: true,
alwaysAllowModeSwitch: true,
alwaysAllowSubtasks: true,
alwaysAllowExecute: true,
allowedCommands: true,
browserToolEnabled: true,
browserViewportSize: true,
screenshotQuality: true,
remoteBrowserEnabled: true,
remoteBrowserHost: true,
enableCheckpoints: true,
checkpointStorage: true,
ttsEnabled: true,
ttsSpeed: true,
soundEnabled: true,
soundVolume: true,
maxOpenTabsContext: true,
maxWorkspaceFiles: true,
showRooIgnoredFiles: true,
maxReadFileLine: true,
terminalOutputLineLimit: true,
terminalShellIntegrationTimeout: true,
rateLimitSeconds: true,
diffEnabled: true,
fuzzyMatchThreshold: true,
experiments: true,
language: true,
telemetrySetting: true,
mcpEnabled: true,
enableMcpServerCreation: true,
mode: true,
modeApiConfigs: true,
customModes: true,
customModePrompts: true,
customSupportPrompts: true,
enhancementApiConfigId: true,
}
export const GLOBAL_STATE_KEYS = Object.keys(globalStateKeys) as GlobalStateKey[]
/**
* PassThroughStateKey
*
* TODO: Why is this necessary?
*/
const PASS_THROUGH_STATE_KEYS = ["taskHistory"] as const
type PassThroughStateKey = (typeof PASS_THROUGH_STATE_KEYS)[number]
export const isPassThroughStateKey = (key: string): key is PassThroughStateKey =>
PASS_THROUGH_STATE_KEYS.includes(key as PassThroughStateKey)
/**
* Schemas
*/
/**
* ModelInfo
*/
const modelInfoSchema = z.object({
maxTokens: z.number().optional(),
contextWindow: z.number(),
supportsImages: z.boolean().optional(),
supportsComputerUse: z.boolean().optional(),
supportsPromptCache: z.boolean(),
inputPrice: z.number().optional(),
outputPrice: z.number().optional(),
cacheWritesPrice: z.number().optional(),
cacheReadsPrice: z.number().optional(),
description: z.string().optional(),
reasoningEffort: z.enum(["low", "medium", "high"]).optional(),
thinking: z.boolean().optional(),
})
// Throws a type error if the inferred type of the modelInfoSchema is not equal
// to ModelInfo.
type _AssertModelInfo = AssertEqual<Equals<ModelInfo, z.infer<typeof modelInfoSchema>>>
/**
* ApiConfigMeta
*/
const apiConfigMetaSchema = z.object({
id: z.string(),
name: z.string(),
apiProvider: z.enum(providerNamesEnum).optional(),
})
type _AssertApiConfigMeta = AssertEqual<Equals<ApiConfigMeta, z.infer<typeof apiConfigMetaSchema>>>
/**
* HistoryItem
*/
const historyItemSchema = z.object({
id: z.string(),
number: z.number(),
ts: z.number(),
task: z.string(),
tokensIn: z.number(),
tokensOut: z.number(),
cacheWrites: z.number().optional(),
cacheReads: z.number().optional(),
totalCost: z.number(),
size: z.number().optional(),
})
type _AssertHistoryItem = AssertEqual<Equals<HistoryItem, z.infer<typeof historyItemSchema>>>
/**
* GroupEntry
*/
const groupEntrySchema = z.union([
z.enum(toolGroupsEnum),
z
.tuple([
z.enum(toolGroupsEnum),
z.object({
fileRegex: z.string().optional(),
description: z.string().optional(),
}),
])
.readonly(),
])
type _AssertGroupEntry = AssertEqual<Equals<GroupEntry, z.infer<typeof groupEntrySchema>>>
/**
* ModeConfig
*/
const modeConfigSchema = z.object({
slug: z.string(),
name: z.string(),
roleDefinition: z.string(),
customInstructions: z.string().optional(),
groups: z.array(groupEntrySchema).readonly(),
source: z.enum(["global", "project"]).optional(),
})
type _AssertModeConfig = AssertEqual<Equals<ModeConfig, z.infer<typeof modeConfigSchema>>>
/**
* ExperimentId
*/
const experimentsSchema = z.object({
experimentalDiffStrategy: z.boolean(),
search_and_replace: z.boolean(),
insert_content: z.boolean(),
powerSteering: z.boolean(),
multi_search_and_replace: z.boolean(),
})
// Throws a type error if the inferred type of the experimentsSchema is not
// equal to ExperimentId.
type _AssertExperiments = AssertEqual<Equals<ExperimentId, Keys<z.infer<typeof experimentsSchema>>>>
/**
* GlobalSettings
*/
export const globalSettingsSchema = z.object({
currentApiConfigName: z.string().optional(),
listApiConfigMeta: z.array(apiConfigMetaSchema).optional(),
pinnedApiConfigs: z.record(z.string(), z.boolean()).optional(),
lastShownAnnouncementId: z.string().optional(),
customInstructions: z.string().optional(),
taskHistory: z.array(historyItemSchema).optional(),
autoApprovalEnabled: z.boolean().optional(),
alwaysAllowReadOnly: z.boolean().optional(),
alwaysAllowReadOnlyOutsideWorkspace: z.boolean().optional(),
alwaysAllowWrite: z.boolean().optional(),
alwaysAllowWriteOutsideWorkspace: z.boolean().optional(),
writeDelayMs: z.number().optional(),
alwaysAllowBrowser: z.boolean().optional(),
alwaysApproveResubmit: z.boolean().optional(),
requestDelaySeconds: z.number().optional(),
alwaysAllowMcp: z.boolean().optional(),
alwaysAllowModeSwitch: z.boolean().optional(),
alwaysAllowSubtasks: z.boolean().optional(),
alwaysAllowExecute: z.boolean().optional(),
allowedCommands: z.array(z.string()).optional(),
browserToolEnabled: z.boolean().optional(),
browserViewportSize: z.string().optional(),
screenshotQuality: z.number().optional(),
remoteBrowserEnabled: z.boolean().optional(),
remoteBrowserHost: z.string().optional(),
enableCheckpoints: z.boolean().optional(),
checkpointStorage: z.enum(checkpointStoragesEnum).optional(),
ttsEnabled: z.boolean().optional(),
ttsSpeed: z.number().optional(),
soundEnabled: z.boolean().optional(),
soundVolume: z.number().optional(),
maxOpenTabsContext: z.number().optional(),
maxWorkspaceFiles: z.number().optional(),
showRooIgnoredFiles: z.boolean().optional(),
maxReadFileLine: z.number().optional(),
terminalOutputLineLimit: z.number().optional(),
terminalShellIntegrationTimeout: z.number().optional(),
rateLimitSeconds: z.number().optional(),
diffEnabled: z.boolean().optional(),
fuzzyMatchThreshold: z.number().optional(),
experiments: experimentsSchema.optional(),
language: z.enum(languagesEnum).optional(),
telemetrySetting: z.enum(telemetrySettingsEnum).optional(),
mcpEnabled: z.boolean().optional(),
enableMcpServerCreation: z.boolean().optional(),
mode: z.string().optional(),
modeApiConfigs: z.record(z.string(), z.string()).optional(),
customModes: z.array(modeConfigSchema).optional(),
customModePrompts: z
.record(
z.string(),
z
.object({
roleDefinition: z.string().optional(),
customInstructions: z.string().optional(),
})
.optional(),
)
.optional(),
customSupportPrompts: z.record(z.string(), z.string().optional()).optional(),
enhancementApiConfigId: z.string().optional(),
})
// Throws a type error if the inferred type of the globalSettingsSchema is not
// equal to GlobalSettings.
type _AssertGlobalSettings = AssertEqual<Equals<GlobalSettings, z.infer<typeof globalSettingsSchema>>>
/**
* ProviderSettings
*/
export const providerSettingsSchema = z.object({
apiProvider: z.enum(providerNamesEnum).optional(),
// Anthropic
apiModelId: z.string().optional(),
apiKey: z.string().optional(),
anthropicBaseUrl: z.string().optional(),
// Glama
glamaModelId: z.string().optional(),
glamaModelInfo: modelInfoSchema.optional(),
glamaApiKey: z.string().optional(),
// OpenRouter
openRouterApiKey: z.string().optional(),
openRouterModelId: z.string().optional(),
openRouterModelInfo: modelInfoSchema.optional(),
openRouterBaseUrl: z.string().optional(),
openRouterSpecificProvider: z.string().optional(),
// AWS Bedrock
awsAccessKey: z.string().optional(),
awsSecretKey: z.string().optional(),
awsSessionToken: z.string().optional(),
awsRegion: z.string().optional(),
awsUseCrossRegionInference: z.boolean().optional(),
awsUsePromptCache: z.boolean().optional(),
awspromptCacheId: z.string().optional(),
awsProfile: z.string().optional(),
awsUseProfile: z.boolean().optional(),
awsCustomArn: z.string().optional(),
// Google Vertex
vertexKeyFile: z.string().optional(),
vertexJsonCredentials: z.string().optional(),
vertexProjectId: z.string().optional(),
vertexRegion: z.string().optional(),
// OpenAI
openAiBaseUrl: z.string().optional(),
openAiApiKey: z.string().optional(),
openAiR1FormatEnabled: z.boolean().optional(),
openAiModelId: z.string().optional(),
openAiCustomModelInfo: modelInfoSchema.optional(),
openAiUseAzure: z.boolean().optional(),
// Ollama
ollamaModelId: z.string().optional(),
ollamaBaseUrl: z.string().optional(),
// VS Code LM
vsCodeLmModelSelector: z
.object({
vendor: z.string().optional(),
family: z.string().optional(),
version: z.string().optional(),
id: z.string().optional(),
})
.optional(),
// LM Studio
lmStudioModelId: z.string().optional(),
lmStudioBaseUrl: z.string().optional(),
lmStudioDraftModelId: z.string().optional(),
lmStudioSpeculativeDecodingEnabled: z.boolean().optional(),
// Gemini
geminiApiKey: z.string().optional(),
googleGeminiBaseUrl: z.string().optional(),
// OpenAI Native
openAiNativeApiKey: z.string().optional(),
// Mistral
mistralApiKey: z.string().optional(),
mistralCodestralUrl: z.string().optional(),
// Azure
azureApiVersion: z.string().optional(),
// OpenRouter
openRouterUseMiddleOutTransform: z.boolean().optional(),
openAiStreamingEnabled: z.boolean().optional(),
// DeepSeek
deepSeekBaseUrl: z.string().optional(),
deepSeekApiKey: z.string().optional(),
// Unbound
unboundApiKey: z.string().optional(),
unboundModelId: z.string().optional(),
unboundModelInfo: modelInfoSchema.optional(),
// Requesty
requestyApiKey: z.string().optional(),
requestyModelId: z.string().optional(),
requestyModelInfo: modelInfoSchema.optional(),
// Claude 3.7 Sonnet Thinking
modelTemperature: z.number().nullish(),
modelMaxTokens: z.number().optional(),
modelMaxThinkingTokens: z.number().optional(),
// Generic
includeMaxTokens: z.boolean().optional(),
// Fake AI
fakeAi: z.unknown().optional(),
})
// Throws a type error if the inferred type of the providerSettingsSchema is not
// equal to ProviderSettings.
type _AssertProviderSettings = AssertEqual<Equals<ProviderSettings, z.infer<typeof providerSettingsSchema>>>

View file

@ -1,7 +1,13 @@
import { type Language } from "../exports/roo-code"
import { isLanguage } from "./globalState"
export type { Language }
/**
* Language name mapping from ISO codes to full language names
*/
export const LANGUAGES: Record<string, string> = {
export const LANGUAGES: Record<Language, string> = {
ca: "Català",
de: "Deutsch",
en: "English",
@ -26,10 +32,12 @@ export const LANGUAGES: Record<string, string> = {
* @param vscodeLocale - The VSCode locale string to format (e.g., "en-us", "fr-ca")
* @returns The formatted locale string with uppercase region code
*/
export function formatLanguage(vscodeLocale: string): string {
export function formatLanguage(vscodeLocale: string): Language {
if (!vscodeLocale) {
return "en" // Default to English if no locale is provided
return "en"
}
return vscodeLocale.replace(/-(\w+)$/, (_, region) => `-${region.toUpperCase()}`)
const formattedLocale = vscodeLocale.replace(/-(\w+)$/, (_, region) => `-${region.toUpperCase()}`)
return isLanguage(formattedLocale) ? formattedLocale : "en"
}

View file

@ -1,38 +1,12 @@
import * as vscode from "vscode"
import { GroupOptions, GroupEntry, ModeConfig, PromptComponent, CustomModePrompts } from "../exports/roo-code"
import { TOOL_GROUPS, ToolGroup, ALWAYS_AVAILABLE_TOOLS } from "./tool-groups"
import { addCustomInstructions } from "../core/prompts/sections/custom-instructions"
// Mode types
export type Mode = string
// Group options type
export type GroupOptions = {
fileRegex?: string // Regular expression pattern
description?: string // Human-readable description of the pattern
}
// Group entry can be either a string or tuple with options
export type GroupEntry = ToolGroup | readonly [ToolGroup, GroupOptions]
// Mode configuration type
export type ModeConfig = {
slug: string
name: string
roleDefinition: string
customInstructions?: string
groups: readonly GroupEntry[] // Now supports both simple strings and tuples with options
source?: "global" | "project" // Where this mode was loaded from
}
// Mode-specific prompts only
export type PromptComponent = {
roleDefinition?: string
customInstructions?: string
}
export type CustomModePrompts = {
[key: string]: PromptComponent | undefined
}
export type { GroupOptions, GroupEntry, ModeConfig, PromptComponent, CustomModePrompts }
// Helper to extract group name regardless of format
export function getGroupName(group: GroupEntry): ToolGroup {

View file

@ -1,3 +1,5 @@
import type { ToolGroup } from "../exports/roo-code"
// Define tool group configuration
export type ToolGroupConfig = {
tools: readonly string[]
@ -23,8 +25,10 @@ export const TOOL_DISPLAY_NAMES = {
new_task: "create new task",
} as const
export type { ToolGroup }
// Define available tool groups
export const TOOL_GROUPS: Record<string, ToolGroupConfig> = {
export const TOOL_GROUPS: Record<ToolGroup, ToolGroupConfig> = {
read: {
tools: ["read_file", "fetch_instructions", "search_files", "list_files", "list_code_definition_names"],
},
@ -46,8 +50,6 @@ export const TOOL_GROUPS: Record<string, ToolGroupConfig> = {
},
}
export type ToolGroup = keyof typeof TOOL_GROUPS
// Tools that are always available to all modes
export const ALWAYS_AVAILABLE_TOOLS = [
"ask_followup_question",

7
src/utils/type-fu.ts Normal file
View file

@ -0,0 +1,7 @@
export type Keys<T> = keyof T
export type Values<T> = T[keyof T]
export type Equals<X, Y> = (<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y ? 1 : 2 ? true : false
export type AssertEqual<T extends true> = T

View file

@ -1033,6 +1033,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
}}
contentClassName="max-h-[300px] overflow-y-auto"
triggerClassName="w-full text-ellipsis overflow-hidden"
itemClassName="group"
renderItem={({ type, value, label, pinned }) => {
if (type !== DropdownOptionType.ITEM) {
return label
@ -1042,9 +1043,16 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
const isCurrentConfig = config?.name === currentApiConfigName
return (
<div className="flex items-center justify-between gap-2 w-full">
<div className="flex justify-between gap-2 w-full h-5">
<div className={cn({ "font-medium": isCurrentConfig })}>{label}</div>
<div className="flex items-center gap-1">
<div className="flex justify-end w-10">
<div
className={cn("size-5 p-1", {
"block group-hover:hidden": !pinned,
hidden: !isCurrentConfig,
})}>
<Check className="size-3" />
</div>
<Button
variant="ghost"
size="icon"
@ -1054,10 +1062,12 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
togglePinnedApiConfig(value)
vscode.postMessage({ type: "toggleApiConfigPin", text: value })
}}
className={cn("w-5 h-5", { "bg-accent": pinned })}>
className={cn("size-5", {
"hidden group-hover:flex": !pinned,
"bg-accent": pinned,
})}>
<Pin className="size-3 p-0.5 opacity-50" />
</Button>
{isCurrentConfig && <Check className="size-3" />}
</div>
</div>
)

View file

@ -1,14 +1,15 @@
import { HTMLAttributes } from "react"
import { useAppTranslation } from "@/i18n/TranslationContext"
import { Trans } from "react-i18next"
import { Info } from "lucide-react"
import { Info, Download, Upload, TriangleAlert } from "lucide-react"
import { VSCodeButton, VSCodeCheckbox, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { VSCodeCheckbox, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { TelemetrySetting } from "../../../../src/shared/TelemetrySetting"
import { vscode } from "@/utils/vscode"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui"
import { SectionHeader } from "./SectionHeader"
import { Section } from "./Section"
@ -34,7 +35,6 @@ export const About = ({ version, telemetrySetting, setTelemetrySetting, classNam
<Section>
<div>
<VSCodeCheckbox
style={{ marginBottom: "5px" }}
checked={telemetrySetting === "enabled"}
onChange={(e: any) => {
const checked = e.target.checked === true
@ -42,12 +42,7 @@ export const About = ({ version, telemetrySetting, setTelemetrySetting, classNam
}}>
{t("settings:footer.telemetry.label")}
</VSCodeCheckbox>
<p
style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
<p className="text-vscode-descriptionForeground text-sm mt-0">
{t("settings:footer.telemetry.description")}
</p>
</div>
@ -63,15 +58,19 @@ export const About = ({ version, telemetrySetting, setTelemetrySetting, classNam
/>
</div>
<div className="flex justify-between items-center gap-3">
<p>{t("settings:footer.reset.description")}</p>
<VSCodeButton
onClick={() => vscode.postMessage({ type: "resetState" })}
appearance="secondary"
className="shrink-0">
<span className="codicon codicon-warning text-vscode-errorForeground mr-1" />
{t("settings:footer.reset.button")}
</VSCodeButton>
<div className="flex items-center gap-2 mt-2">
<Button onClick={() => vscode.postMessage({ type: "exportSettings" })}>
<Upload className="p-0.5" />
{t("settings:footer.settings.export")}
</Button>
<Button onClick={() => vscode.postMessage({ type: "importSettings" })}>
<Download className="p-0.5" />
{t("settings:footer.settings.import")}
</Button>
<Button variant="destructive" onClick={() => vscode.postMessage({ type: "resetState" })}>
<TriangleAlert className="p-0.5" />
{t("settings:footer.settings.reset")}
</Button>
</div>
</Section>
</div>

View file

@ -4,7 +4,7 @@ import { Globe } from "lucide-react"
import { cn } from "@/lib/utils"
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@/components/ui"
import { LANGUAGES } from "../../../../src/shared/language"
import { Language, LANGUAGES } from "../../../../src/shared/language"
import { SetCachedStateField } from "./types"
import { SectionHeader } from "./SectionHeader"
@ -28,7 +28,7 @@ export const LanguageSettings = ({ language, setCachedStateField, className, ...
</SectionHeader>
<Section>
<Select value={language} onValueChange={(value) => setCachedStateField("language", value)}>
<Select value={language} onValueChange={(value) => setCachedStateField("language", value as Language)}>
<SelectTrigger className="w-full">
<SelectValue placeholder={t("settings:common.select")} />
</SelectTrigger>

View file

@ -85,7 +85,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
const { t } = useAppTranslation()
const extensionState = useExtensionState()
const { currentApiConfigName, listApiConfigMeta, uriScheme, version } = extensionState
const { currentApiConfigName, listApiConfigMeta, uriScheme, version, settingsImportedAt } = extensionState
const [isDiscardDialogShow, setDiscardDialogShow] = useState(false)
const [isChangeDetected, setChangeDetected] = useState(false)
@ -138,6 +138,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
// Make sure apiConfiguration is initialized and managed by SettingsView.
const apiConfiguration = useMemo(() => cachedState.apiConfiguration ?? {}, [cachedState.apiConfiguration])
useEffect(() => {
// Update only when currentApiConfigName is changed.
// Expected to be triggered by loadApiConfiguration/upsertApiConfiguration.
@ -150,6 +151,14 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
setChangeDetected(false)
}, [currentApiConfigName, extensionState, isChangeDetected])
// Bust the cache when settings are imported.
useEffect(() => {
if (settingsImportedAt) {
setCachedState((prevCachedState) => ({ ...prevCachedState, ...extensionState }))
setChangeDetected(false)
}
}, [settingsImportedAt, extensionState])
const setCachedStateField: SetCachedStateField<keyof ExtensionStateContextType> = useCallback((field, value) => {
setCachedState((prevState) => {
if (prevState[field] === value) {
@ -182,11 +191,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
}
setChangeDetected(true)
return {
...prevState,
experiments: { ...prevState.experiments, [id]: enabled },
}
return { ...prevState, experiments: { ...prevState.experiments, [id]: enabled } }
})
}, [])
@ -195,11 +200,9 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
if (prevState.telemetrySetting === setting) {
return prevState
}
setChangeDetected(true)
return {
...prevState,
telemetrySetting: setting,
}
return { ...prevState, telemetrySetting: setting }
})
}, [])
@ -460,8 +463,8 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
maxOpenTabsContext={maxOpenTabsContext}
maxWorkspaceFiles={maxWorkspaceFiles ?? 200}
showRooIgnoredFiles={showRooIgnoredFiles}
setCachedStateField={setCachedStateField}
maxReadFileLine={maxReadFileLine}
setCachedStateField={setCachedStateField}
/>
</div>

View file

@ -36,6 +36,7 @@ export interface SelectDropdownProps {
title?: string
triggerClassName?: string
contentClassName?: string
itemClassName?: string
sideOffset?: number
align?: "start" | "center" | "end"
placeholder?: string
@ -53,6 +54,7 @@ export const SelectDropdown = React.forwardRef<React.ElementRef<typeof DropdownM
title = "",
triggerClassName = "",
contentClassName = "",
itemClassName = "",
sideOffset = 4,
align = "start",
placeholder = "",
@ -125,7 +127,8 @@ export const SelectDropdown = React.forwardRef<React.ElementRef<typeof DropdownM
<DropdownMenuItem
key={`item-${option.value}`}
disabled={option.disabled}
onClick={() => handleSelect(option)}>
onClick={() => handleSelect(option)}
className={itemClassName}>
{renderItem ? (
renderItem(option)
) : (

View file

@ -381,9 +381,10 @@
"label": "Permetre informes anònims d'errors i ús",
"description": "Ajudeu a millorar Roo Code enviant dades d'ús anònimes i informes d'errors. Mai s'envia codi, prompts o informació personal. Vegeu la nostra política de privacitat per a més detalls."
},
"reset": {
"description": "Restablir tot l'estat global i emmagatzematge secret a l'extensió.",
"button": "Restablir"
"settings": {
"import": "Importar",
"export": "Exportar",
"reset": "Restablir"
}
},
"thinkingBudget": {

View file

@ -381,9 +381,10 @@
"label": "Anonyme Fehler- und Nutzungsberichte zulassen",
"description": "Helfen Sie, Roo Code zu verbessern, indem Sie anonyme Nutzungsdaten und Fehlerberichte senden. Es werden niemals Code, Prompts oder persönliche Informationen gesendet. Weitere Details finden Sie in unserer Datenschutzrichtlinie."
},
"reset": {
"description": "Setze alle globalen Zustände und geheimen Speicher in der Erweiterung zurück.",
"button": "Zurücksetzen"
"settings": {
"import": "Importieren",
"export": "Exportieren",
"reset": "Zurücksetzen"
}
},
"thinkingBudget": {

View file

@ -380,9 +380,10 @@
"label": "Allow anonymous error and usage reporting",
"description": "Help improve Roo Code by sending anonymous usage data and error reports. No code, prompts, or personal information is ever sent. See our privacy policy for more details."
},
"reset": {
"description": "Reset all global state and secret storage in the extension.",
"button": "Reset"
"settings": {
"import": "Import",
"export": "Export",
"reset": "Reset"
}
},
"thinkingBudget": {

View file

@ -381,9 +381,10 @@
"label": "Permitir informes anónimos de errores y uso",
"description": "Ayude a mejorar Roo Code enviando datos de uso anónimos e informes de errores. Nunca se envía código, prompts o información personal. Consulte nuestra política de privacidad para más detalles."
},
"reset": {
"description": "Restablecer todo el estado global y almacenamiento secreto en la extensión.",
"button": "Restablecer"
"settings": {
"import": "Importar",
"export": "Exportar",
"reset": "Restablecer"
}
},
"thinkingBudget": {

View file

@ -381,9 +381,10 @@
"label": "Autoriser les rapports anonymes d'erreurs et d'utilisation",
"description": "Aidez à améliorer Roo Code en envoyant des données d'utilisation anonymes et des rapports d'erreurs. Aucun code, prompt ou information personnelle n'est jamais envoyé. Consultez notre politique de confidentialité pour plus de détails."
},
"reset": {
"description": "Réinitialiser tous les états globaux et le stockage secret dans l'extension.",
"button": "Réinitialiser"
"settings": {
"import": "Importer",
"export": "Exporter",
"reset": "Réinitialiser"
}
},
"thinkingBudget": {

View file

@ -381,9 +381,10 @@
"label": "गुमनाम त्रुटि और उपयोग रिपोर्टिंग की अनुमति दें",
"description": "गुमनाम उपयोग डेटा और त्रुटि रिपोर्ट भेजकर Roo Code को बेहतर बनाने में मदद करें। कोड, प्रॉम्प्ट, या व्यक्तिगत जानकारी कभी भी नहीं भेजी जाती है। अधिक विवरण के लिए हमारी गोपनीयता नीति देखें।"
},
"reset": {
"description": "एक्सटेंशन में सभी वैश्विक स्थिति और गुप्त भंडारण को रीसेट करें।",
"button": "रीसेट करें"
"settings": {
"import": "इम्पोर्ट",
"export": "एक्सपोर्ट",
"reset": "रीसेट करें"
}
},
"thinkingBudget": {

View file

@ -381,9 +381,10 @@
"label": "Consenti segnalazioni anonime di errori e utilizzo",
"description": "Aiuta a migliorare Roo Code inviando dati di utilizzo anonimi e segnalazioni di errori. Non vengono mai inviati codice, prompt o informazioni personali. Consulta la nostra politica sulla privacy per maggiori dettagli."
},
"reset": {
"description": "Reimposta tutti gli stati globali e l'archivio segreto nell'estensione.",
"button": "Ripristina"
"settings": {
"import": "Importa",
"export": "Esporta",
"reset": "Ripristina"
}
},
"thinkingBudget": {

View file

@ -381,9 +381,10 @@
"label": "匿名のエラーと使用状況レポートを許可",
"description": "匿名の使用データとエラーレポートを送信してRoo Codeの改善にご協力ください。コード、プロンプト、個人情報が送信されることはありません。詳細については、プライバシーポリシーをご覧ください。"
},
"reset": {
"description": "拡張機能内のすべてのグローバル状態とシークレットストレージをリセットします。",
"button": "リセット"
"settings": {
"import": "インポート",
"export": "エクスポート",
"reset": "リセット"
}
},
"thinkingBudget": {

View file

@ -381,9 +381,10 @@
"label": "익명 오류 및 사용 보고 허용",
"description": "익명 사용 데이터 및 오류 보고서를 보내 Roo Code 개선에 도움을 주세요. 코드, 프롬프트 또는 개인 정보는 절대 전송되지 않습니다. 자세한 내용은 개인정보 보호정책을 참조하세요."
},
"reset": {
"description": "확장 프로그램의 모든 전역 상태 및 보안 저장소를 재설정합니다.",
"button": "초기화"
"settings": {
"import": "가져오기",
"export": "내보내기",
"reset": "초기화"
}
},
"thinkingBudget": {

View file

@ -381,9 +381,10 @@
"label": "Zezwól na anonimowe raportowanie błędów i użycia",
"description": "Pomóż ulepszyć Roo Code, wysyłając anonimowe dane o użytkowaniu i raporty o błędach. Nigdy nie są wysyłane kod, podpowiedzi ani informacje osobiste. Zobacz naszą politykę prywatności, aby uzyskać więcej szczegółów."
},
"reset": {
"description": "Zresetuj wszystkie globalne stany i tajne magazyny w rozszerzeniu.",
"button": "Resetuj"
"settings": {
"import": "Importuj",
"export": "Eksportuj",
"reset": "Resetuj"
}
},
"thinkingBudget": {

View file

@ -381,9 +381,10 @@
"label": "Permitir relatórios anônimos de erros e uso",
"description": "Ajude a melhorar o Roo Code enviando dados de uso anônimos e relatórios de erros. Nunca são enviados código, prompts ou informações pessoais. Consulte nossa política de privacidade para mais detalhes."
},
"reset": {
"description": "Redefinir todo o estado global e armazenamento secreto na extensão.",
"button": "Redefinir"
"settings": {
"import": "Importar",
"export": "Exportar",
"reset": "Redefinir"
}
},
"thinkingBudget": {

View file

@ -381,9 +381,10 @@
"label": "Anonim hata ve kullanım raporlamaya izin ver",
"description": "Anonim kullanım verileri ve hata raporları göndererek Roo Code'u geliştirmeye yardımcı olun. Hiçbir kod, istem veya kişisel bilgi asla gönderilmez. Daha fazla ayrıntı için gizlilik politikamıza bakın."
},
"reset": {
"description": "Uzantıdaki tüm global durumu ve gizli depolamayı sıfırlayın.",
"button": "Sıfırla"
"settings": {
"import": "İçe Aktar",
"export": "Dışa Aktar",
"reset": "Sıfırla"
}
},
"thinkingBudget": {

View file

@ -381,9 +381,10 @@
"label": "Cho phép báo cáo lỗi và sử dụng ẩn danh",
"description": "Giúp cải thiện Roo Code bằng cách gửi dữ liệu sử dụng ẩn danh và báo cáo lỗi. Không bao giờ gửi mã, lời nhắc hoặc thông tin cá nhân. Xem chính sách bảo mật của chúng tôi để biết thêm chi tiết."
},
"reset": {
"description": "Đặt lại tất cả trạng thái toàn cầu và lưu trữ bí mật trong tiện ích mở rộng.",
"button": "Đặt lại"
"settings": {
"import": "Nhập",
"export": "Xuất",
"reset": "Đặt lại"
}
},
"thinkingBudget": {

View file

@ -381,9 +381,10 @@
"label": "允许匿名错误和使用情况报告",
"description": "通过发送匿名使用数据和错误报告来帮助改进 Roo Code。绝不会发送代码、提示或个人信息。有关更多详细信息请参阅我们的隐私政策。"
},
"reset": {
"description": "重置扩展中的所有全局状态和密钥存储。",
"button": "重置"
"settings": {
"import": "导入",
"export": "导出",
"reset": "重置"
}
},
"thinkingBudget": {

View file

@ -381,9 +381,10 @@
"label": "允許匿名錯誤和使用報告",
"description": "匿名資料協助改善 Roo Code絕不傳送程式碼或個人資訊"
},
"reset": {
"description": "重設擴充功能的全局狀態和金鑰儲存",
"button": "重設"
"settings": {
"import": "匯入",
"export": "匯出",
"reset": "重設"
}
},
"thinkingBudget": {