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,
+ })
+}