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
This commit is contained in:
Roo Code 2026-01-03 22:49:30 +00:00
parent 1459f0113b
commit 29b437a775
8 changed files with 240 additions and 33 deletions

View file

@ -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%"

View file

@ -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."
}

View file

@ -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")
})
})
})

View file

@ -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<boolean>("codeIndex.fileWatchingEnabled", true)
} catch {
// In test environment, vscode.workspace might not be available
return true
}
}
}

View file

@ -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"

View file

@ -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

View file

@ -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)

View file

@ -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())