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>
This commit is contained in:
James Mtendamema 2026-02-18 23:09:17 -07:00 committed by GitHub
parent d7359ff228
commit 67ea856fd0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
70 changed files with 983 additions and 101 deletions

View file

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

View file

@ -508,9 +508,12 @@ export interface WebviewMessage {
| "condenseTaskContextRequest"
| "requestIndexingStatus"
| "startIndexing"
| "stopIndexing"
| "clearIndexData"
| "indexingStatusUpdate"
| "indexCleared"
| "toggleWorkspaceIndexing"
| "setAutoEnableDefault"
| "focusPanelRequest"
| "openExternal"
| "filterMarketplaceItems"
@ -705,7 +708,7 @@ export const checkoutRestorePayloadSchema = z.object({
export type CheckpointRestorePayload = z.infer<typeof checkoutRestorePayloadSchema>
export interface IndexingStatusPayload {
state: "Standby" | "Indexing" | "Indexed" | "Error"
state: "Standby" | "Indexing" | "Indexed" | "Error" | "Stopping"
message: string
}
@ -739,6 +742,8 @@ export interface IndexingStatus {
totalItems: number
currentItemUnit?: string
workspacePath?: string
workspaceEnabled?: boolean
autoEnableDefault?: boolean
}
export interface IndexingStatusUpdateMessage {

View file

@ -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"
@ -2608,7 +2609,6 @@ export const webviewMessageHandler = async (
try {
const manager = provider.getCurrentWorkspaceCodeIndexManager()
if (!manager) {
// No workspace open - send error status
provider.postMessageToWebview({
type: "indexingStatusUpdate",
values: {
@ -2622,23 +2622,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()
}
@ -2650,6 +2646,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()

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -69,6 +69,8 @@
"fileWatcherStopped": "फाइल वॉचर रुक गया।",
"failedDuringInitialScan": "प्रारंभिक स्कैन के दौरान असफल: {{errorMessage}}",
"unknownError": "अज्ञात त्रुटि",
"indexingRequiresWorkspace": "इंडेक्सिंग के लिए एक खुला वर्कस्पेस फ़ोल्डर आवश्यक है"
"indexingRequiresWorkspace": "इंडेक्सिंग के लिए एक खुला वर्कस्पेस फ़ोल्डर आवश्यक है",
"indexingStopped": "उपयोगकर्ता द्वारा इंडेक्सिंग रोकी गई।",
"indexingStoppedPartial": "इंडेक्सिंग रोकी गई। आंशिक इंडेक्स डेटा संरक्षित।"
}
}

View file

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

View file

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

View file

@ -69,6 +69,8 @@
"fileWatcherStopped": "ファイルウォッチャーが停止されました。",
"failedDuringInitialScan": "初期スキャン中に失敗しました:{{errorMessage}}",
"unknownError": "不明なエラー",
"indexingRequiresWorkspace": "インデックス作成には、開かれたワークスペースフォルダーが必要です"
"indexingRequiresWorkspace": "インデックス作成には、開かれたワークスペースフォルダーが必要です",
"indexingStopped": "ユーザーによりインデックス作成が停止されました。",
"indexingStoppedPartial": "インデックス作成が停止されました。部分的なインデックスデータは保持されています。"
}
}

View file

@ -69,6 +69,8 @@
"fileWatcherStopped": "파일 감시자가 중지되었습니다.",
"failedDuringInitialScan": "초기 스캔 중 실패: {{errorMessage}}",
"unknownError": "알 수 없는 오류",
"indexingRequiresWorkspace": "인덱싱에는 열린 워크스페이스 폴더가 필요합니다"
"indexingRequiresWorkspace": "인덱싱에는 열린 워크스페이스 폴더가 필요합니다",
"indexingStopped": "사용자에 의해 인덱싱이 중지되었습니다.",
"indexingStoppedPartial": "인덱싱이 중지되었습니다. 부분 인덱스 데이터가 보존되었습니다."
}
}

View file

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

View file

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

View file

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

View file

@ -69,6 +69,8 @@
"fileWatcherStopped": "Наблюдатель файлов остановлен.",
"failedDuringInitialScan": "Ошибка во время первоначального сканирования: {{errorMessage}}",
"unknownError": "Неизвестная ошибка",
"indexingRequiresWorkspace": "Для индексации требуется открытая папка рабочего пространства"
"indexingRequiresWorkspace": "Для индексации требуется открытая папка рабочего пространства",
"indexingStopped": "Индексация остановлена пользователем.",
"indexingStoppedPartial": "Индексация остановлена. Частичные данные индекса сохранены."
}
}

View file

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

View file

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

View file

@ -69,6 +69,8 @@
"fileWatcherStopped": "文件监控已停止。",
"failedDuringInitialScan": "初始扫描失败:{{errorMessage}}",
"unknownError": "未知错误",
"indexingRequiresWorkspace": "索引需要打开的工作区文件夹"
"indexingRequiresWorkspace": "索引需要打开的工作区文件夹",
"indexingStopped": "用户已停止索引。",
"indexingStoppedPartial": "索引已停止。部分索引数据已保留。"
}
}

