Explorar o código

Allow processes to access the Roo Code API via a unix socket (#2232)

Chris Estreich hai 9 meses
pai
achega
a73fce9027

+ 1 - 0
.tool-versions

@@ -0,0 +1 @@
+nodejs v20.18.1

+ 3 - 3
e2e/VSCODE_INTEGRATION_TESTS.md

@@ -156,7 +156,7 @@ while (Date.now() - startTime < timeout) {
 6. **Grading**: When grading tests, use the `Grade:` format to ensure the test is graded correctly (See modes.test.ts for an example).
 
 ```typescript
-await globalThis.api.startNewTask(
-	`Given this prompt: ${testPrompt} grade the response from 1 to 10 in the format of "Grade: (1-10)": ${output} \n Be sure to say 'I AM DONE GRADING' after the task is complete`,
-)
+await globalThis.api.startNewTask({
+	text: `Given this prompt: ${testPrompt} grade the response from 1 to 10 in the format of "Grade: (1-10)": ${output} \n Be sure to say 'I AM DONE GRADING' after the task is complete`,
+})
 ```

+ 4 - 5
e2e/package.json

@@ -3,13 +3,12 @@
 	"version": "0.1.0",
 	"private": true,
 	"scripts": {
-		"build": "cd .. && npm run compile && npm run build:webview",
-		"compile": "rm -rf out && tsc -p tsconfig.json",
 		"lint": "eslint src --ext ts",
 		"check-types": "tsc --noEmit",
-		"test": "npm run compile && npx dotenvx run -f .env.local -- node ./out/runTest.js",
-		"ci": "npm run build && npm run test",
-		"clean": "rimraf out"
+		"test": "npm run build && npx dotenvx run -f .env.local -- node ./out/runTest.js",
+		"ci": "npm run vscode-test && npm run test",
+		"build": "rimraf out && tsc -p tsconfig.json",
+		"vscode-test": "cd .. && npm run vscode-test"
 	},
 	"dependencies": {},
 	"devDependencies": {

+ 0 - 6
e2e/src/suite/extension.test.ts

@@ -2,12 +2,6 @@ import * as assert from "assert"
 import * as vscode from "vscode"
 
 suite("Roo Code Extension", () => {
-	test("OPENROUTER_API_KEY environment variable is set", () => {
-		if (!process.env.OPENROUTER_API_KEY) {
-			assert.fail("OPENROUTER_API_KEY environment variable is not set")
-		}
-	})
-
 	test("Commands should be registered", async () => {
 		const expectedCommands = [
 			"roo-cline.plusButtonClicked",

+ 15 - 8
e2e/src/suite/index.ts

@@ -3,9 +3,9 @@ import Mocha from "mocha"
 import { glob } from "glob"
 import * as vscode from "vscode"
 
-import { RooCodeAPI } from "../../../src/exports/roo-code"
+import type { RooCodeAPI } from "../../../src/exports/roo-code"
 
-import { waitUntilReady } from "./utils"
+import { waitFor } from "./utils"
 
 declare global {
 	var api: RooCodeAPI
@@ -18,18 +18,25 @@ export async function run() {
 		throw new Error("Extension not found")
 	}
 
-	// Activate the extension if it's not already active.
 	const api = extension.isActive ? extension.exports : await extension.activate()
 
-	// TODO: We might want to support a "free" model out of the box so
-	// contributors can run the tests locally without having to pay.
 	await api.setConfiguration({
-		apiProvider: "openrouter",
+		apiProvider: "openrouter" as const,
 		openRouterApiKey: process.env.OPENROUTER_API_KEY!,
-		openRouterModelId: "anthropic/claude-3.5-sonnet",
+		openRouterModelId: "google/gemini-2.0-flash-001",
+		openRouterModelInfo: {
+			maxTokens: 8192,
+			contextWindow: 1000000,
+			supportsImages: true,
+			supportsPromptCache: false,
+			inputPrice: 0.1,
+			outputPrice: 0.4,
+			thinking: false,
+		},
 	})
 
-	await waitUntilReady({ api })
+	await vscode.commands.executeCommand("roo-cline.SidebarProvider.focus")
+	await waitFor(() => api.isReady())
 
 	// Expose the API to the tests.
 	globalThis.api = api

+ 33 - 17
e2e/src/suite/modes.test.ts

@@ -1,9 +1,11 @@
 import * as assert from "assert"
 
-import { getCompletion, getMessage, sleep, waitForCompletion, waitUntilAborted } from "./utils"
+import type { ClineMessage } from "../../../src/exports/roo-code"
+
+import { waitUntilCompleted } from "./utils"
 
 suite("Roo Code Modes", () => {
-	test("Should handle switching modes correctly", async function () {
+	test("Should handle switching modes correctly", async () => {
 		const api = globalThis.api
 
 		/**
@@ -14,30 +16,44 @@ suite("Roo Code Modes", () => {
 			"For each mode (Code, Architect, Ask) respond with the mode name and what it specializes in after switching to that mode. " +
 			"Do not start with the current mode."
 
-		await api.setConfiguration({ mode: "Code", alwaysAllowModeSwitch: true, autoApprovalEnabled: true })
-		const switchModesTaskId = await api.startNewTask(switchModesPrompt)
-		await waitForCompletion({ api, taskId: switchModesTaskId, timeout: 60_000 })
+		let messages: ClineMessage[] = []
+
+		api.on("message", ({ message }) => {
+			if (message.type === "say" && message.partial === false) {
+				messages.push(message)
+			}
+		})
+
+		const switchModesTaskId = await api.startNewTask({
+			configuration: { mode: "Code", alwaysAllowModeSwitch: true, autoApprovalEnabled: true },
+			text: switchModesPrompt,
+		})
+
+		await waitUntilCompleted({ api, taskId: switchModesTaskId, timeout: 60_000 })
 
 		/**
 		 * Grade the response.
 		 */
 
-		const gradePrompt =
-			`Given this prompt: ${switchModesPrompt} grade the response from 1 to 10 in the format of "Grade: (1-10)": ` +
-			api
-				.getMessages(switchModesTaskId)
-				.filter(({ type }) => type === "say")
-				.map(({ text }) => text ?? "")
-				.join("\n")
+		const response = messages
+			.filter(({ type, say, partial }) => say === "text")
+			.map(({ text }) => text ?? "")
+			.join("\n")
+
+		const gradePrompt = `Given this prompt: ${switchModesPrompt} grade the response from 1 to 10 in the format of "Grade: (1-10)". For example: Grade 7\n\nResponse: ${response}`
+
+		messages = []
 
-		await api.setConfiguration({ mode: "Ask" })
-		const gradeTaskId = await api.startNewTask(gradePrompt)
-		await waitForCompletion({ api, taskId: gradeTaskId, timeout: 60_000 })
+		const gradeTaskId = await api.startNewTask({ configuration: { mode: "Ask" }, text: gradePrompt })
+		await waitUntilCompleted({ api, taskId: gradeTaskId })
 
-		const completion = getCompletion({ api, taskId: gradeTaskId })
+		const completion = messages.find(({ type, say, partial }) => say === "completion_result")
 		const match = completion?.text?.match(/Grade: (\d+)/)
 		const score = parseInt(match?.[1] ?? "0")
-		assert.ok(score >= 7 && score <= 10, `Grade must be between 7 and 10 - ${completion?.text}`)
+		assert.ok(
+			score >= 7 && score <= 10,
+			`Grade must be between 7 and 10. DEBUG: score = ${score}, completion = ${completion?.text}`,
+		)
 
 		await api.cancelCurrentTask()
 	})

+ 26 - 22
e2e/src/suite/subtasks.test.ts

@@ -1,13 +1,24 @@
 import * as assert from "assert"
 
-import { sleep, waitFor, getMessage, waitForCompletion } from "./utils"
+import type { ClineMessage } from "../../../src/exports/roo-code"
+
+import { sleep, waitFor, waitUntilCompleted } from "./utils"
 
 suite("Roo Code Subtasks", () => {
-	test("Should handle subtask cancellation and resumption correctly", async function () {
+	test("Should handle subtask cancellation and resumption correctly", async () => {
 		const api = globalThis.api
 
+		const messages: Record<string, ClineMessage[]> = {}
+
+		api.on("message", ({ taskId, message }) => {
+			if (message.type === "say" && message.partial === false) {
+				messages[taskId] = messages[taskId] || []
+				messages[taskId].push(message)
+			}
+		})
+
 		await api.setConfiguration({
-			mode: "Code",
+			mode: "ask",
 			alwaysAllowModeSwitch: true,
 			alwaysAllowSubtasks: true,
 			autoApprovalEnabled: true,
@@ -17,18 +28,19 @@ suite("Roo Code Subtasks", () => {
 		const childPrompt = "You are a calculator. Respond only with numbers. What is the square root of 9?"
 
 		// Start a parent task that will create a subtask.
-		const parentTaskId = await api.startNewTask(
-			"You are the parent task. " +
+		const parentTaskId = await api.startNewTask({
+			text:
+				"You are the parent task. " +
 				`Create a subtask by using the new_task tool with the message '${childPrompt}'.` +
 				"After creating the subtask, wait for it to complete and then respond 'Parent task resumed'.",
-		)
+		})
 
 		let spawnedTaskId: string | undefined = undefined
 
 		// Wait for the subtask to be spawned and then cancel it.
 		api.on("taskSpawned", (_, childTaskId) => (spawnedTaskId = childTaskId))
 		await waitFor(() => !!spawnedTaskId)
-		await sleep(2_000) // Give the task a chance to start and populate the history.
+		await sleep(1_000) // Give the task a chance to start and populate the history.
 		await api.cancelCurrentTask()
 
 		// Wait a bit to ensure any task resumption would have happened.
@@ -37,35 +49,27 @@ suite("Roo Code Subtasks", () => {
 		// The parent task should not have resumed yet, so we shouldn't see
 		// "Parent task resumed".
 		assert.ok(
-			getMessage({
-				api,
-				taskId: parentTaskId,
-				include: "Parent task resumed",
-				exclude: "You are the parent task",
-			}) === undefined,
+			messages[parentTaskId].find(({ type, text }) => type === "say" && text === "Parent task resumed") ===
+				undefined,
 			"Parent task should not have resumed after subtask cancellation",
 		)
 
 		// Start a new task with the same message as the subtask.
-		const anotherTaskId = await api.startNewTask(childPrompt)
-		await waitForCompletion({ api, taskId: anotherTaskId })
+		const anotherTaskId = await api.startNewTask({ text: childPrompt })
+		await waitUntilCompleted({ api, taskId: anotherTaskId })
 
 		// Wait a bit to ensure any task resumption would have happened.
 		await sleep(2_000)
 
 		// The parent task should still not have resumed.
 		assert.ok(
-			getMessage({
-				api,
-				taskId: parentTaskId,
-				include: "Parent task resumed",
-				exclude: "You are the parent task",
-			}) === undefined,
+			messages[parentTaskId].find(({ type, text }) => type === "say" && text === "Parent task resumed") ===
+				undefined,
 			"Parent task should not have resumed after subtask cancellation",
 		)
 
 		// Clean up - cancel all tasks.
 		await api.clearCurrentTask()
-		await waitForCompletion({ api, taskId: parentTaskId })
+		await waitUntilCompleted({ api, taskId: parentTaskId })
 	})
 })

+ 28 - 5
e2e/src/suite/task.test.ts

@@ -1,10 +1,33 @@
-import { waitForMessage } from "./utils"
+import * as assert from "assert"
+
+import type { ClineMessage } from "../../../src/exports/roo-code"
+
+import { waitUntilCompleted } from "./utils"
 
 suite("Roo Code Task", () => {
-	test("Should handle prompt and response correctly", async function () {
+	test("Should handle prompt and response correctly", async () => {
 		const api = globalThis.api
-		await api.setConfiguration({ mode: "Ask", alwaysAllowModeSwitch: true, autoApprovalEnabled: true })
-		const taskId = await api.startNewTask("Hello world, what is your name? Respond with 'My name is ...'")
-		await waitForMessage({ api, taskId, include: "My name is Roo" })
+
+		const messages: ClineMessage[] = []
+
+		api.on("message", ({ message }) => {
+			if (message.type === "say" && message.partial === false) {
+				messages.push(message)
+			}
+		})
+
+		const taskId = await api.startNewTask({
+			configuration: { mode: "Ask", alwaysAllowModeSwitch: true, autoApprovalEnabled: true },
+			text: "Hello world, what is your name? Respond with 'My name is ...'",
+		})
+
+		await waitUntilCompleted({ api, taskId })
+
+		const completion = messages.find(({ say, partial }) => say === "completion_result")
+
+		assert.ok(
+			completion?.text?.includes("My name is Roo"),
+			`Completion should include "My name is Roo" - ${completion?.text}`,
+		)
 	})
 })

+ 8 - 41
e2e/src/suite/utils.ts

@@ -1,6 +1,6 @@
 import * as vscode from "vscode"
 
-import { RooCodeAPI } from "../../../src/exports/roo-code"
+import type { RooCodeAPI } from "../../../src/exports/roo-code"
 
 type WaitForOptions = {
 	timeout?: number
@@ -9,7 +9,7 @@ type WaitForOptions = {
 
 export const waitFor = (
 	condition: (() => Promise<boolean>) | (() => boolean),
-	{ timeout = 60_000, interval = 250 }: WaitForOptions = {},
+	{ timeout = 30_000, interval = 250 }: WaitForOptions = {},
 ) => {
 	let timeoutId: NodeJS.Timeout | undefined = undefined
 
@@ -41,15 +41,6 @@ export const waitFor = (
 	])
 }
 
-type WaitUntilReadyOptions = WaitForOptions & {
-	api: RooCodeAPI
-}
-
-export const waitUntilReady = async ({ api, ...options }: WaitUntilReadyOptions) => {
-	await vscode.commands.executeCommand("roo-cline.SidebarProvider.focus")
-	await waitFor(() => api.isReady(), options)
-}
-
 type WaitUntilAbortedOptions = WaitForOptions & {
 	api: RooCodeAPI
 	taskId: string
@@ -61,39 +52,15 @@ export const waitUntilAborted = async ({ api, taskId, ...options }: WaitUntilAbo
 	await waitFor(() => set.has(taskId), options)
 }
 
-export const waitForCompletion = async ({
-	api,
-	taskId,
-	...options
-}: WaitUntilReadyOptions & {
-	taskId: string
-}) => waitFor(() => !!getCompletion({ api, taskId }), options)
-
-export const getCompletion = ({ api, taskId }: { api: RooCodeAPI; taskId: string }) =>
-	api.getMessages(taskId).find(({ say, partial }) => say === "completion_result" && partial === false)
-
-type WaitForMessageOptions = WaitUntilReadyOptions & {
-	taskId: string
-	include: string
-	exclude?: string
-}
-
-export const waitForMessage = async ({ api, taskId, include, exclude, ...options }: WaitForMessageOptions) =>
-	waitFor(() => !!getMessage({ api, taskId, include, exclude }), options)
-
-type GetMessageOptions = {
+type WaitUntilCompletedOptions = WaitForOptions & {
 	api: RooCodeAPI
 	taskId: string
-	include: string
-	exclude?: string
 }
 
-export const getMessage = ({ api, taskId, include, exclude }: GetMessageOptions) =>
-	api
-		.getMessages(taskId)
-		.find(
-			({ type, text }) =>
-				type === "say" && text && text.includes(include) && (!exclude || !text.includes(exclude)),
-		)
+export const waitUntilCompleted = async ({ api, taskId, ...options }: WaitUntilCompletedOptions) => {
+	const set = new Set<string>()
+	api.on("taskCompleted", (taskId) => set.add(taskId))
+	await waitFor(() => set.has(taskId), options)
+}
 
 export const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))

+ 1 - 0
knip.json

@@ -17,6 +17,7 @@
 		"benchmark/**",
 		"src/activate/**",
 		"src/exports/**",
+		"src/schemas/ipc.ts",
 		"src/extension.ts",
 		"scripts/**"
 	],

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 967 - 66
package-lock.json


+ 12 - 10
package.json

@@ -312,23 +312,19 @@
 		"build:webview": "cd webview-ui && npm run build",
 		"build:esbuild": "node esbuild.js --production",
 		"compile": "tsc -p . --outDir out && node esbuild.js",
-		"install:all": "npm install npm-run-all && npm run install:_all",
-		"install:_all": "npm-run-all -p install-*",
+		"install:all": "npm install npm-run-all && npm-run-all -l -p install-*",
 		"install-extension": "npm install",
 		"install-webview": "cd webview-ui && npm install",
 		"install-e2e": "cd e2e && npm install",
-		"install-benchmark": "cd benchmark && npm install",
-		"lint": "npm-run-all -p lint:*",
+		"lint": "npm-run-all -l -p lint:*",
 		"lint:extension": "eslint src --ext ts",
 		"lint:webview": "cd webview-ui && npm run lint",
 		"lint:e2e": "cd e2e && npm run lint",
-		"lint:benchmark": "cd benchmark && npm run lint",
-		"check-types": "npm-run-all -p check-types:*",
+		"check-types": "npm-run-all -l -p check-types:*",
 		"check-types:extension": "tsc --noEmit",
 		"check-types:webview": "cd webview-ui && npm run check-types",
 		"check-types:e2e": "cd e2e && npm run check-types",
-		"check-types:benchmark": "cd benchmark && npm run check-types",
-		"package": "npm-run-all -p build:webview build:esbuild check-types lint",
+		"package": "npm-run-all -l -p build:webview build:esbuild check-types lint",
 		"pretest": "npm run compile",
 		"dev": "cd webview-ui && npm run dev",
 		"test": "node scripts/run-tests.js",
@@ -340,17 +336,20 @@
 		"version-packages": "changeset version && npm install --package-lock-only",
 		"vscode:prepublish": "npm run package",
 		"vsix": "rimraf bin && mkdirp bin && npx vsce package --out bin",
-		"watch": "npm-run-all -p watch:*",
+		"watch": "npm-run-all -l -p watch:*",
 		"watch:esbuild": "node esbuild.js --watch",
 		"watch:tsc": "tsc --noEmit --watch --project tsconfig.json",
 		"watch-tests": "tsc -p . -w --outDir out",
 		"changeset": "changeset",
 		"knip": "knip --include files",
-		"clean": "npm-run-all -p clean:*",
+		"clean": "npm-run-all -l -p clean:*",
 		"clean:extension": "rimraf bin dist out",
 		"clean:webview": "cd webview-ui && npm run clean",
 		"clean:e2e": "cd e2e && npm run clean",
 		"clean:benchmark": "cd benchmark && npm run clean",
+		"vscode-test": "npm-run-all -l -p vscode-test:*",
+		"vscode-test:extension": "tsc -p . --outDir out && node esbuild.js",
+		"vscode-test:webview": "cd webview-ui && npm run build",
 		"update-contributors": "node scripts/update-contributors.js",
 		"generate-types": "tsx scripts/generate-types.mts"
 	},
@@ -388,6 +387,7 @@
 		"js-tiktoken": "^1.0.19",
 		"mammoth": "^1.8.0",
 		"monaco-vscode-textmate-theme-converter": "^0.1.7",
+		"node-ipc": "^12.0.0",
 		"openai": "^4.78.1",
 		"os-name": "^6.0.0",
 		"p-wait-for": "^5.0.2",
@@ -421,9 +421,11 @@
 		"@types/glob": "^8.1.0",
 		"@types/jest": "^29.5.14",
 		"@types/node": "20.x",
+		"@types/node-ipc": "^9.2.3",
 		"@types/string-similarity": "^4.0.2",
 		"@typescript-eslint/eslint-plugin": "^7.14.1",
 		"@typescript-eslint/parser": "^7.11.0",
+		"@vscode/vsce": "^3.3.2",
 		"esbuild": "^0.24.0",
 		"eslint": "^8.57.0",
 		"execa": "^9.5.2",

+ 3 - 1
src/activate/registerCommands.ts

@@ -95,7 +95,7 @@ const getCommandsMap = ({ context, outputChannel, provider }: RegisterCommandOpt
 	}
 }
 
-const openClineInNewTab = async ({ context, outputChannel }: Omit<RegisterCommandOptions, "provider">) => {
+export const openClineInNewTab = async ({ context, outputChannel }: Omit<RegisterCommandOptions, "provider">) => {
 	// (This example uses webviewProvider activation event which is necessary to
 	// deserialize cached webview, but since we use retainContextWhenHidden, we
 	// don't need to use that event).
@@ -139,4 +139,6 @@ const openClineInNewTab = async ({ context, outputChannel }: Omit<RegisterComman
 	// Lock the editor group so clicking on files doesn't open them over the panel.
 	await delay(100)
 	await vscode.commands.executeCommand("workbench.action.lockEditorGroup")
+
+	return tabProvider
 }

+ 153 - 57
src/exports/api.ts

@@ -2,105 +2,201 @@ import { EventEmitter } from "events"
 import * as vscode from "vscode"
 
 import { ClineProvider } from "../core/webview/ClineProvider"
+import { openClineInNewTab } from "../activate/registerCommands"
 
-import { RooCodeAPI, RooCodeEvents, TokenUsage, RooCodeSettings } from "./roo-code"
-import { MessageHistory } from "./message-history"
+import { RooCodeSettings, RooCodeEvents, RooCodeEventName, ClineMessage } from "../schemas"
+import { IpcOrigin, IpcMessageType, TaskCommandName, TaskEvent } from "../schemas/ipc"
+import { RooCodeAPI } from "./interface"
+import { IpcServer } from "./ipc"
+import { outputChannelLog } from "./log"
 
 export class API extends EventEmitter<RooCodeEvents> implements RooCodeAPI {
 	private readonly outputChannel: vscode.OutputChannel
-	private readonly provider: ClineProvider
-	private readonly history: MessageHistory
-	private readonly tokenUsage: Record<string, TokenUsage>
-
-	constructor(outputChannel: vscode.OutputChannel, provider: ClineProvider) {
+	private readonly sidebarProvider: ClineProvider
+	private tabProvider?: ClineProvider
+	private readonly context: vscode.ExtensionContext
+	private readonly ipc?: IpcServer
+	private readonly taskMap = new Map<string, ClineProvider>()
+	private readonly log: (...args: unknown[]) => void
+
+	constructor(
+		outputChannel: vscode.OutputChannel,
+		provider: ClineProvider,
+		socketPath?: string,
+		enableLogging = false,
+	) {
 		super()
 
 		this.outputChannel = outputChannel
-		this.provider = provider
-		this.history = new MessageHistory()
-		this.tokenUsage = {}
-
-		this.provider.on("clineCreated", (cline) => {
-			cline.on("message", (message) => this.emit("message", { taskId: cline.taskId, ...message }))
-			cline.on("taskStarted", () => this.emit("taskStarted", cline.taskId))
-			cline.on("taskPaused", () => this.emit("taskPaused", cline.taskId))
-			cline.on("taskUnpaused", () => this.emit("taskUnpaused", cline.taskId))
-			cline.on("taskAskResponded", () => this.emit("taskAskResponded", cline.taskId))
-			cline.on("taskAborted", () => this.emit("taskAborted", cline.taskId))
-			cline.on("taskSpawned", (childTaskId) => this.emit("taskSpawned", cline.taskId, childTaskId))
-			cline.on("taskCompleted", (_, usage) => this.emit("taskCompleted", cline.taskId, usage))
-			cline.on("taskTokenUsageUpdated", (_, usage) => this.emit("taskTokenUsageUpdated", cline.taskId, usage))
-			this.emit("taskCreated", cline.taskId)
-		})
+		this.sidebarProvider = provider
+		this.context = provider.context
+
+		this.log = enableLogging
+			? (...args: unknown[]) => {
+					outputChannelLog(this.outputChannel, ...args)
+					console.log(args)
+				}
+			: () => {}
+
+		this.registerListeners(this.sidebarProvider)
+
+		if (socketPath) {
+			const ipc = (this.ipc = new IpcServer(socketPath, this.log))
+
+			ipc.listen()
+			this.log(`[API] ipc server started: socketPath=${socketPath}, pid=${process.pid}, ppid=${process.ppid}`)
+
+			ipc.on(IpcMessageType.TaskCommand, async (_clientId, { commandName, data }) => {
+				switch (commandName) {
+					case TaskCommandName.StartNewTask:
+						this.log(`[API] StartNewTask -> ${data.text}, ${JSON.stringify(data.configuration)}`)
+						await this.startNewTask(data)
+						break
+					case TaskCommandName.CancelTask:
+						this.log(`[API] CancelTask -> ${data}`)
+						await this.cancelTask(data)
+						break
+					case TaskCommandName.CloseTask:
+						this.log(`[API] CloseTask -> ${data}`)
+						await vscode.commands.executeCommand("workbench.action.files.saveFiles")
+						await vscode.commands.executeCommand("workbench.action.closeWindow")
+						break
+				}
+			})
+		}
+	}
 
-		this.on("message", ({ taskId, action, message }) => {
-			if (action === "created") {
-				this.history.add(taskId, message)
-			} else if (action === "updated") {
-				this.history.update(taskId, message)
+	public override emit<K extends keyof RooCodeEvents>(
+		eventName: K,
+		...args: K extends keyof RooCodeEvents ? RooCodeEvents[K] : never
+	) {
+		const data = { eventName: eventName as RooCodeEventName, payload: args } as TaskEvent
+		this.ipc?.broadcast({ type: IpcMessageType.TaskEvent, origin: IpcOrigin.Server, data })
+		return super.emit(eventName, ...args)
+	}
+
+	public async startNewTask({
+		configuration,
+		text,
+		images,
+		newTab,
+	}: {
+		configuration: RooCodeSettings
+		text?: string
+		images?: string[]
+		newTab?: boolean
+	}) {
+		let provider: ClineProvider
+
+		if (newTab) {
+			await vscode.commands.executeCommand("workbench.action.closeAllEditors")
+
+			if (!this.tabProvider) {
+				this.tabProvider = await openClineInNewTab({ context: this.context, outputChannel: this.outputChannel })
+				this.registerListeners(this.tabProvider)
 			}
-		})
 
-		this.on("taskTokenUsageUpdated", (taskId, usage) => (this.tokenUsage[taskId] = usage))
-	}
+			provider = this.tabProvider
+		} else {
+			provider = this.sidebarProvider
+		}
+
+		if (configuration) {
+			await provider.setValues(configuration)
 
-	public async startNewTask(text?: string, images?: string[]) {
-		await this.provider.removeClineFromStack()
-		await this.provider.postStateToWebview()
-		await this.provider.postMessageToWebview({ type: "action", action: "chatButtonClicked" })
-		await this.provider.postMessageToWebview({ type: "invoke", invoke: "newChat", text, images })
+			if (configuration.allowedCommands) {
+				await vscode.workspace
+					.getConfiguration("roo-cline")
+					.update("allowedCommands", configuration.allowedCommands, vscode.ConfigurationTarget.Global)
+			}
+		}
 
-		const cline = await this.provider.initClineWithTask(text, images)
-		return cline.taskId
+		await provider.removeClineFromStack()
+		await provider.postStateToWebview()
+		await provider.postMessageToWebview({ type: "action", action: "chatButtonClicked" })
+		await provider.postMessageToWebview({ type: "invoke", invoke: "newChat", text, images })
+
+		const { taskId } = await provider.initClineWithTask(text, images)
+		return taskId
 	}
 
 	public getCurrentTaskStack() {
-		return this.provider.getCurrentTaskStack()
+		return this.sidebarProvider.getCurrentTaskStack()
 	}
 
 	public async clearCurrentTask(lastMessage?: string) {
-		await this.provider.finishSubTask(lastMessage)
-		await this.provider.postStateToWebview()
+		await this.sidebarProvider.finishSubTask(lastMessage)
+		await this.sidebarProvider.postStateToWebview()
 	}
 
 	public async cancelCurrentTask() {
-		await this.provider.cancelTask()
+		await this.sidebarProvider.cancelTask()
+	}
+
+	public async cancelTask(taskId: string) {
+		const provider = this.taskMap.get(taskId)
+
+		if (provider) {
+			await provider.cancelTask()
+			this.taskMap.delete(taskId)
+		}
 	}
 
 	public async sendMessage(text?: string, images?: string[]) {
-		await this.provider.postMessageToWebview({ type: "invoke", invoke: "sendMessage", text, images })
+		await this.sidebarProvider.postMessageToWebview({ type: "invoke", invoke: "sendMessage", text, images })
 	}
 
 	public async pressPrimaryButton() {
-		await this.provider.postMessageToWebview({ type: "invoke", invoke: "primaryButtonClick" })
+		await this.sidebarProvider.postMessageToWebview({ type: "invoke", invoke: "primaryButtonClick" })
 	}
 
 	public async pressSecondaryButton() {
-		await this.provider.postMessageToWebview({ type: "invoke", invoke: "secondaryButtonClick" })
+		await this.sidebarProvider.postMessageToWebview({ type: "invoke", invoke: "secondaryButtonClick" })
 	}
 
 	public getConfiguration() {
-		return this.provider.getValues()
+		return this.sidebarProvider.getValues()
 	}
 
 	public async setConfiguration(values: RooCodeSettings) {
-		await this.provider.setValues(values)
-		await this.provider.postStateToWebview()
+		await this.sidebarProvider.setValues(values)
+		await this.sidebarProvider.postStateToWebview()
 	}
 
 	public isReady() {
-		return this.provider.viewLaunched
+		return this.sidebarProvider.viewLaunched
 	}
 
-	public getMessages(taskId: string) {
-		return this.history.getMessages(taskId)
-	}
+	private registerListeners(provider: ClineProvider) {
+		provider.on("clineCreated", (cline) => {
+			cline.on("taskStarted", () => {
+				this.emit(RooCodeEventName.TaskStarted, cline.taskId)
+				this.taskMap.set(cline.taskId, provider)
+			})
 
-	public getTokenUsage(taskId: string) {
-		return this.tokenUsage[taskId]
-	}
+			cline.on("message", (message) => this.emit(RooCodeEventName.Message, { taskId: cline.taskId, ...message }))
+
+			cline.on("taskTokenUsageUpdated", (_, usage) =>
+				this.emit(RooCodeEventName.TaskTokenUsageUpdated, cline.taskId, usage),
+			)
 
-	public log(message: string) {
-		this.outputChannel.appendLine(message)
+			cline.on("taskAskResponded", () => this.emit(RooCodeEventName.TaskAskResponded, cline.taskId))
+
+			cline.on("taskAborted", () => {
+				this.emit(RooCodeEventName.TaskAborted, cline.taskId)
+				this.taskMap.delete(cline.taskId)
+			})
+
+			cline.on("taskCompleted", (_, usage) => {
+				this.emit(RooCodeEventName.TaskCompleted, cline.taskId, usage)
+				this.taskMap.delete(cline.taskId)
+			})
+
+			cline.on("taskSpawned", (childTaskId) => this.emit(RooCodeEventName.TaskSpawned, cline.taskId, childTaskId))
+			cline.on("taskPaused", () => this.emit(RooCodeEventName.TaskPaused, cline.taskId))
+			cline.on("taskUnpaused", () => this.emit(RooCodeEventName.TaskUnpaused, cline.taskId))
+
+			this.emit(RooCodeEventName.TaskCreated, cline.taskId)
+		})
 	}
 }

+ 16 - 37
src/exports/interface.ts

@@ -1,23 +1,12 @@
 import { EventEmitter } from "events"
 
-import type { ProviderSettings, GlobalSettings, ClineMessage, TokenUsage } from "./types"
+import type { ProviderSettings, GlobalSettings, ClineMessage, TokenUsage, RooCodeEvents } from "./types"
+export type { RooCodeSettings, ProviderSettings, GlobalSettings, ClineMessage, TokenUsage, RooCodeEvents }
 
-type RooCodeSettings = GlobalSettings & ProviderSettings
-
-export type { RooCodeSettings, ProviderSettings, GlobalSettings, ClineMessage, TokenUsage }
+import { RooCodeEventName } from "../schemas"
+export type { RooCodeEventName }
 
-export interface RooCodeEvents {
-	message: [{ taskId: string; action: "created" | "updated"; message: ClineMessage }]
-	taskCreated: [taskId: string]
-	taskStarted: [taskId: string]
-	taskPaused: [taskId: string]
-	taskUnpaused: [taskId: string]
-	taskAskResponded: [taskId: string]
-	taskAborted: [taskId: string]
-	taskSpawned: [taskId: string, childTaskId: string]
-	taskCompleted: [taskId: string, usage: TokenUsage]
-	taskTokenUsageUpdated: [taskId: string, usage: TokenUsage]
-}
+type RooCodeSettings = GlobalSettings & ProviderSettings
 
 export interface RooCodeAPI extends EventEmitter<RooCodeEvents> {
 	/**
@@ -26,7 +15,17 @@ export interface RooCodeAPI extends EventEmitter<RooCodeEvents> {
 	 * @param images Optional array of image data URIs (e.g., "data:image/webp;base64,...").
 	 * @returns The ID of the new task.
 	 */
-	startNewTask(task?: string, images?: string[]): Promise<string>
+	startNewTask({
+		configuration,
+		text,
+		images,
+		newTab,
+	}: {
+		configuration?: RooCodeSettings
+		text?: string
+		images?: string[]
+		newTab?: boolean
+	}): Promise<string>
 
 	/**
 	 * Returns the current task stack.
@@ -77,24 +76,4 @@ export interface RooCodeAPI extends EventEmitter<RooCodeEvents> {
 	 * Returns true if the API is ready to use.
 	 */
 	isReady(): boolean
-
-	/**
-	 * Returns the messages for a given task.
-	 * @param taskId The ID of the task.
-	 * @returns An array of ClineMessage objects.
-	 */
-	getMessages(taskId: string): ClineMessage[]
-
-	/**
-	 * Returns the token usage for a given task.
-	 * @param taskId The ID of the task.
-	 * @returns A TokenUsage object.
-	 */
-	getTokenUsage(taskId: string): TokenUsage
-
-	/**
-	 * Logs a message to the output channel.
-	 * @param message The message to log.
-	 */
-	log(message: string): void
 }

+ 138 - 0
src/exports/ipc.ts

@@ -0,0 +1,138 @@
+import EventEmitter from "node:events"
+import { Socket } from "node:net"
+import * as crypto from "node:crypto"
+
+import ipc from "node-ipc"
+
+import { IpcOrigin, IpcMessageType, IpcMessage, ipcMessageSchema, TaskCommand, TaskEvent } from "../schemas/ipc"
+
+/**
+ * IpcServer
+ */
+
+type IpcServerEvents = {
+	[IpcMessageType.Connect]: [clientId: string]
+	[IpcMessageType.Disconnect]: [clientId: string]
+	[IpcMessageType.TaskCommand]: [clientId: string, data: TaskCommand]
+	[IpcMessageType.TaskEvent]: [relayClientId: string | undefined, data: TaskEvent]
+}
+
+export class IpcServer extends EventEmitter<IpcServerEvents> {
+	private readonly _socketPath: string
+	private readonly _log: (...args: unknown[]) => void
+	private readonly _clients: Map<string, Socket>
+
+	private _isListening = false
+
+	constructor(socketPath: string, log = console.log) {
+		super()
+
+		this._socketPath = socketPath
+		this._log = log
+		this._clients = new Map()
+	}
+
+	public listen() {
+		this._isListening = true
+
+		ipc.config.silent = true
+
+		ipc.serve(this.socketPath, () => {
+			ipc.server.on("connect", (socket) => this.onConnect(socket))
+			ipc.server.on("socket.disconnected", (socket) => this.onDisconnect(socket))
+			ipc.server.on("message", (data) => this.onMessage(data))
+		})
+
+		ipc.server.start()
+	}
+
+	private onConnect(socket: Socket) {
+		const clientId = crypto.randomBytes(6).toString("hex")
+		this._clients.set(clientId, socket)
+		this.log(`[server#onConnect] clientId = ${clientId}, # clients = ${this._clients.size}`)
+
+		this.send(socket, {
+			type: IpcMessageType.Ack,
+			origin: IpcOrigin.Server,
+			data: { clientId, pid: process.pid, ppid: process.ppid },
+		})
+
+		this.emit(IpcMessageType.Connect, clientId)
+	}
+
+	private onDisconnect(destroyedSocket: Socket) {
+		let disconnectedClientId: string | undefined
+
+		for (const [clientId, socket] of this._clients.entries()) {
+			if (socket === destroyedSocket) {
+				disconnectedClientId = clientId
+				this._clients.delete(clientId)
+				break
+			}
+		}
+
+		this.log(`[server#socket.disconnected] clientId = ${disconnectedClientId}, # clients = ${this._clients.size}`)
+
+		if (disconnectedClientId) {
+			this.emit(IpcMessageType.Disconnect, disconnectedClientId)
+		}
+	}
+
+	private onMessage(data: unknown) {
+		if (typeof data !== "object") {
+			this.log("[server#onMessage] invalid data", data)
+			return
+		}
+
+		const result = ipcMessageSchema.safeParse(data)
+
+		if (!result.success) {
+			this.log("[server#onMessage] invalid payload", result.error.format(), data)
+			return
+		}
+
+		const payload = result.data
+
+		if (payload.origin === IpcOrigin.Client) {
+			switch (payload.type) {
+				case IpcMessageType.TaskCommand:
+					this.emit(IpcMessageType.TaskCommand, payload.clientId, payload.data)
+					break
+				default:
+					this.log(`[server#onMessage] unhandled payload: ${JSON.stringify(payload)}`)
+					break
+			}
+		}
+	}
+
+	private log(...args: unknown[]) {
+		this._log(...args)
+	}
+
+	public broadcast(message: IpcMessage) {
+		// this.log("[server#broadcast] message =", message)
+		ipc.server.broadcast("message", message)
+	}
+
+	public send(client: string | Socket, message: IpcMessage) {
+		// this.log("[server#send] message =", message)
+
+		if (typeof client === "string") {
+			const socket = this._clients.get(client)
+
+			if (socket) {
+				ipc.server.emit(socket, "message", message)
+			}
+		} else {
+			ipc.server.emit(client, "message", message)
+		}
+	}
+
+	public get socketPath() {
+		return this._socketPath
+	}
+
+	public get isListening() {
+		return this._isListening
+	}
+}

+ 32 - 0
src/exports/log.ts

@@ -0,0 +1,32 @@
+import * as vscode from "vscode"
+
+export function outputChannelLog(outputChannel: vscode.OutputChannel, ...args: unknown[]) {
+	for (const arg of args) {
+		if (arg === null) {
+			outputChannel.appendLine("null")
+		} else if (arg === undefined) {
+			outputChannel.appendLine("undefined")
+		} else if (typeof arg === "string") {
+			outputChannel.appendLine(arg)
+		} else if (arg instanceof Error) {
+			outputChannel.appendLine(`Error: ${arg.message}\n${arg.stack || ""}`)
+		} else {
+			try {
+				outputChannel.appendLine(
+					JSON.stringify(
+						arg,
+						(key, value) => {
+							if (typeof value === "bigint") return `BigInt(${value})`
+							if (typeof value === "function") return `Function: ${value.name || "anonymous"}`
+							if (typeof value === "symbol") return value.toString()
+							return value
+						},
+						2,
+					),
+				)
+			} catch (error) {
+				outputChannel.appendLine(`[Non-serializable object: ${Object.prototype.toString.call(arg)}]`)
+			}
+		}
+	}
+}

+ 0 - 35
src/exports/message-history.ts

@@ -1,35 +0,0 @@
-import { ClineMessage } from "./roo-code"
-
-export class MessageHistory {
-	private readonly messages: Record<string, Record<number, ClineMessage>>
-	private readonly list: Record<string, number[]>
-
-	constructor() {
-		this.messages = {}
-		this.list = {}
-	}
-
-	public add(taskId: string, message: ClineMessage) {
-		if (!this.messages[taskId]) {
-			this.messages[taskId] = {}
-		}
-
-		this.messages[taskId][message.ts] = message
-
-		if (!this.list[taskId]) {
-			this.list[taskId] = []
-		}
-
-		this.list[taskId].push(message.ts)
-	}
-
-	public update(taskId: string, message: ClineMessage) {
-		if (this.messages[taskId][message.ts]) {
-			this.messages[taskId][message.ts] = message
-		}
-	}
-
-	public getMessages(taskId: string) {
-		return (this.list[taskId] ?? []).map((ts) => this.messages[taskId][ts]).filter(Boolean)
-	}
-}

+ 133 - 32
src/exports/roo-code.d.ts

@@ -397,26 +397,125 @@ type TokenUsage = {
 	contextTokens: number
 }
 
-type RooCodeSettings = GlobalSettings & ProviderSettings
-
-interface RooCodeEvents {
+type RooCodeEvents = {
 	message: [
 		{
 			taskId: string
 			action: "created" | "updated"
-			message: ClineMessage
+			message: {
+				ts: number
+				type: "ask" | "say"
+				ask?:
+					| (
+							| "followup"
+							| "command"
+							| "command_output"
+							| "completion_result"
+							| "tool"
+							| "api_req_failed"
+							| "resume_task"
+							| "resume_completed_task"
+							| "mistake_limit_reached"
+							| "browser_action_launch"
+							| "use_mcp_server"
+							| "finishTask"
+					  )
+					| undefined
+				say?:
+					| (
+							| "task"
+							| "error"
+							| "api_req_started"
+							| "api_req_finished"
+							| "api_req_retried"
+							| "api_req_retry_delayed"
+							| "api_req_deleted"
+							| "text"
+							| "reasoning"
+							| "completion_result"
+							| "user_feedback"
+							| "user_feedback_diff"
+							| "command_output"
+							| "tool"
+							| "shell_integration_warning"
+							| "browser_action"
+							| "browser_action_result"
+							| "command"
+							| "mcp_server_request_started"
+							| "mcp_server_response"
+							| "new_task_started"
+							| "new_task"
+							| "checkpoint_saved"
+							| "rooignore_error"
+					  )
+					| undefined
+				text?: string | undefined
+				images?: string[] | undefined
+				partial?: boolean | undefined
+				reasoning?: string | undefined
+				conversationHistoryIndex?: number | undefined
+				checkpoint?:
+					| {
+							[x: string]: unknown
+					  }
+					| undefined
+				progressStatus?:
+					| {
+							icon?: string | undefined
+							text?: string | undefined
+					  }
+					| undefined
+			}
 		},
 	]
-	taskCreated: [taskId: string]
-	taskStarted: [taskId: string]
-	taskPaused: [taskId: string]
-	taskUnpaused: [taskId: string]
-	taskAskResponded: [taskId: string]
-	taskAborted: [taskId: string]
-	taskSpawned: [taskId: string, childTaskId: string]
-	taskCompleted: [taskId: string, usage: TokenUsage]
-	taskTokenUsageUpdated: [taskId: string, usage: TokenUsage]
+	taskCreated: [string]
+	taskStarted: [string]
+	taskPaused: [string]
+	taskUnpaused: [string]
+	taskAskResponded: [string]
+	taskAborted: [string]
+	taskSpawned: [string, string]
+	taskCompleted: [
+		string,
+		{
+			totalTokensIn: number
+			totalTokensOut: number
+			totalCacheWrites?: number | undefined
+			totalCacheReads?: number | undefined
+			totalCost: number
+			contextTokens: number
+		},
+	]
+	taskTokenUsageUpdated: [
+		string,
+		{
+			totalTokensIn: number
+			totalTokensOut: number
+			totalCacheWrites?: number | undefined
+			totalCacheReads?: number | undefined
+			totalCost: number
+			contextTokens: number
+		},
+	]
+}
+
+/**
+ * RooCodeEvent
+ */
+declare enum RooCodeEventName {
+	Message = "message",
+	TaskCreated = "taskCreated",
+	TaskStarted = "taskStarted",
+	TaskPaused = "taskPaused",
+	TaskUnpaused = "taskUnpaused",
+	TaskAskResponded = "taskAskResponded",
+	TaskAborted = "taskAborted",
+	TaskSpawned = "taskSpawned",
+	TaskCompleted = "taskCompleted",
+	TaskTokenUsageUpdated = "taskTokenUsageUpdated",
 }
+
+type RooCodeSettings = GlobalSettings & ProviderSettings
 interface RooCodeAPI extends EventEmitter<RooCodeEvents> {
 	/**
 	 * Starts a new task with an optional initial message and images.
@@ -424,7 +523,17 @@ interface RooCodeAPI extends EventEmitter<RooCodeEvents> {
 	 * @param images Optional array of image data URIs (e.g., "data:image/webp;base64,...").
 	 * @returns The ID of the new task.
 	 */
-	startNewTask(task?: string, images?: string[]): Promise<string>
+	startNewTask({
+		configuration,
+		text,
+		images,
+		newTab,
+	}: {
+		configuration?: RooCodeSettings
+		text?: string
+		images?: string[]
+		newTab?: boolean
+	}): Promise<string>
 	/**
 	 * Returns the current task stack.
 	 * @returns An array of task IDs.
@@ -466,23 +575,15 @@ interface RooCodeAPI extends EventEmitter<RooCodeEvents> {
 	 * Returns true if the API is ready to use.
 	 */
 	isReady(): boolean
-	/**
-	 * Returns the messages for a given task.
-	 * @param taskId The ID of the task.
-	 * @returns An array of ClineMessage objects.
-	 */
-	getMessages(taskId: string): ClineMessage[]
-	/**
-	 * Returns the token usage for a given task.
-	 * @param taskId The ID of the task.
-	 * @returns A TokenUsage object.
-	 */
-	getTokenUsage(taskId: string): TokenUsage
-	/**
-	 * Logs a message to the output channel.
-	 * @param message The message to log.
-	 */
-	log(message: string): void
 }
 
-export type { ClineMessage, GlobalSettings, ProviderSettings, RooCodeAPI, RooCodeEvents, RooCodeSettings, TokenUsage }
+export {
+	type ClineMessage,
+	type GlobalSettings,
+	type ProviderSettings,
+	type RooCodeAPI,
+	RooCodeEventName,
+	type RooCodeEvents,
+	type RooCodeSettings,
+	type TokenUsage,
+}

+ 104 - 0
src/exports/types.ts

@@ -405,3 +405,107 @@ type TokenUsage = {
 }
 
 export type { TokenUsage }
+
+type RooCodeEvents = {
+	message: [
+		{
+			taskId: string
+			action: "created" | "updated"
+			message: {
+				ts: number
+				type: "ask" | "say"
+				ask?:
+					| (
+							| "followup"
+							| "command"
+							| "command_output"
+							| "completion_result"
+							| "tool"
+							| "api_req_failed"
+							| "resume_task"
+							| "resume_completed_task"
+							| "mistake_limit_reached"
+							| "browser_action_launch"
+							| "use_mcp_server"
+							| "finishTask"
+					  )
+					| undefined
+				say?:
+					| (
+							| "task"
+							| "error"
+							| "api_req_started"
+							| "api_req_finished"
+							| "api_req_retried"
+							| "api_req_retry_delayed"
+							| "api_req_deleted"
+							| "text"
+							| "reasoning"
+							| "completion_result"
+							| "user_feedback"
+							| "user_feedback_diff"
+							| "command_output"
+							| "tool"
+							| "shell_integration_warning"
+							| "browser_action"
+							| "browser_action_result"
+							| "command"
+							| "mcp_server_request_started"
+							| "mcp_server_response"
+							| "new_task_started"
+							| "new_task"
+							| "checkpoint_saved"
+							| "rooignore_error"
+					  )
+					| undefined
+				text?: string | undefined
+				images?: string[] | undefined
+				partial?: boolean | undefined
+				reasoning?: string | undefined
+				conversationHistoryIndex?: number | undefined
+				checkpoint?:
+					| {
+							[x: string]: unknown
+					  }
+					| undefined
+				progressStatus?:
+					| {
+							icon?: string | undefined
+							text?: string | undefined
+					  }
+					| undefined
+			}
+		},
+	]
+	taskCreated: [string]
+	taskStarted: [string]
+	taskPaused: [string]
+	taskUnpaused: [string]
+	taskAskResponded: [string]
+	taskAborted: [string]
+	taskSpawned: [string, string]
+	taskCompleted: [
+		string,
+		{
+			totalTokensIn: number
+			totalTokensOut: number
+			totalCacheWrites?: number | undefined
+			totalCacheReads?: number | undefined
+			totalCost: number
+			contextTokens: number
+		},
+	]
+	taskTokenUsageUpdated: [
+		string,
+		{
+			totalTokensIn: number
+			totalTokensOut: number
+			totalCacheWrites?: number | undefined
+			totalCacheReads?: number | undefined
+			totalCost: number
+			contextTokens: number
+		},
+	]
+}
+
+export type { RooCodeEvents }

+ 3 - 1
src/extension.ts

@@ -119,7 +119,9 @@ export async function activate(context: vscode.ExtensionContext) {
 	vscode.commands.executeCommand("roo-cline.activationCompleted")
 
 	// Implements the `RooCodeAPI` interface.
-	return new API(outputChannel, provider)
+	const socketPath = process.env.ROO_CODE_IPC_SOCKET_PATH
+	const enableLogging = typeof socketPath === "string"
+	return new API(outputChannel, provider, socketPath, enableLogging)
 }
 
 // This method is called when your extension is deactivated

+ 1 - 1
src/integrations/terminal/Terminal.ts

@@ -186,7 +186,7 @@ export class Terminal {
 					console.log(`[Terminal ${this.id}] Shell integration not available. Command execution aborted.`)
 					process.emit(
 						"no_shell_integration",
-						"Shell integration initialization sequence '\\x1b]633;A' was not received within 4 seconds. Shell integration has been disabled for this terminal instance. Increase the timeout in the settings if necessary.",
+						`Shell integration initialization sequence '\\x1b]633;A' was not received within ${Terminal.shellIntegrationTimeout / 1000}s. Shell integration has been disabled for this terminal instance. Increase the timeout in the settings if necessary.`,
 					)
 				})
 		})

+ 41 - 0
src/schemas/index.ts

@@ -626,6 +626,8 @@ export const GLOBAL_SETTINGS_KEYS = Object.keys(globalSettingsRecord) as Keys<Gl
  * RooCodeSettings
  */
 
+export const rooCodeSettingsSchema = providerSettingsSchema.merge(globalSettingsSchema)
+
 export type RooCodeSettings = GlobalSettings & ProviderSettings
 
 /**
@@ -787,6 +789,44 @@ export const tokenUsageSchema = z.object({
 
 export type TokenUsage = z.infer<typeof tokenUsageSchema>
 
+/**
+ * RooCodeEvent
+ */
+
+export enum RooCodeEventName {
+	Message = "message",
+	TaskCreated = "taskCreated",
+	TaskStarted = "taskStarted",
+	TaskPaused = "taskPaused",
+	TaskUnpaused = "taskUnpaused",
+	TaskAskResponded = "taskAskResponded",
+	TaskAborted = "taskAborted",
+	TaskSpawned = "taskSpawned",
+	TaskCompleted = "taskCompleted",
+	TaskTokenUsageUpdated = "taskTokenUsageUpdated",
+}
+
+export const rooCodeEventsSchema = z.object({
+	[RooCodeEventName.Message]: z.tuple([
+		z.object({
+			taskId: z.string(),
+			action: z.union([z.literal("created"), z.literal("updated")]),
+			message: clineMessageSchema,
+		}),
+	]),
+	[RooCodeEventName.TaskCreated]: z.tuple([z.string()]),
+	[RooCodeEventName.TaskStarted]: z.tuple([z.string()]),
+	[RooCodeEventName.TaskPaused]: z.tuple([z.string()]),
+	[RooCodeEventName.TaskUnpaused]: z.tuple([z.string()]),
+	[RooCodeEventName.TaskAskResponded]: z.tuple([z.string()]),
+	[RooCodeEventName.TaskAborted]: z.tuple([z.string()]),
+	[RooCodeEventName.TaskSpawned]: z.tuple([z.string(), z.string()]),
+	[RooCodeEventName.TaskCompleted]: z.tuple([z.string(), tokenUsageSchema]),
+	[RooCodeEventName.TaskTokenUsageUpdated]: z.tuple([z.string(), tokenUsageSchema]),
+})
+
+export type RooCodeEvents = z.infer<typeof rooCodeEventsSchema>
+
 /**
  * TypeDefinition
  */
@@ -801,6 +841,7 @@ export const typeDefinitions: TypeDefinition[] = [
 	{ schema: globalSettingsSchema, identifier: "GlobalSettings" },
 	{ schema: clineMessageSchema, identifier: "ClineMessage" },
 	{ schema: tokenUsageSchema, identifier: "TokenUsage" },
+	{ schema: rooCodeEventsSchema, identifier: "RooCodeEvents" },
 ]
 
 // Also export as default for ESM compatibility

+ 135 - 0
src/schemas/ipc.ts

@@ -0,0 +1,135 @@
+import { z } from "zod"
+
+import { RooCodeEventName, rooCodeEventsSchema, rooCodeSettingsSchema } from "./index"
+
+/**
+ * Ack
+ */
+
+export const ackSchema = z.object({
+	clientId: z.string(),
+	pid: z.number(),
+	ppid: z.number(),
+})
+
+export type Ack = z.infer<typeof ackSchema>
+
+/**
+ * TaskCommand
+ */
+
+export enum TaskCommandName {
+	StartNewTask = "StartNewTask",
+	CancelTask = "CancelTask",
+	CloseTask = "CloseTask",
+}
+
+export const taskCommandSchema = z.discriminatedUnion("commandName", [
+	z.object({
+		commandName: z.literal(TaskCommandName.StartNewTask),
+		data: z.object({
+			configuration: rooCodeSettingsSchema,
+			text: z.string(),
+			images: z.array(z.string()).optional(),
+			newTab: z.boolean().optional(),
+		}),
+	}),
+	z.object({
+		commandName: z.literal(TaskCommandName.CancelTask),
+		data: z.string(),
+	}),
+	z.object({
+		commandName: z.literal(TaskCommandName.CloseTask),
+		data: z.string(),
+	}),
+])
+
+export type TaskCommand = z.infer<typeof taskCommandSchema>
+
+/**
+ * TaskEvent
+ */
+
+export const taskEventSchema = z.discriminatedUnion("eventName", [
+	z.object({
+		eventName: z.literal(RooCodeEventName.Message),
+		payload: rooCodeEventsSchema.shape[RooCodeEventName.Message],
+	}),
+	z.object({
+		eventName: z.literal(RooCodeEventName.TaskCreated),
+		payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskCreated],
+	}),
+	z.object({
+		eventName: z.literal(RooCodeEventName.TaskStarted),
+		payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskStarted],
+	}),
+	z.object({
+		eventName: z.literal(RooCodeEventName.TaskPaused),
+		payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskPaused],
+	}),
+	z.object({
+		eventName: z.literal(RooCodeEventName.TaskUnpaused),
+		payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskUnpaused],
+	}),
+	z.object({
+		eventName: z.literal(RooCodeEventName.TaskAskResponded),
+		payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskAskResponded],
+	}),
+	z.object({
+		eventName: z.literal(RooCodeEventName.TaskAborted),
+		payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskAborted],
+	}),
+	z.object({
+		eventName: z.literal(RooCodeEventName.TaskSpawned),
+		payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskSpawned],
+	}),
+	z.object({
+		eventName: z.literal(RooCodeEventName.TaskCompleted),
+		payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskCompleted],
+	}),
+	z.object({
+		eventName: z.literal(RooCodeEventName.TaskTokenUsageUpdated),
+		payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskTokenUsageUpdated],
+	}),
+])
+
+export type TaskEvent = z.infer<typeof taskEventSchema>
+
+/**
+ * IpcMessage
+ */
+
+export enum IpcMessageType {
+	Connect = "Connect",
+	Disconnect = "Disconnect",
+	Ack = "Ack",
+	TaskCommand = "TaskCommand",
+	TaskEvent = "TaskEvent",
+}
+
+export enum IpcOrigin {
+	Client = "client",
+	Server = "server",
+}
+
+export const ipcMessageSchema = z.discriminatedUnion("type", [
+	z.object({
+		type: z.literal(IpcMessageType.Ack),
+		origin: z.literal(IpcOrigin.Server),
+		data: ackSchema,
+	}),
+	z.object({
+		type: z.literal(IpcMessageType.TaskCommand),
+		origin: z.literal(IpcOrigin.Client),
+		clientId: z.string(),
+		data: taskCommandSchema,
+	}),
+	z.object({
+		type: z.literal(IpcMessageType.TaskEvent),
+		origin: z.literal(IpcOrigin.Server),
+		relayClientId: z.string().optional(),
+		data: taskEventSchema,
+	}),
+])
+
+export type IpcMessage = z.infer<typeof ipcMessageSchema>

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio