Skip to content
Merged
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
2 changes: 1 addition & 1 deletion src/activate/registerCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ const getCommandsMap = ({ context, outputChannel, provider }: RegisterCommandOpt
TelemetryService.instance.captureTitleButtonClicked("plus")

await visibleProvider.removeClineFromStack()
await visibleProvider.postStateToWebview()
await visibleProvider.refreshWorkspace()
Copy link
Member

Choose a reason for hiding this comment

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

I see we're calling refreshWorkspace() here after removing from the stack. What's the purpose of refreshing the workspace at this point? Is it to pick up the active editor's workspace folder?

Also wondering - if getWorkspacePath() returns an empty string during this refresh (no editor open), would that cause issues for the new task that's about to be created?

Copy link
Author

Choose a reason for hiding this comment

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

This and the cmd property of the provider are the core modifications. The purpose is to switch the workspacePath only when the user clicks the plus sign, and to make all file-related operations rely on the Task's cwd first, and then the provider's cwd. In this way, when working in a multi-folder workspace, whether it is indexing, slash command or other actions, the user must switch the workspacePath by clicking the plus sign.

As for getting an empty string, this is not a problem.
getWorkspacePath basically cannot return an empty string. It first gets the workspacePath where the currently opened file is located, then the first folder of the multi-root workspace, and finally the empty string as the default value. When it is an empty string, it is only when the user does not open any directory. At this time, RooCode can be used to initiate tasks and conduct simple questions and answers, but it actually has no effect. All activities related to workspacePath are prohibited.

In fact, the source of all the confusion lies in the abuse of getWorkspacePath. The current main uses getWorkspacePath and vscode.workspace.workspaceFolders[0] extensively, which results in different functions sometimes forcing the first directory to be obtained, and sometimes based on the current active file directory. However, the cwd of the webview may be the one left before it switched the active file, which often leads to the inconsistency between the ui in the root workspace and the cwd of different background jobs, causing confusion.

await visibleProvider.postMessageToWebview({ type: "action", action: "chatButtonClicked" })
// Send focusInput action immediately after chatButtonClicked
// This ensures the focus happens after the view has switched
Expand Down
8 changes: 1 addition & 7 deletions src/core/mentions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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<void> {
export async function openMention(cwd: string, mention?: string): Promise<void> {
Copy link
Member

Choose a reason for hiding this comment

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

Good change to pass cwd as a parameter instead of calling getWorkspacePath() directly. This makes the function more testable and explicit about its dependencies.

Just wondering - what happens if cwd is an empty string here? Should we validate it's not empty before proceeding?

Copy link
Author

Choose a reason for hiding this comment

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

This issue is no different than any other; it all comes down to the source of cwd . If cwd is an empty string, then getWorkspacePath , the root cause, must also return an empty string. Ultimately, I simply fixed getWorkspacePath at the moment the user clicks the plus button and tried to use it across all functions.

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))
Expand Down
4 changes: 3 additions & 1 deletion src/core/task/Task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ export interface TaskOptions extends CreateTaskOptions {
taskNumber?: number
onCreated?: (task: Task) => void
initialTodos?: TodoItem[]
workspacePath?: string
}

export class Task extends EventEmitter<TaskEvents> implements TaskLike {
Expand Down Expand Up @@ -313,6 +314,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
taskNumber = -1,
onCreated,
initialTodos,
workspacePath,
}: TaskOptions) {
super()

Expand All @@ -333,7 +335,7 @@ export class Task extends EventEmitter<TaskEvents> 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")))
Copy link
Member

Choose a reason for hiding this comment

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

I noticed that if workspacePath is an empty string (which can happen from the provider), it gets stored as-is because empty string isn't nullish. This bypasses the Desktop fallback.

Was this intentional? It seems like we'd want to use the Desktop fallback when there's no valid workspace path.

Maybe consider: workspacePath && workspacePath.trim() ? workspacePath : getWorkspacePath(...)

Copy link
Author

Choose a reason for hiding this comment

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

The current behavior is the same as the main branch.

Previously, when a user created a task without opening any folders, path.join(os.homedir(), "Desktop") would be used as the workspace. This was completely meaningless because other functions often bypassed the Task's cwd and used getWorkspacePath to obtain the cwd. Now, I prioritize the Task's cwd, followed by the Provider's cwd, and finally using getWorkspacePath, which actually changes the original behavior.

Previously, when using getWorkspacePath to obtain the workspace path, an empty string would be returned because the user had no folders open, even if the Task's cwd was path.join(os.homedir(), "Desktop") . However, after my changes, if I didn't do this, many functions would assume that the workspacePath existed, allowing users to perform slash commands and MCP project settings without opening a folder, which was undoubtedly confusing. At the same time, the code index would not be initialized because it couldn't detect any folders.


this.instanceId = crypto.randomUUID().slice(0, 8)
this.taskNumber = -1
Expand Down
18 changes: 13 additions & 5 deletions src/core/webview/ClineProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Task, Array<() => void>> = new WeakMap()
private currentWorkspacePath: string | undefined

private recentTasksCache?: string[]

Expand All @@ -136,6 +137,7 @@ export class ClineProvider
mdmService?: MdmService,
) {
super()
this.currentWorkspacePath = getWorkspacePath()

ClineProvider.activeInstances.add(this)

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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),
})
Expand Down Expand Up @@ -1434,6 +1437,11 @@ export class ClineProvider
await this.postStateToWebview()
}

