Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 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
1 change: 1 addition & 0 deletions packages/types/src/codebase-index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
1 change: 1 addition & 0 deletions src/core/webview/webviewMessageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
6 changes: 6 additions & 0 deletions src/services/code-index/__tests__/config-manager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: "",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important: Missing test coverage for collection name functionality.

While the test file has been updated to include qdrantCollectionName in assertions, there are no specific tests for:

  1. Collection name changes triggering restart detection
  2. Collection name being properly loaded and saved
  3. Edge cases like empty or invalid collection names

Would it make sense to add dedicated test cases for this new feature?

searchMinScore: 0.4,
})
expect(result.requiresRestart).toBe(false)
Expand Down
9 changes: 9 additions & 0 deletions src/services/code-index/__tests__/service-factory.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,7 @@ describe("CodeIndexServiceFactory", () => {
"http://localhost:6333",
3072,
"test-key",
undefined, // collectionName
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test correctly passes undefined for the new collectionName parameter, but we're missing tests that verify:

  1. Collection name is passed when configured
  2. Sanitization works correctly
  3. Empty/invalid collection names fall back to auto-generation

Should we add test coverage for the new parameter?

)
})

Expand All @@ -392,6 +393,7 @@ describe("CodeIndexServiceFactory", () => {
"http://localhost:6333",
768,
"test-key",
undefined, // collectionName
)
})

Expand All @@ -417,6 +419,7 @@ describe("CodeIndexServiceFactory", () => {
"http://localhost:6333",
3072,
"test-key",
undefined, // collectionName
)
})

Expand Down Expand Up @@ -449,6 +452,7 @@ describe("CodeIndexServiceFactory", () => {
"http://localhost:6333",
modelDimension, // Should use model's built-in dimension, not manual
"test-key",
undefined, // collectionName
)
})

Expand Down Expand Up @@ -480,6 +484,7 @@ describe("CodeIndexServiceFactory", () => {
"http://localhost:6333",
manualDimension, // Should use manual dimension as fallback
"test-key",
undefined, // collectionName
)
})

Expand Down Expand Up @@ -509,6 +514,7 @@ describe("CodeIndexServiceFactory", () => {
"http://localhost:6333",
768,
"test-key",
undefined, // collectionName
)
})

Expand Down Expand Up @@ -578,6 +584,7 @@ describe("CodeIndexServiceFactory", () => {
"http://localhost:6333",
3072,
"test-key",
undefined, // collectionName
)
})

Expand All @@ -603,6 +610,7 @@ describe("CodeIndexServiceFactory", () => {
"http://localhost:6333",
3072,
"test-key",
undefined, // collectionName
)
})

Expand All @@ -627,6 +635,7 @@ describe("CodeIndexServiceFactory", () => {
"http://localhost:6333",
1536,
"test-key",
undefined, // collectionName
)
})

