Merge branch 'main' into cte/benchmark-monorepo

This commit is contained in:
cte 2025-03-26 16:34:44 -07:00
commit a7472e6533
69 changed files with 2884 additions and 1871 deletions

View file

@ -0,0 +1,5 @@
---
"roo-cline": patch
---
Show requesty key balance on the settings screen

View file

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

View file

@ -448,6 +448,5 @@
"webview-ui/**/*.{ts,tsx}": [
"npx eslint -c webview-ui/.eslintrc.json --max-warnings=0 --fix"
]
},
"packageManager": "pnpm@10.6.5+sha512.cdf928fca20832cd59ec53826492b7dc25dc524d4370b6b4adbf65803d32efaa6c1c88147c0ae4e8d579a6c9eec715757b50d4fa35eea179d868eada4ed043af"
}
}

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,260 +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
*/
async saveConfig(name: string, config: ApiConfiguration): Promise<void> {
try {
return await this.lock(async () => {
const currentConfig = await this.readConfig()
const existingConfig = currentConfig.apiConfigs[name]
currentConfig.apiConfigs[name] = {
...config,
id: existingConfig?.id || 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}`)
}
}
/**
* 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,12 +1,10 @@
// npx jest src/core/__tests__/contextProxy.test.ts
import fs from "fs/promises"
// npx jest src/core/config/__tests__/ContextProxy.test.ts
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: {
@ -79,8 +77,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)
}
})
@ -89,11 +87,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
@ -101,8 +99,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 () => {
@ -110,7 +108,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")
@ -122,37 +120,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")
})
})
@ -222,27 +244,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", () => {
@ -290,7 +291,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")
@ -300,8 +301,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
@ -334,8 +335,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(
@ -399,12 +400,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 () => {
@ -418,76 +419,4 @@ describe("ContextProxy", () => {
expect(initializeSpy).toHaveBeenCalledTimes(1)
})
})
describe("exportGlobalConfiguration", () => {
it("should write configuration to a file when filePath is provided", async () => {
await proxy.updateGlobalState("apiModelId", "gpt-4")
await proxy.updateGlobalState("apiProvider", "openai")
await proxy.storeSecret("openAiApiKey", "test-api-key")
const filePath = `/tmp/roo-global-config-${Date.now()}.json`
const result = await proxy.exportGlobalConfiguration(filePath)
expect(result).toEqual({ apiProvider: "openai" })
const fileContent = await fs.readFile(filePath, "utf-8")
expect(fileContent).toContain('"apiProvider": "openai"')
await proxy.updateGlobalState("apiProvider", "openrouter")
const importedConfig = await proxy.importGlobalConfiguration(filePath)
expect(importedConfig).toEqual({ apiProvider: "openai" })
await fs.unlink(filePath)
})
})
describe("exportApiConfiguration", () => {
it("should write configuration to a file when filePath is provided", async () => {
await proxy.updateGlobalState("apiModelId", "gpt-4")
await proxy.updateGlobalState("apiProvider", "openai")
await proxy.storeSecret("openAiApiKey", "test-api-key")
const filePath = `/tmp/roo-api-config-${Date.now()}.json`
const result = await proxy.exportApiConfiguration(filePath)
expect(result).toEqual({
apiModelId: "gpt-4",
openAiApiKey: "test-api-key",
apiKey: "test-secret",
awsAccessKey: "test-secret",
awsSecretKey: "test-secret",
awsSessionToken: "test-secret",
deepSeekApiKey: "test-secret",
geminiApiKey: "test-secret",
glamaApiKey: "test-secret",
mistralApiKey: "test-secret",
openAiNativeApiKey: "test-secret",
openRouterApiKey: "test-secret",
requestyApiKey: "test-secret",
unboundApiKey: "test-secret",
})
const fileContent = await fs.readFile(filePath, "utf-8")
expect(fileContent).toContain('"openAiApiKey": "test-api-key"')
await proxy.storeSecret("openAiApiKey", "new-text-api-key")
const importedConfig = await proxy.importApiConfiguration(filePath)
expect(importedConfig).toEqual({
apiModelId: "gpt-4",
openAiApiKey: "test-api-key",
apiKey: "test-secret",
awsAccessKey: "test-secret",
awsSecretKey: "test-secret",
awsSessionToken: "test-secret",
deepSeekApiKey: "test-secret",
geminiApiKey: "test-secret",
glamaApiKey: "test-secret",
mistralApiKey: "test-secret",
openAiNativeApiKey: "test-secret",
openRouterApiKey: "test-secret",
requestyApiKey: "test-secret",
unboundApiKey: "test-secret",
})
await fs.unlink(filePath)
})
})
})

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,316 +0,0 @@
import * as vscode from "vscode"
import * as fs from "fs/promises"
import * as path from "path"
import { logger } from "../utils/logging"
import {
GLOBAL_STATE_KEYS,
SECRET_KEYS,
GlobalStateKey,
SecretKey,
ConfigurationKey,
ConfigurationValues,
isSecretKey,
isGlobalStateKey,
isPassThroughStateKey,
globalStateSchema,
} from "../shared/globalState"
import { API_CONFIG_KEYS, ApiConfiguration, apiHandlerOptionsSchema, ApiHandlerOptionsKey } from "../shared/api"
const NON_EXPORTABLE_GLOBAL_CONFIGURATION: GlobalStateKey[] = [
"taskHistory",
"listApiConfigMeta",
"currentApiConfigName",
]
const NON_EXPORTABLE_API_CONFIGURATION: ApiHandlerOptionsKey[] = [
"glamaModelInfo",
"openRouterModelInfo",
"unboundModelInfo",
"requestyModelInfo",
]
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) => isGlobalStateKey(key))
.filter((key) => !!this.stateCache.get(key))
.reduce((acc, key) => ({ ...acc, [key]: undefined }), {} as Partial<ConfigurationValues>),
...apiConfiguration,
})
}
private getAllGlobalStateValues() {
const values: Partial<Record<GlobalStateKey, any>> = {}
for (const key of GLOBAL_STATE_KEYS) {
const value = this.getGlobalState(key)
if (value !== undefined) {
values[key] = value
}
}
return values
}
private getAllSecretValues() {
const values: Partial<Record<SecretKey, string>> = {}
for (const key of SECRET_KEYS) {
const value = this.getSecret(key)
if (value !== undefined) {
values[key] = value
}
}
return values
}
async exportGlobalConfiguration(filePath: string) {
try {
const values = this.getAllGlobalStateValues()
const configuration = globalStateSchema.parse(values)
const omit = new Set<string>([...API_CONFIG_KEYS, ...NON_EXPORTABLE_GLOBAL_CONFIGURATION])
const entries = Object.entries(configuration).filter(([key]) => !omit.has(key))
if (entries.length === 0) {
throw new Error("No configuration values to export.")
}
const globalConfiguration = Object.fromEntries(entries)
const dirname = path.dirname(filePath)
await fs.mkdir(dirname, { recursive: true })
await fs.writeFile(filePath, JSON.stringify(globalConfiguration, null, 2), "utf-8")
return globalConfiguration
} catch (error) {
console.log(error.message)
logger.error(
`Error exporting global configuration to ${filePath}: ${error instanceof Error ? error.message : String(error)}`,
)
return undefined
}
}
async importGlobalConfiguration(filePath: string) {
try {
const configuration = globalStateSchema.parse(JSON.parse(await fs.readFile(filePath, "utf-8")))
const omit = new Set<string>([...API_CONFIG_KEYS, ...NON_EXPORTABLE_GLOBAL_CONFIGURATION])
const entries = Object.entries(configuration).filter(([key]) => !omit.has(key))
if (entries.length === 0) {
throw new Error("No configuration values to import.")
}
const globalConfiguration = Object.fromEntries(entries)
await this.setValues(globalConfiguration)
return globalConfiguration
} catch (error) {
logger.error(
`Error importing global configuration from ${filePath}: ${error instanceof Error ? error.message : String(error)}`,
)
return undefined
}
}
async exportApiConfiguration(filePath: string) {
try {
const apiConfiguration = apiHandlerOptionsSchema
.omit(NON_EXPORTABLE_API_CONFIGURATION.reduce((acc, key) => ({ ...acc, [key]: true }), {}))
.parse({
...this.getAllGlobalStateValues(),
...this.getAllSecretValues(),
})
const dirname = path.dirname(filePath)
await fs.mkdir(dirname, { recursive: true })
await fs.writeFile(filePath, JSON.stringify(apiConfiguration, null, 2), "utf-8")
return apiConfiguration
} catch (error) {
logger.error(
`Error exporting API configuration to ${filePath}: ${error instanceof Error ? error.message : String(error)}`,
)
return undefined
}
}
async importApiConfiguration(filePath: string) {
try {
const apiConfiguration = apiHandlerOptionsSchema
.omit(NON_EXPORTABLE_API_CONFIGURATION.reduce((acc, key) => ({ ...acc, [key]: true }), {}))
.parse(JSON.parse(await fs.readFile(filePath, "utf-8")))
await this.setApiConfiguration(apiConfiguration)
return apiConfiguration
} catch (error) {
logger.error(
`Error importing API configuration from ${filePath}: ${error instanceof Error ? error.message : String(error)}`,
)
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
*/
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])
await 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,18 +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,
isGlobalStateKey,
} 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"
@ -59,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"
@ -102,8 +104,8 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
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
public readonly providerSettingsManager: ProviderSettingsManager
public readonly customModesManager: CustomModesManager
constructor(
readonly context: vscode.ExtensionContext,
@ -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
@ -1082,39 +1090,24 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
this.exportTaskWithId(message.text!)
break
case "importSettings":
const uris = await vscode.window.showOpenDialog({
filters: { JSON: ["json"] },
canSelectMany: false,
const { success } = await importSettings({
providerSettingsManager: this.providerSettingsManager,
contextProxy: this.contextProxy,
})
if (uris) {
if (message.text === "global") {
await this.contextProxy.importGlobalConfiguration(uris[0].fsPath)
} else {
await this.contextProxy.importApiConfiguration(uris[0].fsPath)
}
if (success) {
this.settingsImportedAt = Date.now()
await this.postStateToWebview()
await vscode.window.showInformationMessage(t("common:info.settings_imported"))
}
break
case "exportSettings":
const uri = await vscode.window.showSaveDialog({
filters: { JSON: ["json"] },
defaultUri: vscode.Uri.file(
path.join(os.homedir(), "Documents", `roo-code-${message.text}.json`),
),
await exportSettings({
providerSettingsManager: this.providerSettingsManager,
contextProxy: this.contextProxy,
})
if (uri) {
if (message.text === "global") {
await this.contextProxy.exportGlobalConfiguration(uri.fsPath)
} else {
await this.contextProxy.exportApiConfiguration(uri.fsPath)
}
}
break
case "resetState":
await this.resetState()
@ -1373,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":
@ -1520,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) {
@ -1542,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) {
@ -1562,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": {
@ -1699,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":
@ -1710,6 +1676,21 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
await this.updateGlobalState("maxReadFileLine", message.value)
await this.postStateToWebview()
break
case "toggleApiConfigPin":
if (message.text) {
const currentPinned = this.getGlobalState("pinnedApiConfigs") ?? {}
const updatedPinned: Record<string, boolean> = { ...currentPinned }
if (currentPinned[message.text]) {
delete updatedPinned[message.text]
} else {
updatedPinned[message.text] = true
}
await this.updateGlobalState("pinnedApiConfigs", updatedPinned)
await this.postStateToWebview()
}
break
case "enhancementApiConfigId":
await this.updateGlobalState("enhancementApiConfigId", message.text)
await this.postStateToWebview()
@ -1735,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
}
@ -1858,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(
@ -1883,16 +1864,24 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
break
}
await this.configManager.saveConfig(newName, message.apiConfiguration)
await this.configManager.deleteConfig(oldName)
// Load the old configuration to get its ID
const oldConfig = await this.providerSettingsManager.loadConfig(oldName)
const listApiConfig = await this.configManager.listConfig()
const config = listApiConfig?.find((c) => c.name === newName)
// Create a new configuration with the same ID
const newConfig = {
...message.apiConfiguration,
id: oldConfig.id, // Preserve the ID
}
// Save with the new name but same ID
await this.providerSettingsManager.saveConfig(newName, newConfig)
await this.providerSettingsManager.deleteConfig(oldName)
const listApiConfig = await this.providerSettingsManager.listConfig()
// Update listApiConfigMeta first to ensure UI has latest data
await this.updateGlobalState("listApiConfigMeta", listApiConfig)
await Promise.all([this.updateGlobalState("currentApiConfigName", newName)])
await this.updateGlobalState("currentApiConfigName", newName)
await this.postStateToWebview()
} catch (error) {
@ -1906,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),
@ -1924,6 +1913,29 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
}
}
break
case "loadApiConfigurationById":
if (message.text) {
try {
const { config: apiConfig, name } = await this.providerSettingsManager.loadConfigById(
message.text,
)
const listApiConfig = await this.providerSettingsManager.listConfig()
await Promise.all([
this.updateGlobalState("listApiConfigMeta", listApiConfig),
this.updateGlobalState("currentApiConfigName", name),
this.updateApiConfiguration(apiConfig),
])
await this.postStateToWebview()
} catch (error) {
this.outputChannel.appendLine(
`Error load api configuration by ID: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
)
vscode.window.showErrorMessage(t("common:errors.load_api_config"))
}
}
break
case "deleteApiConfiguration":
if (message.text) {
const answer = await vscode.window.showInformationMessage(
@ -1937,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),
@ -1964,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) {
@ -1980,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)
@ -2154,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)
@ -2164,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),
@ -2172,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)
}
}
}
@ -2184,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)
}
}
@ -2392,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),
@ -2419,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
@ -2428,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,
@ -2439,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)
@ -2509,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()
}
@ -2560,6 +2578,7 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
rateLimitSeconds,
currentApiConfigName,
listApiConfigMeta,
pinnedApiConfigs,
mode,
customModePrompts,
customSupportPrompts,
@ -2626,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 ?? {},
@ -2649,107 +2669,31 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
}
}
// 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.filter((key) => isGlobalStateKey(key)).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,
@ -2788,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 ?? {},
@ -2806,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) {
@ -2814,33 +2759,34 @@ 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)
}
@ -2864,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,15 +687,18 @@ 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()
.mockResolvedValue({ config: { apiProvider: "anthropic", id: "new-id" }, name: "new-config" }),
listConfig: jest.fn().mockResolvedValue([{ name: "new-config", id: "new-id", apiProvider: "anthropic" }]),
setModeConfig: jest.fn(),
getModeConfigId: jest.fn().mockResolvedValue(undefined),
@ -712,7 +711,36 @@ 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 as any).providerSettingsManager = {
loadConfigById: jest.fn().mockResolvedValue({
config: { apiProvider: "anthropic", id: "config-id-123" },
name: "config-by-id",
}),
listConfig: jest
.fn()
.mockResolvedValue([{ name: "config-by-id", id: "config-id-123", apiProvider: "anthropic" }]),
setModeConfig: jest.fn(),
getModeConfigId: jest.fn().mockResolvedValue(undefined),
} as any
// First set the mode
await messageHandler({ type: "mode", text: "architect" })
// Then load the config by ID
await messageHandler({ type: "loadApiConfigurationById", text: "config-id-123" })
// Should save new config as default for architect mode
expect(provider.providerSettingsManager.setModeConfig).toHaveBeenCalledWith("architect", "config-id-123")
// Ensure the loadConfigById method was called with the correct ID
expect(provider.providerSettingsManager.loadConfigById).toHaveBeenCalledWith("config-id-123")
})
test("handles browserToolEnabled setting", async () => {
@ -941,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
@ -953,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 () => {
@ -1587,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()
@ -1604,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
@ -1613,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()
@ -1635,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" }))
@ -1648,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: [
@ -1718,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()
@ -1753,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()
@ -1774,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", [
@ -1795,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()
@ -1838,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()
@ -1859,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", [
@ -2073,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"
import { IpcClientMessageType, IpcServerMessageType, IpcServer } from "./ipc"
@ -104,8 +104,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,102 +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"
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

@ -70,7 +70,8 @@
"mcp_server_not_found": "Servidor \"{{serverName}}\" no trobat a la configuració",
"settings_imported": "Configuració importada correctament.",
"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

@ -66,7 +66,8 @@
"mcp_server_not_found": "Server \"{{serverName}}\" nicht in der Konfiguration gefunden",
"settings_imported": "Einstellungen erfolgreich importiert.",
"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

@ -66,7 +66,8 @@
"mcp_server_not_found": "Server \"{{serverName}}\" not found in configuration",
"settings_imported": "Settings imported successfully.",
"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

@ -66,7 +66,8 @@
"mcp_server_not_found": "Servidor \"{{serverName}}\" no encontrado en la configuración",
"settings_imported": "Configuración importada correctamente.",
"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

@ -66,7 +66,8 @@
"mcp_server_not_found": "Serveur \"{{serverName}}\" introuvable dans la configuration",
"settings_imported": "Paramètres importés avec succès.",
"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

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

View file

@ -66,7 +66,8 @@
"mcp_server_not_found": "Server \"{{serverName}}\" non trovato nella configurazione",
"settings_imported": "Impostazioni importate con successo.",
"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

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

View file

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

View file

@ -66,7 +66,8 @@
"mcp_server_not_found": "Serwer \"{{serverName}}\" nie znaleziony w konfiguracji",
"settings_imported": "Ustawienia zostały pomyślnie zaimportowane.",
"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

@ -70,7 +70,8 @@
"mcp_server_not_found": "Servidor \"{{serverName}}\" não encontrado na configuração",
"settings_imported": "Configurações importadas com sucesso.",
"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

@ -66,7 +66,8 @@
"mcp_server_not_found": "Yapılandırmada \"{{serverName}}\" sunucusu bulunamadı",
"settings_imported": "Ayarlar başarıyla içe aktarıldı.",
"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

@ -66,7 +66,8 @@
"mcp_server_not_found": "Không tìm thấy máy chủ \"{{serverName}}\" trong cấu hình",
"settings_imported": "Cài đặt đã được nhập thành công.",
"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

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

View file

@ -66,7 +66,8 @@
"mcp_server_not_found": "在配置中未找到服務器\"{{serverName}}\"",
"settings_imported": "設置已成功導入。",
"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
@ -58,6 +59,7 @@ export interface ExtensionMessage {
| "ttsStop"
| "maxReadFileLine"
| "fileSearchResults"
| "toggleApiConfigPin"
text?: string
action?:
| "chatButtonClicked"
@ -102,73 +104,95 @@ 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"
maxReadFileLine: number // Maximum number of lines to read from a file before truncating
settingsImportedAt?: number
}

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

@ -17,6 +17,7 @@ export interface WebviewMessage {
| "upsertApiConfiguration"
| "deleteApiConfiguration"
| "loadApiConfiguration"
| "loadApiConfigurationById"
| "renameApiConfiguration"
| "getListApiConfiguration"
| "customInstructions"
@ -119,6 +120,7 @@ export interface WebviewMessage {
| "language"
| "maxReadFileLine"
| "searchFiles"
| "toggleApiConfigPin"
text?: string
disabled?: boolean
askResponse?: ClineAskResponse

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,296 +1,10 @@
import * as vscode from "vscode"
import { z } from "zod"
import { ModelInfo, ProviderName, ProviderSettings } from "../exports/roo-code"
import { AssertEqual, Equals } from "../utils/type-fu"
export type { ModelInfo, ProviderName as ApiProvider }
// Models
export type ApiHandlerOptions = Omit<ProviderSettings, "apiProvider" | "id">
export 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(),
})
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
}
// Throws a type error if the inferred type of the schema is not equal to the
// type of the ModelInfo.
type _AssertModelInfoMatchesSchema = AssertEqual<Equals<ModelInfo, z.infer<typeof modelInfoSchema>>>
// Providers
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 interface ApiHandlerOptions {
apiModelId?: string
// Anthropic
apiKey?: string
anthropicBaseUrl?: string
// Glama
glamaModelId?: string
glamaModelInfo?: ModelInfo
glamaApiKey?: string
// OpenRouter
openRouterApiKey?: string
openRouterModelId?: string
openRouterModelInfo?: ModelInfo
openRouterBaseUrl?: string
openRouterSpecificProvider?: string
// AWS Bedrock
awsAccessKey?: string
awsSecretKey?: string
awsSessionToken?: string
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
openAiBaseUrl?: string
openAiApiKey?: string
openAiR1FormatEnabled?: boolean
openAiModelId?: string
openAiCustomModelInfo?: ModelInfo
openAiUseAzure?: 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
googleGeminiBaseUrl?: string
// OpenAI Native
openAiNativeApiKey?: string
// Mistral
mistralApiKey?: string
mistralCodestralUrl?: string // New option for Codestral URL.
// Azure
azureApiVersion?: string
// OpenRouter
openRouterUseMiddleOutTransform?: boolean
openAiStreamingEnabled?: boolean
// DeepSeek
deepSeekBaseUrl?: string
deepSeekApiKey?: string
includeMaxTokens?: boolean
// Unbound
unboundApiKey?: string
unboundModelId?: string
unboundModelInfo?: ModelInfo
// Requesty
requestyApiKey?: string
requestyModelId?: string
requestyModelInfo?: ModelInfo
// Claude 3.7 Sonnet Thinking
modelTemperature?: number | null
modelMaxTokens?: number
modelMaxThinkingTokens?: number
// Fake AI
fakeAi?: unknown
}
export type ApiHandlerOptionsKey = keyof ApiHandlerOptions
export const apiHandlerOptionsSchema = z.object({
// 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(),
includeMaxTokens: z.boolean().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(),
// Fake AI
fakeAi: z.unknown().optional(),
})
// Throws a type error if the inferred type of the schema is not equal to the
// type of the ApiHandlerOptions.
type _AssertApiHandlerOptionsMatchesSchema = AssertEqual<
Equals<ApiHandlerOptions, z.infer<typeof apiHandlerOptionsSchema>>
>
export type ApiConfiguration = ApiHandlerOptions & {
apiProvider?: ApiProvider
id?: string // Stable unique identifier.
}
// Define API configuration keys for dynamic object building.
export const API_CONFIG_KEYS: ApiHandlerOptionsKey[] = [
"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",
]
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,169 +1,695 @@
import { z } from "zod"
import type { SecretKey, GlobalStateKey, ConfigurationKey, ConfigurationValues } from "../exports/roo-code"
import type {
ProviderName,
CheckpointStorage,
ToolGroup,
Language,
TelemetrySetting,
ProviderSettingsKey,
SecretStateKey,
GlobalStateKey,
ModelInfo,
ApiConfigMeta,
HistoryItem,
GroupEntry,
ModeConfig,
ExperimentId,
ProviderSettings,
GlobalSettings,
} from "../exports/roo-code"
export type { SecretKey, GlobalStateKey, ConfigurationKey, ConfigurationValues }
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
*/
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,
}
const PROVIDER_NAMES = Object.keys(providerNames) as ProviderName[]
const providerNamesEnum: [ProviderName, ...ProviderName[]] = [
PROVIDER_NAMES[0],
...PROVIDER_NAMES.slice(1).map((p) => p),
]
/**
* CheckpointStorage
*/
const checkpointStorages: Record<CheckpointStorage, true> = {
task: true,
workspace: true,
}
const CHECKPOINT_STORAGES = Object.keys(checkpointStorages) as CheckpointStorage[]
const checkpointStoragesEnum: [CheckpointStorage, ...CheckpointStorage[]] = [
CHECKPOINT_STORAGES[0],
...CHECKPOINT_STORAGES.slice(1).map((p) => p),
]
/**
* ToolGroup
*/
const toolGroups: Record<ToolGroup, true> = {
read: true,
edit: true,
browser: true,
command: true,
mcp: true,
modes: true,
}
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
*/
/**
* Secret keys.
* ModelInfo
*/
export const SECRET_KEYS = [
"apiKey",
"glamaApiKey",
"openRouterApiKey",
"awsAccessKey",
"awsSecretKey",
"awsSessionToken",
"openAiApiKey",
"geminiApiKey",
"openAiNativeApiKey",
"deepSeekApiKey",
"mistralApiKey",
"unboundApiKey",
"requestyApiKey",
] as const
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 we're missing a key.
type CheckSecretKeysExhaustiveness = Exclude<SecretKey, (typeof SECRET_KEYS)[number]> extends never ? true : false
const _checkSecretKeysExhaustiveness: CheckSecretKeysExhaustiveness = true
export const isSecretKey = (key: string): key is SecretKey => SECRET_KEYS.includes(key as SecretKey)
// TODO: Replace `z.unknown()` with the actual types.
export const secretStateSchema = z.record(z.enum(SECRET_KEYS), z.unknown())
// 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>>>
/**
* Global state keys.
* ApiConfigMeta
*/
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",
] as const
const apiConfigMetaSchema = z.object({
id: z.string(),
name: z.string(),
apiProvider: z.enum(providerNamesEnum).optional(),
})
// Throws a type error we're missing a key.
type CheckGlobalStateKeysExhaustiveness =
Exclude<GlobalStateKey, (typeof GLOBAL_STATE_KEYS)[number]> extends never ? true : false
const _checkGlobalStateKeysExhaustiveness: CheckGlobalStateKeysExhaustiveness = true
export const isGlobalStateKey = (key: string): key is GlobalStateKey =>
GLOBAL_STATE_KEYS.includes(key as GlobalStateKey)
// TODO: Replace `z.unknown()` with the actual types.
export const globalStateSchema = z.record(z.enum(GLOBAL_STATE_KEYS), z.unknown())
type _AssertApiConfigMeta = AssertEqual<Equals<ApiConfigMeta, z.infer<typeof apiConfigMetaSchema>>>
/**
* Pass-through state keys.
* TODO: What are these?
* HistoryItem
*/
export const PASS_THROUGH_STATE_KEYS = ["taskHistory"] as const
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(),
})
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])
type _AssertHistoryItem = AssertEqual<Equals<HistoryItem, z.infer<typeof historyItemSchema>>>
// TODO: Replace `z.unknown()` with the actual types.
export const passThroughStateSchema = z.record(z.enum(PASS_THROUGH_STATE_KEYS), z.unknown())
/**
* 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",

View file

@ -1,3 +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

@ -24,7 +24,7 @@ import { SelectDropdown, DropdownOptionType, Button } from "@/components/ui"
import Thumbnails from "../common/Thumbnails"
import { MAX_IMAGES_PER_MESSAGE } from "./ChatView"
import ContextMenu from "./ContextMenu"
import { VolumeX } from "lucide-react"
import { VolumeX, Pin, Check } from "lucide-react"
import { IconButton } from "./IconButton"
import { cn } from "@/lib/utils"
@ -64,7 +64,26 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
ref,
) => {
const { t } = useAppTranslation()
const { filePaths, openedTabs, currentApiConfigName, listApiConfigMeta, customModes, cwd } = useExtensionState()
const {
filePaths,
openedTabs,
currentApiConfigName,
listApiConfigMeta,
customModes,
cwd,
pinnedApiConfigs,
togglePinnedApiConfig,
} = useExtensionState()
// Find the ID and display text for the currently selected API configuration
const { currentConfigId, displayName } = useMemo(() => {
const currentConfig = listApiConfigMeta?.find((config) => config.name === currentApiConfigName)
return {
currentConfigId: currentConfig?.id || "",
displayName: currentApiConfigName || "", // Use the name directly for display
}
}, [listApiConfigMeta, currentApiConfigName])
const [gitCommits, setGitCommits] = useState<any[]>([])
const [showDropdown, setShowDropdown] = useState(false)
const [fileSearchResults, setFileSearchResults] = useState<SearchResult[]>([])
@ -955,15 +974,45 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
{/* API configuration selector - flexible width */}
<div className={cn("flex-1", "min-w-0", "overflow-hidden")}>
<SelectDropdown
value={currentApiConfigName || ""}
value={currentConfigId}
disabled={textAreaDisabled}
title={t("chat:selectApiConfig")}
placeholder={displayName} // Always show the current name
options={[
...(listApiConfigMeta || []).map((config) => ({
value: config.name,
label: config.name,
type: DropdownOptionType.ITEM,
})),
// Pinned items first
...(listApiConfigMeta || [])
.filter((config) => pinnedApiConfigs && pinnedApiConfigs[config.id])
.map((config) => ({
value: config.id,
label: config.name,
name: config.name, // Keep name for comparison with currentApiConfigName
type: DropdownOptionType.ITEM,
pinned: true,
}))
.sort((a, b) => a.label.localeCompare(b.label)),
// If we have pinned items and unpinned items, add a separator
...(pinnedApiConfigs &&
Object.keys(pinnedApiConfigs).length > 0 &&
(listApiConfigMeta || []).some((config) => !pinnedApiConfigs[config.id])
? [
{
value: "sep-pinned",
label: t("chat:separator"),
type: DropdownOptionType.SEPARATOR,
},
]
: []),
// Unpinned items sorted alphabetically
...(listApiConfigMeta || [])
.filter((config) => !pinnedApiConfigs || !pinnedApiConfigs[config.id])
.map((config) => ({
value: config.id,
label: config.name,
name: config.name, // Keep name for comparison with currentApiConfigName
type: DropdownOptionType.ITEM,
pinned: false,
}))
.sort((a, b) => a.label.localeCompare(b.label)),
{
value: "sep-2",
label: t("chat:separator"),
@ -975,9 +1024,54 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
type: DropdownOptionType.ACTION,
},
]}
onChange={(value) => vscode.postMessage({ type: "loadApiConfiguration", text: value })}
onChange={(value) => {
if (value === "settingsButtonClicked") {
vscode.postMessage({ type: "loadApiConfiguration", text: value })
} else {
vscode.postMessage({ type: "loadApiConfigurationById", text: value })
}
}}
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
}
const config = listApiConfigMeta?.find((c) => c.id === value)
const isCurrentConfig = config?.name === currentApiConfigName
return (
<div className="flex justify-between gap-2 w-full h-5">
<div className={cn({ "font-medium": isCurrentConfig })}>{label}</div>
<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"
title={pinned ? t("chat:unpin") : t("chat:pin")}
onClick={(e) => {
e.stopPropagation()
togglePinnedApiConfig(value)
vscode.postMessage({ type: "toggleApiConfigPin", text: value })
}}
className={cn("size-5", {
"hidden group-hover:flex": !pinned,
"bg-accent": pinned,
})}>
<Pin className="size-3 p-0.5 opacity-50" />
</Button>
</div>
</div>
)
}}
/>
</div>
</div>

