Skip to content
Merged
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
2 changes: 2 additions & 0 deletions packages/types/src/codebase-index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export const codebaseIndexConfigSchema = z.object({
"vercel-ai-gateway",
"bedrock",
"openrouter",
"semble",
])
.optional(),
codebaseIndexEmbedderBaseUrl: z.string().optional(),
Expand Down Expand Up @@ -67,6 +68,7 @@ export const codebaseIndexModelsSchema = z.object({
"vercel-ai-gateway": z.record(z.string(), z.object({ dimension: z.number() })).optional(),
openrouter: z.record(z.string(), z.object({ dimension: z.number() })).optional(),
bedrock: z.record(z.string(), z.object({ dimension: z.number() })).optional(),
semble: z.record(z.string(), z.object({ dimension: z.number() })).optional(),
})

export type CodebaseIndexModels = z.infer<typeof codebaseIndexModelsSchema>
Expand Down
3 changes: 2 additions & 1 deletion packages/types/src/embedding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ export type EmbedderProvider =
| "mistral"
| "vercel-ai-gateway"
| "bedrock"
| "openrouter" // Add other providers as needed.
| "openrouter"
| "semble" // Local hybrid search via semble CLI — no API keys or Qdrant required.

export interface EmbeddingModelProfile {
dimension: number
Expand Down
7 changes: 7 additions & 0 deletions packages/types/src/vscode-extension-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,12 @@ export type ExtensionState = Pick<
deviceName?: string
debug?: boolean

/**
* Platform info for conditional feature support (e.g. semble binary availability).
*/
platform?: string
arch?: string

/**
* Monotonically increasing sequence number for clineMessages state pushes.
* When present, the frontend should only apply clineMessages from a state push
Expand Down Expand Up @@ -666,6 +672,7 @@ export interface WebviewMessage {
| "vercel-ai-gateway"
| "bedrock"
| "openrouter"
| "semble"
codebaseIndexEmbedderBaseUrl?: string
codebaseIndexEmbedderModelId: string
codebaseIndexEmbedderModelDimension?: number // Generic dimension for all providers
Expand Down
2 changes: 2 additions & 0 deletions src/core/webview/ClineProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2452,6 +2452,8 @@ export class ClineProvider
}
})(),
...zooCodeState,
platform: process.platform,
arch: process.arch,
debug: vscode.workspace.getConfiguration(Package.name).get<boolean>("debug", false),
}
}
Expand Down
104 changes: 104 additions & 0 deletions src/services/code-index/__tests__/config-manager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1127,6 +1127,86 @@ describe("CodeIndexConfigManager", () => {
expect(requiresRestart).toBe(true)
})
})

describe("semble provider configuration", () => {
it("should load semble provider configuration", async () => {
mockContextProxy.getGlobalState.mockReturnValue({
codebaseIndexEnabled: true,
codebaseIndexEmbedderProvider: "semble",
})
mockContextProxy.getSecret.mockReturnValue(undefined)

const result = await configManager.loadConfiguration()

expect(result.currentConfig.embedderProvider).toBe("semble")
expect(result.currentConfig.isConfigured).toBe(true)
})

it("should require restart when switching from openai to semble", async () => {
// Initial state with OpenAI
mockContextProxy.getGlobalState.mockReturnValue({
codebaseIndexEnabled: true,
codebaseIndexQdrantUrl: "http://qdrant.local",
codebaseIndexEmbedderProvider: "openai",
codebaseIndexEmbedderModelId: "text-embedding-3-small",
})
setupSecretMocks({
codeIndexOpenAiKey: "test-key",
})

await configManager.loadConfiguration()

// Switch to semble
mockContextProxy.getGlobalState.mockReturnValue({
codebaseIndexEnabled: true,
codebaseIndexEmbedderProvider: "semble",
})
mockContextProxy.getSecret.mockReturnValue(undefined)

const result = await configManager.loadConfiguration()
expect(result.requiresRestart).toBe(true)
})

it("should require restart when switching from semble to openai", async () => {
// Initial state with semble
mockContextProxy.getGlobalState.mockReturnValue({
codebaseIndexEnabled: true,
codebaseIndexEmbedderProvider: "semble",
})
mockContextProxy.getSecret.mockReturnValue(undefined)

await configManager.loadConfiguration()

// Switch to openai
mockContextProxy.getGlobalState.mockReturnValue({
codebaseIndexEnabled: true,
codebaseIndexQdrantUrl: "http://qdrant.local",
codebaseIndexEmbedderProvider: "openai",
codebaseIndexEmbedderModelId: "text-embedding-3-small",
})
setupSecretMocks({
codeIndexOpenAiKey: "test-key",
})

const result = await configManager.loadConfiguration()
expect(result.requiresRestart).toBe(true)
})

it("should not require restart when semble config stays the same", async () => {
// Initial state with semble
mockContextProxy.getGlobalState.mockReturnValue({
codebaseIndexEnabled: true,
codebaseIndexEmbedderProvider: "semble",
})
mockContextProxy.getSecret.mockReturnValue(undefined)

await configManager.loadConfiguration()

// Same semble config again
const result = await configManager.loadConfiguration()
expect(result.requiresRestart).toBe(false)
})
})
})

describe("isConfigured", () => {
Expand Down Expand Up @@ -1684,6 +1764,30 @@ describe("CodeIndexConfigManager", () => {
expect(configManager.isConfigured()).toBe(false)
})

it("should always return true for semble provider (no API keys or Qdrant needed)", () => {
mockContextProxy.getGlobalState.mockReturnValue({
codebaseIndexEnabled: true,
codebaseIndexEmbedderProvider: "semble",
})
mockContextProxy.getSecret.mockReturnValue(undefined)

configManager = new CodeIndexConfigManager(mockContextProxy)
expect(configManager.isConfigured()).toBe(true)
})

it("should return true for semble even without any other configuration", () => {
mockContextProxy.getGlobalState.mockReturnValue({
codebaseIndexEnabled: true,
codebaseIndexEmbedderProvider: "semble",
// No qdrant URL, no API keys
})
mockContextProxy.getSecret.mockReturnValue(undefined)

configManager = new CodeIndexConfigManager(mockContextProxy)
expect(configManager.isConfigured()).toBe(true)
expect(configManager.isFeatureConfigured).toBe(true)
})

describe("currentModelDimension", () => {
beforeEach(() => {
vi.clearAllMocks()
Expand Down
22 changes: 22 additions & 0 deletions src/services/code-index/__tests__/service-factory.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,17 @@ describe("CodeIndexServiceFactory", () => {
// Act & Assert
expect(() => factory.createEmbedder()).toThrow("serviceFactory.invalidEmbedderType")
})

it("should throw when provider is semble (semble handles its own embedding)", () => {
const testConfig = {
embedderProvider: "semble",
}
mockConfigManager.getConfig.mockReturnValue(testConfig as any)

expect(() => factory.createEmbedder()).toThrow(
"Semble provider handles its own embedding. Do not call createEmbedder() for semble",
)
})
})

describe("createVectorStore", () => {
Expand Down Expand Up @@ -678,6 +689,17 @@ describe("CodeIndexServiceFactory", () => {
// Act & Assert
expect(() => factory.createVectorStore()).toThrow("serviceFactory.qdrantUrlMissing")
})

it("should throw when provider is semble (semble handles its own vector storage)", () => {
const testConfig = {
embedderProvider: "semble",
}
mockConfigManager.getConfig.mockReturnValue(testConfig as any)

expect(() => factory.createVectorStore()).toThrow(
"Semble provider handles its own vector storage. Do not call createVectorStore() for semble",
)
})
})

describe("validateEmbedder", () => {
Expand Down
7 changes: 7 additions & 0 deletions src/services/code-index/config-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ export class CodeIndexConfigManager {
this.embedderProvider = "bedrock"
} else if (codebaseIndexEmbedderProvider === "openrouter") {
this.embedderProvider = "openrouter"
} else if (codebaseIndexEmbedderProvider === "semble") {
this.embedderProvider = "semble"
} else {
this.embedderProvider = "openai"
}
Expand Down Expand Up @@ -231,6 +233,11 @@ export class CodeIndexConfigManager {
* Checks if the service is properly configured based on the embedder type.
*/
public isConfigured(): boolean {
if (this.embedderProvider === "semble") {
// Semble requires no API keys or Qdrant — it's always configured
return true
}

if (this.embedderProvider === "openai") {
const openAiKey = this.openAiOptions?.openAiNativeApiKey
const qdrantUrl = this.qdrantUrl
Expand Down
1 change: 1 addition & 0 deletions src/services/code-index/interfaces/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ export type EmbedderProvider =
| "vercel-ai-gateway"
| "bedrock"
| "openrouter"
| "semble"

export interface IndexProgressUpdate {
systemStatus: IndexingState
Expand Down
Loading
Loading