diff --git a/src/activate/registerCommands.ts b/src/activate/registerCommands.ts index 203af497e383..fac615edf11f 100644 --- a/src/activate/registerCommands.ts +++ b/src/activate/registerCommands.ts @@ -95,7 +95,7 @@ const getCommandsMap = ({ context, outputChannel, provider }: RegisterCommandOpt TelemetryService.instance.captureTitleButtonClicked("plus") await visibleProvider.removeClineFromStack() - await visibleProvider.postStateToWebview() + await visibleProvider.refreshWorkspace() await visibleProvider.postMessageToWebview({ type: "action", action: "chatButtonClicked" }) // Send focusInput action immediately after chatButtonClicked // This ensures the focus happens after the view has switched diff --git a/src/core/mentions/index.ts b/src/core/mentions/index.ts index 282e795ce4d7..f038b5b7836b 100644 --- a/src/core/mentions/index.ts +++ b/src/core/mentions/index.ts @@ -7,7 +7,6 @@ import { isBinaryFile } from "isbinaryfile" import { mentionRegexGlobal, commandRegexGlobal, unescapeSpaces } from "../../shared/context-mentions" import { getCommitInfo, getWorkingState } from "../../utils/git" -import { getWorkspacePath } from "../../utils/path" import { openFile } from "../../integrations/misc/open-file" import { extractTextFromFile } from "../../integrations/misc/extract-text" @@ -49,16 +48,11 @@ function getUrlErrorMessage(error: unknown): string { return t("common:errors.url_fetch_failed", { error: errorMessage }) } -export async function openMention(mention?: string): Promise { +export async function openMention(cwd: string, mention?: string): Promise { if (!mention) { return } - const cwd = getWorkspacePath() - if (!cwd) { - return - } - if (mention.startsWith("/")) { // Slice off the leading slash and unescape any spaces in the path const relPath = unescapeSpaces(mention.slice(1)) diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index be932b098f32..e8fcb975f638 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -138,6 +138,7 @@ export interface TaskOptions extends CreateTaskOptions { taskNumber?: number onCreated?: (task: Task) => void initialTodos?: TodoItem[] + workspacePath?: string } export class Task extends EventEmitter implements TaskLike { @@ -313,6 +314,7 @@ export class Task extends EventEmitter implements TaskLike { taskNumber = -1, onCreated, initialTodos, + workspacePath, }: TaskOptions) { super() @@ -333,7 +335,7 @@ export class Task extends EventEmitter implements TaskLike { // Normal use-case is usually retry similar history task with new workspace. this.workspacePath = parentTask ? parentTask.workspacePath - : getWorkspacePath(path.join(os.homedir(), "Desktop")) + : (workspacePath ?? getWorkspacePath(path.join(os.homedir(), "Desktop"))) this.instanceId = crypto.randomUUID().slice(0, 8) this.taskNumber = -1 diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 544922a1878b..51db364e5120 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -112,13 +112,14 @@ export class ClineProvider private view?: vscode.WebviewView | vscode.WebviewPanel private clineStack: Task[] = [] private codeIndexStatusSubscription?: vscode.Disposable - private currentWorkspaceManager?: CodeIndexManager + private codeIndexManager?: CodeIndexManager private _workspaceTracker?: WorkspaceTracker // workSpaceTracker read-only for access outside this class protected mcpHub?: McpHub // Change from private to protected private marketplaceManager: MarketplaceManager private mdmService?: MdmService private taskCreationCallback: (task: Task) => void private taskEventListeners: WeakMap void>> = new WeakMap() + private currentWorkspacePath: string | undefined private recentTasksCache?: string[] @@ -136,6 +137,7 @@ export class ClineProvider mdmService?: MdmService, ) { super() + this.currentWorkspacePath = getWorkspacePath() ClineProvider.activeInstances.add(this) @@ -707,7 +709,7 @@ export class ClineProvider this.log("Clearing webview resources for sidebar view") this.clearWebviewResources() // Reset current workspace manager reference when view is disposed - this.currentWorkspaceManager = undefined + this.codeIndexManager = undefined } }, null, @@ -795,6 +797,7 @@ export class ClineProvider rootTask: historyItem.rootTask, parentTask: historyItem.parentTask, taskNumber: historyItem.number, + workspacePath: historyItem.workspace, onCreated: this.taskCreationCallback, enableBridge: BridgeOrchestrator.isEnabled(cloudUserInfo, remoteControlEnabled), }) @@ -1434,6 +1437,11 @@ export class ClineProvider await this.postStateToWebview() } + async refreshWorkspace() { + this.currentWorkspacePath = getWorkspacePath() + await this.postStateToWebview() + } + async postStateToWebview() { const state = await this.getStateToPostToWebview() this.postMessageToWebview({ type: "state", state }) @@ -2159,7 +2167,7 @@ export class ClineProvider const currentManager = this.getCurrentWorkspaceCodeIndexManager() // If the manager hasn't changed, no need to update subscription - if (currentManager === this.currentWorkspaceManager) { + if (currentManager === this.codeIndexManager) { return } @@ -2170,7 +2178,7 @@ export class ClineProvider } // Update the current workspace manager reference - this.currentWorkspaceManager = currentManager + this.codeIndexManager = currentManager // Subscribe to the new manager's progress updates if it exists if (currentManager) { @@ -2531,7 +2539,7 @@ export class ClineProvider } public get cwd() { - return getWorkspacePath() + return this.currentWorkspacePath || getWorkspacePath() } /** diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index a495489cc1d6..865fa929ad9f 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -67,6 +67,9 @@ export const webviewMessageHandler = async ( const updateGlobalState = async (key: K, value: GlobalState[K]) => await provider.contextProxy.setValue(key, value) + const getCurrentCwd = () => { + return provider.getCurrentTask()?.cwd || provider.cwd + } /** * Shared utility to find message indices based on timestamp */ @@ -742,10 +745,14 @@ export const webviewMessageHandler = async ( saveImage(message.dataUri!) break case "openFile": - openFile(message.text!, message.values as { create?: boolean; content?: string; line?: number }) + let filePath: string = message.text! + if (!path.isAbsolute(filePath)) { + filePath = path.join(getCurrentCwd(), filePath) + } + openFile(filePath, message.values as { create?: boolean; content?: string; line?: number }) break case "openMention": - openMention(message.text) + openMention(getCurrentCwd(), message.text) break case "openExternal": if (message.url) { @@ -840,8 +847,8 @@ export const webviewMessageHandler = async ( return } - const workspaceFolder = vscode.workspace.workspaceFolders[0] - const rooDir = path.join(workspaceFolder.uri.fsPath, ".roo") + const workspaceFolder = getCurrentCwd() + const rooDir = path.join(workspaceFolder, ".roo") const mcpPath = path.join(rooDir, "mcp.json") try { @@ -1490,7 +1497,7 @@ export const webviewMessageHandler = async ( } break case "searchCommits": { - const cwd = provider.cwd + const cwd = getCurrentCwd() if (cwd) { try { const commits = await searchCommits(message.query || "", cwd) @@ -1508,7 +1515,7 @@ export const webviewMessageHandler = async ( break } case "searchFiles": { - const workspacePath = getWorkspacePath() + const workspacePath = getCurrentCwd() if (!workspacePath) { // Handle case where workspace path is not available @@ -2487,7 +2494,7 @@ export const webviewMessageHandler = async ( case "requestCommands": { try { const { getCommands } = await import("../../services/command/commands") - const commands = await getCommands(provider.cwd || "") + const commands = await getCommands(getCurrentCwd()) // Convert to the format expected by the frontend const commandList = commands.map((command) => ({ @@ -2516,7 +2523,7 @@ export const webviewMessageHandler = async ( try { if (message.text) { const { getCommand } = await import("../../services/command/commands") - const command = await getCommand(provider.cwd || "", message.text) + const command = await getCommand(getCurrentCwd(), message.text) if (command && command.filePath) { openFile(command.filePath) @@ -2536,7 +2543,7 @@ export const webviewMessageHandler = async ( try { if (message.text && message.values?.source) { const { getCommand } = await import("../../services/command/commands") - const command = await getCommand(provider.cwd || "", message.text) + const command = await getCommand(getCurrentCwd(), message.text) if (command && command.filePath) { // Delete the command file @@ -2568,8 +2575,12 @@ export const webviewMessageHandler = async ( const globalConfigDir = path.join(os.homedir(), ".roo") commandsDir = path.join(globalConfigDir, "commands") } else { + if (!vscode.workspace.workspaceFolders?.length) { + vscode.window.showErrorMessage(t("common:errors.no_workspace")) + return + } // Project commands - const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath + const workspaceRoot = getCurrentCwd() if (!workspaceRoot) { vscode.window.showErrorMessage(t("common:errors.no_workspace_for_project_command")) break @@ -2649,7 +2660,7 @@ export const webviewMessageHandler = async ( // Refresh commands list const { getCommands } = await import("../../services/command/commands") - const commands = await getCommands(provider.cwd || "") + const commands = await getCommands(getCurrentCwd() || "") const commandList = commands.map((command) => ({ name: command.name, source: command.source, diff --git a/src/integrations/workspace/WorkspaceTracker.ts b/src/integrations/workspace/WorkspaceTracker.ts index 7de8f994c033..546cd97cd17f 100644 --- a/src/integrations/workspace/WorkspaceTracker.ts +++ b/src/integrations/workspace/WorkspaceTracker.ts @@ -17,7 +17,7 @@ class WorkspaceTracker { private resetTimer: NodeJS.Timeout | null = null get cwd() { - return getWorkspacePath() + return this.providerRef?.deref()?.cwd ?? getWorkspacePath() } constructor(provider: ClineProvider) { this.providerRef = new WeakRef(provider) diff --git a/src/services/code-index/manager.ts b/src/services/code-index/manager.ts index d82760533d49..dd79a3f16169 100644 --- a/src/services/code-index/manager.ts +++ b/src/services/code-index/manager.ts @@ -1,5 +1,4 @@ import * as vscode from "vscode" -import { getWorkspacePath } from "../../utils/path" import { ContextProxy } from "../../core/config/ContextProxy" import { VectorStoreSearchResult } from "./interfaces" import { IndexingState } from "./interfaces/manager" @@ -133,7 +132,7 @@ export class CodeIndexManager { } // 3. Check if workspace is available - const workspacePath = getWorkspacePath() + const workspacePath = this.workspacePath if (!workspacePath) { this._stateManager.setSystemState("Standby", "No workspace folder open") return { requiresRestart } @@ -306,7 +305,7 @@ export class CodeIndexManager { ) const ignoreInstance = ignore() - const workspacePath = getWorkspacePath() + const workspacePath = this.workspacePath if (!workspacePath) { this._stateManager.setSystemState("Standby", "") diff --git a/src/services/code-index/vector-store/qdrant-client.ts b/src/services/code-index/vector-store/qdrant-client.ts index f18f3883237a..ce152824a730 100644 --- a/src/services/code-index/vector-store/qdrant-client.ts +++ b/src/services/code-index/vector-store/qdrant-client.ts @@ -17,6 +17,7 @@ export class QdrantVectorStore implements IVectorStore { private client: QdrantClient private readonly collectionName: string private readonly qdrantUrl: string = "http://localhost:6333" + private readonly workspacePath: string /** * Creates a new Qdrant vector store @@ -29,6 +30,7 @@ export class QdrantVectorStore implements IVectorStore { // Store the resolved URL for our property this.qdrantUrl = parsedUrl + this.workspacePath = workspacePath try { const urlObj = new URL(parsedUrl) @@ -457,7 +459,7 @@ export class QdrantVectorStore implements IVectorStore { return } - const workspaceRoot = getWorkspacePath() + const workspaceRoot = this.workspacePath // Build filters using pathSegments to match the indexed fields const filters = filePaths.map((filePath) => { diff --git a/src/services/mcp/McpHub.ts b/src/services/mcp/McpHub.ts index 6ec5b839e8cf..caca5ddb392e 100644 --- a/src/services/mcp/McpHub.ts +++ b/src/services/mcp/McpHub.ts @@ -30,7 +30,7 @@ import { McpToolCallResponse, } from "../../shared/mcp" import { fileExistsAtPath } from "../../utils/fs" -import { arePathsEqual } from "../../utils/path" +import { arePathsEqual, getWorkspacePath } from "../../utils/path" import { injectVariables } from "../../utils/config" // Discriminated union for connection states @@ -349,7 +349,7 @@ export class McpHub { return } - const workspaceFolder = vscode.workspace.workspaceFolders[0] + const workspaceFolder = this.providerRef.deref()?.cwd ?? getWorkspacePath() const projectMcpPattern = new vscode.RelativePattern(workspaceFolder, ".roo/mcp.json") // Create a file system watcher for the project MCP file pattern @@ -551,12 +551,8 @@ export class McpHub { // Get project-level MCP configuration path private async getProjectMcpPath(): Promise { - if (!vscode.workspace.workspaceFolders?.length) { - return null - } - - const workspaceFolder = vscode.workspace.workspaceFolders[0] - const projectMcpDir = path.join(workspaceFolder.uri.fsPath, ".roo") + const workspacePath = this.providerRef.deref()?.cwd ?? getWorkspacePath() + const projectMcpDir = path.join(workspacePath, ".roo") const projectMcpPath = path.join(projectMcpDir, "mcp.json") try {