diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 97ef0ddc3c..b25c3957a2 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -43,7 +43,7 @@ import { getOpenAiModels } from "../../api/providers/openai" import { getVsCodeLmModels } from "../../api/providers/vscode-lm" import { openMention } from "../mentions" import { TelemetrySetting } from "../../shared/TelemetrySetting" -import { getWorkspacePath } from "../../utils/path" +import { getWorkspacePath, findWorkspaceWithRoo } from "../../utils/path" import { ensureSettingsDirectoryExists } from "../../utils/globalContext" import { Mode, defaultModeSlug } from "../../shared/modes" import { getModels, flushModels } from "../../api/providers/fetchers/modelCache" @@ -2477,13 +2477,13 @@ export const webviewMessageHandler = async ( const globalConfigDir = path.join(os.homedir(), ".roo") commandsDir = path.join(globalConfigDir, "commands") } else { - // Project commands - const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath - if (!workspaceRoot) { + // Project commands - find the workspace with .roo directory + const workspaceWithRoo = await findWorkspaceWithRoo() + if (!workspaceWithRoo) { vscode.window.showErrorMessage(t("common:errors.no_workspace_for_project_command")) break } - commandsDir = path.join(workspaceRoot, ".roo", "commands") + commandsDir = path.join(workspaceWithRoo.uri.fsPath, ".roo", "commands") } // Ensure the commands directory exists diff --git a/src/utils/__tests__/path.spec.ts b/src/utils/__tests__/path.spec.ts index a8cf84b68c..f159b84145 100644 --- a/src/utils/__tests__/path.spec.ts +++ b/src/utils/__tests__/path.spec.ts @@ -3,9 +3,18 @@ import os from "os" import * as path from "path" -import { arePathsEqual, getReadablePath, getWorkspacePath } from "../path" +// Use vi.hoisted to ensure mocks are available during module loading +const { mockWorkspaceFolders, mockGetWorkspaceFolder, mockFileExistsAtPath } = vi.hoisted(() => { + const mockWorkspaceFolders = vi.fn() + const mockGetWorkspaceFolder = vi.fn() + const mockFileExistsAtPath = vi.fn() + return { mockWorkspaceFolders, mockGetWorkspaceFolder, mockFileExistsAtPath } +}) -// Mock modules +// Mock modules before imports +vi.mock("../fs", () => ({ + fileExistsAtPath: mockFileExistsAtPath, +})) vi.mock("vscode", () => ({ window: { @@ -16,23 +25,37 @@ vi.mock("vscode", () => ({ }, }, workspace: { - workspaceFolders: [ + get workspaceFolders() { + return mockWorkspaceFolders() + }, + getWorkspaceFolder: mockGetWorkspaceFolder, + }, +})) + +import { arePathsEqual, getReadablePath, getWorkspacePath, findWorkspaceWithRoo } from "../path" +import { fileExistsAtPath } from "../fs" + +describe("Path Utilities", () => { + const originalPlatform = process.platform + // Helper to mock VS Code configuration + + beforeEach(() => { + // Reset mocks before each test + vi.clearAllMocks() + // Set default workspace folders + mockWorkspaceFolders.mockReturnValue([ { uri: { fsPath: "/test/workspace" }, name: "test", index: 0, }, - ], - getWorkspaceFolder: vi.fn().mockReturnValue({ + ]) + mockGetWorkspaceFolder.mockReturnValue({ uri: { fsPath: "/test/workspaceFolder", }, - }), - }, -})) -describe("Path Utilities", () => { - const originalPlatform = process.platform - // Helper to mock VS Code configuration + }) + }) afterEach(() => { Object.defineProperty(process, "platform", { diff --git a/src/utils/path.ts b/src/utils/path.ts index 48e2ce6673..903cc8d876 100644 --- a/src/utils/path.ts +++ b/src/utils/path.ts @@ -1,6 +1,7 @@ import * as path from "path" import os from "os" import * as vscode from "vscode" +import { fileExistsAtPath } from "./fs" /* The Node.js 'path' module resolves and normalizes paths differently depending on the platform: @@ -130,3 +131,33 @@ export const getWorkspacePathForContext = (contextPath?: string): string => { // Fall back to current behavior return getWorkspacePath() } + +/** + * Finds the workspace folder that contains a .roo directory. + * In multi-root workspaces, this ensures we find the correct workspace + * rather than just using the first one. + * + * @returns The workspace folder containing .roo, or undefined if none found + */ +export async function findWorkspaceWithRoo(): Promise { + const workspaceFolders = vscode.workspace.workspaceFolders + if (!workspaceFolders || workspaceFolders.length === 0) { + return undefined + } + + // If there's only one workspace folder, return it + if (workspaceFolders.length === 1) { + return workspaceFolders[0] + } + + // Check each workspace folder for a .roo directory + for (const folder of workspaceFolders) { + const rooPath = path.join(folder.uri.fsPath, ".roo") + if (await fileExistsAtPath(rooPath)) { + return folder + } + } + + // If no .roo directory found in any workspace, return the first one as fallback + return workspaceFolders[0] +}