View file

@ -69,6 +69,8 @@
"fileWatcherStopped": "檔案監控已停止。",
"failedDuringInitialScan": "初始掃描失敗:{{errorMessage}}",
"unknownError": "未知錯誤",
"indexingRequiresWorkspace": "索引需要開啟的工作區資料夾"
"indexingRequiresWorkspace": "索引需要開啟的工作區資料夾",
"indexingStopped": "使用者已停止索引。",
"indexingStoppedPartial": "索引已停止。部分索引資料已保留。"
}
}

View file

@ -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<string, any> = {}
const globalStateStore: Record<string, any> = {}
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<string, any> = {}
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")
})
})
})

View file

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

View file

@ -110,6 +110,13 @@ export class CacheManager implements ICacheManager {
this._debouncedSaveCache()
}
/**
* Flushes any pending debounced cache writes to disk immediately.
*/
async flush(): Promise<void> {
await this._performSave()
}
/**
* Gets a copy of all file hashes
* @returns A copy of the file hashes record

View file

@ -2,5 +2,6 @@ export interface ICacheManager {
getHash(filePath: string): string | undefined
updateHash(filePath: string, hash: string): void
deleteHash(filePath: string): void
flush(): Promise<void>
getAllHashes(): Record<string, string>
}

View file

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

View file

@ -39,6 +39,11 @@ export interface ICodeIndexManager {
*/
startIndexing(): Promise<void>
/**
* 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"

View file

@ -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<boolean | undefined>(this._workspaceEnabledKey(), undefined)
if (explicit !== undefined) return explicit
return this.autoEnableDefault
}
public async setWorkspaceEnabled(enabled: boolean): Promise<void> {
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<void> {
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<void> {
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
}

View file

@ -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,9 +353,22 @@ 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()
}
/**
* Stops the file watcher and cleans up resources.
*/
@ -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

View file

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

View file

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

View file

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

View file

