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
This commit is contained in:
Roo Code 2025-07-30 02:20:22 +00:00
parent fd5bdc7cb1
commit 6d414c3525
4 changed files with 131 additions and 15 deletions

View file

@ -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

View file

@ -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

View file

@ -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()
})
})
})

View file

@ -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 {