Skip to content

Commit 6d414c3

Browse files
committed
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
1 parent fd5bdc7 commit 6d414c3

File tree

4 files changed

+131
-15
lines changed

4 files changed

+131
-15
lines changed

src/core/config/ContextProxy.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,22 @@ export class ContextProxy {
128128
return Object.fromEntries(GLOBAL_STATE_KEYS.map((key) => [key, this.getGlobalState(key)]))
129129
}
130130

131+
/**
132+
* ExtensionContext.workspaceState
133+
* https://code.visualstudio.com/api/references/vscode-api#ExtensionContext.workspaceState
134+
*/
135+
136+
getWorkspaceState<T = any>(key: string): T | undefined
137+
getWorkspaceState<T = any>(key: string, defaultValue: T): T
138+
getWorkspaceState<T = any>(key: string, defaultValue?: T): T | undefined {
139+
const value = this.originalContext.workspaceState.get<T>(key)
140+
return value !== undefined ? value : defaultValue
141+
}
142+
143+
updateWorkspaceState<T = any>(key: string, value: T) {
144+
return this.originalContext.workspaceState.update(key, value)
145+
}
146+
131147
/**
132148
* ExtensionContext.secrets
133149
* https://code.visualstudio.com/api/references/vscode-api#ExtensionContext.secrets

src/core/webview/webviewMessageHandler.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1990,12 +1990,12 @@ export const webviewMessageHandler = async (
19901990

19911991
try {
19921992
// Check if embedder provider has changed
1993-
const currentConfig = getGlobalState("codebaseIndexConfig") || {}
1993+
const currentConfig = provider.contextProxy.getWorkspaceState("codebaseIndexConfig") || {}
19941994
const embedderProviderChanged =
19951995
currentConfig.codebaseIndexEmbedderProvider !== settings.codebaseIndexEmbedderProvider
19961996

1997-
// Save global state settings atomically
1998-
const globalStateConfig = {
1997+
// Save workspace state settings atomically
1998+
const workspaceStateConfig = {
19991999
...currentConfig,
20002000
codebaseIndexEnabled: settings.codebaseIndexEnabled,
20012001
codebaseIndexQdrantUrl: settings.codebaseIndexQdrantUrl,
@@ -2008,8 +2008,8 @@ export const webviewMessageHandler = async (
20082008
codebaseIndexSearchMinScore: settings.codebaseIndexSearchMinScore,
20092009
}
20102010

2011-
// Save global state first
2012-
await updateGlobalState("codebaseIndexConfig", globalStateConfig)
2011+
// Save workspace state first
2012+
await provider.contextProxy.updateWorkspaceState("codebaseIndexConfig", workspaceStateConfig)
20132013

20142014
// Save secrets directly using context proxy
20152015
if (settings.codeIndexOpenAiKey !== undefined) {
@@ -2041,7 +2041,7 @@ export const webviewMessageHandler = async (
20412041
await provider.postMessageToWebview({
20422042
type: "codeIndexSettingsSaved",
20432043
success: true,
2044-
settings: globalStateConfig,
2044+
settings: workspaceStateConfig,
20452045
})
20462046

20472047
// Update webview state

src/services/code-index/__tests__/config-manager.spec.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ describe("CodeIndexConfigManager", () => {
3030
// Setup mock ContextProxy
3131
mockContextProxy = {
3232
getGlobalState: vi.fn(),
33+
getWorkspaceState: vi.fn(),
34+
updateWorkspaceState: vi.fn(),
3335
getSecret: vi.fn().mockReturnValue(undefined),
3436
refreshSecrets: vi.fn().mockResolvedValue(undefined),
3537
updateGlobalState: vi.fn(),
@@ -1811,4 +1813,89 @@ describe("CodeIndexConfigManager", () => {
18111813
})
18121814
})
18131815
})
1816+
1817+
describe("workspace-specific storage", () => {
1818+
it("should use workspace storage instead of global storage", async () => {
1819+
const mockWorkspaceConfig = {
1820+
codebaseIndexEnabled: true,
1821+
codebaseIndexQdrantUrl: "http://workspace-qdrant.local",
1822+
codebaseIndexEmbedderProvider: "openai",
1823+
codebaseIndexEmbedderModelId: "text-embedding-3-large",
1824+
}
1825+
1826+
mockContextProxy.getWorkspaceState.mockReturnValue(mockWorkspaceConfig)
1827+
mockContextProxy.getGlobalState.mockReturnValue(undefined)
1828+
setupSecretMocks({
1829+
codeIndexOpenAiKey: "test-openai-key",
1830+
codeIndexQdrantApiKey: "test-qdrant-key",
1831+
})
1832+
1833+
const result = await configManager.loadConfiguration()
1834+
1835+
expect(mockContextProxy.getWorkspaceState).toHaveBeenCalledWith("codebaseIndexConfig")
1836+
expect(result.currentConfig.qdrantUrl).toBe("http://workspace-qdrant.local")
1837+
expect(result.currentConfig.modelId).toBe("text-embedding-3-large")
1838+
})
1839+
1840+
it("should migrate global config to workspace storage when workspace config doesn't exist", async () => {
1841+
const mockGlobalConfig = {
1842+
codebaseIndexEnabled: true,
1843+
codebaseIndexQdrantUrl: "http://global-qdrant.local",
1844+
codebaseIndexEmbedderProvider: "openai",
1845+
codebaseIndexEmbedderModelId: "text-embedding-3-small",
1846+
}
1847+
1848+
mockContextProxy.getWorkspaceState.mockReturnValue(undefined)
1849+
mockContextProxy.getGlobalState.mockReturnValue(mockGlobalConfig)
1850+
setupSecretMocks({
1851+
codeIndexOpenAiKey: "test-openai-key",
1852+
})
1853+
1854+
const result = await configManager.loadConfiguration()
1855+
1856+
// Should have migrated global config to workspace
1857+
expect(mockContextProxy.updateWorkspaceState).toHaveBeenCalledWith("codebaseIndexConfig", mockGlobalConfig)
1858+
expect(result.currentConfig.qdrantUrl).toBe("http://global-qdrant.local")
1859+
expect(result.currentConfig.modelId).toBe("text-embedding-3-small")
1860+
})
1861+
1862+
it("should use default config when neither workspace nor global config exists", async () => {
1863+
mockContextProxy.getWorkspaceState.mockReturnValue(undefined)
1864+
mockContextProxy.getGlobalState.mockReturnValue(undefined)
1865+
mockContextProxy.getSecret.mockReturnValue(undefined)
1866+
1867+
const result = await configManager.loadConfiguration()
1868+
1869+
expect(result.currentConfig.qdrantUrl).toBe("http://localhost:6333")
1870+
expect(result.currentConfig.embedderProvider).toBe("openai")
1871+
expect(result.currentConfig.isConfigured).toBe(false)
1872+
})
1873+
1874+
it("should prefer workspace config over global config when both exist", async () => {
1875+
const mockWorkspaceConfig = {
1876+
codebaseIndexEnabled: true,
1877+
codebaseIndexQdrantUrl: "http://workspace-qdrant.local",
1878+
codebaseIndexEmbedderProvider: "ollama",
1879+
codebaseIndexEmbedderBaseUrl: "http://workspace-ollama.local",
1880+
}
1881+
1882+
const mockGlobalConfig = {
1883+
codebaseIndexEnabled: true,
1884+
codebaseIndexQdrantUrl: "http://global-qdrant.local",
1885+
codebaseIndexEmbedderProvider: "openai",
1886+
codebaseIndexEmbedderModelId: "text-embedding-3-small",
1887+
}
1888+
1889+
mockContextProxy.getWorkspaceState.mockReturnValue(mockWorkspaceConfig)
1890+
mockContextProxy.getGlobalState.mockReturnValue(mockGlobalConfig)
1891+
1892+
const result = await configManager.loadConfiguration()
1893+
1894+
// Should use workspace config, not global
1895+
expect(result.currentConfig.qdrantUrl).toBe("http://workspace-qdrant.local")
1896+
expect(result.currentConfig.embedderProvider).toBe("ollama")
1897+
// Should NOT have called updateWorkspaceState since workspace config already exists
1898+
expect(mockContextProxy.updateWorkspaceState).not.toHaveBeenCalled()
1899+
})
1900+
})
18141901
})

src/services/code-index/config-manager.ts

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -41,15 +41,28 @@ export class CodeIndexConfigManager {
4141
* This eliminates code duplication between initializeWithCurrentConfig() and loadConfiguration().
4242
*/
4343
private _loadAndSetConfiguration(): void {
44-
// Load configuration from storage
45-
const codebaseIndexConfig = this.contextProxy?.getGlobalState("codebaseIndexConfig") ?? {
46-
codebaseIndexEnabled: true,
47-
codebaseIndexQdrantUrl: "http://localhost:6333",
48-
codebaseIndexEmbedderProvider: "openai",
49-
codebaseIndexEmbedderBaseUrl: "",
50-
codebaseIndexEmbedderModelId: "",
51-
codebaseIndexSearchMinScore: undefined,
52-
codebaseIndexSearchMaxResults: undefined,
44+
// Load configuration from workspace storage first, then fall back to global storage for migration
45+
let codebaseIndexConfig = this.contextProxy?.getWorkspaceState("codebaseIndexConfig")
46+
47+
// If no workspace config exists, check for global config and migrate it
48+
if (!codebaseIndexConfig) {
49+
const globalConfig = this.contextProxy?.getGlobalState("codebaseIndexConfig")
50+
if (globalConfig) {
51+
// Migrate global config to workspace
52+
this.contextProxy?.updateWorkspaceState("codebaseIndexConfig", globalConfig)
53+
codebaseIndexConfig = globalConfig
54+
} else {
55+
// Use default configuration
56+
codebaseIndexConfig = {
57+
codebaseIndexEnabled: true,
58+
codebaseIndexQdrantUrl: "http://localhost:6333",
59+
codebaseIndexEmbedderProvider: "openai",
60+
codebaseIndexEmbedderBaseUrl: "",
61+
codebaseIndexEmbedderModelId: "",
62+
codebaseIndexSearchMinScore: undefined,
63+
codebaseIndexSearchMaxResults: undefined,
64+
}
65+
}
5366
}
5467

5568
const {

0 commit comments

Comments
 (0)