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
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { getApiMetrics } from "../../../shared/getApiMetrics"
import { listFiles } from "../../../services/glob/list-files"
import { TerminalRegistry } from "../../../integrations/terminal/TerminalRegistry"
import { Terminal } from "../../../integrations/terminal/Terminal"
import { arePathsEqual } from "../../../utils/path"
import { arePathsEqual, getAllWorkspacePaths } from "../../../utils/path"
import { FileContextTracker } from "../../context-tracking/FileContextTracker"
import { ApiHandler } from "../../../api/index"
import { ClineProvider } from "../../webview/ClineProvider"
Expand Down Expand Up @@ -129,6 +129,7 @@ describe("getEnvironmentDetails", () => {
;(listFiles as Mock).mockResolvedValue([["file1.ts", "file2.ts"], false])
;(formatResponse.formatFilesList as Mock).mockReturnValue("file1.ts\nfile2.ts")
;(arePathsEqual as Mock).mockReturnValue(false)
;(getAllWorkspacePaths as Mock).mockReturnValue([mockCwd])
;(Terminal.compressTerminalOutput as Mock).mockImplementation((output: string) => output)
;(TerminalRegistry.getTerminals as Mock).mockReturnValue([])
;(TerminalRegistry.getBackgroundTerminals as Mock).mockReturnValue([])
Expand Down
46 changes: 28 additions & 18 deletions src/core/environment/getEnvironmentDetails.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { getApiMetrics } from "../../shared/getApiMetrics"
import { listFiles } from "../../services/glob/list-files"
import { TerminalRegistry } from "../../integrations/terminal/TerminalRegistry"
import { Terminal } from "../../integrations/terminal/Terminal"
import { arePathsEqual } from "../../utils/path"
import { arePathsEqual, getAllWorkspacePaths } from "../../utils/path"
import { formatResponse } from "../prompts/responses"

import { Task } from "../task/Task"
Expand Down Expand Up @@ -249,27 +249,37 @@ export async function getEnvironmentDetails(cline: Task, includeFileDetails: boo

if (includeFileDetails) {
details += `\n\n# Current Workspace Directory (${cline.cwd.toPosix()}) Files\n`
const isDesktop = arePathsEqual(cline.cwd, path.join(os.homedir(), "Desktop"))

if (isDesktop) {
const allWorkspacePaths = getAllWorkspacePaths()
const maxFilesPerWorkspace = Math.floor((maxWorkspaceFiles ?? 200) / allWorkspacePaths.length)

// Collect all files from all workspaces
let allFiles: string[] = []
let anyDidHitLimit = false

for (const workspacePath of allWorkspacePaths) {
const isDesktop = arePathsEqual(workspacePath, path.join(os.homedir(), "Desktop"))
// Don't want to immediately access desktop since it would show
// permission popup.
details += "(Desktop files not shown automatically. Use list_files to explore if needed.)"
} else {
const maxFiles = maxWorkspaceFiles ?? 200
const [files, didHitLimit] = await listFiles(cline.cwd, true, maxFiles)
const { showRooIgnoredFiles = true } = state ?? {}

const result = formatResponse.formatFilesList(
cline.cwd,
files,
didHitLimit,
cline.rooIgnoreController,
showRooIgnoredFiles,
)

details += result
if (isDesktop) {
details += "(Desktop files not shown automatically. Use list_files to explore if needed.)\n"
continue
}

const [files, didHitLimit] = await listFiles(workspacePath, true, maxFilesPerWorkspace)
anyDidHitLimit = anyDidHitLimit || didHitLimit
allFiles.push(...files)
}

// Format all file paths relative to `cline.cwd`
const { showRooIgnoredFiles = true } = state ?? {}
details += formatResponse.formatFilesList(
cline.cwd,
allFiles,
anyDidHitLimit,
cline.rooIgnoreController,
showRooIgnoredFiles,
)
}

return `<environment_details>\n${details.trim()}\n</environment_details>`
Expand Down
3 changes: 2 additions & 1 deletion src/core/mentions/__tests__/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ vi.mock("../../../integrations/misc/extract-text", () => ({
import { parseMentions, openMention } from "../index"
import { UrlContentFetcher } from "../../../services/browser/UrlContentFetcher"
import * as git from "../../../utils/git"
import { getWorkspacePath } from "../../../utils/path"
import { getWorkspacePath, getAllWorkspacePaths } from "../../../utils/path"
import fs from "fs/promises"
import * as path from "path"
import { openFile } from "../../../integrations/misc/open-file"
Expand Down Expand Up @@ -141,6 +141,7 @@ describe("mentions", () => {
mockUrlFetcher = new (UrlContentFetcher as any)()
;(fs.stat as Mock).mockResolvedValue({ isFile: () => true, isDirectory: () => false })
;(extractTextFromFile as Mock).mockResolvedValue("Mock file content")
;(getAllWorkspacePaths as Mock).mockReturnValue([mockCwd])
})

it("should parse git commit mentions", async () => {
Expand Down
3 changes: 3 additions & 0 deletions src/core/task/__tests__/Task.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,9 @@ vi.mock("vscode", () => {
from: vi.fn(),
},
TabInputText: vi.fn(),
RelativePattern: vi.fn().mockImplementation((base, pattern) => ({ base, pattern })),
Uri: { file: vi.fn((path) => ({ fsPath: path })) },
FileType: { File: 1, Directory: 2 },
}
})

Expand Down
6 changes: 4 additions & 2 deletions src/core/tools/searchFilesTool.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import * as vscode from "vscode"
import path from "path"

import { Task } from "../task/Task"
import { ToolUse, AskApproval, HandleError, PushToolResult, RemoveClosingTag } from "../../shared/tools"
import { ClineSayTool } from "../../shared/ExtensionMessage"
import { getReadablePath } from "../../utils/path"
import { getAllWorkspacePaths, getReadablePath } from "../../utils/path"
import { isPathOutsideWorkspace } from "../../utils/pathUtils"
import { regexSearchFiles } from "../../services/ripgrep"

Expand Down Expand Up @@ -52,9 +53,10 @@ export async function searchFilesTool(

cline.consecutiveMistakeCount = 0

const workspacePaths = getAllWorkspacePaths()
const results = await regexSearchFiles(
cline.cwd,
absolutePath,
workspacePaths,
regex,
filePattern,
cline.rooIgnoreController,
Expand Down
27 changes: 4 additions & 23 deletions src/core/webview/webviewMessageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { openImage, saveImage } from "../../integrations/misc/image-handler"
import { selectImages } from "../../integrations/misc/process-images"
import { getTheme } from "../../integrations/theme/getTheme"
import { discoverChromeHostUrl, tryChromeHostUrl } from "../../services/browser/browserDiscovery"
import { searchWorkspaceFiles } from "../../services/search/file-search"
import { searchAllWorkspaceFiles } from "../../services/search/file-search"
import { fileExistsAtPath } from "../../utils/fs"
import { playTts, setTtsEnabled, setTtsSpeed, stopTts } from "../../utils/tts"
import { singleCompletionHandler } from "../../utils/single-completion-handler"
Expand All @@ -33,7 +33,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 { getPrimaryWorkspaceFolder } from "../../utils/path"
import { Mode, defaultModeSlug } from "../../shared/modes"
import { getModels, flushModels } from "../../api/providers/fetchers/modelCache"
import { GetModelsOptions } from "../../shared/api"
Expand Down Expand Up @@ -600,7 +600,7 @@ export const webviewMessageHandler = async (
return
}

const workspaceFolder = vscode.workspace.workspaceFolders[0]
const workspaceFolder = getPrimaryWorkspaceFolder()
const rooDir = path.join(workspaceFolder.uri.fsPath, ".roo")
const mcpPath = path.join(rooDir, "mcp.json")

Expand Down Expand Up @@ -1252,27 +1252,8 @@ export const webviewMessageHandler = async (
break
}
case "searchFiles": {
const workspacePath = getWorkspacePath()

if (!workspacePath) {
// Handle case where workspace path is not available
await provider.postMessageToWebview({
type: "fileSearchResults",
results: [],
requestId: message.requestId,
error: "No workspace path available",
})
break
}
try {
// Call file search service with query from message
const results = await searchWorkspaceFiles(
message.query || "",
workspacePath,
20, // Use default limit, as filtering is now done in the backend
)

// Send results back to webview
const results = await searchAllWorkspaceFiles(message.query || "", 20)
await provider.postMessageToWebview({
type: "fileSearchResults",
results,
Expand Down
58 changes: 37 additions & 21 deletions src/integrations/workspace/WorkspaceTracker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as path from "path"

import { listFiles } from "../../services/glob/list-files"
import { ClineProvider } from "../../core/webview/ClineProvider"
import { toRelativePath, getWorkspacePath } from "../../utils/path"
import { toRelativePath, getWorkspacePath, getAllWorkspacePaths } from "../../utils/path"

const MAX_INITIAL_FILES = 1_000

Expand All @@ -19,6 +19,7 @@ class WorkspaceTracker {
get cwd() {
return getWorkspacePath()
}

constructor(provider: ClineProvider) {
this.providerRef = new WeakRef(provider)
this.registerListeners()
Expand All @@ -30,17 +31,47 @@ class WorkspaceTracker {
return
}
const tempCwd = this.cwd
const [files, _] = await listFiles(tempCwd, true, MAX_INITIAL_FILES)
if (this.prevWorkSpacePath !== tempCwd) {
return
const allWorkspaces = getAllWorkspacePaths()

// Distribute file limit across all workspaces
const filesPerWorkspace = Math.ceil(MAX_INITIAL_FILES / allWorkspaces.length)
for (const workspacePath of allWorkspaces) {
const [files, _] = await listFiles(workspacePath, true, filesPerWorkspace)
if (this.prevWorkSpacePath !== tempCwd) {
return
}
files.slice(0, filesPerWorkspace).forEach((file) => {
const absolutePath = path.resolve(workspacePath, file)
this.filePaths.add(this.normalizeFilePath(absolutePath))
})
}
files.slice(0, MAX_INITIAL_FILES).forEach((file) => this.filePaths.add(this.normalizeFilePath(file)))
this.workspaceDidUpdate()
}

private registerListeners() {
const watcher = vscode.workspace.createFileSystemWatcher("**")
this.prevWorkSpacePath = this.cwd

const workspaceFolders = vscode.workspace.workspaceFolders ?? ["."]
workspaceFolders.forEach((folder) => {
const pattern = new vscode.RelativePattern(folder, "**")
const watcher = vscode.workspace.createFileSystemWatcher(pattern)
this.setupWatcherEvents(watcher)
this.disposables.push(watcher)
})

this.disposables.push(
vscode.window.tabGroups.onDidChangeTabs(() => {
// Reset if workspace path has changed
if (this.prevWorkSpacePath !== this.cwd) {
this.workspaceDidReset()
} else {
this.workspaceDidUpdate()
}
}),
)
}

private setupWatcherEvents(watcher: vscode.FileSystemWatcher) {
this.disposables.push(
watcher.onDidCreate(async (uri) => {
await this.addFilePath(uri.fsPath)
Expand All @@ -56,21 +87,6 @@ class WorkspaceTracker {
}
}),
)

this.disposables.push(watcher)

// Listen for tab changes and call workspaceDidUpdate directly
this.disposables.push(
vscode.window.tabGroups.onDidChangeTabs(() => {
// Reset if workspace path has changed
if (this.prevWorkSpacePath !== this.cwd) {
this.workspaceDidReset()
} else {
// Otherwise just update
this.workspaceDidUpdate()
}
}),
)
}

private getOpenedTabsInfo() {
Expand Down
16 changes: 10 additions & 6 deletions src/integrations/workspace/__tests__/WorkspaceTracker.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as vscode from "vscode"
import WorkspaceTracker from "../WorkspaceTracker"
import { ClineProvider } from "../../../core/webview/ClineProvider"
import { listFiles } from "../../../services/glob/list-files"
import { getWorkspacePath } from "../../../utils/path"
import { getWorkspacePath, getAllWorkspacePaths } from "../../../utils/path"

// Mock functions - must be defined before vitest.mock calls
const mockOnDidCreate = vitest.fn()
Expand All @@ -16,6 +16,10 @@ let registeredTabChangeCallback: (() => Promise<void>) | null = null
// Mock workspace path
vitest.mock("../../../utils/path", () => ({
getWorkspacePath: vitest.fn().mockReturnValue("/test/workspace"),
getAllWorkspacePaths: vitest.fn().mockReturnValue(["/test/workspace"]),
getPrimaryWorkspaceFolder: vitest
.fn()
.mockReturnValue({ uri: { fsPath: "/test/workspace" }, name: "test-workspace", index: 0 }),
toRelativePath: vitest.fn((path, cwd) => {
// Handle both Windows and POSIX paths by using path.relative
const relativePath = require("path").relative(cwd, path)
Expand Down Expand Up @@ -59,6 +63,9 @@ vitest.mock("vscode", () => ({
},
},
FileType: { File: 1, Directory: 2 },
RelativePattern: vitest.fn().mockImplementation((base, pattern) => ({ base, pattern })),
Uri: { file: vitest.fn((path) => ({ fsPath: path })) },
TabInputText: vitest.fn(),
}))

vitest.mock("../../../services/glob/list-files", () => ({
Expand Down Expand Up @@ -148,7 +155,7 @@ describe("WorkspaceTracker", () => {

expect(mockProvider.postMessageToWebview).toHaveBeenCalledWith({
type: "workspaceUpdated",
filePaths: expect.arrayContaining(["newdir"]),
filePaths: expect.arrayContaining(["newdir/"]),
openedTabs: [],
})
const lastCall = (mockProvider.postMessageToWebview as Mock).mock.calls.slice(-1)[0]
Expand Down Expand Up @@ -208,10 +215,6 @@ describe("WorkspaceTracker", () => {
it("should handle workspace path changes when tabs change", async () => {
expect(registeredTabChangeCallback).not.toBeNull()

// Set initial workspace path and create tracker
;(getWorkspacePath as Mock).mockReturnValue("/test/workspace")
workspaceTracker = new WorkspaceTracker(mockProvider)

// Clear any initialization calls
vitest.clearAllMocks()

Expand All @@ -221,6 +224,7 @@ describe("WorkspaceTracker", () => {

// Change workspace path
;(getWorkspacePath as Mock).mockReturnValue("/test/new-workspace")
;(getAllWorkspacePaths as Mock).mockReturnValue(["/test/new-workspace"])

// Simulate tab change event
await registeredTabChangeCallback!()
Expand Down
19 changes: 18 additions & 1 deletion src/services/ripgrep/__tests__/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// npx vitest run src/services/ripgrep/__tests__/index.spec.ts

import { truncateLine } from "../index"
import { truncateLine, regexSearchFiles } from "../index"

describe("Ripgrep line truncation", () => {
// The default MAX_LINE_LENGTH is 500 in the implementation
Expand Down Expand Up @@ -48,3 +48,20 @@ describe("Ripgrep line truncation", () => {
expect(truncated).toContain("[truncated...]")
})
})

describe("Multi-workspace search", () => {
it("should handle empty workspace paths array", async () => {
const result = await regexSearchFiles("/mock/cwd", [], "test")
expect(result).toBe("No workspace paths provided")
})

it("should search multiple workspace paths", async () => {
const workspacePaths = ["/workspace1", "/workspace2"]

try {
await regexSearchFiles("/mock/cwd", workspacePaths, "test")
} catch (error) {
expect(error).toBeDefined()
}
})
})
Loading