diff --git a/package.json b/package.json index ec0c6bbfac..6051ad562b 100644 --- a/package.json +++ b/package.json @@ -332,14 +332,14 @@ "package": "npm run check-types && npm run build:webview && npm run lint && node esbuild.js --production", "protos": "node proto/build-proto.js && node scripts/generate-server-setup.mjs && node scripts/generate-host-bridge-client.mjs", "postprotos": "prettier src/shared/proto src/core/controller src/hosts/ webview-ui/src/services src/generated --write --log-level warn", - "clean": "rimraf dist dist-standalone webview-ui/build src/generated", + "clean": "rimraf dist dist-standalone webview-ui/build src/generated out/", "compile-tests": "node ./scripts/build-tests.js", "watch-tests": "tsc -p . -w --outDir out", - "pretest": "npm run compile-tests && npm run compile && npm run compile-standalone && npm run lint", "check-types": "npm run protos && tsc --noEmit", "lint": "eslint src --ext ts && eslint webview-ui/src --ext ts && buf lint && cd webview-ui && npm run lint", "format": "prettier . --check", "format:fix": "prettier . --write", + "pretest": "npm run compile && npm run compile-tests && npm run compile-standalone && npm run lint", "test": "npm-run-all test:unit test:integration", "test:ci": "node scripts/test-ci.js", "test:integration": "vscode-test", diff --git a/src/core/context/context-tracking/FileContextTracker.test.ts b/src/core/context/context-tracking/FileContextTracker.test.ts index f5fe65879b..27fea102c5 100644 --- a/src/core/context/context-tracking/FileContextTracker.test.ts +++ b/src/core/context/context-tracking/FileContextTracker.test.ts @@ -6,6 +6,9 @@ import * as path from "path" import { FileContextTracker } from "./FileContextTracker" import * as diskModule from "@core/storage/disk" import type { TaskMetadata, FileMetadataEntry } from "./ContextTrackerTypes" +import type { WebviewProviderCreator } from "@/hosts/host-providers" +import * as hostProviders from "@hosts/host-providers" +import { vscodeHostBridgeClient } from "@/hosts/vscode/client/host-grpc-client" describe("FileContextTracker", () => { let sandbox: sinon.SinonSandbox @@ -37,7 +40,6 @@ describe("FileContextTracker", () => { } // Use a function replacement instead of a direct stub - const originalCreateFileSystemWatcher = vscode.workspace.createFileSystemWatcher vscode.workspace.createFileSystemWatcher = function () { return mockFileSystemWatcher } @@ -51,6 +53,7 @@ describe("FileContextTracker", () => { mockTaskMetadata = { files_in_context: [], model_usage: [] } getTaskMetadataStub = sandbox.stub(diskModule, "getTaskMetadata").resolves(mockTaskMetadata) saveTaskMetadataStub = sandbox.stub(diskModule, "saveTaskMetadata").resolves() + hostProviders.initializeHostProviders(((_) => {}) as WebviewProviderCreator, vscodeHostBridgeClient) // Create tracker instance taskId = "test-task-id" diff --git a/src/core/context/context-tracking/FileContextTracker.ts b/src/core/context/context-tracking/FileContextTracker.ts index 26c70b10c1..14f7d4ce51 100644 --- a/src/core/context/context-tracking/FileContextTracker.ts +++ b/src/core/context/context-tracking/FileContextTracker.ts @@ -5,6 +5,8 @@ import { getWorkspaceState, updateWorkspaceState } from "@core/storage/state" import { getGlobalState } from "@core/storage/state" import type { FileMetadataEntry } from "./ContextTrackerTypes" import type { ClineMessage } from "@shared/ExtensionMessage" +import { getHostBridgeProvider } from "@/hosts/host-providers" +import { getCwd } from "@/utils/path" // This class is responsible for tracking file operations that may result in stale context. // If a user modifies a file outside of Cline, the context may become stale and need to be updated. @@ -37,8 +39,8 @@ export class FileContextTracker { /** * Gets the current working directory or returns undefined if it cannot be determined */ - private getCwd(): string | undefined { - const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) + private async getCwd(): Promise { + const cwd = await getCwd(undefined) if (!cwd) { console.info("No workspace folder available - cannot determine current working directory") } @@ -54,7 +56,7 @@ export class FileContextTracker { return } - const cwd = this.getCwd() + const cwd = await this.getCwd() if (!cwd) { return } @@ -85,7 +87,7 @@ export class FileContextTracker { */ async trackFileContext(filePath: string, operation: "read_tool" | "user_edited" | "cline_edited" | "file_mentioned") { try { - const cwd = this.getCwd() + const cwd = await this.getCwd() if (!cwd) { return } diff --git a/src/core/controller/file/searchCommits.ts b/src/core/controller/file/searchCommits.ts index 772be38137..7665f29277 100644 --- a/src/core/controller/file/searchCommits.ts +++ b/src/core/controller/file/searchCommits.ts @@ -13,7 +13,7 @@ import { convertGitCommitsToProtoGitCommits } from "@shared/proto-conversions/fi * @returns GitCommits containing the matching commits */ export const searchCommits: FileMethodHandler = async (controller: Controller, request: StringRequest): Promise => { - const cwd = getWorkspacePath() + const cwd = await getWorkspacePath() if (!cwd) { return GitCommits.create({ commits: [] }) } diff --git a/src/core/controller/file/searchFiles.ts b/src/core/controller/file/searchFiles.ts index f68fb254ca..fe482e24cd 100644 --- a/src/core/controller/file/searchFiles.ts +++ b/src/core/controller/file/searchFiles.ts @@ -12,10 +12,10 @@ import { convertSearchResultsToProtoFileInfos } from "@shared/proto-conversions/ * @returns Results containing matching files/folders */ export const searchFiles: FileMethodHandler = async ( - controller: Controller, + _controller: Controller, request: FileSearchRequest, ): Promise => { - const workspacePath = getWorkspacePath() + const workspacePath = await getWorkspacePath() if (!workspacePath) { // Handle case where workspace path is not available diff --git a/src/core/controller/grpc-service.ts b/src/core/controller/grpc-service.ts index 23cb54ba59..aa9d4a33e3 100644 --- a/src/core/controller/grpc-service.ts +++ b/src/core/controller/grpc-service.ts @@ -37,6 +37,7 @@ export class ServiceRegistry { * @param serviceName The name of the service (used for logging) */ constructor(serviceName: string) { + console.log(`Registering Protobus service: ${serviceName}...`) this.serviceName = serviceName } @@ -56,7 +57,6 @@ export class ServiceRegistry { } this.methodMetadata[methodName] = { isStreaming, ...metadata } - console.log(`Registered ${this.serviceName} method: ${methodName}${isStreaming ? " (streaming)" : ""}`) } /** diff --git a/src/core/controller/index.ts b/src/core/controller/index.ts index efd25b0be8..688c818b00 100644 --- a/src/core/controller/index.ts +++ b/src/core/controller/index.ts @@ -46,6 +46,9 @@ import { sendMcpMarketplaceCatalogEvent } from "./mcp/subscribeToMcpMarketplaceC import { sendRelinquishControlEvent } from "./ui/subscribeToRelinquishControl" import { handleTaskServiceRequest } from "./task" import { BooleanRequest } from "@shared/proto/common" +import { getHostBridgeProvider } from "@/hosts/host-providers" +import { GetWorkspacePathsRequest } from "@/shared/proto/index.host" +import { getCwd } from "@/utils/path" /* https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts @@ -683,8 +686,8 @@ export class Controller { // Context menus and code actions - getFileMentionFromPath(filePath: string) { - const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) + async getFileMentionFromPath(filePath: string) { + const cwd = await getCwd() if (!cwd) { return "@/" + filePath } @@ -988,7 +991,7 @@ export class Controller { async generateGitCommitMessage() { try { // Check if there's a workspace folder open - const cwd = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath + const cwd = await getCwd() if (!cwd) { vscode.window.showErrorMessage("No workspace folder open") return diff --git a/src/core/controller/task/getTaskHistory.ts b/src/core/controller/task/getTaskHistory.ts index a9c73433e6..90c0d65989 100644 --- a/src/core/controller/task/getTaskHistory.ts +++ b/src/core/controller/task/getTaskHistory.ts @@ -15,7 +15,7 @@ export async function getTaskHistory(controller: Controller, request: GetTaskHis // Get task history from global state const taskHistory = ((await getGlobalState(controller.context, "taskHistory")) as any[]) || [] - const workspacePath = getWorkspacePath() + const workspacePath = await getWorkspacePath() // Apply filters let filteredTasks = taskHistory.filter((item) => { diff --git a/src/core/task/ToolExecutor.ts b/src/core/task/ToolExecutor.ts index 6507b39e3c..777b7e1082 100644 --- a/src/core/task/ToolExecutor.ts +++ b/src/core/task/ToolExecutor.ts @@ -574,7 +574,7 @@ export class ToolExecutor { tool: fileExists ? "editedExistingFile" : "newFileCreated", path: getReadablePath(this.cwd, this.removeClosingTag(block, "path", relPath)), content: diff || content, - operationIsLocatedInWorkspace: isLocatedInWorkspace(relPath), + operationIsLocatedInWorkspace: await isLocatedInWorkspace(relPath), } if (block.partial) { @@ -645,7 +645,7 @@ export class ToolExecutor { const completeMessage = JSON.stringify({ ...sharedMessageProps, content: diff || content, - operationIsLocatedInWorkspace: isLocatedInWorkspace(relPath), + operationIsLocatedInWorkspace: await isLocatedInWorkspace(relPath), // ? formatResponse.createPrettyPatch( // relPath, // this.diffViewProvider.originalContent, @@ -788,7 +788,7 @@ export class ToolExecutor { const partialMessage = JSON.stringify({ ...sharedMessageProps, content: undefined, - operationIsLocatedInWorkspace: isLocatedInWorkspace(relPath), + operationIsLocatedInWorkspace: await isLocatedInWorkspace(relPath), } satisfies ClineSayTool) if (this.shouldAutoApproveToolWithPath(block.name, block.params.path)) { this.removeLastPartialMessageIfExistsWithType("ask", "tool") @@ -819,7 +819,7 @@ export class ToolExecutor { const completeMessage = JSON.stringify({ ...sharedMessageProps, content: absolutePath, - operationIsLocatedInWorkspace: isLocatedInWorkspace(relPath), + operationIsLocatedInWorkspace: await isLocatedInWorkspace(relPath), } satisfies ClineSayTool) if (this.shouldAutoApproveToolWithPath(block.name, block.params.path)) { this.removeLastPartialMessageIfExistsWithType("ask", "tool") @@ -870,7 +870,7 @@ export class ToolExecutor { const partialMessage = JSON.stringify({ ...sharedMessageProps, content: "", - operationIsLocatedInWorkspace: isLocatedInWorkspace(block.params.path), + operationIsLocatedInWorkspace: await isLocatedInWorkspace(block.params.path), } satisfies ClineSayTool) if (this.shouldAutoApproveToolWithPath(block.name, block.params.path)) { this.removeLastPartialMessageIfExistsWithType("ask", "tool") @@ -902,7 +902,7 @@ export class ToolExecutor { const completeMessage = JSON.stringify({ ...sharedMessageProps, content: result, - operationIsLocatedInWorkspace: isLocatedInWorkspace(block.params.path), + operationIsLocatedInWorkspace: await isLocatedInWorkspace(block.params.path), } satisfies ClineSayTool) if (this.shouldAutoApproveToolWithPath(block.name, block.params.path)) { this.removeLastPartialMessageIfExistsWithType("ask", "tool") @@ -945,7 +945,7 @@ export class ToolExecutor { const partialMessage = JSON.stringify({ ...sharedMessageProps, content: "", - operationIsLocatedInWorkspace: isLocatedInWorkspace(block.params.path), + operationIsLocatedInWorkspace: await isLocatedInWorkspace(block.params.path), } satisfies ClineSayTool) if (this.shouldAutoApproveToolWithPath(block.name, block.params.path)) { this.removeLastPartialMessageIfExistsWithType("ask", "tool") @@ -974,7 +974,7 @@ export class ToolExecutor { const completeMessage = JSON.stringify({ ...sharedMessageProps, content: result, - operationIsLocatedInWorkspace: isLocatedInWorkspace(block.params.path), + operationIsLocatedInWorkspace: await isLocatedInWorkspace(block.params.path), } satisfies ClineSayTool) if (this.shouldAutoApproveToolWithPath(block.name, block.params.path)) { this.removeLastPartialMessageIfExistsWithType("ask", "tool") @@ -1021,7 +1021,7 @@ export class ToolExecutor { const partialMessage = JSON.stringify({ ...sharedMessageProps, content: "", - operationIsLocatedInWorkspace: isLocatedInWorkspace(block.params.path), + operationIsLocatedInWorkspace: await isLocatedInWorkspace(block.params.path), } satisfies ClineSayTool) if (this.shouldAutoApproveToolWithPath(block.name, block.params.path)) { this.removeLastPartialMessageIfExistsWithType("ask", "tool") @@ -1058,7 +1058,7 @@ export class ToolExecutor { const completeMessage = JSON.stringify({ ...sharedMessageProps, content: results, - operationIsLocatedInWorkspace: isLocatedInWorkspace(block.params.path), + operationIsLocatedInWorkspace: await isLocatedInWorkspace(block.params.path), } satisfies ClineSayTool) if (this.shouldAutoApproveToolWithPath(block.name, block.params.path)) { this.removeLastPartialMessageIfExistsWithType("ask", "tool") diff --git a/src/integrations/workspace/WorkspaceTracker.ts b/src/integrations/workspace/WorkspaceTracker.ts index c64ce321cf..7a589dbc81 100644 --- a/src/integrations/workspace/WorkspaceTracker.ts +++ b/src/integrations/workspace/WorkspaceTracker.ts @@ -2,13 +2,22 @@ import * as vscode from "vscode" import * as path from "path" import { listFiles } from "@services/glob/list-files" import { sendWorkspaceUpdateEvent } from "@core/controller/file/subscribeToWorkspaceUpdates" - -const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) +import { getCwd } from "@/utils/path" // Note: this is not a drop-in replacement for listFiles at the start of tasks, since that will be done for Desktops when there is no workspace selected class WorkspaceTracker { private disposables: vscode.Disposable[] = [] private filePaths: Set = new Set() + private cwd: string = "" + + constructor() { + this.initializeCwd() + this.registerListeners() + } + + private async initializeCwd() { + this.cwd = await getCwd() + } private get activeFiles() { return new Set( @@ -18,16 +27,12 @@ class WorkspaceTracker { ) } - constructor() { - this.registerListeners() - } - async populateFilePaths() { // should not auto get filepaths for desktop since it would immediately show permission popup before cline ever creates a file - if (!cwd) { + if (!this.cwd) { return } - const [files, _] = await listFiles(cwd, true, 1_000) + const [files, _] = await listFiles(this.cwd, true, 1_000) files.forEach((file) => this.filePaths.add(this.normalizeFilePath(file))) this.workspaceDidUpdate() } @@ -91,18 +96,18 @@ class WorkspaceTracker { } private async workspaceDidUpdate() { - if (!cwd) { + if (!this.cwd) { return } const filePaths = Array.from(new Set([...this.activeFiles, ...this.filePaths])).map((file) => { - const relativePath = path.relative(cwd, file).toPosix() + const relativePath = path.relative(this.cwd, file).toPosix() return file.endsWith("/") ? relativePath + "/" : relativePath }) await sendWorkspaceUpdateEvent(filePaths) } private normalizeFilePath(filePath: string): string { - const resolvedPath = cwd ? path.resolve(cwd, filePath) : path.resolve(filePath) + const resolvedPath = this.cwd ? path.resolve(this.cwd, filePath) : path.resolve(filePath) return filePath.endsWith("/") ? resolvedPath + "/" : resolvedPath } diff --git a/src/utils/path.ts b/src/utils/path.ts index 4a1aee4cd2..fd1f85e714 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 { getHostBridgeProvider } from "@/hosts/host-providers" /* The Node.js 'path' module resolves and normalizes paths differently depending on the platform: @@ -101,9 +102,17 @@ export function getReadablePath(cwd: string, relPath?: string): string { } } -export const getWorkspacePath = (defaultCwdPath = "") => { - const cwdPath = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) || defaultCwdPath +// Returns the path of the first workspace directory, or the defaultCwdPath if there is no workspace open. +export const getCwd = async (defaultCwdPath = ""): Promise => { + const workspaceFolders = await getHostBridgeProvider().workspaceClient.getWorkspacePaths({}) + return workspaceFolders.paths.shift() || defaultCwdPath +} + +// Returns the workspace path of the file in the current editor. +// If there is no path, it returns the top level workspace directory. +export const getWorkspacePath = async (defaultCwdPath = "") => { const currentFileUri = vscode.window.activeTextEditor?.document.uri + const cwdPath = await getCwd(defaultCwdPath) if (currentFileUri) { const workspaceFolder = vscode.workspace.getWorkspaceFolder(currentFileUri) return workspaceFolder?.uri.fsPath || cwdPath @@ -111,8 +120,8 @@ export const getWorkspacePath = (defaultCwdPath = "") => { return cwdPath } -export const isLocatedInWorkspace = (pathToCheck: string = ""): boolean => { - const workspacePath = getWorkspacePath() +export const isLocatedInWorkspace = async (pathToCheck: string = ""): Promise => { + const workspacePath = await getWorkspacePath() // Handle long paths in Windows if (pathToCheck.startsWith("\\\\?\\") || workspacePath.startsWith("\\\\?\\")) { diff --git a/test-setup.js b/test-setup.js index a096b73744..bdeab16dd8 100644 --- a/test-setup.js +++ b/test-setup.js @@ -16,7 +16,7 @@ const tsConfig = JSON.parse(fs.readFileSync(path.join(baseUrl, "tsconfig.json"), const outPaths = {} Object.keys(tsConfig.compilerOptions.paths).forEach((key) => { const value = tsConfig.compilerOptions.paths[key] - outPaths[key] = value.map((path) => path.replace("src", "out")) + outPaths[key] = value.map((path) => path.replace("src", "out/src")) }) tsConfigPaths.register({ diff --git a/tsconfig.test.json b/tsconfig.test.json index 4699ca7e62..fef4465be6 100644 --- a/tsconfig.test.json +++ b/tsconfig.test.json @@ -11,7 +11,7 @@ "types": ["node", "mocha", "should", "vscode", "chai"], "typeRoots": ["./node_modules/@types", "./src/test/types"], "outDir": "out", - "rootDir": "src" + "rootDir": "." }, "include": ["src/**/*.test.ts"], "exclude": ["src/test/**/*.js", "src/**/__tests__/*"]