async refreshWorkspace() {
Copy link
Member

Choose a reason for hiding this comment

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

The refreshWorkspace method updates currentWorkspacePath but doesn't seem to notify anything about the change. Should this also post a message to the webview or update the state somehow?

Also, what happens if getWorkspacePath() throws here? There's no error handling.

Copy link
Author

Choose a reason for hiding this comment

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

Actually, no processing is needed here, the behavior here is the same as before. It's just that before, the workspacePath (the directory where the current active file is located) was dynamically obtained through the provider's cwd method, but now it refreshes the provider's currentWorkspacePath first and then pushes it to the webview.postStateToWebview is called after refreshWorkspace. Maybe I can combine them into one method

this.currentWorkspacePath = getWorkspacePath()
await this.postStateToWebview()
}

async postStateToWebview() {
const state = await this.getStateToPostToWebview()
this.postMessageToWebview({ type: "state", state })
Expand Down Expand Up @@ -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
}

Expand All @@ -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) {
Expand Down Expand Up @@ -2531,7 +2539,7 @@ export class ClineProvider
}

public get cwd() {
return getWorkspacePath()
return this.currentWorkspacePath || getWorkspacePath()
}

/**
Expand Down
33 changes: 22 additions & 11 deletions src/core/webview/webviewMessageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ export const webviewMessageHandler = async (
const updateGlobalState = async <K extends keyof GlobalState>(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
*/
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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) => ({
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion src/integrations/workspace/WorkspaceTracker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class WorkspaceTracker {
private resetTimer: NodeJS.Timeout | null = null

get cwd() {
return getWorkspacePath()
return this.providerRef?.deref()?.cwd ?? getWorkspacePath()
Copy link
Member

Choose a reason for hiding this comment

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

Interesting change to use the provider's cwd when available. This creates a dependency cycle though - WorkspaceTracker uses provider.cwd, and provider might use WorkspaceTracker.

Is this intentional? Could this cause any issues with initialization order?

Copy link
Author

Choose a reason for hiding this comment

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

My answer is no. But this question is certainly interesting, and it will become even more useful later.

Currently, I've discovered that its only purpose is to push workspaceUpdated every 300ms and return the file list corresponding to the cwd.

workspaceUpdated is used in two places: first, as the file list carried in the context sent with the user message, and second, as an index (I used it in previous multi-workspace support, but it's no longer used, and I'll refactor the code later).

The purpose of this change is to ensure that the cwd is consistent with the provider. Otherwise, if the user's active folder changes, the file list will be refreshed to the other folder. This will cause the file list carried in the context sent by the user to be inconsistent with the cwd used in the message, so this change is necessary.

}
constructor(provider: ClineProvider) {
this.providerRef = new WeakRef(provider)
Expand Down
5 changes: 2 additions & 3 deletions src/services/code-index/manager.ts
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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 }
Expand Down Expand Up @@ -306,7 +305,7 @@ export class CodeIndexManager {
)

const ignoreInstance = ignore()
const workspacePath = getWorkspacePath()
const workspacePath = this.workspacePath

if (!workspacePath) {
this._stateManager.setSystemState("Standby", "")
Expand Down
4 changes: 3 additions & 1 deletion src/services/code-index/vector-store/qdrant-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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) => {
Expand Down
12 changes: 4 additions & 8 deletions src/services/mcp/McpHub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -551,12 +551,8 @@ export class McpHub {

// Get project-level MCP configuration path
private async getProjectMcpPath(): Promise<string | null> {
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 {
Expand Down
Loading