mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-05 08:10:14 +00:00
* Fix code index secret persistence with async VSCode storage - Add async secret methods to CodeIndexConfigManager - Implement direct VSCode secret storage access bypassing ContextProxy cache - Update loadConfiguration to use async secret loading - Modify webview message handler to use new async secret storage - Add public secret methods to CodeIndexManager - Enhance debugging throughout secret flow This fixes the issue where API keys were saved but not loaded immediately into services due to ContextProxy cache synchronization issues. * Fix code index secret persistence and test failures - Add async secret handling to CodeIndexConfigManager with new methods: - getSecretAsync(), storeSecretAsync() for individual secrets - loadSecretsAsync(), storeSecretsAsync() for batch operations - Update doesConfigChangeRequireRestart() to check OpenAI Compatible modelDimension changes - Fix all failing tests by using setupSecretMocks() helper consistently - Update manager.spec.ts to properly mock _recreateServices to avoid real service creation This ensures API keys and other secrets are properly loaded from VSCode's async secret storage and that configuration changes requiring service restart are correctly detected. * feat: improve code index settings secret handling in UI - Show placeholder dots (••••••••••••••••) in password fields when secrets are already set - Only send modified secret fields to prevent overwriting existing secrets with empty values - Track which fields have been modified by the user - Add requestCodeIndexSecretStatus message handler to check if secrets exist - Fix console.log to handle empty string keys without errors - Ensure changing one setting doesn't clear other unmodified secrets * refactor: disconnect code index from unified settings system - Rename handleExternalSettingsChange to handleSettingsChange for clarity - Remove handleSettingsChange call from ClineProvider (not related to code index) - Remove codebaseIndexConfig from general settings save in SettingsView - Delete unused codebaseIndexConfig message handler - Remove codebaseIndexConfig from WebviewMessage type definition - Code index settings are now fully independent with their own dedicated UI * feat: separate code index enable/disable from indexing settings - Move 'Enable codebase indexing' toggle to global settings in Experimental section - Keep indexing-specific settings (API keys, URLs, models) in dedicated Code Index Settings component - Add codebaseIndexEnabled handler to webview message handler - Update translations with new settings title and disabled message - Ensure code index service properly responds to enable/disable changes - Maintain backward compatibility with existing codebaseIndexConfig structure * refactor: remove ContextProxy.getVSCodeContext() and pass ExtensionContext directly - Updated CodeIndexConfigManager to accept vscode.ExtensionContext in constructor - Modified CodeIndexManager to pass context directly to CodeIndexConfigManager - Updated webviewMessageHandler to use provider.context.secrets directly - Removed getVSCodeContext() method from ContextProxy - Updated all related tests to reflect these changes - Fixed CodeIndexSettings webview tests after UI changes * refactor: streamline secret handling by removing async methods and utilizing ContextProxy directly * feat: translations and popover component * refactor: simplify test mocks and improve checkbox handling in CodeIndexSettings tests * refactor: remove debug logging from CodeIndexConfigManager, CodeIndexManager, CodeIndexServiceFactory, and QdrantVectorStore * fix: merge missing translation keys from main after rebase - Add advancedConfigLabel, searchMinScoreLabel, searchMinScoreDescription, searchMinScoreResetTooltip keys - Update startIndexingButton and clearIndexDataButton labels to match main - Preserve all CodeIndexPopover translations added in this PR * Revert "fix: merge missing translation keys from main after rebase" This reverts commit beb1de4924ac1475731fcd06d994ddb96eb1e5fd. * fix: add missing translation keys from main branch after rebase - Added codeIndex.advancedConfigLabel - Added codeIndex.searchMinScoreLabel - Added codeIndex.searchMinScoreDescription - Added codeIndex.searchMinScoreResetTooltip These keys exist on main but were missing from non-English locales after rebase. * fix: remove clickIndicatorMessage, fix toggle message type, and clean up translation key inconsistencies * refactor: streamline settings management in CodeIndexPopover and improve secret handling * refactor: remove debug logging from configuration checks in CodeIndexConfigManager and webviewMessageHandler * fix: translations * refactor: remove CodeIndexSettings component and associated tests
313 lines
8.7 KiB
TypeScript
313 lines
8.7 KiB
TypeScript
import * as vscode from "vscode"
|
|
import { ZodError } from "zod"
|
|
|
|
import {
|
|
PROVIDER_SETTINGS_KEYS,
|
|
GLOBAL_SETTINGS_KEYS,
|
|
SECRET_STATE_KEYS,
|
|
GLOBAL_STATE_KEYS,
|
|
type ProviderSettings,
|
|
type GlobalSettings,
|
|
type SecretState,
|
|
type GlobalState,
|
|
type RooCodeSettings,
|
|
providerSettingsSchema,
|
|
globalSettingsSchema,
|
|
isSecretStateKey,
|
|
} from "@roo-code/types"
|
|
import { TelemetryService } from "@roo-code/telemetry"
|
|
|
|
import { logger } from "../../utils/logging"
|
|
|
|
type GlobalStateKey = keyof GlobalState
|
|
type SecretStateKey = keyof SecretState
|
|
type RooCodeSettingsKey = keyof RooCodeSettings
|
|
|
|
const PASS_THROUGH_STATE_KEYS = ["taskHistory"]
|
|
|
|
export const isPassThroughStateKey = (key: string) => PASS_THROUGH_STATE_KEYS.includes(key)
|
|
|
|
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 {
|
|
// Revert to original assignment
|
|
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)
|
|
}
|
|
|
|
/**
|
|
* Refresh secrets from storage and update cache
|
|
* This is useful when you need to ensure the cache has the latest values
|
|
*/
|
|
async refreshSecrets(): Promise<void> {
|
|
const promises = SECRET_STATE_KEYS.map(async (key) => {
|
|
try {
|
|
this.secretCache[key] = await this.originalContext.secrets.get(key)
|
|
} catch (error) {
|
|
logger.error(
|
|
`Error refreshing secret ${key}: ${error instanceof Error ? error.message : String(error)}`,
|
|
)
|
|
}
|
|
})
|
|
await Promise.all(promises)
|
|
}
|
|
|
|
private getAllSecretState(): SecretState {
|
|
return Object.fromEntries(SECRET_STATE_KEYS.map((key) => [key, this.getSecret(key)]))
|
|
}
|
|
|
|
/**
|
|
* GlobalSettings
|
|
*/
|
|
|
|
public getGlobalSettings(): GlobalSettings {
|
|
const values = this.getValues()
|
|
|
|
try {
|
|
return globalSettingsSchema.parse(values)
|
|
} catch (error) {
|
|
if (error instanceof ZodError) {
|
|
TelemetryService.instance.captureSchemaValidationError({ schemaName: "GlobalSettings", error })
|
|
}
|
|
|
|
return GLOBAL_SETTINGS_KEYS.reduce((acc, key) => ({ ...acc, [key]: values[key] }), {} as GlobalSettings)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* ProviderSettings
|
|
*/
|
|
|
|
public getProviderSettings(): ProviderSettings {
|
|
const values = this.getValues()
|
|
|
|
try {
|
|
return providerSettingsSchema.parse(values)
|
|
} catch (error) {
|
|
if (error instanceof ZodError) {
|
|
TelemetryService.instance.captureSchemaValidationError({ schemaName: "ProviderSettings", error })
|
|
}
|
|
|
|
return PROVIDER_SETTINGS_KEYS.reduce((acc, key) => ({ ...acc, [key]: values[key] }), {} as ProviderSettings)
|
|
}
|
|
}
|
|
|
|
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.
|
|
|
|
// Ensure openAiHeaders is always an object even when empty
|
|
// This is critical for proper serialization/deserialization through IPC
|
|
if (values.openAiHeaders !== undefined) {
|
|
// Check if it's empty or null
|
|
if (!values.openAiHeaders || Object.keys(values.openAiHeaders).length === 0) {
|
|
values.openAiHeaders = {}
|
|
}
|
|
}
|
|
|
|
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())
|
|
|
|
// Exports should only contain global settings, so this skips project custom modes (those exist in the .roomode folder)
|
|
globalSettings.customModes = globalSettings.customModes?.filter((mode) => mode.source === "global")
|
|
|
|
return Object.fromEntries(Object.entries(globalSettings).filter(([_, value]) => value !== undefined))
|
|
} catch (error) {
|
|
if (error instanceof ZodError) {
|
|
TelemetryService.instance.captureSchemaValidationError({ schemaName: "GlobalSettings", 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
|
|
*/
|
|
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()
|
|
}
|
|
|
|
private static _instance: ContextProxy | null = null
|
|
|
|
static get instance() {
|
|
if (!this._instance) {
|
|
throw new Error("ContextProxy not initialized")
|
|
}
|
|
|
|
return this._instance
|
|
}
|
|
|
|
static async getInstance(context: vscode.ExtensionContext) {
|
|
if (this._instance) {
|
|
return this._instance
|
|
}
|
|
|
|
this._instance = new ContextProxy(context)
|
|
await this._instance.initialize()
|
|
|
|
return this._instance
|
|
}
|
|
}
|