import { EventEmitter } from "events" import * as vscode from "vscode" import fs from "fs/promises" import * as path from "path" import { getWorkspacePath } from "../utils/path" import { ClineProvider } from "../core/webview/ClineProvider" import { openClineInNewTab } from "../activate/registerCommands" import { RooCodeSettings, RooCodeEvents, RooCodeEventName } 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 implements RooCodeAPI { private readonly outputChannel: vscode.OutputChannel private readonly sidebarProvider: ClineProvider private readonly context: vscode.ExtensionContext private readonly ipc?: IpcServer private readonly taskMap = new Map() private readonly log: (...args: unknown[]) => void private logfile?: string constructor( outputChannel: vscode.OutputChannel, provider: ClineProvider, socketPath?: string, enableLogging = false, ) { super() this.outputChannel = outputChannel this.sidebarProvider = provider this.context = provider.context if (enableLogging) { this.log = (...args: unknown[]) => { outputChannelLog(this.outputChannel, ...args) console.log(args) } this.logfile = path.join(getWorkspacePath(), "roo-code-messages.log") } else { this.log = () => {} } 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 } }) } } public override emit( 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.files.revert") await vscode.commands.executeCommand("workbench.action.closeAllEditors") provider = await openClineInNewTab({ context: this.context, outputChannel: this.outputChannel }) this.registerListeners(provider) } else { await vscode.commands.executeCommand("roo-cline.SidebarProvider.focus") provider = this.sidebarProvider } if (configuration) { await provider.setValues(configuration) if (configuration.allowedCommands) { await vscode.workspace .getConfiguration("roo-cline") .update("allowedCommands", configuration.allowedCommands, vscode.ConfigurationTarget.Global) } } 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, undefined, { consecutiveMistakeLimit: Number.MAX_SAFE_INTEGER, }) return taskId } public async resumeTask(taskId: string): Promise { const { historyItem } = await this.sidebarProvider.getTaskWithId(taskId) await this.sidebarProvider.initClineWithHistoryItem(historyItem) await this.sidebarProvider.postMessageToWebview({ type: "action", action: "chatButtonClicked" }) } public async isTaskInHistory(taskId: string): Promise { try { await this.sidebarProvider.getTaskWithId(taskId) return true } catch { return false } } public getCurrentTaskStack() { return this.sidebarProvider.getCurrentTaskStack() } public async clearCurrentTask(lastMessage?: string) { await this.sidebarProvider.finishSubTask(lastMessage ?? "") await this.sidebarProvider.postStateToWebview() } public async cancelCurrentTask() { 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.sidebarProvider.postMessageToWebview({ type: "invoke", invoke: "sendMessage", text, images }) } public async pressPrimaryButton() { await this.sidebarProvider.postMessageToWebview({ type: "invoke", invoke: "primaryButtonClick" }) } public async pressSecondaryButton() { await this.sidebarProvider.postMessageToWebview({ type: "invoke", invoke: "secondaryButtonClick" }) } public isReady() { return this.sidebarProvider.viewLaunched } private registerListeners(provider: ClineProvider) { provider.on("clineCreated", (cline) => { cline.on("taskStarted", async () => { this.emit(RooCodeEventName.TaskStarted, cline.taskId) this.taskMap.set(cline.taskId, provider) await this.fileLog(`[${new Date().toISOString()}] taskStarted -> ${cline.taskId}\n`) }) cline.on("message", async (message) => { this.emit(RooCodeEventName.Message, { taskId: cline.taskId, ...message }) if (message.message.partial !== true) { await this.fileLog(`[${new Date().toISOString()}] ${JSON.stringify(message.message, null, 2)}\n`) } }) cline.on("taskModeSwitched", (taskId, mode) => this.emit(RooCodeEventName.TaskModeSwitched, taskId, mode)) 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", async (_, tokenUsage, toolUsage) => { this.emit(RooCodeEventName.TaskCompleted, cline.taskId, tokenUsage, toolUsage) this.taskMap.delete(cline.taskId) await this.fileLog( `[${new Date().toISOString()}] taskCompleted -> ${cline.taskId} | ${JSON.stringify(tokenUsage, null, 2)} | ${JSON.stringify(toolUsage, null, 2)}\n`, ) }) 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)) cline.on("taskTokenUsageUpdated", (_, usage) => this.emit(RooCodeEventName.TaskTokenUsageUpdated, cline.taskId, usage), ) cline.on("taskToolFailed", (taskId, tool, error) => this.emit(RooCodeEventName.TaskToolFailed, taskId, tool, error), ) this.emit(RooCodeEventName.TaskCreated, cline.taskId) }) } private async fileLog(message: string) { if (!this.logfile) { return } try { await fs.appendFile(this.logfile, message, "utf8") } catch (_) { this.logfile = undefined } } // Global Settings Management public getConfiguration() { return this.sidebarProvider.getValues() } public async setConfiguration(values: RooCodeSettings) { await this.sidebarProvider.contextProxy.setValues(values) await this.sidebarProvider.providerSettingsManager.saveConfig(values.currentApiConfigName || "default", values) await this.sidebarProvider.postStateToWebview() } // Provider Profile Management private getProfilesMeta() { return this.getConfiguration().listApiConfigMeta || [] } public getProfiles() { return this.getProfilesMeta().map((profile) => profile.name) } public hasProfile(name: string): boolean { return !!(this.getConfiguration().listApiConfigMeta || []).find((profile) => profile.name === name) } public async createProfile(name: string) { if (!name || !name.trim()) { throw new Error("Profile name cannot be empty") } const currentSettings = this.getConfiguration() const profiles = currentSettings.listApiConfigMeta || [] if (profiles.some((profile) => profile.name === name)) { throw new Error(`A profile with the name "${name}" already exists`) } const id = this.sidebarProvider.providerSettingsManager.generateId() await this.setConfiguration({ ...currentSettings, listApiConfigMeta: [ ...profiles, { id, name: name.trim(), apiProvider: "openai" as const, }, ], }) return id } public async deleteProfile(name: string) { const currentSettings = this.getConfiguration() const listApiConfigMeta = this.getProfilesMeta() const targetIndex = listApiConfigMeta.findIndex((p) => p.name === name) if (targetIndex === -1) { throw new Error(`Profile with name "${name}" does not exist`) } const profileToDelete = listApiConfigMeta[targetIndex] listApiConfigMeta.splice(targetIndex, 1) // If we're deleting the active profile, clear the currentApiConfigName. const currentApiConfigName = currentSettings.currentApiConfigName === profileToDelete.name ? undefined : currentSettings.currentApiConfigName await this.setConfiguration({ ...currentSettings, listApiConfigMeta, currentApiConfigName }) } public getActiveProfile(): string | undefined { return this.getConfiguration().currentApiConfigName } public async setActiveProfile(name: string) { if (!this.hasProfile(name)) { throw new Error(`Profile with name "${name}" does not exist`) } await this.sidebarProvider.activateProviderProfile({ name }) } }