Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 58 additions & 3 deletions src/core/config/ContextProxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -290,6 +297,50 @@ export class ContextProxy {
await this.initialize()
}

/**
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding more detailed JSDoc comments for these new public methods. It would help future developers understand the behavior, especially around edge cases like what happens when workspace path is undefined.

* Get workspace-specific state value
* Falls back to global state if workspace value doesn't exist
*/
public getWorkspaceState<T>(key: string, defaultValue?: T): T | undefined {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The workspace state operations don't have error handling. What happens if VSCode storage operations fail? Consider wrapping these in try-catch blocks to handle potential failures gracefully.

if (!this._workspacePath) {
// If no workspace, fall back to global state
return this.originalContext.globalState.get<T>(key) ?? defaultValue
}

// Create a workspace-specific key
const workspaceKey = `workspace:${this._workspacePath}:${key}`
const workspaceValue = this.originalContext.globalState.get<T>(workspaceKey)

if (workspaceValue !== undefined) {
return workspaceValue
}

// Fall back to global state
return this.originalContext.globalState.get<T>(key) ?? defaultValue
}

/**
* Update workspace-specific state value
*/
public async updateWorkspaceState<T>(key: string, value: T): Promise<void> {
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() {
Expand All @@ -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
Expand Down
58 changes: 56 additions & 2 deletions src/core/webview/webviewMessageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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)
Expand Down Expand Up @@ -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"))
Expand Down
7 changes: 6 additions & 1 deletion src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this intentional? The workspace path is set after getting the ContextProxy instance. Could this cause issues if multiple workspaces try to initialize simultaneously? Consider setting the workspace path as part of the getInstance call or ensuring thread safety.

contextProxy.setWorkspacePath(workspacePath)
}

// Initialize code index managers for all workspace folders.
const codeIndexManagers: CodeIndexManager[] = []
Expand Down
4 changes: 4 additions & 0 deletions src/services/code-index/__tests__/config-manager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ describe("CodeIndexConfigManager", () => {
getSecret: vi.fn().mockReturnValue(undefined),
refreshSecrets: vi.fn().mockResolvedValue(undefined),
updateGlobalState: vi.fn(),
getWorkspaceState: vi.fn().mockReturnValue(undefined),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good that you updated the mocks! Consider adding specific test cases for the workspace override scenarios - like testing what happens when workspace setting exists vs when it doesn't, and verifying the fallback behavior.

updateWorkspaceState: vi.fn(),
setWorkspacePath: vi.fn(),
workspacePath: undefined,
}

configManager = new CodeIndexConfigManager(mockContextProxy)
Expand Down
4 changes: 4 additions & 0 deletions src/services/code-index/__tests__/manager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
42 changes: 40 additions & 2 deletions src/services/code-index/config-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<boolean>("codebaseIndexEnabled")

// Load global configuration from storage
const globalConfig = this.contextProxy?.getGlobalState("codebaseIndexConfig") ?? {
codebaseIndexEnabled: true,
codebaseIndexQdrantUrl: "http://localhost:6333",
codebaseIndexEmbedderProvider: "openai",
Expand All @@ -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,
Expand Down Expand Up @@ -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<void> {
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<boolean>("codebaseIndexEnabled")
}

/**
* Clears the workspace-specific indexing setting, reverting to global default
*/
public async clearWorkspaceIndexingSetting(): Promise<void> {
await this.contextProxy?.updateWorkspaceState("codebaseIndexEnabled", undefined)
// Reload configuration to apply the change
await this.loadConfiguration()
}
}
9 changes: 9 additions & 0 deletions src/services/code-index/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The workspace path mismatch check only updates the path but doesn't handle potential conflicts. Should this be more defensive? What if two different CodeIndexManager instances are trying to use different workspace paths?

}
this._configManager = new CodeIndexConfigManager(contextProxy)
}
// Load configuration once to get current state and restart requirements
Expand Down
2 changes: 2 additions & 0 deletions src/shared/ExtensionMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ export interface ExtensionMessage {
| "shareTaskSuccess"
| "codeIndexSettingsSaved"
| "codeIndexSecretStatus"
| "workspaceIndexingSetting"
| "showDeleteMessageDialog"
| "showEditMessageDialog"
| "commands"
Expand Down Expand Up @@ -196,6 +197,7 @@ export interface ExtensionMessage {
messageTs?: number
context?: string
commands?: Command[]
enabled?: boolean // For workspace indexing setting
}

export type ExtensionState = Pick<
Expand Down
6 changes: 6 additions & 0 deletions src/shared/WebviewMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,9 @@ export interface WebviewMessage {
| "checkRulesDirectoryResult"
| "saveCodeIndexSettingsAtomic"
| "requestCodeIndexSecretStatus"
| "clearWorkspaceIndexingSetting"
| "getWorkspaceIndexingSetting"
| "workspaceIndexingSetting"
| "requestCommands"
| "openCommandFile"
| "deleteCommand"
Expand Down Expand Up @@ -280,6 +283,9 @@ export interface WebviewMessage {
codebaseIndexGeminiApiKey?: string
codebaseIndexMistralApiKey?: string
codebaseIndexVercelAiGatewayApiKey?: string

// Workspace-specific flag
workspaceSpecific?: boolean
}
}

Expand Down
Loading
Loading