diff --git a/packages/types/src/codebase-index.ts b/packages/types/src/codebase-index.ts index be7778f538..0cd5d825a2 100644 --- a/packages/types/src/codebase-index.ts +++ b/packages/types/src/codebase-index.ts @@ -21,6 +21,7 @@ export const CODEBASE_INDEX_DEFAULTS = { export const codebaseIndexConfigSchema = z.object({ codebaseIndexEnabled: z.boolean().optional(), codebaseIndexQdrantUrl: z.string().optional(), + codebaseIndexQdrantCollectionName: z.string().optional(), codebaseIndexEmbedderProvider: z .enum(["openai", "ollama", "openai-compatible", "gemini", "mistral", "vercel-ai-gateway"]) .optional(), diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 080fbbcd94..867fd6b09a 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -2270,6 +2270,7 @@ export const webviewMessageHandler = async ( ...currentConfig, codebaseIndexEnabled: settings.codebaseIndexEnabled, codebaseIndexQdrantUrl: settings.codebaseIndexQdrantUrl, + codebaseIndexQdrantCollectionName: settings.codebaseIndexQdrantCollectionName, codebaseIndexEmbedderProvider: settings.codebaseIndexEmbedderProvider, codebaseIndexEmbedderBaseUrl: settings.codebaseIndexEmbedderBaseUrl, codebaseIndexEmbedderModelId: settings.codebaseIndexEmbedderModelId, diff --git a/src/services/code-index/__tests__/config-manager.spec.ts b/src/services/code-index/__tests__/config-manager.spec.ts index 9fc096ba74..4e18f17d04 100644 --- a/src/services/code-index/__tests__/config-manager.spec.ts +++ b/src/services/code-index/__tests__/config-manager.spec.ts @@ -104,8 +104,14 @@ describe("CodeIndexConfigManager", () => { modelId: undefined, openAiOptions: { openAiNativeApiKey: "" }, ollamaOptions: { ollamaBaseUrl: "" }, + geminiOptions: undefined, + mistralOptions: undefined, + openAiCompatibleOptions: undefined, + vercelAiGatewayOptions: undefined, + modelDimension: undefined, qdrantUrl: "http://localhost:6333", qdrantApiKey: "", + qdrantCollectionName: "", searchMinScore: 0.4, }) expect(result.requiresRestart).toBe(false) diff --git a/src/services/code-index/__tests__/service-factory.spec.ts b/src/services/code-index/__tests__/service-factory.spec.ts index 1d8f7ba478..5d3d16fec0 100644 --- a/src/services/code-index/__tests__/service-factory.spec.ts +++ b/src/services/code-index/__tests__/service-factory.spec.ts @@ -367,6 +367,7 @@ describe("CodeIndexServiceFactory", () => { "http://localhost:6333", 3072, "test-key", + undefined, // collectionName ) }) @@ -392,6 +393,7 @@ describe("CodeIndexServiceFactory", () => { "http://localhost:6333", 768, "test-key", + undefined, // collectionName ) }) @@ -417,6 +419,7 @@ describe("CodeIndexServiceFactory", () => { "http://localhost:6333", 3072, "test-key", + undefined, // collectionName ) }) @@ -449,6 +452,7 @@ describe("CodeIndexServiceFactory", () => { "http://localhost:6333", modelDimension, // Should use model's built-in dimension, not manual "test-key", + undefined, // collectionName ) }) @@ -480,6 +484,7 @@ describe("CodeIndexServiceFactory", () => { "http://localhost:6333", manualDimension, // Should use manual dimension as fallback "test-key", + undefined, // collectionName ) }) @@ -509,6 +514,7 @@ describe("CodeIndexServiceFactory", () => { "http://localhost:6333", 768, "test-key", + undefined, // collectionName ) }) @@ -578,6 +584,7 @@ describe("CodeIndexServiceFactory", () => { "http://localhost:6333", 3072, "test-key", + undefined, // collectionName ) }) @@ -603,6 +610,7 @@ describe("CodeIndexServiceFactory", () => { "http://localhost:6333", 3072, "test-key", + undefined, // collectionName ) }) @@ -627,6 +635,7 @@ describe("CodeIndexServiceFactory", () => { "http://localhost:6333", 1536, "test-key", + undefined, // collectionName ) }) diff --git a/src/services/code-index/config-manager.ts b/src/services/code-index/config-manager.ts index 2c0e8bb5c9..1a623590d4 100644 --- a/src/services/code-index/config-manager.ts +++ b/src/services/code-index/config-manager.ts @@ -21,6 +21,7 @@ export class CodeIndexConfigManager { private mistralOptions?: { apiKey: string } private vercelAiGatewayOptions?: { apiKey: string } private qdrantUrl?: string = "http://localhost:6333" + private qdrantCollectionName?: string private qdrantApiKey?: string private searchMinScore?: number private searchMaxResults?: number @@ -46,6 +47,7 @@ export class CodeIndexConfigManager { const codebaseIndexConfig = this.contextProxy?.getGlobalState("codebaseIndexConfig") ?? { codebaseIndexEnabled: true, codebaseIndexQdrantUrl: "http://localhost:6333", + codebaseIndexQdrantCollectionName: "", codebaseIndexEmbedderProvider: "openai", codebaseIndexEmbedderBaseUrl: "", codebaseIndexEmbedderModelId: "", @@ -56,6 +58,7 @@ export class CodeIndexConfigManager { const { codebaseIndexEnabled, codebaseIndexQdrantUrl, + codebaseIndexQdrantCollectionName, codebaseIndexEmbedderProvider, codebaseIndexEmbedderBaseUrl, codebaseIndexEmbedderModelId, @@ -75,6 +78,7 @@ export class CodeIndexConfigManager { // Update instance variables with configuration this.codebaseIndexEnabled = codebaseIndexEnabled ?? true this.qdrantUrl = codebaseIndexQdrantUrl + this.qdrantCollectionName = codebaseIndexQdrantCollectionName this.qdrantApiKey = qdrantApiKey ?? "" this.searchMinScore = codebaseIndexSearchMinScore this.searchMaxResults = codebaseIndexSearchMaxResults @@ -148,6 +152,7 @@ export class CodeIndexConfigManager { mistralOptions?: { apiKey: string } vercelAiGatewayOptions?: { apiKey: string } qdrantUrl?: string + qdrantCollectionName?: string qdrantApiKey?: string searchMinScore?: number } @@ -168,6 +173,7 @@ export class CodeIndexConfigManager { mistralApiKey: this.mistralOptions?.apiKey ?? "", vercelAiGatewayApiKey: this.vercelAiGatewayOptions?.apiKey ?? "", qdrantUrl: this.qdrantUrl ?? "", + qdrantCollectionName: this.qdrantCollectionName ?? "", qdrantApiKey: this.qdrantApiKey ?? "", } @@ -193,6 +199,7 @@ export class CodeIndexConfigManager { mistralOptions: this.mistralOptions, vercelAiGatewayOptions: this.vercelAiGatewayOptions, qdrantUrl: this.qdrantUrl, + qdrantCollectionName: this.qdrantCollectionName, qdrantApiKey: this.qdrantApiKey, searchMinScore: this.currentSearchMinScore, }, @@ -270,6 +277,7 @@ export class CodeIndexConfigManager { const prevMistralApiKey = prev?.mistralApiKey ?? "" const prevVercelAiGatewayApiKey = prev?.vercelAiGatewayApiKey ?? "" const prevQdrantUrl = prev?.qdrantUrl ?? "" + const prevQdrantCollectionName = prev?.qdrantCollectionName ?? "" const prevQdrantApiKey = prev?.qdrantApiKey ?? "" // 1. Transition from disabled/unconfigured to enabled/configured @@ -308,6 +316,7 @@ export class CodeIndexConfigManager { const currentMistralApiKey = this.mistralOptions?.apiKey ?? "" const currentVercelAiGatewayApiKey = this.vercelAiGatewayOptions?.apiKey ?? "" const currentQdrantUrl = this.qdrantUrl ?? "" + const currentQdrantCollectionName = this.qdrantCollectionName ?? "" const currentQdrantApiKey = this.qdrantApiKey ?? "" if (prevOpenAiKey !== currentOpenAiKey) { @@ -342,7 +351,11 @@ export class CodeIndexConfigManager { return true } - if (prevQdrantUrl !== currentQdrantUrl || prevQdrantApiKey !== currentQdrantApiKey) { + if ( + prevQdrantUrl !== currentQdrantUrl || + prevQdrantCollectionName !== currentQdrantCollectionName || + prevQdrantApiKey !== currentQdrantApiKey + ) { return true } @@ -396,6 +409,7 @@ export class CodeIndexConfigManager { mistralOptions: this.mistralOptions, vercelAiGatewayOptions: this.vercelAiGatewayOptions, qdrantUrl: this.qdrantUrl, + qdrantCollectionName: this.qdrantCollectionName, qdrantApiKey: this.qdrantApiKey, searchMinScore: this.currentSearchMinScore, searchMaxResults: this.currentSearchMaxResults, @@ -426,9 +440,10 @@ export class CodeIndexConfigManager { /** * Gets the current Qdrant configuration */ - public get qdrantConfig(): { url?: string; apiKey?: string } { + public get qdrantConfig(): { url?: string; collectionName?: string; apiKey?: string } { return { url: this.qdrantUrl, + collectionName: this.qdrantCollectionName, apiKey: this.qdrantApiKey, } } diff --git a/src/services/code-index/interfaces/config.ts b/src/services/code-index/interfaces/config.ts index f168e26869..2d18b649dc 100644 --- a/src/services/code-index/interfaces/config.ts +++ b/src/services/code-index/interfaces/config.ts @@ -16,6 +16,7 @@ export interface CodeIndexConfig { mistralOptions?: { apiKey: string } vercelAiGatewayOptions?: { apiKey: string } qdrantUrl?: string + qdrantCollectionName?: string qdrantApiKey?: string searchMinScore?: number searchMaxResults?: number @@ -38,5 +39,6 @@ export type PreviousConfigSnapshot = { mistralApiKey?: string vercelAiGatewayApiKey?: string qdrantUrl?: string + qdrantCollectionName?: string qdrantApiKey?: string } diff --git a/src/services/code-index/service-factory.ts b/src/services/code-index/service-factory.ts index 6d69e1f0b6..8ae95bfe63 100644 --- a/src/services/code-index/service-factory.ts +++ b/src/services/code-index/service-factory.ts @@ -145,8 +145,14 @@ export class CodeIndexServiceFactory { throw new Error(t("embeddings:serviceFactory.qdrantUrlMissing")) } - // Assuming constructor is updated: new QdrantVectorStore(workspacePath, url, vectorSize, apiKey?) - return new QdrantVectorStore(this.workspacePath, config.qdrantUrl, vectorSize, config.qdrantApiKey) + // Pass collection name if configured, otherwise let QdrantVectorStore generate it + return new QdrantVectorStore( + this.workspacePath, + config.qdrantUrl, + vectorSize, + config.qdrantApiKey, + config.qdrantCollectionName, + ) } /** diff --git a/src/services/code-index/vector-store/qdrant-client.ts b/src/services/code-index/vector-store/qdrant-client.ts index ce152824a7..bce2535ad3 100644 --- a/src/services/code-index/vector-store/qdrant-client.ts +++ b/src/services/code-index/vector-store/qdrant-client.ts @@ -24,7 +24,7 @@ export class QdrantVectorStore implements IVectorStore { * @param workspacePath Path to the workspace * @param url Optional URL to the Qdrant server */ - constructor(workspacePath: string, url: string, vectorSize: number, apiKey?: string) { + constructor(workspacePath: string, url: string, vectorSize: number, apiKey?: string, collectionName?: string) { // Parse the URL to determine the appropriate QdrantClient configuration const parsedUrl = this.parseQdrantUrl(url) @@ -77,10 +77,17 @@ export class QdrantVectorStore implements IVectorStore { }) } - // Generate collection name from workspace path - const hash = createHash("sha256").update(workspacePath).digest("hex") + // Use provided collection name or generate from workspace path + if (collectionName && collectionName.trim()) { + // Sanitize the collection name to ensure it's valid for Qdrant + // Qdrant collection names must match: ^[a-zA-Z0-9_-]+$ + this.collectionName = collectionName.trim().replace(/[^a-zA-Z0-9_-]/g, "-") + } else { + // Generate collection name from workspace path (default behavior) + const hash = createHash("sha256").update(workspacePath).digest("hex") + this.collectionName = `ws-${hash.substring(0, 16)}` + } this.vectorSize = vectorSize - this.collectionName = `ws-${hash.substring(0, 16)}` } /** diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index 565712bfbf..d8f6f00f98 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -270,6 +270,7 @@ export interface WebviewMessage { // Global state settings codebaseIndexEnabled: boolean codebaseIndexQdrantUrl: string + codebaseIndexQdrantCollectionName?: string codebaseIndexEmbedderProvider: | "openai" | "ollama" diff --git a/webview-ui/src/components/chat/CodeIndexPopover.tsx b/webview-ui/src/components/chat/CodeIndexPopover.tsx index 45bf4224a1..bd3a4e5752 100644 --- a/webview-ui/src/components/chat/CodeIndexPopover.tsx +++ b/webview-ui/src/components/chat/CodeIndexPopover.tsx @@ -58,6 +58,7 @@ interface LocalCodeIndexSettings { // Global state settings codebaseIndexEnabled: boolean codebaseIndexQdrantUrl: string + codebaseIndexQdrantCollectionName?: string codebaseIndexEmbedderProvider: EmbedderProvider codebaseIndexEmbedderBaseUrl?: string codebaseIndexEmbedderModelId: string @@ -181,6 +182,7 @@ export const CodeIndexPopover: React.FC = ({ const getDefaultSettings = (): LocalCodeIndexSettings => ({ codebaseIndexEnabled: true, codebaseIndexQdrantUrl: "", + codebaseIndexQdrantCollectionName: "", codebaseIndexEmbedderProvider: "openai", codebaseIndexEmbedderBaseUrl: "", codebaseIndexEmbedderModelId: "", @@ -213,6 +215,7 @@ export const CodeIndexPopover: React.FC = ({ const settings = { codebaseIndexEnabled: codebaseIndexConfig.codebaseIndexEnabled ?? true, codebaseIndexQdrantUrl: codebaseIndexConfig.codebaseIndexQdrantUrl || "", + codebaseIndexQdrantCollectionName: codebaseIndexConfig.codebaseIndexQdrantCollectionName || "", codebaseIndexEmbedderProvider: codebaseIndexConfig.codebaseIndexEmbedderProvider || "openai", codebaseIndexEmbedderBaseUrl: codebaseIndexConfig.codebaseIndexEmbedderBaseUrl || "", codebaseIndexEmbedderModelId: codebaseIndexConfig.codebaseIndexEmbedderModelId || "", @@ -1160,6 +1163,30 @@ export const CodeIndexPopover: React.FC = ({ )} +
+ + + updateSetting("codebaseIndexQdrantCollectionName", e.target.value) + } + placeholder={t("settings:codeIndex.qdrantCollectionNamePlaceholder")} + className={cn("w-full", { + "border-red-500": formErrors.codebaseIndexQdrantCollectionName, + })} + /> + {formErrors.codebaseIndexQdrantCollectionName && ( +

+ {formErrors.codebaseIndexQdrantCollectionName} +

+ )} +

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

+
+