Roo-Code/src/services/code-index/manager.ts
Daniel e508eaf3df
Fix code index secret persistence and improve settings UX (#5158)
* 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
2025-07-03 18:33:20 -04:00

282 lines
8.2 KiB
TypeScript

import * as vscode from "vscode"
import { getWorkspacePath } from "../../utils/path"
import { ContextProxy } from "../../core/config/ContextProxy"
import { VectorStoreSearchResult } from "./interfaces"
import { IndexingState } from "./interfaces/manager"
import { CodeIndexConfigManager } from "./config-manager"
import { CodeIndexStateManager } from "./state-manager"
import { CodeIndexServiceFactory } from "./service-factory"
import { CodeIndexSearchService } from "./search-service"
import { CodeIndexOrchestrator } from "./orchestrator"
import { CacheManager } from "./cache-manager"
import fs from "fs/promises"
import ignore from "ignore"
import path from "path"
export class CodeIndexManager {
// --- Singleton Implementation ---
private static instances = new Map<string, CodeIndexManager>() // Map workspace path to instance
// Specialized class instances
private _configManager: CodeIndexConfigManager | undefined
private readonly _stateManager: CodeIndexStateManager
private _serviceFactory: CodeIndexServiceFactory | undefined
private _orchestrator: CodeIndexOrchestrator | undefined
private _searchService: CodeIndexSearchService | undefined
private _cacheManager: CacheManager | undefined
public static getInstance(context: vscode.ExtensionContext): CodeIndexManager | undefined {
const workspacePath = getWorkspacePath() // Assumes single workspace for now
if (!workspacePath) {
return undefined
}
if (!CodeIndexManager.instances.has(workspacePath)) {
CodeIndexManager.instances.set(workspacePath, new CodeIndexManager(workspacePath, context))
}
return CodeIndexManager.instances.get(workspacePath)!
}
public static disposeAll(): void {
for (const instance of CodeIndexManager.instances.values()) {
instance.dispose()
}
CodeIndexManager.instances.clear()
}
private readonly workspacePath: string
private readonly context: vscode.ExtensionContext
// Private constructor for singleton pattern
private constructor(workspacePath: string, context: vscode.ExtensionContext) {
this.workspacePath = workspacePath
this.context = context
this._stateManager = new CodeIndexStateManager()
}
// --- Public API ---
public get onProgressUpdate() {
return this._stateManager.onProgressUpdate
}
private assertInitialized() {
if (!this._configManager || !this._orchestrator || !this._searchService || !this._cacheManager) {
throw new Error("CodeIndexManager not initialized. Call initialize() first.")
}
}
public get state(): IndexingState {
if (!this.isFeatureEnabled) {
return "Standby"
}
this.assertInitialized()
return this._orchestrator!.state
}
public get isFeatureEnabled(): boolean {
return this._configManager?.isFeatureEnabled ?? false
}
public get isFeatureConfigured(): boolean {
return this._configManager?.isFeatureConfigured ?? false
}
public get isInitialized(): boolean {
try {
this.assertInitialized()
return true
} catch (error) {
return false
}
}
/**
* Initializes the manager with configuration and dependent services.
* Must be called before using any other methods.
* @returns Object indicating if a restart is needed
*/
public async initialize(contextProxy: ContextProxy): Promise<{ requiresRestart: boolean }> {
// 1. ConfigManager Initialization and Configuration Loading
if (!this._configManager) {
this._configManager = new CodeIndexConfigManager(contextProxy)
}
// Load configuration once to get current state and restart requirements
const { requiresRestart } = await this._configManager.loadConfiguration()
// 2. Check if feature is enabled
if (!this.isFeatureEnabled) {
if (this._orchestrator) {
this._orchestrator.stopWatcher()
}
return { requiresRestart }
}
// 3. CacheManager Initialization
if (!this._cacheManager) {
this._cacheManager = new CacheManager(this.context, this.workspacePath)
await this._cacheManager.initialize()
}
// 4. Determine if Core Services Need Recreation
const needsServiceRecreation = !this._serviceFactory || requiresRestart
if (needsServiceRecreation) {
await this._recreateServices()
}
// 5. Handle Indexing Start/Restart
// The enhanced vectorStore.initialize() in startIndexing() now handles dimension changes automatically
// by detecting incompatible collections and recreating them, so we rely on that for dimension changes
const shouldStartOrRestartIndexing =
requiresRestart ||
(needsServiceRecreation && (!this._orchestrator || this._orchestrator.state !== "Indexing"))
if (shouldStartOrRestartIndexing) {
this._orchestrator?.startIndexing() // This method is async, but we don't await it here
}
return { requiresRestart }
}
/**
* Initiates the indexing process (initial scan and starts watcher).
*/
public async startIndexing(): Promise<void> {
if (!this.isFeatureEnabled) {
return
}
this.assertInitialized()
await this._orchestrator!.startIndexing()
}
/**
* Stops the file watcher and potentially cleans up resources.
*/
public stopWatcher(): void {
if (!this.isFeatureEnabled) {
return
}
if (this._orchestrator) {
this._orchestrator.stopWatcher()
}
}
/**
* Cleans up the manager instance.
*/
public dispose(): void {
if (this._orchestrator) {
this.stopWatcher()
}
this._stateManager.dispose()
}
/**
* Clears all index data by stopping the watcher, clearing the Qdrant collection,
* and deleting the cache file.
*/
public async clearIndexData(): Promise<void> {
if (!this.isFeatureEnabled) {
return
}
this.assertInitialized()
await this._orchestrator!.clearIndexData()
await this._cacheManager!.clearCacheFile()
}
// --- Private Helpers ---
public getCurrentStatus() {
return this._stateManager.getCurrentStatus()
}
public async searchIndex(query: string, directoryPrefix?: string): Promise<VectorStoreSearchResult[]> {
if (!this.isFeatureEnabled) {
return []
}
this.assertInitialized()
return this._searchService!.searchIndex(query, directoryPrefix)
}
/**
* Private helper method to recreate services with current configuration.
* Used by both initialize() and handleSettingsChange().
*/
private async _recreateServices(): Promise<void> {
// Stop watcher if it exists
if (this._orchestrator) {
this.stopWatcher()
}
// (Re)Initialize service factory
this._serviceFactory = new CodeIndexServiceFactory(
this._configManager!,
this.workspacePath,
this._cacheManager!,
)
const ignoreInstance = ignore()
const ignorePath = path.join(getWorkspacePath(), ".gitignore")
try {
const content = await fs.readFile(ignorePath, "utf8")
ignoreInstance.add(content)
ignoreInstance.add(".gitignore")
} catch (error) {
// Should never happen: reading file failed even though it exists
console.error("Unexpected error loading .gitignore:", error)
}
// (Re)Create shared service instances
const { embedder, vectorStore, scanner, fileWatcher } = this._serviceFactory.createServices(
this.context,
this._cacheManager!,
ignoreInstance,
)
// (Re)Initialize orchestrator
this._orchestrator = new CodeIndexOrchestrator(
this._configManager!,
this._stateManager,
this.workspacePath,
this._cacheManager!,
vectorStore,
scanner,
fileWatcher,
)
// (Re)Initialize search service
this._searchService = new CodeIndexSearchService(
this._configManager!,
this._stateManager,
embedder,
vectorStore,
)
}
/**
* Handle code index settings changes.
* This method should be called when code index settings are updated
* to ensure the CodeIndexConfigManager picks up the new configuration.
* If the configuration changes require a restart, the service will be restarted.
*/
public async handleSettingsChange(): Promise<void> {
if (this._configManager) {
const { requiresRestart } = await this._configManager.loadConfiguration()
const isFeatureEnabled = this.isFeatureEnabled
const isFeatureConfigured = this.isFeatureConfigured
// If configuration changes require a restart and the manager is initialized, restart the service
if (requiresRestart && isFeatureEnabled && isFeatureConfigured && this.isInitialized) {
// Recreate services with new configuration
await this._recreateServices()
// Start indexing with new services
this.startIndexing()
}
}
}
}