diff --git a/packages/cloud/src/bridge/BaseChannel.ts b/packages/cloud/src/bridge/BaseChannel.ts index 45b9b525f6..95db835d1f 100644 --- a/packages/cloud/src/bridge/BaseChannel.ts +++ b/packages/cloud/src/bridge/BaseChannel.ts @@ -83,7 +83,7 @@ export abstract class BaseChannel /** * Handle connection-specific logic. diff --git a/packages/cloud/src/bridge/ExtensionChannel.ts b/packages/cloud/src/bridge/ExtensionChannel.ts index 99649f76f4..b38e3b9a8b 100644 --- a/packages/cloud/src/bridge/ExtensionChannel.ts +++ b/packages/cloud/src/bridge/ExtensionChannel.ts @@ -53,10 +53,7 @@ export class ExtensionChannel extends BaseChannel< this.setupListeners() } - /** - * Handle extension-specific commands from the web app - */ - public handleCommand(command: ExtensionBridgeCommand): void { + public async handleCommand(command: ExtensionBridgeCommand): Promise { if (command.instanceId !== this.instanceId) { console.log(`[ExtensionChannel] command -> instance id mismatch | ${this.instanceId}`, { messageInstanceId: command.instanceId, @@ -69,13 +66,22 @@ export class ExtensionChannel extends BaseChannel< console.log(`[ExtensionChannel] command -> createTask() | ${command.instanceId}`, { text: command.payload.text?.substring(0, 100) + "...", hasImages: !!command.payload.images, + mode: command.payload.mode, + providerProfile: command.payload.providerProfile, }) - this.provider.createTask(command.payload.text, command.payload.images) + this.provider.createTask( + command.payload.text, + command.payload.images, + undefined, // parentTask + undefined, // options + { mode: command.payload.mode, currentApiConfigName: command.payload.providerProfile }, + ) + break } case ExtensionBridgeCommandName.StopTask: { - const instance = this.updateInstance() + const instance = await this.updateInstance() if (instance.task.taskStatus === TaskStatus.Running) { console.log(`[ExtensionChannel] command -> cancelTask() | ${command.instanceId}`) @@ -86,6 +92,7 @@ export class ExtensionChannel extends BaseChannel< this.provider.clearTask() this.provider.postStateToWebview() } + break } case ExtensionBridgeCommandName.ResumeTask: { @@ -93,7 +100,6 @@ export class ExtensionChannel extends BaseChannel< taskId: command.payload.taskId, }) - // Resume the task from history by taskId this.provider.resumeTask(command.payload.taskId) this.provider.postStateToWebview() break @@ -122,12 +128,12 @@ export class ExtensionChannel extends BaseChannel< } private async registerInstance(_socket: Socket): Promise { - const instance = this.updateInstance() + const instance = await this.updateInstance() await this.publish(ExtensionSocketEvents.REGISTER, instance) } private async unregisterInstance(_socket: Socket): Promise { - const instance = this.updateInstance() + const instance = await this.updateInstance() await this.publish(ExtensionSocketEvents.UNREGISTER, instance) } @@ -135,7 +141,7 @@ export class ExtensionChannel extends BaseChannel< this.stopHeartbeat() this.heartbeatInterval = setInterval(async () => { - const instance = this.updateInstance() + const instance = await this.updateInstance() try { socket.emit(ExtensionSocketEvents.HEARTBEAT, instance) @@ -172,11 +178,11 @@ export class ExtensionChannel extends BaseChannel< ] as const eventMapping.forEach(({ from, to }) => { - // Create and store the listener function for cleanup/ - const listener = (..._args: unknown[]) => { + // Create and store the listener function for cleanup. + const listener = async (..._args: unknown[]) => { this.publish(ExtensionSocketEvents.EVENT, { type: to, - instance: this.updateInstance(), + instance: await this.updateInstance(), timestamp: Date.now(), }) } @@ -195,10 +201,16 @@ export class ExtensionChannel extends BaseChannel< this.eventListeners.clear() } - private updateInstance(): ExtensionInstance { + private async updateInstance(): Promise { const task = this.provider?.getCurrentTask() const taskHistory = this.provider?.getRecentTasks() ?? [] + const mode = await this.provider?.getMode() + const modes = (await this.provider?.getModes()) ?? [] + + const providerProfile = await this.provider?.getProviderProfile() + const providerProfiles = (await this.provider?.getProviderProfiles()) ?? [] + this.extensionInstance = { ...this.extensionInstance, appProperties: this.extensionInstance.appProperties ?? this.provider.appProperties, @@ -213,6 +225,10 @@ export class ExtensionChannel extends BaseChannel< : { taskId: "", taskStatus: TaskStatus.None }, taskAsk: task?.taskAsk, taskHistory, + mode, + providerProfile, + modes, + providerProfiles, } return this.extensionInstance diff --git a/packages/cloud/src/bridge/TaskChannel.ts b/packages/cloud/src/bridge/TaskChannel.ts index cf2a4a2516..f974a3e559 100644 --- a/packages/cloud/src/bridge/TaskChannel.ts +++ b/packages/cloud/src/bridge/TaskChannel.ts @@ -73,7 +73,7 @@ export class TaskChannel extends BaseChannel< super(instanceId) } - public handleCommand(command: TaskBridgeCommand): void { + public async handleCommand(command: TaskBridgeCommand): Promise { const task = this.subscribedTasks.get(command.taskId) if (!task) { @@ -87,7 +87,14 @@ export class TaskChannel extends BaseChannel< `[TaskChannel] ${TaskBridgeCommandName.Message} ${command.taskId} -> submitUserMessage()`, command, ) - task.submitUserMessage(command.payload.text, command.payload.images) + + await task.submitUserMessage( + command.payload.text, + command.payload.images, + command.payload.mode, + command.payload.providerProfile, + ) + break case TaskBridgeCommandName.ApproveAsk: @@ -95,6 +102,7 @@ export class TaskChannel extends BaseChannel< `[TaskChannel] ${TaskBridgeCommandName.ApproveAsk} ${command.taskId} -> approveAsk()`, command, ) + task.approveAsk(command.payload) break diff --git a/packages/cloud/src/bridge/__tests__/ExtensionChannel.test.ts b/packages/cloud/src/bridge/__tests__/ExtensionChannel.test.ts index 89979c9a66..7d25891840 100644 --- a/packages/cloud/src/bridge/__tests__/ExtensionChannel.test.ts +++ b/packages/cloud/src/bridge/__tests__/ExtensionChannel.test.ts @@ -53,6 +53,13 @@ describe("ExtensionChannel", () => { postStateToWebview: vi.fn(), postMessageToWebview: vi.fn(), getTelemetryProperties: vi.fn(), + getMode: vi.fn().mockResolvedValue("code"), + getModes: vi.fn().mockResolvedValue([ + { slug: "code", name: "Code", description: "Code mode" }, + { slug: "architect", name: "Architect", description: "Architect mode" }, + ]), + getProviderProfile: vi.fn().mockResolvedValue("default"), + getProviderProfiles: vi.fn().mockResolvedValue([{ name: "default", description: "Default profile" }]), on: vi.fn((event: keyof TaskProviderEvents, listener: (...args: unknown[]) => unknown) => { if (!eventListeners.has(event)) { eventListeners.set(event, new Set()) @@ -184,6 +191,9 @@ describe("ExtensionChannel", () => { // Connect the socket to enable publishing await extensionChannel.onConnect(mockSocket) + // Clear the mock calls from the connection (which emits a register event) + ;(mockSocket.emit as any).mockClear() + // Get a listener that was registered for TaskStarted const taskStartedListeners = eventListeners.get(RooCodeEventName.TaskStarted) expect(taskStartedListeners).toBeDefined() @@ -192,7 +202,7 @@ describe("ExtensionChannel", () => { // Trigger the listener const listener = Array.from(taskStartedListeners!)[0] if (listener) { - listener("test-task-id") + await listener("test-task-id") } // Verify the event was published to the socket diff --git a/packages/cloud/src/bridge/__tests__/TaskChannel.test.ts b/packages/cloud/src/bridge/__tests__/TaskChannel.test.ts index e69cb0ce3e..4a6aa72468 100644 --- a/packages/cloud/src/bridge/__tests__/TaskChannel.test.ts +++ b/packages/cloud/src/bridge/__tests__/TaskChannel.test.ts @@ -333,7 +333,12 @@ describe("TaskChannel", () => { taskChannel.handleCommand(command) - expect(mockTask.submitUserMessage).toHaveBeenCalledWith(command.payload.text, command.payload.images) + expect(mockTask.submitUserMessage).toHaveBeenCalledWith( + command.payload.text, + command.payload.images, + undefined, + undefined, + ) }) it("should handle ApproveAsk command", () => { diff --git a/packages/types/npm/package.metadata.json b/packages/types/npm/package.metadata.json index 1005327120..f5ccde8888 100644 --- a/packages/types/npm/package.metadata.json +++ b/packages/types/npm/package.metadata.json @@ -1,6 +1,6 @@ { "name": "@roo-code/types", - "version": "1.65.0", + "version": "1.66.0", "description": "TypeScript type definitions for Roo Code.", "publishConfig": { "access": "public", diff --git a/packages/types/src/cloud.ts b/packages/types/src/cloud.ts index dbf79b6bfa..44dec96271 100644 --- a/packages/types/src/cloud.ts +++ b/packages/types/src/cloud.ts @@ -378,6 +378,10 @@ export const extensionInstanceSchema = z.object({ task: extensionTaskSchema, taskAsk: clineMessageSchema.optional(), taskHistory: z.array(z.string()), + mode: z.string().optional(), + modes: z.array(z.object({ slug: z.string(), name: z.string() })).optional(), + providerProfile: z.string().optional(), + providerProfiles: z.array(z.object({ name: z.string(), provider: z.string().optional() })).optional(), }) export type ExtensionInstance = z.infer @@ -398,6 +402,9 @@ export enum ExtensionBridgeEventName { TaskResumable = RooCodeEventName.TaskResumable, TaskIdle = RooCodeEventName.TaskIdle, + ModeChanged = RooCodeEventName.ModeChanged, + ProviderProfileChanged = RooCodeEventName.ProviderProfileChanged, + InstanceRegistered = "instance_registered", InstanceUnregistered = "instance_unregistered", HeartbeatUpdated = "heartbeat_updated", @@ -469,6 +476,18 @@ export const extensionBridgeEventSchema = z.discriminatedUnion("type", [ instance: extensionInstanceSchema, timestamp: z.number(), }), + z.object({ + type: z.literal(ExtensionBridgeEventName.ModeChanged), + instance: extensionInstanceSchema, + mode: z.string(), + timestamp: z.number(), + }), + z.object({ + type: z.literal(ExtensionBridgeEventName.ProviderProfileChanged), + instance: extensionInstanceSchema, + providerProfile: z.object({ name: z.string(), provider: z.string().optional() }), + timestamp: z.number(), + }), ]) export type ExtensionBridgeEvent = z.infer @@ -490,6 +509,8 @@ export const extensionBridgeCommandSchema = z.discriminatedUnion("type", [ payload: z.object({ text: z.string(), images: z.array(z.string()).optional(), + mode: z.string().optional(), + providerProfile: z.string().optional(), }), timestamp: z.number(), }), @@ -502,9 +523,7 @@ export const extensionBridgeCommandSchema = z.discriminatedUnion("type", [ z.object({ type: z.literal(ExtensionBridgeCommandName.ResumeTask), instanceId: z.string(), - payload: z.object({ - taskId: z.string(), - }), + payload: z.object({ taskId: z.string() }), timestamp: z.number(), }), ]) @@ -558,6 +577,8 @@ export const taskBridgeCommandSchema = z.discriminatedUnion("type", [ payload: z.object({ text: z.string(), images: z.array(z.string()).optional(), + mode: z.string().optional(), + providerProfile: z.string().optional(), }), timestamp: z.number(), }), diff --git a/packages/types/src/task.ts b/packages/types/src/task.ts index 74597b5446..b005852119 100644 --- a/packages/types/src/task.ts +++ b/packages/types/src/task.ts @@ -2,7 +2,7 @@ import { z } from "zod" import { RooCodeEventName } from "./events.js" import type { RooCodeSettings } from "./global-settings.js" -import type { ClineMessage, QueuedMessage, TokenUsage } from "./message.js" +import type { ClineMessage, TokenUsage } from "./message.js" import type { ToolUsage, ToolName } from "./tool.js" import type { StaticAppProperties, GitProperties, TelemetryProperties } from "./telemetry.js" import type { TodoItem } from "./todo.js" @@ -59,6 +59,8 @@ export interface TaskProviderLike { export type TaskProviderEvents = { [RooCodeEventName.TaskCreated]: [task: TaskLike] + + // Proxied from the Task EventEmitter. [RooCodeEventName.TaskStarted]: [taskId: string] [RooCodeEventName.TaskCompleted]: [taskId: string, tokenUsage: TokenUsage, toolUsage: ToolUsage] [RooCodeEventName.TaskAborted]: [taskId: string] @@ -68,21 +70,14 @@ export type TaskProviderEvents = { [RooCodeEventName.TaskInteractive]: [taskId: string] [RooCodeEventName.TaskResumable]: [taskId: string] [RooCodeEventName.TaskIdle]: [taskId: string] - [RooCodeEventName.TaskPaused]: [taskId: string] - [RooCodeEventName.TaskUnpaused]: [taskId: string] [RooCodeEventName.TaskSpawned]: [taskId: string] - - [RooCodeEventName.TaskUserMessage]: [taskId: string] - - [RooCodeEventName.TaskTokenUsageUpdated]: [taskId: string, tokenUsage: TokenUsage] - [RooCodeEventName.ModeChanged]: [mode: string] [RooCodeEventName.ProviderProfileChanged]: [config: { name: string; provider?: string }] } /** - * TaskLike - */ + * TaskLike + */ export interface CreateTaskOptions { enableDiff?: boolean @@ -110,14 +105,11 @@ export type TaskMetadata = z.infer export interface TaskLike { readonly taskId: string - readonly rootTaskId?: string - readonly parentTaskId?: string - readonly childTaskId?: string - readonly metadata: TaskMetadata readonly taskStatus: TaskStatus readonly taskAsk: ClineMessage | undefined - readonly queuedMessages: QueuedMessage[] - readonly tokenUsage: TokenUsage | undefined + readonly metadata: TaskMetadata + + readonly rootTask?: TaskLike on(event: K, listener: (...args: TaskEvents[K]) => void | Promise): this off(event: K, listener: (...args: TaskEvents[K]) => void | Promise): this @@ -141,15 +133,14 @@ export type TaskEvents = { [RooCodeEventName.TaskIdle]: [taskId: string] // Subtask Lifecycle - [RooCodeEventName.TaskPaused]: [taskId: string] - [RooCodeEventName.TaskUnpaused]: [taskId: string] + [RooCodeEventName.TaskPaused]: [] + [RooCodeEventName.TaskUnpaused]: [] [RooCodeEventName.TaskSpawned]: [taskId: string] // Task Execution [RooCodeEventName.Message]: [{ action: "created" | "updated"; message: ClineMessage }] [RooCodeEventName.TaskModeSwitched]: [taskId: string, mode: string] [RooCodeEventName.TaskAskResponded]: [] - [RooCodeEventName.TaskUserMessage]: [taskId: string] // Task Analytics [RooCodeEventName.TaskToolFailed]: [taskId: string, tool: ToolName, error: string] diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 3e56712fda..8e7e76c8f0 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -10,6 +10,7 @@ import pWaitFor from "p-wait-for" import { serializeError } from "serialize-error" import { + type RooCodeSettings, type TaskLike, type TaskMetadata, type TaskEvents, @@ -23,6 +24,7 @@ import { type ClineAsk, type ToolProgressStatus, type HistoryItem, + type CreateTaskOptions, RooCodeEventName, TelemetryEventName, TaskStatus, @@ -111,7 +113,7 @@ const DEFAULT_USAGE_COLLECTION_TIMEOUT_MS = 5000 // 5 seconds const FORCED_CONTEXT_REDUCTION_PERCENT = 75 // Keep 75% of context (remove 25%) on context window errors const MAX_CONTEXT_WINDOW_RETRIES = 3 // Maximum retries for context window errors -export type TaskOptions = { +export interface TaskOptions extends CreateTaskOptions { provider: ClineProvider apiConfiguration: ProviderSettings enableDiff?: boolean @@ -847,7 +849,12 @@ export class Task extends EventEmitter implements TaskLike { this.handleWebviewAskResponse("noButtonClicked", text, images) } - public submitUserMessage(text: string, images?: string[]): void { + public async submitUserMessage( + text: string, + images?: string[], + mode?: string, + providerProfile?: string, + ): Promise { try { text = (text ?? "").trim() images = images ?? [] @@ -859,6 +866,14 @@ export class Task extends EventEmitter implements TaskLike { const provider = this.providerRef.deref() if (provider) { + if (mode) { + await provider.setMode(mode) + } + + if (providerProfile) { + await provider.setProviderProfile(providerProfile) + } + provider.postMessageToWebview({ type: "invoke", invoke: "sendMessage", text, images }) } else { console.error("[Task#submitUserMessage] Provider reference lost") diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 1e9aa28758..2fa6ffb43f 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -30,6 +30,7 @@ import { type TerminalActionPromptType, type HistoryItem, type CloudUserInfo, + type CreateTaskOptions, RooCodeEventName, requestyDefaultModelId, openRouterDefaultModelId, @@ -37,9 +38,10 @@ import { DEFAULT_TERMINAL_OUTPUT_CHARACTER_LIMIT, DEFAULT_WRITE_DELAY_MS, ORGANIZATION_ALLOW_ALL, + DEFAULT_MODES, } from "@roo-code/types" import { TelemetryService } from "@roo-code/telemetry" -import { CloudService, BridgeOrchestrator, getRooCodeApiUrl } from "@roo-code/cloud" +import { CloudService, getRooCodeApiUrl } from "@roo-code/cloud" import { Package } from "../../shared/package" import { findLast } from "../../shared/array" @@ -70,6 +72,7 @@ import { fileExistsAtPath } from "../../utils/fs" import { setTtsEnabled, setTtsSpeed } from "../../utils/tts" import { getWorkspaceGitInfo } from "../../utils/git" import { getWorkspacePath } from "../../utils/path" +import { OrganizationAllowListViolationError } from "../../utils/errors" import { setPanel } from "../../activate/registerCommands" @@ -81,7 +84,7 @@ import { forceFullModelDetailsLoad, hasLoadedFullDetails } from "../../api/provi import { ContextProxy } from "../config/ContextProxy" import { ProviderSettingsManager } from "../config/ProviderSettingsManager" import { CustomModesManager } from "../config/CustomModesManager" -import { Task, TaskOptions } from "../task/Task" +import { Task } from "../task/Task" import { getSystemPromptFilePath } from "../prompts/sections/custom-system-prompt" import { webviewMessageHandler } from "./webviewMessageHandler" @@ -266,27 +269,29 @@ export class ClineProvider } /** - * Synchronize cloud profiles with local profiles + * Synchronize cloud profiles with local profiles. */ private async syncCloudProfiles() { try { const settings = CloudService.instance.getOrganizationSettings() + if (!settings?.providerProfiles) { return } const currentApiConfigName = this.getGlobalState("currentApiConfigName") + const result = await this.providerSettingsManager.syncCloudProfiles( settings.providerProfiles, currentApiConfigName, ) if (result.hasChanges) { - // Update list + // Update list. await this.updateGlobalState("listApiConfigMeta", await this.providerSettingsManager.listConfig()) if (result.activeProfileChanged && result.activeProfileId) { - // Reload full settings for new active profile + // Reload full settings for new active profile. const profile = await this.providerSettingsManager.getProfile({ id: result.activeProfileId, }) @@ -376,17 +381,6 @@ export class ClineProvider } } - // returns the current cline object in the stack (the top one) - // if the stack is empty, returns undefined - getCurrentTask(): Task | undefined { - if (this.clineStack.length === 0) { - return undefined - } - - return this.clineStack[this.clineStack.length - 1] - } - - // returns the current clineStack length (how many cline objects are in the stack) getTaskStackSize(): number { return this.clineStack.length } @@ -409,58 +403,6 @@ export class ClineProvider await this.getCurrentTask()?.resumePausedTask(lastMessage) } - resumeTask(taskId: string): void { - // Use the existing showTaskWithId method which handles both current and historical tasks - this.showTaskWithId(taskId).catch((error) => { - this.log(`Failed to resume task ${taskId}: ${error.message}`) - }) - } - - getRecentTasks(): string[] { - if (this.recentTasksCache) { - return this.recentTasksCache - } - - const history = this.getGlobalState("taskHistory") ?? [] - const workspaceTasks: HistoryItem[] = [] - - for (const item of history) { - if (!item.ts || !item.task || item.workspace !== this.cwd) { - continue - } - - workspaceTasks.push(item) - } - - if (workspaceTasks.length === 0) { - this.recentTasksCache = [] - return this.recentTasksCache - } - - workspaceTasks.sort((a, b) => b.ts - a.ts) - let recentTaskIds: string[] = [] - - if (workspaceTasks.length >= 100) { - // If we have at least 100 tasks, return tasks from the last 7 days. - const sevenDaysAgo = Date.now() - 7 * 24 * 60 * 60 * 1000 - - for (const item of workspaceTasks) { - // Stop when we hit tasks older than 7 days. - if (item.ts < sevenDaysAgo) { - break - } - - recentTaskIds.push(item.id) - } - } else { - // Otherwise, return the most recent 100 tasks (or all if less than 100). - recentTaskIds = workspaceTasks.slice(0, Math.min(100, workspaceTasks.length)).map((item) => item.id) - } - - this.recentTasksCache = recentTaskIds - return this.recentTasksCache - } - /* VSCode extensions use the disposable pattern to clean up resources when the sidebar/editor tab is closed by the user or system. This applies to event listening, commands, interacting with the UI, etc. - https://vscode-docs.readthedocs.io/en/stable/extensions/patterns-and-principles/ @@ -741,84 +683,19 @@ export class ClineProvider await this.removeClineFromStack() } - // When initializing a new task, (not from history but from a tool command - // new_task) there is no need to remove the previous task since the new - // task is a subtask of the previous one, and when it finishes it is removed - // from the stack and the caller is resumed in this way we can have a chain - // of tasks, each one being a sub task of the previous one until the main - // task is finished. - public async createTask( - text?: string, - images?: string[], - parentTask?: Task, - options: Partial< - Pick< - TaskOptions, - | "enableDiff" - | "enableCheckpoints" - | "fuzzyMatchThreshold" - | "consecutiveMistakeLimit" - | "experiments" - | "initialTodos" - > - > = {}, - ) { - const { - apiConfiguration, - organizationAllowList, - diffEnabled: enableDiff, - enableCheckpoints, - fuzzyMatchThreshold, - experiments, - cloudUserInfo, - remoteControlEnabled, - } = await this.getState() - - if (!ProfileValidator.isProfileAllowed(apiConfiguration, organizationAllowList)) { - throw new OrganizationAllowListViolationError(t("common:errors.violated_organization_allowlist")) - } - - const task = new Task({ - provider: this, - apiConfiguration, - enableDiff, - enableCheckpoints, - fuzzyMatchThreshold, - consecutiveMistakeLimit: apiConfiguration.consecutiveMistakeLimit, - task: text, - images, - experiments, - rootTask: this.clineStack.length > 0 ? this.clineStack[0] : undefined, - parentTask, - taskNumber: this.clineStack.length + 1, - onCreated: this.taskCreationCallback, - enableBridge: BridgeOrchestrator.isEnabled(cloudUserInfo, remoteControlEnabled), - initialTodos: options.initialTodos, - ...options, - }) - - await this.addClineToStack(task) - - this.log( - `[createTask] ${task.parentTask ? "child" : "parent"} task ${task.taskId}.${task.instanceId} instantiated`, - ) - - return task - } - - public async createTaskWithHistoryItem( +public async createTaskWithHistoryItem( historyItem: HistoryItem & { rootTask?: Task; parentTask?: Task; preservedFCOState?: any }, ) { await this.removeClineFromStack() - // If the history item has a saved mode, restore it and its associated API configuration + // If the history item has a saved mode, restore it and its associated API configuration. if (historyItem.mode) { // Validate that the mode still exists const customModes = await this.customModesManager.getCustomModes() const modeExists = getModeBySlug(historyItem.mode, customModes) !== undefined if (!modeExists) { - // Mode no longer exists, fall back to default mode + // Mode no longer exists, fall back to default mode. this.log( `Mode '${historyItem.mode}' from history no longer exists. Falling back to default mode '${defaultModeSlug}'.`, ) @@ -827,14 +704,14 @@ export class ClineProvider await this.updateGlobalState("mode", historyItem.mode) - // Load the saved API config for the restored mode if it exists + // Load the saved API config for the restored mode if it exists. const savedConfigId = await this.providerSettingsManager.getModeConfigId(historyItem.mode) const listApiConfig = await this.providerSettingsManager.listConfig() - // Update listApiConfigMeta first to ensure UI has latest data + // Update listApiConfigMeta first to ensure UI has latest data. await this.updateGlobalState("listApiConfigMeta", listApiConfig) - // If this mode has a saved config, use it + // If this mode has a saved config, use it. if (savedConfigId) { const profile = listApiConfig.find(({ id }) => id === savedConfigId) @@ -842,13 +719,13 @@ export class ClineProvider try { await this.activateProviderProfile({ name: profile.name }) } catch (error) { - // Log the error but continue with task restoration + // Log the error but continue with task restoration. this.log( `Failed to restore API configuration for mode '${historyItem.mode}': ${ error instanceof Error ? error.message : String(error) }. Continuing with default configuration.`, ) - // The task will continue with the current/default configuration + // The task will continue with the current/default configuration. } } } @@ -877,7 +754,7 @@ export class ClineProvider parentTask: historyItem.parentTask, taskNumber: historyItem.number, onCreated: this.taskCreationCallback, - enableBridge: BridgeOrchestrator.isEnabled(cloudUserInfo, remoteControlEnabled), + enableBridge: false, // BridgeOrchestrator removed in main }) await this.addClineToStack(task) @@ -1127,39 +1004,39 @@ export class ClineProvider TelemetryService.instance.captureModeSwitch(cline.taskId, newMode) cline.emit(RooCodeEventName.TaskModeSwitched, cline.taskId, newMode) - // Store the current mode in case we need to rollback - const previousMode = (cline as any)._taskMode - try { - // Update the task history with the new mode first + // Update the task history with the new mode first. const history = this.getGlobalState("taskHistory") ?? [] const taskHistoryItem = history.find((item) => item.id === cline.taskId) + if (taskHistoryItem) { taskHistoryItem.mode = newMode await this.updateTaskHistory(taskHistoryItem) } - // Only update the task's mode after successful persistence + // Only update the task's mode after successful persistence. ;(cline as any)._taskMode = newMode } catch (error) { - // If persistence fails, log the error but don't update the in-memory state + // If persistence fails, log the error but don't update the in-memory state. this.log( `Failed to persist mode switch for task ${cline.taskId}: ${error instanceof Error ? error.message : String(error)}`, ) - // Optionally, we could emit an event to notify about the failure - // This ensures the in-memory state remains consistent with persisted state + // Optionally, we could emit an event to notify about the failure. + // This ensures the in-memory state remains consistent with persisted state. throw error } } await this.updateGlobalState("mode", newMode) - // Load the saved API config for the new mode if it exists + this.emit(RooCodeEventName.ModeChanged, newMode) + + // Load the saved API config for the new mode if it exists. const savedConfigId = await this.providerSettingsManager.getModeConfigId(newMode) const listApiConfig = await this.providerSettingsManager.listConfig() - // Update listApiConfigMeta first to ensure UI has latest data + // Update listApiConfigMeta first to ensure UI has latest data. await this.updateGlobalState("listApiConfigMeta", listApiConfig) // If this mode has a saved config, use it. @@ -1302,73 +1179,75 @@ export class ClineProvider } await this.postStateToWebview() + + if (providerSettings.apiProvider) { + this.emit(RooCodeEventName.ProviderProfileChanged, { name, provider: providerSettings.apiProvider }) + } +} + +public async cancelTask(): Promise { + const cline = this.getCurrentTask() + + if (!cline) { + return } - // Task Management + console.log(`[cancelTask] cancelling task ${cline.taskId}.${cline.instanceId}`) - async cancelTask() { - const cline = this.getCurrentTask() + const { historyItem } = await this.getTaskWithId(cline.taskId) + // Preserve parent and root task information for history item. + const rootTask = cline.rootTask + const parentTask = cline.parentTask - if (!cline) { - return + // Preserve FCO state before aborting task to prevent FCO from disappearing + let preservedFCOState: any = undefined + try { + const fileChangeManager = this.getFileChangeManager() + if (fileChangeManager) { + preservedFCOState = fileChangeManager.getChanges() + this.log(`[cancelTask] Preserved FCO state with ${preservedFCOState.files.length} files`) } - - console.log(`[cancelTask] cancelling task ${cline.taskId}.${cline.instanceId}`) - - const { historyItem } = await this.getTaskWithId(cline.taskId) - // Preserve parent and root task information for history item. - const rootTask = cline.rootTask - const parentTask = cline.parentTask - - // Preserve FCO state before aborting task to prevent FCO from disappearing - let preservedFCOState: any = undefined - try { - const fileChangeManager = this.getFileChangeManager() - if (fileChangeManager) { - preservedFCOState = fileChangeManager.getChanges() - this.log(`[cancelTask] Preserved FCO state with ${preservedFCOState.files.length} files`) - } - } catch (error) { - this.log(`[cancelTask] Failed to preserve FCO state: ${error}`) - } - - cline.abortTask() - - await pWaitFor( - () => - this.getCurrentTask()! === undefined || - this.getCurrentTask()!.isStreaming === false || - this.getCurrentTask()!.didFinishAbortingStream || - // If only the first chunk is processed, then there's no - // need to wait for graceful abort (closes edits, browser, - // etc). - this.getCurrentTask()!.isWaitingForFirstChunk, - { - timeout: 3_000, - }, - ).catch(() => { - console.error("Failed to abort task") - }) - - if (this.getCurrentTask()) { - // 'abandoned' will prevent this Cline instance from affecting - // future Cline instances. This may happen if its hanging on a - // streaming request. - this.getCurrentTask()!.abandoned = true - } - - // Clears task again, so we need to abortTask manually above. - await this.createTaskWithHistoryItem({ ...historyItem, rootTask, parentTask, preservedFCOState }) + } catch (error) { + this.log(`[cancelTask] Failed to preserve FCO state: ${error}`) } - // Clear the current task without treating it as a subtask. - // This is used when the user cancels a task that is not a subtask. - async clearTask() { - if (this.clineStack.length > 0) { - const task = this.clineStack[this.clineStack.length - 1] - console.log(`[clearTask] clearing task ${task.taskId}.${task.instanceId}`) - await this.removeClineFromStack() - } + cline.abortTask() + + await pWaitFor( + () => + this.getCurrentTask()! === undefined || + this.getCurrentTask()!.isStreaming === false || + this.getCurrentTask()!.didFinishAbortingStream || + // If only the first chunk is processed, then there's no + // need to wait for graceful abort (closes edits, browser, + // etc). + this.getCurrentTask()!.isWaitingForFirstChunk, + { + timeout: 3_000, + }, + ).catch(() => { + console.error("Failed to abort task") + }) + + if (this.getCurrentTask()) { + // 'abandoned' will prevent this Cline instance from affecting + // future Cline instances. This may happen if its hanging on a + // streaming request. + this.getCurrentTask()!.abandoned = true + } + + // Clears task again, so we need to abortTask manually above. + await this.createTaskWithHistoryItem({ ...historyItem, rootTask, parentTask, preservedFCOState }) +} + +// Clear the current task without treating it as a subtask. +// This is used when the user cancels a task that is not a subtask. +public async clearTask(): Promise { + if (this.clineStack.length > 0) { + const task = this.clineStack[this.clineStack.length - 1] + console.log(`[clearTask] clearing task ${task.taskId}.${task.instanceId}`) + await this.removeClineFromStack() + } } async updateCustomInstructions(instructions?: string) { @@ -2143,18 +2022,8 @@ export class ClineProvider maxDiagnosticMessages: stateValues.maxDiagnosticMessages ?? 50, // Add includeTaskHistoryInEnhance setting includeTaskHistoryInEnhance: stateValues.includeTaskHistoryInEnhance ?? true, - // Add remoteControlEnabled setting - get from cloud settings - remoteControlEnabled: (() => { - try { - const cloudSettings = CloudService.instance.getUserSettings() - return cloudSettings?.settings?.extensionBridgeEnabled ?? false - } catch (error) { - console.error( - `[getState] failed to get remote control setting from cloud: ${error instanceof Error ? error.message : String(error)}`, - ) - return false - } - })(), + // Remote control functionality removed in main branch + remoteControlEnabled: false, // Add image generation settings openRouterImageApiKey: stateValues.openRouterImageApiKey, openRouterImageGenerationSelectedModel: stateValues.openRouterImageGenerationSelectedModel, @@ -2205,12 +2074,6 @@ export class ClineProvider await this.contextProxy.setValues(values) } - // cwd - - get cwd() { - return getWorkspacePath() - } - // dev async resetState() { @@ -2285,43 +2148,252 @@ export class ClineProvider return } - await BridgeOrchestrator.connectOrDisconnect(userInfo, enabled, { - ...config, - provider: this, - sessionId: vscode.env.sessionId, - }) + // BridgeOrchestrator functionality removed in main branch + this.log(`[ClineProvider#remoteControlEnabled] Remote control ${enabled ? 'enabled' : 'disabled'}`) + } - const bridge = BridgeOrchestrator.getInstance() + /** + * Gets the CodeIndexManager for the current active workspace + * @returns CodeIndexManager instance for the current workspace or the default one + */ + public getCurrentWorkspaceCodeIndexManager(): CodeIndexManager | undefined { + return CodeIndexManager.getInstance(this.context) + } - if (bridge) { - const currentTask = this.getCurrentTask() + /** + * Updates the code index status subscription to listen to the current workspace manager + */ + private updateCodeIndexStatusSubscription(): void { + // Get the current workspace manager + const currentManager = this.getCurrentWorkspaceCodeIndexManager() - if (currentTask && !currentTask.enableBridge) { - try { - currentTask.enableBridge = true - await BridgeOrchestrator.subscribeToTask(currentTask) - } catch (error) { - const message = `[ClineProvider#remoteControlEnabled] BridgeOrchestrator.subscribeToTask() failed: ${error instanceof Error ? error.message : String(error)}` - this.log(message) - console.error(message) - } - } - } else { - for (const task of this.clineStack) { - if (task.enableBridge) { - try { - await BridgeOrchestrator.getInstance()?.unsubscribeFromTask(task.taskId) - } catch (error) { - const message = `[ClineProvider#remoteControlEnabled] BridgeOrchestrator#unsubscribeFromTask() failed: ${error instanceof Error ? error.message : String(error)}` - this.log(message) - console.error(message) - } + // If the manager hasn't changed, no need to update subscription + if (currentManager === this.currentWorkspaceManager) { + return + } + + // Dispose the old subscription if it exists + if (this.codeIndexStatusSubscription) { + this.codeIndexStatusSubscription.dispose() + this.codeIndexStatusSubscription = undefined + } + + // Update the current workspace manager reference + this.currentWorkspaceManager = currentManager + + // Subscribe to the new manager's progress updates if it exists + if (currentManager) { + this.codeIndexStatusSubscription = currentManager.onProgressUpdate((update: IndexProgressUpdate) => { + // Only send updates if this manager is still the current one + if (currentManager === this.getCurrentWorkspaceCodeIndexManager()) { + // Get the full status from the manager to ensure we have all fields correctly formatted + const fullStatus = currentManager.getCurrentStatus() + this.postMessageToWebview({ + type: "indexingStatusUpdate", + values: fullStatus, + }) } + }) + + if (this.view) { + this.webviewDisposables.push(this.codeIndexStatusSubscription) } + + // Send initial status for the current workspace + this.postMessageToWebview({ + type: "indexingStatusUpdate", + values: currentManager.getCurrentStatus(), + }) } } + /** + * TaskProviderLike, TelemetryPropertiesProvider + */ + + public getCurrentTask(): Task | undefined { + if (this.clineStack.length === 0) { + return undefined + } + + return this.clineStack[this.clineStack.length - 1] + } + + public getRecentTasks(): string[] { + if (this.recentTasksCache) { + return this.recentTasksCache + } + + const history = this.getGlobalState("taskHistory") ?? [] + const workspaceTasks: HistoryItem[] = [] + + for (const item of history) { + if (!item.ts || !item.task || item.workspace !== this.cwd) { + continue + } + + workspaceTasks.push(item) + } + + if (workspaceTasks.length === 0) { + this.recentTasksCache = [] + return this.recentTasksCache + } + + workspaceTasks.sort((a, b) => b.ts - a.ts) + let recentTaskIds: string[] = [] + + if (workspaceTasks.length >= 100) { + // If we have at least 100 tasks, return tasks from the last 7 days. + const sevenDaysAgo = Date.now() - 7 * 24 * 60 * 60 * 1000 + + for (const item of workspaceTasks) { + // Stop when we hit tasks older than 7 days. + if (item.ts < sevenDaysAgo) { + break + } + + recentTaskIds.push(item.id) + } + } else { + // Otherwise, return the most recent 100 tasks (or all if less than 100). + recentTaskIds = workspaceTasks.slice(0, Math.min(100, workspaceTasks.length)).map((item) => item.id) + } + + this.recentTasksCache = recentTaskIds + return this.recentTasksCache + } + + // When initializing a new task, (not from history but from a tool command + // new_task) there is no need to remove the previous task since the new + // task is a subtask of the previous one, and when it finishes it is removed + // from the stack and the caller is resumed in this way we can have a chain + // of tasks, each one being a sub task of the previous one until the main + // task is finished. + public async createTask( + text?: string, + images?: string[], + parentTask?: Task, + options: CreateTaskOptions = {}, + configuration: RooCodeSettings = {}, + ): Promise { + if (configuration) { + await this.setValues(configuration) + + if (configuration.allowedCommands) { + await vscode.workspace + .getConfiguration(Package.name) + .update("allowedCommands", configuration.allowedCommands, vscode.ConfigurationTarget.Global) + } + + if (configuration.deniedCommands) { + await vscode.workspace + .getConfiguration(Package.name) + .update("deniedCommands", configuration.deniedCommands, vscode.ConfigurationTarget.Global) + } + + if (configuration.commandExecutionTimeout !== undefined) { + await vscode.workspace + .getConfiguration(Package.name) + .update( + "commandExecutionTimeout", + configuration.commandExecutionTimeout, + vscode.ConfigurationTarget.Global, + ) + } + + if (configuration.currentApiConfigName) { + await this.setProviderProfile(configuration.currentApiConfigName) + } + } + + const { + apiConfiguration, + organizationAllowList, + diffEnabled: enableDiff, + enableCheckpoints, + fuzzyMatchThreshold, + experiments, + cloudUserInfo, + remoteControlEnabled, + } = await this.getState() + + if (!ProfileValidator.isProfileAllowed(apiConfiguration, organizationAllowList)) { + throw new OrganizationAllowListViolationError(t("common:errors.violated_organization_allowlist")) + } + + const task = new Task({ + provider: this, + apiConfiguration, + enableDiff, + enableCheckpoints, + fuzzyMatchThreshold, + consecutiveMistakeLimit: apiConfiguration.consecutiveMistakeLimit, + task: text, + images, + experiments, + rootTask: this.clineStack.length > 0 ? this.clineStack[0] : undefined, + parentTask, + taskNumber: this.clineStack.length + 1, + onCreated: this.taskCreationCallback, + enableBridge: false, // BridgeOrchestrator removed in main + initialTodos: options.initialTodos, + ...options, + }) + + await this.addClineToStack(task) + + this.log( + `[createTask] ${task.parentTask ? "child" : "parent"} task ${task.taskId}.${task.instanceId} instantiated`, + ) + + return task + } + + + public resumeTask(taskId: string): void { + // Use the existing showTaskWithId method which handles both current and + // historical tasks. + this.showTaskWithId(taskId).catch((error) => { + this.log(`Failed to resume task ${taskId}: ${error.message}`) + }) + } + + // Modes + + public async getModes(): Promise<{ slug: string; name: string }[]> { + return DEFAULT_MODES.map((mode) => ({ slug: mode.slug, name: mode.name })) + } + + public async getMode(): Promise { + const { mode } = await this.getState() + return mode + } + + public async setMode(mode: string): Promise { + await this.setValues({ mode }) + } + + // Provider Profiles + + public async getProviderProfiles(): Promise<{ name: string; provider?: string }[]> { + const { listApiConfigMeta } = await this.getState() + return listApiConfigMeta.map((profile) => ({ name: profile.name, provider: profile.apiProvider })) + } + + public async getProviderProfile(): Promise { + const { currentApiConfigName } = await this.getState() + return currentApiConfigName + } + + public async setProviderProfile(name: string): Promise { + await this.activateProviderProfile({ name }) + } + + // Telemetry + private _appProperties?: StaticAppProperties + private _gitProperties?: GitProperties private getAppProperties(): StaticAppProperties { if (!this._appProperties) { @@ -2388,8 +2460,6 @@ export class ClineProvider } } - private _gitProperties?: GitProperties - private async getGitProperties(): Promise { if (!this._gitProperties) { this._gitProperties = await getWorkspaceGitInfo() @@ -2411,61 +2481,6 @@ export class ClineProvider } } - /** - * Gets the CodeIndexManager for the current active workspace - * @returns CodeIndexManager instance for the current workspace or the default one - */ - public getCurrentWorkspaceCodeIndexManager(): CodeIndexManager | undefined { - return CodeIndexManager.getInstance(this.context) - } - - /** - * Updates the code index status subscription to listen to the current workspace manager - */ - private updateCodeIndexStatusSubscription(): void { - // Get the current workspace manager - const currentManager = this.getCurrentWorkspaceCodeIndexManager() - - // If the manager hasn't changed, no need to update subscription - if (currentManager === this.currentWorkspaceManager) { - return - } - - // Dispose the old subscription if it exists - if (this.codeIndexStatusSubscription) { - this.codeIndexStatusSubscription.dispose() - this.codeIndexStatusSubscription = undefined - } - - // Update the current workspace manager reference - this.currentWorkspaceManager = currentManager - - // Subscribe to the new manager's progress updates if it exists - if (currentManager) { - this.codeIndexStatusSubscription = currentManager.onProgressUpdate((update: IndexProgressUpdate) => { - // Only send updates if this manager is still the current one - if (currentManager === this.getCurrentWorkspaceCodeIndexManager()) { - // Get the full status from the manager to ensure we have all fields correctly formatted - const fullStatus = currentManager.getCurrentStatus() - this.postMessageToWebview({ - type: "indexingStatusUpdate", - values: fullStatus, - }) - } - }) - - if (this.view) { - this.webviewDisposables.push(this.codeIndexStatusSubscription) - } - - // Send initial status for the current workspace - this.postMessageToWebview({ - type: "indexingStatusUpdate", - values: currentManager.getCurrentStatus(), - }) - } - } - public getFileChangeManager(): | import("../../services/file-changes/FileChangeManager").FileChangeManager | undefined { @@ -2481,10 +2496,8 @@ export class ClineProvider } return this.globalFileChangeManager } -} -class OrganizationAllowListViolationError extends Error { - constructor(message: string) { - super(message) + public get cwd() { + return getWorkspacePath() } }