Skip to content
Closed
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
16 changes: 16 additions & 0 deletions src/core/config/ContextProxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,22 @@ export class ContextProxy {
return Object.fromEntries(GLOBAL_STATE_KEYS.map((key) => [key, this.getGlobalState(key)]))
}

/**
* ExtensionContext.workspaceState
* https://code.visualstudio.com/api/references/vscode-api#ExtensionContext.workspaceState
*/

getWorkspaceState<T = any>(key: string): T | undefined
getWorkspaceState<T = any>(key: string, defaultValue: T): T
getWorkspaceState<T = any>(key: string, defaultValue?: T): T | undefined {
const value = this.originalContext.workspaceState.get<T>(key)
return value !== undefined ? value : defaultValue
}

updateWorkspaceState<T = any>(key: string, value: T) {
return this.originalContext.workspaceState.update(key, value)
}

/**
* ExtensionContext.secrets
* https://code.visualstudio.com/api/references/vscode-api#ExtensionContext.secrets
Expand Down
12 changes: 6 additions & 6 deletions src/core/webview/webviewMessageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1990,12 +1990,12 @@ export const webviewMessageHandler = async (

try {
// Check if embedder provider has changed
const currentConfig = getGlobalState("codebaseIndexConfig") || {}
const currentConfig = provider.contextProxy.getWorkspaceState("codebaseIndexConfig") || {}
const embedderProviderChanged =
currentConfig.codebaseIndexEmbedderProvider !== settings.codebaseIndexEmbedderProvider

// Save global state settings atomically
const globalStateConfig = {
// Save workspace state settings atomically
const workspaceStateConfig = {
...currentConfig,
codebaseIndexEnabled: settings.codebaseIndexEnabled,
codebaseIndexQdrantUrl: settings.codebaseIndexQdrantUrl,
Expand All @@ -2008,8 +2008,8 @@ export const webviewMessageHandler = async (
codebaseIndexSearchMinScore: settings.codebaseIndexSearchMinScore,
}

// Save global state first
await updateGlobalState("codebaseIndexConfig", globalStateConfig)
// Save workspace state first
await provider.contextProxy.updateWorkspaceState("codebaseIndexConfig", workspaceStateConfig)

// Save secrets directly using context proxy
if (settings.codeIndexOpenAiKey !== undefined) {
Expand Down Expand Up @@ -2041,7 +2041,7 @@ export const webviewMessageHandler = async (
await provider.postMessageToWebview({
type: "codeIndexSettingsSaved",
success: true,
settings: globalStateConfig,
settings: workspaceStateConfig,
})

// Update webview state
Expand Down
87 changes: 87 additions & 0 deletions src/services/code-index/__tests__/config-manager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ describe("CodeIndexConfigManager", () => {
// Setup mock ContextProxy
mockContextProxy = {
getGlobalState: vi.fn(),
getWorkspaceState: vi.fn(),
updateWorkspaceState: vi.fn(),
getSecret: vi.fn().mockReturnValue(undefined),
refreshSecrets: vi.fn().mockResolvedValue(undefined),
updateGlobalState: vi.fn(),
Expand Down Expand Up @@ -1811,4 +1813,89 @@ describe("CodeIndexConfigManager", () => {
})
})
})

describe("workspace-specific storage", () => {
it("should use workspace storage instead of global storage", async () => {
const mockWorkspaceConfig = {
codebaseIndexEnabled: true,
codebaseIndexQdrantUrl: "http://workspace-qdrant.local",
codebaseIndexEmbedderProvider: "openai",
codebaseIndexEmbedderModelId: "text-embedding-3-large",
}

mockContextProxy.getWorkspaceState.mockReturnValue(mockWorkspaceConfig)
mockContextProxy.getGlobalState.mockReturnValue(undefined)
setupSecretMocks({
codeIndexOpenAiKey: "test-openai-key",
codeIndexQdrantApiKey: "test-qdrant-key",
})

const result = await configManager.loadConfiguration()

expect(mockContextProxy.getWorkspaceState).toHaveBeenCalledWith("codebaseIndexConfig")
expect(result.currentConfig.qdrantUrl).toBe("http://workspace-qdrant.local")
expect(result.currentConfig.modelId).toBe("text-embedding-3-large")
})

it("should migrate global config to workspace storage when workspace config doesn't exist", async () => {
const mockGlobalConfig = {
codebaseIndexEnabled: true,
codebaseIndexQdrantUrl: "http://global-qdrant.local",
codebaseIndexEmbedderProvider: "openai",
codebaseIndexEmbedderModelId: "text-embedding-3-small",
}

mockContextProxy.getWorkspaceState.mockReturnValue(undefined)
mockContextProxy.getGlobalState.mockReturnValue(mockGlobalConfig)
setupSecretMocks({
codeIndexOpenAiKey: "test-openai-key",
})

const result = await configManager.loadConfiguration()

// Should have migrated global config to workspace
expect(mockContextProxy.updateWorkspaceState).toHaveBeenCalledWith("codebaseIndexConfig", mockGlobalConfig)
expect(result.currentConfig.qdrantUrl).toBe("http://global-qdrant.local")
expect(result.currentConfig.modelId).toBe("text-embedding-3-small")
})

it("should use default config when neither workspace nor global config exists", async () => {
mockContextProxy.getWorkspaceState.mockReturnValue(undefined)
mockContextProxy.getGlobalState.mockReturnValue(undefined)
mockContextProxy.getSecret.mockReturnValue(undefined)

const result = await configManager.loadConfiguration()

expect(result.currentConfig.qdrantUrl).toBe("http://localhost:6333")
expect(result.currentConfig.embedderProvider).toBe("openai")
expect(result.currentConfig.isConfigured).toBe(false)
})

it("should prefer workspace config over global config when both exist", async () => {
const mockWorkspaceConfig = {
codebaseIndexEnabled: true,
codebaseIndexQdrantUrl: "http://workspace-qdrant.local",
codebaseIndexEmbedderProvider: "ollama",
codebaseIndexEmbedderBaseUrl: "http://workspace-ollama.local",
}

const mockGlobalConfig = {
codebaseIndexEnabled: true,
codebaseIndexQdrantUrl: "http://global-qdrant.local",
codebaseIndexEmbedderProvider: "openai",
codebaseIndexEmbedderModelId: "text-embedding-3-small",
}

mockContextProxy.getWorkspaceState.mockReturnValue(mockWorkspaceConfig)
mockContextProxy.getGlobalState.mockReturnValue(mockGlobalConfig)

const result = await configManager.loadConfiguration()

// Should use workspace config, not global
expect(result.currentConfig.qdrantUrl).toBe("http://workspace-qdrant.local")
expect(result.currentConfig.embedderProvider).toBe("ollama")
// Should NOT have called updateWorkspaceState since workspace config already exists
expect(mockContextProxy.updateWorkspaceState).not.toHaveBeenCalled()
})
})
})
31 changes: 22 additions & 9 deletions src/services/code-index/config-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,28 @@ export class CodeIndexConfigManager {
* This eliminates code duplication between initializeWithCurrentConfig() and loadConfiguration().
*/
private _loadAndSetConfiguration(): void {
// Load configuration from storage
const codebaseIndexConfig = this.contextProxy?.getGlobalState("codebaseIndexConfig") ?? {
codebaseIndexEnabled: true,
codebaseIndexQdrantUrl: "http://localhost:6333",
codebaseIndexEmbedderProvider: "openai",
codebaseIndexEmbedderBaseUrl: "",
codebaseIndexEmbedderModelId: "",
codebaseIndexSearchMinScore: undefined,
codebaseIndexSearchMaxResults: undefined,
// Load configuration from workspace storage first, then fall back to global storage for migration
let codebaseIndexConfig = this.contextProxy?.getWorkspaceState("codebaseIndexConfig")

// If no workspace config exists, check for global config and migrate it
if (!codebaseIndexConfig) {
const globalConfig = this.contextProxy?.getGlobalState("codebaseIndexConfig")
if (globalConfig) {
// Migrate global config to workspace
this.contextProxy?.updateWorkspaceState("codebaseIndexConfig", globalConfig)
codebaseIndexConfig = globalConfig
} else {
// Use default configuration
codebaseIndexConfig = {
codebaseIndexEnabled: true,
codebaseIndexQdrantUrl: "http://localhost:6333",
codebaseIndexEmbedderProvider: "openai",
codebaseIndexEmbedderBaseUrl: "",
codebaseIndexEmbedderModelId: "",
codebaseIndexSearchMinScore: undefined,
codebaseIndexSearchMaxResults: undefined,
}
}
}

const {
Expand Down