Quellcode durchsuchen

feat: add enable/disable toggle for code indexing (#5599)

Daniel vor 5 Monaten
Ursprung
Commit
5bffebde58

+ 14 - 12
src/core/webview/ClineProvider.ts

@@ -1536,12 +1536,12 @@ export class ClineProvider
 			condensingApiConfigId,
 			customCondensingPrompt,
 			codebaseIndexModels: codebaseIndexModels ?? EMBEDDING_MODEL_PROFILES,
-			codebaseIndexConfig: codebaseIndexConfig ?? {
-				codebaseIndexEnabled: true,
-				codebaseIndexQdrantUrl: "http://localhost:6333",
-				codebaseIndexEmbedderProvider: "openai",
-				codebaseIndexEmbedderBaseUrl: "",
-				codebaseIndexEmbedderModelId: "",
+			codebaseIndexConfig: {
+				codebaseIndexEnabled: codebaseIndexConfig?.codebaseIndexEnabled ?? true,
+				codebaseIndexQdrantUrl: codebaseIndexConfig?.codebaseIndexQdrantUrl ?? "http://localhost:6333",
+				codebaseIndexEmbedderProvider: codebaseIndexConfig?.codebaseIndexEmbedderProvider ?? "openai",
+				codebaseIndexEmbedderBaseUrl: codebaseIndexConfig?.codebaseIndexEmbedderBaseUrl ?? "",
+				codebaseIndexEmbedderModelId: codebaseIndexConfig?.codebaseIndexEmbedderModelId ?? "",
 			},
 			mdmCompliant: this.checkMdmCompliance(),
 			profileThresholds: profileThresholds ?? {},
@@ -1695,12 +1695,14 @@ export class ClineProvider
 			condensingApiConfigId: stateValues.condensingApiConfigId,
 			customCondensingPrompt: stateValues.customCondensingPrompt,
 			codebaseIndexModels: stateValues.codebaseIndexModels ?? EMBEDDING_MODEL_PROFILES,
-			codebaseIndexConfig: stateValues.codebaseIndexConfig ?? {
-				codebaseIndexEnabled: true,
-				codebaseIndexQdrantUrl: "http://localhost:6333",
-				codebaseIndexEmbedderProvider: "openai",
-				codebaseIndexEmbedderBaseUrl: "",
-				codebaseIndexEmbedderModelId: "",
+			codebaseIndexConfig: {
+				codebaseIndexEnabled: stateValues.codebaseIndexConfig?.codebaseIndexEnabled ?? true,
+				codebaseIndexQdrantUrl:
+					stateValues.codebaseIndexConfig?.codebaseIndexQdrantUrl ?? "http://localhost:6333",
+				codebaseIndexEmbedderProvider:
+					stateValues.codebaseIndexConfig?.codebaseIndexEmbedderProvider ?? "openai",
+				codebaseIndexEmbedderBaseUrl: stateValues.codebaseIndexConfig?.codebaseIndexEmbedderBaseUrl ?? "",
+				codebaseIndexEmbedderModelId: stateValues.codebaseIndexConfig?.codebaseIndexEmbedderModelId ?? "",
 			},
 			profileThresholds: stateValues.profileThresholds ?? {},
 		}

+ 2 - 1
src/core/webview/webviewMessageHandler.ts

@@ -1952,9 +1952,10 @@ export const webviewMessageHandler = async (
 				const embedderProviderChanged =
 					currentConfig.codebaseIndexEmbedderProvider !== settings.codebaseIndexEmbedderProvider
 
-				// Save global state settings atomically (without codebaseIndexEnabled which is now in global settings)
+				// Save global state settings atomically
 				const globalStateConfig = {
 					...currentConfig,
+					codebaseIndexEnabled: settings.codebaseIndexEnabled,
 					codebaseIndexQdrantUrl: settings.codebaseIndexQdrantUrl,
 					codebaseIndexEmbedderProvider: settings.codebaseIndexEmbedderProvider,
 					codebaseIndexEmbedderBaseUrl: settings.codebaseIndexEmbedderBaseUrl,

+ 338 - 3
src/services/code-index/__tests__/config-manager.spec.ts

@@ -1,15 +1,27 @@
+// npx vitest services/code-index/__tests__/config-manager.spec.ts
+
+import { describe, it, expect, beforeEach, vi } from "vitest"
 import { CodeIndexConfigManager } from "../config-manager"
+import { ContextProxy } from "../../../core/config/ContextProxy"
+import { PreviousConfigSnapshot } from "../interfaces/config"
+
+// Mock ContextProxy
+vi.mock("../../../core/config/ContextProxy")
 
 describe("CodeIndexConfigManager", () => {
 	let mockContextProxy: any
 	let configManager: CodeIndexConfigManager
 
 	beforeEach(() => {
+		// Reset mocks
+		vi.clearAllMocks()
+
 		// Setup mock ContextProxy
 		mockContextProxy = {
-			getGlobalState: vitest.fn(),
-			getSecret: vitest.fn().mockReturnValue(undefined),
-			refreshSecrets: vitest.fn().mockResolvedValue(undefined),
+			getGlobalState: vi.fn(),
+			getSecret: vi.fn().mockReturnValue(undefined),
+			refreshSecrets: vi.fn().mockResolvedValue(undefined),
+			updateGlobalState: vi.fn(),
 		}
 
 		configManager = new CodeIndexConfigManager(mockContextProxy)
@@ -37,6 +49,39 @@ describe("CodeIndexConfigManager", () => {
 		})
 	})
 
+	describe("isFeatureEnabled", () => {
+		it("should return false when codebaseIndexEnabled is false", async () => {
+			mockContextProxy.getGlobalState.mockReturnValue({
+				codebaseIndexEnabled: false,
+			})
+			mockContextProxy.getSecret.mockReturnValue(undefined)
+
+			// Re-create instance to load the configuration
+			configManager = new CodeIndexConfigManager(mockContextProxy)
+			expect(configManager.isFeatureEnabled).toBe(false)
+		})
+
+		it("should return true when codebaseIndexEnabled is true", async () => {
+			mockContextProxy.getGlobalState.mockReturnValue({
+				codebaseIndexEnabled: true,
+			})
+			mockContextProxy.getSecret.mockReturnValue(undefined)
+
+			// Re-create instance to load the configuration
+			configManager = new CodeIndexConfigManager(mockContextProxy)
+			expect(configManager.isFeatureEnabled).toBe(true)
+		})
+
+		it("should default to true when codebaseIndexEnabled is not set", async () => {
+			mockContextProxy.getGlobalState.mockReturnValue({})
+			mockContextProxy.getSecret.mockReturnValue(undefined)
+
+			// Re-create instance to load the configuration
+			configManager = new CodeIndexConfigManager(mockContextProxy)
+			expect(configManager.isFeatureEnabled).toBe(true)
+		})
+	})
+
 	describe("loadConfiguration", () => {
 		it("should load default configuration when no state exists", async () => {
 			mockContextProxy.getGlobalState.mockReturnValue(undefined)
@@ -1300,4 +1345,294 @@ describe("CodeIndexConfigManager", () => {
 			expect(result.requiresRestart).toBe(false)
 		})
 	})
+
+	describe("doesConfigChangeRequireRestart", () => {
+		it("should return true when enabling the feature", async () => {
+			// Initial state: disabled
+			mockContextProxy.getGlobalState.mockReturnValue({
+				codebaseIndexEnabled: false,
+				codebaseIndexEmbedderProvider: "openai",
+				codebaseIndexQdrantUrl: "http://localhost:6333",
+			})
+			mockContextProxy.getSecret.mockReturnValue(undefined)
+			configManager = new CodeIndexConfigManager(mockContextProxy)
+
+			// Get the initial snapshot
+			const { configSnapshot: previousSnapshot } = await configManager.loadConfiguration()
+
+			// Update the internal state to enabled with proper configuration
+			mockContextProxy.getGlobalState.mockReturnValue({
+				codebaseIndexEnabled: true,
+				codebaseIndexEmbedderProvider: "openai",
+				codebaseIndexQdrantUrl: "http://localhost:6333",
+			})
+			mockContextProxy.getSecret.mockImplementation((key: string) => {
+				if (key === "codeIndexOpenAiKey") return "test-key"
+				return undefined
+			})
+
+			// Load the new configuration - this will internally call doesConfigChangeRequireRestart
+			const { requiresRestart } = await configManager.loadConfiguration()
+
+			expect(requiresRestart).toBe(true)
+		})
+
+		it("should return true when disabling the feature", async () => {
+			// Initial state: enabled and configured
+			mockContextProxy.getGlobalState.mockReturnValue({
+				codebaseIndexEnabled: true,
+				codebaseIndexEmbedderProvider: "openai",
+				codebaseIndexQdrantUrl: "http://localhost:6333",
+			})
+			mockContextProxy.getSecret.mockImplementation((key: string) => {
+				if (key === "codeIndexOpenAiKey") return "test-key"
+				return undefined
+			})
+			configManager = new CodeIndexConfigManager(mockContextProxy)
+
+			const previousSnapshot: PreviousConfigSnapshot = {
+				enabled: true,
+				configured: true,
+				embedderProvider: "openai",
+				openAiKey: "test-key",
+				qdrantUrl: "http://localhost:6333",
+			}
+
+			// Update to disabled
+			mockContextProxy.getGlobalState.mockReturnValue({
+				codebaseIndexEnabled: false,
+				codebaseIndexEmbedderProvider: "openai",
+				codebaseIndexQdrantUrl: "http://localhost:6333",
+			})
+			mockContextProxy.getSecret.mockImplementation((key: string) => {
+				if (key === "codeIndexOpenAiKey") return "test-key"
+				return undefined
+			})
+
+			await configManager.loadConfiguration()
+
+			const result = configManager.doesConfigChangeRequireRestart(previousSnapshot)
+			expect(result).toBe(true)
+		})
+
+		it("should return false when enabled state does not change (both enabled)", async () => {
+			// Initial state: enabled and configured
+			mockContextProxy.getGlobalState.mockReturnValue({
+				codebaseIndexEnabled: true,
+				codebaseIndexEmbedderProvider: "openai",
+				codebaseIndexQdrantUrl: "http://localhost:6333",
+			})
+			mockContextProxy.getSecret.mockImplementation((key: string) => {
+				if (key === "codeIndexOpenAiKey") return "test-key"
+				return undefined
+			})
+			configManager = new CodeIndexConfigManager(mockContextProxy)
+
+			// Get initial configuration
+			const { configSnapshot: previousSnapshot } = await configManager.loadConfiguration()
+
+			// Load again with same config - should not require restart
+			const { requiresRestart } = await configManager.loadConfiguration()
+
+			expect(requiresRestart).toBe(false)
+		})
+
+		it("should return false when enabled state does not change (both disabled)", async () => {
+			// Initial state: disabled
+			mockContextProxy.getGlobalState.mockReturnValue({
+				codebaseIndexEnabled: false,
+			})
+			mockContextProxy.getSecret.mockReturnValue(undefined)
+			configManager = new CodeIndexConfigManager(mockContextProxy)
+
+			const previousSnapshot: PreviousConfigSnapshot = {
+				enabled: false,
+				configured: false,
+				embedderProvider: "openai",
+			}
+
+			// Same config, still disabled
+			const result = configManager.doesConfigChangeRequireRestart(previousSnapshot)
+			expect(result).toBe(false)
+		})
+
+		it("should return true when provider changes while enabled", async () => {
+			// Initial state: enabled with openai
+			mockContextProxy.getGlobalState.mockReturnValue({
+				codebaseIndexEnabled: true,
+				codebaseIndexEmbedderProvider: "ollama",
+				codebaseIndexOllamaBaseUrl: "http://localhost:11434",
+				codebaseIndexQdrantUrl: "http://localhost:6333",
+			})
+			mockContextProxy.getSecret.mockReturnValue(undefined)
+			configManager = new CodeIndexConfigManager(mockContextProxy)
+
+			const previousSnapshot: PreviousConfigSnapshot = {
+				enabled: true,
+				configured: true,
+				embedderProvider: "openai",
+				openAiKey: "test-key",
+				qdrantUrl: "http://localhost:6333",
+			}
+
+			const result = configManager.doesConfigChangeRequireRestart(previousSnapshot)
+			expect(result).toBe(true)
+		})
+
+		it("should return false when provider changes while disabled", async () => {
+			// Initial state: disabled with openai
+			mockContextProxy.getGlobalState.mockReturnValue({
+				codebaseIndexEnabled: false,
+				codebaseIndexEmbedderProvider: "ollama",
+			})
+			mockContextProxy.getSecret.mockReturnValue(undefined)
+			configManager = new CodeIndexConfigManager(mockContextProxy)
+
+			const previousSnapshot: PreviousConfigSnapshot = {
+				enabled: false,
+				configured: false,
+				embedderProvider: "openai",
+			}
+
+			// Provider changed but feature is disabled
+			const result = configManager.doesConfigChangeRequireRestart(previousSnapshot)
+			expect(result).toBe(false)
+		})
+	})
+
+	describe("loadConfiguration", () => {
+		it("should load configuration and return proper structure", async () => {
+			const mockConfigValues = {
+				codebaseIndexEnabled: true,
+				codebaseIndexEmbedderProvider: "openai",
+				codebaseIndexEmbedderModelId: "text-embedding-ada-002",
+				codebaseIndexQdrantUrl: "http://localhost:6333",
+				codebaseIndexSearchMinScore: 0.5,
+				codebaseIndexSearchMaxResults: 20,
+			}
+
+			mockContextProxy.getGlobalState.mockReturnValue(mockConfigValues)
+			mockContextProxy.getSecret.mockImplementation((key: string) => {
+				if (key === "codeIndexOpenAiKey") return "test-key"
+				if (key === "codeIndexQdrantApiKey") return "qdrant-key"
+				return undefined
+			})
+
+			const result = await configManager.loadConfiguration()
+
+			// Verify the structure
+			expect(result).toHaveProperty("configSnapshot")
+			expect(result).toHaveProperty("currentConfig")
+			expect(result).toHaveProperty("requiresRestart")
+
+			// Verify current config reflects loaded values
+			expect(result.currentConfig.embedderProvider).toBe("openai")
+			expect(result.currentConfig.isConfigured).toBe(true)
+		})
+
+		it("should detect restart requirement when configuration changes", async () => {
+			// Initial state: disabled
+			mockContextProxy.getGlobalState.mockReturnValue({
+				codebaseIndexEnabled: false,
+				codebaseIndexEmbedderProvider: "openai",
+				codebaseIndexQdrantUrl: "http://localhost:6333",
+			})
+			mockContextProxy.getSecret.mockReturnValue(undefined)
+			configManager = new CodeIndexConfigManager(mockContextProxy)
+
+			// Get initial state
+			await configManager.loadConfiguration()
+
+			// Change to enabled with proper configuration
+			mockContextProxy.getGlobalState.mockReturnValue({
+				codebaseIndexEnabled: true,
+				codebaseIndexEmbedderProvider: "openai",
+				codebaseIndexQdrantUrl: "http://localhost:6333",
+			})
+			mockContextProxy.getSecret.mockImplementation((key: string) => {
+				if (key === "codeIndexOpenAiKey") return "test-key"
+				return undefined
+			})
+
+			const result = await configManager.loadConfiguration()
+			expect(result.requiresRestart).toBe(true)
+		})
+	})
+
+	describe("getConfig", () => {
+		it("should return the current configuration", () => {
+			mockContextProxy.getGlobalState.mockReturnValue({
+				codebaseIndexEnabled: true,
+				codebaseIndexEmbedderProvider: "openai",
+				codebaseIndexQdrantUrl: "http://localhost:6333",
+			})
+			mockContextProxy.getSecret.mockImplementation((key: string) => {
+				if (key === "codeIndexOpenAiKey") return "test-key"
+				return undefined
+			})
+
+			configManager = new CodeIndexConfigManager(mockContextProxy)
+			const config = configManager.getConfig()
+
+			expect(config).toHaveProperty("isConfigured")
+			expect(config).toHaveProperty("embedderProvider")
+			expect(config.embedderProvider).toBe("openai")
+		})
+	})
+
+	describe("isConfigured", () => {
+		it("should return true when OpenAI provider is properly configured", () => {
+			mockContextProxy.getGlobalState.mockReturnValue({
+				codebaseIndexEnabled: true,
+				codebaseIndexEmbedderProvider: "openai",
+				codebaseIndexQdrantUrl: "http://localhost:6333",
+			})
+			mockContextProxy.getSecret.mockImplementation((key: string) => {
+				if (key === "codeIndexOpenAiKey") return "test-key"
+				return undefined
+			})
+
+			configManager = new CodeIndexConfigManager(mockContextProxy)
+			expect(configManager.isConfigured()).toBe(true)
+		})
+
+		it("should return false when OpenAI provider is missing API key", () => {
+			mockContextProxy.getGlobalState.mockReturnValue({
+				codebaseIndexEnabled: true,
+				codebaseIndexEmbedderProvider: "openai",
+				codebaseIndexQdrantUrl: "http://localhost:6333",
+			})
+			mockContextProxy.getSecret.mockReturnValue(undefined)
+
+			configManager = new CodeIndexConfigManager(mockContextProxy)
+			expect(configManager.isConfigured()).toBe(false)
+		})
+
+		it("should return true when Ollama provider is properly configured", () => {
+			mockContextProxy.getGlobalState.mockReturnValue({
+				codebaseIndexEnabled: true,
+				codebaseIndexEmbedderProvider: "ollama",
+				codebaseIndexEmbedderBaseUrl: "http://localhost:11434",
+				codebaseIndexQdrantUrl: "http://localhost:6333",
+			})
+			mockContextProxy.getSecret.mockReturnValue(undefined)
+
+			configManager = new CodeIndexConfigManager(mockContextProxy)
+			expect(configManager.isConfigured()).toBe(true)
+		})
+
+		it("should return false when Qdrant URL is missing", () => {
+			mockContextProxy.getGlobalState.mockReturnValue({
+				codebaseIndexEnabled: true,
+				codebaseIndexEmbedderProvider: "openai",
+			})
+			mockContextProxy.getSecret.mockImplementation((key: string) => {
+				if (key === "codeIndexOpenAiKey") return "test-key"
+				return undefined
+			})
+
+			configManager = new CodeIndexConfigManager(mockContextProxy)
+			expect(configManager.isConfigured()).toBe(false)
+		})
+	})
 })

+ 16 - 8
src/services/code-index/config-manager.ts

@@ -10,6 +10,7 @@ import { getDefaultModelId, getModelDimension, getModelScoreThreshold } from "..
  * Handles loading, validating, and providing access to configuration values.
  */
 export class CodeIndexConfigManager {
+	private codebaseIndexEnabled: boolean = true
 	private embedderProvider: EmbedderProvider = "openai"
 	private modelId?: string
 	private modelDimension?: number
@@ -68,7 +69,7 @@ export class CodeIndexConfigManager {
 		const geminiApiKey = this.contextProxy?.getSecret("codebaseIndexGeminiApiKey") ?? ""
 
 		// Update instance variables with configuration
-		// Note: codebaseIndexEnabled is no longer used as the feature is always enabled
+		this.codebaseIndexEnabled = codebaseIndexEnabled ?? true
 		this.qdrantUrl = codebaseIndexQdrantUrl
 		this.qdrantApiKey = qdrantApiKey ?? ""
 		this.searchMinScore = codebaseIndexSearchMinScore
@@ -142,7 +143,7 @@ export class CodeIndexConfigManager {
 	}> {
 		// Capture the ACTUAL previous state before loading new configuration
 		const previousConfigSnapshot: PreviousConfigSnapshot = {
-			enabled: true, // Feature is always enabled
+			enabled: this.codebaseIndexEnabled,
 			configured: this.isConfigured(),
 			embedderProvider: this.embedderProvider,
 			modelId: this.modelId,
@@ -243,19 +244,26 @@ export class CodeIndexConfigManager {
 		const prevQdrantUrl = prev?.qdrantUrl ?? ""
 		const prevQdrantApiKey = prev?.qdrantApiKey ?? ""
 
-		// 1. Transition from unconfigured to configured
-		// Since the feature is always enabled, we only check configuration status
-		if (!prevConfigured && nowConfigured) {
+		// 1. Transition from disabled/unconfigured to enabled/configured
+		if ((!prevEnabled || !prevConfigured) && this.codebaseIndexEnabled && nowConfigured) {
+			return true
+		}
+
+		// 2. Transition from enabled to disabled
+		if (prevEnabled && !this.codebaseIndexEnabled) {
 			return true
 		}
 
 		// 3. If wasn't ready before and isn't ready now, no restart needed
-		if (!prevConfigured && !nowConfigured) {
+		if ((!prevEnabled || !prevConfigured) && (!this.codebaseIndexEnabled || !nowConfigured)) {
 			return false
 		}
 
 		// 4. CRITICAL CHANGES - Always restart for these
-		// Since feature is always enabled, we always check for critical changes
+		// Only check for critical changes if feature is enabled
+		if (!this.codebaseIndexEnabled) {
+			return false
+		}
 
 		// Provider change
 		if (prevProvider !== this.embedderProvider) {
@@ -354,7 +362,7 @@ export class CodeIndexConfigManager {
 	 * Gets whether the code indexing feature is enabled
 	 */
 	public get isFeatureEnabled(): boolean {
-		return true
+		return this.codebaseIndexEnabled
 	}
 
 	/**

+ 17 - 0
src/services/code-index/manager.ts

@@ -310,8 +310,25 @@ export class CodeIndexManager {
 			const isFeatureEnabled = this.isFeatureEnabled
 			const isFeatureConfigured = this.isFeatureConfigured
 
+			// If feature is disabled, stop the service
+			if (!isFeatureEnabled) {
+				// Stop the orchestrator if it exists
+				if (this._orchestrator) {
+					this._orchestrator.stopWatcher()
+				}
+				// Set state to indicate service is disabled
+				this._stateManager.setSystemState("Standby", "Code indexing is disabled")
+				return
+			}
+
 			if (requiresRestart && isFeatureEnabled && isFeatureConfigured) {
 				try {
+					// Ensure cacheManager is initialized before recreating services
+					if (!this._cacheManager) {
+						this._cacheManager = new CacheManager(this.context, this.workspacePath)
+						await this._cacheManager.initialize()
+					}
+
 					// Recreate services with new configuration
 					await this._recreateServices()
 				} catch (error) {

+ 89 - 54
webview-ui/src/components/chat/CodeIndexPopover.tsx

@@ -7,6 +7,7 @@ import {
 	VSCodeDropdown,
 	VSCodeOption,
 	VSCodeLink,
+	VSCodeCheckbox,
 } from "@vscode/webview-ui-toolkit/react"
 import * as ProgressPrimitive from "@radix-ui/react-progress"
 import { vscode } from "@src/utils/vscode"
@@ -72,6 +73,7 @@ interface LocalCodeIndexSettings {
 // Validation schema for codebase index settings
 const createValidationSchema = (provider: EmbedderProvider, t: any) => {
 	const baseSchema = z.object({
+		codebaseIndexEnabled: z.boolean(),
 		codebaseIndexQdrantUrl: z
 			.string()
 			.min(1, t("settings:codeIndex.validation.qdrantUrlRequired"))
@@ -213,6 +215,10 @@ export const CodeIndexPopover: React.FC<CodeIndexPopoverProps> = ({
 		}
 	}, [open])
 
+	// Use a ref to capture current settings for the save handler
+	const currentSettingsRef = useRef(currentSettings)
+	currentSettingsRef.current = currentSettings
+
 	// Listen for indexing status updates and save responses
 	useEffect(() => {
 		const handleMessage = (event: MessageEvent<any>) => {
@@ -227,21 +233,24 @@ export const CodeIndexPopover: React.FC<CodeIndexPopoverProps> = ({
 			} else if (event.data.type === "codeIndexSettingsSaved") {
 				if (event.data.success) {
 					setSaveStatus("saved")
-					// Don't update initial settings here - wait for the secret status response
-					// Request updated secret status after save
+					// Update initial settings to match current settings after successful save
+					// This ensures hasUnsavedChanges becomes false
+					const savedSettings = { ...currentSettingsRef.current }
+					setInitialSettings(savedSettings)
+					// Also update current settings to maintain consistency
+					setCurrentSettings(savedSettings)
+					// Request secret status to ensure we have the latest state
+					// This is important to maintain placeholder display after save
+
 					vscode.postMessage({ type: "requestCodeIndexSecretStatus" })
-					// Reset status after 3 seconds
-					setTimeout(() => {
-						setSaveStatus("idle")
-					}, 3000)
+
+					setSaveStatus("idle")
 				} else {
 					setSaveStatus("error")
 					setSaveError(event.data.error || t("settings:codeIndex.saveError"))
 					// Clear error message after 5 seconds
-					setTimeout(() => {
-						setSaveStatus("idle")
-						setSaveError(null)
-					}, 5000)
+					setSaveStatus("idle")
+					setSaveError(null)
 				}
 			}
 		}
@@ -284,14 +293,18 @@ export const CodeIndexPopover: React.FC<CodeIndexPopoverProps> = ({
 					return updated
 				}
 
-				setCurrentSettings(updateWithSecrets)
-				setInitialSettings(updateWithSecrets)
+				// Only update settings if we're not in the middle of saving
+				// After save is complete (saved status), we still want to update to maintain consistency
+				if (saveStatus === "idle" || saveStatus === "saved") {
+					setCurrentSettings(updateWithSecrets)
+					setInitialSettings(updateWithSecrets)
+				}
 			}
 		}
 
 		window.addEventListener("message", handleMessage)
 		return () => window.removeEventListener("message", handleMessage)
-	}, [])
+	}, [saveStatus])
 
 	// Generic comparison function that detects changes between initial and current settings
 	const hasUnsavedChanges = useMemo(() => {
@@ -417,20 +430,26 @@ export const CodeIndexPopover: React.FC<CodeIndexPopoverProps> = ({
 		setSaveStatus("saving")
 		setSaveError(null)
 
-		// Prepare settings to save - include all fields except secrets with placeholder values
+		// Prepare settings to save
 		const settingsToSave: any = {}
 
 		// Iterate through all current settings
 		for (const [key, value] of Object.entries(currentSettings)) {
-			// Skip secret fields that still have placeholder value
+			// For secret fields with placeholder, don't send the placeholder
+			// but also don't send an empty string - just skip the field
+			// This tells the backend to keep the existing secret
 			if (value === SECRET_PLACEHOLDER) {
+				// Skip sending placeholder values - backend will preserve existing secrets
 				continue
 			}
 
-			// Include all other fields
+			// Include all other fields, including empty strings (which clear secrets)
 			settingsToSave[key] = value
 		}
 
+		// Always include codebaseIndexEnabled to ensure it's persisted
+		settingsToSave.codebaseIndexEnabled = currentSettings.codebaseIndexEnabled
+
 		// Save settings to backend
 		vscode.postMessage({
 			type: "saveCodeIndexSettingsAtomic",
@@ -494,6 +513,20 @@ export const CodeIndexPopover: React.FC<CodeIndexPopoverProps> = ({
 					</div>
 
 					<div className="p-4">
+						{/* Enable/Disable Toggle */}
+						<div className="mb-4">
+							<div className="flex items-center gap-2">
+								<VSCodeCheckbox
+									checked={currentSettings.codebaseIndexEnabled}
+									onChange={(e: any) => updateSetting("codebaseIndexEnabled", e.target.checked)}>
+									<span className="font-medium">{t("settings:codeIndex.enableLabel")}</span>
+								</VSCodeCheckbox>
+								<StandardTooltip content={t("settings:codeIndex.enableDescription")}>
+									<span className="codicon codicon-info text-xs text-vscode-descriptionForeground cursor-help" />
+								</StandardTooltip>
+							</div>
+						</div>
+
 						{/* Status Section */}
 						<div className="space-y-2">
 							<h4 className="text-sm font-medium">{t("settings:codeIndex.statusTitle")}</h4>
@@ -1049,44 +1082,46 @@ export const CodeIndexPopover: React.FC<CodeIndexPopoverProps> = ({
 						{/* Action Buttons */}
 						<div className="flex items-center justify-between gap-2 pt-6">
 							<div className="flex gap-2">
-								{(indexingStatus.systemStatus === "Error" ||
-									indexingStatus.systemStatus === "Standby") && (
-									<VSCodeButton
-										onClick={() => vscode.postMessage({ type: "startIndexing" })}
-										disabled={saveStatus === "saving" || hasUnsavedChanges}>
-										{t("settings:codeIndex.startIndexingButton")}
-									</VSCodeButton>
-								)}
-
-								{(indexingStatus.systemStatus === "Indexed" ||
-									indexingStatus.systemStatus === "Error") && (
-									<AlertDialog>
-										<AlertDialogTrigger asChild>
-											<VSCodeButton appearance="secondary">
-												{t("settings:codeIndex.clearIndexDataButton")}
-											</VSCodeButton>
-										</AlertDialogTrigger>
-										<AlertDialogContent>
-											<AlertDialogHeader>
-												<AlertDialogTitle>
-													{t("settings:codeIndex.clearDataDialog.title")}
-												</AlertDialogTitle>
-												<AlertDialogDescription>
-													{t("settings:codeIndex.clearDataDialog.description")}
-												</AlertDialogDescription>
-											</AlertDialogHeader>
-											<AlertDialogFooter>
-												<AlertDialogCancel>
-													{t("settings:codeIndex.clearDataDialog.cancelButton")}
-												</AlertDialogCancel>
-												<AlertDialogAction
-													onClick={() => vscode.postMessage({ type: "clearIndexData" })}>
-													{t("settings:codeIndex.clearDataDialog.confirmButton")}
-												</AlertDialogAction>
-											</AlertDialogFooter>
-										</AlertDialogContent>
-									</AlertDialog>
-								)}
+								{currentSettings.codebaseIndexEnabled &&
+									(indexingStatus.systemStatus === "Error" ||
+										indexingStatus.systemStatus === "Standby") && (
+										<VSCodeButton
+											onClick={() => vscode.postMessage({ type: "startIndexing" })}
+											disabled={saveStatus === "saving" || hasUnsavedChanges}>
+											{t("settings:codeIndex.startIndexingButton")}
+										</VSCodeButton>
+									)}
+
+								{currentSettings.codebaseIndexEnabled &&
+									(indexingStatus.systemStatus === "Indexed" ||
+										indexingStatus.systemStatus === "Error") && (
+										<AlertDialog>
+											<AlertDialogTrigger asChild>
+												<VSCodeButton appearance="secondary">
+													{t("settings:codeIndex.clearIndexDataButton")}
+												</VSCodeButton>
+											</AlertDialogTrigger>
+											<AlertDialogContent>
+												<AlertDialogHeader>
+													<AlertDialogTitle>
+														{t("settings:codeIndex.clearDataDialog.title")}
+													</AlertDialogTitle>
+													<AlertDialogDescription>
+														{t("settings:codeIndex.clearDataDialog.description")}
+													</AlertDialogDescription>
+												</AlertDialogHeader>
+												<AlertDialogFooter>
+													<AlertDialogCancel>
+														{t("settings:codeIndex.clearDataDialog.cancelButton")}
+													</AlertDialogCancel>
+													<AlertDialogAction
+														onClick={() => vscode.postMessage({ type: "clearIndexData" })}>
+														{t("settings:codeIndex.clearDataDialog.confirmButton")}
+													</AlertDialogAction>
+												</AlertDialogFooter>
+											</AlertDialogContent>
+										</AlertDialog>
+									)}
 							</div>
 
 							<VSCodeButton

+ 6 - 0
webview-ui/src/components/chat/__tests__/CodeIndexPopover.validation.spec.tsx

@@ -103,6 +103,12 @@ vi.mock("@vscode/webview-ui-toolkit/react", () => ({
 			{children}
 		</a>
 	),
+	VSCodeCheckbox: ({ checked, onChange, children, ...rest }: any) => (
+		<label>
+			<input type="checkbox" checked={checked || false} onChange={(e) => onChange && onChange(e)} {...rest} />
+			{children}
+		</label>
+	),
 }))
 
 // Helper function to simulate input on form elements

+ 1 - 1
webview-ui/src/i18n/locales/ca/settings.json

@@ -39,7 +39,7 @@
 	"codeIndex": {
 		"title": "Indexació de codi",
 		"enableLabel": "Habilitar indexació de codi",
-		"enableDescription": "<0>Indexació de codi</0> és una característica experimental que crea un índex de cerca semàntica del vostre projecte utilitzant embeddings d'IA. Això permet a Roo Code entendre millor i navegar per grans bases de codi trobant codi rellevant basat en significat en lloc de només paraules clau.",
+		"enableDescription": "Habilita la indexació de codi per millorar la cerca i la comprensió del context",
 		"providerLabel": "Proveïdor d'embeddings",
 		"selectProviderPlaceholder": "Seleccionar proveïdor",
 		"openaiProvider": "OpenAI",

+ 1 - 1
webview-ui/src/i18n/locales/de/settings.json

@@ -41,7 +41,7 @@
 		"description": "Konfiguriere Codebase-Indexierungseinstellungen, um semantische Suche in deinem Projekt zu aktivieren. <0>Mehr erfahren</0>",
 		"statusTitle": "Status",
 		"enableLabel": "Codebase-Indexierung aktivieren",
-		"enableDescription": "<0>Codebase-Indexierung</0> ist eine experimentelle Funktion, die einen semantischen Suchindex deines Projekts mit KI-Embeddings erstellt. Dies ermöglicht es Roo Code, große Codebasen besser zu verstehen und zu navigieren, indem relevanter Code basierend auf Bedeutung statt nur Schlüsselwörtern gefunden wird.",
+		"enableDescription": "Aktiviere die Code-Indizierung für eine verbesserte Suche und ein besseres Kontextverständnis",
 		"settingsTitle": "Indexierungseinstellungen",
 		"disabledMessage": "Codebase-Indexierung ist derzeit deaktiviert. Aktiviere sie in den globalen Einstellungen, um Indexierungsoptionen zu konfigurieren.",
 		"providerLabel": "Embeddings-Anbieter",

+ 1 - 1
webview-ui/src/i18n/locales/en/settings.json

@@ -41,7 +41,7 @@
 		"description": "Configure codebase indexing settings to enable semantic search of your project. <0>Learn more</0>",
 		"statusTitle": "Status",
 		"enableLabel": "Enable Codebase Indexing",
-		"enableDescription": "<0>Codebase Indexing</0> is an experimental feature that creates a semantic search index of your project using AI embeddings. This enables Roo Code to better understand and navigate large codebases by finding relevant code based on meaning rather than just keywords.",
+		"enableDescription": "Enable code indexing for improved search and context understanding",
 		"settingsTitle": "Indexing Settings",
 		"disabledMessage": "Codebase indexing is currently disabled. Enable it in the global settings to configure indexing options.",
 		"providerLabel": "Embeddings Provider",

+ 1 - 1
webview-ui/src/i18n/locales/es/settings.json

@@ -41,7 +41,7 @@
 		"description": "Configura los ajustes de indexación de código para habilitar búsqueda semántica en tu proyecto. <0>Más información</0>",
 		"statusTitle": "Estado",
 		"enableLabel": "Habilitar indexación de código",
-		"enableDescription": "<0>La indexación de código</0> es una función experimental que crea un índice de búsqueda semántica de tu proyecto usando embeddings de IA. Esto permite a Roo Code entender mejor y navegar grandes bases de código encontrando código relevante basado en significado en lugar de solo palabras clave.",
+		"enableDescription": "Habilita la indexación de código para mejorar la búsqueda y la comprensión del contexto",
 		"settingsTitle": "Configuración de indexación",
 		"disabledMessage": "La indexación de código está actualmente deshabilitada. Habilítala en la configuración global para configurar las opciones de indexación.",
 		"providerLabel": "Proveedor de embeddings",

+ 1 - 1
webview-ui/src/i18n/locales/fr/settings.json

@@ -41,7 +41,7 @@
 		"description": "Configurez les paramètres d'indexation de la base de code pour activer la recherche sémantique dans votre projet. <0>En savoir plus</0>",
 		"statusTitle": "Statut",
 		"enableLabel": "Activer l'indexation de la base de code",
-		"enableDescription": "<0>L'indexation de la base de code</0> est une fonctionnalité expérimentale qui crée un index de recherche sémantique de votre projet en utilisant des embeddings IA. Cela permet à Roo Code de mieux comprendre et naviguer dans de grandes bases de code en trouvant du code pertinent basé sur le sens plutôt que seulement sur des mots-clés.",
+		"enableDescription": "Activer l'indexation du code pour une recherche et une compréhension du contexte améliorées",
 		"settingsTitle": "Paramètres d'indexation",
 		"disabledMessage": "L'indexation de la base de code est actuellement désactivée. Activez-la dans les paramètres globaux pour configurer les options d'indexation.",
 		"providerLabel": "Fournisseur d'embeddings",

+ 1 - 1
webview-ui/src/i18n/locales/hi/settings.json

@@ -39,7 +39,7 @@
 	"codeIndex": {
 		"title": "कोडबेस इंडेक्सिंग",
 		"enableLabel": "कोडबेस इंडेक्सिंग सक्षम करें",
-		"enableDescription": "<0>कोडबेस इंडेक्सिंग</0> एक प्रयोगात्मक सुविधा है जो AI एम्बेडिंग का उपयोग करके आपके प्रोजेक्ट का सिमेंटिक सर्च इंडेक्स बनाती है। यह Roo Code को केवल कीवर्ड के बजाय अर्थ के आधार पर संबंधित कोड खोजकर बड़े कोडबेस को बेहतर तरीके से समझने और नेविगेट करने में सक्षम बनाता है।",
+		"enableDescription": "बेहतर खोज और संदर्भ समझने के लिए कोड इंडेक्सिंग सक्षम करें",
 		"providerLabel": "एम्बेडिंग प्रदाता",
 		"selectProviderPlaceholder": "प्रदाता चुनें",
 		"openaiProvider": "OpenAI",

+ 1 - 1
webview-ui/src/i18n/locales/id/settings.json

@@ -39,7 +39,7 @@
 	"codeIndex": {
 		"title": "Pengindeksan Codebase",
 		"enableLabel": "Aktifkan Pengindeksan Codebase",
-		"enableDescription": "<0>Pengindeksan Codebase</0> adalah fitur eksperimental yang membuat indeks pencarian semantik dari proyek kamu menggunakan AI embeddings. Ini memungkinkan Roo Code untuk lebih memahami dan menavigasi codebase besar dengan menemukan kode yang relevan berdasarkan makna daripada hanya kata kunci.",
+		"enableDescription": "Aktifkan pengindeksan kode untuk pencarian dan pemahaman konteks yang lebih baik",
 		"providerLabel": "Provider Embeddings",
 		"selectProviderPlaceholder": "Pilih provider",
 		"openaiProvider": "OpenAI",

+ 1 - 1
webview-ui/src/i18n/locales/it/settings.json

@@ -39,7 +39,7 @@
 	"codeIndex": {
 		"title": "Indicizzazione del codice",
 		"enableLabel": "Abilita indicizzazione del codice",
-		"enableDescription": "<0>L'indicizzazione del codice</0> è una funzionalità sperimentale che crea un indice di ricerca semantica del tuo progetto utilizzando embedding AI. Questo permette a Roo Code di comprendere meglio e navigare grandi basi di codice trovando codice rilevante basato sul significato piuttosto che solo su parole chiave.",
+		"enableDescription": "Abilita l'indicizzazione del codice per una ricerca e una comprensione del contesto migliorate",
 		"providerLabel": "Fornitore di embedding",
 		"selectProviderPlaceholder": "Seleziona fornitore",
 		"openaiProvider": "OpenAI",

+ 1 - 1
webview-ui/src/i18n/locales/ja/settings.json

@@ -39,7 +39,7 @@
 	"codeIndex": {
 		"title": "コードベースのインデックス作成",
 		"enableLabel": "コードベースのインデックス作成を有効化",
-		"enableDescription": "<0>コードベースのインデックス作成</0>は、AIエンベディングを使用してプロジェクトのセマンティック検索インデックスを作成する実験的機能です。これにより、Roo Codeは単なるキーワードではなく意味に基づいて関連するコードを見つけることで、大規模なコードベースをより良く理解し、ナビゲートできるようになります。",
+		"enableDescription": "コードのインデックス作成を有効にして、検索とコンテキストの理解を向上させます",
 		"providerLabel": "埋め込みプロバイダー",
 		"selectProviderPlaceholder": "プロバイダーを選択",
 		"openaiProvider": "OpenAI",

+ 1 - 1
webview-ui/src/i18n/locales/ko/settings.json

@@ -39,7 +39,7 @@
 	"codeIndex": {
 		"title": "코드베이스 인덱싱",
 		"enableLabel": "코드베이스 인덱싱 활성화",
-		"enableDescription": "<0>코드베이스 인덱싱</0>은 AI 임베딩을 사용하여 프로젝트의 의미론적 검색 인덱스를 생성하는 실험적 기능입니다. 이를 통해 Roo Code는 단순한 키워드가 아닌 의미를 기반으로 관련 코드를 찾아 대규모 코드베이스를 더 잘 이해하고 탐색할 수 있습니다.",
+		"enableDescription": "향상된 검색 및 컨텍스트 이해를 위해 코드 인덱싱 활성화",
 		"providerLabel": "임베딩 제공자",
 		"selectProviderPlaceholder": "제공자 선택",
 		"openaiProvider": "OpenAI",

+ 1 - 1
webview-ui/src/i18n/locales/nl/settings.json

@@ -39,7 +39,7 @@
 	"codeIndex": {
 		"title": "Codebase indexering",
 		"enableLabel": "Codebase indexering inschakelen",
-		"enableDescription": "<0>Codebase indexering</0> is een experimentele functie die een semantische zoekindex van je project creëert met behulp van AI-embeddings. Dit stelt Roo Code in staat om grote codebases beter te begrijpen en te navigeren door relevante code te vinden op basis van betekenis in plaats van alleen trefwoorden.",
+		"enableDescription": "Code-indexering inschakelen voor verbeterde zoekresultaten en contextbegrip",
 		"providerLabel": "Embeddings provider",
 		"selectProviderPlaceholder": "Selecteer provider",
 		"openaiProvider": "OpenAI",

+ 1 - 1
webview-ui/src/i18n/locales/pl/settings.json

@@ -39,7 +39,7 @@
 	"codeIndex": {
 		"title": "Indeksowanie kodu",
 		"enableLabel": "Włącz indeksowanie kodu",
-		"enableDescription": "<0>Indeksowanie kodu</0> to eksperymentalna funkcja, która tworzy semantyczny indeks wyszukiwania Twojego projektu przy użyciu osadzeń AI. Umożliwia to Roo Code lepsze zrozumienie i nawigację po dużych bazach kodu poprzez znajdowanie odpowiedniego kodu na podstawie znaczenia, a nie tylko słów kluczowych.",
+		"enableDescription": "Włącz indeksowanie kodu, aby poprawić wyszukiwanie i zrozumienie kontekstu",
 		"providerLabel": "Dostawca osadzania",
 		"selectProviderPlaceholder": "Wybierz dostawcę",
 		"openaiProvider": "OpenAI",

+ 1 - 1
webview-ui/src/i18n/locales/pt-BR/settings.json

@@ -39,7 +39,7 @@
 	"codeIndex": {
 		"title": "Indexação de Código",
 		"enableLabel": "Ativar Indexação de Código",
-		"enableDescription": "<0>Indexação de Código</0> é um recurso experimental que cria um índice de busca semântica do seu projeto usando embeddings de IA. Isso permite ao Roo Code entender melhor e navegar grandes bases de código encontrando código relevante baseado em significado ao invés de apenas palavras-chave.",
+		"enableDescription": "Ative a indexação de código para pesquisa e compreensão de contexto aprimoradas",
 		"providerLabel": "Provedor de Embeddings",
 		"selectProviderPlaceholder": "Selecionar provedor",
 		"openaiProvider": "OpenAI",

+ 1 - 1
webview-ui/src/i18n/locales/ru/settings.json

@@ -39,7 +39,7 @@
 	"codeIndex": {
 		"title": "Индексация кодовой базы",
 		"enableLabel": "Включить индексацию кодовой базы",
-		"enableDescription": "<0>Индексация кодовой базы</0> — это экспериментальная функция, которая создает семантический поисковый индекс вашего проекта с использованием ИИ-эмбеддингов. Это позволяет Roo Code лучше понимать и навигировать по большим кодовым базам, находя релевантный код на основе смысла, а не только ключевых слов.",
+		"enableDescription": "Включите индексацию кода для улучшения поиска и понимания контекста",
 		"providerLabel": "Провайдер эмбеддингов",
 		"selectProviderPlaceholder": "Выберите провайдера",
 		"openaiProvider": "OpenAI",

+ 1 - 1
webview-ui/src/i18n/locales/tr/settings.json

@@ -39,7 +39,7 @@
 	"codeIndex": {
 		"title": "Kod Tabanı İndeksleme",
 		"enableLabel": "Kod Tabanı İndekslemeyi Etkinleştir",
-		"enableDescription": "<0>Kod Tabanı İndeksleme</0>, AI gömme teknolojisini kullanarak projenizin semantik arama indeksini oluşturan deneysel bir özelliktir. Bu, Roo Code'un sadece anahtar kelimeler yerine anlam temelinde ilgili kodu bularak büyük kod tabanlarını daha iyi anlamasını ve gezinmesini sağlar.",
+		"enableDescription": "Geliştirilmiş arama ve bağlam anlayışı için kod indekslemeyi etkinleştirin",
 		"providerLabel": "Gömme Sağlayıcısı",
 		"selectProviderPlaceholder": "Sağlayıcı seç",
 		"openaiProvider": "OpenAI",

+ 1 - 1
webview-ui/src/i18n/locales/vi/settings.json

@@ -39,7 +39,7 @@
 	"codeIndex": {
 		"title": "Lập chỉ mục mã nguồn",
 		"enableLabel": "Bật lập chỉ mục mã nguồn",
-		"enableDescription": "<0>Lập chỉ mục mã nguồn</0> là một tính năng thử nghiệm tạo ra chỉ mục tìm kiếm ngữ nghĩa cho dự án của bạn bằng cách sử dụng AI embeddings. Điều này cho phép Roo Code hiểu rõ hơn và điều hướng các codebase lớn bằng cách tìm mã liên quan dựa trên ý nghĩa thay vì chỉ từ khóa.",
+		"enableDescription": "Bật lập chỉ mục mã để cải thiện tìm kiếm và sự hiểu biết về ngữ cảnh",
 		"providerLabel": "Nhà cung cấp nhúng",
 		"selectProviderPlaceholder": "Chọn nhà cung cấp",
 		"openaiProvider": "OpenAI",

+ 1 - 1
webview-ui/src/i18n/locales/zh-CN/settings.json

@@ -41,7 +41,7 @@
 		"description": "配置代码库索引设置以启用项目的语义搜索。<0>了解更多</0>",
 		"statusTitle": "状态",
 		"enableLabel": "启用代码库索引",
-		"enableDescription": "<0>代码库索引</0>是一个实验性功能,使用 AI 嵌入为您的项目创建语义搜索索引。这使 Roo Code 能够通过基于含义而非仅仅关键词来查找相关代码,从而更好地理解和导航大型代码库。",
+		"enableDescription": "启用代码索引以改进搜索和上下文理解",
 		"settingsTitle": "索引设置",
 		"disabledMessage": "代码库索引当前已禁用。在全局设置中启用它以配置索引选项。",
 		"providerLabel": "嵌入提供商",

+ 1 - 1
webview-ui/src/i18n/locales/zh-TW/settings.json

@@ -39,7 +39,7 @@
 	"codeIndex": {
 		"title": "程式碼庫索引",
 		"enableLabel": "啟用程式碼庫索引",
-		"enableDescription": "<0>程式碼庫索引</0>是一個實驗性功能,使用 AI 嵌入為您的專案建立語義搜尋索引。這使 Roo Code 能夠透過基於含義而非僅僅關鍵詞來尋找相關程式碼,從而更好地理解和導覽大型程式碼庫。",
+		"enableDescription": "啟用程式碼索引以改進搜尋和上下文理解",
 		"providerLabel": "嵌入提供者",
 		"selectProviderPlaceholder": "選擇提供者",
 		"openaiProvider": "OpenAI",