Roo-Code/src/services/code-index/state-manager.ts
James Mtendamema 67ea856fd0
feat: add per-workspace indexing opt-in and stop/cancel control (#11456)
* feat: add per-workspace indexing opt-in and stop/cancel control

- Add codeIndexWorkspaceEnabled flag in workspaceState (default: false)
- Thread AbortController/AbortSignal through orchestrator → scanner
- Add Stop Indexing button and Stopping state to UI
- Fix handleSettingsChange() to abort active scan when disabling toggle
- Add translations for all 18 locales

* fix: correct abort handling in indexing scanner and orchestrator

- Re-throw AbortError in scanner's file processing catch block to prevent
  abort signals from being silently swallowed as file errors
- Reorder stopWatcher() before setSystemState() in orchestrator abort
  catch path to ensure watcher cleanup before state transition
- Update scanner test to assert AbortError propagation on mid-scan abort

* fix: optimize workspace check ordering, translate new i18n keys, fix abort handling

- Move workspace-enabled check before _recreateServices() in initialize()
  to avoid creating Qdrant/embedder connections for disabled workspaces
- Translate new i18n keys (indexingStopped, indexingStoppedPartial, stopping,
  stopIndexingButton, stoppingButton, workspaceToggleLabel,
  workspaceDisabledMessage) in all 17 non-English locales
- Re-throw AbortError in scanner catch block to prevent silent swallowing
- Reorder stopWatcher() before setSystemState() in orchestrator abort path
- Update scanner test to assert AbortError propagation on mid-scan abort
- Fix recoverFromError test for workspace-enabled check ordering

* fix: per-folder enablement key, abort-safe dispose and back-pressure, translate i18n

Addresses 0xMink review feedback:
- Store workspace enablement keyed by folder path to support multi-root
  workspaces (codeIndexWorkspaceEnabled:<path> instead of single boolean)
- Add test proving folder A enabled does not enable folder B
- dispose() now calls stopIndexing() to abort orphaned scans on folder removal
- Scanner back-pressure loop checks abort signal to avoid spin-waiting
- Move workspace-enabled check before _recreateServices() in initialize()
- Translate new i18n keys in all 17 non-English locales
- Fix abort handling in orchestrator and scanner catch blocks

* fix: flush debounced cache writes on abort to preserve indexing progress

* feat: add global auto-enable default for backward-compatible workspace indexing

* fix: stop/start indexer when auto-enable default changes effective state

* fix: URI-keyed enablement, throw AbortError in back-pressure, stopWatcher on early-return

* fix: iterate all managers when auto-enable default changes in multi-root workspaces

---------

Co-authored-by: James Mtendamema <jmtendamema@geologicai.com>
2026-02-18 23:09:17 -07:00

119 lines
4.1 KiB
TypeScript

import * as vscode from "vscode"
export type IndexingState = "Standby" | "Indexing" | "Indexed" | "Error" | "Stopping"
export class CodeIndexStateManager {
private _systemStatus: IndexingState = "Standby"
private _statusMessage: string = ""
private _processedItems: number = 0
private _totalItems: number = 0
private _currentItemUnit: string = "blocks"
private _progressEmitter = new vscode.EventEmitter<ReturnType<typeof this.getCurrentStatus>>()
// --- Public API ---
public readonly onProgressUpdate = this._progressEmitter.event
public get state(): IndexingState {
return this._systemStatus
}
public getCurrentStatus() {
return {
systemStatus: this._systemStatus,
message: this._statusMessage,
processedItems: this._processedItems,
totalItems: this._totalItems,
currentItemUnit: this._currentItemUnit,
}
}
// --- State Management ---
public setSystemState(newState: IndexingState, message?: string): void {
const stateChanged =
newState !== this._systemStatus || (message !== undefined && message !== this._statusMessage)
if (stateChanged) {
this._systemStatus = newState
if (message !== undefined) {
this._statusMessage = message
}
// Reset progress counters if moving to a non-indexing state or starting fresh
if (newState !== "Indexing") {
this._processedItems = 0
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."
}
this._progressEmitter.fire(this.getCurrentStatus())
}
}
public reportBlockIndexingProgress(processedItems: number, totalItems: number): void {
const progressChanged = processedItems !== this._processedItems || totalItems !== this._totalItems
// Don't override Stopping state with progress updates
if (this._systemStatus === "Stopping") return
// Update if progress changes OR if the system wasn't already in 'Indexing' state
if (progressChanged || this._systemStatus !== "Indexing") {
this._processedItems = processedItems
this._totalItems = totalItems
this._currentItemUnit = "blocks"
const message = `Indexed ${this._processedItems} / ${this._totalItems} ${this._currentItemUnit} found`
const oldStatus = this._systemStatus
const oldMessage = this._statusMessage
this._systemStatus = "Indexing" // Ensure state is Indexing
this._statusMessage = message
// Only fire update if status, message or progress actually changed
if (oldStatus !== this._systemStatus || oldMessage !== this._statusMessage || progressChanged) {
this._progressEmitter.fire(this.getCurrentStatus())
}
}
}
public reportFileQueueProgress(processedFiles: number, totalFiles: number, currentFileBasename?: string): void {
const progressChanged = processedFiles !== this._processedItems || totalFiles !== this._totalItems
// Don't override Stopping state with progress updates
if (this._systemStatus === "Stopping") return
if (progressChanged || this._systemStatus !== "Indexing") {
this._processedItems = processedFiles
this._totalItems = totalFiles
this._currentItemUnit = "files"
this._systemStatus = "Indexing"
let message: string
if (totalFiles > 0 && processedFiles < totalFiles) {
message = `Processing ${processedFiles} / ${totalFiles} ${this._currentItemUnit}. Current: ${
currentFileBasename || "..."
}`
} else if (totalFiles > 0 && processedFiles === totalFiles) {
message = `Finished processing ${totalFiles} ${this._currentItemUnit} from queue.`
} else {
message = `File queue processed.`
}
const oldStatus = this._systemStatus
const oldMessage = this._statusMessage
this._statusMessage = message
if (oldStatus !== this._systemStatus || oldMessage !== this._statusMessage || progressChanged) {
this._progressEmitter.fire(this.getCurrentStatus())
}
}
}
public dispose(): void {
this._progressEmitter.dispose()
}
}