mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
* fix: add image content support to MCP tool responses (#10874) Co-authored-by: Roo Code <roomote@roocode.com> * fix: transform tool blocks to text before condensing (EXT-624) (#10975) * refactor(read_file): Codex-inspired read_file refactor EXT-617 (#10981) * feat: allow import settings in initial welcome screen (#10994) Co-authored-by: Roo Code <roomote@roocode.com> Co-authored-by: Matt Rubens <mrubens@users.noreply.github.com> * fix(code-index): remove deprecated text-embedding-004 and migrate to gemini-embedding-001 (#11038) Co-authored-by: Roo Code <roomote@roocode.com> Co-authored-by: Hannes Rudolph <hrudolph@gmail.com> * chore: treat extension .env as optional (#11116) * fix: sanitize tool_use_id in tool_result blocks to match API history (#11131) Tool IDs from providers like Gemini/OpenRouter contain special characters (e.g., 'functions.read_file:0') that are sanitized when saving tool_use blocks to API history. However, tool_result blocks were using the original unsanitized IDs, causing ToolResultIdMismatchError. This fix ensures tool_result blocks use sanitizeToolUseId() to match the sanitized tool_use IDs in conversation history. Fixes EXT-711 * fix: queue messages during command execution instead of losing them (#11140) * IPC fixes for task cancellation and queued messages (#11162) * feat: add support for AGENTS.local.md personal override files (#11183) Co-authored-by: Roo Code <roomote@roocode.com> Co-authored-by: roomote[bot] <219738659+roomote[bot]@users.noreply.github.com> * fix(cli): resolve race condition causing provider switch during mode changes (#11205) When using slash commands with `mode:` frontmatter (e.g., `/cli-release` with `mode: code`), the CLI would fail with "Could not resolve authentication method" from the Anthropic SDK, even when using a non-Anthropic provider like `--provider roo`. Root cause: In `markWebviewReady()`, the `webviewDidLaunch` message was sent before `updateSettings`, creating a race condition. The `webviewDidLaunch` handler's "first-time init" sync would read `getState()` before CLI-provided settings were applied to the context proxy. Since `getState()` defaults `apiProvider` to "anthropic" when unset, this default was saved to the provider profile. When a slash command triggered `handleModeSwitch()`, it found this corrupted profile with `apiProvider: "anthropic"` (but no API key) and activated it, overwriting the CLI's working roo provider configuration. Fix: 1. Reorder `markWebviewReady()` to send `updateSettings` before `webviewDidLaunch`, ensuring the context proxy has CLI-provided values when the initialization handler runs. 2. Guard the first-time init sync with `checkExistKey(apiConfiguration)` to prevent saving a profile with only the default "anthropic" fallback and no actual API keys configured. Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> * chore: remove dead toolFormat code from getEnvironmentDetails (#11207) Remove the toolFormat constant and <tool_format> line from environment details output. Native tool calling is now the only supported protocol, making this code unnecessary. Fixes #11206 Co-authored-by: Roo Code <roomote@roocode.com> * feat: extract translation and merge resolver modes into reusable skills (#11215) * feat: extract translation and merge resolver modes into reusable skills - Add roo-translation skill with comprehensive i18n guidelines - Add roo-conflict-resolution skill for intelligent merge conflict resolution - Add /roo-translate slash command as shortcut for translation skill - Add /roo-resolve-conflicts slash command as shortcut for conflict resolution skill The existing translate and merge-resolver modes are preserved. These new skills and commands provide reusable access to the same functionality. Closes CLO-722 * feat: add guidances directory with translator guidance file - Add .roo/guidances/roo-translator.md for brand voice, tone, and word choice guidance - Update roo-translation skill to reference the guidance file The guidance file serves as a placeholder for translation style guidelines that will be interpolated at runtime. * fix: rename guidances directory to guidance (singular) * fix: remove language-specific section from translator guidance The guidance file should focus on brand voice, tone, and word choice only. * fix: remove language-specific guidelines section from skill file * Update .roo/skills/roo-translation/SKILL.md Co-authored-by: roomote[bot] <219738659+roomote[bot]@users.noreply.github.com> --------- Co-authored-by: Roo Code <roomote@roocode.com> Co-authored-by: Bruno Bergher <bruno@roocode.com> Co-authored-by: roomote[bot] <219738659+roomote[bot]@users.noreply.github.com> * feat: add Claude Opus 4.6 support across all providers (#11224) * feat: add Claude Opus 4.6 support across all providers Add Claude Opus 4.6 (claude-opus-4-6) model definitions and 1M context support across Anthropic, Bedrock, Vertex AI, OpenRouter, and Vercel AI Gateway providers. - Anthropic: 128K max output, /5 pricing, 1M context tiers - Bedrock: anthropic.claude-opus-4-6-v1:0 with 1M context + global inference - Vertex: claude-opus-4-6 with 1M context tiers - OpenRouter: prompt caching + reasoning budget sets - Vercel AI Gateway: Opus 4.5 and 4.6 added to capability sets - UI: 1M context checkbox for Opus 4.6 on all providers - i18n: Updated 1M context descriptions across 18 locales Also adds Opus 4.5 to Vercel AI Gateway (previously missing) and OpenRouter maxTokens overrides for Opus 4.5/4.6. Closes #11223 * fix: apply tier pricing when 1M context is enabled on Bedrock When awsBedrock1MContext is enabled for tiered models like Opus 4.6, also apply the 1M tier pricing (inputPrice, outputPrice, cache prices) instead of only updating contextWindow. This ensures cost calculations and UI display use the correct >200K rates. * feat: add gpt-5.3-codex model to OpenAI Codex provider (#11225) feat: add gpt-5.3-codex model and make it default for OpenAI Codex provider Co-authored-by: Roo Code <roomote@roocode.com> * fix: prevent parent task state loss during orchestrator delegation (#11281) * fix: make removeClineFromStack() delegation-aware to prevent orphaned parent tasks (#11302) * fix: make removeClineFromStack() delegation-aware to prevent orphaned parent tasks When a delegated child task is removed via removeClineFromStack() (e.g., Clear Task, navigate to history, start new task), the parent task was left orphaned in "delegated" status with a stale awaitingChildId. This made the parent unresumable without manual history repair. This fix captures parentTaskId and childTaskId before abort/dispose, then repairs the parent metadata (status -> active, clear awaitingChildId) when the popped task is a delegated child and awaitingChildId matches. Parent lookup + updateTaskHistory are wrapped in try/catch so failures are non-fatal (logged but do not block the pop). Closes #11301 * fix: add skipDelegationRepair opt-out to removeClineFromStack() for nested delegation --------- Co-authored-by: Roo Code <roomote@roocode.com> * fix(reliability): prevent webview postMessage crashes and make dispose idempotent (#11313) * fix(reliability): prevent webview postMessage crashes and make dispose idempotent Closes: #11311 1. postMessageToWebview() now catches rejections from webview.postMessage() so that messages sent after the webview is disposed do not surface as unhandled promise rejections. 2. dispose() is guarded by a _disposed flag so that repeated calls (e.g. during rapid extension deactivation) are no-ops. 3. CloudService mock in ClineProvider.spec.ts updated to include off() — a pre-existing gap exposed by the new dispose test. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: add early _disposed check in postMessageToWebview Skip the postMessage call entirely when the provider is already disposed, avoiding unnecessary try/catch execution. Added test coverage for this path. * chore: trigger CI --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: daniel-lxs <ricciodaniel98@gmail.com> * fix: resolve race condition in new_task delegation that loses parent task history (#11331) * fix: resolve race condition in new_task delegation that loses parent task history When delegateParentAndOpenChild creates a child task via createTask(), the Task constructor fires startTask() as a fire-and-forget async call. The child immediately begins its task loop and eventually calls saveClineMessages() → updateTaskHistory(), which reads globalState, modifies it, and writes back. Meanwhile, delegateParentAndOpenChild persists the parent's delegation metadata (status: 'delegated', delegatedToId, awaitingChildId, childIds) via a separate updateTaskHistory() call AFTER createTask() returns. These two concurrent read-modify-write operations on globalState race: the last writer wins, overwriting the other's changes. When the child's write lands last, the parent's delegation fields are lost, making the parent task unresumable when the child finishes. Fix: create the child task with startTask: false, persist the parent's delegation metadata first, then manually call child.start(). This ensures the parent metadata is safely in globalState before the child begins writing. * docs: clarify Task.start() only handles new tasks, not history resume * fix: serialize taskHistory writes and fix delegation status overwrite race (#11335) Add a promise-chain mutex (withTaskHistoryLock) to serialize all read-modify-write operations on taskHistory, preventing concurrent interleaving from silently dropping entries. Reorder reopenParentFromDelegation to close the child instance before marking it completed, so the abort path's stale 'active' status write no longer overwrites the 'completed' state. Covered by new tests: RPD-04/05/06, UTH-02/04, and a full mutex concurrency suite. * Fix task resumption in the API module (#11369) * chore: clean up repo-facing mode rules (#11410) * fix: add maxReadFileLine to ExtensionState type for webview compatibility --------- Co-authored-by: roomote[bot] <219738659+roomote[bot]@users.noreply.github.com> Co-authored-by: Roo Code <roomote@roocode.com> Co-authored-by: Daniel <57051444+daniel-lxs@users.noreply.github.com> Co-authored-by: Matt Rubens <mrubens@users.noreply.github.com> Co-authored-by: Chris Estreich <cestreich@gmail.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: Bruno Bergher <bruno@roocode.com> Co-authored-by: 0xMink <dennis@dennismink.com> Co-authored-by: daniel-lxs <ricciodaniel98@gmail.com>
495 lines
14 KiB
TypeScript
495 lines
14 KiB
TypeScript
import { EventEmitter } from "events"
|
|
import fs from "fs/promises"
|
|
import * as path from "path"
|
|
import * as os from "os"
|
|
|
|
import * as vscode from "vscode"
|
|
import pWaitFor from "p-wait-for"
|
|
|
|
import {
|
|
type RooCodeAPI,
|
|
type RooCodeSettings,
|
|
type RooCodeEvents,
|
|
type ProviderSettings,
|
|
type ProviderSettingsEntry,
|
|
type TaskEvent,
|
|
type CreateTaskOptions,
|
|
RooCodeEventName,
|
|
TaskCommandName,
|
|
isSecretStateKey,
|
|
IpcOrigin,
|
|
IpcMessageType,
|
|
} from "@roo-code/types"
|
|
import { IpcServer } from "@roo-code/ipc"
|
|
|
|
import { Package } from "../shared/package"
|
|
import { ClineProvider } from "../core/webview/ClineProvider"
|
|
import { openClineInNewTab } from "../activate/registerCommands"
|
|
|
|
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 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[]) => {
|
|
this.outputChannelLog(...args)
|
|
console.log(args)
|
|
}
|
|
|
|
this.logfile = path.join(os.tmpdir(), "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, command) => {
|
|
switch (command.commandName) {
|
|
case TaskCommandName.StartNewTask:
|
|
this.log(
|
|
`[API] StartNewTask -> ${command.data.text}, ${JSON.stringify(command.data.configuration)}`,
|
|
)
|
|
await this.startNewTask(command.data)
|
|
break
|
|
case TaskCommandName.CancelTask:
|
|
this.log(`[API] CancelTask`)
|
|
await this.cancelCurrentTask()
|
|
break
|
|
case TaskCommandName.CloseTask:
|
|
this.log(`[API] CloseTask`)
|
|
await vscode.commands.executeCommand("workbench.action.files.saveFiles")
|
|
await vscode.commands.executeCommand("workbench.action.closeWindow")
|
|
break
|
|
case TaskCommandName.ResumeTask:
|
|
this.log(`[API] ResumeTask -> ${command.data}`)
|
|
try {
|
|
await this.resumeTask(command.data)
|
|
} catch (error) {
|
|
const errorMessage = error instanceof Error ? error.message : String(error)
|
|
this.log(`[API] ResumeTask failed for taskId ${command.data}: ${errorMessage}`)
|
|
// Don't rethrow - we want to prevent IPC server crashes
|
|
// The error is logged for debugging purposes
|
|
}
|
|
break
|
|
case TaskCommandName.SendMessage:
|
|
this.log(`[API] SendMessage -> ${command.data.text}`)
|
|
await this.sendMessage(command.data.text, command.data.images)
|
|
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(`${Package.name}.SidebarProvider.focus`)
|
|
|
|
provider = this.sidebarProvider
|
|
}
|
|
|
|
await provider.removeClineFromStack()
|
|
await provider.postStateToWebview()
|
|
await provider.postMessageToWebview({ type: "action", action: "chatButtonClicked" })
|
|
await provider.postMessageToWebview({ type: "invoke", invoke: "newChat", text, images })
|
|
|
|
const options: CreateTaskOptions = {
|
|
consecutiveMistakeLimit: Number.MAX_SAFE_INTEGER,
|
|
}
|
|
|
|
const task = await provider.createTask(text, images, undefined, options, configuration)
|
|
|
|
if (!task) {
|
|
throw new Error("Failed to create task due to policy restrictions")
|
|
}
|
|
|
|
return task.taskId
|
|
}
|
|
|
|
public async resumeTask(taskId: string): Promise<void> {
|
|
await vscode.commands.executeCommand(`${Package.name}.SidebarProvider.focus`)
|
|
await this.waitForWebviewLaunch(5_000)
|
|
|
|
const { historyItem } = await this.sidebarProvider.getTaskWithId(taskId)
|
|
await this.sidebarProvider.createTaskWithHistoryItem(historyItem)
|
|
|
|
if (this.sidebarProvider.viewLaunched) {
|
|
await this.sidebarProvider.postMessageToWebview({ type: "action", action: "chatButtonClicked" })
|
|
} else {
|
|
this.log(
|
|
`[API#resumeTask] webview not launched after resume for task ${taskId}; continuing in headless mode`,
|
|
)
|
|
}
|
|
}
|
|
|
|
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) {
|
|
// Legacy finishSubTask removed; clear current by closing active task instance.
|
|
await this.sidebarProvider.removeClineFromStack()
|
|
await this.sidebarProvider.postStateToWebview()
|
|
}
|
|
|
|
public async cancelCurrentTask() {
|
|
await this.sidebarProvider.cancelTask()
|
|
}
|
|
|
|
public async sendMessage(text?: string, images?: string[]) {
|
|
const currentTask = this.sidebarProvider.getCurrentTask()
|
|
|
|
// In headless/sandbox flows the webview may not be launched, so routing
|
|
// through invoke=sendMessage drops the message. Deliver directly to the
|
|
// task ask-response channel instead.
|
|
if (!this.sidebarProvider.viewLaunched) {
|
|
if (!currentTask) {
|
|
this.log("[API#sendMessage] no current task in headless mode; message dropped")
|
|
return
|
|
}
|
|
|
|
await currentTask.submitUserMessage(text ?? "", images)
|
|
return
|
|
}
|
|
|
|
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 async waitForWebviewLaunch(timeoutMs: number): Promise<boolean> {
|
|
try {
|
|
await pWaitFor(() => this.sidebarProvider.viewLaunched, {
|
|
timeout: timeoutMs,
|
|
interval: 50,
|
|
})
|
|
|
|
return true
|
|
} catch {
|
|
this.log(`[API#waitForWebviewLaunch] webview did not launch within ${timeoutMs}ms`)
|
|
return false
|
|
}
|
|
}
|
|
|
|
private registerListeners(provider: ClineProvider) {
|
|
provider.on(RooCodeEventName.TaskCreated, (task) => {
|
|
// Task Lifecycle
|
|
|
|
task.on(RooCodeEventName.TaskStarted, async () => {
|
|
this.emit(RooCodeEventName.TaskStarted, task.taskId)
|
|
await this.fileLog(`[${new Date().toISOString()}] taskStarted -> ${task.taskId}\n`)
|
|
})
|
|
|
|
task.on(RooCodeEventName.TaskCompleted, async (_, tokenUsage, toolUsage) => {
|
|
this.emit(RooCodeEventName.TaskCompleted, task.taskId, tokenUsage, toolUsage, {
|
|
isSubtask: !!task.parentTaskId,
|
|
})
|
|
|
|
await this.fileLog(
|
|
`[${new Date().toISOString()}] taskCompleted -> ${task.taskId} | ${JSON.stringify(tokenUsage, null, 2)} | ${JSON.stringify(toolUsage, null, 2)}\n`,
|
|
)
|
|
})
|
|
|
|
task.on(RooCodeEventName.TaskAborted, () => {
|
|
this.emit(RooCodeEventName.TaskAborted, task.taskId)
|
|
})
|
|
|
|
task.on(RooCodeEventName.TaskFocused, () => {
|
|
this.emit(RooCodeEventName.TaskFocused, task.taskId)
|
|
})
|
|
|
|
task.on(RooCodeEventName.TaskUnfocused, () => {
|
|
this.emit(RooCodeEventName.TaskUnfocused, task.taskId)
|
|
})
|
|
|
|
task.on(RooCodeEventName.TaskActive, () => {
|
|
this.emit(RooCodeEventName.TaskActive, task.taskId)
|
|
})
|
|
|
|
task.on(RooCodeEventName.TaskInteractive, () => {
|
|
this.emit(RooCodeEventName.TaskInteractive, task.taskId)
|
|
})
|
|
|
|
task.on(RooCodeEventName.TaskResumable, () => {
|
|
this.emit(RooCodeEventName.TaskResumable, task.taskId)
|
|
})
|
|
|
|
task.on(RooCodeEventName.TaskIdle, () => {
|
|
this.emit(RooCodeEventName.TaskIdle, task.taskId)
|
|
})
|
|
|
|
// Subtask Lifecycle
|
|
|
|
task.on(RooCodeEventName.TaskPaused, () => {
|
|
this.emit(RooCodeEventName.TaskPaused, task.taskId)
|
|
})
|
|
|
|
task.on(RooCodeEventName.TaskUnpaused, () => {
|
|
this.emit(RooCodeEventName.TaskUnpaused, task.taskId)
|
|
})
|
|
|
|
task.on(RooCodeEventName.TaskSpawned, (childTaskId) => {
|
|
this.emit(RooCodeEventName.TaskSpawned, task.taskId, childTaskId)
|
|
})
|
|
|
|
task.on(RooCodeEventName.TaskDelegated as any, (childTaskId: string) => {
|
|
;(this.emit as any)(RooCodeEventName.TaskDelegated, task.taskId, childTaskId)
|
|
})
|
|
|
|
task.on(RooCodeEventName.TaskDelegationCompleted as any, (childTaskId: string, summary: string) => {
|
|
;(this.emit as any)(RooCodeEventName.TaskDelegationCompleted, task.taskId, childTaskId, summary)
|
|
})
|
|
|
|
task.on(RooCodeEventName.TaskDelegationResumed as any, (childTaskId: string) => {
|
|
;(this.emit as any)(RooCodeEventName.TaskDelegationResumed, task.taskId, childTaskId)
|
|
})
|
|
|
|
// Task Execution
|
|
|
|
task.on(RooCodeEventName.Message, async (message) => {
|
|
this.emit(RooCodeEventName.Message, { taskId: task.taskId, ...message })
|
|
|
|
if (message.message.partial !== true) {
|
|
await this.fileLog(`[${new Date().toISOString()}] ${JSON.stringify(message.message, null, 2)}\n`)
|
|
}
|
|
})
|
|
|
|
task.on(RooCodeEventName.TaskModeSwitched, (taskId, mode) => {
|
|
this.emit(RooCodeEventName.TaskModeSwitched, taskId, mode)
|
|
})
|
|
|
|
task.on(RooCodeEventName.TaskAskResponded, () => {
|
|
this.emit(RooCodeEventName.TaskAskResponded, task.taskId)
|
|
})
|
|
|
|
task.on(RooCodeEventName.QueuedMessagesUpdated, (taskId, messages) => {
|
|
this.emit(RooCodeEventName.QueuedMessagesUpdated, taskId, messages)
|
|
})
|
|
|
|
// Task Analytics
|
|
|
|
task.on(RooCodeEventName.TaskToolFailed, (taskId, tool, error) => {
|
|
this.emit(RooCodeEventName.TaskToolFailed, taskId, tool, error)
|
|
})
|
|
|
|
task.on(RooCodeEventName.TaskTokenUsageUpdated, (_, tokenUsage, toolUsage) => {
|
|
this.emit(RooCodeEventName.TaskTokenUsageUpdated, task.taskId, tokenUsage, toolUsage)
|
|
})
|
|
|
|
// Let's go!
|
|
|
|
this.emit(RooCodeEventName.TaskCreated, task.taskId)
|
|
})
|
|
}
|
|
|
|
// Logging
|
|
|
|
private outputChannelLog(...args: unknown[]) {
|
|
for (const arg of args) {
|
|
if (arg === null) {
|
|
this.outputChannel.appendLine("null")
|
|
} else if (arg === undefined) {
|
|
this.outputChannel.appendLine("undefined")
|
|
} else if (typeof arg === "string") {
|
|
this.outputChannel.appendLine(arg)
|
|
} else if (arg instanceof Error) {
|
|
this.outputChannel.appendLine(`Error: ${arg.message}\n${arg.stack || ""}`)
|
|
} else {
|
|
try {
|
|
this.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) {
|
|
this.outputChannel.appendLine(`[Non-serializable object: ${Object.prototype.toString.call(arg)}]`)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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(): RooCodeSettings {
|
|
return Object.fromEntries(
|
|
Object.entries(this.sidebarProvider.getValues()).filter(([key]) => !isSecretStateKey(key)),
|
|
)
|
|
}
|
|
|
|
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
|
|
|
|
public getProfiles(): string[] {
|
|
return this.sidebarProvider.getProviderProfileEntries().map(({ name }) => name)
|
|
}
|
|
|
|
public getProfileEntry(name: string): ProviderSettingsEntry | undefined {
|
|
return this.sidebarProvider.getProviderProfileEntry(name)
|
|
}
|
|
|
|
public async createProfile(name: string, profile?: ProviderSettings, activate: boolean = true) {
|
|
const entry = this.getProfileEntry(name)
|
|
|
|
if (entry) {
|
|
throw new Error(`Profile with name "${name}" already exists`)
|
|
}
|
|
|
|
const id = await this.sidebarProvider.upsertProviderProfile(name, profile ?? {}, activate)
|
|
|
|
if (!id) {
|
|
throw new Error(`Failed to create profile with name "${name}"`)
|
|
}
|
|
|
|
return id
|
|
}
|
|
|
|
public async updateProfile(
|
|
name: string,
|
|
profile: ProviderSettings,
|
|
activate: boolean = true,
|
|
): Promise<string | undefined> {
|
|
const entry = this.getProfileEntry(name)
|
|
|
|
if (!entry) {
|
|
throw new Error(`Profile with name "${name}" does not exist`)
|
|
}
|
|
|
|
const id = await this.sidebarProvider.upsertProviderProfile(name, profile, activate)
|
|
|
|
if (!id) {
|
|
throw new Error(`Failed to update profile with name "${name}"`)
|
|
}
|
|
|
|
return id
|
|
}
|
|
|
|
public async upsertProfile(
|
|
name: string,
|
|
profile: ProviderSettings,
|
|
activate: boolean = true,
|
|
): Promise<string | undefined> {
|
|
const id = await this.sidebarProvider.upsertProviderProfile(name, profile, activate)
|
|
|
|
if (!id) {
|
|
throw new Error(`Failed to upsert profile with name "${name}"`)
|
|
}
|
|
|
|
return id
|
|
}
|
|
|
|
public async deleteProfile(name: string): Promise<void> {
|
|
const entry = this.getProfileEntry(name)
|
|
|
|
if (!entry) {
|
|
throw new Error(`Profile with name "${name}" does not exist`)
|
|
}
|
|
|
|
await this.sidebarProvider.deleteProviderProfile(entry)
|
|
}
|
|
|
|
public getActiveProfile(): string | undefined {
|
|
return this.getConfiguration().currentApiConfigName
|
|
}
|
|
|
|
public async setActiveProfile(name: string): Promise<string | undefined> {
|
|
const entry = this.getProfileEntry(name)
|
|
|
|
if (!entry) {
|
|
throw new Error(`Profile with name "${name}" does not exist`)
|
|
}
|
|
|
|
await this.sidebarProvider.activateProviderProfile({ name })
|
|
return this.getActiveProfile()
|
|
}
|
|
}
|