From 29b437a775353be1ed3a52209ef1e9450ee092a6 Mon Sep 17 00:00:00 2001 From: Roo Code Date: Sat, 3 Jan 2026 22:49:30 +0000 Subject: [PATCH] feat: add option to pause file-watching after initial indexing This addresses Issue #8131 by adding a workspace-scoped setting roo-cline.codeIndex.fileWatchingEnabled that allows users to: - Enable initial indexing for codebase_search functionality - Pause file-watching to reduce CPU usage (especially useful with git worktrees) - Each workspace/worktree maintains independent settings Changes: - Add fileWatchingEnabled setting in package.json (scope: resource) - Add IndexedPaused state to the indexing state machine - Update ConfigManager with isFileWatchingEnabled getter - Modify orchestrator to conditionally start/stop file watcher - Add comprehensive tests for the new functionality --- src/package.json | 20 ++- src/package.nls.json | 1 + .../__tests__/file-watching-pause.spec.ts | 156 ++++++++++++++++++ src/services/code-index/config-manager.ts | 17 ++ src/services/code-index/interfaces/manager.ts | 2 +- src/services/code-index/manager.ts | 16 +- src/services/code-index/orchestrator.ts | 51 ++++-- src/services/code-index/state-manager.ts | 10 +- 8 files changed, 240 insertions(+), 33 deletions(-) create mode 100644 src/services/code-index/__tests__/file-watching-pause.spec.ts diff --git a/src/package.json b/src/package.json index 2bd14a7c0d8..9e2d7c38af5 100644 --- a/src/package.json +++ b/src/package.json @@ -401,13 +401,19 @@ "description": "%settings.newTaskRequireTodos.description%" }, "roo-cline.codeIndex.embeddingBatchSize": { - "type": "number", - "default": 60, - "minimum": 1, - "maximum": 200, - "description": "%settings.codeIndex.embeddingBatchSize.description%" - }, - "roo-cline.debug": { + "type": "number", + "default": 60, + "minimum": 1, + "maximum": 200, + "description": "%settings.codeIndex.embeddingBatchSize.description%" + }, + "roo-cline.codeIndex.fileWatchingEnabled": { + "type": "boolean", + "default": true, + "scope": "resource", + "description": "%settings.codeIndex.fileWatchingEnabled.description%" + }, + "roo-cline.debug": { "type": "boolean", "default": false, "description": "%settings.debug.description%" diff --git a/src/package.nls.json b/src/package.nls.json index 0030d1ce7b2..ee4c9fa78c0 100644 --- a/src/package.nls.json +++ b/src/package.nls.json @@ -42,5 +42,6 @@ "settings.apiRequestTimeout.description": "Maximum time in seconds to wait for API responses (0 = no timeout, 1-3600s, default: 600s). Higher values are recommended for local providers like LM Studio and Ollama that may need more processing time.", "settings.newTaskRequireTodos.description": "Require todos parameter when creating new tasks with the new_task tool", "settings.codeIndex.embeddingBatchSize.description": "The batch size for embedding operations during code indexing. Adjust this based on your API provider's limits. Default is 60.", + "settings.codeIndex.fileWatchingEnabled.description": "Enable file watching to automatically update the index when files change. Disable to reduce CPU usage after initial indexing. When disabled, use 'Re-Index' to manually update. (Workspace setting)", "settings.debug.description": "Enable debug mode to show additional buttons for viewing API conversation history and UI messages as prettified JSON in temporary files." } diff --git a/src/services/code-index/__tests__/file-watching-pause.spec.ts b/src/services/code-index/__tests__/file-watching-pause.spec.ts new file mode 100644 index 00000000000..257c11d8834 --- /dev/null +++ b/src/services/code-index/__tests__/file-watching-pause.spec.ts @@ -0,0 +1,156 @@ +// npx vitest services/code-index/__tests__/file-watching-pause.spec.ts + +import { CodeIndexConfigManager } from "../config-manager" +import { CodeIndexStateManager } from "../state-manager" + +// Mock vscode +vi.mock("vscode", () => ({ + workspace: { + getConfiguration: vi.fn().mockImplementation(() => ({ + get: vi.fn().mockReturnValue(true), + })), + workspaceFolders: [{ uri: { fsPath: "/test/workspace" } }], + }, + EventEmitter: vi.fn().mockImplementation(() => ({ + event: vi.fn(), + fire: vi.fn(), + dispose: vi.fn(), + })), +})) + +// Mock ContextProxy +vi.mock("../../../core/config/ContextProxy") + +// Mock embeddingModels module +vi.mock("../../../shared/embeddingModels") + +// Mock Package +vi.mock("../../../shared/package", () => ({ + Package: { + name: "roo-cline", + }, +})) + +import * as vscode from "vscode" + +describe("File Watching Pause Feature", () => { + let mockContextProxy: any + + beforeEach(() => { + vi.clearAllMocks() + + mockContextProxy = { + getGlobalState: vi.fn(), + getSecret: vi.fn().mockReturnValue(undefined), + refreshSecrets: vi.fn().mockResolvedValue(undefined), + updateGlobalState: vi.fn(), + } + }) + + describe("CodeIndexConfigManager.isFileWatchingEnabled", () => { + it("should return true when file watching is enabled in workspace settings", () => { + // Mock vscode.workspace.getConfiguration to return true + const mockGetConfiguration = vi.mocked(vscode.workspace.getConfiguration) + mockGetConfiguration.mockReturnValue({ + get: vi.fn().mockReturnValue(true), + } as any) + + mockContextProxy.getGlobalState.mockReturnValue({ + codebaseIndexEnabled: true, + }) + + const configManager = new CodeIndexConfigManager(mockContextProxy) + expect(configManager.isFileWatchingEnabled).toBe(true) + }) + + it("should return false when file watching is disabled in workspace settings", () => { + // Mock vscode.workspace.getConfiguration to return false + const mockGetConfiguration = vi.mocked(vscode.workspace.getConfiguration) + mockGetConfiguration.mockReturnValue({ + get: vi.fn().mockReturnValue(false), + } as any) + + mockContextProxy.getGlobalState.mockReturnValue({ + codebaseIndexEnabled: true, + }) + + const configManager = new CodeIndexConfigManager(mockContextProxy) + expect(configManager.isFileWatchingEnabled).toBe(false) + }) + + it("should default to true when file watching setting is not set", () => { + // Mock vscode.workspace.getConfiguration to return default value + const mockGetConfiguration = vi.mocked(vscode.workspace.getConfiguration) + mockGetConfiguration.mockReturnValue({ + get: vi.fn().mockImplementation((key: string, defaultValue: any) => defaultValue), + } as any) + + mockContextProxy.getGlobalState.mockReturnValue({ + codebaseIndexEnabled: true, + }) + + const configManager = new CodeIndexConfigManager(mockContextProxy) + expect(configManager.isFileWatchingEnabled).toBe(true) + }) + }) + + describe("CodeIndexStateManager IndexedPaused state", () => { + it("should set state to IndexedPaused with default message", () => { + const stateManager = new CodeIndexStateManager() + + stateManager.setSystemState("IndexedPaused") + + const status = stateManager.getCurrentStatus() + expect(status.systemStatus).toBe("IndexedPaused") + expect(status.message).toBe("Index ready. File watching paused.") + }) + + it("should set state to IndexedPaused with custom message", () => { + const stateManager = new CodeIndexStateManager() + + stateManager.setSystemState("IndexedPaused", "Custom paused message") + + const status = stateManager.getCurrentStatus() + expect(status.systemStatus).toBe("IndexedPaused") + expect(status.message).toBe("Custom paused message") + }) + + it("should reset progress counters when transitioning to IndexedPaused", () => { + const stateManager = new CodeIndexStateManager() + + // First set to indexing with progress + stateManager.reportBlockIndexingProgress(50, 100) + + // Then transition to IndexedPaused + stateManager.setSystemState("IndexedPaused") + + const status = stateManager.getCurrentStatus() + expect(status.processedItems).toBe(0) + expect(status.totalItems).toBe(0) + }) + + it("should allow transition from IndexedPaused to Indexing", () => { + const stateManager = new CodeIndexStateManager() + + // Start in IndexedPaused state + stateManager.setSystemState("IndexedPaused") + + // Then transition to Indexing (simulating re-index) + stateManager.setSystemState("Indexing", "Re-indexing...") + + const status = stateManager.getCurrentStatus() + expect(status.systemStatus).toBe("Indexing") + expect(status.message).toBe("Re-indexing...") + }) + }) + + describe("IndexingState type includes IndexedPaused", () => { + it("should accept IndexedPaused as valid state", () => { + const stateManager = new CodeIndexStateManager() + + // Should not throw + stateManager.setSystemState("IndexedPaused") + expect(stateManager.state).toBe("IndexedPaused") + }) + }) +}) diff --git a/src/services/code-index/config-manager.ts b/src/services/code-index/config-manager.ts index e7f239e621f..6650c2a9506 100644 --- a/src/services/code-index/config-manager.ts +++ b/src/services/code-index/config-manager.ts @@ -1,9 +1,11 @@ +import * as vscode from "vscode" import { ApiHandlerOptions } from "../../shared/api" import { ContextProxy } from "../../core/config/ContextProxy" import { EmbedderProvider } from "./interfaces/manager" import { CodeIndexConfig, PreviousConfigSnapshot } from "./interfaces/config" import { DEFAULT_SEARCH_MIN_SCORE, DEFAULT_MAX_SEARCH_RESULTS } from "./constants" import { getDefaultModelId, getModelDimension, getModelScoreThreshold } from "../../shared/embeddingModels" +import { Package } from "../../shared/package" /** * Manages configuration state and validation for the code indexing feature. @@ -541,4 +543,19 @@ export class CodeIndexConfigManager { public get currentSearchMaxResults(): number { return this.searchMaxResults ?? DEFAULT_MAX_SEARCH_RESULTS } + + /** + * Gets whether file watching is enabled for the current workspace. + * This is a workspace-scoped setting that allows users to disable file watching + * to reduce CPU usage while still using the codebase search with an existing index. + * Defaults to true (file watching enabled). + */ + public get isFileWatchingEnabled(): boolean { + try { + return vscode.workspace.getConfiguration(Package.name).get("codeIndex.fileWatchingEnabled", true) + } catch { + // In test environment, vscode.workspace might not be available + return true + } + } } diff --git a/src/services/code-index/interfaces/manager.ts b/src/services/code-index/interfaces/manager.ts index 28ff5523277..44c7cf4210c 100644 --- a/src/services/code-index/interfaces/manager.ts +++ b/src/services/code-index/interfaces/manager.ts @@ -69,7 +69,7 @@ export interface ICodeIndexManager { dispose(): void } -export type IndexingState = "Standby" | "Indexing" | "Indexed" | "Error" +export type IndexingState = "Standby" | "Indexing" | "Indexed" | "IndexedPaused" | "Error" export type EmbedderProvider = | "openai" | "ollama" diff --git a/src/services/code-index/manager.ts b/src/services/code-index/manager.ts index dd79a3f1616..7386d91126c 100644 --- a/src/services/code-index/manager.ts +++ b/src/services/code-index/manager.ts @@ -98,10 +98,18 @@ export class CodeIndexManager { } public get isFeatureConfigured(): boolean { - return this._configManager?.isFeatureConfigured ?? false - } - - public get isInitialized(): boolean { + return this._configManager?.isFeatureConfigured ?? false + } + + /** + * Gets whether file watching is enabled for the current workspace. + * When disabled, the index can still be used for searches but won't auto-update. + */ + public get isFileWatchingEnabled(): boolean { + return this._configManager?.isFileWatchingEnabled ?? true + } + + public get isInitialized(): boolean { try { this.assertInitialized() return true diff --git a/src/services/code-index/orchestrator.ts b/src/services/code-index/orchestrator.ts index 99f317882b8..5a5bb3217ae 100644 --- a/src/services/code-index/orchestrator.ts +++ b/src/services/code-index/orchestrator.ts @@ -109,11 +109,12 @@ export class CodeIndexOrchestrator { } if ( - this._isProcessing || - (this.stateManager.state !== "Standby" && - this.stateManager.state !== "Error" && - this.stateManager.state !== "Indexed") - ) { + this._isProcessing || + (this.stateManager.state !== "Standby" && + this.stateManager.state !== "Error" && + this.stateManager.state !== "Indexed" && + this.stateManager.state !== "IndexedPaused") + ) { console.warn( `[CodeIndexOrchestrator] Start rejected: Already processing or in state ${this.stateManager.state}.`, ) @@ -193,12 +194,20 @@ export class CodeIndexOrchestrator { console.log("[CodeIndexOrchestrator] No new or changed files found") } - await this._startWatcher() - - // Mark indexing as complete after successful incremental scan - await this.vectorStore.markIndexingComplete() - - this.stateManager.setSystemState("Indexed", t("embeddings:orchestrator.fileWatcherStarted")) + // Check if file watching is enabled before starting watcher + if (this.configManager.isFileWatchingEnabled) { + await this._startWatcher() + // Mark indexing as complete after successful incremental scan + await this.vectorStore.markIndexingComplete() + this.stateManager.setSystemState("Indexed", t("embeddings:orchestrator.fileWatcherStarted")) + } else { + // Mark indexing as complete but skip file watching + await this.vectorStore.markIndexingComplete() + this.stateManager.setSystemState( + "IndexedPaused", + "Index ready. File watching disabled. Use 'Re-Index' to update.", + ) + } } else { // No existing data or collection was just created - do a full scan this.stateManager.setSystemState("Indexing", "Services ready. Starting workspace scan...") @@ -274,12 +283,20 @@ export class CodeIndexOrchestrator { throw new Error(t("embeddings:orchestrator.indexingFailedCritical")) } - await this._startWatcher() - - // Mark indexing as complete after successful full scan - await this.vectorStore.markIndexingComplete() - - this.stateManager.setSystemState("Indexed", t("embeddings:orchestrator.fileWatcherStarted")) + // Check if file watching is enabled before starting watcher + if (this.configManager.isFileWatchingEnabled) { + await this._startWatcher() + // Mark indexing as complete after successful full scan + await this.vectorStore.markIndexingComplete() + this.stateManager.setSystemState("Indexed", t("embeddings:orchestrator.fileWatcherStarted")) + } else { + // Mark indexing as complete but skip file watching + await this.vectorStore.markIndexingComplete() + this.stateManager.setSystemState( + "IndexedPaused", + "Index ready. File watching disabled. Use 'Re-Index' to update.", + ) + } } } catch (error: any) { console.error("[CodeIndexOrchestrator] Error during indexing:", error) diff --git a/src/services/code-index/state-manager.ts b/src/services/code-index/state-manager.ts index 90257fdfb19..2e141669f6a 100644 --- a/src/services/code-index/state-manager.ts +++ b/src/services/code-index/state-manager.ts @@ -1,6 +1,6 @@ import * as vscode from "vscode" -export type IndexingState = "Standby" | "Indexing" | "Indexed" | "Error" +export type IndexingState = "Standby" | "Indexing" | "Indexed" | "IndexedPaused" | "Error" export class CodeIndexStateManager { private _systemStatus: IndexingState = "Standby" @@ -46,9 +46,11 @@ export class CodeIndexStateManager { this._totalItems = 0 this._currentItemUnit = "blocks" // Reset to default unit // Optionally clear the message or set a default for non-indexing states - if (newState === "Standby" && message === undefined) this._statusMessage = "Ready." - if (newState === "Indexed" && message === undefined) this._statusMessage = "Index up-to-date." - if (newState === "Error" && message === undefined) this._statusMessage = "An error occurred." + if (newState === "Standby" && message === undefined) this._statusMessage = "Ready." + if (newState === "Indexed" && message === undefined) this._statusMessage = "Index up-to-date." + if (newState === "IndexedPaused" && message === undefined) + this._statusMessage = "Index ready. File watching paused." + if (newState === "Error" && message === undefined) this._statusMessage = "An error occurred." } this._progressEmitter.fire(this.getCurrentStatus())