@ -1590,6 +1590,58 @@ export const CodeIndexPopover: React.FC<CodeIndexPopoverProps> = ({
)}
</div>
{/* Auto-enable default */}
{currentSettings.codebaseIndexEnabled && (
<div className="flex items-center gap-2 pt-4 pb-1">
<input
type="checkbox"
id="auto-enable-default-toggle"
checked={indexingStatus.autoEnableDefault ?? true}
onChange={(e) =>
vscode.postMessage({
type: "setAutoEnableDefault",
bool: e.target.checked,
})
}
className="accent-vscode-focusBorder"
/>
<label
htmlFor="auto-enable-default-toggle"
className="text-xs text-vscode-foreground cursor-pointer">
{t("settings:codeIndex.autoEnableDefaultLabel")}
</label>
</div>
)}
{/* Workspace Toggle */}
{currentSettings.codebaseIndexEnabled && (
<div className="flex items-center gap-2 pt-1 pb-2">
<input
type="checkbox"
id="workspace-indexing-toggle"
checked={indexingStatus.workspaceEnabled ?? false}
onChange={(e) =>
vscode.postMessage({
type: "toggleWorkspaceIndexing",
bool: e.target.checked,
})
}
className="accent-vscode-focusBorder"
/>
<label
htmlFor="workspace-indexing-toggle"
className="text-xs text-vscode-foreground cursor-pointer">
{t("settings:codeIndex.workspaceToggleLabel")}
</label>
</div>
)}
{currentSettings.codebaseIndexEnabled && !indexingStatus.workspaceEnabled && (
<p className="text-xs text-vscode-descriptionForeground pb-2">
{t("settings:codeIndex.workspaceDisabledMessage")}
</p>
)}
{/* Action Buttons */}
<div className="flex items-center justify-between gap-2 pt-6">
<div className="flex gap-2">
@ -1603,6 +1655,20 @@ export const CodeIndexPopover: React.FC<CodeIndexPopoverProps> = ({
</Button>
)}
{currentSettings.codebaseIndexEnabled && indexingStatus.systemStatus === "Indexing" && (
<Button
variant="destructive"
onClick={() => vscode.postMessage({ type: "stopIndexing" })}>
{t("settings:codeIndex.stopIndexingButton")}
</Button>
)}
{currentSettings.codebaseIndexEnabled && indexingStatus.systemStatus === "Stopping" && (
<Button variant="destructive" disabled>
{t("settings:codeIndex.stoppingButton")}
</Button>
)}
{currentSettings.codebaseIndexEnabled &&
(indexingStatus.systemStatus === "Indexed" ||
indexingStatus.systemStatus === "Error") && (

View file

@ -64,6 +64,8 @@ export const IndexingStatusBadge: React.FC<IndexingStatusBadgeProps> = ({ 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<IndexingStatusBadgeProps> = ({ 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",
}

View file

@ -428,7 +428,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"

View file

@ -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ó <SettingsLink>a les preferències del teu IDE</SettingsLink>.",

View file

@ -428,7 +428,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"

View file

@ -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 <SettingsLink>in deinen IDE-Einstellungen</SettingsLink> einen globalen Shortcut für diese Einstellung konfigurieren.",

View file

@ -424,7 +424,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"

View file

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

View file

@ -428,7 +428,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"

View file

@ -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 <SettingsLink>en las preferencias de tu IDE</SettingsLink>.",

View file

@ -428,7 +428,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"

View file

@ -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 <SettingsLink>dans les préférences de votre IDE</SettingsLink>.",

View file

@ -428,7 +428,8 @@
"indexing": "इंडेक्सिंग {{percentage}}%",
"indexed": "इंडेक्स किया गया",
"error": "इंडेक्स त्रुटि",
"status": "इंडेक्स स्थिति"
"status": "इंडेक्स स्थिति",
"stopping": "इंडेक्सिंग रोक रहा है..."
},
"versionIndicator": {
"ariaLabel": "संस्करण {{version}} - रिलीज़ नोट्स देखने के लिए क्लिक करें"

View file

@ -211,7 +211,12 @@
"searchMinScoreResetTooltip": "डिफ़ॉल्ट मान पर रीसेट करें (0.4)",
"searchMaxResultsLabel": "अधिकतम खोज परिणाम",
"searchMaxResultsDescription": "कोडबेस इंडेक्स को क्वेरी करते समय वापस करने के लिए खोज परिणामों की अधिकतम संख्या। उच्च मान अधिक संदर्भ प्रदान करते हैं लेकिन कम प्रासंगिक परिणाम शामिल कर सकते हैं।",
"resetToDefault": "डिफ़ॉल्ट पर रीसेट करें"
"resetToDefault": "डिफ़ॉल्ट पर रीसेट करें",
"stopIndexingButton": "इंडेक्सिंग रोकें",
"stoppingButton": "रोक रहा है...",
"workspaceToggleLabel": "इस वर्कस्पेस के लिए इंडेक्सिंग सक्षम करें",
"workspaceDisabledMessage": "इंडेक्सिंग कॉन्फ़िगर की गई है लेकिन इस वर्कस्पेस के लिए सक्षम नहीं है।",
"autoEnableDefaultLabel": "नए वर्कस्पेस के लिए स्वचालित रूप से इंडेक्सिंग सक्षम करें"
},
"autoApprove": {
"toggleShortcut": "आप <SettingsLink>अपनी आईडीई वरीयताओं में</SettingsLink> इस सेटिंग के लिए एक वैश्विक शॉर्टकट कॉन्फ़िगर कर सकते हैं।",

View file

@ -434,7 +434,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"

View file

@ -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 <SettingsLink>di preferensi IDE Anda</SettingsLink>.",

View file

@ -428,7 +428,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"

View file

@ -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 <SettingsLink>nelle preferenze del tuo IDE</SettingsLink>.",

View file

@ -428,7 +428,8 @@
"indexing": "インデックス作成中 {{percentage}}%",
"indexed": "インデックス作成済み",
"error": "インデックスエラー",
"status": "インデックス状態"
"status": "インデックス状態",
"stopping": "インデックス作成を停止中..."
},
"versionIndicator": {
"ariaLabel": "バージョン {{version}} - クリックしてリリースノートを表示"

View file

@ -211,7 +211,12 @@
"searchMinScoreResetTooltip": "デフォルト値0.4)にリセット",
"searchMaxResultsLabel": "最大検索結果数",
"searchMaxResultsDescription": "コードベースインデックスをクエリする際に返される検索結果の最大数。値を高くするとより多くのコンテキストが提供されますが、関連性の低い結果が含まれる可能性があります。",
"resetToDefault": "デフォルトにリセット"
"resetToDefault": "デフォルトにリセット",
"stopIndexingButton": "インデックス作成を停止",
"stoppingButton": "停止中...",
"workspaceToggleLabel": "このワークスペースのインデックス作成を有効にする",
"workspaceDisabledMessage": "インデックス作成は設定済みですが、このワークスペースでは有効になっていません。",
"autoEnableDefaultLabel": "新しいワークスペースのインデックス作成を自動的に有効にする"
},
"autoApprove": {
"toggleShortcut": "<SettingsLink>IDEの環境設定</SettingsLink>で、この設定のグローバルショートカットを設定できます。",

View file

@ -428,7 +428,8 @@
"indexing": "인덱싱 중 {{percentage}}%",
"indexed": "인덱싱 완료",
"error": "인덱스 오류",
"status": "인덱스 상태"
"status": "인덱스 상태",
"stopping": "인덱싱 중지 중..."
},
"versionIndicator": {
"ariaLabel": "버전 {{version}} - 릴리스 노트를 보려면 클릭하세요"

View file

@ -211,7 +211,12 @@
"searchMinScoreResetTooltip": "기본값(0.4)으로 재설정",
"searchMaxResultsLabel": "최대 검색 결과",
"searchMaxResultsDescription": "코드베이스 인덱스를 쿼리할 때 반환할 최대 검색 결과 수입니다. 값이 높을수록 더 많은 컨텍스트를 제공하지만 관련성이 낮은 결과가 포함될 수 있습니다.",
"resetToDefault": "기본값으로 재설정"
"resetToDefault": "기본값으로 재설정",
"stopIndexingButton": "인덱싱 중지",
"stoppingButton": "중지 중...",
"workspaceToggleLabel": "이 워크스페이스에 대한 인덱싱 활성화",
"workspaceDisabledMessage": "인덱싱이 구성되었지만 이 워크스페이스에서는 활성화되지 않았습니다.",
"autoEnableDefaultLabel": "새 워크스페이스에 대한 인덱싱 자동 활성화"
},
"autoApprove": {
"toggleShortcut": "<SettingsLink>IDE 환경 설정</SettingsLink>에서 이 설정에 대한 전역 바로 가기를 구성할 수 있습니다.",

View file

@ -428,7 +428,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"

View file

@ -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 <SettingsLink>in de voorkeuren van uw IDE</SettingsLink>.",

View file

@ -428,7 +428,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"

View file

@ -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 <SettingsLink>w preferencjach swojego IDE</SettingsLink>.",

View file

@ -428,7 +428,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"

View file

@ -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 <SettingsLink>nas preferências do seu IDE</SettingsLink>.",

View file

@ -429,7 +429,8 @@
"indexing": "Индексация {{percentage}}%",
"indexed": "Проиндексировано",
"error": "Ошибка индекса",
"status": "Статус индекса"
"status": "Статус индекса",
"stopping": "Остановка индексации..."
},
"versionIndicator": {
"ariaLabel": "Версия {{version}} - Нажмите, чтобы просмотреть примечания к выпуску"

View file

@ -211,7 +211,12 @@
"searchMinScoreResetTooltip": "Сбросить к значению по умолчанию (0.4)",
"searchMaxResultsLabel": "Максимальное количество результатов поиска",
"searchMaxResultsDescription": "Максимальное количество результатов поиска, возвращаемых при запросе индекса кодовой базы. Более высокие значения предоставляют больше контекста, но могут включать менее релевантные результаты.",
"resetToDefault": "Сбросить к значению по умолчанию"
"resetToDefault": "Сбросить к значению по умолчанию",
"stopIndexingButton": "Остановить индексацию",
"stoppingButton": "Остановка...",
"workspaceToggleLabel": "Включить индексацию для этого рабочего пространства",
"workspaceDisabledMessage": "Индексация настроена, но не включена для этого рабочего пространства.",
"autoEnableDefaultLabel": "Автоматически включать индексацию для новых рабочих пространств"
},
"autoApprove": {
"toggleShortcut": "Вы можете настроить глобальное сочетание клавиш для этого параметра <SettingsLink>в настройках вашей IDE</SettingsLink>.",

View file

@ -429,7 +429,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"

View file

@ -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": "<SettingsLink>IDE tercihlerinizde</SettingsLink> bu ayar için genel bir kısayol yapılandırabilirsiniz.",

View file

@ -429,7 +429,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"

View file

@ -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 <SettingsLink>trong tùy chọn IDE của bạn</SettingsLink>.",

View file

@ -429,7 +429,8 @@
"indexing": "索引中 {{percentage}}%",
"indexed": "已索引",
"error": "索引错误",
"status": "索引状态"
"status": "索引状态",
"stopping": "正在停止索引..."
},
"versionIndicator": {
"ariaLabel": "版本 {{version}} - 点击查看发布说明"

View file

@ -211,7 +211,12 @@
"searchMinScoreResetTooltip": "恢复默认值 (0.4)",
"searchMaxResultsLabel": "最大搜索结果数",
"searchMaxResultsDescription": "查询代码库索引时返回的最大搜索结果数。较高的值提供更多上下文,但可能包含相关性较低的结果。",
"resetToDefault": "恢复默认值"
"resetToDefault": "恢复默认值",
"stopIndexingButton": "停止索引",
"stoppingButton": "正在停止...",
"workspaceToggleLabel": "为此工作区启用索引",
"workspaceDisabledMessage": "索引已配置,但尚未为此工作区启用。",
"autoEnableDefaultLabel": "自动为新工作区启用索引"
},
"autoApprove": {
"toggleShortcut": "您可以<SettingsLink>在 IDE 首选项中</SettingsLink>为此设置配置全局快捷方式。",

View file

@ -427,7 +427,8 @@
"indexing": "索引中 {{percentage}}%",
"indexed": "已索引",
"error": "索引錯誤",
"status": "索引狀態"
"status": "索引狀態",
"stopping": "正在停止索引..."
},
"versionIndicator": {
"ariaLabel": "版本 {{version}} - 點選查看發布說明"

View file

@ -221,7 +221,12 @@
"baseUrlRequired": "需要基礎 URL",
"modelDimensionMinValue": "模型維度必須大於 0"
},
"optional": "選用"
"optional": "選用",
"stopIndexingButton": "停止索引",
"stoppingButton": "正在停止...",
"workspaceToggleLabel": "為此工作區啟用索引",
"workspaceDisabledMessage": "索引已設定,但尚未為此工作區啟用。",
"autoEnableDefaultLabel": "自動為新工作區啟用索引"
},
"autoApprove": {
"description": "無需詢問許可即可執行下列動作。請僅在您完全信任且了解安全風險的情況下啟用此功能。",