diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 2efb2cbdff..41859ad4fb 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -2073,15 +2073,94 @@ export const webviewMessageHandler = async ( case "startIndexing": { try { const manager = provider.codeIndexManager! - if (manager.isFeatureEnabled && manager.isFeatureConfigured) { - if (!manager.isInitialized) { - await manager.initialize(provider.contextProxy) - } + if (!manager.isFeatureEnabled) { + await provider.postMessageToWebview({ + type: "indexingStatusUpdate", + values: { + systemStatus: "Standby", + message: "Code indexing is disabled. Please enable it in settings.", + processedItems: 0, + totalItems: 0, + currentItemUnit: "blocks", + }, + }) + break + } - manager.startIndexing() + if (!manager.isFeatureConfigured) { + await provider.postMessageToWebview({ + type: "indexingStatusUpdate", + values: { + systemStatus: "Standby", + message: "Code indexing is not configured. Please configure your embedder settings.", + processedItems: 0, + totalItems: 0, + currentItemUnit: "blocks", + }, + }) + break + } + + // Initialize manager if needed + if (!manager.isInitialized) { + try { + await manager.initialize(provider.contextProxy) + } catch (initError) { + const errorMessage = initError instanceof Error ? initError.message : String(initError) + provider.log(`Error initializing code index manager: ${errorMessage}`) + await provider.postMessageToWebview({ + type: "indexingStatusUpdate", + values: { + systemStatus: "Error", + message: `Failed to initialize: ${errorMessage}`, + processedItems: 0, + totalItems: 0, + currentItemUnit: "blocks", + }, + }) + break + } + } + + // Attempt to start indexing with enhanced error recovery + try { + await manager.startIndexing() + // Send updated status to webview + await provider.postMessageToWebview({ + type: "indexingStatusUpdate", + values: manager.getCurrentStatus(), + }) + } catch (startError) { + const errorMessage = startError instanceof Error ? startError.message : String(startError) + provider.log(`Error starting indexing: ${errorMessage}`) + + // Send error status with recovery suggestion to webview + await provider.postMessageToWebview({ + type: "indexingStatusUpdate", + values: { + systemStatus: "Error", + message: `Failed to start indexing: ${errorMessage}. You can try starting indexing again.`, + processedItems: 0, + totalItems: 0, + currentItemUnit: "blocks", + }, + }) } } catch (error) { - provider.log(`Error starting indexing: ${error instanceof Error ? error.message : String(error)}`) + const errorMessage = error instanceof Error ? error.message : String(error) + provider.log(`Unexpected error in startIndexing handler: ${errorMessage}`) + + // Send generic error status to webview + await provider.postMessageToWebview({ + type: "indexingStatusUpdate", + values: { + systemStatus: "Error", + message: `An unexpected error occurred. Please try again or check your configuration.`, + processedItems: 0, + totalItems: 0, + currentItemUnit: "blocks", + }, + }) } break } diff --git a/src/services/code-index/__tests__/error-recovery.spec.ts b/src/services/code-index/__tests__/error-recovery.spec.ts new file mode 100644 index 0000000000..cc2398d750 --- /dev/null +++ b/src/services/code-index/__tests__/error-recovery.spec.ts @@ -0,0 +1,161 @@ +import { describe, it, expect, beforeEach, vi } from "vitest" +import { CodeIndexStateManager } from "../state-manager" +import { CodeIndexOrchestrator } from "../orchestrator" +import { CodeIndexManager } from "../manager" + +describe("Code Index Error Recovery", () => { + let stateManager: CodeIndexStateManager + let mockConfigManager: any + let mockVectorStore: any + let mockScanner: any + let mockFileWatcher: any + let mockCacheManager: any + + beforeEach(() => { + stateManager = new CodeIndexStateManager() + + mockConfigManager = { + isFeatureConfigured: true, + isFeatureEnabled: true, + } + + mockVectorStore = { + initialize: vi.fn().mockResolvedValue(false), + clearCollection: vi.fn().mockResolvedValue(undefined), + } + + mockScanner = { + scanDirectory: vi.fn().mockResolvedValue({ + stats: { totalFiles: 5, processedFiles: 5 } + }), + } + + mockFileWatcher = { + initialize: vi.fn().mockResolvedValue(undefined), + onDidStartBatchProcessing: vi.fn().mockReturnValue({ dispose: vi.fn() }), + onBatchProgressUpdate: vi.fn().mockReturnValue({ dispose: vi.fn() }), + onDidFinishBatchProcessing: vi.fn().mockReturnValue({ dispose: vi.fn() }), + dispose: vi.fn(), + } + + mockCacheManager = { + clearCacheFile: vi.fn().mockResolvedValue(undefined), + } + }) + + describe("CodeIndexStateManager", () => { + it("should allow resetting from error state", () => { + // Set to error state + stateManager.setSystemState("Error", "Test error") + expect(stateManager.state).toBe("Error") + + // Reset from error + stateManager.resetFromError() + expect(stateManager.state).toBe("Standby") + }) + + it("should not reset if not in error state", () => { + stateManager.setSystemState("Indexed", "All good") + stateManager.resetFromError() + expect(stateManager.state).toBe("Indexed") + }) + + it("should correctly identify when indexing can start", () => { + // Test all valid states + stateManager.setSystemState("Standby") + expect(stateManager.canStartIndexing()).toBe(true) + + stateManager.setSystemState("Error") + expect(stateManager.canStartIndexing()).toBe(true) + + stateManager.setSystemState("Indexed") + expect(stateManager.canStartIndexing()).toBe(true) + + // Test invalid state + stateManager.setSystemState("Indexing") + expect(stateManager.canStartIndexing()).toBe(false) + }) + }) + + describe("CodeIndexOrchestrator", () => { + let orchestrator: CodeIndexOrchestrator + + beforeEach(() => { + orchestrator = new CodeIndexOrchestrator( + mockConfigManager, + stateManager, + "/test/workspace", + mockCacheManager, + mockVectorStore, + mockScanner, + mockFileWatcher + ) + }) + + it("should recover from error state when starting indexing", async () => { + // Set to error state + stateManager.setSystemState("Error", "Previous error") + expect(stateManager.state).toBe("Error") + + // Start indexing should recover from error + await orchestrator.startIndexing() + + // Should have reset from error and started indexing + expect(stateManager.state).toBe("Indexed") + }) + + it("should not start if already processing", async () => { + // Start indexing once + const firstStart = orchestrator.startIndexing() + + // Try to start again while processing + await orchestrator.startIndexing() + + // Wait for first start to complete + await firstStart + + // Should only have been called once + expect(mockVectorStore.initialize).toHaveBeenCalledTimes(1) + }) + + it("should handle configuration errors gracefully", async () => { + mockConfigManager.isFeatureConfigured = false + + await orchestrator.startIndexing() + + expect(stateManager.state).toBe("Standby") + expect(mockVectorStore.initialize).not.toHaveBeenCalled() + }) + }) + + describe("Integration Test", () => { + it("should allow restarting indexing after error without workspace reload", async () => { + const orchestrator = new CodeIndexOrchestrator( + mockConfigManager, + stateManager, + "/test/workspace", + mockCacheManager, + mockVectorStore, + mockScanner, + mockFileWatcher + ) + + // Simulate an error during indexing + mockVectorStore.initialize.mockRejectedValueOnce(new Error("Connection failed")) + + // First attempt should fail + await orchestrator.startIndexing() + expect(stateManager.state).toBe("Error") + + // Fix the mock to succeed + mockVectorStore.initialize.mockResolvedValueOnce(false) + + // Second attempt should succeed without requiring workspace reload + await orchestrator.startIndexing() + expect(stateManager.state).toBe("Indexed") + + // Should have attempted initialization twice + expect(mockVectorStore.initialize).toHaveBeenCalledTimes(2) + }) + }) +}) \ No newline at end of file diff --git a/src/services/code-index/manager.ts b/src/services/code-index/manager.ts index 18e0752c34..790a912a75 100644 --- a/src/services/code-index/manager.ts +++ b/src/services/code-index/manager.ts @@ -157,14 +157,36 @@ export class CodeIndexManager { /** * Initiates the indexing process (initial scan and starts watcher). + * Enhanced with error recovery to prevent users from needing to reload workspace. */ - public async startIndexing(): Promise { if (!this.isFeatureEnabled) { return } this.assertInitialized() - await this._orchestrator!.startIndexing() + + // If we're in an error state, attempt to recover by resetting the state + if (this._stateManager.state === "Error") { + console.log("[CodeIndexManager] Attempting to recover from error state before starting indexing.") + this._stateManager.resetFromError() + } + + try { + await this._orchestrator!.startIndexing() + } catch (error) { + // Enhanced error handling with recovery suggestions + const errorMessage = error instanceof Error ? error.message : String(error) + console.error("[CodeIndexManager] Failed to start indexing:", errorMessage) + + // Set a more informative error state that suggests recovery options + this._stateManager.setSystemState( + "Error", + `Failed to start indexing: ${errorMessage}. Try starting indexing again or check your configuration.` + ) + + // Re-throw the error so callers can handle it appropriately + throw error + } } /** diff --git a/src/services/code-index/orchestrator.ts b/src/services/code-index/orchestrator.ts index 505aee7668..b9aa71ed42 100644 --- a/src/services/code-index/orchestrator.ts +++ b/src/services/code-index/orchestrator.ts @@ -100,18 +100,22 @@ export class CodeIndexOrchestrator { return } - if ( - this._isProcessing || - (this.stateManager.state !== "Standby" && - this.stateManager.state !== "Error" && - this.stateManager.state !== "Indexed") - ) { + // Check if we can start indexing using the state manager's logic + if (this._isProcessing || !this.stateManager.canStartIndexing()) { console.warn( `[CodeIndexOrchestrator] Start rejected: Already processing or in state ${this.stateManager.state}.`, ) return } + // If we're recovering from an error state, reset it first + if (this.stateManager.state === "Error") { + console.log("[CodeIndexOrchestrator] Recovering from error state, resetting to standby.") + this.stateManager.resetFromError() + // Stop any existing watcher to ensure clean state + this.stopWatcher() + } + this._isProcessing = true this.stateManager.setSystemState("Indexing", "Initializing services...") diff --git a/src/services/code-index/state-manager.ts b/src/services/code-index/state-manager.ts index 90257fdfb1..cf0219a867 100644 --- a/src/services/code-index/state-manager.ts +++ b/src/services/code-index/state-manager.ts @@ -55,6 +55,24 @@ export class CodeIndexStateManager { } } + /** + * Resets the system from an error state to standby, allowing indexing to be restarted. + * This method provides a clean recovery path from error states without requiring workspace reload. + */ + public resetFromError(): void { + if (this._systemStatus === "Error") { + this.setSystemState("Standby", "Ready to start indexing.") + } + } + + /** + * Checks if the system is in a state that allows starting indexing. + * @returns true if indexing can be started, false otherwise + */ + public canStartIndexing(): boolean { + return this._systemStatus === "Standby" || this._systemStatus === "Error" || this._systemStatus === "Indexed" + } + public reportBlockIndexingProgress(processedItems: number, totalItems: number): void { const progressChanged = processedItems !== this._processedItems || totalItems !== this._totalItems