View file

@ -9,7 +9,7 @@ import { TelemetrySetting } from "../../../../src/shared/TelemetrySetting"
import { vscode } from "@/utils/vscode"
import { cn } from "@/lib/utils"
import { Button, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui"
import { Button } from "@/components/ui"
import { SectionHeader } from "./SectionHeader"
import { Section } from "./Section"
@ -59,45 +59,17 @@ export const About = ({ version, telemetrySetting, setTelemetrySetting, classNam
</div>
<div className="flex items-center gap-2 mt-2">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button>
<Download className="p-0.5" />
Import
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem
onClick={() => vscode.postMessage({ type: "importSettings", text: "provider" })}>
Current Provider Settings
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => vscode.postMessage({ type: "importSettings", text: "global" })}>
Global Settings
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button>
<Upload className="p-0.5" />
Export
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem
onClick={() => vscode.postMessage({ type: "exportSettings", text: "provider" })}>
Current Provider Settings
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => vscode.postMessage({ type: "exportSettings", text: "global" })}>
Global Settings
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<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.reset.button")}
{t("settings:footer.settings.reset")}
</Button>
</div>
</Section>

View file

@ -47,6 +47,7 @@ import {
OPENROUTER_DEFAULT_PROVIDER_NAME,
} from "@/components/ui/hooks/useOpenRouterModelProviders"
import { useOpenRouterKeyInfo } from "@/components/ui/hooks/useOpenRouterKeyInfo"
import { useRequestyKeyInfo } from "@/components/ui/hooks/useRequestyKeyInfo"
import { MODELS_BY_PROVIDER, PROVIDERS, AWS_REGIONS, VERTEX_REGIONS } from "./constants"
import { VSCodeButtonLink } from "../common/VSCodeButtonLink"
import { ModelInfoView } from "./ModelInfoView"
@ -74,6 +75,24 @@ const OpenRouterBalanceDisplay = ({ apiKey, baseUrl }: { apiKey: string; baseUrl
)
}
const RequestyBalanceDisplay = ({ apiKey }: { apiKey: string }) => {
const { data: keyInfo } = useRequestyKeyInfo(apiKey)
if (!keyInfo) {
return null
}
// Parse the balance to a number and format it to 2 decimal places
const balance = parseFloat(keyInfo.org_balance)
const formattedBalance = balance.toFixed(2)
return (
<VSCodeLink href="https://app.requesty.ai/settings" className="text-vscode-foreground hover:underline">
${formattedBalance}
</VSCodeLink>
)
}
interface ApiOptionsProps {
uriScheme: string | undefined
apiConfiguration: ApiConfiguration
@ -427,7 +446,12 @@ const ApiOptions = ({
onInput={handleInputChange("requestyApiKey")}
placeholder={t("settings:providers.getRequestyApiKey")}
className="w-full">
<label className="block font-medium mb-1">{t("settings:providers.requestyApiKey")}</label>
<div className="flex justify-between items-center mb-1">
<label className="block font-medium">{t("settings:providers.requestyApiKey")}</label>
{apiConfiguration?.requestyApiKey && (
<RequestyBalanceDisplay apiKey={apiConfiguration.requestyApiKey} />
)}
</div>
</VSCodeTextField>
<div className="text-sm text-vscode-descriptionForeground -mt-2">
{t("settings:providers.apiKeyStorageNotice")}

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

@ -151,6 +151,7 @@ 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 }))
@ -462,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

@ -0,0 +1,50 @@
import axios from "axios"
import { z } from "zod"
import { useQuery, UseQueryOptions } from "@tanstack/react-query"
const requestyKeyInfoSchema = z.object({
name: z.string(),
monthly_limit: z.string(),
monthly_spend: z.string(),
org_balance: z.string(),
config: z.object({
aliases: z.record(z.string(), z.any()).optional(),
}),
})
export type RequestyKeyInfo = z.infer<typeof requestyKeyInfoSchema>
async function getRequestyKeyInfo(apiKey?: string) {
if (!apiKey) return null
try {
const response = await axios.get("https://api.requesty.ai/x/apikey", {
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
})
const result = requestyKeyInfoSchema.safeParse(response.data)
if (!result.success) {
console.error("Requesty API key info validation failed:", result.error)
return null
}
return result.data
} catch (error) {
console.error("Error fetching Requesty key info:", error)
return null
}
}
type UseRequestyKeyInfoOptions = Omit<UseQueryOptions<RequestyKeyInfo | null>, "queryKey" | "queryFn">
export const useRequestyKeyInfo = (apiKey?: string, options?: UseRequestyKeyInfoOptions) => {
return useQuery<RequestyKeyInfo | null>({
queryKey: ["requesty-key-info", apiKey],
queryFn: () => getRequestyKeyInfo(apiKey),
staleTime: 30 * 1000, // 30 seconds
enabled: !!apiKey,
...options,
})
}

View file

@ -25,6 +25,7 @@ export interface DropdownOption {
label: string
disabled?: boolean
type?: DropdownOptionType
pinned?: boolean
}
export interface SelectDropdownProps {
@ -35,10 +36,12 @@ export interface SelectDropdownProps {
title?: string
triggerClassName?: string
contentClassName?: string
itemClassName?: string
sideOffset?: number
align?: "start" | "center" | "end"
placeholder?: string
shortcutText?: string
renderItem?: (option: DropdownOption) => React.ReactNode
}
export const SelectDropdown = React.forwardRef<React.ElementRef<typeof DropdownMenuTrigger>, SelectDropdownProps>(
@ -51,18 +54,22 @@ export const SelectDropdown = React.forwardRef<React.ElementRef<typeof DropdownM
title = "",
triggerClassName = "",
contentClassName = "",
itemClassName = "",
sideOffset = 4,
align = "start",
placeholder = "",
shortcutText = "",
renderItem,
},
ref,
) => {
const [open, setOpen] = React.useState(false)
const portalContainer = useRooPortal("roo-portal")
// If the selected option isn't in the list yet, but we have a placeholder, prioritize showing the placeholder
const selectedOption = options.find((option) => option.value === value)
const displayText = selectedOption?.label || placeholder || ""
const displayText =
value && !selectedOption && placeholder ? placeholder : selectedOption?.label || placeholder || ""
const handleSelect = (option: DropdownOption) => {
if (option.type === DropdownOptionType.ACTION) {
@ -120,12 +127,19 @@ export const SelectDropdown = React.forwardRef<React.ElementRef<typeof DropdownM
<DropdownMenuItem
key={`item-${option.value}`}
disabled={option.disabled}
onClick={() => handleSelect(option)}>
{option.label}
{option.value === value && (
<DropdownMenuShortcut>
<Check className="size-4 p-0.5" />
</DropdownMenuShortcut>
onClick={() => handleSelect(option)}
className={itemClassName}>
{renderItem ? (
renderItem(option)
) : (
<>
{option.label}
{option.value === value && (
<DropdownMenuShortcut>
<Check className="size-4 p-0.5" />
</DropdownMenuShortcut>
)}
</>
)}
</DropdownMenuItem>
)

View file

@ -81,6 +81,9 @@ export interface ExtensionStateContextType extends ExtensionState {
maxReadFileLine: number
setMaxReadFileLine: (value: number) => void
machineId?: string
pinnedApiConfigs?: Record<string, boolean>
setPinnedApiConfigs: (value: Record<string, boolean>) => void
togglePinnedApiConfig: (configName: string) => void
}
export const ExtensionStateContext = createContext<ExtensionStateContextType | undefined>(undefined)
@ -155,6 +158,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
showRooIgnoredFiles: true, // Default to showing .rooignore'd files with lock symbol (current behavior).
renderContext: "sidebar",
maxReadFileLine: 500, // Default max read file line limit
pinnedApiConfigs: {}, // Empty object for pinned API configs
})
const [didHydrateState, setDidHydrateState] = useState(false)
@ -302,6 +306,22 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
setShowRooIgnoredFiles: (value) => setState((prevState) => ({ ...prevState, showRooIgnoredFiles: value })),
setRemoteBrowserEnabled: (value) => setState((prevState) => ({ ...prevState, remoteBrowserEnabled: value })),
setMaxReadFileLine: (value) => setState((prevState) => ({ ...prevState, maxReadFileLine: value })),
setPinnedApiConfigs: (value) => setState((prevState) => ({ ...prevState, pinnedApiConfigs: value })),
togglePinnedApiConfig: (configId) =>
setState((prevState) => {
const currentPinned = prevState.pinnedApiConfigs || {}
const newPinned = {
...currentPinned,
[configId]: !currentPinned[configId],
}
// If the config is now unpinned, remove it from the object
if (!newPinned[configId]) {
delete newPinned[configId]
}
return { ...prevState, pinnedApiConfigs: newPinned }
}),
}
return <ExtensionStateContext.Provider value={contextValue}>{children}</ExtensionStateContext.Provider>

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": {