diff --git a/src/core/config/ContextProxy.ts b/src/core/config/ContextProxy.ts index 5535cd2ff4..f6f7016bb1 100644 --- a/src/core/config/ContextProxy.ts +++ b/src/core/config/ContextProxy.ts @@ -39,18 +39,25 @@ export class ContextProxy { private stateCache: GlobalState private secretCache: SecretState private _isInitialized = false + private _workspacePath: string | undefined - constructor(context: vscode.ExtensionContext) { + constructor(context: vscode.ExtensionContext, workspacePath?: string) { this.originalContext = context this.stateCache = {} this.secretCache = {} this._isInitialized = false + this._workspacePath = workspacePath } public get isInitialized() { return this._isInitialized } + // Public getter for workspacePath to allow checking current workspace + public get workspacePath(): string | undefined { + return this._workspacePath + } + public async initialize() { for (const key of GLOBAL_STATE_KEYS) { try { @@ -290,6 +297,50 @@ export class ContextProxy { await this.initialize() } + /** + * Get workspace-specific state value + * Falls back to global state if workspace value doesn't exist + */ + public getWorkspaceState(key: string, defaultValue?: T): T | undefined { + if (!this._workspacePath) { + // If no workspace, fall back to global state + return this.originalContext.globalState.get(key) ?? defaultValue + } + + // Create a workspace-specific key + const workspaceKey = `workspace:${this._workspacePath}:${key}` + const workspaceValue = this.originalContext.globalState.get(workspaceKey) + + if (workspaceValue !== undefined) { + return workspaceValue + } + + // Fall back to global state + return this.originalContext.globalState.get(key) ?? defaultValue + } + + /** + * Update workspace-specific state value + */ + public async updateWorkspaceState(key: string, value: T): Promise { + if (!this._workspacePath) { + // If no workspace, update global state + await this.originalContext.globalState.update(key, value) + return + } + + // Create a workspace-specific key + const workspaceKey = `workspace:${this._workspacePath}:${key}` + await this.originalContext.globalState.update(workspaceKey, value) + } + + /** + * Set the workspace path for workspace-specific settings + */ + public setWorkspacePath(workspacePath: string | undefined): void { + this._workspacePath = workspacePath + } + private static _instance: ContextProxy | null = null static get instance() { @@ -300,12 +351,16 @@ export class ContextProxy { return this._instance } - static async getInstance(context: vscode.ExtensionContext) { + static async getInstance(context: vscode.ExtensionContext, workspacePath?: string) { if (this._instance) { + // Update workspace path if provided + if (workspacePath !== undefined) { + this._instance.setWorkspacePath(workspacePath) + } return this._instance } - this._instance = new ContextProxy(context) + this._instance = new ContextProxy(context, workspacePath) await this._instance.initialize() return this._instance diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 6bf1320ccf..cb4f7dddff 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -2054,10 +2054,15 @@ export const webviewMessageHandler = async ( const embedderProviderChanged = currentConfig.codebaseIndexEmbedderProvider !== settings.codebaseIndexEmbedderProvider - // Save global state settings atomically + // Check if this is a workspace-specific setting update + const isWorkspaceSpecific = settings.workspaceSpecific === true + + // Save global state settings atomically (always needed for non-enabled settings) const globalStateConfig = { ...currentConfig, - codebaseIndexEnabled: settings.codebaseIndexEnabled, + codebaseIndexEnabled: isWorkspaceSpecific + ? currentConfig.codebaseIndexEnabled + : settings.codebaseIndexEnabled, codebaseIndexQdrantUrl: settings.codebaseIndexQdrantUrl, codebaseIndexEmbedderProvider: settings.codebaseIndexEmbedderProvider, codebaseIndexEmbedderBaseUrl: settings.codebaseIndexEmbedderBaseUrl, @@ -2071,6 +2076,14 @@ export const webviewMessageHandler = async ( // Save global state first await updateGlobalState("codebaseIndexConfig", globalStateConfig) + if (isWorkspaceSpecific) { + // Save workspace-specific indexing enabled state + const manager = provider.getCurrentWorkspaceCodeIndexManager() + if (manager && manager.configManager) { + await manager.configManager.setWorkspaceIndexingEnabled(settings.codebaseIndexEnabled) + } + } + // Save secrets directly using context proxy if (settings.codeIndexOpenAiKey !== undefined) { await provider.contextProxy.storeSecret("codeIndexOpenAiKey", settings.codeIndexOpenAiKey) @@ -2319,6 +2332,47 @@ export const webviewMessageHandler = async ( } break } + case "clearWorkspaceIndexingSetting": { + try { + const manager = provider.getCurrentWorkspaceCodeIndexManager() + if (manager && manager.configManager) { + await manager.configManager.clearWorkspaceIndexingSetting() + // Send updated status + const status = manager.getCurrentStatus() + provider.postMessageToWebview({ + type: "indexingStatusUpdate", + values: status, + }) + } + } catch (error) { + provider.log( + `Error clearing workspace indexing setting: ${error instanceof Error ? error.message : String(error)}`, + ) + } + break + } + case "getWorkspaceIndexingSetting": { + try { + const manager = provider.getCurrentWorkspaceCodeIndexManager() + if (manager && manager.configManager) { + const workspaceSetting = manager.configManager.getWorkspaceIndexingEnabled() + provider.postMessageToWebview({ + type: "workspaceIndexingSetting", + enabled: workspaceSetting, + }) + } else { + provider.postMessageToWebview({ + type: "workspaceIndexingSetting", + enabled: undefined, + }) + } + } catch (error) { + provider.log( + `Error getting workspace indexing setting: ${error instanceof Error ? error.message : String(error)}`, + ) + } + break + } case "focusPanelRequest": { // Execute the focusPanel command to focus the WebView await vscode.commands.executeCommand(getCommand("focusPanel")) diff --git a/src/extension.ts b/src/extension.ts index 6060bb341f..2075c07d48 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -17,6 +17,7 @@ import { CloudService, ExtensionBridgeService } from "@roo-code/cloud" import { TelemetryService, PostHogTelemetryClient } from "@roo-code/telemetry" import "./utils/path" // Necessary to have access to String.prototype.toPosix. +import { getWorkspacePath } from "./utils/path" import { createOutputChannelLogger, createDualLogger } from "./utils/outputChannelLogger" import { Package } from "./shared/package" @@ -97,8 +98,12 @@ export async function activate(context: vscode.ExtensionContext) { if (!context.globalState.get("allowedCommands")) { context.globalState.update("allowedCommands", defaultCommands) } - const contextProxy = await ContextProxy.getInstance(context) + // Set the workspace path for the ContextProxy to enable workspace-specific settings + const workspacePath = getWorkspacePath() + if (workspacePath) { + contextProxy.setWorkspacePath(workspacePath) + } // Initialize code index managers for all workspace folders. const codeIndexManagers: CodeIndexManager[] = [] diff --git a/src/services/code-index/__tests__/config-manager.spec.ts b/src/services/code-index/__tests__/config-manager.spec.ts index 9fc096ba74..485a7ff4a7 100644 --- a/src/services/code-index/__tests__/config-manager.spec.ts +++ b/src/services/code-index/__tests__/config-manager.spec.ts @@ -31,6 +31,10 @@ describe("CodeIndexConfigManager", () => { getSecret: vi.fn().mockReturnValue(undefined), refreshSecrets: vi.fn().mockResolvedValue(undefined), updateGlobalState: vi.fn(), + getWorkspaceState: vi.fn().mockReturnValue(undefined), + updateWorkspaceState: vi.fn(), + setWorkspacePath: vi.fn(), + workspacePath: undefined, } configManager = new CodeIndexConfigManager(mockContextProxy) diff --git a/src/services/code-index/__tests__/manager.spec.ts b/src/services/code-index/__tests__/manager.spec.ts index 929f6f93c8..65ffdb3a83 100644 --- a/src/services/code-index/__tests__/manager.spec.ts +++ b/src/services/code-index/__tests__/manager.spec.ts @@ -538,6 +538,10 @@ describe("CodeIndexManager - handleSettingsChange regression", () => { codebaseIndexSearchMaxResults: 10, codebaseIndexSearchMinScore: 0.4, }), + getWorkspaceState: vi.fn().mockReturnValue(undefined), + updateWorkspaceState: vi.fn(), + setWorkspacePath: vi.fn(), + workspacePath: testWorkspacePath, } // Re-initialize diff --git a/src/services/code-index/config-manager.ts b/src/services/code-index/config-manager.ts index 2c0e8bb5c9..78bd3c9534 100644 --- a/src/services/code-index/config-manager.ts +++ b/src/services/code-index/config-manager.ts @@ -42,8 +42,11 @@ export class CodeIndexConfigManager { * This eliminates code duplication between initializeWithCurrentConfig() and loadConfiguration(). */ private _loadAndSetConfiguration(): void { - // Load configuration from storage - const codebaseIndexConfig = this.contextProxy?.getGlobalState("codebaseIndexConfig") ?? { + // First check for workspace-specific override for indexing enabled state + const workspaceIndexEnabled = this.contextProxy?.getWorkspaceState("codebaseIndexEnabled") + + // Load global configuration from storage + const globalConfig = this.contextProxy?.getGlobalState("codebaseIndexConfig") ?? { codebaseIndexEnabled: true, codebaseIndexQdrantUrl: "http://localhost:6333", codebaseIndexEmbedderProvider: "openai", @@ -53,6 +56,14 @@ export class CodeIndexConfigManager { codebaseIndexSearchMaxResults: undefined, } + // Apply workspace override if it exists + const codebaseIndexConfig = { + ...globalConfig, + // Workspace setting overrides global setting if defined + codebaseIndexEnabled: + workspaceIndexEnabled !== undefined ? workspaceIndexEnabled : globalConfig.codebaseIndexEnabled, + } + const { codebaseIndexEnabled, codebaseIndexQdrantUrl, @@ -480,4 +491,31 @@ export class CodeIndexConfigManager { public get currentSearchMaxResults(): number { return this.searchMaxResults ?? DEFAULT_MAX_SEARCH_RESULTS } + + /** + * Sets the workspace-specific indexing enabled state + * @param enabled Whether indexing should be enabled for this workspace + */ + public async setWorkspaceIndexingEnabled(enabled: boolean): Promise { + await this.contextProxy?.updateWorkspaceState("codebaseIndexEnabled", enabled) + // Reload configuration to apply the change + await this.loadConfiguration() + } + + /** + * Gets the workspace-specific indexing enabled state + * @returns The workspace-specific setting, or undefined if not set + */ + public getWorkspaceIndexingEnabled(): boolean | undefined { + return this.contextProxy?.getWorkspaceState("codebaseIndexEnabled") + } + + /** + * Clears the workspace-specific indexing setting, reverting to global default + */ + public async clearWorkspaceIndexingSetting(): Promise { + await this.contextProxy?.updateWorkspaceState("codebaseIndexEnabled", undefined) + // Reload configuration to apply the change + await this.loadConfiguration() + } } diff --git a/src/services/code-index/manager.ts b/src/services/code-index/manager.ts index d82760533d..7f6ede9342 100644 --- a/src/services/code-index/manager.ts +++ b/src/services/code-index/manager.ts @@ -32,6 +32,11 @@ export class CodeIndexManager { // Flag to prevent race conditions during error recovery private _isRecoveringFromError = false + // Public getter for configManager to allow workspace-specific settings + public get configManager(): CodeIndexConfigManager | undefined { + return this._configManager + } + public static getInstance(context: vscode.ExtensionContext, workspacePath?: string): CodeIndexManager | undefined { // If workspacePath is not provided, try to get it from the active editor or first workspace folder if (!workspacePath) { @@ -119,6 +124,10 @@ export class CodeIndexManager { public async initialize(contextProxy: ContextProxy): Promise<{ requiresRestart: boolean }> { // 1. ConfigManager Initialization and Configuration Loading if (!this._configManager) { + // Ensure the ContextProxy has the workspace path set for workspace-specific settings + if (this.workspacePath && contextProxy.workspacePath !== this.workspacePath) { + contextProxy.setWorkspacePath(this.workspacePath) + } this._configManager = new CodeIndexConfigManager(contextProxy) } // Load configuration once to get current state and restart requirements diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 65fe181859..f14e966235 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -118,6 +118,7 @@ export interface ExtensionMessage { | "shareTaskSuccess" | "codeIndexSettingsSaved" | "codeIndexSecretStatus" + | "workspaceIndexingSetting" | "showDeleteMessageDialog" | "showEditMessageDialog" | "commands" @@ -196,6 +197,7 @@ export interface ExtensionMessage { messageTs?: number context?: string commands?: Command[] + enabled?: boolean // For workspace indexing setting } export type ExtensionState = Pick< diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index e2df805340..dfe5cad81d 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -206,6 +206,9 @@ export interface WebviewMessage { | "checkRulesDirectoryResult" | "saveCodeIndexSettingsAtomic" | "requestCodeIndexSecretStatus" + | "clearWorkspaceIndexingSetting" + | "getWorkspaceIndexingSetting" + | "workspaceIndexingSetting" | "requestCommands" | "openCommandFile" | "deleteCommand" @@ -280,6 +283,9 @@ export interface WebviewMessage { codebaseIndexGeminiApiKey?: string codebaseIndexMistralApiKey?: string codebaseIndexVercelAiGatewayApiKey?: string + + // Workspace-specific flag + workspaceSpecific?: boolean } } diff --git a/webview-ui/src/components/chat/CodeIndexPopover.tsx b/webview-ui/src/components/chat/CodeIndexPopover.tsx index 45bf4224a1..69594ec8eb 100644 --- a/webview-ui/src/components/chat/CodeIndexPopover.tsx +++ b/webview-ui/src/components/chat/CodeIndexPopover.tsx @@ -57,6 +57,7 @@ interface CodeIndexPopoverProps { interface LocalCodeIndexSettings { // Global state settings codebaseIndexEnabled: boolean + workspaceIndexingOverride?: boolean | undefined // undefined means use global, true/false means override codebaseIndexQdrantUrl: string codebaseIndexEmbedderProvider: EmbedderProvider codebaseIndexEmbedderBaseUrl?: string @@ -164,6 +165,7 @@ export const CodeIndexPopover: React.FC = ({ const [open, setOpen] = useState(false) const [isAdvancedSettingsOpen, setIsAdvancedSettingsOpen] = useState(false) const [isSetupSettingsOpen, setIsSetupSettingsOpen] = useState(false) + const [workspaceOverrideEnabled, setWorkspaceOverrideEnabled] = useState(false) const [indexingStatus, setIndexingStatus] = useState(externalIndexingStatus) @@ -180,6 +182,7 @@ export const CodeIndexPopover: React.FC = ({ // Default settings template const getDefaultSettings = (): LocalCodeIndexSettings => ({ codebaseIndexEnabled: true, + workspaceIndexingOverride: undefined, codebaseIndexQdrantUrl: "", codebaseIndexEmbedderProvider: "openai", codebaseIndexEmbedderBaseUrl: "", @@ -212,6 +215,7 @@ export const CodeIndexPopover: React.FC = ({ if (codebaseIndexConfig) { const settings = { codebaseIndexEnabled: codebaseIndexConfig.codebaseIndexEnabled ?? true, + workspaceIndexingOverride: undefined, codebaseIndexQdrantUrl: codebaseIndexConfig.codebaseIndexQdrantUrl || "", codebaseIndexEmbedderProvider: codebaseIndexConfig.codebaseIndexEmbedderProvider || "openai", codebaseIndexEmbedderBaseUrl: codebaseIndexConfig.codebaseIndexEmbedderBaseUrl || "", @@ -235,6 +239,8 @@ export const CodeIndexPopover: React.FC = ({ // Request secret status to check if secrets exist vscode.postMessage({ type: "requestCodeIndexSecretStatus" }) + // Request workspace-specific setting + vscode.postMessage({ type: "getWorkspaceIndexingSetting" }) } }, [codebaseIndexConfig]) @@ -243,6 +249,7 @@ export const CodeIndexPopover: React.FC = ({ if (open) { vscode.postMessage({ type: "requestIndexingStatus" }) vscode.postMessage({ type: "requestCodeIndexSecretStatus" }) + vscode.postMessage({ type: "getWorkspaceIndexingSetting" }) } const handleMessage = (event: MessageEvent) => { if (event.data.type === "workspaceUpdated") { @@ -250,6 +257,7 @@ export const CodeIndexPopover: React.FC = ({ if (open) { vscode.postMessage({ type: "requestIndexingStatus" }) vscode.postMessage({ type: "requestCodeIndexSecretStatus" }) + vscode.postMessage({ type: "getWorkspaceIndexingSetting" }) } } } @@ -297,6 +305,14 @@ export const CodeIndexPopover: React.FC = ({ setSaveStatus("idle") setSaveError(null) } + } else if (event.data.type === "workspaceIndexingSetting") { + // Update workspace override state + const hasOverride = event.data.enabled !== undefined + setWorkspaceOverrideEnabled(hasOverride) + if (hasOverride) { + setCurrentSettings((prev) => ({ ...prev, workspaceIndexingOverride: event.data.enabled })) + setInitialSettings((prev) => ({ ...prev, workspaceIndexingOverride: event.data.enabled })) + } } } @@ -511,6 +527,12 @@ export const CodeIndexPopover: React.FC = ({ // Always include codebaseIndexEnabled to ensure it's persisted settingsToSave.codebaseIndexEnabled = currentSettings.codebaseIndexEnabled + // Handle workspace-specific setting + if (workspaceOverrideEnabled && currentSettings.workspaceIndexingOverride !== undefined) { + settingsToSave.workspaceSpecific = true + settingsToSave.codebaseIndexEnabled = currentSettings.workspaceIndexingOverride + } + // Save settings to backend vscode.postMessage({ type: "saveCodeIndexSettingsAtomic", @@ -586,6 +608,55 @@ export const CodeIndexPopover: React.FC = ({ + + {/* Workspace-specific override */} + {cwd && ( +
+
+ { + const enabled = e.target.checked + setWorkspaceOverrideEnabled(enabled) + if (enabled) { + // When enabling override, set to current global value + updateSetting( + "workspaceIndexingOverride", + currentSettings.codebaseIndexEnabled, + ) + } else { + // When disabling override, clear the workspace setting + updateSetting("workspaceIndexingOverride", undefined) + vscode.postMessage({ type: "clearWorkspaceIndexingSetting" }) + } + }}> + + {t("settings:codeIndex.workspaceOverrideLabel")} + + + + + +
+ + {workspaceOverrideEnabled && ( +
+ + updateSetting("workspaceIndexingOverride", e.target.checked) + }> + + {t("settings:codeIndex.workspaceEnableLabel")} + + +
+ )} +
+ )} {/* Status Section */} diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index 33fba24b8e..c6333ee296 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -42,6 +42,9 @@ "statusTitle": "Status", "enableLabel": "Enable Codebase Indexing", "enableDescription": "Enable code indexing for improved search and context understanding", + "workspaceOverrideLabel": "Override for this workspace", + "workspaceOverrideDescription": "Use workspace-specific settings instead of global settings", + "workspaceEnableLabel": "Enable indexing for this workspace", "settingsTitle": "Indexing Settings", "disabledMessage": "Codebase indexing is currently disabled. Enable it in the global settings to configure indexing options.", "providerLabel": "Embeddings Provider",