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
81 changes: 59 additions & 22 deletions src/core/environment/getEnvironmentDetails.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,32 +238,69 @@ 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"))
// Check if we have multiple workspace folders
const workspaceFolders = vscode.workspace.workspaceFolders
const isMultiRoot = workspaceFolders && workspaceFolders.length > 1

if (isMultiRoot) {
// For multi-root workspaces, show files from all workspace folders
details += `\n\n# Multi-Root Workspace Files\n`
const maxFilesPerFolder = Math.floor((maxWorkspaceFiles ?? 200) / workspaceFolders.length)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Potential performance concern: The current implementation divides maxWorkspaceFiles equally among all workspace folders (line 248). If one folder is significantly larger than others, this could lead to uneven file representation.

Could we consider implementing a more dynamic allocation based on folder sizes? For example:

Suggested change
const maxFilesPerFolder = Math.floor((maxWorkspaceFiles ?? 200) / workspaceFolders.length)
const folderSizes = await Promise.all(workspaceFolders.map(async (folder) => {
const [files] = await listFiles(folder.uri.fsPath, true, 1000)
return files.length
}))
const totalFiles = folderSizes.reduce((sum, size) => sum + size, 0)
const maxFilesPerFolder = workspaceFolders.map((_, i) =>
Math.floor((folderSizes[i] / totalFiles) * (maxWorkspaceFiles ?? 200))
)


for (const folder of workspaceFolders) {
const folderPath = folder.uri.fsPath
const folderName = folder.name
details += `\n## ${folderName} (${folderPath.toPosix()})\n`

const isDesktop = arePathsEqual(folderPath, path.join(os.homedir(), "Desktop"))
if (isDesktop) {
details += "(Desktop files not shown automatically. Use list_files to explore if needed.)\n"
} else if (maxFilesPerFolder === 0) {
details += "(Workspace files context disabled. Use list_files to explore if needed.)\n"
} else {
const [files, didHitLimit] = await listFiles(folderPath, true, maxFilesPerFolder)
const { showRooIgnoredFiles = false } = state ?? {}

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

if (isDesktop) {
// 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.)"
details += result + "\n"
}
}
} else {
const maxFiles = maxWorkspaceFiles ?? 200

// Early return for limit of 0
if (maxFiles === 0) {
details += "(Workspace files context disabled. Use list_files to explore if needed.)"
// Single workspace folder - use existing logic
details += `\n\n# Current Workspace Directory (${cline.cwd.toPosix()}) Files\n`
const isDesktop = arePathsEqual(cline.cwd, path.join(os.homedir(), "Desktop"))

if (isDesktop) {
// 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 [files, didHitLimit] = await listFiles(cline.cwd, true, maxFiles)
const { showRooIgnoredFiles = false } = state ?? {}

const result = formatResponse.formatFilesList(
cline.cwd,
files,
didHitLimit,
cline.rooIgnoreController,
showRooIgnoredFiles,
)
const maxFiles = maxWorkspaceFiles ?? 200

// Early return for limit of 0
if (maxFiles === 0) {
details += "(Workspace files context disabled. Use list_files to explore if needed.)"
} else {
const [files, didHitLimit] = await listFiles(cline.cwd, true, maxFiles)
const { showRooIgnoredFiles = false } = state ?? {}

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

details += result
details += result
}
}
}
}
Expand Down
27 changes: 27 additions & 0 deletions src/core/task/Task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2909,6 +2909,33 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
return this.workspacePath
}

/**
* Get all workspace folders for multi-root workspace support
* @returns Array of all workspace folder paths
*/
public get workspaceFolders(): string[] {
const folders = vscode.workspace.workspaceFolders
if (!folders || folders.length === 0) {
return [this.workspacePath]
}
return folders.map((folder) => folder.uri.fsPath)
}

/**
* Check if a path is within any of the workspace folders
* @param filePath The file path to check
* @returns true if the path is in a workspace folder, false otherwise
*/
public isPathInWorkspace(filePath: string): boolean {
const absolutePath = path.resolve(filePath)
const normalizedPath = path.normalize(absolutePath)

return this.workspaceFolders.some((folderPath) => {
const normalizedFolderPath = path.normalize(folderPath)
return normalizedPath === normalizedFolderPath || normalizedPath.startsWith(normalizedFolderPath + path.sep)
})
}

