Roo-Code/src/exports/api.ts
2025-05-09 00:05:29 -07:00

334 lines
10 KiB
TypeScript

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<RooCodeEvents> 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<string, ClineProvider>()
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<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.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<void> {
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<boolean> {
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 })
}
}