diff --git a/.changeset/indexing-workspace-opt-in-and-stop-control.md b/.changeset/indexing-workspace-opt-in-and-stop-control.md new file mode 100644 index 00000000000..27a1fbe0b0e --- /dev/null +++ b/.changeset/indexing-workspace-opt-in-and-stop-control.md @@ -0,0 +1,9 @@ +--- +"roo-cline": minor +--- + +Add per-workspace indexing opt-in and stop/cancel indexing controls + +- **Per-workspace indexing opt-in**: Indexing no longer auto-starts on every workspace. A new `codeIndexWorkspaceEnabled` flag (stored in `workspaceState`, default: false) requires users to explicitly enable indexing per workspace via a toggle in the CodeIndex popover. The choice is remembered across sessions. +- **Stop/cancel indexing**: Users can stop an in-progress indexing operation via a "Stop Indexing" button. Uses `AbortController`/`AbortSignal` threaded through the orchestrator → scanner pipeline with graceful abort at file and batch boundaries. +- **Disable toggle bug fix**: Unchecking "Enable Codebase Indexing" during active indexing now properly stops the scan via `stopIndexing()` instead of only calling `stopWatcher()`, which left the scanner running asynchronously. diff --git a/packages/types/src/vscode-extension-host.ts b/packages/types/src/vscode-extension-host.ts index a72f087d24c..d5d7ea6d5c0 100644 --- a/packages/types/src/vscode-extension-host.ts +++ b/packages/types/src/vscode-extension-host.ts @@ -507,9 +507,12 @@ export interface WebviewMessage { | "condenseTaskContextRequest" | "requestIndexingStatus" | "startIndexing" + | "stopIndexing" | "clearIndexData" | "indexingStatusUpdate" | "indexCleared" + | "toggleWorkspaceIndexing" + | "setAutoEnableDefault" | "focusPanelRequest" | "openExternal" | "filterMarketplaceItems" @@ -704,7 +707,7 @@ export const checkoutRestorePayloadSchema = z.object({ export type CheckpointRestorePayload = z.infer export interface IndexingStatusPayload { - state: "Standby" | "Indexing" | "Indexed" | "Error" + state: "Standby" | "Indexing" | "Indexed" | "Error" | "Stopping" message: string } @@ -738,6 +741,8 @@ export interface IndexingStatus { totalItems: number currentItemUnit?: string workspacePath?: string + workspaceEnabled?: boolean + autoEnableDefault?: boolean } export interface IndexingStatusUpdateMessage { diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 041cc729835..ddeff675697 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -44,6 +44,7 @@ import { Package } from "../../shared/package" import { type RouterName, toRouterName } from "../../shared/api" import { MessageEnhancer } from "./messageEnhancer" +import { CodeIndexManager } from "../../services/code-index/manager" import { checkExistKey } from "../../shared/checkExistApiConfig" import { experimentDefault } from "../../shared/experiments" import { Terminal } from "../../integrations/terminal/Terminal" @@ -2611,7 +2612,6 @@ export const webviewMessageHandler = async ( try { const manager = provider.getCurrentWorkspaceCodeIndexManager() if (!manager) { - // No workspace open - send error status provider.postMessageToWebview({ type: "indexingStatusUpdate", values: { @@ -2625,23 +2625,19 @@ export const webviewMessageHandler = async ( provider.log("Cannot start indexing: No workspace folder open") return } + + // "Start Indexing" implicitly enables the workspace + await manager.setWorkspaceEnabled(true) + if (manager.isFeatureEnabled && manager.isFeatureConfigured) { - // Mimic extension startup behavior: initialize first, which will - // check if Qdrant container is active and reuse existing collection await manager.initialize(provider.contextProxy) - // Only call startIndexing if we're in a state that requires it - // (e.g., Standby or Error). If already Indexed or Indexing, the - // initialize() call above will have already started the watcher. const currentState = manager.state if (currentState === "Standby" || currentState === "Error") { - // startIndexing now handles error recovery internally manager.startIndexing() - // If startIndexing recovered from error, we need to reinitialize if (!manager.isInitialized) { await manager.initialize(provider.contextProxy) - // Try starting again after initialization if (manager.state === "Standby" || manager.state === "Error") { manager.startIndexing() } @@ -2653,6 +2649,82 @@ export const webviewMessageHandler = async ( } break } + case "stopIndexing": { + try { + const manager = provider.getCurrentWorkspaceCodeIndexManager() + if (!manager) { + provider.log("Cannot stop indexing: No workspace folder open") + return + } + manager.stopIndexing() + provider.postMessageToWebview({ + type: "indexingStatusUpdate", + values: manager.getCurrentStatus(), + }) + } catch (error) { + provider.log(`Error stopping indexing: ${error instanceof Error ? error.message : String(error)}`) + } + break + } + case "toggleWorkspaceIndexing": { + try { + const manager = provider.getCurrentWorkspaceCodeIndexManager() + if (!manager) { + provider.log("Cannot toggle workspace indexing: No workspace folder open") + return + } + const enabled = message.bool ?? false + await manager.setWorkspaceEnabled(enabled) + if (enabled && manager.isFeatureEnabled && manager.isFeatureConfigured) { + await manager.initialize(provider.contextProxy) + manager.startIndexing() + } else if (!enabled) { + manager.stopIndexing() + } + provider.postMessageToWebview({ + type: "indexingStatusUpdate", + values: manager.getCurrentStatus(), + }) + } catch (error) { + provider.log( + `Error toggling workspace indexing: ${error instanceof Error ? error.message : String(error)}`, + ) + } + break + } + case "setAutoEnableDefault": { + try { + const manager = provider.getCurrentWorkspaceCodeIndexManager() + if (!manager) { + provider.log("Cannot set auto-enable default: No workspace folder open") + return + } + // Capture prior state for every manager before persisting the global change + const allManagers = CodeIndexManager.getAllInstances() + const priorStates = new Map(allManagers.map((m) => [m, m.isWorkspaceEnabled])) + await manager.setAutoEnableDefault(message.bool ?? true) + // Apply stop/start to every affected manager + for (const m of allManagers) { + const wasEnabled = priorStates.get(m)! + const isNowEnabled = m.isWorkspaceEnabled + if (wasEnabled && !isNowEnabled) { + m.stopIndexing() + } else if (!wasEnabled && isNowEnabled && m.isFeatureEnabled && m.isFeatureConfigured) { + await m.initialize(provider.contextProxy) + m.startIndexing() + } + } + provider.postMessageToWebview({ + type: "indexingStatusUpdate", + values: manager.getCurrentStatus(), + }) + } catch (error) { + provider.log( + `Error setting auto-enable default: ${error instanceof Error ? error.message : String(error)}`, + ) + } + break + } case "clearIndexData": { try { const manager = provider.getCurrentWorkspaceCodeIndexManager() diff --git a/src/i18n/locales/ca/embeddings.json b/src/i18n/locales/ca/embeddings.json index 21a4a27ab41..9ceec7d05c3 100644 --- a/src/i18n/locales/ca/embeddings.json +++ b/src/i18n/locales/ca/embeddings.json @@ -69,6 +69,8 @@ "fileWatcherStopped": "Monitor de fitxers aturat.", "failedDuringInitialScan": "Ha fallat durant l'escaneig inicial: {{errorMessage}}", "unknownError": "Error desconegut", - "indexingRequiresWorkspace": "Indexació requereix una carpeta de workspace oberta" + "indexingRequiresWorkspace": "Indexació requereix una carpeta de workspace oberta", + "indexingStopped": "Indexació aturada per l'usuari.", + "indexingStoppedPartial": "Indexació aturada. Dades d'índex parcials conservades." } } diff --git a/src/i18n/locales/de/embeddings.json b/src/i18n/locales/de/embeddings.json index 0297ec03091..766d31d5ba0 100644 --- a/src/i18n/locales/de/embeddings.json +++ b/src/i18n/locales/de/embeddings.json @@ -69,6 +69,8 @@ "fileWatcherStopped": "Datei-Watcher gestoppt.", "failedDuringInitialScan": "Fehler während des ersten Scans: {{errorMessage}}", "unknownError": "Unbekannter Fehler", - "indexingRequiresWorkspace": "Indexierung erfordert einen offenen Workspace-Ordner" + "indexingRequiresWorkspace": "Indexierung erfordert einen offenen Workspace-Ordner", + "indexingStopped": "Indexierung vom Benutzer gestoppt.", + "indexingStoppedPartial": "Indexierung gestoppt. Teilweise Indexdaten beibehalten." } } diff --git a/src/i18n/locales/en/embeddings.json b/src/i18n/locales/en/embeddings.json index 5819e45c1a8..7777af9027e 100644 --- a/src/i18n/locales/en/embeddings.json +++ b/src/i18n/locales/en/embeddings.json @@ -69,6 +69,8 @@ "fileWatcherStopped": "File watcher stopped.", "failedDuringInitialScan": "Failed during initial scan: {{errorMessage}}", "unknownError": "Unknown error", - "indexingRequiresWorkspace": "Indexing requires an open workspace folder" + "indexingRequiresWorkspace": "Indexing requires an open workspace folder", + "indexingStopped": "Indexing stopped by user.", + "indexingStoppedPartial": "Indexing stopped. Partial index data preserved." } } diff --git a/src/i18n/locales/es/embeddings.json b/src/i18n/locales/es/embeddings.json index eca9efcc075..930404de1f5 100644 --- a/src/i18n/locales/es/embeddings.json +++ b/src/i18n/locales/es/embeddings.json @@ -69,6 +69,8 @@ "fileWatcherStopped": "Monitor de archivos detenido.", "failedDuringInitialScan": "Falló durante el escaneo inicial: {{errorMessage}}", "unknownError": "Error desconocido", - "indexingRequiresWorkspace": "La indexación requiere una carpeta de workspace abierta" + "indexingRequiresWorkspace": "La indexación requiere una carpeta de workspace abierta", + "indexingStopped": "Indexación detenida por el usuario.", + "indexingStoppedPartial": "Indexación detenida. Datos de índice parciales conservados." } } diff --git a/src/i18n/locales/fr/embeddings.json b/src/i18n/locales/fr/embeddings.json index fa922179870..7de086307ea 100644 --- a/src/i18n/locales/fr/embeddings.json +++ b/src/i18n/locales/fr/embeddings.json @@ -69,6 +69,8 @@ "fileWatcherStopped": "Surveillant de fichiers arrêté.", "failedDuringInitialScan": "Échec lors du scan initial : {{errorMessage}}", "unknownError": "Erreur inconnue", - "indexingRequiresWorkspace": "L'indexation nécessite l'ouverture d'un dossier workspace" + "indexingRequiresWorkspace": "L'indexation nécessite l'ouverture d'un dossier workspace", + "indexingStopped": "Indexation arrêtée par l'utilisateur.", + "indexingStoppedPartial": "Indexation arrêtée. Données d'index partielles conservées." } } diff --git a/src/i18n/locales/hi/embeddings.json b/src/i18n/locales/hi/embeddings.json index eb7f066c56f..9c7f9ca50ae 100644 --- a/src/i18n/locales/hi/embeddings.json +++ b/src/i18n/locales/hi/embeddings.json @@ -69,6 +69,8 @@ "fileWatcherStopped": "फाइल वॉचर रुक गया।", "failedDuringInitialScan": "प्रारंभिक स्कैन के दौरान असफल: {{errorMessage}}", "unknownError": "अज्ञात त्रुटि", - "indexingRequiresWorkspace": "इंडेक्सिंग के लिए एक खुला वर्कस्पेस फ़ोल्डर आवश्यक है" + "indexingRequiresWorkspace": "इंडेक्सिंग के लिए एक खुला वर्कस्पेस फ़ोल्डर आवश्यक है", + "indexingStopped": "उपयोगकर्ता द्वारा इंडेक्सिंग रोकी गई।", + "indexingStoppedPartial": "इंडेक्सिंग रोकी गई। आंशिक इंडेक्स डेटा संरक्षित।" } } diff --git a/src/i18n/locales/id/embeddings.json b/src/i18n/locales/id/embeddings.json index cceb965430e..955a039effe 100644 --- a/src/i18n/locales/id/embeddings.json +++ b/src/i18n/locales/id/embeddings.json @@ -69,6 +69,8 @@ "fileWatcherStopped": "Pemantau file dihentikan.", "failedDuringInitialScan": "Gagal selama pemindaian awal: {{errorMessage}}", "unknownError": "Kesalahan tidak diketahui", - "indexingRequiresWorkspace": "Pengindeksan memerlukan folder workspace yang terbuka" + "indexingRequiresWorkspace": "Pengindeksan memerlukan folder workspace yang terbuka", + "indexingStopped": "Pengindeksan dihentikan oleh pengguna.", + "indexingStoppedPartial": "Pengindeksan dihentikan. Data indeks parsial dipertahankan." } } diff --git a/src/i18n/locales/it/embeddings.json b/src/i18n/locales/it/embeddings.json index 2e339ef5d88..b7314c244dc 100644 --- a/src/i18n/locales/it/embeddings.json +++ b/src/i18n/locales/it/embeddings.json @@ -69,6 +69,8 @@ "fileWatcherStopped": "Monitoraggio file fermato.", "failedDuringInitialScan": "Fallito durante la scansione iniziale: {{errorMessage}}", "unknownError": "Errore sconosciuto", - "indexingRequiresWorkspace": "L'indicizzazione richiede una cartella di workspace aperta" + "indexingRequiresWorkspace": "L'indicizzazione richiede una cartella di workspace aperta", + "indexingStopped": "Indicizzazione interrotta dall'utente.", + "indexingStoppedPartial": "Indicizzazione interrotta. Dati di indice parziali conservati." } } diff --git a/src/i18n/locales/ja/embeddings.json b/src/i18n/locales/ja/embeddings.json index 5223c204e0d..ce7150cf1ca 100644 --- a/src/i18n/locales/ja/embeddings.json +++ b/src/i18n/locales/ja/embeddings.json @@ -69,6 +69,8 @@ "fileWatcherStopped": "ファイルウォッチャーが停止されました。", "failedDuringInitialScan": "初期スキャン中に失敗しました:{{errorMessage}}", "unknownError": "不明なエラー", - "indexingRequiresWorkspace": "インデックス作成には、開かれたワークスペースフォルダーが必要です" + "indexingRequiresWorkspace": "インデックス作成には、開かれたワークスペースフォルダーが必要です", + "indexingStopped": "ユーザーによりインデックス作成が停止されました。", + "indexingStoppedPartial": "インデックス作成が停止されました。部分的なインデックスデータは保持されています。" } } diff --git a/src/i18n/locales/ko/embeddings.json b/src/i18n/locales/ko/embeddings.json index 236662eea22..436fa985c02 100644 --- a/src/i18n/locales/ko/embeddings.json +++ b/src/i18n/locales/ko/embeddings.json @@ -69,6 +69,8 @@ "fileWatcherStopped": "파일 감시자가 중지되었습니다.", "failedDuringInitialScan": "초기 스캔 중 실패: {{errorMessage}}", "unknownError": "알 수 없는 오류", - "indexingRequiresWorkspace": "인덱싱에는 열린 워크스페이스 폴더가 필요합니다" + "indexingRequiresWorkspace": "인덱싱에는 열린 워크스페이스 폴더가 필요합니다", + "indexingStopped": "사용자에 의해 인덱싱이 중지되었습니다.", + "indexingStoppedPartial": "인덱싱이 중지되었습니다. 부분 인덱스 데이터가 보존되었습니다." } } diff --git a/src/i18n/locales/nl/embeddings.json b/src/i18n/locales/nl/embeddings.json index cce3f05c625..01e68683d3a 100644 --- a/src/i18n/locales/nl/embeddings.json +++ b/src/i18n/locales/nl/embeddings.json @@ -69,6 +69,8 @@ "fileWatcherStopped": "Bestandsmonitor gestopt.", "failedDuringInitialScan": "Mislukt tijdens initiële scan: {{errorMessage}}", "unknownError": "Onbekende fout", - "indexingRequiresWorkspace": "Indexering vereist een geopende workspace map" + "indexingRequiresWorkspace": "Indexering vereist een geopende workspace map", + "indexingStopped": "Indexering gestopt door gebruiker.", + "indexingStoppedPartial": "Indexering gestopt. Gedeeltelijke indexgegevens bewaard." } } diff --git a/src/i18n/locales/pl/embeddings.json b/src/i18n/locales/pl/embeddings.json index 133f9f40da2..0ef846b2cc9 100644 --- a/src/i18n/locales/pl/embeddings.json +++ b/src/i18n/locales/pl/embeddings.json @@ -69,6 +69,8 @@ "fileWatcherStopped": "Monitor plików zatrzymany.", "failedDuringInitialScan": "Niepowodzenie podczas początkowego skanowania: {{errorMessage}}", "unknownError": "Nieznany błąd", - "indexingRequiresWorkspace": "Indeksowanie wymaga otwartego folderu workspace" + "indexingRequiresWorkspace": "Indeksowanie wymaga otwartego folderu workspace", + "indexingStopped": "Indeksowanie zatrzymane przez użytkownika.", + "indexingStoppedPartial": "Indeksowanie zatrzymane. Częściowe dane indeksu zachowane." } } diff --git a/src/i18n/locales/pt-BR/embeddings.json b/src/i18n/locales/pt-BR/embeddings.json index 09f4a557874..9cdf775e76e 100644 --- a/src/i18n/locales/pt-BR/embeddings.json +++ b/src/i18n/locales/pt-BR/embeddings.json @@ -69,6 +69,8 @@ "fileWatcherStopped": "Monitor de arquivos parado.", "failedDuringInitialScan": "Falhou durante a varredura inicial: {{errorMessage}}", "unknownError": "Erro desconhecido", - "indexingRequiresWorkspace": "A indexação requer uma pasta de workspace aberta" + "indexingRequiresWorkspace": "A indexação requer uma pasta de workspace aberta", + "indexingStopped": "Indexação interrompida pelo usuário.", + "indexingStoppedPartial": "Indexação interrompida. Dados de índice parciais preservados." } } diff --git a/src/i18n/locales/ru/embeddings.json b/src/i18n/locales/ru/embeddings.json index 9e94082bbfb..873b1c06308 100644 --- a/src/i18n/locales/ru/embeddings.json +++ b/src/i18n/locales/ru/embeddings.json @@ -69,6 +69,8 @@ "fileWatcherStopped": "Наблюдатель файлов остановлен.", "failedDuringInitialScan": "Ошибка во время первоначального сканирования: {{errorMessage}}", "unknownError": "Неизвестная ошибка", - "indexingRequiresWorkspace": "Для индексации требуется открытая папка рабочего пространства" + "indexingRequiresWorkspace": "Для индексации требуется открытая папка рабочего пространства", + "indexingStopped": "Индексация остановлена пользователем.", + "indexingStoppedPartial": "Индексация остановлена. Частичные данные индекса сохранены." } } diff --git a/src/i18n/locales/tr/embeddings.json b/src/i18n/locales/tr/embeddings.json index 411ed7ab526..30b703a93f1 100644 --- a/src/i18n/locales/tr/embeddings.json +++ b/src/i18n/locales/tr/embeddings.json @@ -69,6 +69,8 @@ "fileWatcherStopped": "Dosya izleyici durduruldu.", "failedDuringInitialScan": "İlk tarama sırasında başarısız: {{errorMessage}}", "unknownError": "Bilinmeyen hata", - "indexingRequiresWorkspace": "İndeksleme açık bir workspace klasörü gerektirir" + "indexingRequiresWorkspace": "İndeksleme açık bir workspace klasörü gerektirir", + "indexingStopped": "İndeksleme kullanıcı tarafından durduruldu.", + "indexingStoppedPartial": "İndeksleme durduruldu. Kısmi indeks verileri korundu." } } diff --git a/src/i18n/locales/vi/embeddings.json b/src/i18n/locales/vi/embeddings.json index c9f9880df08..c92ebba2765 100644 --- a/src/i18n/locales/vi/embeddings.json +++ b/src/i18n/locales/vi/embeddings.json @@ -69,6 +69,8 @@ "fileWatcherStopped": "Trình theo dõi tệp đã dừng.", "failedDuringInitialScan": "Thất bại trong quá trình quét ban đầu: {{errorMessage}}", "unknownError": "Lỗi không xác định", - "indexingRequiresWorkspace": "Lập chỉ mục yêu cầu một thư mục workspace đang mở" + "indexingRequiresWorkspace": "Lập chỉ mục yêu cầu một thư mục workspace đang mở", + "indexingStopped": "Lập chỉ mục đã bị dừng bởi người dùng.", + "indexingStoppedPartial": "Lập chỉ mục đã dừng. Dữ liệu chỉ mục một phần được bảo toàn." } } diff --git a/src/i18n/locales/zh-CN/embeddings.json b/src/i18n/locales/zh-CN/embeddings.json index c27bc078015..b4f4eaad1d1 100644 --- a/src/i18n/locales/zh-CN/embeddings.json +++ b/src/i18n/locales/zh-CN/embeddings.json @@ -69,6 +69,8 @@ "fileWatcherStopped": "文件监控已停止。", "failedDuringInitialScan": "初始扫描失败:{{errorMessage}}", "unknownError": "未知错误", - "indexingRequiresWorkspace": "索引需要打开的工作区文件夹" + "indexingRequiresWorkspace": "索引需要打开的工作区文件夹", + "indexingStopped": "用户已停止索引。", + "indexingStoppedPartial": "索引已停止。部分索引数据已保留。" } } diff --git a/src/i18n/locales/zh-TW/embeddings.json b/src/i18n/locales/zh-TW/embeddings.json index 744e7022ea1..26845ed9488 100644 --- a/src/i18n/locales/zh-TW/embeddings.json +++ b/src/i18n/locales/zh-TW/embeddings.json @@ -69,6 +69,8 @@ "fileWatcherStopped": "檔案監控已停止。", "failedDuringInitialScan": "初始掃描失敗:{{errorMessage}}", "unknownError": "未知錯誤", - "indexingRequiresWorkspace": "索引需要開啟的工作區資料夾" + "indexingRequiresWorkspace": "索引需要開啟的工作區資料夾", + "indexingStopped": "使用者已停止索引。", + "indexingStoppedPartial": "索引已停止。部分索引資料已保留。" } } diff --git a/src/services/code-index/__tests__/manager.spec.ts b/src/services/code-index/__tests__/manager.spec.ts index 929f6f93c8a..49a6d91c767 100644 --- a/src/services/code-index/__tests__/manager.spec.ts +++ b/src/services/code-index/__tests__/manager.spec.ts @@ -3,18 +3,45 @@ import { CodeIndexServiceFactory } from "../service-factory" import type { MockedClass } from "vitest" import * as path from "path" +// Helper: create a mock vscode.Uri from an fsPath +function mockUri(fsPath: string, scheme = "file") { + return { + fsPath, + scheme, + authority: "", + path: fsPath, + toString: (skipEncoding?: boolean) => `${scheme}://${fsPath}`, + } +} + // Mock vscode module vi.mock("vscode", () => { const testPath = require("path") const testWorkspacePath = testPath.join(testPath.sep, "test", "workspace") return { + Uri: { + file: (p: string) => ({ + fsPath: p, + scheme: "file", + authority: "", + path: p, + toString: (_skipEncoding?: boolean) => `file://${p}`, + }), + joinPath: vi.fn((...args: any[]) => ({ fsPath: args.join("/") })), + }, window: { activeTextEditor: null, }, workspace: { workspaceFolders: [ { - uri: { fsPath: testWorkspacePath }, + uri: { + fsPath: testWorkspacePath, + scheme: "file", + authority: "", + path: testWorkspacePath, + toString: (_skipEncoding?: boolean) => `file://${testWorkspacePath}`, + }, name: "test", index: 0, }, @@ -25,8 +52,9 @@ vi.mock("vscode", () => { onDidDelete: vi.fn().mockReturnValue({ dispose: vi.fn() }), dispose: vi.fn(), }), + getWorkspaceFolder: vi.fn(), }, - RelativePattern: vi.fn().mockImplementation((base, pattern) => ({ base, pattern })), + RelativePattern: vi.fn().mockImplementation((base: any, pattern: any) => ({ base, pattern })), } }) @@ -95,10 +123,22 @@ describe("CodeIndexManager - handleSettingsChange regression", () => { // Clear all instances before each test CodeIndexManager.disposeAll() + const workspaceStateStore: Record = {} + const globalStateStore: Record = {} mockContext = { subscriptions: [], - workspaceState: {} as any, - globalState: {} as any, + workspaceState: { + get: vi.fn((key: string, defaultValue?: any) => workspaceStateStore[key] ?? defaultValue), + update: vi.fn(async (key: string, value: any) => { + workspaceStateStore[key] = value + }), + } as any, + globalState: { + get: vi.fn((key: string, defaultValue?: any) => globalStateStore[key] ?? defaultValue), + update: vi.fn(async (key: string, value: any) => { + globalStateStore[key] = value + }), + } as any, extensionUri: {} as any, extensionPath: testExtensionPath, asAbsolutePath: vi.fn(), @@ -222,7 +262,7 @@ describe("CodeIndexManager - handleSettingsChange regression", () => { ;(manager as any)._cacheManager = mockCacheManager // Simulate an initialized manager by setting the required properties - ;(manager as any)._orchestrator = { stopWatcher: vi.fn() } + ;(manager as any)._orchestrator = { stopWatcher: vi.fn(), stopIndexing: vi.fn() } ;(manager as any)._searchService = {} // Verify manager is considered initialized @@ -456,7 +496,7 @@ describe("CodeIndexManager - handleSettingsChange regression", () => { }) // Mock orchestrator and search service to simulate initialized state - ;(manager as any)._orchestrator = { stopWatcher: vi.fn(), state: "Error" } + ;(manager as any)._orchestrator = { stopWatcher: vi.fn(), stopIndexing: vi.fn(), state: "Error" } ;(manager as any)._searchService = {} ;(manager as any)._serviceFactory = {} }) @@ -540,6 +580,9 @@ describe("CodeIndexManager - handleSettingsChange regression", () => { }), } + // Enable workspace indexing before re-initialization + await manager.setWorkspaceEnabled(true) + // Re-initialize await manager.initialize(mockContextProxy as any) @@ -583,7 +626,7 @@ describe("CodeIndexManager - handleSettingsChange regression", () => { // Setup manager with service instances ;(manager as any)._configManager = mockConfigManager ;(manager as any)._serviceFactory = {} - ;(manager as any)._orchestrator = { stopWatcher: vi.fn() } + ;(manager as any)._orchestrator = { stopWatcher: vi.fn(), stopIndexing: vi.fn() } ;(manager as any)._searchService = {} // Spy on console.error @@ -608,4 +651,155 @@ describe("CodeIndexManager - handleSettingsChange regression", () => { consoleErrorSpy.mockRestore() }) }) + + describe("workspace-enabled gating", () => { + it("should not start indexing when workspace is not enabled", async () => { + await manager.setAutoEnableDefault(false) + + const mockStateManager = (manager as any)._stateManager + mockStateManager.setSystemState = vi.fn() + mockStateManager.getCurrentStatus = vi.fn().mockReturnValue({ + systemStatus: "Standby", + message: "", + processedItems: 0, + totalItems: 0, + currentItemUnit: "items", + }) + + expect(manager.isWorkspaceEnabled).toBe(false) + + await manager.startIndexing() + + expect(mockStateManager.setSystemState).not.toHaveBeenCalledWith("Indexing", expect.any(String)) + }) + + it("should include workspaceEnabled in getCurrentStatus", async () => { + await manager.setAutoEnableDefault(false) + + const mockStateManager = (manager as any)._stateManager + mockStateManager.getCurrentStatus = vi.fn().mockReturnValue({ + systemStatus: "Standby", + message: "", + processedItems: 0, + totalItems: 0, + currentItemUnit: "items", + }) + + const status = manager.getCurrentStatus() + expect(status.workspaceEnabled).toBe(false) + }) + + it("should persist workspace enabled state", async () => { + await manager.setAutoEnableDefault(false) + expect(manager.isWorkspaceEnabled).toBe(false) + + await manager.setWorkspaceEnabled(true) + expect(manager.isWorkspaceEnabled).toBe(true) + + await manager.setWorkspaceEnabled(false) + expect(manager.isWorkspaceEnabled).toBe(false) + }) + + it("should store enablement per folder URI, not per window", async () => { + CodeIndexManager.disposeAll() + + const vscode = await import("vscode") + + const folderAPath = path.join(path.sep, "test", "folderA") + const folderBPath = path.join(path.sep, "test", "folderB") + const folderAUri = mockUri(folderAPath) + const folderBUri = mockUri(folderBPath) + + // Both folders share the same workspaceState (same window) + const sharedStore: Record = {} + const sharedContext = { + ...mockContext, + workspaceState: { + get: vi.fn((key: string, defaultValue?: any) => sharedStore[key] ?? defaultValue), + update: vi.fn(async (key: string, value: any) => { + sharedStore[key] = value + }), + } as any, + globalState: { + get: vi.fn((_key: string, _defaultValue?: any) => false), + update: vi.fn(), + } as any, + } + + // Patch workspaceFolders to include both folders + ;(vscode.workspace as any).workspaceFolders = [ + { uri: folderAUri, name: "folderA", index: 0 }, + { uri: folderBUri, name: "folderB", index: 1 }, + ] + + const managerA = CodeIndexManager.getInstance(sharedContext as any, folderAPath)! + const managerB = CodeIndexManager.getInstance(sharedContext as any, folderBPath)! + + // Both start disabled (autoEnableDefault is false via globalState mock) + expect(managerA.isWorkspaceEnabled).toBe(false) + expect(managerB.isWorkspaceEnabled).toBe(false) + + // Enable A only + await managerA.setWorkspaceEnabled(true) + + expect(managerA.isWorkspaceEnabled).toBe(true) + expect(managerB.isWorkspaceEnabled).toBe(false) + + // Enable B, disable A + await managerB.setWorkspaceEnabled(true) + await managerA.setWorkspaceEnabled(false) + + expect(managerA.isWorkspaceEnabled).toBe(false) + expect(managerB.isWorkspaceEnabled).toBe(true) + + CodeIndexManager.disposeAll() + }) + }) + + describe("stopIndexing", () => { + it("should delegate to orchestrator.stopIndexing()", () => { + const mockOrchestrator = { + stopIndexing: vi.fn(), + stopWatcher: vi.fn(), + state: "Indexing", + } + ;(manager as any)._orchestrator = mockOrchestrator + + manager.stopIndexing() + + expect(mockOrchestrator.stopIndexing).toHaveBeenCalled() + }) + + it("should be safe to call when orchestrator is not set", () => { + ;(manager as any)._orchestrator = undefined + + expect(() => manager.stopIndexing()).not.toThrow() + }) + }) + + describe("handleSettingsChange - disable toggle bug fix", () => { + it("should abort active indexing when feature is disabled", async () => { + const mockOrchestrator = { + stopIndexing: vi.fn(), + stopWatcher: vi.fn(), + state: "Indexing", + } + ;(manager as any)._orchestrator = mockOrchestrator + + const mockConfigManager = { + loadConfiguration: vi.fn().mockResolvedValue({ requiresRestart: false }), + isFeatureConfigured: true, + isFeatureEnabled: false, + } + ;(manager as any)._configManager = mockConfigManager + + const mockStateManager = (manager as any)._stateManager + mockStateManager.setSystemState = vi.fn() + + await manager.handleSettingsChange() + + expect(mockOrchestrator.stopIndexing).toHaveBeenCalled() + expect(mockStateManager.setSystemState).toHaveBeenCalledWith("Standby", "Code indexing is disabled") + }) + }) }) diff --git a/src/services/code-index/__tests__/orchestrator.spec.ts b/src/services/code-index/__tests__/orchestrator.spec.ts index aab1ef888d3..e940ea04c24 100644 --- a/src/services/code-index/__tests__/orchestrator.spec.ts +++ b/src/services/code-index/__tests__/orchestrator.spec.ts @@ -79,6 +79,7 @@ describe("CodeIndexOrchestrator - error path cleanup gating", () => { cacheManager = { clearCacheFile: vi.fn().mockResolvedValue(undefined), + flush: vi.fn().mockResolvedValue(undefined), } vectorStore = { @@ -158,3 +159,178 @@ describe("CodeIndexOrchestrator - error path cleanup gating", () => { expect(lastCall[0]).toBe("Error") }) }) + +describe("CodeIndexOrchestrator - stopIndexing", () => { + const workspacePath = "/test/workspace" + + let configManager: any + let stateManager: any + let cacheManager: any + let vectorStore: any + let scanner: any + let fileWatcher: any + + beforeEach(() => { + vi.clearAllMocks() + + configManager = { + isFeatureConfigured: true, + } + + let currentState = "Standby" + stateManager = { + get state() { + return currentState + }, + setSystemState: vi.fn().mockImplementation((state: string, _msg: string) => { + currentState = state + }), + reportFileQueueProgress: vi.fn(), + reportBlockIndexingProgress: vi.fn(), + } + + cacheManager = { + clearCacheFile: vi.fn().mockResolvedValue(undefined), + flush: vi.fn().mockResolvedValue(undefined), + } + + vectorStore = { + initialize: vi.fn().mockResolvedValue(false), + hasIndexedData: vi.fn().mockResolvedValue(false), + markIndexingIncomplete: vi.fn().mockResolvedValue(undefined), + markIndexingComplete: vi.fn().mockResolvedValue(undefined), + clearCollection: vi.fn().mockResolvedValue(undefined), + } + + scanner = { + scanDirectory: vi.fn(), + } + + fileWatcher = { + initialize: vi.fn().mockResolvedValue(undefined), + onDidStartBatchProcessing: vi.fn().mockReturnValue({ dispose: vi.fn() }), + onBatchProgressUpdate: vi.fn().mockReturnValue({ dispose: vi.fn() }), + onDidFinishBatchProcessing: vi.fn().mockReturnValue({ dispose: vi.fn() }), + dispose: vi.fn(), + } + }) + + it("should abort indexing when stopIndexing() is called", async () => { + // Make scanner hang until aborted + scanner.scanDirectory.mockImplementation( + async (_dir: string, _onError?: any, _onBlocksIndexed?: any, _onFileParsed?: any, signal?: AbortSignal) => { + // Wait for abort signal + await new Promise((resolve) => { + if (signal?.aborted) { + resolve() + return + } + signal?.addEventListener("abort", () => resolve()) + }) + return { stats: { processed: 0, skipped: 0 }, totalBlockCount: 0 } + }, + ) + + const orchestrator = new CodeIndexOrchestrator( + configManager, + stateManager, + workspacePath, + cacheManager, + vectorStore, + scanner, + fileWatcher, + ) + + // Start indexing (async, don't await) + const indexingPromise = orchestrator.startIndexing() + + // Give it a tick to begin + await new Promise((resolve) => setTimeout(resolve, 10)) + + // Stop indexing + orchestrator.stopIndexing() + + // Wait for indexing to complete + await indexingPromise + + // State should be Standby (not Error) + const setStateCalls = stateManager.setSystemState.mock.calls + const lastCall = setStateCalls[setStateCalls.length - 1] + expect(lastCall[0]).toBe("Standby") + }) + + it("should set state to Standby after abort, not Error", async () => { + // Make scanner throw AbortError when signal is aborted + scanner.scanDirectory.mockImplementation( + async (_dir: string, _onError?: any, _onBlocksIndexed?: any, _onFileParsed?: any, signal?: AbortSignal) => { + await new Promise((resolve) => { + if (signal?.aborted) { + resolve() + return + } + signal?.addEventListener("abort", () => resolve()) + }) + throw new DOMException("Indexing aborted", "AbortError") + }, + ) + + const orchestrator = new CodeIndexOrchestrator( + configManager, + stateManager, + workspacePath, + cacheManager, + vectorStore, + scanner, + fileWatcher, + ) + + const indexingPromise = orchestrator.startIndexing() + await new Promise((resolve) => setTimeout(resolve, 10)) + + orchestrator.stopIndexing() + await indexingPromise + + // Should NOT have set Error state — abort is handled gracefully + const errorCalls = stateManager.setSystemState.mock.calls.filter((call: any[]) => call[0] === "Error") + expect(errorCalls).toHaveLength(0) + + // Should NOT have cleared collection on abort + expect(vectorStore.clearCollection).not.toHaveBeenCalled() + }) + + it("should preserve partial index data after stop", async () => { + scanner.scanDirectory.mockImplementation( + async (_dir: string, _onError?: any, _onBlocksIndexed?: any, _onFileParsed?: any, signal?: AbortSignal) => { + await new Promise((resolve) => { + if (signal?.aborted) { + resolve() + return + } + signal?.addEventListener("abort", () => resolve()) + }) + return { stats: { processed: 5, skipped: 0 }, totalBlockCount: 5 } + }, + ) + + const orchestrator = new CodeIndexOrchestrator( + configManager, + stateManager, + workspacePath, + cacheManager, + vectorStore, + scanner, + fileWatcher, + ) + + const indexingPromise = orchestrator.startIndexing() + await new Promise((resolve) => setTimeout(resolve, 10)) + + orchestrator.stopIndexing() + await indexingPromise + + // Cache should NOT be cleared on user-initiated stop + expect(cacheManager.clearCacheFile).not.toHaveBeenCalled() + // Collection should NOT be cleared on user-initiated stop + expect(vectorStore.clearCollection).not.toHaveBeenCalled() + }) +}) diff --git a/src/services/code-index/cache-manager.ts b/src/services/code-index/cache-manager.ts index a9a4f0ac471..eadaa9e346b 100644 --- a/src/services/code-index/cache-manager.ts +++ b/src/services/code-index/cache-manager.ts @@ -110,6 +110,13 @@ export class CacheManager implements ICacheManager { this._debouncedSaveCache() } + /** + * Flushes any pending debounced cache writes to disk immediately. + */ + async flush(): Promise { + await this._performSave() + } + /** * Gets a copy of all file hashes * @returns A copy of the file hashes record diff --git a/src/services/code-index/interfaces/cache.ts b/src/services/code-index/interfaces/cache.ts index a2e62bcac1d..01931a3a8b0 100644 --- a/src/services/code-index/interfaces/cache.ts +++ b/src/services/code-index/interfaces/cache.ts @@ -2,5 +2,6 @@ export interface ICacheManager { getHash(filePath: string): string | undefined updateHash(filePath: string, hash: string): void deleteHash(filePath: string): void + flush(): Promise getAllHashes(): Record } diff --git a/src/services/code-index/interfaces/file-processor.ts b/src/services/code-index/interfaces/file-processor.ts index 88b19007c37..8ecdc518c8d 100644 --- a/src/services/code-index/interfaces/file-processor.ts +++ b/src/services/code-index/interfaces/file-processor.ts @@ -37,6 +37,7 @@ export interface IDirectoryScanner { onError?: (error: Error) => void, onBlocksIndexed?: (indexedCount: number) => void, onFileParsed?: (fileBlockCount: number) => void, + signal?: AbortSignal, ): Promise<{ stats: { processed: number diff --git a/src/services/code-index/interfaces/manager.ts b/src/services/code-index/interfaces/manager.ts index 28ff5523277..d657ad667c7 100644 --- a/src/services/code-index/interfaces/manager.ts +++ b/src/services/code-index/interfaces/manager.ts @@ -39,6 +39,11 @@ export interface ICodeIndexManager { */ startIndexing(): Promise + /** + * Stops any in-progress indexing operation and the file watcher + */ + stopIndexing(): void + /** * Stops the file watcher */ @@ -69,7 +74,7 @@ export interface ICodeIndexManager { dispose(): void } -export type IndexingState = "Standby" | "Indexing" | "Indexed" | "Error" +export type IndexingState = "Standby" | "Indexing" | "Indexed" | "Error" | "Stopping" export type EmbedderProvider = | "openai" | "ollama" diff --git a/src/services/code-index/manager.ts b/src/services/code-index/manager.ts index dd79a3f1616..91ea515e402 100644 --- a/src/services/code-index/manager.ts +++ b/src/services/code-index/manager.ts @@ -32,30 +32,47 @@ export class CodeIndexManager { private _isRecoveringFromError = false public static getInstance(context: vscode.ExtensionContext, workspacePath?: string): CodeIndexManager | undefined { - // If workspacePath is not provided, try to get it from the active editor or first workspace folder - if (!workspacePath) { + // Resolve the workspace folder to get both fsPath and the real URI + let folder: vscode.WorkspaceFolder | undefined + + if (workspacePath) { + folder = vscode.workspace.workspaceFolders?.find((f) => f.uri.fsPath === workspacePath) + } else { const activeEditor = vscode.window.activeTextEditor if (activeEditor) { - const workspaceFolder = vscode.workspace.getWorkspaceFolder(activeEditor.document.uri) - workspacePath = workspaceFolder?.uri.fsPath + folder = vscode.workspace.getWorkspaceFolder(activeEditor.document.uri) } - - if (!workspacePath) { + if (!folder) { const workspaceFolders = vscode.workspace.workspaceFolders if (!workspaceFolders || workspaceFolders.length === 0) { return undefined } - // Use the first workspace folder as fallback - workspacePath = workspaceFolders[0].uri.fsPath + folder = workspaceFolders[0] } + workspacePath = folder.uri.fsPath } if (!CodeIndexManager.instances.has(workspacePath)) { - CodeIndexManager.instances.set(workspacePath, new CodeIndexManager(workspacePath, context)) + // folder may be undefined when workspacePath was provided but doesn't match + // any workspace folder (e.g. cwd passed from a tool). Fall back to file:// URI. + const folderUri = + folder?.uri ?? + ({ + fsPath: workspacePath, + scheme: "file", + authority: "", + path: workspacePath, + toString: () => `file://${workspacePath}`, + } as unknown as vscode.Uri) + CodeIndexManager.instances.set(workspacePath, new CodeIndexManager(workspacePath, folderUri, context)) } return CodeIndexManager.instances.get(workspacePath)! } + public static getAllInstances(): CodeIndexManager[] { + return Array.from(CodeIndexManager.instances.values()) + } + public static disposeAll(): void { for (const instance of CodeIndexManager.instances.values()) { instance.dispose() @@ -64,17 +81,45 @@ export class CodeIndexManager { } private readonly workspacePath: string + private readonly _folderUri: vscode.Uri private readonly context: vscode.ExtensionContext // Private constructor for singleton pattern - private constructor(workspacePath: string, context: vscode.ExtensionContext) { + private constructor(workspacePath: string, folderUri: vscode.Uri, context: vscode.ExtensionContext) { this.workspacePath = workspacePath + this._folderUri = folderUri this.context = context this._stateManager = new CodeIndexStateManager() } // --- Public API --- + /** + * Returns the workspaceState key for per-folder indexing enablement, + * keyed by the real workspace folder URI so local/remote schemes cannot collide. + */ + private _workspaceEnabledKey(): string { + return "codeIndexWorkspaceEnabled:" + this._folderUri.toString(true) + } + + public get isWorkspaceEnabled(): boolean { + const explicit = this.context.workspaceState.get(this._workspaceEnabledKey(), undefined) + if (explicit !== undefined) return explicit + return this.autoEnableDefault + } + + public async setWorkspaceEnabled(enabled: boolean): Promise { + await this.context.workspaceState.update(this._workspaceEnabledKey(), enabled) + } + + public get autoEnableDefault(): boolean { + return this.context.globalState.get("codeIndexAutoEnableDefault", true) + } + + public async setAutoEnableDefault(enabled: boolean): Promise { + await this.context.globalState.update("codeIndexAutoEnableDefault", enabled) + } + public get onProgressUpdate() { return this._stateManager.onProgressUpdate } @@ -138,28 +183,32 @@ export class CodeIndexManager { return { requiresRestart } } - // 4. CacheManager Initialization + // 4. Check workspace-level enablement (before creating expensive services) + if (!this.isWorkspaceEnabled) { + this._stateManager.setSystemState("Standby", "Indexing not enabled for this workspace") + return { requiresRestart } + } + + // 5. CacheManager Initialization if (!this._cacheManager) { this._cacheManager = new CacheManager(this.context, this.workspacePath) await this._cacheManager.initialize() } - // 4. Determine if Core Services Need Recreation + // 6. Determine if Core Services Need Recreation const needsServiceRecreation = !this._serviceFactory || requiresRestart if (needsServiceRecreation) { await this._recreateServices() } - // 5. Handle Indexing Start/Restart - // The enhanced vectorStore.initialize() in startIndexing() now handles dimension changes automatically - // by detecting incompatible collections and recreating them, so we rely on that for dimension changes + // 7. Handle Indexing Start/Restart const shouldStartOrRestartIndexing = requiresRestart || (needsServiceRecreation && (!this._orchestrator || this._orchestrator.state !== "Indexing")) if (shouldStartOrRestartIndexing) { - this._orchestrator?.startIndexing() // This method is async, but we don't await it here + this._orchestrator?.startIndexing() } return { requiresRestart } @@ -173,7 +222,7 @@ export class CodeIndexManager { * The indexing will continue asynchronously and progress will be reported through events. */ public async startIndexing(): Promise { - if (!this.isFeatureEnabled) { + if (!this.isFeatureEnabled || !this.isWorkspaceEnabled) { return } @@ -191,6 +240,15 @@ export class CodeIndexManager { await this._orchestrator!.startIndexing() } + /** + * Stops any in-progress indexing operation and the file watcher. + */ + public stopIndexing(): void { + if (this._orchestrator) { + this._orchestrator.stopIndexing() + } + } + /** * Stops the file watcher and potentially cleans up resources. */ @@ -247,9 +305,7 @@ export class CodeIndexManager { * Cleans up the manager instance. */ public dispose(): void { - if (this._orchestrator) { - this.stopWatcher() - } + this.stopIndexing() this._stateManager.dispose() } @@ -273,6 +329,8 @@ export class CodeIndexManager { return { ...status, workspacePath: this.workspacePath, + workspaceEnabled: this.isWorkspaceEnabled, + autoEnableDefault: this.autoEnableDefault, } } @@ -384,13 +442,9 @@ export class CodeIndexManager { const isFeatureEnabled = this.isFeatureEnabled const isFeatureConfigured = this.isFeatureConfigured - // If feature is disabled, stop the service + // If feature is disabled, stop the service (including any active scan) if (!isFeatureEnabled) { - // Stop the orchestrator if it exists - if (this._orchestrator) { - this._orchestrator.stopWatcher() - } - // Set state to indicate service is disabled + this.stopIndexing() this._stateManager.setSystemState("Standby", "Code indexing is disabled") return } diff --git a/src/services/code-index/orchestrator.ts b/src/services/code-index/orchestrator.ts index 99f317882b8..cd65fceb5ef 100644 --- a/src/services/code-index/orchestrator.ts +++ b/src/services/code-index/orchestrator.ts @@ -15,6 +15,7 @@ import { t } from "../../i18n" export class CodeIndexOrchestrator { private _fileWatcherSubscriptions: vscode.Disposable[] = [] private _isProcessing: boolean = false + private _abortController: AbortController | null = null constructor( private readonly configManager: CodeIndexConfigManager, @@ -121,6 +122,8 @@ export class CodeIndexOrchestrator { } this._isProcessing = true + this._abortController = new AbortController() + const signal = this._abortController.signal this.stateManager.setSystemState("Indexing", "Initializing services...") // Track whether we successfully connected to Qdrant and started indexing @@ -178,8 +181,16 @@ export class CodeIndexOrchestrator { }, handleBlocksIndexed, handleFileParsed, + signal, ) + if (signal.aborted) { + await this.cacheManager.flush() + this.stopWatcher() + this.stateManager.setSystemState("Standby", t("embeddings:orchestrator.indexingStopped")) + return + } + if (!result) { throw new Error("Incremental scan failed, is scanner initialized?") } @@ -231,8 +242,16 @@ export class CodeIndexOrchestrator { }, handleBlocksIndexed, handleFileParsed, + signal, ) + if (signal.aborted) { + await this.cacheManager.flush() + this.stopWatcher() + this.stateManager.setSystemState("Standby", t("embeddings:orchestrator.indexingStopped")) + return + } + if (!result) { throw new Error("Scan failed, is scanner initialized?") } @@ -282,6 +301,15 @@ export class CodeIndexOrchestrator { this.stateManager.setSystemState("Indexed", t("embeddings:orchestrator.fileWatcherStarted")) } } catch (error: any) { + // Handle abort gracefully — not an error, just a user-initiated stop + if (error?.name === "AbortError" || signal.aborted) { + console.log("[CodeIndexOrchestrator] Indexing aborted by user.") + await this.cacheManager.flush() + this.stopWatcher() + this.stateManager.setSystemState("Standby", t("embeddings:orchestrator.indexingStopped")) + return + } + console.error("[CodeIndexOrchestrator] Error during indexing:", error) TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, { error: error instanceof Error ? error.message : String(error), @@ -325,7 +353,20 @@ export class CodeIndexOrchestrator { this.stopWatcher() } finally { this._isProcessing = false + this._abortController = null + } + } + + /** + * Stops any in-progress indexing by aborting the scan and stopping the file watcher. + */ + public stopIndexing(): void { + if (this._abortController) { + this.stateManager.setSystemState("Stopping", t("embeddings:orchestrator.indexingStoppedPartial")) + this._abortController.abort() + this._abortController = null } + this.stopWatcher() } /** @@ -336,7 +377,7 @@ export class CodeIndexOrchestrator { this._fileWatcherSubscriptions.forEach((sub) => sub.dispose()) this._fileWatcherSubscriptions = [] - if (this.stateManager.state !== "Error") { + if (this.stateManager.state !== "Error" && this.stateManager.state !== "Stopping") { this.stateManager.setSystemState("Standby", t("embeddings:orchestrator.fileWatcherStopped")) } this._isProcessing = false diff --git a/src/services/code-index/processors/__tests__/scanner.spec.ts b/src/services/code-index/processors/__tests__/scanner.spec.ts index 4d4150b4439..a6e68bc96b6 100644 --- a/src/services/code-index/processors/__tests__/scanner.spec.ts +++ b/src/services/code-index/processors/__tests__/scanner.spec.ts @@ -394,5 +394,68 @@ describe("DirectoryScanner", () => { expect(points[1].payload.segmentHash).toBe("unique-segment-hash-2") expect(points[2].payload.segmentHash).toBe("unique-segment-hash-3") }) + + it("should stop processing files when signal is aborted", async () => { + const { listFiles } = await import("../../../glob/list-files") + vi.mocked(listFiles).mockResolvedValue([["test/file1.js", "test/file2.js", "test/file3.js"], false]) + + // Create an already-aborted signal + const controller = new AbortController() + controller.abort() + + const result = await scanner.scanDirectory("/test", undefined, undefined, undefined, controller.signal) + + // No files should have been processed since signal was already aborted + expect(mockCodeParser.parseFile).not.toHaveBeenCalled() + expect(result.stats.processed).toBe(0) + }) + + it("should stop processing batches when signal is aborted mid-scan", async () => { + const { listFiles } = await import("../../../glob/list-files") + vi.mocked(listFiles).mockResolvedValue([["test/file1.js", "test/file2.js"], false]) + + const controller = new AbortController() + + const mockBlocks: any[] = [ + { + file_path: "test/file1.js", + content: "function hello() {}", + start_line: 1, + end_line: 3, + identifier: "hello", + type: "function", + fileHash: "hash1", + segmentHash: "seg-hash-1", + }, + ] + + // Abort after first file is parsed + ;(mockCodeParser.parseFile as any).mockImplementation(async () => { + controller.abort() + return mockBlocks + }) + + // AbortError should propagate up (the orchestrator handles it in its catch block) + await expect( + scanner.scanDirectory("/test", undefined, undefined, undefined, controller.signal), + ).rejects.toThrow("Indexing aborted") + }) + + it("should not process deleted files when signal is aborted", async () => { + const { listFiles } = await import("../../../glob/list-files") + vi.mocked(listFiles).mockResolvedValue([[], false]) + + // Set up cached files that would normally be detected as deleted + ;(mockCacheManager.getAllHashes as any).mockReturnValue({ "old/file.js": "old-hash" }) + + // Create an already-aborted signal + const controller = new AbortController() + controller.abort() + + await scanner.scanDirectory("/test", undefined, undefined, undefined, controller.signal) + + // Deleted file cleanup should not have run + expect(mockVectorStore.deletePointsByFilePath).not.toHaveBeenCalled() + }) }) }) diff --git a/src/services/code-index/processors/scanner.ts b/src/services/code-index/processors/scanner.ts index 91689a56d7c..5d9ff5e362d 100644 --- a/src/services/code-index/processors/scanner.ts +++ b/src/services/code-index/processors/scanner.ts @@ -71,6 +71,7 @@ export class DirectoryScanner implements IDirectoryScanner { onError?: (error: Error) => void, onBlocksIndexed?: (indexedCount: number) => void, onFileParsed?: (fileBlockCount: number) => void, + signal?: AbortSignal, ): Promise<{ stats: { processed: number; skipped: number }; totalBlockCount: number }> { const directoryPath = directory // Capture workspace context at scan start @@ -127,6 +128,9 @@ export class DirectoryScanner implements IDirectoryScanner { // Process all files in parallel with concurrency control const parsePromises = supportedPaths.map((filePath) => parseLimiter(async () => { + // Check abort signal before processing each file + if (signal?.aborted) return + try { // Check file size const stats = await stat(filePath) @@ -173,10 +177,17 @@ export class DirectoryScanner implements IDirectoryScanner { addedBlocksFromFile = true // Check if batch threshold is met + // Check abort signal before dispatching batch + if (signal?.aborted) { + throw new DOMException("Indexing aborted", "AbortError") + } + if (currentBatchBlocks.length >= this.batchSegmentThreshold) { // Wait if we've reached the maximum pending batches while (pendingBatchCount >= MAX_PENDING_BATCHES) { - // Wait for at least one batch to complete + if (signal?.aborted) { + throw new DOMException("Indexing aborted", "AbortError") + } await Promise.race(activeBatchPromises) } @@ -235,6 +246,10 @@ export class DirectoryScanner implements IDirectoryScanner { await this.cacheManager.updateHash(filePath, currentFileHash) } } catch (error) { + // Re-throw AbortError — it's not a file processing error, just a user-initiated stop + if (error instanceof DOMException && error.name === "AbortError") { + throw error + } console.error(`Error processing file ${filePath} in workspace ${scanWorkspace}:`, error) TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, { error: sanitizeErrorMessage(error instanceof Error ? error.message : String(error)), @@ -258,6 +273,17 @@ export class DirectoryScanner implements IDirectoryScanner { // Wait for all parsing to complete await Promise.all(parsePromises) + // Check abort signal before processing remaining batch + if (signal?.aborted) { + return { + stats: { + processed: processedCount, + skipped: skippedCount, + }, + totalBlockCount, + } + } + // Process any remaining items in batch if (currentBatchBlocks.length > 0) { const release = await mutex.acquire() @@ -292,6 +318,17 @@ export class DirectoryScanner implements IDirectoryScanner { // Wait for all batch processing to complete await Promise.all(activeBatchPromises) + // Check abort signal before handling deleted files + if (signal?.aborted) { + return { + stats: { + processed: processedCount, + skipped: skippedCount, + }, + totalBlockCount, + } + } + // Handle deleted files const oldHashes = this.cacheManager.getAllHashes() for (const cachedFilePath of Object.keys(oldHashes)) { diff --git a/src/services/code-index/state-manager.ts b/src/services/code-index/state-manager.ts index 90257fdfb19..b6788251478 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" | "Error" | "Stopping" export class CodeIndexStateManager { private _systemStatus: IndexingState = "Standby" @@ -58,6 +58,8 @@ export class CodeIndexStateManager { 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 @@ -81,6 +83,8 @@ export class CodeIndexStateManager { 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 diff --git a/webview-ui/src/components/chat/CodeIndexPopover.tsx b/webview-ui/src/components/chat/CodeIndexPopover.tsx index 4fcf6406e3b..763c243ec1e 100644 --- a/webview-ui/src/components/chat/CodeIndexPopover.tsx +++ b/webview-ui/src/components/chat/CodeIndexPopover.tsx @@ -1590,6 +1590,58 @@ export const CodeIndexPopover: React.FC = ({ )} + {/* Auto-enable default */} + {currentSettings.codebaseIndexEnabled && ( +
+ + vscode.postMessage({ + type: "setAutoEnableDefault", + bool: e.target.checked, + }) + } + className="accent-vscode-focusBorder" + /> + +
+ )} + + {/* Workspace Toggle */} + {currentSettings.codebaseIndexEnabled && ( +
+ + vscode.postMessage({ + type: "toggleWorkspaceIndexing", + bool: e.target.checked, + }) + } + className="accent-vscode-focusBorder" + /> + +
+ )} + + {currentSettings.codebaseIndexEnabled && !indexingStatus.workspaceEnabled && ( +

+ {t("settings:codeIndex.workspaceDisabledMessage")} +

+ )} + {/* Action Buttons */}
@@ -1603,6 +1655,20 @@ export const CodeIndexPopover: React.FC = ({ )} + {currentSettings.codebaseIndexEnabled && indexingStatus.systemStatus === "Indexing" && ( + + )} + + {currentSettings.codebaseIndexEnabled && indexingStatus.systemStatus === "Stopping" && ( + + )} + {currentSettings.codebaseIndexEnabled && (indexingStatus.systemStatus === "Indexed" || indexingStatus.systemStatus === "Error") && ( diff --git a/webview-ui/src/components/chat/IndexingStatusBadge.tsx b/webview-ui/src/components/chat/IndexingStatusBadge.tsx index 82f654a82fa..227df3e645f 100644 --- a/webview-ui/src/components/chat/IndexingStatusBadge.tsx +++ b/webview-ui/src/components/chat/IndexingStatusBadge.tsx @@ -64,6 +64,8 @@ export const IndexingStatusBadge: React.FC = ({ classN return t("chat:indexingStatus.indexing", { percentage: progressPercentage }) case "Indexed": return t("chat:indexingStatus.indexed") + case "Stopping": + return t("chat:indexingStatus.stopping") case "Error": return t("chat:indexingStatus.error") default: @@ -76,6 +78,7 @@ export const IndexingStatusBadge: React.FC = ({ classN Standby: "bg-vscode-descriptionForeground/60", Indexing: "bg-yellow-500 animate-pulse", Indexed: "bg-green-500", + Stopping: "bg-amber-500 animate-pulse", Error: "bg-red-500", } diff --git a/webview-ui/src/i18n/locales/ca/chat.json b/webview-ui/src/i18n/locales/ca/chat.json index d16a4466958..33f4cff443c 100644 --- a/webview-ui/src/i18n/locales/ca/chat.json +++ b/webview-ui/src/i18n/locales/ca/chat.json @@ -424,7 +424,8 @@ "indexing": "Indexant {{percentage}}%", "indexed": "Indexat", "error": "Error d'índex", - "status": "Estat de l'índex" + "status": "Estat de l'índex", + "stopping": "Aturant la indexació..." }, "versionIndicator": { "ariaLabel": "Versió {{version}} - Feu clic per veure les notes de llançament" diff --git a/webview-ui/src/i18n/locales/ca/settings.json b/webview-ui/src/i18n/locales/ca/settings.json index f1e5ad10e6d..f5bef272a9f 100644 --- a/webview-ui/src/i18n/locales/ca/settings.json +++ b/webview-ui/src/i18n/locales/ca/settings.json @@ -211,7 +211,12 @@ "searchMinScoreResetTooltip": "Restablir al valor per defecte (0.4)", "searchMaxResultsLabel": "Màxim de resultats de cerca", "searchMaxResultsDescription": "Nombre màxim de resultats de cerca a retornar quan es consulta l'índex de la base de codi. Els valors més alts proporcionen més context però poden incloure resultats menys rellevants.", - "resetToDefault": "Restablir al valor per defecte" + "resetToDefault": "Restablir al valor per defecte", + "stopIndexingButton": "Aturar indexació", + "stoppingButton": "Aturant...", + "workspaceToggleLabel": "Activar la indexació per a aquest espai de treball", + "workspaceDisabledMessage": "La indexació està configurada però no habilitada per a aquest espai de treball.", + "autoEnableDefaultLabel": "Habilitar automàticament la indexació per a nous espais de treball" }, "autoApprove": { "toggleShortcut": "Pots configurar una drecera global per a aquesta configuració a les preferències del teu IDE.", diff --git a/webview-ui/src/i18n/locales/de/chat.json b/webview-ui/src/i18n/locales/de/chat.json index fb1dd293004..5130de59244 100644 --- a/webview-ui/src/i18n/locales/de/chat.json +++ b/webview-ui/src/i18n/locales/de/chat.json @@ -424,7 +424,8 @@ "indexing": "Indizierung {{percentage}}%", "indexed": "Indiziert", "error": "Index-Fehler", - "status": "Index-Status" + "status": "Index-Status", + "stopping": "Indexierung wird gestoppt..." }, "versionIndicator": { "ariaLabel": "Version {{version}} - Klicken Sie, um die Versionshinweise anzuzeigen" diff --git a/webview-ui/src/i18n/locales/de/settings.json b/webview-ui/src/i18n/locales/de/settings.json index 0e8d7a3a8e3..1d2abfc096b 100644 --- a/webview-ui/src/i18n/locales/de/settings.json +++ b/webview-ui/src/i18n/locales/de/settings.json @@ -211,7 +211,12 @@ "searchMinScoreResetTooltip": "Auf Standardwert zurücksetzen (0.4)", "searchMaxResultsLabel": "Maximale Suchergebnisse", "searchMaxResultsDescription": "Maximale Anzahl von Suchergebnissen, die bei der Abfrage des Codebase-Index zurückgegeben werden. Höhere Werte bieten mehr Kontext, können aber weniger relevante Ergebnisse enthalten.", - "resetToDefault": "Auf Standard zurücksetzen" + "resetToDefault": "Auf Standard zurücksetzen", + "stopIndexingButton": "Indexierung stoppen", + "stoppingButton": "Wird gestoppt...", + "workspaceToggleLabel": "Indexierung für diesen Arbeitsbereich aktivieren", + "workspaceDisabledMessage": "Indexierung ist konfiguriert, aber nicht für diesen Arbeitsbereich aktiviert.", + "autoEnableDefaultLabel": "Indexierung für neue Arbeitsbereiche automatisch aktivieren" }, "autoApprove": { "toggleShortcut": "Du kannst in deinen IDE-Einstellungen einen globalen Shortcut für diese Einstellung konfigurieren.", diff --git a/webview-ui/src/i18n/locales/en/chat.json b/webview-ui/src/i18n/locales/en/chat.json index b597fdbcc33..4e6fe8b47ae 100644 --- a/webview-ui/src/i18n/locales/en/chat.json +++ b/webview-ui/src/i18n/locales/en/chat.json @@ -420,7 +420,8 @@ "indexing": "Indexing {{percentage}}%", "indexed": "Indexed", "error": "Index error", - "status": "Index status" + "status": "Index status", + "stopping": "Stopping indexing..." }, "versionIndicator": { "ariaLabel": "Version {{version}} - Click to view release notes" diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index 471309718bc..b5595c5e829 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -274,7 +274,12 @@ "baseUrlRequired": "Base URL is required", "modelDimensionMinValue": "Model dimension must be greater than 0" }, - "optional": "optional" + "optional": "optional", + "stopIndexingButton": "Stop Indexing", + "stoppingButton": "Stopping...", + "workspaceToggleLabel": "Enable indexing for this workspace", + "workspaceDisabledMessage": "Indexing is configured but not enabled for this workspace.", + "autoEnableDefaultLabel": "Auto-enable indexing for new workspaces" }, "autoApprove": { "description": "Run these actions without asking for permission. Only enable for actions you fully trust and if you understand the security risks.", diff --git a/webview-ui/src/i18n/locales/es/chat.json b/webview-ui/src/i18n/locales/es/chat.json index eb8833b8337..10b3d1eab4c 100644 --- a/webview-ui/src/i18n/locales/es/chat.json +++ b/webview-ui/src/i18n/locales/es/chat.json @@ -424,7 +424,8 @@ "indexing": "Indexando {{percentage}}%", "indexed": "Indexado", "error": "Error de índice", - "status": "Estado del índice" + "status": "Estado del índice", + "stopping": "Deteniendo la indexación..." }, "versionIndicator": { "ariaLabel": "Versión {{version}} - Haz clic para ver las notas de la versión" diff --git a/webview-ui/src/i18n/locales/es/settings.json b/webview-ui/src/i18n/locales/es/settings.json index 42688dd5411..19f1cbb3cc0 100644 --- a/webview-ui/src/i18n/locales/es/settings.json +++ b/webview-ui/src/i18n/locales/es/settings.json @@ -211,7 +211,12 @@ "searchMinScoreResetTooltip": "Restablecer al valor predeterminado (0.4)", "searchMaxResultsLabel": "Resultados máximos de búsqueda", "searchMaxResultsDescription": "Número máximo de resultados de búsqueda a devolver al consultar el índice de código. Valores más altos proporcionan más contexto pero pueden incluir resultados menos relevantes.", - "resetToDefault": "Restablecer al valor predeterminado" + "resetToDefault": "Restablecer al valor predeterminado", + "stopIndexingButton": "Detener indexación", + "stoppingButton": "Deteniendo...", + "workspaceToggleLabel": "Activar indexación para este espacio de trabajo", + "workspaceDisabledMessage": "La indexación está configurada pero no habilitada para este espacio de trabajo.", + "autoEnableDefaultLabel": "Habilitar automáticamente la indexación para nuevos espacios de trabajo" }, "autoApprove": { "toggleShortcut": "Puedes configurar un atajo global para esta configuración en las preferencias de tu IDE.", diff --git a/webview-ui/src/i18n/locales/fr/chat.json b/webview-ui/src/i18n/locales/fr/chat.json index e2858c8e8c1..af9375f8815 100644 --- a/webview-ui/src/i18n/locales/fr/chat.json +++ b/webview-ui/src/i18n/locales/fr/chat.json @@ -424,7 +424,8 @@ "indexing": "Indexation {{percentage}}%", "indexed": "Indexé", "error": "Erreur d'index", - "status": "Statut de l'index" + "status": "Statut de l'index", + "stopping": "Arrêt de l'indexation..." }, "versionIndicator": { "ariaLabel": "Version {{version}} - Cliquez pour voir les notes de version" diff --git a/webview-ui/src/i18n/locales/fr/settings.json b/webview-ui/src/i18n/locales/fr/settings.json index 4a213f31801..ea476f08e02 100644 --- a/webview-ui/src/i18n/locales/fr/settings.json +++ b/webview-ui/src/i18n/locales/fr/settings.json @@ -211,7 +211,12 @@ "searchMinScoreResetTooltip": "Réinitialiser à la valeur par défaut (0.4)", "searchMaxResultsLabel": "Résultats de recherche maximum", "searchMaxResultsDescription": "Nombre maximum de résultats de recherche à retourner lors de l'interrogation de l'index de code. Des valeurs plus élevées fournissent plus de contexte mais peuvent inclure des résultats moins pertinents.", - "resetToDefault": "Réinitialiser par défaut" + "resetToDefault": "Réinitialiser par défaut", + "stopIndexingButton": "Arrêter l'indexation", + "stoppingButton": "Arrêt en cours...", + "workspaceToggleLabel": "Activer l'indexation pour cet espace de travail", + "workspaceDisabledMessage": "L'indexation est configurée mais non activée pour cet espace de travail.", + "autoEnableDefaultLabel": "Activer automatiquement l'indexation pour les nouveaux espaces de travail" }, "autoApprove": { "toggleShortcut": "Vous pouvez configurer un raccourci global pour ce paramètre dans les préférences de votre IDE.", diff --git a/webview-ui/src/i18n/locales/hi/chat.json b/webview-ui/src/i18n/locales/hi/chat.json index 75ee2c9abf5..96195823f27 100644 --- a/webview-ui/src/i18n/locales/hi/chat.json +++ b/webview-ui/src/i18n/locales/hi/chat.json @@ -424,7 +424,8 @@ "indexing": "इंडेक्सिंग {{percentage}}%", "indexed": "इंडेक्स किया गया", "error": "इंडेक्स त्रुटि", - "status": "इंडेक्स स्थिति" + "status": "इंडेक्स स्थिति", + "stopping": "इंडेक्सिंग रोक रहा है..." }, "versionIndicator": { "ariaLabel": "संस्करण {{version}} - रिलीज़ नोट्स देखने के लिए क्लिक करें" diff --git a/webview-ui/src/i18n/locales/hi/settings.json b/webview-ui/src/i18n/locales/hi/settings.json index 3544dbb7681..5e523e5e6cd 100644 --- a/webview-ui/src/i18n/locales/hi/settings.json +++ b/webview-ui/src/i18n/locales/hi/settings.json @@ -211,7 +211,12 @@ "searchMinScoreResetTooltip": "डिफ़ॉल्ट मान पर रीसेट करें (0.4)", "searchMaxResultsLabel": "अधिकतम खोज परिणाम", "searchMaxResultsDescription": "कोडबेस इंडेक्स को क्वेरी करते समय वापस करने के लिए खोज परिणामों की अधिकतम संख्या। उच्च मान अधिक संदर्भ प्रदान करते हैं लेकिन कम प्रासंगिक परिणाम शामिल कर सकते हैं।", - "resetToDefault": "डिफ़ॉल्ट पर रीसेट करें" + "resetToDefault": "डिफ़ॉल्ट पर रीसेट करें", + "stopIndexingButton": "इंडेक्सिंग रोकें", + "stoppingButton": "रोक रहा है...", + "workspaceToggleLabel": "इस वर्कस्पेस के लिए इंडेक्सिंग सक्षम करें", + "workspaceDisabledMessage": "इंडेक्सिंग कॉन्फ़िगर की गई है लेकिन इस वर्कस्पेस के लिए सक्षम नहीं है।", + "autoEnableDefaultLabel": "नए वर्कस्पेस के लिए स्वचालित रूप से इंडेक्सिंग सक्षम करें" }, "autoApprove": { "toggleShortcut": "आप अपनी आईडीई वरीयताओं में इस सेटिंग के लिए एक वैश्विक शॉर्टकट कॉन्फ़िगर कर सकते हैं।", diff --git a/webview-ui/src/i18n/locales/id/chat.json b/webview-ui/src/i18n/locales/id/chat.json index ee41b9ceb65..0d04ba26862 100644 --- a/webview-ui/src/i18n/locales/id/chat.json +++ b/webview-ui/src/i18n/locales/id/chat.json @@ -430,7 +430,8 @@ "indexing": "Mengindeks {{percentage}}%", "indexed": "Terindeks", "error": "Error indeks", - "status": "Status indeks" + "status": "Status indeks", + "stopping": "Menghentikan pengindeksan..." }, "versionIndicator": { "ariaLabel": "Versi {{version}} - Klik untuk melihat catatan rilis" diff --git a/webview-ui/src/i18n/locales/id/settings.json b/webview-ui/src/i18n/locales/id/settings.json index bc5db3ae85a..ac940f9a505 100644 --- a/webview-ui/src/i18n/locales/id/settings.json +++ b/webview-ui/src/i18n/locales/id/settings.json @@ -211,7 +211,12 @@ "searchMinScoreResetTooltip": "Reset ke nilai default (0.4)", "searchMaxResultsLabel": "Hasil Pencarian Maksimum", "searchMaxResultsDescription": "Jumlah maksimum hasil pencarian yang dikembalikan saat melakukan query indeks basis kode. Nilai yang lebih tinggi memberikan lebih banyak konteks tetapi mungkin menyertakan hasil yang kurang relevan.", - "resetToDefault": "Reset ke default" + "resetToDefault": "Reset ke default", + "stopIndexingButton": "Hentikan pengindeksan", + "stoppingButton": "Menghentikan...", + "workspaceToggleLabel": "Aktifkan pengindeksan untuk ruang kerja ini", + "workspaceDisabledMessage": "Pengindeksan dikonfigurasi tetapi tidak diaktifkan untuk ruang kerja ini.", + "autoEnableDefaultLabel": "Aktifkan pengindeksan secara otomatis untuk ruang kerja baru" }, "autoApprove": { "toggleShortcut": "Anda dapat mengonfigurasi pintasan global untuk pengaturan ini di preferensi IDE Anda.", diff --git a/webview-ui/src/i18n/locales/it/chat.json b/webview-ui/src/i18n/locales/it/chat.json index 54f5bf9061d..0bf8e4195ab 100644 --- a/webview-ui/src/i18n/locales/it/chat.json +++ b/webview-ui/src/i18n/locales/it/chat.json @@ -424,7 +424,8 @@ "indexing": "Indicizzazione {{percentage}}%", "indexed": "Indicizzato", "error": "Errore indice", - "status": "Stato indice" + "status": "Stato indice", + "stopping": "Interruzione dell'indicizzazione..." }, "versionIndicator": { "ariaLabel": "Versione {{version}} - Clicca per visualizzare le note di rilascio" diff --git a/webview-ui/src/i18n/locales/it/settings.json b/webview-ui/src/i18n/locales/it/settings.json index 7eddf6c6127..79297f130fd 100644 --- a/webview-ui/src/i18n/locales/it/settings.json +++ b/webview-ui/src/i18n/locales/it/settings.json @@ -211,7 +211,12 @@ "searchMinScoreResetTooltip": "Ripristina al valore predefinito (0.4)", "searchMaxResultsLabel": "Risultati di ricerca massimi", "searchMaxResultsDescription": "Numero massimo di risultati di ricerca da restituire quando si interroga l'indice del codice. Valori più alti forniscono più contesto ma possono includere risultati meno pertinenti.", - "resetToDefault": "Ripristina al valore predefinito" + "resetToDefault": "Ripristina al valore predefinito", + "stopIndexingButton": "Interrompi indicizzazione", + "stoppingButton": "Interruzione...", + "workspaceToggleLabel": "Abilita l'indicizzazione per questo workspace", + "workspaceDisabledMessage": "L'indicizzazione è configurata ma non abilitata per questo workspace.", + "autoEnableDefaultLabel": "Abilita automaticamente l'indicizzazione per i nuovi workspace" }, "autoApprove": { "toggleShortcut": "Puoi configurare una scorciatoia globale per questa impostazione nelle preferenze del tuo IDE.", diff --git a/webview-ui/src/i18n/locales/ja/chat.json b/webview-ui/src/i18n/locales/ja/chat.json index 2cf71855032..0d196ef5ff4 100644 --- a/webview-ui/src/i18n/locales/ja/chat.json +++ b/webview-ui/src/i18n/locales/ja/chat.json @@ -424,7 +424,8 @@ "indexing": "インデックス作成中 {{percentage}}%", "indexed": "インデックス作成済み", "error": "インデックスエラー", - "status": "インデックス状態" + "status": "インデックス状態", + "stopping": "インデックス作成を停止中..." }, "versionIndicator": { "ariaLabel": "バージョン {{version}} - クリックしてリリースノートを表示" diff --git a/webview-ui/src/i18n/locales/ja/settings.json b/webview-ui/src/i18n/locales/ja/settings.json index 533acf119c9..25ebd575c8e 100644 --- a/webview-ui/src/i18n/locales/ja/settings.json +++ b/webview-ui/src/i18n/locales/ja/settings.json @@ -211,7 +211,12 @@ "searchMinScoreResetTooltip": "デフォルト値(0.4)にリセット", "searchMaxResultsLabel": "最大検索結果数", "searchMaxResultsDescription": "コードベースインデックスをクエリする際に返される検索結果の最大数。値を高くするとより多くのコンテキストが提供されますが、関連性の低い結果が含まれる可能性があります。", - "resetToDefault": "デフォルトにリセット" + "resetToDefault": "デフォルトにリセット", + "stopIndexingButton": "インデックス作成を停止", + "stoppingButton": "停止中...", + "workspaceToggleLabel": "このワークスペースのインデックス作成を有効にする", + "workspaceDisabledMessage": "インデックス作成は設定済みですが、このワークスペースでは有効になっていません。", + "autoEnableDefaultLabel": "新しいワークスペースのインデックス作成を自動的に有効にする" }, "autoApprove": { "toggleShortcut": "IDEの環境設定で、この設定のグローバルショートカットを設定できます。", diff --git a/webview-ui/src/i18n/locales/ko/chat.json b/webview-ui/src/i18n/locales/ko/chat.json index 24debd83edc..4f54090ae37 100644 --- a/webview-ui/src/i18n/locales/ko/chat.json +++ b/webview-ui/src/i18n/locales/ko/chat.json @@ -424,7 +424,8 @@ "indexing": "인덱싱 중 {{percentage}}%", "indexed": "인덱싱 완료", "error": "인덱스 오류", - "status": "인덱스 상태" + "status": "인덱스 상태", + "stopping": "인덱싱 중지 중..." }, "versionIndicator": { "ariaLabel": "버전 {{version}} - 릴리스 노트를 보려면 클릭하세요" diff --git a/webview-ui/src/i18n/locales/ko/settings.json b/webview-ui/src/i18n/locales/ko/settings.json index 52544154027..5ff16d00f8c 100644 --- a/webview-ui/src/i18n/locales/ko/settings.json +++ b/webview-ui/src/i18n/locales/ko/settings.json @@ -211,7 +211,12 @@ "searchMinScoreResetTooltip": "기본값(0.4)으로 재설정", "searchMaxResultsLabel": "최대 검색 결과", "searchMaxResultsDescription": "코드베이스 인덱스를 쿼리할 때 반환할 최대 검색 결과 수입니다. 값이 높을수록 더 많은 컨텍스트를 제공하지만 관련성이 낮은 결과가 포함될 수 있습니다.", - "resetToDefault": "기본값으로 재설정" + "resetToDefault": "기본값으로 재설정", + "stopIndexingButton": "인덱싱 중지", + "stoppingButton": "중지 중...", + "workspaceToggleLabel": "이 워크스페이스에 대한 인덱싱 활성화", + "workspaceDisabledMessage": "인덱싱이 구성되었지만 이 워크스페이스에서는 활성화되지 않았습니다.", + "autoEnableDefaultLabel": "새 워크스페이스에 대한 인덱싱 자동 활성화" }, "autoApprove": { "toggleShortcut": "IDE 환경 설정에서 이 설정에 대한 전역 바로 가기를 구성할 수 있습니다.", diff --git a/webview-ui/src/i18n/locales/nl/chat.json b/webview-ui/src/i18n/locales/nl/chat.json index 0bec6846c04..d3c510a1ed2 100644 --- a/webview-ui/src/i18n/locales/nl/chat.json +++ b/webview-ui/src/i18n/locales/nl/chat.json @@ -424,7 +424,8 @@ "indexing": "Indexeren {{percentage}}%", "indexed": "Geïndexeerd", "error": "Index fout", - "status": "Index status" + "status": "Index status", + "stopping": "Indexering wordt gestopt..." }, "versionIndicator": { "ariaLabel": "Versie {{version}} - Klik om release notes te bekijken" diff --git a/webview-ui/src/i18n/locales/nl/settings.json b/webview-ui/src/i18n/locales/nl/settings.json index db7b0554c3a..55c9cdac6fd 100644 --- a/webview-ui/src/i18n/locales/nl/settings.json +++ b/webview-ui/src/i18n/locales/nl/settings.json @@ -211,7 +211,12 @@ "searchMinScoreResetTooltip": "Reset naar standaardwaarde (0.4)", "searchMaxResultsLabel": "Maximum Zoekresultaten", "searchMaxResultsDescription": "Maximum aantal zoekresultaten dat wordt geretourneerd bij het doorzoeken van de codebase-index. Hogere waarden bieden meer context maar kunnen minder relevante resultaten bevatten.", - "resetToDefault": "Reset naar standaard" + "resetToDefault": "Reset naar standaard", + "stopIndexingButton": "Indexering stoppen", + "stoppingButton": "Stoppen...", + "workspaceToggleLabel": "Indexering inschakelen voor deze werkruimte", + "workspaceDisabledMessage": "Indexering is geconfigureerd maar niet ingeschakeld voor deze werkruimte.", + "autoEnableDefaultLabel": "Indexering automatisch inschakelen voor nieuwe werkruimtes" }, "autoApprove": { "toggleShortcut": "U kunt een globale sneltoets voor deze instelling configureren in de voorkeuren van uw IDE.", diff --git a/webview-ui/src/i18n/locales/pl/chat.json b/webview-ui/src/i18n/locales/pl/chat.json index be012354aa0..c6a76b631b4 100644 --- a/webview-ui/src/i18n/locales/pl/chat.json +++ b/webview-ui/src/i18n/locales/pl/chat.json @@ -424,7 +424,8 @@ "indexing": "Indeksowanie {{percentage}}%", "indexed": "Zaindeksowane", "error": "Błąd indeksu", - "status": "Status indeksu" + "status": "Status indeksu", + "stopping": "Zatrzymywanie indeksowania..." }, "versionIndicator": { "ariaLabel": "Wersja {{version}} - Kliknij, aby wyświetlić informacje o wydaniu" diff --git a/webview-ui/src/i18n/locales/pl/settings.json b/webview-ui/src/i18n/locales/pl/settings.json index 875f00838da..4f70809e78b 100644 --- a/webview-ui/src/i18n/locales/pl/settings.json +++ b/webview-ui/src/i18n/locales/pl/settings.json @@ -211,7 +211,12 @@ "searchMinScoreResetTooltip": "Zresetuj do wartości domyślnej (0.4)", "searchMaxResultsLabel": "Maksymalna liczba wyników wyszukiwania", "searchMaxResultsDescription": "Maksymalna liczba wyników wyszukiwania zwracanych podczas zapytania do indeksu bazy kodu. Wyższe wartości zapewniają więcej kontekstu, ale mogą zawierać mniej istotne wyniki.", - "resetToDefault": "Przywróć domyślne" + "resetToDefault": "Przywróć domyślne", + "stopIndexingButton": "Zatrzymaj indeksowanie", + "stoppingButton": "Zatrzymywanie...", + "workspaceToggleLabel": "Włącz indeksowanie dla tego workspace'a", + "workspaceDisabledMessage": "Indeksowanie jest skonfigurowane, ale nie włączone dla tego workspace'a.", + "autoEnableDefaultLabel": "Automatycznie włączaj indeksowanie dla nowych workspace'ów" }, "autoApprove": { "toggleShortcut": "Możesz skonfigurować globalny skrót dla tego ustawienia w preferencjach swojego IDE.", diff --git a/webview-ui/src/i18n/locales/pt-BR/chat.json b/webview-ui/src/i18n/locales/pt-BR/chat.json index ed4eaf541ea..ecee8c160d7 100644 --- a/webview-ui/src/i18n/locales/pt-BR/chat.json +++ b/webview-ui/src/i18n/locales/pt-BR/chat.json @@ -424,7 +424,8 @@ "indexing": "Indexando {{percentage}}%", "indexed": "Indexado", "error": "Erro do índice", - "status": "Status do índice" + "status": "Status do índice", + "stopping": "Parando a indexação..." }, "versionIndicator": { "ariaLabel": "Versão {{version}} - Clique para ver as notas de lançamento" diff --git a/webview-ui/src/i18n/locales/pt-BR/settings.json b/webview-ui/src/i18n/locales/pt-BR/settings.json index 0d77ea6e5aa..ba68f18eff8 100644 --- a/webview-ui/src/i18n/locales/pt-BR/settings.json +++ b/webview-ui/src/i18n/locales/pt-BR/settings.json @@ -211,7 +211,12 @@ "searchMinScoreResetTooltip": "Redefinir para o valor padrão (0.4)", "searchMaxResultsLabel": "Resultados máximos de busca", "searchMaxResultsDescription": "Número máximo de resultados de busca a retornar ao consultar o índice de código. Valores mais altos fornecem mais contexto, mas podem incluir resultados menos relevantes.", - "resetToDefault": "Redefinir para o padrão" + "resetToDefault": "Redefinir para o padrão", + "stopIndexingButton": "Parar indexação", + "stoppingButton": "Parando...", + "workspaceToggleLabel": "Ativar indexação para este workspace", + "workspaceDisabledMessage": "A indexação está configurada, mas não ativada para este workspace.", + "autoEnableDefaultLabel": "Ativar indexação automaticamente para novos workspaces" }, "autoApprove": { "toggleShortcut": "Você pode configurar um atalho global para esta configuração nas preferências do seu IDE.", diff --git a/webview-ui/src/i18n/locales/ru/chat.json b/webview-ui/src/i18n/locales/ru/chat.json index 15d8c9ee990..99ecd811da2 100644 --- a/webview-ui/src/i18n/locales/ru/chat.json +++ b/webview-ui/src/i18n/locales/ru/chat.json @@ -425,7 +425,8 @@ "indexing": "Индексация {{percentage}}%", "indexed": "Проиндексировано", "error": "Ошибка индекса", - "status": "Статус индекса" + "status": "Статус индекса", + "stopping": "Остановка индексации..." }, "versionIndicator": { "ariaLabel": "Версия {{version}} - Нажмите, чтобы просмотреть примечания к выпуску" diff --git a/webview-ui/src/i18n/locales/ru/settings.json b/webview-ui/src/i18n/locales/ru/settings.json index 87563bdab66..8c49bf6ee8d 100644 --- a/webview-ui/src/i18n/locales/ru/settings.json +++ b/webview-ui/src/i18n/locales/ru/settings.json @@ -211,7 +211,12 @@ "searchMinScoreResetTooltip": "Сбросить к значению по умолчанию (0.4)", "searchMaxResultsLabel": "Максимальное количество результатов поиска", "searchMaxResultsDescription": "Максимальное количество результатов поиска, возвращаемых при запросе индекса кодовой базы. Более высокие значения предоставляют больше контекста, но могут включать менее релевантные результаты.", - "resetToDefault": "Сбросить к значению по умолчанию" + "resetToDefault": "Сбросить к значению по умолчанию", + "stopIndexingButton": "Остановить индексацию", + "stoppingButton": "Остановка...", + "workspaceToggleLabel": "Включить индексацию для этого рабочего пространства", + "workspaceDisabledMessage": "Индексация настроена, но не включена для этого рабочего пространства.", + "autoEnableDefaultLabel": "Автоматически включать индексацию для новых рабочих пространств" }, "autoApprove": { "toggleShortcut": "Вы можете настроить глобальное сочетание клавиш для этого параметра в настройках вашей IDE.", diff --git a/webview-ui/src/i18n/locales/tr/chat.json b/webview-ui/src/i18n/locales/tr/chat.json index e9552d5bfd1..38170926c76 100644 --- a/webview-ui/src/i18n/locales/tr/chat.json +++ b/webview-ui/src/i18n/locales/tr/chat.json @@ -425,7 +425,8 @@ "indexing": "İndeksleniyor {{percentage}}%", "indexed": "İndekslendi", "error": "İndeks hatası", - "status": "İndeks durumu" + "status": "İndeks durumu", + "stopping": "İndeksleme durduruluyor..." }, "versionIndicator": { "ariaLabel": "Sürüm {{version}} - Sürüm notlarını görüntülemek için tıklayın" diff --git a/webview-ui/src/i18n/locales/tr/settings.json b/webview-ui/src/i18n/locales/tr/settings.json index ee46b9f01cd..73178c816ff 100644 --- a/webview-ui/src/i18n/locales/tr/settings.json +++ b/webview-ui/src/i18n/locales/tr/settings.json @@ -211,7 +211,12 @@ "searchMinScoreResetTooltip": "Varsayılan değere sıfırla (0.4)", "searchMaxResultsLabel": "Maksimum Arama Sonuçları", "searchMaxResultsDescription": "Kod tabanı dizinini sorgularken döndürülecek maksimum arama sonucu sayısı. Daha yüksek değerler daha fazla bağlam sağlar ancak daha az alakalı sonuçlar içerebilir.", - "resetToDefault": "Varsayılana sıfırla" + "resetToDefault": "Varsayılana sıfırla", + "stopIndexingButton": "İndekslemeyi durdur", + "stoppingButton": "Durduruluyor...", + "workspaceToggleLabel": "Bu çalışma alanı için indekslemeyi etkinleştir", + "workspaceDisabledMessage": "İndeksleme yapılandırıldı ancak bu çalışma alanı için etkinleştirilmedi.", + "autoEnableDefaultLabel": "Yeni çalışma alanları için indekslemeyi otomatik etkinleştir" }, "autoApprove": { "toggleShortcut": "IDE tercihlerinizde bu ayar için genel bir kısayol yapılandırabilirsiniz.", diff --git a/webview-ui/src/i18n/locales/vi/chat.json b/webview-ui/src/i18n/locales/vi/chat.json index dfa5b56c4b2..3d003c9ed74 100644 --- a/webview-ui/src/i18n/locales/vi/chat.json +++ b/webview-ui/src/i18n/locales/vi/chat.json @@ -425,7 +425,8 @@ "indexing": "Đang lập chỉ mục {{percentage}}%", "indexed": "Đã lập chỉ mục", "error": "Lỗi chỉ mục", - "status": "Trạng thái chỉ mục" + "status": "Trạng thái chỉ mục", + "stopping": "Đang dừng lập chỉ mục..." }, "versionIndicator": { "ariaLabel": "Phiên bản {{version}} - Nhấp để xem ghi chú phát hành" diff --git a/webview-ui/src/i18n/locales/vi/settings.json b/webview-ui/src/i18n/locales/vi/settings.json index 1eb0fb09d30..ba1776471e7 100644 --- a/webview-ui/src/i18n/locales/vi/settings.json +++ b/webview-ui/src/i18n/locales/vi/settings.json @@ -211,7 +211,12 @@ "searchMinScoreResetTooltip": "Đặt lại về giá trị mặc định (0.4)", "searchMaxResultsLabel": "Số Kết Quả Tìm Kiếm Tối Đa", "searchMaxResultsDescription": "Số lượng kết quả tìm kiếm tối đa được trả về khi truy vấn chỉ mục cơ sở mã. Giá trị cao hơn cung cấp nhiều ngữ cảnh hơn nhưng có thể bao gồm các kết quả ít liên quan hơn.", - "resetToDefault": "Đặt lại về mặc định" + "resetToDefault": "Đặt lại về mặc định", + "stopIndexingButton": "Dừng lập chỉ mục", + "stoppingButton": "Đang dừng...", + "workspaceToggleLabel": "Bật lập chỉ mục cho không gian làm việc này", + "workspaceDisabledMessage": "Lập chỉ mục đã được cấu hình nhưng chưa được bật cho không gian làm việc này.", + "autoEnableDefaultLabel": "Tự động bật lập chỉ mục cho không gian làm việc mới" }, "autoApprove": { "toggleShortcut": "Bạn có thể định cấu hình một phím tắt chung cho cài đặt này trong tùy chọn IDE của bạn.", diff --git a/webview-ui/src/i18n/locales/zh-CN/chat.json b/webview-ui/src/i18n/locales/zh-CN/chat.json index 35646dc1dfd..b9142072bd0 100644 --- a/webview-ui/src/i18n/locales/zh-CN/chat.json +++ b/webview-ui/src/i18n/locales/zh-CN/chat.json @@ -425,7 +425,8 @@ "indexing": "索引中 {{percentage}}%", "indexed": "已索引", "error": "索引错误", - "status": "索引状态" + "status": "索引状态", + "stopping": "正在停止索引..." }, "versionIndicator": { "ariaLabel": "版本 {{version}} - 点击查看发布说明" diff --git a/webview-ui/src/i18n/locales/zh-CN/settings.json b/webview-ui/src/i18n/locales/zh-CN/settings.json index aa0391ac559..d35bb63d726 100644 --- a/webview-ui/src/i18n/locales/zh-CN/settings.json +++ b/webview-ui/src/i18n/locales/zh-CN/settings.json @@ -211,7 +211,12 @@ "searchMinScoreResetTooltip": "恢复默认值 (0.4)", "searchMaxResultsLabel": "最大搜索结果数", "searchMaxResultsDescription": "查询代码库索引时返回的最大搜索结果数。较高的值提供更多上下文,但可能包含相关性较低的结果。", - "resetToDefault": "恢复默认值" + "resetToDefault": "恢复默认值", + "stopIndexingButton": "停止索引", + "stoppingButton": "正在停止...", + "workspaceToggleLabel": "为此工作区启用索引", + "workspaceDisabledMessage": "索引已配置,但尚未为此工作区启用。", + "autoEnableDefaultLabel": "自动为新工作区启用索引" }, "autoApprove": { "toggleShortcut": "您可以在 IDE 首选项中为此设置配置全局快捷方式。", diff --git a/webview-ui/src/i18n/locales/zh-TW/chat.json b/webview-ui/src/i18n/locales/zh-TW/chat.json index 8009d33f21a..ff51b13c2ce 100644 --- a/webview-ui/src/i18n/locales/zh-TW/chat.json +++ b/webview-ui/src/i18n/locales/zh-TW/chat.json @@ -423,7 +423,8 @@ "indexing": "索引中 {{percentage}}%", "indexed": "已索引", "error": "索引錯誤", - "status": "索引狀態" + "status": "索引狀態", + "stopping": "正在停止索引..." }, "versionIndicator": { "ariaLabel": "版本 {{version}} - 點選查看發布說明" diff --git a/webview-ui/src/i18n/locales/zh-TW/settings.json b/webview-ui/src/i18n/locales/zh-TW/settings.json index 0e51b95b6af..e1e8709d81a 100644 --- a/webview-ui/src/i18n/locales/zh-TW/settings.json +++ b/webview-ui/src/i18n/locales/zh-TW/settings.json @@ -221,7 +221,12 @@ "baseUrlRequired": "需要基礎 URL", "modelDimensionMinValue": "模型維度必須大於 0" }, - "optional": "選用" + "optional": "選用", + "stopIndexingButton": "停止索引", + "stoppingButton": "正在停止...", + "workspaceToggleLabel": "為此工作區啟用索引", + "workspaceDisabledMessage": "索引已設定,但尚未為此工作區啟用。", + "autoEnableDefaultLabel": "自動為新工作區啟用索引" }, "autoApprove": { "description": "無需詢問許可即可執行下列動作。請僅在您完全信任且了解安全風險的情況下啟用此功能。",