feat: add toggle to disable codebase indexing (#5623)

- Add toggle switch to CodeIndexPopover UI component
- Add conditional rendering for setup/advanced settings sections
- Update backend logic to respect codebaseIndexEnabled setting
- Implement proper state caching for restart detection
- Add comprehensive test coverage for enable/disable functionality
- Add translation keys for toggle UI elements

Fixes #5623
This commit is contained in:
Roo Code 2025-07-12 13:51:14 +00:00
parent 50e45a2bc6
commit c6f32c1584
4 changed files with 672 additions and 502 deletions

View file

@ -343,6 +343,89 @@ describe("CodeIndexConfigManager", () => {
expect(result.requiresRestart).toBe(true)
})
it("should detect restart requirement when feature is enabled", async () => {
// Create a fresh config manager to ensure clean state
const freshConfigManager = new CodeIndexConfigManager(mockContextProxy)
// Initial state - disabled
mockContextProxy.getGlobalState.mockReturnValue({
codebaseIndexEnabled: false,
codebaseIndexQdrantUrl: "http://qdrant.local",
codebaseIndexEmbedderProvider: "openai",
codebaseIndexEmbedderModelId: "text-embedding-3-small",
})
setupSecretMocks({
codeIndexOpenAiKey: "test-key",
codeIndexQdrantApiKey: "test-key",
})
await freshConfigManager.loadConfiguration()
// Enable the feature
mockContextProxy.getGlobalState.mockReturnValue({
codebaseIndexEnabled: true,
codebaseIndexQdrantUrl: "http://qdrant.local",
codebaseIndexEmbedderProvider: "openai",
codebaseIndexEmbedderModelId: "text-embedding-3-small",
})
const result = await freshConfigManager.loadConfiguration()
expect(result.requiresRestart).toBe(true)
})
it("should detect restart requirement when feature is disabled", async () => {
// Create a fresh config manager to ensure clean state
const freshConfigManager = new CodeIndexConfigManager(mockContextProxy)
// Initial state - enabled
mockContextProxy.getGlobalState.mockReturnValue({
codebaseIndexEnabled: true,
codebaseIndexQdrantUrl: "http://qdrant.local",
codebaseIndexEmbedderProvider: "openai",
codebaseIndexEmbedderModelId: "text-embedding-3-small",
})
setupSecretMocks({
codeIndexOpenAiKey: "test-key",
codeIndexQdrantApiKey: "test-key",
})
await freshConfigManager.loadConfiguration()
// Disable the feature
mockContextProxy.getGlobalState.mockReturnValue({
codebaseIndexEnabled: false,
codebaseIndexQdrantUrl: "http://qdrant.local",
codebaseIndexEmbedderProvider: "openai",
codebaseIndexEmbedderModelId: "text-embedding-3-small",
})
const result = await freshConfigManager.loadConfiguration()
expect(result.requiresRestart).toBe(true)
})
it("should not require restart when feature remains disabled", async () => {
// Initial state - disabled
mockContextProxy.getGlobalState.mockReturnValue({
codebaseIndexEnabled: false,
codebaseIndexQdrantUrl: "http://qdrant.local",
codebaseIndexEmbedderProvider: "openai",
codebaseIndexEmbedderModelId: "text-embedding-3-small",
})
await configManager.loadConfiguration()
// Change other settings while keeping feature disabled
mockContextProxy.getGlobalState.mockReturnValue({
codebaseIndexEnabled: false,
codebaseIndexQdrantUrl: "http://qdrant.local",
codebaseIndexEmbedderProvider: "openai",
codebaseIndexEmbedderModelId: "text-embedding-3-large", // Changed model
})
const result = await configManager.loadConfiguration()
expect(result.requiresRestart).toBe(false)
})
describe("simplified restart detection", () => {
it("should detect restart requirement for API key changes", async () => {
// Initial state
@ -1215,7 +1298,39 @@ describe("CodeIndexConfigManager", () => {
})
})
it("should return correct feature enabled state", () => {
it("should return correct feature enabled state when enabled", () => {
expect(configManager.isFeatureEnabled).toBe(true)
})
it("should return false when codebaseIndexEnabled is false", async () => {
mockContextProxy.getGlobalState.mockReturnValue({
codebaseIndexEnabled: false,
codebaseIndexQdrantUrl: "http://qdrant.local",
codebaseIndexEmbedderProvider: "openai",
codebaseIndexEmbedderModelId: "text-embedding-3-large",
})
setupSecretMocks({
codeIndexOpenAiKey: "test-openai-key",
codeIndexQdrantApiKey: "test-qdrant-key",
})
await configManager.loadConfiguration()
expect(configManager.isFeatureEnabled).toBe(false)
})
it("should return true when codebaseIndexEnabled is undefined (default)", async () => {
mockContextProxy.getGlobalState.mockReturnValue({
// codebaseIndexEnabled is undefined, should default to true
codebaseIndexQdrantUrl: "http://qdrant.local",
codebaseIndexEmbedderProvider: "openai",
codebaseIndexEmbedderModelId: "text-embedding-3-large",
})
setupSecretMocks({
codeIndexOpenAiKey: "test-openai-key",
codeIndexQdrantApiKey: "test-qdrant-key",
})
await configManager.loadConfiguration()
expect(configManager.isFeatureEnabled).toBe(true)
})

View file

@ -21,6 +21,7 @@ export class CodeIndexConfigManager {
private qdrantApiKey?: string
private searchMinScore?: number
private searchMaxResults?: number
private codebaseIndexEnabled?: boolean
constructor(private readonly contextProxy: ContextProxy) {
// Initialize with current configuration to avoid false restart triggers
@ -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
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.isFeatureEnabled,
configured: this.isConfigured(),
embedderProvider: this.embedderProvider,
modelId: this.modelId,
@ -243,8 +244,13 @@ 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
// 1. Check for enable/disable transitions
const nowEnabled = this.isFeatureEnabled
if (prevEnabled !== nowEnabled) {
return true
}
// 2. Transition from unconfigured to configured
if (!prevConfigured && nowConfigured) {
return true
}
@ -255,7 +261,6 @@ export class CodeIndexConfigManager {
}
// 4. CRITICAL CHANGES - Always restart for these
// Since feature is always enabled, we always check for critical changes
// Provider change
if (prevProvider !== this.embedderProvider) {
@ -354,7 +359,7 @@ export class CodeIndexConfigManager {
* Gets whether the code indexing feature is enabled
*/
public get isFeatureEnabled(): boolean {
return true
return this.codebaseIndexEnabled ?? true
}
/**

File diff suppressed because it is too large Load diff

View file

@ -40,6 +40,8 @@
"title": "Codebase Indexing",
"description": "Configure codebase indexing settings to enable semantic search of your project. <0>Learn more</0>",
"statusTitle": "Status",
"enabledLabel": "Enable Codebase Indexing",
"enabledDescription": "Toggle codebase indexing on or off. When disabled, the codebase_search tool will not be available.",
"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.",
"settingsTitle": "Indexing Settings",