diff --git a/src/activate/registerCommands.ts b/src/activate/registerCommands.ts index bd925b0e900..2f8212ffa00 100644 --- a/src/activate/registerCommands.ts +++ b/src/activate/registerCommands.ts @@ -237,7 +237,7 @@ export const openClineInNewTab = async ({ context, outputChannel }: Omit editor.viewColumn || 0)) // Check if there are any visible text editors, otherwise open a new group diff --git a/src/core/prompts/__tests__/custom-system-prompt.spec.ts b/src/core/prompts/__tests__/custom-system-prompt.spec.ts index acf34ac4591..b2ae067a3a6 100644 --- a/src/core/prompts/__tests__/custom-system-prompt.spec.ts +++ b/src/core/prompts/__tests__/custom-system-prompt.spec.ts @@ -1,4 +1,22 @@ // Mocks must come first, before imports +vi.mock("vscode", () => ({ + env: { + language: "en", + }, + workspace: { + workspaceFolders: [{ uri: { fsPath: "/test/path" } }], + getWorkspaceFolder: vi.fn().mockReturnValue({ uri: { fsPath: "/test/path" } }), + }, + window: { + activeTextEditor: undefined, + }, + EventEmitter: vi.fn().mockImplementation(() => ({ + event: vi.fn(), + fire: vi.fn(), + dispose: vi.fn(), + })), +})) + vi.mock("fs/promises", () => { const mockReadFile = vi.fn() const mockMkdir = vi.fn().mockResolvedValue(undefined) diff --git a/src/core/prompts/system.ts b/src/core/prompts/system.ts index cbe91903ee3..a91424cd1c7 100644 --- a/src/core/prompts/system.ts +++ b/src/core/prompts/system.ts @@ -85,7 +85,7 @@ async function generatePrompt( : Promise.resolve(""), ]) - const codeIndexManager = CodeIndexManager.getInstance(context) + const codeIndexManager = CodeIndexManager.getInstance(context, cwd) const basePrompt = `${roleDefinition} diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 384de58be7b..c4f146bd349 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -104,6 +104,7 @@ export class ClineProvider private view?: vscode.WebviewView | vscode.WebviewPanel private clineStack: Task[] = [] private codeIndexStatusSubscription?: vscode.Disposable + private currentWorkspaceManager?: CodeIndexManager private _workspaceTracker?: WorkspaceTracker // workSpaceTracker read-only for access outside this class public get workspaceTracker(): WorkspaceTracker | undefined { return this._workspaceTracker @@ -125,7 +126,6 @@ export class ClineProvider private readonly outputChannel: vscode.OutputChannel, private readonly renderContext: "sidebar" | "editor" = "sidebar", public readonly contextProxy: ContextProxy, - public readonly codeIndexManager?: CodeIndexManager, mdmService?: MdmService, ) { super() @@ -133,7 +133,6 @@ export class ClineProvider this.log("ClineProvider instantiated") ClineProvider.activeInstances.add(this) - this.codeIndexManager = codeIndexManager this.mdmService = mdmService this.updateGlobalState("codebaseIndexModels", EMBEDDING_MODEL_PROFILES) @@ -602,16 +601,15 @@ export class ClineProvider // and executes code based on the message that is received this.setWebviewMessageListener(webviewView.webview) - // Subscribe to code index status updates if the manager exists - if (this.codeIndexManager) { - this.codeIndexStatusSubscription = this.codeIndexManager.onProgressUpdate((update: IndexProgressUpdate) => { - this.postMessageToWebview({ - type: "indexingStatusUpdate", - values: update, - }) - }) - this.webviewDisposables.push(this.codeIndexStatusSubscription) - } + // Initialize code index status subscription for the current workspace + this.updateCodeIndexStatusSubscription() + + // Listen for active editor changes to update code index status for the current workspace + const activeEditorSubscription = vscode.window.onDidChangeActiveTextEditor(() => { + // Update subscription when workspace might have changed + this.updateCodeIndexStatusSubscription() + }) + this.webviewDisposables.push(activeEditorSubscription) // Logs show up in bottom panel > Debug Console //console.log("registering listener") @@ -647,8 +645,8 @@ export class ClineProvider } else { this.log("Clearing webview resources for sidebar view") this.clearWebviewResources() - this.codeIndexStatusSubscription?.dispose() - this.codeIndexStatusSubscription = undefined + // Reset current workspace manager reference when view is disposed + this.currentWorkspaceManager = undefined } }, null, @@ -2223,6 +2221,61 @@ export class ClineProvider ...gitInfo, } } + + /** + * Gets the CodeIndexManager for the current active workspace + * @returns CodeIndexManager instance for the current workspace or the default one + */ + public getCurrentWorkspaceCodeIndexManager(): CodeIndexManager | undefined { + return CodeIndexManager.getInstance(this.context) + } + + /** + * Updates the code index status subscription to listen to the current workspace manager + */ + private updateCodeIndexStatusSubscription(): void { + // Get the current workspace manager + const currentManager = this.getCurrentWorkspaceCodeIndexManager() + + // If the manager hasn't changed, no need to update subscription + if (currentManager === this.currentWorkspaceManager) { + return + } + + // Dispose the old subscription if it exists + if (this.codeIndexStatusSubscription) { + this.codeIndexStatusSubscription.dispose() + this.codeIndexStatusSubscription = undefined + } + + // Update the current workspace manager reference + this.currentWorkspaceManager = currentManager + + // Subscribe to the new manager's progress updates if it exists + if (currentManager) { + this.codeIndexStatusSubscription = currentManager.onProgressUpdate((update: IndexProgressUpdate) => { + // Only send updates if this manager is still the current one + if (currentManager === this.getCurrentWorkspaceCodeIndexManager()) { + // Get the full status from the manager to ensure we have all fields correctly formatted + const fullStatus = currentManager.getCurrentStatus() + this.postMessageToWebview({ + type: "indexingStatusUpdate", + values: fullStatus, + }) + } + }) + + if (this.view) { + this.webviewDisposables.push(this.codeIndexStatusSubscription) + } + + // Send initial status for the current workspace + this.postMessageToWebview({ + type: "indexingStatusUpdate", + values: currentManager.getCurrentStatus(), + }) + } + } } class OrganizationAllowListViolationError extends Error { diff --git a/src/core/webview/__tests__/ClineProvider.spec.ts b/src/core/webview/__tests__/ClineProvider.spec.ts index 66c1db55a86..eeae44451d3 100644 --- a/src/core/webview/__tests__/ClineProvider.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.spec.ts @@ -147,6 +147,7 @@ vi.mock("vscode", () => ({ showInformationMessage: vi.fn(), showWarningMessage: vi.fn(), showErrorMessage: vi.fn(), + onDidChangeActiveTextEditor: vi.fn(() => ({ dispose: vi.fn() })), }, workspace: { getConfiguration: vi.fn().mockReturnValue({ diff --git a/src/core/webview/__tests__/ClineProvider.sticky-mode.spec.ts b/src/core/webview/__tests__/ClineProvider.sticky-mode.spec.ts index e7eff427cf7..e55e0910abd 100644 --- a/src/core/webview/__tests__/ClineProvider.sticky-mode.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.sticky-mode.spec.ts @@ -27,6 +27,7 @@ vi.mock("vscode", () => ({ showInformationMessage: vi.fn(), showWarningMessage: vi.fn(), showErrorMessage: vi.fn(), + onDidChangeActiveTextEditor: vi.fn(() => ({ dispose: vi.fn() })), }, workspace: { getConfiguration: vi.fn().mockReturnValue({ diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 743e3b0c132..97ef0ddc3c5 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -29,6 +29,7 @@ import { checkExistKey } from "../../shared/checkExistApiConfig" import { experimentDefault } from "../../shared/experiments" import { Terminal } from "../../integrations/terminal/Terminal" import { openFile } from "../../integrations/misc/open-file" +import { CodeIndexManager } from "../../services/code-index/manager" import { openImage, saveImage } from "../../integrations/misc/image-handler" import { selectImages } from "../../integrations/misc/process-images" import { getTheme } from "../../integrations/theme/getTheme" @@ -2062,13 +2063,14 @@ export const webviewMessageHandler = async ( // Update webview state await provider.postStateToWebview() - // Then handle validation and initialization - if (provider.codeIndexManager) { + // Then handle validation and initialization for the current workspace + const currentCodeIndexManager = provider.getCurrentWorkspaceCodeIndexManager() + if (currentCodeIndexManager) { // If embedder provider changed, perform proactive validation if (embedderProviderChanged) { try { // Force handleSettingsChange which will trigger validation - await provider.codeIndexManager.handleSettingsChange() + await currentCodeIndexManager.handleSettingsChange() } catch (error) { // Validation failed - the error state is already set by handleSettingsChange provider.log( @@ -2077,7 +2079,7 @@ export const webviewMessageHandler = async ( // Send validation error to webview await provider.postMessageToWebview({ type: "indexingStatusUpdate", - values: provider.codeIndexManager.getCurrentStatus(), + values: currentCodeIndexManager.getCurrentStatus(), }) // Exit early - don't try to start indexing with invalid configuration break @@ -2085,7 +2087,7 @@ export const webviewMessageHandler = async ( } else { // No provider change, just handle settings normally try { - await provider.codeIndexManager.handleSettingsChange() + await currentCodeIndexManager.handleSettingsChange() } catch (error) { // Log but don't fail - settings are saved provider.log( @@ -2098,10 +2100,10 @@ export const webviewMessageHandler = async ( await new Promise((resolve) => setTimeout(resolve, 200)) // Auto-start indexing if now enabled and configured - if (provider.codeIndexManager.isFeatureEnabled && provider.codeIndexManager.isFeatureConfigured) { - if (!provider.codeIndexManager.isInitialized) { + if (currentCodeIndexManager.isFeatureEnabled && currentCodeIndexManager.isFeatureConfigured) { + if (!currentCodeIndexManager.isInitialized) { try { - await provider.codeIndexManager.initialize(provider.contextProxy) + await currentCodeIndexManager.initialize(provider.contextProxy) provider.log(`Code index manager initialized after settings save`) } catch (error) { provider.log( @@ -2110,7 +2112,7 @@ export const webviewMessageHandler = async ( // Send error status to webview await provider.postMessageToWebview({ type: "indexingStatusUpdate", - values: provider.codeIndexManager.getCurrentStatus(), + values: currentCodeIndexManager.getCurrentStatus(), }) } } @@ -2141,7 +2143,7 @@ export const webviewMessageHandler = async ( } case "requestIndexingStatus": { - const manager = provider.codeIndexManager + const manager = provider.getCurrentWorkspaceCodeIndexManager() if (!manager) { // No workspace open - send error status provider.postMessageToWebview({ @@ -2152,11 +2154,23 @@ export const webviewMessageHandler = async ( processedItems: 0, totalItems: 0, currentItemUnit: "items", + workerspacePath: undefined, }, }) return } - const status = manager.getCurrentStatus() + + const status = manager + ? manager.getCurrentStatus() + : { + systemStatus: "Standby", + message: "No workspace folder open", + processedItems: 0, + totalItems: 0, + currentItemUnit: "items", + workspacePath: undefined, + } + provider.postMessageToWebview({ type: "indexingStatusUpdate", values: status, @@ -2187,7 +2201,7 @@ export const webviewMessageHandler = async ( } case "startIndexing": { try { - const manager = provider.codeIndexManager + const manager = provider.getCurrentWorkspaceCodeIndexManager() if (!manager) { // No workspace open - send error status provider.postMessageToWebview({ @@ -2217,7 +2231,7 @@ export const webviewMessageHandler = async ( } case "clearIndexData": { try { - const manager = provider.codeIndexManager + const manager = provider.getCurrentWorkspaceCodeIndexManager() if (!manager) { provider.log("Cannot clear index data: No workspace folder open") provider.postMessageToWebview({ diff --git a/src/extension.ts b/src/extension.ts index ea6ab4e1b48..15df88d4d94 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -93,14 +93,24 @@ export async function activate(context: vscode.ExtensionContext) { } const contextProxy = await ContextProxy.getInstance(context) - const codeIndexManager = CodeIndexManager.getInstance(context) - try { - await codeIndexManager?.initialize(contextProxy) - } catch (error) { - outputChannel.appendLine( - `[CodeIndexManager] Error during background CodeIndexManager configuration/indexing: ${error.message || error}`, - ) + // Initialize code index managers for all workspace folders + const codeIndexManagers: CodeIndexManager[] = [] + if (vscode.workspace.workspaceFolders) { + for (const folder of vscode.workspace.workspaceFolders) { + const manager = CodeIndexManager.getInstance(context, folder.uri.fsPath) + if (manager) { + codeIndexManagers.push(manager) + try { + await manager.initialize(contextProxy) + } catch (error) { + outputChannel.appendLine( + `[CodeIndexManager] Error during background CodeIndexManager configuration/indexing for ${folder.uri.fsPath}: ${error.message || error}`, + ) + } + context.subscriptions.push(manager) + } + } } // Initialize Roo Code Cloud service. @@ -126,13 +136,9 @@ export async function activate(context: vscode.ExtensionContext) { // Add to subscriptions for proper cleanup on deactivate. context.subscriptions.push(cloudService) - const provider = new ClineProvider(context, outputChannel, "sidebar", contextProxy, codeIndexManager, mdmService) + const provider = new ClineProvider(context, outputChannel, "sidebar", contextProxy, mdmService) TelemetryService.instance.setProvider(provider) - if (codeIndexManager) { - context.subscriptions.push(codeIndexManager) - } - context.subscriptions.push( vscode.window.registerWebviewViewProvider(ClineProvider.sideBarId, provider, { webviewOptions: { retainContextWhenHidden: true }, diff --git a/src/services/code-index/__tests__/manager.spec.ts b/src/services/code-index/__tests__/manager.spec.ts index 8c64c2fdc62..3995825f707 100644 --- a/src/services/code-index/__tests__/manager.spec.ts +++ b/src/services/code-index/__tests__/manager.spec.ts @@ -4,6 +4,9 @@ import type { MockedClass } from "vitest" // Mock vscode module vi.mock("vscode", () => ({ + window: { + activeTextEditor: null, + }, workspace: { workspaceFolders: [ { diff --git a/src/services/code-index/manager.ts b/src/services/code-index/manager.ts index 18e0752c34d..027734d2132 100644 --- a/src/services/code-index/manager.ts +++ b/src/services/code-index/manager.ts @@ -28,17 +28,24 @@ export class CodeIndexManager { private _searchService: CodeIndexSearchService | undefined private _cacheManager: CacheManager | undefined - public static getInstance(context: vscode.ExtensionContext): CodeIndexManager | undefined { - // Use first workspace folder consistently - const workspaceFolders = vscode.workspace.workspaceFolders - if (!workspaceFolders || workspaceFolders.length === 0) { - return undefined - } + 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) { + const activeEditor = vscode.window.activeTextEditor + if (activeEditor) { + const workspaceFolder = vscode.workspace.getWorkspaceFolder(activeEditor.document.uri) + workspacePath = workspaceFolder?.uri.fsPath + } - // Always use the first workspace folder for consistency across all indexing operations. - // This ensures that the same workspace context is used throughout the indexing pipeline, - // preventing path resolution errors in multi-workspace scenarios. - const workspacePath = workspaceFolders[0].uri.fsPath + if (!workspacePath) { + const workspaceFolders = vscode.workspace.workspaceFolders + if (!workspaceFolders || workspaceFolders.length === 0) { + return undefined + } + // Use the first workspace folder as fallback + workspacePath = workspaceFolders[0].uri.fsPath + } + } if (!CodeIndexManager.instances.has(workspacePath)) { CodeIndexManager.instances.set(workspacePath, new CodeIndexManager(workspacePath, context)) @@ -205,7 +212,11 @@ export class CodeIndexManager { // --- Private Helpers --- public getCurrentStatus() { - return this._stateManager.getCurrentStatus() + const status = this._stateManager.getCurrentStatus() + return { + ...status, + workspacePath: this.workspacePath, + } } public async searchIndex(query: string, directoryPrefix?: string): Promise { diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 2313d7d177c..f9ac305e07f 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -41,6 +41,7 @@ export interface IndexingStatus { processedItems: number totalItems: number currentItemUnit?: string + workspacePath?: string } export interface IndexingStatusUpdateMessage { diff --git a/webview-ui/src/components/chat/CodeIndexPopover.tsx b/webview-ui/src/components/chat/CodeIndexPopover.tsx index c85aaf6ea5b..4a90a60f3d7 100644 --- a/webview-ui/src/components/chat/CodeIndexPopover.tsx +++ b/webview-ui/src/components/chat/CodeIndexPopover.tsx @@ -147,7 +147,7 @@ export const CodeIndexPopover: React.FC = ({ }) => { const SECRET_PLACEHOLDER = "••••••••••••••••" const { t } = useAppTranslation() - const { codebaseIndexConfig, codebaseIndexModels } = useExtensionState() + const { codebaseIndexConfig, codebaseIndexModels, cwd } = useExtensionState() const [open, setOpen] = useState(false) const [isAdvancedSettingsOpen, setIsAdvancedSettingsOpen] = useState(false) const [isSetupSettingsOpen, setIsSetupSettingsOpen] = useState(false) @@ -229,6 +229,18 @@ export const CodeIndexPopover: React.FC = ({ vscode.postMessage({ type: "requestIndexingStatus" }) vscode.postMessage({ type: "requestCodeIndexSecretStatus" }) } + const handleMessage = (event: MessageEvent) => { + if (event.data.type === "workspaceUpdated") { + // When workspace changes, request updated indexing status + if (open) { + vscode.postMessage({ type: "requestIndexingStatus" }) + vscode.postMessage({ type: "requestCodeIndexSecretStatus" }) + } + } + } + + window.addEventListener("message", handleMessage) + return () => window.removeEventListener("message", handleMessage) }, [open]) // Use a ref to capture current settings for the save handler @@ -239,13 +251,15 @@ export const CodeIndexPopover: React.FC = ({ useEffect(() => { const handleMessage = (event: MessageEvent) => { if (event.data.type === "indexingStatusUpdate") { - setIndexingStatus({ - systemStatus: event.data.values.systemStatus, - message: event.data.values.message || "", - processedItems: event.data.values.processedItems, - totalItems: event.data.values.totalItems, - currentItemUnit: event.data.values.currentItemUnit || "items", - }) + if (!event.data.values.workspacePath || event.data.values.workspacePath === cwd) { + setIndexingStatus({ + systemStatus: event.data.values.systemStatus, + message: event.data.values.message || "", + processedItems: event.data.values.processedItems, + totalItems: event.data.values.totalItems, + currentItemUnit: event.data.values.currentItemUnit || "items", + }) + } } else if (event.data.type === "codeIndexSettingsSaved") { if (event.data.success) { setSaveStatus("saved") @@ -273,7 +287,7 @@ export const CodeIndexPopover: React.FC = ({ window.addEventListener("message", handleMessage) return () => window.removeEventListener("message", handleMessage) - }, [t]) + }, [t, cwd]) // Listen for secret status useEffect(() => { diff --git a/webview-ui/src/components/chat/IndexingStatusBadge.tsx b/webview-ui/src/components/chat/IndexingStatusBadge.tsx index ff5a0171b5a..2462780b1d4 100644 --- a/webview-ui/src/components/chat/IndexingStatusBadge.tsx +++ b/webview-ui/src/components/chat/IndexingStatusBadge.tsx @@ -4,6 +4,7 @@ import { cn } from "@src/lib/utils" import { vscode } from "@src/utils/vscode" import { useAppTranslation } from "@/i18n/TranslationContext" import { useTooltip } from "@/hooks/useTooltip" +import { useExtensionState } from "@src/context/ExtensionStateContext" import { CodeIndexPopover } from "./CodeIndexPopover" import type { IndexingStatus, IndexingStatusUpdateMessage } from "@roo/ExtensionMessage" @@ -13,6 +14,7 @@ interface IndexingStatusBadgeProps { export const IndexingStatusBadge: React.FC = ({ className }) => { const { t } = useAppTranslation() + const { cwd } = useExtensionState() const { showTooltip, handleMouseEnter, handleMouseLeave, cleanup } = useTooltip({ delay: 300 }) const [isHovered, setIsHovered] = useState(false) @@ -31,7 +33,9 @@ export const IndexingStatusBadge: React.FC = ({ classN const handleMessage = (event: MessageEvent) => { if (event.data.type === "indexingStatusUpdate") { const status = event.data.values - setIndexingStatus(status) + if (!status.workspacePath || status.workspacePath === cwd) { + setIndexingStatus(status) + } } } @@ -41,7 +45,7 @@ export const IndexingStatusBadge: React.FC = ({ classN window.removeEventListener("message", handleMessage) cleanup() } - }, [cleanup]) + }, [cleanup, cwd]) // Calculate progress percentage with memoization const progressPercentage = useMemo(