Browse Source

Mode and provider profile selector (#7545)

Chris Estreich 4 months ago
parent
commit
20929b0f16

+ 1 - 1
packages/cloud/src/bridge/BaseChannel.ts

@@ -83,7 +83,7 @@ export abstract class BaseChannel<TCommand = unknown, TEventName extends string
 	/**
 	/**
 	 * Handle incoming commands - must be implemented by subclasses.
 	 * Handle incoming commands - must be implemented by subclasses.
 	 */
 	 */
-	public abstract handleCommand(command: TCommand): void
+	public abstract handleCommand(command: TCommand): Promise<void>
 
 
 	/**
 	/**
 	 * Handle connection-specific logic.
 	 * Handle connection-specific logic.

+ 30 - 14
packages/cloud/src/bridge/ExtensionChannel.ts

@@ -53,10 +53,7 @@ export class ExtensionChannel extends BaseChannel<
 		this.setupListeners()
 		this.setupListeners()
 	}
 	}
 
 
-	/**
-	 * Handle extension-specific commands from the web app
-	 */
-	public handleCommand(command: ExtensionBridgeCommand): void {
+	public async handleCommand(command: ExtensionBridgeCommand): Promise<void> {
 		if (command.instanceId !== this.instanceId) {
 		if (command.instanceId !== this.instanceId) {
 			console.log(`[ExtensionChannel] command -> instance id mismatch | ${this.instanceId}`, {
 			console.log(`[ExtensionChannel] command -> instance id mismatch | ${this.instanceId}`, {
 				messageInstanceId: command.instanceId,
 				messageInstanceId: command.instanceId,
@@ -69,13 +66,22 @@ export class ExtensionChannel extends BaseChannel<
 				console.log(`[ExtensionChannel] command -> createTask() | ${command.instanceId}`, {
 				console.log(`[ExtensionChannel] command -> createTask() | ${command.instanceId}`, {
 					text: command.payload.text?.substring(0, 100) + "...",
 					text: command.payload.text?.substring(0, 100) + "...",
 					hasImages: !!command.payload.images,
 					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
 				break
 			}
 			}
 			case ExtensionBridgeCommandName.StopTask: {
 			case ExtensionBridgeCommandName.StopTask: {
-				const instance = this.updateInstance()
+				const instance = await this.updateInstance()
 
 
 				if (instance.task.taskStatus === TaskStatus.Running) {
 				if (instance.task.taskStatus === TaskStatus.Running) {
 					console.log(`[ExtensionChannel] command -> cancelTask() | ${command.instanceId}`)
 					console.log(`[ExtensionChannel] command -> cancelTask() | ${command.instanceId}`)
@@ -86,6 +92,7 @@ export class ExtensionChannel extends BaseChannel<
 					this.provider.clearTask()
 					this.provider.clearTask()
 					this.provider.postStateToWebview()
 					this.provider.postStateToWebview()
 				}
 				}
+
 				break
 				break
 			}
 			}
 			case ExtensionBridgeCommandName.ResumeTask: {
 			case ExtensionBridgeCommandName.ResumeTask: {
@@ -93,7 +100,6 @@ export class ExtensionChannel extends BaseChannel<
 					taskId: command.payload.taskId,
 					taskId: command.payload.taskId,
 				})
 				})
 
 
-				// Resume the task from history by taskId
 				this.provider.resumeTask(command.payload.taskId)
 				this.provider.resumeTask(command.payload.taskId)
 				this.provider.postStateToWebview()
 				this.provider.postStateToWebview()
 				break
 				break
@@ -122,12 +128,12 @@ export class ExtensionChannel extends BaseChannel<
 	}
 	}
 
 
 	private async registerInstance(_socket: Socket): Promise<void> {
 	private async registerInstance(_socket: Socket): Promise<void> {
-		const instance = this.updateInstance()
+		const instance = await this.updateInstance()
 		await this.publish(ExtensionSocketEvents.REGISTER, instance)
 		await this.publish(ExtensionSocketEvents.REGISTER, instance)
 	}
 	}
 
 
 	private async unregisterInstance(_socket: Socket): Promise<void> {
 	private async unregisterInstance(_socket: Socket): Promise<void> {
-		const instance = this.updateInstance()
+		const instance = await this.updateInstance()
 		await this.publish(ExtensionSocketEvents.UNREGISTER, instance)
 		await this.publish(ExtensionSocketEvents.UNREGISTER, instance)
 	}
 	}
 
 
@@ -135,7 +141,7 @@ export class ExtensionChannel extends BaseChannel<
 		this.stopHeartbeat()
 		this.stopHeartbeat()
 
 
 		this.heartbeatInterval = setInterval(async () => {
 		this.heartbeatInterval = setInterval(async () => {
-			const instance = this.updateInstance()
+			const instance = await this.updateInstance()
 
 
 			try {
 			try {
 				socket.emit(ExtensionSocketEvents.HEARTBEAT, instance)
 				socket.emit(ExtensionSocketEvents.HEARTBEAT, instance)
@@ -172,11 +178,11 @@ export class ExtensionChannel extends BaseChannel<
 		] as const
 		] as const
 
 
 		eventMapping.forEach(({ from, to }) => {
 		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, {
 				this.publish(ExtensionSocketEvents.EVENT, {
 					type: to,
 					type: to,
-					instance: this.updateInstance(),
+					instance: await this.updateInstance(),
 					timestamp: Date.now(),
 					timestamp: Date.now(),
 				})
 				})
 			}
 			}
@@ -195,10 +201,16 @@ export class ExtensionChannel extends BaseChannel<
 		this.eventListeners.clear()
 		this.eventListeners.clear()
 	}
 	}
 
 
-	private updateInstance(): ExtensionInstance {
+	private async updateInstance(): Promise<ExtensionInstance> {
 		const task = this.provider?.getCurrentTask()
 		const task = this.provider?.getCurrentTask()
 		const taskHistory = this.provider?.getRecentTasks() ?? []
 		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 = {
 			...this.extensionInstance,
 			...this.extensionInstance,
 			appProperties: this.extensionInstance.appProperties ?? this.provider.appProperties,
 			appProperties: this.extensionInstance.appProperties ?? this.provider.appProperties,
@@ -213,6 +225,10 @@ export class ExtensionChannel extends BaseChannel<
 				: { taskId: "", taskStatus: TaskStatus.None },
 				: { taskId: "", taskStatus: TaskStatus.None },
 			taskAsk: task?.taskAsk,
 			taskAsk: task?.taskAsk,
 			taskHistory,
 			taskHistory,
+			mode,
+			providerProfile,
+			modes,
+			providerProfiles,
 		}
 		}
 
 
 		return this.extensionInstance
 		return this.extensionInstance

+ 10 - 2
packages/cloud/src/bridge/TaskChannel.ts

@@ -73,7 +73,7 @@ export class TaskChannel extends BaseChannel<
 		super(instanceId)
 		super(instanceId)
 	}
 	}
 
 
-	public handleCommand(command: TaskBridgeCommand): void {
+	public async handleCommand(command: TaskBridgeCommand): Promise<void> {
 		const task = this.subscribedTasks.get(command.taskId)
 		const task = this.subscribedTasks.get(command.taskId)
 
 
 		if (!task) {
 		if (!task) {
@@ -87,7 +87,14 @@ export class TaskChannel extends BaseChannel<
 					`[TaskChannel] ${TaskBridgeCommandName.Message} ${command.taskId} -> submitUserMessage()`,
 					`[TaskChannel] ${TaskBridgeCommandName.Message} ${command.taskId} -> submitUserMessage()`,
 					command,
 					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
 				break
 
 
 			case TaskBridgeCommandName.ApproveAsk:
 			case TaskBridgeCommandName.ApproveAsk:
@@ -95,6 +102,7 @@ export class TaskChannel extends BaseChannel<
 					`[TaskChannel] ${TaskBridgeCommandName.ApproveAsk} ${command.taskId} -> approveAsk()`,
 					`[TaskChannel] ${TaskBridgeCommandName.ApproveAsk} ${command.taskId} -> approveAsk()`,
 					command,
 					command,
 				)
 				)
+
 				task.approveAsk(command.payload)
 				task.approveAsk(command.payload)
 				break
 				break
 
 

+ 11 - 1
packages/cloud/src/bridge/__tests__/ExtensionChannel.test.ts

@@ -53,6 +53,13 @@ describe("ExtensionChannel", () => {
 			postStateToWebview: vi.fn(),
 			postStateToWebview: vi.fn(),
 			postMessageToWebview: vi.fn(),
 			postMessageToWebview: vi.fn(),
 			getTelemetryProperties: 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) => {
 			on: vi.fn((event: keyof TaskProviderEvents, listener: (...args: unknown[]) => unknown) => {
 				if (!eventListeners.has(event)) {
 				if (!eventListeners.has(event)) {
 					eventListeners.set(event, new Set())
 					eventListeners.set(event, new Set())
@@ -184,6 +191,9 @@ describe("ExtensionChannel", () => {
 			// Connect the socket to enable publishing
 			// Connect the socket to enable publishing
 			await extensionChannel.onConnect(mockSocket)
 			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
 			// Get a listener that was registered for TaskStarted
 			const taskStartedListeners = eventListeners.get(RooCodeEventName.TaskStarted)
 			const taskStartedListeners = eventListeners.get(RooCodeEventName.TaskStarted)
 			expect(taskStartedListeners).toBeDefined()
 			expect(taskStartedListeners).toBeDefined()
@@ -192,7 +202,7 @@ describe("ExtensionChannel", () => {
 			// Trigger the listener
 			// Trigger the listener
 			const listener = Array.from(taskStartedListeners!)[0]
 			const listener = Array.from(taskStartedListeners!)[0]
 			if (listener) {
 			if (listener) {
-				listener("test-task-id")
+				await listener("test-task-id")
 			}
 			}
 
 
 			// Verify the event was published to the socket
 			// Verify the event was published to the socket

+ 6 - 1
packages/cloud/src/bridge/__tests__/TaskChannel.test.ts

@@ -333,7 +333,12 @@ describe("TaskChannel", () => {
 
 
 			taskChannel.handleCommand(command)
 			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", () => {
 		it("should handle ApproveAsk command", () => {

+ 1 - 1
packages/types/npm/package.metadata.json

@@ -1,6 +1,6 @@
 {
 {
 	"name": "@roo-code/types",
 	"name": "@roo-code/types",
-	"version": "1.65.0",
+	"version": "1.66.0",
 	"description": "TypeScript type definitions for Roo Code.",
 	"description": "TypeScript type definitions for Roo Code.",
 	"publishConfig": {
 	"publishConfig": {
 		"access": "public",
 		"access": "public",

+ 24 - 3
packages/types/src/cloud.ts

@@ -378,6 +378,10 @@ export const extensionInstanceSchema = z.object({
 	task: extensionTaskSchema,
 	task: extensionTaskSchema,
 	taskAsk: clineMessageSchema.optional(),
 	taskAsk: clineMessageSchema.optional(),
 	taskHistory: z.array(z.string()),
 	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<typeof extensionInstanceSchema>
 export type ExtensionInstance = z.infer<typeof extensionInstanceSchema>
@@ -398,6 +402,9 @@ export enum ExtensionBridgeEventName {
 	TaskResumable = RooCodeEventName.TaskResumable,
 	TaskResumable = RooCodeEventName.TaskResumable,
 	TaskIdle = RooCodeEventName.TaskIdle,
 	TaskIdle = RooCodeEventName.TaskIdle,
 
 
+	ModeChanged = RooCodeEventName.ModeChanged,
+	ProviderProfileChanged = RooCodeEventName.ProviderProfileChanged,
+
 	InstanceRegistered = "instance_registered",
 	InstanceRegistered = "instance_registered",
 	InstanceUnregistered = "instance_unregistered",
 	InstanceUnregistered = "instance_unregistered",
 	HeartbeatUpdated = "heartbeat_updated",
 	HeartbeatUpdated = "heartbeat_updated",
@@ -469,6 +476,18 @@ export const extensionBridgeEventSchema = z.discriminatedUnion("type", [
 		instance: extensionInstanceSchema,
 		instance: extensionInstanceSchema,
 		timestamp: z.number(),
 		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<typeof extensionBridgeEventSchema>
 export type ExtensionBridgeEvent = z.infer<typeof extensionBridgeEventSchema>
@@ -490,6 +509,8 @@ export const extensionBridgeCommandSchema = z.discriminatedUnion("type", [
 		payload: z.object({
 		payload: z.object({
 			text: z.string(),
 			text: z.string(),
 			images: z.array(z.string()).optional(),
 			images: z.array(z.string()).optional(),
+			mode: z.string().optional(),
+			providerProfile: z.string().optional(),
 		}),
 		}),
 		timestamp: z.number(),
 		timestamp: z.number(),
 	}),
 	}),
@@ -502,9 +523,7 @@ export const extensionBridgeCommandSchema = z.discriminatedUnion("type", [
 	z.object({
 	z.object({
 		type: z.literal(ExtensionBridgeCommandName.ResumeTask),
 		type: z.literal(ExtensionBridgeCommandName.ResumeTask),
 		instanceId: z.string(),
 		instanceId: z.string(),
-		payload: z.object({
-			taskId: z.string(),
-		}),
+		payload: z.object({ taskId: z.string() }),
 		timestamp: z.number(),
 		timestamp: z.number(),
 	}),
 	}),
 ])
 ])
@@ -558,6 +577,8 @@ export const taskBridgeCommandSchema = z.discriminatedUnion("type", [
 		payload: z.object({
 		payload: z.object({
 			text: z.string(),
 			text: z.string(),
 			images: z.array(z.string()).optional(),
 			images: z.array(z.string()).optional(),
+			mode: z.string().optional(),
+			providerProfile: z.string().optional(),
 		}),
 		}),
 		timestamp: z.number(),
 		timestamp: z.number(),
 	}),
 	}),

+ 7 - 0
packages/types/src/events.ts

@@ -36,6 +36,10 @@ export enum RooCodeEventName {
 	TaskTokenUsageUpdated = "taskTokenUsageUpdated",
 	TaskTokenUsageUpdated = "taskTokenUsageUpdated",
 	TaskToolFailed = "taskToolFailed",
 	TaskToolFailed = "taskToolFailed",
 
 
+	// Configuration Changes
+	ModeChanged = "modeChanged",
+	ProviderProfileChanged = "providerProfileChanged",
+
 	// Evals
 	// Evals
 	EvalPass = "evalPass",
 	EvalPass = "evalPass",
 	EvalFail = "evalFail",
 	EvalFail = "evalFail",
@@ -81,6 +85,9 @@ export const rooCodeEventsSchema = z.object({
 
 
 	[RooCodeEventName.TaskToolFailed]: z.tuple([z.string(), toolNamesSchema, z.string()]),
 	[RooCodeEventName.TaskToolFailed]: z.tuple([z.string(), toolNamesSchema, z.string()]),
 	[RooCodeEventName.TaskTokenUsageUpdated]: z.tuple([z.string(), tokenUsageSchema]),
 	[RooCodeEventName.TaskTokenUsageUpdated]: z.tuple([z.string(), tokenUsageSchema]),
+
+	[RooCodeEventName.ModeChanged]: z.tuple([z.string()]),
+	[RooCodeEventName.ProviderProfileChanged]: z.tuple([z.object({ name: z.string(), provider: z.string() })]),
 })
 })
 
 
 export type RooCodeEvents = z.infer<typeof rooCodeEventsSchema>
 export type RooCodeEvents = z.infer<typeof rooCodeEventsSchema>

+ 3 - 1
packages/types/src/provider-settings.ts

@@ -414,9 +414,11 @@ export const providerSettingsSchema = z.object({
 export type ProviderSettings = z.infer<typeof providerSettingsSchema>
 export type ProviderSettings = z.infer<typeof providerSettingsSchema>
 
 
 export const providerSettingsWithIdSchema = providerSettingsSchema.extend({ id: z.string().optional() })
 export const providerSettingsWithIdSchema = providerSettingsSchema.extend({ id: z.string().optional() })
+
 export const discriminatedProviderSettingsWithIdSchema = providerSettingsSchemaDiscriminated.and(
 export const discriminatedProviderSettingsWithIdSchema = providerSettingsSchemaDiscriminated.and(
 	z.object({ id: z.string().optional() }),
 	z.object({ id: z.string().optional() }),
 )
 )
+
 export type ProviderSettingsWithId = z.infer<typeof providerSettingsWithIdSchema>
 export type ProviderSettingsWithId = z.infer<typeof providerSettingsWithIdSchema>
 
 
 export const PROVIDER_SETTINGS_KEYS = providerSettingsSchema.keyof().options
 export const PROVIDER_SETTINGS_KEYS = providerSettingsSchema.keyof().options
@@ -454,7 +456,7 @@ export const getApiProtocol = (provider: ProviderName | undefined, modelId?: str
 		return "anthropic"
 		return "anthropic"
 	}
 	}
 
 
-	// Vercel AI Gateway uses anthropic protocol for anthropic models
+	// Vercel AI Gateway uses anthropic protocol for anthropic models.
 	if (provider && provider === "vercel-ai-gateway" && modelId && modelId.toLowerCase().startsWith("anthropic/")) {
 	if (provider && provider === "vercel-ai-gateway" && modelId && modelId.toLowerCase().startsWith("anthropic/")) {
 		return "anthropic"
 		return "anthropic"
 	}
 	}

+ 41 - 19
packages/types/src/task.ts

@@ -1,38 +1,48 @@
 import { z } from "zod"
 import { z } from "zod"
 
 
 import { RooCodeEventName } from "./events.js"
 import { RooCodeEventName } from "./events.js"
-import { type ClineMessage, type TokenUsage } from "./message.js"
-import { type ToolUsage, type ToolName } from "./tool.js"
+import type { RooCodeSettings } from "./global-settings.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 { StaticAppProperties, GitProperties, TelemetryProperties } from "./telemetry.js"
+import type { TodoItem } from "./todo.js"
 
 
 /**
 /**
  * TaskProviderLike
  * TaskProviderLike
  */
  */
 
 
-export interface TaskProviderState {
-	mode?: string
-}
-
 export interface TaskProviderLike {
 export interface TaskProviderLike {
-	readonly cwd: string
-	readonly appProperties: StaticAppProperties
-	readonly gitProperties: GitProperties | undefined
-
+	// Tasks
 	getCurrentTask(): TaskLike | undefined
 	getCurrentTask(): TaskLike | undefined
-	getCurrentTaskStack(): string[]
 	getRecentTasks(): string[]
 	getRecentTasks(): string[]
-
-	createTask(text?: string, images?: string[], parentTask?: TaskLike): Promise<TaskLike>
+	createTask(
+		text?: string,
+		images?: string[],
+		parentTask?: TaskLike,
+		options?: CreateTaskOptions,
+		configuration?: RooCodeSettings,
+	): Promise<TaskLike>
 	cancelTask(): Promise<void>
 	cancelTask(): Promise<void>
 	clearTask(): Promise<void>
 	clearTask(): Promise<void>
 	resumeTask(taskId: string): void
 	resumeTask(taskId: string): void
 
 
-	getState(): Promise<TaskProviderState>
-	postStateToWebview(): Promise<void>
-	postMessageToWebview(message: unknown): Promise<void>
+	// Modes
+	getModes(): Promise<{ slug: string; name: string }[]>
+	getMode(): Promise<string>
+	setMode(mode: string): Promise<void>
+
+	// Provider Profiles
+	getProviderProfiles(): Promise<{ name: string; provider?: string }[]>
+	getProviderProfile(): Promise<string>
+	setProviderProfile(providerProfile: string): Promise<void>
 
 
+	// Telemetry
+	readonly appProperties: StaticAppProperties
+	readonly gitProperties: GitProperties | undefined
 	getTelemetryProperties(): Promise<TelemetryProperties>
 	getTelemetryProperties(): Promise<TelemetryProperties>
+	readonly cwd: string
 
 
+	// Event Emitter
 	on<K extends keyof TaskProviderEvents>(
 	on<K extends keyof TaskProviderEvents>(
 		event: K,
 		event: K,
 		listener: (...args: TaskProviderEvents[K]) => void | Promise<void>,
 		listener: (...args: TaskProviderEvents[K]) => void | Promise<void>,
@@ -42,6 +52,9 @@ export interface TaskProviderLike {
 		event: K,
 		event: K,
 		listener: (...args: TaskProviderEvents[K]) => void | Promise<void>,
 		listener: (...args: TaskProviderEvents[K]) => void | Promise<void>,
 	): this
 	): this
+
+	// @TODO: Find a better way to do this.
+	postStateToWebview(): Promise<void>
 }
 }
 
 
 export type TaskProviderEvents = {
 export type TaskProviderEvents = {
@@ -57,15 +70,24 @@ export type TaskProviderEvents = {
 	[RooCodeEventName.TaskInteractive]: [taskId: string]
 	[RooCodeEventName.TaskInteractive]: [taskId: string]
 	[RooCodeEventName.TaskResumable]: [taskId: string]
 	[RooCodeEventName.TaskResumable]: [taskId: string]
 	[RooCodeEventName.TaskIdle]: [taskId: string]
 	[RooCodeEventName.TaskIdle]: [taskId: string]
-
-	// Subtask Lifecycle
 	[RooCodeEventName.TaskSpawned]: [taskId: string]
 	[RooCodeEventName.TaskSpawned]: [taskId: string]
+	[RooCodeEventName.ModeChanged]: [mode: string]
+	[RooCodeEventName.ProviderProfileChanged]: [config: { name: string; provider?: string }]
 }
 }
 
 
 /**
 /**
  * TaskLike
  * TaskLike
  */
  */
 
 
+export interface CreateTaskOptions {
+	enableDiff?: boolean
+	enableCheckpoints?: boolean
+	fuzzyMatchThreshold?: number
+	consecutiveMistakeLimit?: number
+	experiments?: Record<string, boolean>
+	initialTodos?: TodoItem[]
+}
+
 export enum TaskStatus {
 export enum TaskStatus {
 	Running = "running",
 	Running = "running",
 	Interactive = "interactive",
 	Interactive = "interactive",
@@ -94,7 +116,7 @@ export interface TaskLike {
 
 
 	approveAsk(options?: { text?: string; images?: string[] }): void
 	approveAsk(options?: { text?: string; images?: string[] }): void
 	denyAsk(options?: { text?: string; images?: string[] }): void
 	denyAsk(options?: { text?: string; images?: string[] }): void
-	submitUserMessage(text: string, images?: string[]): void
+	submitUserMessage(text: string, images?: string[], mode?: string, providerProfile?: string): Promise<void>
 	abortTask(): void
 	abortTask(): void
 }
 }
 
 

+ 17 - 2
src/core/task/Task.ts

@@ -10,6 +10,7 @@ import pWaitFor from "p-wait-for"
 import { serializeError } from "serialize-error"
 import { serializeError } from "serialize-error"
 
 
 import {
 import {
+	type RooCodeSettings,
 	type TaskLike,
 	type TaskLike,
 	type TaskMetadata,
 	type TaskMetadata,
 	type TaskEvents,
 	type TaskEvents,
@@ -23,6 +24,7 @@ import {
 	type ClineAsk,
 	type ClineAsk,
 	type ToolProgressStatus,
 	type ToolProgressStatus,
 	type HistoryItem,
 	type HistoryItem,
+	type CreateTaskOptions,
 	RooCodeEventName,
 	RooCodeEventName,
 	TelemetryEventName,
 	TelemetryEventName,
 	TaskStatus,
 	TaskStatus,
@@ -110,7 +112,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 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
 const MAX_CONTEXT_WINDOW_RETRIES = 3 // Maximum retries for context window errors
 
 
-export type TaskOptions = {
+export interface TaskOptions extends CreateTaskOptions {
 	provider: ClineProvider
 	provider: ClineProvider
 	apiConfiguration: ProviderSettings
 	apiConfiguration: ProviderSettings
 	enableDiff?: boolean
 	enableDiff?: boolean
@@ -845,7 +847,12 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
 		this.handleWebviewAskResponse("noButtonClicked", text, images)
 		this.handleWebviewAskResponse("noButtonClicked", text, images)
 	}
 	}
 
 
-	public submitUserMessage(text: string, images?: string[]): void {
+	public async submitUserMessage(
+		text: string,
+		images?: string[],
+		mode?: string,
+		providerProfile?: string,
+	): Promise<void> {
 		try {
 		try {
 			text = (text ?? "").trim()
 			text = (text ?? "").trim()
 			images = images ?? []
 			images = images ?? []
@@ -857,6 +864,14 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
 			const provider = this.providerRef.deref()
 			const provider = this.providerRef.deref()
 
 
 			if (provider) {
 			if (provider) {
+				if (mode) {
+					await provider.setMode(mode)
+				}
+
+				if (providerProfile) {
+					await provider.setProviderProfile(providerProfile)
+				}
+
 				provider.postMessageToWebview({ type: "invoke", invoke: "sendMessage", text, images })
 				provider.postMessageToWebview({ type: "invoke", invoke: "sendMessage", text, images })
 			} else {
 			} else {
 				console.error("[Task#submitUserMessage] Provider reference lost")
 				console.error("[Task#submitUserMessage] Provider reference lost")

+ 323 - 269
src/core/webview/ClineProvider.ts

@@ -30,6 +30,7 @@ import {
 	type TerminalActionPromptType,
 	type TerminalActionPromptType,
 	type HistoryItem,
 	type HistoryItem,
 	type CloudUserInfo,
 	type CloudUserInfo,
+	type CreateTaskOptions,
 	RooCodeEventName,
 	RooCodeEventName,
 	requestyDefaultModelId,
 	requestyDefaultModelId,
 	openRouterDefaultModelId,
 	openRouterDefaultModelId,
@@ -37,6 +38,7 @@ import {
 	DEFAULT_TERMINAL_OUTPUT_CHARACTER_LIMIT,
 	DEFAULT_TERMINAL_OUTPUT_CHARACTER_LIMIT,
 	DEFAULT_WRITE_DELAY_MS,
 	DEFAULT_WRITE_DELAY_MS,
 	ORGANIZATION_ALLOW_ALL,
 	ORGANIZATION_ALLOW_ALL,
+	DEFAULT_MODES,
 } from "@roo-code/types"
 } from "@roo-code/types"
 import { TelemetryService } from "@roo-code/telemetry"
 import { TelemetryService } from "@roo-code/telemetry"
 import { CloudService, BridgeOrchestrator, getRooCodeApiUrl } from "@roo-code/cloud"
 import { CloudService, BridgeOrchestrator, getRooCodeApiUrl } from "@roo-code/cloud"
@@ -70,6 +72,7 @@ import { fileExistsAtPath } from "../../utils/fs"
 import { setTtsEnabled, setTtsSpeed } from "../../utils/tts"
 import { setTtsEnabled, setTtsSpeed } from "../../utils/tts"
 import { getWorkspaceGitInfo } from "../../utils/git"
 import { getWorkspaceGitInfo } from "../../utils/git"
 import { getWorkspacePath } from "../../utils/path"
 import { getWorkspacePath } from "../../utils/path"
+import { OrganizationAllowListViolationError } from "../../utils/errors"
 
 
 import { setPanel } from "../../activate/registerCommands"
 import { setPanel } from "../../activate/registerCommands"
 
 
@@ -81,7 +84,7 @@ import { forceFullModelDetailsLoad, hasLoadedFullDetails } from "../../api/provi
 import { ContextProxy } from "../config/ContextProxy"
 import { ContextProxy } from "../config/ContextProxy"
 import { ProviderSettingsManager } from "../config/ProviderSettingsManager"
 import { ProviderSettingsManager } from "../config/ProviderSettingsManager"
 import { CustomModesManager } from "../config/CustomModesManager"
 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 { getSystemPromptFilePath } from "../prompts/sections/custom-system-prompt"
 
 
 import { webviewMessageHandler } from "./webviewMessageHandler"
 import { webviewMessageHandler } from "./webviewMessageHandler"
@@ -264,27 +267,29 @@ export class ClineProvider
 	}
 	}
 
 
 	/**
 	/**
-	 * Synchronize cloud profiles with local profiles
+	 * Synchronize cloud profiles with local profiles.
 	 */
 	 */
 	private async syncCloudProfiles() {
 	private async syncCloudProfiles() {
 		try {
 		try {
 			const settings = CloudService.instance.getOrganizationSettings()
 			const settings = CloudService.instance.getOrganizationSettings()
+
 			if (!settings?.providerProfiles) {
 			if (!settings?.providerProfiles) {
 				return
 				return
 			}
 			}
 
 
 			const currentApiConfigName = this.getGlobalState("currentApiConfigName")
 			const currentApiConfigName = this.getGlobalState("currentApiConfigName")
+
 			const result = await this.providerSettingsManager.syncCloudProfiles(
 			const result = await this.providerSettingsManager.syncCloudProfiles(
 				settings.providerProfiles,
 				settings.providerProfiles,
 				currentApiConfigName,
 				currentApiConfigName,
 			)
 			)
 
 
 			if (result.hasChanges) {
 			if (result.hasChanges) {
-				// Update list
+				// Update list.
 				await this.updateGlobalState("listApiConfigMeta", await this.providerSettingsManager.listConfig())
 				await this.updateGlobalState("listApiConfigMeta", await this.providerSettingsManager.listConfig())
 
 
 				if (result.activeProfileChanged && result.activeProfileId) {
 				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({
 					const profile = await this.providerSettingsManager.getProfile({
 						id: result.activeProfileId,
 						id: result.activeProfileId,
 					})
 					})
@@ -374,17 +379,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 {
 	getTaskStackSize(): number {
 		return this.clineStack.length
 		return this.clineStack.length
 	}
 	}
@@ -407,58 +401,6 @@ export class ClineProvider
 		await this.getCurrentTask()?.resumePausedTask(lastMessage)
 		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.
 	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/
 	- https://vscode-docs.readthedocs.io/en/stable/extensions/patterns-and-principles/
@@ -737,82 +679,17 @@ export class ClineProvider
 		await this.removeClineFromStack()
 		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(historyItem: HistoryItem & { rootTask?: Task; parentTask?: Task }) {
 	public async createTaskWithHistoryItem(historyItem: HistoryItem & { rootTask?: Task; parentTask?: Task }) {
 		await this.removeClineFromStack()
 		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) {
 		if (historyItem.mode) {
 			// Validate that the mode still exists
 			// Validate that the mode still exists
 			const customModes = await this.customModesManager.getCustomModes()
 			const customModes = await this.customModesManager.getCustomModes()
 			const modeExists = getModeBySlug(historyItem.mode, customModes) !== undefined
 			const modeExists = getModeBySlug(historyItem.mode, customModes) !== undefined
 
 
 			if (!modeExists) {
 			if (!modeExists) {
-				// Mode no longer exists, fall back to default mode
+				// Mode no longer exists, fall back to default mode.
 				this.log(
 				this.log(
 					`Mode '${historyItem.mode}' from history no longer exists. Falling back to default mode '${defaultModeSlug}'.`,
 					`Mode '${historyItem.mode}' from history no longer exists. Falling back to default mode '${defaultModeSlug}'.`,
 				)
 				)
@@ -821,14 +698,14 @@ export class ClineProvider
 
 
 			await this.updateGlobalState("mode", historyItem.mode)
 			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 savedConfigId = await this.providerSettingsManager.getModeConfigId(historyItem.mode)
 			const listApiConfig = await this.providerSettingsManager.listConfig()
 			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)
 			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) {
 			if (savedConfigId) {
 				const profile = listApiConfig.find(({ id }) => id === savedConfigId)
 				const profile = listApiConfig.find(({ id }) => id === savedConfigId)
 
 
@@ -836,13 +713,13 @@ export class ClineProvider
 					try {
 					try {
 						await this.activateProviderProfile({ name: profile.name })
 						await this.activateProviderProfile({ name: profile.name })
 					} catch (error) {
 					} catch (error) {
-						// Log the error but continue with task restoration
+						// Log the error but continue with task restoration.
 						this.log(
 						this.log(
 							`Failed to restore API configuration for mode '${historyItem.mode}': ${
 							`Failed to restore API configuration for mode '${historyItem.mode}': ${
 								error instanceof Error ? error.message : String(error)
 								error instanceof Error ? error.message : String(error)
 							}. Continuing with default configuration.`,
 							}. Continuing with default configuration.`,
 						)
 						)
-						// The task will continue with the current/default configuration
+						// The task will continue with the current/default configuration.
 					}
 					}
 				}
 				}
 			}
 			}
@@ -1081,39 +958,39 @@ export class ClineProvider
 			TelemetryService.instance.captureModeSwitch(cline.taskId, newMode)
 			TelemetryService.instance.captureModeSwitch(cline.taskId, newMode)
 			cline.emit(RooCodeEventName.TaskModeSwitched, 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 {
 			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 history = this.getGlobalState("taskHistory") ?? []
 				const taskHistoryItem = history.find((item) => item.id === cline.taskId)
 				const taskHistoryItem = history.find((item) => item.id === cline.taskId)
+
 				if (taskHistoryItem) {
 				if (taskHistoryItem) {
 					taskHistoryItem.mode = newMode
 					taskHistoryItem.mode = newMode
 					await this.updateTaskHistory(taskHistoryItem)
 					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
 				;(cline as any)._taskMode = newMode
 			} catch (error) {
 			} 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(
 				this.log(
 					`Failed to persist mode switch for task ${cline.taskId}: ${error instanceof Error ? error.message : String(error)}`,
 					`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
 				throw error
 			}
 			}
 		}
 		}
 
 
 		await this.updateGlobalState("mode", newMode)
 		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 savedConfigId = await this.providerSettingsManager.getModeConfigId(newMode)
 		const listApiConfig = await this.providerSettingsManager.listConfig()
 		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)
 		await this.updateGlobalState("listApiConfigMeta", listApiConfig)
 
 
 		// If this mode has a saved config, use it.
 		// If this mode has a saved config, use it.
@@ -1256,60 +1133,9 @@ export class ClineProvider
 		}
 		}
 
 
 		await this.postStateToWebview()
 		await this.postStateToWebview()
-	}
-
-	// Task Management
-
-	async cancelTask() {
-		const cline = this.getCurrentTask()
 
 
-		if (!cline) {
-			return
-		}
-
-		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
-
-		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 })
-	}
-
-	// 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()
+		if (providerSettings.apiProvider) {
+			this.emit(RooCodeEventName.ProviderProfileChanged, { name, provider: providerSettings.apiProvider })
 		}
 		}
 	}
 	}
 
 
@@ -2146,12 +1972,6 @@ export class ClineProvider
 		await this.contextProxy.setValues(values)
 		await this.contextProxy.setValues(values)
 	}
 	}
 
 
-	// cwd
-
-	get cwd() {
-		return getWorkspacePath()
-	}
-
 	// dev
 	// dev
 
 
 	async resetState() {
 	async resetState() {
@@ -2262,7 +2082,300 @@ 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(),
+			})
+		}
+	}
+
+	/**
+	 * 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<Task> {
+		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: 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 cancelTask(): Promise<void> {
+		const cline = this.getCurrentTask()
+
+		if (!cline) {
+			return
+		}
+
+		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
+
+		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 })
+	}
+
+	// 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<void> {
+		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()
+		}
+	}
+
+	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<string> {
+		const { mode } = await this.getState()
+		return mode
+	}
+
+	public async setMode(mode: string): Promise<void> {
+		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<string> {
+		const { currentApiConfigName } = await this.getState()
+		return currentApiConfigName
+	}
+
+	public async setProviderProfile(name: string): Promise<void> {
+		await this.activateProviderProfile({ name })
+	}
+
+	// Telemetry
+
 	private _appProperties?: StaticAppProperties
 	private _appProperties?: StaticAppProperties
+	private _gitProperties?: GitProperties
 
 
 	private getAppProperties(): StaticAppProperties {
 	private getAppProperties(): StaticAppProperties {
 		if (!this._appProperties) {
 		if (!this._appProperties) {
@@ -2329,8 +2442,6 @@ export class ClineProvider
 		}
 		}
 	}
 	}
 
 
-	private _gitProperties?: GitProperties
-
 	private async getGitProperties(): Promise<GitProperties> {
 	private async getGitProperties(): Promise<GitProperties> {
 		if (!this._gitProperties) {
 		if (!this._gitProperties) {
 			this._gitProperties = await getWorkspaceGitInfo()
 			this._gitProperties = await getWorkspaceGitInfo()
@@ -2352,64 +2463,7 @@ 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(),
-			})
-		}
-	}
-}
-
-class OrganizationAllowListViolationError extends Error {
-	constructor(message: string) {
-		super(message)
+	public get cwd() {
+		return getWorkspacePath()
 	}
 	}
 }
 }

+ 7 - 30
src/extension/api.ts

@@ -11,6 +11,7 @@ import {
 	type ProviderSettings,
 	type ProviderSettings,
 	type ProviderSettingsEntry,
 	type ProviderSettingsEntry,
 	type TaskEvent,
 	type TaskEvent,
+	type CreateTaskOptions,
 	RooCodeEventName,
 	RooCodeEventName,
 	TaskCommandName,
 	TaskCommandName,
 	isSecretStateKey,
 	isSecretStateKey,
@@ -128,46 +129,22 @@ export class API extends EventEmitter<RooCodeEvents> implements RooCodeAPI {
 			provider = this.sidebarProvider
 			provider = this.sidebarProvider
 		}
 		}
 
 
-		if (configuration) {
-			await provider.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,
-					)
-			}
-		}
-
 		await provider.removeClineFromStack()
 		await provider.removeClineFromStack()
 		await provider.postStateToWebview()
 		await provider.postStateToWebview()
 		await provider.postMessageToWebview({ type: "action", action: "chatButtonClicked" })
 		await provider.postMessageToWebview({ type: "action", action: "chatButtonClicked" })
 		await provider.postMessageToWebview({ type: "invoke", invoke: "newChat", text, images })
 		await provider.postMessageToWebview({ type: "invoke", invoke: "newChat", text, images })
 
 
-		const cline = await provider.createTask(text, images, undefined, {
+		const options: CreateTaskOptions = {
 			consecutiveMistakeLimit: Number.MAX_SAFE_INTEGER,
 			consecutiveMistakeLimit: Number.MAX_SAFE_INTEGER,
-		})
+		}
+
+		const task = await provider.createTask(text, images, undefined, options, configuration)
 
 
-		if (!cline) {
+		if (!task) {
 			throw new Error("Failed to create task due to policy restrictions")
 			throw new Error("Failed to create task due to policy restrictions")
 		}
 		}
 
 
-		return cline.taskId
+		return task.taskId
 	}
 	}
 
 
 	public async resumeTask(taskId: string): Promise<void> {
 	public async resumeTask(taskId: string): Promise<void> {

+ 5 - 0
src/utils/errors.ts

@@ -0,0 +1,5 @@
+export class OrganizationAllowListViolationError extends Error {
+	constructor(message: string) {
+		super(message)
+	}
+}