Skip to content

Commit cfc5529

Browse files
committed
feat: Add per-workspace codebase indexing control
- Add workspace-specific storage support to ContextProxy - Update CodeIndexConfigManager to check workspace settings before global - Add message handlers for workspace-specific indexing settings - Add UI support with workspace override toggle in CodeIndexPopover - Add translation keys for new UI elements - Update tests to support new workspace-specific methods Fixes #7511
1 parent cd9e92f commit cfc5529

File tree

11 files changed

+259
-8
lines changed

11 files changed

+259
-8
lines changed

src/core/config/ContextProxy.ts

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,18 +39,25 @@ export class ContextProxy {
3939
private stateCache: GlobalState
4040
private secretCache: SecretState
4141
private _isInitialized = false
42+
private _workspacePath: string | undefined
4243

43-
constructor(context: vscode.ExtensionContext) {
44+
constructor(context: vscode.ExtensionContext, workspacePath?: string) {
4445
this.originalContext = context
4546
this.stateCache = {}
4647
this.secretCache = {}
4748
this._isInitialized = false
49+
this._workspacePath = workspacePath
4850
}
4951

5052
public get isInitialized() {
5153
return this._isInitialized
5254
}
5355

56+
// Public getter for workspacePath to allow checking current workspace
57+
public get workspacePath(): string | undefined {
58+
return this._workspacePath
59+
}
60+
5461
public async initialize() {
5562
for (const key of GLOBAL_STATE_KEYS) {
5663
try {
@@ -290,6 +297,50 @@ export class ContextProxy {
290297
await this.initialize()
291298
}
292299

300+
/**
301+
* Get workspace-specific state value
302+
* Falls back to global state if workspace value doesn't exist
303+
*/
304+
public getWorkspaceState<T>(key: string, defaultValue?: T): T | undefined {
305+
if (!this._workspacePath) {
306+
// If no workspace, fall back to global state
307+
return this.originalContext.globalState.get<T>(key) ?? defaultValue
308+
}
309+
310+
// Create a workspace-specific key
311+
const workspaceKey = `workspace:${this._workspacePath}:${key}`
312+
const workspaceValue = this.originalContext.globalState.get<T>(workspaceKey)
313+
314+
if (workspaceValue !== undefined) {
315+
return workspaceValue
316+
}
317+
318+
// Fall back to global state
319+
return this.originalContext.globalState.get<T>(key) ?? defaultValue
320+
}
321+
322+
/**
323+
* Update workspace-specific state value
324+
*/
325+
public async updateWorkspaceState<T>(key: string, value: T): Promise<void> {
326+
if (!this._workspacePath) {
327+
// If no workspace, update global state
328+
await this.originalContext.globalState.update(key, value)
329+
return
330+
}
331+
332+
// Create a workspace-specific key
333+
const workspaceKey = `workspace:${this._workspacePath}:${key}`
334+
await this.originalContext.globalState.update(workspaceKey, value)
335+
}
336+
337+
/**
338+
* Set the workspace path for workspace-specific settings
339+
*/
340+
public setWorkspacePath(workspacePath: string | undefined): void {
341+
this._workspacePath = workspacePath
342+
}
343+
293344
private static _instance: ContextProxy | null = null
294345

295346
static get instance() {
@@ -300,12 +351,16 @@ export class ContextProxy {
300351
return this._instance
301352
}
302353

303-
static async getInstance(context: vscode.ExtensionContext) {
354+
static async getInstance(context: vscode.ExtensionContext, workspacePath?: string) {
304355
if (this._instance) {
356+
// Update workspace path if provided
357+
if (workspacePath !== undefined) {
358+
this._instance.setWorkspacePath(workspacePath)
359+
}
305360
return this._instance
306361
}
307362

308-
this._instance = new ContextProxy(context)
363+
this._instance = new ContextProxy(context, workspacePath)
309364
await this._instance.initialize()
310365

311366
return this._instance

src/core/webview/webviewMessageHandler.ts

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2054,10 +2054,15 @@ export const webviewMessageHandler = async (
20542054
const embedderProviderChanged =
20552055
currentConfig.codebaseIndexEmbedderProvider !== settings.codebaseIndexEmbedderProvider
20562056

2057-
// Save global state settings atomically
2057+
// Check if this is a workspace-specific setting update
2058+
const isWorkspaceSpecific = settings.workspaceSpecific === true
2059+
2060+
// Save global state settings atomically (always needed for non-enabled settings)
20582061
const globalStateConfig = {
20592062
...currentConfig,
2060-
codebaseIndexEnabled: settings.codebaseIndexEnabled,
2063+
codebaseIndexEnabled: isWorkspaceSpecific
2064+
? currentConfig.codebaseIndexEnabled
2065+
: settings.codebaseIndexEnabled,
20612066
codebaseIndexQdrantUrl: settings.codebaseIndexQdrantUrl,
20622067
codebaseIndexEmbedderProvider: settings.codebaseIndexEmbedderProvider,
20632068
codebaseIndexEmbedderBaseUrl: settings.codebaseIndexEmbedderBaseUrl,
@@ -2071,6 +2076,14 @@ export const webviewMessageHandler = async (
20712076
// Save global state first
20722077
await updateGlobalState("codebaseIndexConfig", globalStateConfig)
20732078

2079+
if (isWorkspaceSpecific) {
2080+
// Save workspace-specific indexing enabled state
2081+
const manager = provider.getCurrentWorkspaceCodeIndexManager()
2082+
if (manager && manager.configManager) {
2083+
await manager.configManager.setWorkspaceIndexingEnabled(settings.codebaseIndexEnabled)
2084+
}
2085+
}
2086+
20742087
// Save secrets directly using context proxy
20752088
if (settings.codeIndexOpenAiKey !== undefined) {
20762089
await provider.contextProxy.storeSecret("codeIndexOpenAiKey", settings.codeIndexOpenAiKey)
@@ -2319,6 +2332,47 @@ export const webviewMessageHandler = async (
23192332
}
23202333
break
23212334
}
2335+
case "clearWorkspaceIndexingSetting": {
2336+
try {
2337+
const manager = provider.getCurrentWorkspaceCodeIndexManager()
2338+
if (manager && manager.configManager) {
2339+
await manager.configManager.clearWorkspaceIndexingSetting()
2340+
// Send updated status
2341+
const status = manager.getCurrentStatus()
2342+
provider.postMessageToWebview({
2343+
type: "indexingStatusUpdate",
2344+
values: status,
2345+
})
2346+
}
2347+
} catch (error) {
2348+
provider.log(
2349+
`Error clearing workspace indexing setting: ${error instanceof Error ? error.message : String(error)}`,
2350+
)
2351+
}
2352+
break
2353+
}
2354+
case "getWorkspaceIndexingSetting": {
2355+
try {
2356+
const manager = provider.getCurrentWorkspaceCodeIndexManager()
2357+
if (manager && manager.configManager) {
2358+
const workspaceSetting = manager.configManager.getWorkspaceIndexingEnabled()
2359+
provider.postMessageToWebview({
2360+
type: "workspaceIndexingSetting",
2361+
enabled: workspaceSetting,
2362+
})
2363+
} else {
2364+
provider.postMessageToWebview({
2365+
type: "workspaceIndexingSetting",
2366+
enabled: undefined,
2367+
})
2368+
}
2369+
} catch (error) {
2370+
provider.log(
2371+
`Error getting workspace indexing setting: ${error instanceof Error ? error.message : String(error)}`,
2372+
)
2373+
}
2374+
break
2375+
}
23222376
case "focusPanelRequest": {
23232377
// Execute the focusPanel command to focus the WebView
23242378
await vscode.commands.executeCommand(getCommand("focusPanel"))

src/extension.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { CloudService, ExtensionBridgeService } from "@roo-code/cloud"
1717
import { TelemetryService, PostHogTelemetryClient } from "@roo-code/telemetry"
1818

1919
import "./utils/path" // Necessary to have access to String.prototype.toPosix.
20+
import { getWorkspacePath } from "./utils/path"
2021
import { createOutputChannelLogger, createDualLogger } from "./utils/outputChannelLogger"
2122

2223
import { Package } from "./shared/package"
@@ -97,8 +98,12 @@ export async function activate(context: vscode.ExtensionContext) {
9798
if (!context.globalState.get("allowedCommands")) {
9899
context.globalState.update("allowedCommands", defaultCommands)
99100
}
100-
101101
const contextProxy = await ContextProxy.getInstance(context)
102+
// Set the workspace path for the ContextProxy to enable workspace-specific settings
103+
const workspacePath = getWorkspacePath()
104+
if (workspacePath) {
105+
contextProxy.setWorkspacePath(workspacePath)
106+
}
102107

103108
// Initialize code index managers for all workspace folders.
104109
const codeIndexManagers: CodeIndexManager[] = []

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ describe("CodeIndexConfigManager", () => {
3131
getSecret: vi.fn().mockReturnValue(undefined),
3232
refreshSecrets: vi.fn().mockResolvedValue(undefined),
3333
updateGlobalState: vi.fn(),
34+
getWorkspaceState: vi.fn().mockReturnValue(undefined),
35+
updateWorkspaceState: vi.fn(),
36+
setWorkspacePath: vi.fn(),
37+
workspacePath: undefined,
3438
}
3539

3640
configManager = new CodeIndexConfigManager(mockContextProxy)

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -538,6 +538,10 @@ describe("CodeIndexManager - handleSettingsChange regression", () => {
538538
codebaseIndexSearchMaxResults: 10,
539539
codebaseIndexSearchMinScore: 0.4,
540540
}),
541+
getWorkspaceState: vi.fn().mockReturnValue(undefined),
542+
updateWorkspaceState: vi.fn(),
543+
setWorkspacePath: vi.fn(),
544+
workspacePath: testWorkspacePath,
541545
}
542546

543547
// Re-initialize

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

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,11 @@ export class CodeIndexConfigManager {
4242
* This eliminates code duplication between initializeWithCurrentConfig() and loadConfiguration().
4343
*/
4444
private _loadAndSetConfiguration(): void {
45-
// Load configuration from storage
46-
const codebaseIndexConfig = this.contextProxy?.getGlobalState("codebaseIndexConfig") ?? {
45+
// First check for workspace-specific override for indexing enabled state
46+
const workspaceIndexEnabled = this.contextProxy?.getWorkspaceState<boolean>("codebaseIndexEnabled")
47+
48+
// Load global configuration from storage
49+
const globalConfig = this.contextProxy?.getGlobalState("codebaseIndexConfig") ?? {
4750
codebaseIndexEnabled: true,
4851
codebaseIndexQdrantUrl: "http://localhost:6333",
4952
codebaseIndexEmbedderProvider: "openai",
@@ -53,6 +56,14 @@ export class CodeIndexConfigManager {
5356
codebaseIndexSearchMaxResults: undefined,
5457
}
5558

59+
// Apply workspace override if it exists
60+
const codebaseIndexConfig = {
61+
...globalConfig,
62+
// Workspace setting overrides global setting if defined
63+
codebaseIndexEnabled:
64+
workspaceIndexEnabled !== undefined ? workspaceIndexEnabled : globalConfig.codebaseIndexEnabled,
65+
}
66+
5667
const {
5768
codebaseIndexEnabled,
5869
codebaseIndexQdrantUrl,
@@ -480,4 +491,31 @@ export class CodeIndexConfigManager {
480491
public get currentSearchMaxResults(): number {
481492
return this.searchMaxResults ?? DEFAULT_MAX_SEARCH_RESULTS
482493
}
494+
495+
/**
496+
* Sets the workspace-specific indexing enabled state
497+
* @param enabled Whether indexing should be enabled for this workspace
498+
*/
499+
public async setWorkspaceIndexingEnabled(enabled: boolean): Promise<void> {
500+
await this.contextProxy?.updateWorkspaceState("codebaseIndexEnabled", enabled)
501+
// Reload configuration to apply the change
502+
await this.loadConfiguration()
503+
}
504+
505+
/**
506+
* Gets the workspace-specific indexing enabled state
507+
* @returns The workspace-specific setting, or undefined if not set
508+
*/
509+
public getWorkspaceIndexingEnabled(): boolean | undefined {
510+
return this.contextProxy?.getWorkspaceState<boolean>("codebaseIndexEnabled")
511+
}
512+
513+
/**
514+
* Clears the workspace-specific indexing setting, reverting to global default
515+
*/
516+
public async clearWorkspaceIndexingSetting(): Promise<void> {
517+
await this.contextProxy?.updateWorkspaceState("codebaseIndexEnabled", undefined)
518+
// Reload configuration to apply the change
519+
await this.loadConfiguration()
520+
}
483521
}

src/services/code-index/manager.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ export class CodeIndexManager {
3232
// Flag to prevent race conditions during error recovery
3333
private _isRecoveringFromError = false
3434

35+
// Public getter for configManager to allow workspace-specific settings
36+
public get configManager(): CodeIndexConfigManager | undefined {
37+
return this._configManager
38+
}
39+
3540
public static getInstance(context: vscode.ExtensionContext, workspacePath?: string): CodeIndexManager | undefined {
3641
// If workspacePath is not provided, try to get it from the active editor or first workspace folder
3742
if (!workspacePath) {
@@ -119,6 +124,10 @@ export class CodeIndexManager {
119124
public async initialize(contextProxy: ContextProxy): Promise<{ requiresRestart: boolean }> {
120125
// 1. ConfigManager Initialization and Configuration Loading
121126
if (!this._configManager) {
127+
// Ensure the ContextProxy has the workspace path set for workspace-specific settings
128+
if (this.workspacePath && contextProxy.workspacePath !== this.workspacePath) {
129+
contextProxy.setWorkspacePath(this.workspacePath)
130+
}
122131
this._configManager = new CodeIndexConfigManager(contextProxy)
123132
}
124133
// Load configuration once to get current state and restart requirements

src/shared/ExtensionMessage.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ export interface ExtensionMessage {
118118
| "shareTaskSuccess"
119119
| "codeIndexSettingsSaved"
120120
| "codeIndexSecretStatus"
121+
| "workspaceIndexingSetting"
121122
| "showDeleteMessageDialog"
122123
| "showEditMessageDialog"
123124
| "commands"
@@ -196,6 +197,7 @@ export interface ExtensionMessage {
196197
messageTs?: number
197198
context?: string
198199
commands?: Command[]
200+
enabled?: boolean // For workspace indexing setting
199201
}
200202

201203
export type ExtensionState = Pick<

src/shared/WebviewMessage.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,9 @@ export interface WebviewMessage {
206206
| "checkRulesDirectoryResult"
207207
| "saveCodeIndexSettingsAtomic"
208208
| "requestCodeIndexSecretStatus"
209+
| "clearWorkspaceIndexingSetting"
210+
| "getWorkspaceIndexingSetting"
211+
| "workspaceIndexingSetting"
209212
| "requestCommands"
210213
| "openCommandFile"
211214
| "deleteCommand"
@@ -280,6 +283,9 @@ export interface WebviewMessage {
280283
codebaseIndexGeminiApiKey?: string
281284
codebaseIndexMistralApiKey?: string
282285
codebaseIndexVercelAiGatewayApiKey?: string
286+
287+
// Workspace-specific flag
288+
workspaceSpecific?: boolean
283289
}
284290
}
285291

0 commit comments

Comments
 (0)