Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions packages/types/src/codebase-index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ export const CODEBASE_INDEX_DEFAULTS = {
MAX_SEARCH_SCORE: 1,
DEFAULT_SEARCH_MIN_SCORE: 0.4,
SEARCH_SCORE_STEP: 0.05,
// Auto-start and concurrency control settings
DEFAULT_AUTO_START: true,
DEFAULT_MAX_CONCURRENT: 1,
MIN_MAX_CONCURRENT: 1,
MAX_MAX_CONCURRENT: 10,
} as const

/**
Expand Down Expand Up @@ -42,6 +47,13 @@ export const codebaseIndexConfigSchema = z.object({
.min(CODEBASE_INDEX_DEFAULTS.MIN_SEARCH_RESULTS)
.max(CODEBASE_INDEX_DEFAULTS.MAX_SEARCH_RESULTS)
.optional(),
// Auto-start and concurrency control settings
codebaseIndexAutoStart: z.boolean().optional(),
codebaseIndexMaxConcurrent: z
.number()
.min(CODEBASE_INDEX_DEFAULTS.MIN_MAX_CONCURRENT)
.max(CODEBASE_INDEX_DEFAULTS.MAX_MAX_CONCURRENT)
.optional(),
// OpenAI Compatible specific fields
codebaseIndexOpenAiCompatibleBaseUrl: z.string().optional(),
codebaseIndexOpenAiCompatibleModelDimension: z.number().optional(),
Expand Down
84 changes: 74 additions & 10 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { TerminalRegistry } from "./integrations/terminal/TerminalRegistry"
import { claudeCodeOAuthManager } from "./integrations/claude-code/oauth"
import { McpServerManager } from "./services/mcp/McpServerManager"
import { CodeIndexManager } from "./services/code-index/manager"
import { CodeIndexConfigManager } from "./services/code-index/config-manager"
import { MdmService } from "./services/mdm/MdmService"
import { migrateSettings } from "./utils/migrateSettings"
import { autoImportSettings } from "./utils/autoImportSettings"
Expand Down Expand Up @@ -114,26 +115,89 @@ export async function activate(context: vscode.ExtensionContext) {

const contextProxy = await ContextProxy.getInstance(context)

// Initialize code index managers for all workspace folders.
// Initialize code index managers for all workspace folders with concurrency control.
// This prevents overwhelming local embedding providers (like Ollama) on multi-root workspaces.
const codeIndexManagers: CodeIndexManager[] = []

if (vscode.workspace.workspaceFolders) {
// Create all managers first (without initializing them)
for (const folder of vscode.workspace.workspaceFolders) {
const manager = CodeIndexManager.getInstance(context, folder.uri.fsPath)

if (manager) {
codeIndexManagers.push(manager)
context.subscriptions.push(manager)
}
}

// Initialize in background; do not block extension activation
void manager.initialize(contextProxy).catch((error) => {
const message = error instanceof Error ? error.message : String(error)
outputChannel.appendLine(
`[CodeIndexManager] Error during background CodeIndexManager configuration/indexing for ${folder.uri.fsPath}: ${message}`,
)
})
// Get configuration to check auto-start and max concurrent settings
// We use a temporary config manager just to read the settings
const tempConfigManager = new CodeIndexConfigManager(contextProxy)
const shouldAutoStart = tempConfigManager.shouldAutoStart
const maxConcurrent = tempConfigManager.currentMaxConcurrent

context.subscriptions.push(manager)
if (shouldAutoStart && codeIndexManagers.length > 0) {
// Initialize managers with concurrency control
outputChannel.appendLine(
`[CodeIndexManager] Starting sequential indexing for ${codeIndexManagers.length} workspace folder(s) with max ${maxConcurrent} concurrent`,
)

// Create a queue to manage initialization with concurrency control
const initializeWithConcurrencyControl = async () => {
const pendingManagers = [...codeIndexManagers]
const activePromises: Promise<void>[] = []

const startNext = async () => {
while (pendingManagers.length > 0 && activePromises.length < maxConcurrent) {
const manager = pendingManagers.shift()
if (!manager) break

const workspacePath = (manager as any).workspacePath || "unknown"
outputChannel.appendLine(`[CodeIndexManager] Starting initialization for: ${workspacePath}`)

const initPromise = manager
.initialize(contextProxy)
.then(() => {
outputChannel.appendLine(
`[CodeIndexManager] Completed initialization for: ${workspacePath}`,
)
})
.catch((error) => {
const message = error instanceof Error ? error.message : String(error)
outputChannel.appendLine(
`[CodeIndexManager] Error during initialization for ${workspacePath}: ${message}`,
)
})
.finally(() => {
// Remove this promise from active list
const index = activePromises.indexOf(initPromise)
if (index > -1) {
activePromises.splice(index, 1)
}
// Start next manager if there are more pending
if (pendingManagers.length > 0) {
startNext()
}
})

activePromises.push(initPromise)
}
}

// Start initial batch up to maxConcurrent
await startNext()

// Wait for all to complete
while (activePromises.length > 0) {
await Promise.race(activePromises)
}
}

// Run initialization in background; do not block extension activation
void initializeWithConcurrencyControl()
} else if (!shouldAutoStart) {
outputChannel.appendLine(
`[CodeIndexManager] Auto-start disabled. Skipping automatic indexing for ${codeIndexManagers.length} workspace folder(s).`,
)
}
}

Expand Down
142 changes: 142 additions & 0 deletions src/services/code-index/__tests__/config-manager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1928,4 +1928,146 @@ describe("CodeIndexConfigManager", () => {
})
})
})

describe("auto-start and concurrency settings", () => {
describe("shouldAutoStart", () => {
it("should return true by default when codebaseIndexAutoStart is not set", async () => {
mockContextProxy.getGlobalState.mockReturnValue({
codebaseIndexEnabled: true,
codebaseIndexEmbedderProvider: "openai",
codebaseIndexQdrantUrl: "http://localhost:6333",
// codebaseIndexAutoStart not set
})
mockContextProxy.getSecret.mockImplementation((key: string) => {
if (key === "codeIndexOpenAiKey") return "test-key"
return undefined
})

configManager = new CodeIndexConfigManager(mockContextProxy)
await configManager.loadConfiguration()
expect(configManager.shouldAutoStart).toBe(true)
})

it("should return true when codebaseIndexAutoStart is explicitly true", async () => {
mockContextProxy.getGlobalState.mockReturnValue({
codebaseIndexEnabled: true,
codebaseIndexEmbedderProvider: "openai",
codebaseIndexQdrantUrl: "http://localhost:6333",
codebaseIndexAutoStart: true,
})
mockContextProxy.getSecret.mockImplementation((key: string) => {
if (key === "codeIndexOpenAiKey") return "test-key"
return undefined
})

configManager = new CodeIndexConfigManager(mockContextProxy)
await configManager.loadConfiguration()
expect(configManager.shouldAutoStart).toBe(true)
})

it("should return false when codebaseIndexAutoStart is false", async () => {
mockContextProxy.getGlobalState.mockReturnValue({
codebaseIndexEnabled: true,
codebaseIndexEmbedderProvider: "openai",
codebaseIndexQdrantUrl: "http://localhost:6333",
codebaseIndexAutoStart: false,
})
mockContextProxy.getSecret.mockImplementation((key: string) => {
if (key === "codeIndexOpenAiKey") return "test-key"
return undefined
})

configManager = new CodeIndexConfigManager(mockContextProxy)
await configManager.loadConfiguration()
expect(configManager.shouldAutoStart).toBe(false)
})
})

describe("currentMaxConcurrent", () => {
it("should return 1 by default when codebaseIndexMaxConcurrent is not set", async () => {
mockContextProxy.getGlobalState.mockReturnValue({
codebaseIndexEnabled: true,
codebaseIndexEmbedderProvider: "openai",
codebaseIndexQdrantUrl: "http://localhost:6333",
// codebaseIndexMaxConcurrent not set
})
mockContextProxy.getSecret.mockImplementation((key: string) => {
if (key === "codeIndexOpenAiKey") return "test-key"
return undefined
})

configManager = new CodeIndexConfigManager(mockContextProxy)
await configManager.loadConfiguration()
expect(configManager.currentMaxConcurrent).toBe(1)
})

it("should return the configured value when codebaseIndexMaxConcurrent is set", async () => {
mockContextProxy.getGlobalState.mockReturnValue({
codebaseIndexEnabled: true,
codebaseIndexEmbedderProvider: "openai",
codebaseIndexQdrantUrl: "http://localhost:6333",
codebaseIndexMaxConcurrent: 3,
})
mockContextProxy.getSecret.mockImplementation((key: string) => {
if (key === "codeIndexOpenAiKey") return "test-key"
return undefined
})

configManager = new CodeIndexConfigManager(mockContextProxy)
await configManager.loadConfiguration()
expect(configManager.currentMaxConcurrent).toBe(3)
})

it("should handle boundary values correctly", async () => {
// Test minimum value (1)
mockContextProxy.getGlobalState.mockReturnValue({
codebaseIndexEnabled: true,
codebaseIndexEmbedderProvider: "openai",
codebaseIndexQdrantUrl: "http://localhost:6333",
codebaseIndexMaxConcurrent: 1,
})
mockContextProxy.getSecret.mockImplementation((key: string) => {
if (key === "codeIndexOpenAiKey") return "test-key"
return undefined
})

configManager = new CodeIndexConfigManager(mockContextProxy)
await configManager.loadConfiguration()
expect(configManager.currentMaxConcurrent).toBe(1)

// Test maximum value (10)
mockContextProxy.getGlobalState.mockReturnValue({
codebaseIndexEnabled: true,
codebaseIndexEmbedderProvider: "openai",
codebaseIndexQdrantUrl: "http://localhost:6333",
codebaseIndexMaxConcurrent: 10,
})

const maxManager = new CodeIndexConfigManager(mockContextProxy)
await maxManager.loadConfiguration()
expect(maxManager.currentMaxConcurrent).toBe(10)
})
})

describe("combined auto-start and concurrency behavior", () => {
it("should respect both settings when both are configured", async () => {
mockContextProxy.getGlobalState.mockReturnValue({
codebaseIndexEnabled: true,
codebaseIndexEmbedderProvider: "openai",
codebaseIndexQdrantUrl: "http://localhost:6333",
codebaseIndexAutoStart: false,
codebaseIndexMaxConcurrent: 2,
})
mockContextProxy.getSecret.mockImplementation((key: string) => {
if (key === "codeIndexOpenAiKey") return "test-key"
return undefined
})

configManager = new CodeIndexConfigManager(mockContextProxy)
await configManager.loadConfiguration()
expect(configManager.shouldAutoStart).toBe(false)
expect(configManager.currentMaxConcurrent).toBe(2)
})
})
})
})
25 changes: 24 additions & 1 deletion src/services/code-index/config-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { ApiHandlerOptions } from "../../shared/api"
import { ContextProxy } from "../../core/config/ContextProxy"
import { EmbedderProvider } from "./interfaces/manager"
import { CodeIndexConfig, PreviousConfigSnapshot } from "./interfaces/config"
import { DEFAULT_SEARCH_MIN_SCORE, DEFAULT_MAX_SEARCH_RESULTS } from "./constants"
import { DEFAULT_SEARCH_MIN_SCORE, DEFAULT_MAX_SEARCH_RESULTS, DEFAULT_AUTO_START, DEFAULT_MAX_CONCURRENT } from "./constants"
import { getDefaultModelId, getModelDimension, getModelScoreThreshold } from "../../shared/embeddingModels"

/**
Expand All @@ -26,6 +26,9 @@ export class CodeIndexConfigManager {
private qdrantApiKey?: string
private searchMinScore?: number
private searchMaxResults?: number
// Auto-start and concurrency control settings
private autoStart?: boolean
private maxConcurrent?: number

constructor(private readonly contextProxy: ContextProxy) {
// Initialize with current configuration to avoid false restart triggers
Expand Down Expand Up @@ -148,6 +151,10 @@ export class CodeIndexConfigManager {
this.bedrockOptions = bedrockRegion
? { region: bedrockRegion, profile: bedrockProfile || undefined }
: undefined

// Load auto-start and concurrency control settings
this.autoStart = codebaseIndexConfig.codebaseIndexAutoStart
this.maxConcurrent = codebaseIndexConfig.codebaseIndexMaxConcurrent
}

/**
Expand Down Expand Up @@ -541,4 +548,20 @@ export class CodeIndexConfigManager {
public get currentSearchMaxResults(): number {
return this.searchMaxResults ?? DEFAULT_MAX_SEARCH_RESULTS
}

/**
* Gets whether indexing should auto-start on extension activation.
* Returns user setting if configured, otherwise returns default (true).
*/
public get shouldAutoStart(): boolean {
return this.autoStart ?? DEFAULT_AUTO_START
}

