diff --git a/src/core/Cline.ts b/src/core/Cline.ts index 652c0ebf33..7a290412a7 100644 --- a/src/core/Cline.ts +++ b/src/core/Cline.ts @@ -2481,12 +2481,14 @@ export class Cline { case "open_cursor": { const mode: string | undefined = block.params.mode const prompt: string | undefined = block.params.prompt + const projectDir: string | undefined = block.params.projectDir try { if (block.partial) { const partialMessage = JSON.stringify({ tool: "openCursor", mode: removeClosingTag("mode", mode), prompt: removeClosingTag("prompt", prompt), + projectDir: removeClosingTag("projectDir", projectDir), }) await this.ask("tool", partialMessage, block.partial).catch(() => {}) break @@ -2501,11 +2503,18 @@ export class Cline { pushToolResult(await this.sayAndCreateMissingParamError("open_cursor", "prompt")) break } + if (!projectDir) { + this.consecutiveMistakeCount++ + pushToolResult( + await this.sayAndCreateMissingParamError("open_cursor", "projectDir"), + ) + break + } this.consecutiveMistakeCount = 0 const provider = this.providerRef.deref() if (provider) { - await provider.openCursorInstance(prompt, mode) + await provider.openCursorInstance(prompt, mode, projectDir) pushToolResult( `Successfully opened new Cursor instance in ${mode} mode with prompt: ${prompt}`, ) diff --git a/src/core/assistant-message/index.ts b/src/core/assistant-message/index.ts index 7b7e8bfb23..883303a01d 100644 --- a/src/core/assistant-message/index.ts +++ b/src/core/assistant-message/index.ts @@ -59,6 +59,7 @@ export const toolParamNames = [ "message", "prompt", "monitor", + "projectDir", ] as const export type ToolParamName = (typeof toolParamNames)[number] diff --git a/src/core/prompts/tools/open_cursor.ts b/src/core/prompts/tools/open_cursor.ts index 6c78c20cdf..58c069aca9 100644 --- a/src/core/prompts/tools/open_cursor.ts +++ b/src/core/prompts/tools/open_cursor.ts @@ -9,17 +9,20 @@ Parameters: - prompt: (required) The initial prompt or instructions to send to the new Cursor instance. - mode: (optional) The mode to start the new instance in (e.g., "code", "ask", "architect"). If not provided, will use the current mode. - monitor: (optional) Whether to monitor the task's progress. Defaults to true. +- projectDir: (optional) The directory to open Cursor in. If not provided, will use the current project directory. Usage: Your initial prompt here Mode to start in (optional) +Project directory to open Cursor in (optional) Example: Create a new React component code +/Users/rooCode/Projects/Roo-Code ` } diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 7a61795b8c..9eca4ef2b0 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -1,7 +1,7 @@ import { Anthropic } from "@anthropic-ai/sdk" import delay from "delay" import axios from "axios" -import fs from "fs/promises" +import fs, { cp } from "fs/promises" import os from "os" import pWaitFor from "p-wait-for" import * as path from "path" @@ -44,7 +44,8 @@ import { EXPERIMENT_IDS, experiments as Experiments, experimentDefault, Experime import { CustomSupportPrompts, supportPrompt } from "../../shared/support-prompt" import { ACTION_NAMES } from "../CodeActionProvider" - +import { spawn } from "child_process" +import { openCursorInstance, startRooCodeTask } from "./openCursorInstance" /* https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts @@ -1413,26 +1414,12 @@ export class ClineProvider implements vscode.WebviewViewProvider { await this.postStateToWebview() } - public async openCursorInstance(prompt: string, mode: string) { - // Construct command to open new Cursor instance - const cursorCommand = { - command: "workbench.action.newWindow", - args: [ - { - mode, - prompt, - }, - ], - } - - // Execute command to open new window - const result = await vscode.commands.executeCommand(cursorCommand.command, ...cursorCommand.args) - console.log("result", result) - const windows = vscode.window.state.active - console.log("windows", windows) - await vscode.commands.executeCommand("workbench.view.extension.roo-cline-ActivityBar") - await this.postMessageToWebview({ type: "action", action: "openCursorInstance" }) - await this.postStateToWebview() + public async openCursorInstance(prompt: string, mode: string, projectDir: string) { + const workingDir = + projectDir || vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) || "" + const instanceId = await openCursorInstance(prompt, mode, workingDir) + await startRooCodeTask(instanceId, prompt, mode) + this.outputChannel.appendLine(`Cursor instance opened in ${workingDir}`) } private async updateApiConfiguration(apiConfiguration: ApiConfiguration) { diff --git a/src/core/webview/openCursorInstance.ts b/src/core/webview/openCursorInstance.ts new file mode 100644 index 0000000000..e0034d6c52 --- /dev/null +++ b/src/core/webview/openCursorInstance.ts @@ -0,0 +1,169 @@ +import { spawn, ChildProcess } from "child_process" +import * as path from "path" +import * as vscode from "vscode" +import * as fs from "fs/promises" + +interface CursorInstance { + process: ChildProcess + projectDir: string + prompt: string + mode?: string +} + +// Track active cursor instances +const activeCursorInstances: Map = new Map() + +/** + * Gets the path to the Cursor application + * @returns Promise<{command: string, args: string[]}> Command and args to launch Cursor + */ +async function getCursorLaunchCommand(projectDir: string): Promise<{ command: string; args: string[] }> { + // Use code command with Cursor profile + return { + command: "cursor", + args: ["--new-window", projectDir], + } +} + +/** + * Opens a new instance of Cursor at a specific project directory + * @param prompt The initial prompt to send to Roo Code + * @param mode Optional mode to start in (code, ask, architect) + * @param projectDir The directory to open Cursor in + * @returns Promise Instance ID if successful + */ +export async function openCursorInstance(prompt: string, mode: string = "code", projectDir: string): Promise { + try { + // Ensure project directory exists + const normalizedPath = path.normalize(projectDir) + await fs.access(normalizedPath) + + // Generate unique instance ID + const instanceId = `cursor-${Date.now()}-${Math.random().toString(36).substr(2, 9)}` + + // Get platform-specific launch command + const { command, args } = await getCursorLaunchCommand(normalizedPath) + + // Spawn process with inherited stdio + const cursorProcess = spawn(command, args, { + detached: false, + stdio: "inherit", + }) + + // Handle process error + cursorProcess.on("error", (error) => { + console.error(`Error launching Cursor instance ${instanceId}:`, error) + activeCursorInstances.delete(instanceId) + throw error + }) + + // Store instance information + const instance: CursorInstance = { + process: cursorProcess, + projectDir: normalizedPath, + prompt, + mode, + } + + activeCursorInstances.set(instanceId, instance) + + console.log(instance) + cursorProcess.stdin?.write(prompt) + + // Show information message + vscode.window.showInformationMessage(`New Cursor instance opened at ${normalizedPath}`) + + return instanceId + } catch (error) { + console.error("Failed to open Cursor instance:", error) + if (error instanceof Error) { + vscode.window.showErrorMessage(`Failed to open Cursor: ${error.message}`) + } + throw error + } +} + +/** + * Get all active cursor instances + * @returns Map of active cursor instances + */ +export function getActiveCursorInstances(): Map { + return new Map(activeCursorInstances) +} + +/** + * Close a specific cursor instance + * @param instanceId The ID of the instance to close + * @returns boolean indicating if the instance was successfully closed + */ +export function closeCursorInstance(instanceId: string): boolean { + const instance = activeCursorInstances.get(instanceId) + if (!instance) { + return false + } + + try { + instance.process.kill() + activeCursorInstances.delete(instanceId) + return true + } catch (error) { + console.error(`Error closing Cursor instance ${instanceId}:`, error) + return false + } +} + +/** + * Close all active cursor instances + */ +export function closeAllCursorInstances(): void { + for (const [instanceId] of activeCursorInstances) { + closeCursorInstance(instanceId) + } +} + +/** + * Send a command to the Roo Code extension in a specific Cursor instance + * @param instanceId The ID of the Cursor instance to send the command to + * @param command The Roo Code command to execute (e.g., 'startNewTask') + * @param args Optional arguments for the command + * @returns Promise + */ +export async function sendRooCodeCommand(instanceId: string, command: string, args?: any): Promise { + const instance = activeCursorInstances.get(instanceId) + if (!instance) { + throw new Error(`No active Cursor instance found with ID: ${instanceId}`) + } + + try { + // Wait for Cursor to be ready + await new Promise((resolve) => setTimeout(resolve, 2000)) + + // Focus the window first + await vscode.commands.executeCommand("vscode.openFolder", vscode.Uri.file(instance.projectDir), true) + await new Promise((resolve) => setTimeout(resolve, 1000)) + + // Send the command + await vscode.commands.executeCommand(command, args) + + // Log command for debugging + console.log(`Executed command in Cursor instance ${instanceId}:`, command, args) + } catch (error) { + console.error(`Failed to send command to Cursor instance ${instanceId}:`, error) + throw error + } +} + +/** + * Start a new Roo Code task in a specific Cursor instance + * @param instanceId The ID of the Cursor instance + * @param prompt The prompt for the new task + * @param mode Optional mode to start in (code, ask, architect) + * @returns Promise + */ +export async function startRooCodeTask(instanceId: string, prompt: string, mode: string = "code"): Promise { + await sendRooCodeCommand(instanceId, "roo-cline.startTask", { + prompt, + mode, + projectDir: activeCursorInstances.get(instanceId)?.projectDir, + }) +}