From 6d414c3525ce709789f84a19c4d307bc11b6ac6f Mon Sep 17 00:00:00 2001 From: Roo Code Date: Wed, 30 Jul 2025 02:20:22 +0000 Subject: [PATCH] feat: implement workspace-specific index settings storage - Add workspace storage methods to ContextProxy - Update CodeIndexConfigManager to use workspace storage instead of global - Add migration logic to move existing global settings to workspace - Update webview message handler to save settings per workspace - Add comprehensive tests for workspace-specific storage functionality Fixes #6406 --- src/core/config/ContextProxy.ts | 16 ++++ src/core/webview/webviewMessageHandler.ts | 12 +-- .../__tests__/config-manager.spec.ts | 87 +++++++++++++++++++ src/services/code-index/config-manager.ts | 31 +++++-- 4 files changed, 131 insertions(+), 15 deletions(-) diff --git a/src/core/config/ContextProxy.ts b/src/core/config/ContextProxy.ts index 5535cd2ff4..f9818c1e47 100644 --- a/src/core/config/ContextProxy.ts +++ b/src/core/config/ContextProxy.ts @@ -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(key: string): T | undefined + getWorkspaceState(key: string, defaultValue: T): T + getWorkspaceState(key: string, defaultValue?: T): T | undefined { + const value = this.originalContext.workspaceState.get(key) + return value !== undefined ? value : defaultValue + } + + updateWorkspaceState(key: string, value: T) { + return this.originalContext.workspaceState.update(key, value) + } + /** * ExtensionContext.secrets * https://code.visualstudio.com/api/references/vscode-api#ExtensionContext.secrets diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 763e118125..5e745be464 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -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, @@ -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) { @@ -2041,7 +2041,7 @@ export const webviewMessageHandler = async ( await provider.postMessageToWebview({ type: "codeIndexSettingsSaved", success: true, - settings: globalStateConfig, + settings: workspaceStateConfig, }) // Update webview state diff --git a/src/services/code-index/__tests__/config-manager.spec.ts b/src/services/code-index/__tests__/config-manager.spec.ts index 2d6e704d76..688992cb42 100644 --- a/src/services/code-index/__tests__/config-manager.spec.ts +++ b/src/services/code-index/__tests__/config-manager.spec.ts @@ -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(), @@ -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() + }) + }) }) diff --git a/src/services/code-index/config-manager.ts b/src/services/code-index/config-manager.ts index 1723f1c2a0..3be6de53d3 100644 --- a/src/services/code-index/config-manager.ts +++ b/src/services/code-index/config-manager.ts @@ -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 {