Expand Down
19 changes: 17 additions & 2 deletions src/services/code-index/config-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -46,6 +47,7 @@ export class CodeIndexConfigManager {
const codebaseIndexConfig = this.contextProxy?.getGlobalState("codebaseIndexConfig") ?? {
codebaseIndexEnabled: true,
codebaseIndexQdrantUrl: "http://localhost:6333",
codebaseIndexQdrantCollectionName: "",
codebaseIndexEmbedderProvider: "openai",
codebaseIndexEmbedderBaseUrl: "",
codebaseIndexEmbedderModelId: "",
Expand All @@ -56,6 +58,7 @@ export class CodeIndexConfigManager {
const {
codebaseIndexEnabled,
codebaseIndexQdrantUrl,
codebaseIndexQdrantCollectionName,
codebaseIndexEmbedderProvider,
codebaseIndexEmbedderBaseUrl,
codebaseIndexEmbedderModelId,
Expand All @@ -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
Expand Down Expand Up @@ -148,6 +152,7 @@ export class CodeIndexConfigManager {
mistralOptions?: { apiKey: string }
vercelAiGatewayOptions?: { apiKey: string }
qdrantUrl?: string
qdrantCollectionName?: string
qdrantApiKey?: string
searchMinScore?: number
}
Expand All @@ -168,6 +173,7 @@ export class CodeIndexConfigManager {
mistralApiKey: this.mistralOptions?.apiKey ?? "",
vercelAiGatewayApiKey: this.vercelAiGatewayOptions?.apiKey ?? "",
qdrantUrl: this.qdrantUrl ?? "",
qdrantCollectionName: this.qdrantCollectionName ?? "",
qdrantApiKey: this.qdrantApiKey ?? "",
}

Expand All @@ -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,
},
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -342,7 +351,11 @@ export class CodeIndexConfigManager {
return true
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Critical Issue: Missing restart detection for collection name changes.

The doesConfigChangeRequireRestart method doesn't check if qdrantCollectionName has changed. When users change the collection name, it requires a restart to take effect (since it affects which Qdrant collection is used), but this won't be detected.

Could we add this check around line 351?

Suggested change
return true
if (
prevQdrantUrl !== currentQdrantUrl ||
prevQdrantCollectionName !== currentQdrantCollectionName ||
prevQdrantApiKey !== currentQdrantApiKey
) {
return true
}

}

if (prevQdrantUrl !== currentQdrantUrl || prevQdrantApiKey !== currentQdrantApiKey) {
if (
prevQdrantUrl !== currentQdrantUrl ||
prevQdrantCollectionName !== currentQdrantCollectionName ||
prevQdrantApiKey !== currentQdrantApiKey
) {
return true
}

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
}
}
Expand Down
2 changes: 2 additions & 0 deletions src/services/code-index/interfaces/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export interface CodeIndexConfig {
mistralOptions?: { apiKey: string }
vercelAiGatewayOptions?: { apiKey: string }
qdrantUrl?: string
qdrantCollectionName?: string
qdrantApiKey?: string
searchMinScore?: number
searchMaxResults?: number
Expand All @@ -38,5 +39,6 @@ export type PreviousConfigSnapshot = {
mistralApiKey?: string
vercelAiGatewayApiKey?: string
qdrantUrl?: string
qdrantCollectionName?: string
qdrantApiKey?: string
}
10 changes: 8 additions & 2 deletions src/services/code-index/service-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
}

/**
Expand Down
15 changes: 11 additions & 4 deletions src/services/code-index/vector-store/qdrant-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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()) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice sanitization logic! However, could we add a comment explaining the regex pattern for future maintainers?

Suggested change
if (collectionName && collectionName.trim()) {
// 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_-]+$
// Invalid characters are replaced with hyphens
this.collectionName = collectionName.trim().replace(/[^a-zA-Z0-9_-]/g, "-")
} else {

// 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)}`
}

/**
Expand Down
1 change: 1 addition & 0 deletions src/shared/WebviewMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,7 @@ export interface WebviewMessage {
// Global state settings
codebaseIndexEnabled: boolean
codebaseIndexQdrantUrl: string
codebaseIndexQdrantCollectionName?: string
codebaseIndexEmbedderProvider:
| "openai"
| "ollama"
Expand Down
27 changes: 27 additions & 0 deletions webview-ui/src/components/chat/CodeIndexPopover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ interface LocalCodeIndexSettings {
// Global state settings
codebaseIndexEnabled: boolean
codebaseIndexQdrantUrl: string
codebaseIndexQdrantCollectionName?: string
codebaseIndexEmbedderProvider: EmbedderProvider
codebaseIndexEmbedderBaseUrl?: string
codebaseIndexEmbedderModelId: string
Expand Down Expand Up @@ -181,6 +182,7 @@ export const CodeIndexPopover: React.FC<CodeIndexPopoverProps> = ({
const getDefaultSettings = (): LocalCodeIndexSettings => ({
codebaseIndexEnabled: true,
codebaseIndexQdrantUrl: "",
codebaseIndexQdrantCollectionName: "",
codebaseIndexEmbedderProvider: "openai",
codebaseIndexEmbedderBaseUrl: "",
codebaseIndexEmbedderModelId: "",
Expand Down Expand Up @@ -213,6 +215,7 @@ export const CodeIndexPopover: React.FC<CodeIndexPopoverProps> = ({
const settings = {
codebaseIndexEnabled: codebaseIndexConfig.codebaseIndexEnabled ?? true,
codebaseIndexQdrantUrl: codebaseIndexConfig.codebaseIndexQdrantUrl || "",
codebaseIndexQdrantCollectionName: codebaseIndexConfig.codebaseIndexQdrantCollectionName || "",
codebaseIndexEmbedderProvider: codebaseIndexConfig.codebaseIndexEmbedderProvider || "openai",
codebaseIndexEmbedderBaseUrl: codebaseIndexConfig.codebaseIndexEmbedderBaseUrl || "",
codebaseIndexEmbedderModelId: codebaseIndexConfig.codebaseIndexEmbedderModelId || "",
Expand Down Expand Up @@ -1160,6 +1163,30 @@ export const CodeIndexPopover: React.FC<CodeIndexPopoverProps> = ({
)}
</div>

<div className="space-y-2">
<label className="text-sm font-medium">
{t("settings:codeIndex.qdrantCollectionNameLabel")}
</label>
<VSCodeTextField
value={currentSettings.codebaseIndexQdrantCollectionName || ""}
onInput={(e: any) =>
updateSetting("codebaseIndexQdrantCollectionName", e.target.value)
}
placeholder={t("settings:codeIndex.qdrantCollectionNamePlaceholder")}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: The UI doesn't validate collection names before saving.

While the backend sanitizes invalid characters, it might be better UX to warn users in the UI when they enter invalid characters. Could we add validation feedback similar to other fields?

Also, is the placeholder text informative enough about valid characters? Maybe something like: "Enter collection name (letters, numbers, -, _ only)"?

className={cn("w-full", {
"border-red-500": formErrors.codebaseIndexQdrantCollectionName,
})}
/>
{formErrors.codebaseIndexQdrantCollectionName && (
<p className="text-xs text-vscode-errorForeground mt-1 mb-0">
{formErrors.codebaseIndexQdrantCollectionName}
</p>
)}
<p className="text-xs text-vscode-descriptionForeground mt-1">
{t("settings:codeIndex.qdrantCollectionNameDescription")}
</p>
</div>

<div className="space-y-2">
<label className="text-sm font-medium">
{t("settings:codeIndex.qdrantApiKeyLabel")}
Expand Down
3 changes: 3 additions & 0 deletions webview-ui/src/i18n/locales/en/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@
"qdrantKeyLabel": "Qdrant Key:",
"qdrantApiKeyLabel": "Qdrant API Key",
"qdrantApiKeyPlaceholder": "Enter your Qdrant API key (optional)",
"qdrantCollectionNameLabel": "Qdrant Collection Name",
"qdrantCollectionNamePlaceholder": "Enter collection name (optional)",
"qdrantCollectionNameDescription": "Custom collection name for storing embeddings. If empty, a name will be auto-generated from your workspace path.",
"setupConfigLabel": "Setup",
"advancedConfigLabel": "Advanced Configuration",
"searchMinScoreLabel": "Search Score Threshold",
Expand Down
Loading