/**
* Process any queued messages by dequeuing and submitting them.
* This ensures that queued user messages are sent when appropriate,
Expand Down
176 changes: 158 additions & 18 deletions src/utils/__tests__/path.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,36 +3,61 @@
import os from "os"
import * as path from "path"

import { arePathsEqual, getReadablePath, getWorkspacePath } from "../path"
import {
arePathsEqual,
getReadablePath,
getWorkspacePath,
getAllWorkspacePaths,
getWorkspaceFolderForPath,
} from "../path"

// Mock modules
const mockWorkspaceFolders = vi.fn()
const mockGetWorkspaceFolder = vi.fn()
const mockActiveTextEditor = vi.fn()

vi.mock("vscode", () => ({
window: {
activeTextEditor: {
document: {
uri: { fsPath: "/test/workspaceFolder/file.ts" },
},
get activeTextEditor() {
return mockActiveTextEditor()
},
},
workspace: {
workspaceFolders: [
get workspaceFolders() {
return mockWorkspaceFolders()
},
getWorkspaceFolder: mockGetWorkspaceFolder,
},
}))
describe("Path Utilities", () => {
const originalPlatform = process.platform
// Helper to mock VS Code configuration

beforeEach(() => {
// Reset mocks before each test
vi.clearAllMocks()

// Set default mock values
mockWorkspaceFolders.mockReturnValue([
{
uri: { fsPath: "/test/workspace" },
name: "test",
index: 0,
},
],
getWorkspaceFolder: vi.fn().mockReturnValue({
])

mockActiveTextEditor.mockReturnValue({
document: {
uri: { fsPath: "/test/workspaceFolder/file.ts" },
},
})

mockGetWorkspaceFolder.mockReturnValue({
uri: {
fsPath: "/test/workspaceFolder",
},
}),
},
}))
describe("Path Utilities", () => {
const originalPlatform = process.platform
// Helper to mock VS Code configuration
})
})

afterEach(() => {
Object.defineProperty(process, "platform", {
Expand All @@ -56,14 +81,129 @@ describe("Path Utilities", () => {
expect(extendedPath.toPosix()).toBe("\\\\?\\C:\\Very\\Long\\Path")
})
})

describe("getWorkspacePath", () => {
it("should return the current workspace path", () => {
const workspacePath = "/Users/test/project"
expect(getWorkspacePath(workspacePath)).toBe("/Users/test/project")
it("should return the workspace folder of the active editor", () => {
mockActiveTextEditor.mockReturnValue({
document: {
uri: { fsPath: "/test/workspaceFolder/file.ts" },
},
})
mockGetWorkspaceFolder.mockReturnValue({
uri: { fsPath: "/test/workspaceFolder" },
})
mockWorkspaceFolders.mockReturnValue([{ uri: { fsPath: "/test/workspace" }, name: "workspace", index: 0 }])

expect(getWorkspacePath()).toBe("/test/workspaceFolder")
})

it("should return the first workspace folder when no active editor", () => {
mockActiveTextEditor.mockReturnValue(undefined)
mockGetWorkspaceFolder.mockReturnValue(undefined)
mockWorkspaceFolders.mockReturnValue([
{ uri: { fsPath: "/test/workspace1" }, name: "workspace1", index: 0 },
{ uri: { fsPath: "/test/workspace2" }, name: "workspace2", index: 1 },
])

expect(getWorkspacePath()).toBe("/test/workspace1")
})

it("should return default path when no workspace folders", () => {
mockActiveTextEditor.mockReturnValue(undefined)
mockGetWorkspaceFolder.mockReturnValue(undefined)
mockWorkspaceFolders.mockReturnValue(undefined)

expect(getWorkspacePath("/default/path")).toBe("/default/path")
})

it("should handle multi-root workspaces correctly", () => {
mockWorkspaceFolders.mockReturnValue([
{ uri: { fsPath: "/test/frontend" }, name: "frontend", index: 0 },
{ uri: { fsPath: "/test/backend" }, name: "backend", index: 1 },
])

// When active editor is in backend folder
mockActiveTextEditor.mockReturnValue({
document: { uri: { fsPath: "/test/backend/src/app.ts" } },
})
mockGetWorkspaceFolder.mockReturnValue({
uri: { fsPath: "/test/backend" },
})

expect(getWorkspacePath()).toBe("/test/backend")
})
})

describe("getAllWorkspacePaths", () => {
it("should return all workspace folder paths", () => {
mockWorkspaceFolders.mockReturnValue([
{ uri: { fsPath: "/test/frontend" }, name: "frontend", index: 0 },
{ uri: { fsPath: "/test/backend" }, name: "backend", index: 1 },
])

const paths = getAllWorkspacePaths()
expect(paths).toEqual(["/test/frontend", "/test/backend"])
})

it("should return empty array when no workspace folders", () => {
mockWorkspaceFolders.mockReturnValue(undefined)

const paths = getAllWorkspacePaths()
expect(paths).toEqual([])
})

it("should handle single workspace folder", () => {
mockWorkspaceFolders.mockReturnValue([{ uri: { fsPath: "/test/workspace" }, name: "workspace", index: 0 }])

const paths = getAllWorkspacePaths()
expect(paths).toEqual(["/test/workspace"])
})
})

describe("getWorkspaceFolderForPath", () => {
beforeEach(() => {
mockWorkspaceFolders.mockReturnValue([
{ uri: { fsPath: "/test/frontend" }, name: "frontend", index: 0 },
{ uri: { fsPath: "/test/backend" }, name: "backend", index: 1 },
])
})

it("should return the workspace folder containing the path", () => {
expect(getWorkspaceFolderForPath("/test/frontend/src/app.ts")).toBe("/test/frontend")
expect(getWorkspaceFolderForPath("/test/backend/src/server.ts")).toBe("/test/backend")
})

it("should return the workspace folder for exact match", () => {
expect(getWorkspaceFolderForPath("/test/frontend")).toBe("/test/frontend")
expect(getWorkspaceFolderForPath("/test/backend")).toBe("/test/backend")
})

it("should return null for paths outside any workspace", () => {
expect(getWorkspaceFolderForPath("/other/path/file.ts")).toBeNull()
expect(getWorkspaceFolderForPath("/test/other/file.ts")).toBeNull()
})

it("should return null when no workspace folders", () => {
mockWorkspaceFolders.mockReturnValue(undefined)
expect(getWorkspaceFolderForPath("/test/frontend/src/app.ts")).toBeNull()
})

it("should return undefined when outside a workspace", () => {})
it("should handle relative paths by resolving them", () => {
// This depends on the current working directory, so we'll use path.resolve
const resolvedPath = path.resolve("./src/app.ts")
const result = getWorkspaceFolderForPath("./src/app.ts")

// The result should be based on the resolved path
if (resolvedPath.startsWith("/test/frontend")) {
expect(result).toBe("/test/frontend")
} else if (resolvedPath.startsWith("/test/backend")) {
expect(result).toBe("/test/backend")
} else {
expect(result).toBeNull()
}
})
})

describe("arePathsEqual", () => {
describe("on Windows", () => {
beforeEach(() => {
Expand Down
Loading
Loading