/**
* Gets the maximum number of concurrent indexing operations.
* Returns user setting if configured, otherwise returns default (1).
*/
public get currentMaxConcurrent(): number {
return this.maxConcurrent ?? DEFAULT_MAX_CONCURRENT
}
}
4 changes: 4 additions & 0 deletions src/services/code-index/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ export const MAX_CHARS_TOLERANCE_FACTOR = 1.15 // 15% tolerance for max chars
export const DEFAULT_SEARCH_MIN_SCORE = CODEBASE_INDEX_DEFAULTS.DEFAULT_SEARCH_MIN_SCORE
export const DEFAULT_MAX_SEARCH_RESULTS = CODEBASE_INDEX_DEFAULTS.DEFAULT_SEARCH_RESULTS

/**Auto-start and concurrency control */
export const DEFAULT_AUTO_START = CODEBASE_INDEX_DEFAULTS.DEFAULT_AUTO_START
export const DEFAULT_MAX_CONCURRENT = CODEBASE_INDEX_DEFAULTS.DEFAULT_MAX_CONCURRENT

/**File Watcher */
export const QDRANT_CODE_BLOCK_NAMESPACE = "f47ac10b-58cc-4372-a567-0e02b2c3d479"
export const MAX_FILE_SIZE_BYTES = 1 * 1024 * 1024 // 1MB
Expand Down
Loading
Loading