Kaynağa Gözat

Move message queue to the extension host (#7604)

Co-authored-by: roomote[bot] <219738659+roomote[bot]@users.noreply.github.com>
Chris Estreich 4 ay önce
ebeveyn
işleme
5196c75017

+ 3 - 0
packages/cloud/src/bridge/ExtensionChannel.ts

@@ -175,6 +175,7 @@ export class ExtensionChannel extends BaseChannel<
 			{ from: RooCodeEventName.TaskInteractive, to: ExtensionBridgeEventName.TaskInteractive },
 			{ from: RooCodeEventName.TaskResumable, to: ExtensionBridgeEventName.TaskResumable },
 			{ from: RooCodeEventName.TaskIdle, to: ExtensionBridgeEventName.TaskIdle },
+			{ from: RooCodeEventName.TaskUserMessage, to: ExtensionBridgeEventName.TaskUserMessage },
 		] as const
 
 		eventMapping.forEach(({ from, to }) => {
@@ -220,6 +221,8 @@ export class ExtensionChannel extends BaseChannel<
 				? {
 						taskId: task.taskId,
 						taskStatus: task.taskStatus,
+						taskAsk: task?.taskAsk,
+						queuedMessages: task.queuedMessages,
 						...task.metadata,
 					}
 				: { taskId: "", taskStatus: TaskStatus.None },

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

@@ -101,6 +101,7 @@ describe("ExtensionChannel", () => {
 				RooCodeEventName.TaskInteractive,
 				RooCodeEventName.TaskResumable,
 				RooCodeEventName.TaskIdle,
+				RooCodeEventName.TaskUserMessage,
 			]
 
 			// Check that on() was called for each event
@@ -230,7 +231,7 @@ describe("ExtensionChannel", () => {
 			}
 
 			// Listeners should still be the same count (not accumulated)
-			const expectedEventCount = 10 // Number of events we listen to
+			const expectedEventCount = 11 // Number of events we listen to (including TaskUserMessage)
 			expect(eventListeners.size).toBe(expectedEventCount)
 
 			// Each event should have exactly 1 listener

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

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

+ 20 - 8
packages/types/src/cloud.ts

@@ -7,7 +7,7 @@ import { TaskStatus, taskMetadataSchema } from "./task.js"
 import { globalSettingsSchema } from "./global-settings.js"
 import { providerSettingsWithIdSchema } from "./provider-settings.js"
 import { mcpMarketplaceItemSchema } from "./marketplace.js"
-import { clineMessageSchema } from "./message.js"
+import { clineMessageSchema, queuedMessageSchema } from "./message.js"
 import { staticAppPropertiesSchema, gitPropertiesSchema } from "./telemetry.js"
 
 /**
@@ -359,6 +359,8 @@ export const INSTANCE_TTL_SECONDS = 60
 const extensionTaskSchema = z.object({
 	taskId: z.string(),
 	taskStatus: z.nativeEnum(TaskStatus),
+	taskAsk: clineMessageSchema.optional(),
+	queuedMessages: z.array(queuedMessageSchema).optional(),
 	...taskMetadataSchema.shape,
 })
 
@@ -402,6 +404,8 @@ export enum ExtensionBridgeEventName {
 	TaskResumable = RooCodeEventName.TaskResumable,
 	TaskIdle = RooCodeEventName.TaskIdle,
 
+	TaskUserMessage = RooCodeEventName.TaskUserMessage,
+
 	ModeChanged = RooCodeEventName.ModeChanged,
 	ProviderProfileChanged = RooCodeEventName.ProviderProfileChanged,
 
@@ -461,31 +465,39 @@ export const extensionBridgeEventSchema = z.discriminatedUnion("type", [
 		instance: extensionInstanceSchema,
 		timestamp: z.number(),
 	}),
+
 	z.object({
-		type: z.literal(ExtensionBridgeEventName.InstanceRegistered),
+		type: z.literal(ExtensionBridgeEventName.TaskUserMessage),
 		instance: extensionInstanceSchema,
 		timestamp: z.number(),
 	}),
+
 	z.object({
-		type: z.literal(ExtensionBridgeEventName.InstanceUnregistered),
+		type: z.literal(ExtensionBridgeEventName.ModeChanged),
 		instance: extensionInstanceSchema,
+		mode: z.string(),
 		timestamp: z.number(),
 	}),
 	z.object({
-		type: z.literal(ExtensionBridgeEventName.HeartbeatUpdated),
+		type: z.literal(ExtensionBridgeEventName.ProviderProfileChanged),
 		instance: extensionInstanceSchema,
+		providerProfile: z.object({ name: z.string(), provider: z.string().optional() }),
 		timestamp: z.number(),
 	}),
+
 	z.object({
-		type: z.literal(ExtensionBridgeEventName.ModeChanged),
+		type: z.literal(ExtensionBridgeEventName.InstanceRegistered),
 		instance: extensionInstanceSchema,
-		mode: z.string(),
 		timestamp: z.number(),
 	}),
 	z.object({
-		type: z.literal(ExtensionBridgeEventName.ProviderProfileChanged),
+		type: z.literal(ExtensionBridgeEventName.InstanceUnregistered),
+		instance: extensionInstanceSchema,
+		timestamp: z.number(),
+	}),
+	z.object({
+		type: z.literal(ExtensionBridgeEventName.HeartbeatUpdated),
 		instance: extensionInstanceSchema,
-		providerProfile: z.object({ name: z.string(), provider: z.string().optional() }),
 		timestamp: z.number(),
 	}),
 ])

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

@@ -31,6 +31,7 @@ export enum RooCodeEventName {
 	Message = "message",
 	TaskModeSwitched = "taskModeSwitched",
 	TaskAskResponded = "taskAskResponded",
+	TaskUserMessage = "taskUserMessage",
 
 	// Task Analytics
 	TaskTokenUsageUpdated = "taskTokenUsageUpdated",
@@ -82,6 +83,7 @@ export const rooCodeEventsSchema = z.object({
 	]),
 	[RooCodeEventName.TaskModeSwitched]: z.tuple([z.string(), z.string()]),
 	[RooCodeEventName.TaskAskResponded]: z.tuple([z.string()]),
+	[RooCodeEventName.TaskUserMessage]: z.tuple([z.string()]),
 
 	[RooCodeEventName.TaskToolFailed]: z.tuple([z.string(), toolNamesSchema, z.string()]),
 	[RooCodeEventName.TaskTokenUsageUpdated]: z.tuple([z.string(), tokenUsageSchema]),

+ 8 - 11
packages/types/src/message.ts

@@ -248,14 +248,11 @@ export type TokenUsage = z.infer<typeof tokenUsageSchema>
  * QueuedMessage
  */
 
-/**
- * Represents a message that is queued to be sent when sending is enabled
- */
-export interface QueuedMessage {
-	/** Unique identifier for the queued message */
-	id: string
-	/** The text content of the message */
-	text: string
-	/** Array of image data URLs attached to the message */
-	images: string[]
-}
+export const queuedMessageSchema = z.object({
+	timestamp: z.number(),
+	id: z.string(),
+	text: z.string(),
+	images: z.array(z.string()).optional(),
+})
+
+export type QueuedMessage = z.infer<typeof queuedMessageSchema>

+ 9 - 6
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, TokenUsage } from "./message.js"
+import type { ClineMessage, QueuedMessage, 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,8 +59,6 @@ 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]
@@ -71,6 +69,9 @@ export type TaskProviderEvents = {
 	[RooCodeEventName.TaskResumable]: [taskId: string]
 	[RooCodeEventName.TaskIdle]: [taskId: string]
 	[RooCodeEventName.TaskSpawned]: [taskId: string]
+
+	[RooCodeEventName.TaskUserMessage]: [taskId: string]
+
 	[RooCodeEventName.ModeChanged]: [mode: string]
 	[RooCodeEventName.ProviderProfileChanged]: [config: { name: string; provider?: string }]
 }
@@ -105,11 +106,12 @@ export type TaskMetadata = z.infer<typeof taskMetadataSchema>
 
 export interface TaskLike {
 	readonly taskId: string
-	readonly taskStatus: TaskStatus
-	readonly taskAsk: ClineMessage | undefined
+	readonly rootTask?: TaskLike
 	readonly metadata: TaskMetadata
 
-	readonly rootTask?: TaskLike
+	readonly taskStatus: TaskStatus
+	readonly taskAsk: ClineMessage | undefined
+	readonly queuedMessages: QueuedMessage[]
 
 	on<K extends keyof TaskEvents>(event: K, listener: (...args: TaskEvents[K]) => void | Promise<void>): this
 	off<K extends keyof TaskEvents>(event: K, listener: (...args: TaskEvents[K]) => void | Promise<void>): this
@@ -141,6 +143,7 @@ export type TaskEvents = {
 	[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]

+ 98 - 0
src/core/message-queue/MessageQueueService.ts

@@ -0,0 +1,98 @@
+import { EventEmitter } from "events"
+
+import { v4 as uuidv4 } from "uuid"
+
+import { QueuedMessage } from "@roo-code/types"
+
+export interface MessageQueueState {
+	messages: QueuedMessage[]
+	isProcessing: boolean
+	isPaused: boolean
+}
+
+export interface QueueEvents {
+	stateChanged: [messages: QueuedMessage[]]
+}
+
+export class MessageQueueService extends EventEmitter<QueueEvents> {
+	private _messages: QueuedMessage[]
+
+	constructor() {
+		super()
+
+		this._messages = []
+	}
+
+	private findMessage(id: string) {
+		const index = this._messages.findIndex((msg) => msg.id === id)
+
+		if (index === -1) {
+			return { index, message: undefined }
+		}
+
+		return { index, message: this._messages[index] }
+	}
+
+	public addMessage(text: string, images?: string[]): QueuedMessage | undefined {
+		if (!text && !images?.length) {
+			return undefined
+		}
+
+		const message: QueuedMessage = {
+			timestamp: Date.now(),
+			id: uuidv4(),
+			text,
+			images,
+		}
+
+		this._messages.push(message)
+		this.emit("stateChanged", this._messages)
+
+		return message
+	}
+
+	public removeMessage(id: string): boolean {
+		const { index, message } = this.findMessage(id)
+
+		if (!message) {
+			return false
+		}
+
+		this._messages.splice(index, 1)
+		this.emit("stateChanged", this._messages)
+		return true
+	}
+
+	public updateMessage(id: string, text: string, images?: string[]): boolean {
+		const { message } = this.findMessage(id)
+
+		if (!message) {
+			return false
+		}
+
+		message.timestamp = Date.now()
+		message.text = text
+		message.images = images
+		this.emit("stateChanged", this._messages)
+		return true
+	}
+
+	public dequeueMessage(): QueuedMessage | undefined {
+		const message = this._messages.shift()
+		this.emit("stateChanged", this._messages)
+		return message
+	}
+
+	public get messages(): QueuedMessage[] {
+		return this._messages
+	}
+
+	public isEmpty(): boolean {
+		return this._messages.length === 0
+	}
+
+	public dispose(): void {
+		this._messages = []
+		this.removeAllListeners()
+	}
+}

+ 1 - 1
src/core/task-persistence/index.ts

@@ -1,3 +1,3 @@
-export { readApiMessages, saveApiMessages } from "./apiMessages"
+export { type ApiMessage, readApiMessages, saveApiMessages } from "./apiMessages"
 export { readTaskMessages, saveTaskMessages } from "./taskMessages"
 export { taskMetadata } from "./taskMetadata"

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

@@ -10,7 +10,6 @@ import pWaitFor from "p-wait-for"
 import { serializeError } from "serialize-error"
 
 import {
-	type RooCodeSettings,
 	type TaskLike,
 	type TaskMetadata,
 	type TaskEvents,
@@ -35,6 +34,7 @@ import {
 	isIdleAsk,
 	isInteractiveAsk,
 	isResumableAsk,
+	QueuedMessage,
 } from "@roo-code/types"
 import { TelemetryService } from "@roo-code/telemetry"
 import { CloudService, BridgeOrchestrator } from "@roo-code/cloud"
@@ -42,6 +42,7 @@ import { CloudService, BridgeOrchestrator } from "@roo-code/cloud"
 // api
 import { ApiHandler, ApiHandlerCreateMessageMetadata, buildApiHandler } from "../../api"
 import { ApiStream } from "../../api/transform/stream"
+import { maybeRemoveImageBlocks } from "../../api/transform/image-cleaning"
 
 // shared
 import { findLastIndex } from "../../shared/array"
@@ -79,6 +80,7 @@ import { SYSTEM_PROMPT } from "../prompts/system"
 
 // core modules
 import { ToolRepetitionDetector } from "../tools/ToolRepetitionDetector"
+import { restoreTodoListForTask } from "../tools/updateTodoListTool"
 import { FileContextTracker } from "../context-tracking/FileContextTracker"
 import { RooIgnoreController } from "../ignore/RooIgnoreController"
 import { RooProtectedController } from "../protect/RooProtectedController"
@@ -88,7 +90,14 @@ import { truncateConversationIfNeeded } from "../sliding-window"
 import { ClineProvider } from "../webview/ClineProvider"
 import { MultiSearchReplaceDiffStrategy } from "../diff/strategies/multi-search-replace"
 import { MultiFileSearchReplaceDiffStrategy } from "../diff/strategies/multi-file-search-replace"
-import { readApiMessages, saveApiMessages, readTaskMessages, saveTaskMessages, taskMetadata } from "../task-persistence"
+import {
+	type ApiMessage,
+	readApiMessages,
+	saveApiMessages,
+	readTaskMessages,
+	saveTaskMessages,
+	taskMetadata,
+} from "../task-persistence"
 import { getEnvironmentDetails } from "../environment/getEnvironmentDetails"
 import { checkContextWindowExceededError } from "../context/context-management/context-error-handling"
 import {
@@ -100,12 +109,11 @@ import {
 	checkpointDiff,
 } from "../checkpoints"
 import { processUserContentMentions } from "../mentions/processUserContentMentions"
-import { ApiMessage } from "../task-persistence/apiMessages"
 import { getMessagesSinceLastSummary, summarizeConversation } from "../condense"
-import { maybeRemoveImageBlocks } from "../../api/transform/image-cleaning"
-import { restoreTodoListForTask } from "../tools/updateTodoListTool"
-import { AutoApprovalHandler } from "./AutoApprovalHandler"
 import { Gpt5Metadata, ClineMessageWithMetadata } from "./types"
+import { MessageQueueService } from "../message-queue/MessageQueueService"
+
+import { AutoApprovalHandler } from "./AutoApprovalHandler"
 
 const MAX_EXPONENTIAL_BACKOFF_SECONDS = 600 // 10 minutes
 const DEFAULT_USAGE_COLLECTION_TIMEOUT_MS = 5000 // 5 seconds
@@ -259,6 +267,10 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
 	// Task Bridge
 	enableBridge: boolean
 
+	// Message Queue Service
+	public readonly messageQueueService: MessageQueueService
+	private messageQueueStateChangedHandler: (() => void) | undefined
+
 	// Streaming
 	isWaitingForFirstChunk = false
 	isStreaming = false
@@ -356,9 +368,18 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
 			TelemetryService.instance.captureTaskCreated(this.taskId)
 		}
 
-		// Initialize the assistant message parser
+		// Initialize the assistant message parser.
 		this.assistantMessageParser = new AssistantMessageParser()
 
+		this.messageQueueService = new MessageQueueService()
+
+		this.messageQueueStateChangedHandler = () => {
+			this.emit(RooCodeEventName.TaskUserMessage, this.taskId)
+			this.providerRef.deref()?.postStateToWebview()
+		}
+
+		this.messageQueueService.on("stateChanged", this.messageQueueStateChangedHandler)
+
 		// Only set up diff strategy if diff is enabled.
 		if (this.diffEnabled) {
 			// Default to old strategy, will be updated if experiment is enabled.
@@ -759,10 +780,13 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
 		// The state is mutable if the message is complete and the task will
 		// block (via the `pWaitFor`).
 		const isBlocking = !(this.askResponse !== undefined || this.lastMessageTs !== askTs)
-		const isStatusMutable = !partial && isBlocking
+		const isMessageQueued = !this.messageQueueService.isEmpty()
+		const isStatusMutable = !partial && isBlocking && !isMessageQueued
 		let statusMutationTimeouts: NodeJS.Timeout[] = []
 
 		if (isStatusMutable) {
+			console.log(`Task#ask will block -> type: ${type}`)
+
 			if (isInteractiveAsk(type)) {
 				statusMutationTimeouts.push(
 					setTimeout(() => {
@@ -797,9 +821,19 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
 					}, 1_000),
 				)
 			}
+		} else if (isMessageQueued) {
+			console.log("Task#ask will process message queue")
+
+			const message = this.messageQueueService.dequeueMessage()
+
+			if (message) {
+				setTimeout(async () => {
+					await this.submitUserMessage(message.text, message.images)
+				}, 0)
+			}
 		}
 
-		// Wait for askResponse to be set
+		// Wait for askResponse to be set.
 		await pWaitFor(() => this.askResponse !== undefined || this.lastMessageTs !== askTs, { interval: 100 })
 
 		if (this.lastMessageTs !== askTs) {
@@ -872,6 +906,8 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
 					await provider.setProviderProfile(providerProfile)
 				}
 
+				this.emit(RooCodeEventName.TaskUserMessage, this.taskId)
+
 				provider.postMessageToWebview({ type: "invoke", invoke: "sendMessage", text, images })
 			} else {
 				console.error("[Task#submitUserMessage] Provider reference lost")
@@ -1406,8 +1442,8 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
 			newUserContent.push(...formatResponse.imageBlocks(responseImages))
 		}
 
-		// Ensure we have at least some content to send to the API
-		// If newUserContent is empty, add a minimal resumption message
+		// Ensure we have at least some content to send to the API.
+		// If newUserContent is empty, add a minimal resumption message.
 		if (newUserContent.length === 0) {
 			newUserContent.push({
 				type: "text",
@@ -1417,14 +1453,25 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
 
 		await this.overwriteApiConversationHistory(modifiedApiConversationHistory)
 
-		// Task resuming from history item
-
+		// Task resuming from history item.
 		await this.initiateTaskLoop(newUserContent)
 	}
 
 	public dispose(): void {
 		console.log(`[Task#dispose] disposing task ${this.taskId}.${this.instanceId}`)
 
+		// Dispose message queue and remove event listeners.
+		try {
+			if (this.messageQueueStateChangedHandler) {
+				this.messageQueueService.removeListener("stateChanged", this.messageQueueStateChangedHandler)
+				this.messageQueueStateChangedHandler = undefined
+			}
+
+			this.messageQueueService.dispose()
+		} catch (error) {
+			console.error("Error disposing message queue:", error)
+		}
+
 		// Remove all event listeners to prevent memory leaks.
 		try {
 			this.removeAllListeners()
@@ -2719,10 +2766,6 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
 
 	// Getters
 
-	public get cwd() {
-		return this.workspacePath
-	}
-
 	public get taskStatus(): TaskStatus {
 		if (this.interactiveAsk) {
 			return TaskStatus.Interactive
@@ -2742,4 +2785,12 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
 	public get taskAsk(): ClineMessage | undefined {
 		return this.idleAsk || this.resumableAsk || this.interactiveAsk
 	}
+
+	public get queuedMessages(): QueuedMessage[] {
+		return this.messageQueueService.messages
+	}
+
+	public get cwd() {
+		return this.workspacePath
+	}
 }

+ 35 - 18
src/core/webview/ClineProvider.ts

@@ -47,7 +47,7 @@ import { Package } from "../../shared/package"
 import { findLast } from "../../shared/array"
 import { supportPrompt } from "../../shared/support-prompt"
 import { GlobalFileNames } from "../../shared/globalFileNames"
-import { ExtensionMessage, MarketplaceInstalledMetadata } from "../../shared/ExtensionMessage"
+import type { ExtensionMessage, ExtensionState, MarketplaceInstalledMetadata } from "../../shared/ExtensionMessage"
 import { Mode, defaultModeSlug, getModeBySlug } from "../../shared/modes"
 import { experimentDefault } from "../../shared/experiments"
 import { formatLanguage } from "../../shared/language"
@@ -184,6 +184,7 @@ export class ClineProvider
 			const onTaskInteractive = (taskId: string) => this.emit(RooCodeEventName.TaskInteractive, taskId)
 			const onTaskResumable = (taskId: string) => this.emit(RooCodeEventName.TaskResumable, taskId)
 			const onTaskIdle = (taskId: string) => this.emit(RooCodeEventName.TaskIdle, taskId)
+			const onTaskUserMessage = (taskId: string) => this.emit(RooCodeEventName.TaskUserMessage, taskId)
 
 			// Attach the listeners.
 			instance.on(RooCodeEventName.TaskStarted, onTaskStarted)
@@ -195,6 +196,7 @@ export class ClineProvider
 			instance.on(RooCodeEventName.TaskInteractive, onTaskInteractive)
 			instance.on(RooCodeEventName.TaskResumable, onTaskResumable)
 			instance.on(RooCodeEventName.TaskIdle, onTaskIdle)
+			instance.on(RooCodeEventName.TaskUserMessage, onTaskUserMessage)
 
 			// Store the cleanup functions for later removal.
 			this.taskEventListeners.set(instance, [
@@ -207,6 +209,7 @@ export class ClineProvider
 				() => instance.off(RooCodeEventName.TaskInteractive, onTaskInteractive),
 				() => instance.off(RooCodeEventName.TaskResumable, onTaskResumable),
 				() => instance.off(RooCodeEventName.TaskIdle, onTaskIdle),
+				() => instance.off(RooCodeEventName.TaskUserMessage, onTaskUserMessage),
 			])
 		}
 
@@ -1210,14 +1213,16 @@ export class ClineProvider
 	// OpenRouter
 
 	async handleOpenRouterCallback(code: string) {
-		let { apiConfiguration, currentApiConfigName } = await this.getState()
+		let { apiConfiguration, currentApiConfigName = "default" } = await this.getState()
 
 		let apiKey: string
+
 		try {
 			const baseUrl = apiConfiguration.openRouterBaseUrl || "https://openrouter.ai/api/v1"
-			// Extract the base domain for the auth endpoint
+			// Extract the base domain for the auth endpoint.
 			const baseUrlDomain = baseUrl.match(/^(https?:\/\/[^\/]+)/)?.[1] || "https://openrouter.ai"
 			const response = await axios.post(`${baseUrlDomain}/api/v1/auth/keys`, { code })
+
 			if (response.data && response.data.key) {
 				apiKey = response.data.key
 			} else {
@@ -1227,6 +1232,7 @@ export class ClineProvider
 			this.log(
 				`Error exchanging code for API key: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
 			)
+
 			throw error
 		}
 
@@ -1244,8 +1250,10 @@ export class ClineProvider
 
 	async handleGlamaCallback(code: string) {
 		let apiKey: string
+
 		try {
 			const response = await axios.post("https://glama.ai/api/gateway/v1/auth/exchange-code", { code })
+
 			if (response.data && response.data.apiKey) {
 				apiKey = response.data.apiKey
 			} else {
@@ -1255,10 +1263,11 @@ export class ClineProvider
 			this.log(
 				`Error exchanging code for API key: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
 			)
+
 			throw error
 		}
 
-		const { apiConfiguration, currentApiConfigName } = await this.getState()
+		const { apiConfiguration, currentApiConfigName = "default" } = await this.getState()
 
 		const newConfiguration: ProviderSettings = {
 			...apiConfiguration,
@@ -1273,7 +1282,7 @@ export class ClineProvider
 	// Requesty
 
 	async handleRequestyCallback(code: string) {
-		let { apiConfiguration, currentApiConfigName } = await this.getState()
+		let { apiConfiguration, currentApiConfigName = "default" } = await this.getState()
 
 		const newConfiguration: ProviderSettings = {
 			...apiConfiguration,
@@ -1531,7 +1540,7 @@ export class ClineProvider
 		}
 	}
 
-	async getStateToPostToWebview() {
+	async getStateToPostToWebview(): Promise<ExtensionState> {
 		const {
 			apiConfiguration,
 			lastShownAnnouncementId,
@@ -1621,6 +1630,7 @@ export class ClineProvider
 			remoteControlEnabled,
 			openRouterImageApiKey,
 			openRouterImageGenerationSelectedModel,
+			openRouterUseMiddleOutTransform,
 		} = await this.getState()
 
 		const telemetryKey = process.env.POSTHOG_API_KEY
@@ -1658,6 +1668,7 @@ export class ClineProvider
 				: undefined,
 			clineMessages: this.getCurrentTask()?.clineMessages || [],
 			currentTaskTodos: this.getCurrentTask()?.todoList || [],
+			messageQueue: this.getCurrentTask()?.messageQueueService?.messages,
 			taskHistory: (taskHistory || [])
 				.filter((item: HistoryItem) => item.ts && item.task)
 				.sort((a: HistoryItem, b: HistoryItem) => b.ts - a.ts),
@@ -1754,6 +1765,7 @@ export class ClineProvider
 			remoteControlEnabled,
 			openRouterImageApiKey,
 			openRouterImageGenerationSelectedModel,
+			openRouterUseMiddleOutTransform,
 		}
 	}
 
@@ -1763,7 +1775,17 @@ export class ClineProvider
 	 * https://www.eliostruyf.com/devhack-code-extension-storage-options/
 	 */
 
-	async getState() {
+	async getState(): Promise<
+		Omit<
+			ExtensionState,
+			| "clineMessages"
+			| "renderContext"
+			| "hasOpenedModeSelector"
+			| "version"
+			| "shouldShowAnnouncement"
+			| "hasSystemPromptOverride"
+		>
+	> {
 		const stateValues = this.contextProxy.getValues()
 		const customModes = await this.customModesManager.getCustomModes()
 
@@ -1831,7 +1853,7 @@ export class ClineProvider
 			)
 		}
 
-		// Return the same structure as before
+		// Return the same structure as before.
 		return {
 			apiConfiguration: providerSettings,
 			lastShownAnnouncementId: stateValues.lastShownAnnouncementId,
@@ -1855,7 +1877,7 @@ export class ClineProvider
 			allowedMaxCost: stateValues.allowedMaxCost,
 			autoCondenseContext: stateValues.autoCondenseContext ?? true,
 			autoCondenseContextPercent: stateValues.autoCondenseContextPercent ?? 100,
-			taskHistory: stateValues.taskHistory,
+			taskHistory: stateValues.taskHistory ?? [],
 			allowedCommands: stateValues.allowedCommands,
 			deniedCommands: stateValues.deniedCommands,
 			soundEnabled: stateValues.soundEnabled ?? false,
@@ -1902,7 +1924,7 @@ export class ClineProvider
 			customModes,
 			maxOpenTabsContext: stateValues.maxOpenTabsContext ?? 20,
 			maxWorkspaceFiles: stateValues.maxWorkspaceFiles ?? 200,
-			openRouterUseMiddleOutTransform: stateValues.openRouterUseMiddleOutTransform ?? true,
+			openRouterUseMiddleOutTransform: stateValues.openRouterUseMiddleOutTransform,
 			browserToolEnabled: stateValues.browserToolEnabled ?? true,
 			telemetrySetting: stateValues.telemetrySetting || "unset",
 			showRooIgnoredFiles: stateValues.showRooIgnoredFiles ?? false,
@@ -1916,7 +1938,6 @@ export class ClineProvider
 			sharingEnabled,
 			organizationAllowList,
 			organizationSettingsVersion,
-			// Explicitly add condensing settings
 			condensingApiConfigId: stateValues.condensingApiConfigId,
 			customCondensingPrompt: stateValues.customCondensingPrompt,
 			codebaseIndexModels: stateValues.codebaseIndexModels ?? EMBEDDING_MODEL_PROFILES,
@@ -1936,12 +1957,9 @@ export class ClineProvider
 				codebaseIndexSearchMinScore: stateValues.codebaseIndexConfig?.codebaseIndexSearchMinScore,
 			},
 			profileThresholds: stateValues.profileThresholds ?? {},
-			// Add diagnostic message settings
 			includeDiagnosticMessages: stateValues.includeDiagnosticMessages ?? true,
 			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()
@@ -1953,7 +1971,6 @@ export class ClineProvider
 					return false
 				}
 			})(),
-			// Add image generation settings
 			openRouterImageApiKey: stateValues.openRouterImageApiKey,
 			openRouterImageGenerationSelectedModel: stateValues.openRouterImageGenerationSelectedModel,
 		}
@@ -2390,12 +2407,12 @@ export class ClineProvider
 	// Provider Profiles
 
 	public async getProviderProfiles(): Promise<{ name: string; provider?: string }[]> {
-		const { listApiConfigMeta } = await this.getState()
+		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()
+		const { currentApiConfigName = "default" } = await this.getState()
 		return currentApiConfigName
 	}
 
@@ -2446,7 +2463,7 @@ export class ClineProvider
 	}
 
 	private async getTaskProperties(): Promise<DynamicAppProperties & TaskProperties> {
-		const { language, mode, apiConfiguration } = await this.getState()
+		const { language = "en", mode, apiConfiguration } = await this.getState()
 
 		const task = this.getCurrentTask()
 		const todoList = task?.todoList

+ 28 - 2
src/core/webview/webviewMessageHandler.ts

@@ -23,7 +23,12 @@ import { Package } from "../../shared/package"
 import { RouterName, toRouterName, ModelRecord } from "../../shared/api"
 import { MessageEnhancer } from "./messageEnhancer"
 
-import { checkoutDiffPayloadSchema, checkoutRestorePayloadSchema, WebviewMessage } from "../../shared/WebviewMessage"
+import {
+	type WebviewMessage,
+	type EditQueuedMessagePayload,
+	checkoutDiffPayloadSchema,
+	checkoutRestorePayloadSchema,
+} from "../../shared/WebviewMessage"
 import { checkExistKey } from "../../shared/checkExistApiConfig"
 import { experimentDefault } from "../../shared/experiments"
 import { Terminal } from "../../integrations/terminal/Terminal"
@@ -1412,7 +1417,7 @@ export const webviewMessageHandler = async (
 					const {
 						apiConfiguration,
 						customSupportPrompts,
-						listApiConfigMeta,
+						listApiConfigMeta = [],
 						enhancementApiConfigId,
 						includeTaskHistoryInEnhance,
 					} = state
@@ -2671,5 +2676,26 @@ export const webviewMessageHandler = async (
 			vscode.window.showWarningMessage(t("common:mdm.info.organization_requires_auth"))
 			break
 		}
+
+		/**
+		 * Chat Message Queue
+		 */
+
+		case "queueMessage": {
+			provider.getCurrentTask()?.messageQueueService.addMessage(message.text ?? "", message.images)
+			break
+		}
+		case "removeQueuedMessage": {
+			provider.getCurrentTask()?.messageQueueService.removeMessage(message.text ?? "")
+			break
+		}
+		case "editQueuedMessage": {
+			if (message.payload) {
+				const { id, text, images } = message.payload as EditQueuedMessagePayload
+				provider.getCurrentTask()?.messageQueueService.updateMessage(id, text, images)
+			}
+
+			break
+		}
 	}
 }

+ 14 - 1
src/shared/ExtensionMessage.ts

@@ -12,6 +12,7 @@ import type {
 	CloudUserInfo,
 	OrganizationAllowList,
 	ShareVisibility,
+	QueuedMessage,
 } from "@roo-code/types"
 
 import { GitCommit } from "../utils/git"
@@ -196,6 +197,7 @@ export interface ExtensionMessage {
 	messageTs?: number
 	context?: string
 	commands?: Command[]
+	queuedMessages?: QueuedMessage[]
 }
 
 export type ExtensionState = Pick<
@@ -219,8 +221,10 @@ export type ExtensionState = Pick<
 	| "alwaysAllowMcp"
 	| "alwaysAllowModeSwitch"
 	| "alwaysAllowSubtasks"
+	| "alwaysAllowFollowupQuestions"
 	| "alwaysAllowExecute"
 	| "alwaysAllowUpdateTodoList"
+	| "followupAutoApproveTimeoutMs"
 	| "allowedCommands"
 	| "deniedCommands"
 	| "allowedMaxRequests"
@@ -229,6 +233,7 @@ export type ExtensionState = Pick<
 	| "browserViewportSize"
 	| "screenshotQuality"
 	| "remoteBrowserEnabled"
+	| "cachedChromeHostUrl"
 	| "remoteBrowserHost"
 	// | "enableCheckpoints" // Optional in GlobalSettings, required here.
 	| "ttsEnabled"
@@ -274,12 +279,13 @@ export type ExtensionState = Pick<
 	| "maxDiagnosticMessages"
 	| "remoteControlEnabled"
 	| "openRouterImageGenerationSelectedModel"
+	| "includeTaskHistoryInEnhance"
 > & {
 	version: string
 	clineMessages: ClineMessage[]
 	currentTaskItem?: HistoryItem
 	currentTaskTodos?: TodoItem[] // Initial todos for the current task
-	apiConfiguration?: ProviderSettings
+	apiConfiguration: ProviderSettings
 	uriScheme?: string
 	shouldShowAnnouncement: boolean
 
@@ -328,6 +334,13 @@ export type ExtensionState = Pick<
 	profileThresholds: Record<string, number>
 	hasOpenedModeSelector: boolean
 	openRouterImageApiKey?: string
+	openRouterUseMiddleOutTransform?: boolean
+	messageQueue?: QueuedMessage[]
+	lastShownAnnouncementId?: string
+	apiModelId?: string
+	mcpServers?: McpServer[]
+	hasSystemPromptOverride?: boolean
+	mdmCompliant?: boolean
 }
 
 export interface ClineSayTool {

+ 7 - 0
src/shared/WebviewMessage.ts

@@ -7,6 +7,7 @@ import {
 	type InstallMarketplaceItemOptions,
 	type MarketplaceItem,
 	type ShareVisibility,
+	type QueuedMessage,
 	marketplaceItemSchema,
 } from "@roo-code/types"
 
@@ -22,6 +23,8 @@ export interface UpdateTodoListPayload {
 	todos: any[]
 }
 
+export type EditQueuedMessagePayload = Pick<QueuedMessage, "id" | "text" | "images">
+
 export interface WebviewMessage {
 	type:
 		| "updateTodoList"
@@ -215,6 +218,9 @@ export interface WebviewMessage {
 		| "imageGenerationSettings"
 		| "openRouterImageApiKey"
 		| "openRouterImageGenerationSelectedModel"
+		| "queueMessage"
+		| "removeQueuedMessage"
+		| "editQueuedMessage"
 	text?: string
 	editedMessageContent?: string
 	tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "cloud"
@@ -330,3 +336,4 @@ export type WebViewMessagePayload =
 	| IndexClearedPayload
 	| InstallMarketplaceItemWithParametersPayload
 	| UpdateTodoListPayload
+	| EditQueuedMessagePayload

+ 65 - 135
webview-ui/src/components/chat/ChatView.tsx

@@ -23,6 +23,7 @@ import { getApiMetrics } from "@roo/getApiMetrics"
 import { AudioType } from "@roo/WebviewMessage"
 import { getAllModes } from "@roo/modes"
 import { ProfileValidator } from "@roo/ProfileValidator"
+import { getLatestTodo } from "@roo/todo"
 
 import { vscode } from "@src/utils/vscode"
 import {
@@ -54,9 +55,7 @@ import AutoApproveMenu from "./AutoApproveMenu"
 import SystemPromptWarning from "./SystemPromptWarning"
 import ProfileViolationWarning from "./ProfileViolationWarning"
 import { CheckpointWarning } from "./CheckpointWarning"
-import QueuedMessages from "./QueuedMessages"
-import { getLatestTodo } from "@roo/todo"
-import { QueuedMessage } from "@roo-code/types"
+import { QueuedMessages } from "./QueuedMessages"
 
 export interface ChatViewProps {
 	isHidden: boolean
@@ -121,6 +120,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 		soundEnabled,
 		soundVolume,
 		cloudIsAuthenticated,
+		messageQueue = [],
 	} = useExtensionState()
 
 	const messagesRef = useRef(messages)
@@ -174,10 +174,6 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 	const textAreaRef = useRef<HTMLTextAreaElement>(null)
 	const [sendingDisabled, setSendingDisabled] = useState(false)
 	const [selectedImages, setSelectedImages] = useState<string[]>([])
-	const [messageQueue, setMessageQueue] = useState<QueuedMessage[]>([])
-	const isProcessingQueueRef = useRef(false)
-	const retryCountRef = useRef<Map<string, number>>(new Map())
-	const MAX_RETRY_ATTEMPTS = 3
 
 	// we need to hold on to the ask because useEffect > lastMessage will always let us know when an ask comes in and handle it, but by the time handleMessage is called, the last message might not be the ask anymore (it could be a say that followed)
 	const [clineAsk, setClineAsk] = useState<ClineAsk | undefined>(undefined)
@@ -470,11 +466,6 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 		}
 		// Reset user response flag for new task
 		userRespondedRef.current = false
-
-		// Clear message queue when starting a new task
-		setMessageQueue([])
-		// Clear retry counts
-		retryCountRef.current.clear()
 	}, [task?.ts])
 
 	useEffect(() => {
@@ -586,128 +577,70 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 	 * Handles sending messages to the extension
 	 * @param text - The message text to send
 	 * @param images - Array of image data URLs to send with the message
-	 * @param fromQueue - Internal flag indicating if this message is being sent from the queue (prevents re-queueing)
 	 */
 	const handleSendMessage = useCallback(
-		(text: string, images: string[], fromQueue = false) => {
-			try {
-				text = text.trim()
-
-				if (text || images.length > 0) {
-					if (sendingDisabled && !fromQueue) {
-						// Generate a more unique ID using timestamp + random component
-						const messageId = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
-						setMessageQueue((prev: QueuedMessage[]) => [...prev, { id: messageId, text, images }])
+		(text: string, images: string[]) => {
+			text = text.trim()
+
+			if (text || images.length > 0) {
+				if (sendingDisabled) {
+					try {
+						console.log("queueMessage", text, images)
+						vscode.postMessage({ type: "queueMessage", text, images })
 						setInputValue("")
 						setSelectedImages([])
-						return
-					}
-					// Mark that user has responded - this prevents any pending auto-approvals
-					userRespondedRef.current = true
-
-					if (messagesRef.current.length === 0) {
-						vscode.postMessage({ type: "newTask", text, images })
-					} else if (clineAskRef.current) {
-						if (clineAskRef.current === "followup") {
-							markFollowUpAsAnswered()
-						}
-
-						// Use clineAskRef.current
-						switch (
-							clineAskRef.current // Use clineAskRef.current
-						) {
-							case "followup":
-							case "tool":
-							case "browser_action_launch":
-							case "command": // User can provide feedback to a tool or command use.
-							case "command_output": // User can send input to command stdin.
-							case "use_mcp_server":
-							case "completion_result": // If this happens then the user has feedback for the completion result.
-							case "resume_task":
-							case "resume_completed_task":
-							case "mistake_limit_reached":
-								vscode.postMessage({
-									type: "askResponse",
-									askResponse: "messageResponse",
-									text,
-									images,
-								})
-								break
-							// There is no other case that a textfield should be enabled.
-						}
-					} else {
-						// This is a new message in an ongoing task.
-						vscode.postMessage({ type: "askResponse", askResponse: "messageResponse", text, images })
+					} catch (error) {
+						console.error(
+							`Failed to queue message: ${error instanceof Error ? error.message : String(error)}`,
+						)
 					}
 
-					handleChatReset()
-				}
-			} catch (error) {
-				console.error("Error in handleSendMessage:", error)
-				// If this was a queued message, we should handle it differently
-				if (fromQueue) {
-					throw error // Re-throw to be caught by the queue processor
+					return
 				}
-				// For direct sends, we could show an error to the user
-				// but for now we'll just log it
-			}
-		},
-		[handleChatReset, markFollowUpAsAnswered, sendingDisabled], // messagesRef and clineAskRef are stable
-	)
-
-	useEffect(() => {
-		// Early return if conditions aren't met
-		// Also don't process queue if there's an API error (clineAsk === "api_req_failed")
-		if (
-			sendingDisabled ||
-			messageQueue.length === 0 ||
-			isProcessingQueueRef.current ||
-			clineAsk === "api_req_failed"
-		) {
-			return
-		}
-
-		// Mark as processing immediately to prevent race conditions
-		isProcessingQueueRef.current = true
-
-		// Process the first message in the queue
-		const [nextMessage, ...remaining] = messageQueue
-
-		// Update queue immediately to prevent duplicate processing
-		setMessageQueue(remaining)
 
-		// Process the message
-		Promise.resolve()
-			.then(() => {
-				handleSendMessage(nextMessage.text, nextMessage.images, true)
-				// Clear retry count on success
-				retryCountRef.current.delete(nextMessage.id)
-			})
-			.catch((error) => {
-				console.error("Failed to send queued message:", error)
+				// Mark that user has responded - this prevents any pending auto-approvals.
+				userRespondedRef.current = true
 
-				// Get current retry count
-				const retryCount = retryCountRef.current.get(nextMessage.id) || 0
+				if (messagesRef.current.length === 0) {
+					vscode.postMessage({ type: "newTask", text, images })
+				} else if (clineAskRef.current) {
+					if (clineAskRef.current === "followup") {
+						markFollowUpAsAnswered()
+					}
 
-				// Only re-add if under retry limit
-				if (retryCount < MAX_RETRY_ATTEMPTS) {
-					retryCountRef.current.set(nextMessage.id, retryCount + 1)
-					// Re-add the message to the end of the queue
-					setMessageQueue((current: QueuedMessage[]) => [...current, nextMessage])
+					// Use clineAskRef.current
+					switch (
+						clineAskRef.current // Use clineAskRef.current
+					) {
+						case "followup":
+						case "tool":
+						case "browser_action_launch":
+						case "command": // User can provide feedback to a tool or command use.
+						case "command_output": // User can send input to command stdin.
+						case "use_mcp_server":
+						case "completion_result": // If this happens then the user has feedback for the completion result.
+						case "resume_task":
+						case "resume_completed_task":
+						case "mistake_limit_reached":
+							vscode.postMessage({
+								type: "askResponse",
+								askResponse: "messageResponse",
+								text,
+								images,
+							})
+							break
+						// There is no other case that a textfield should be enabled.
+					}
 				} else {
-					console.error(`Message ${nextMessage.id} failed after ${MAX_RETRY_ATTEMPTS} attempts, discarding`)
-					retryCountRef.current.delete(nextMessage.id)
+					// This is a new message in an ongoing task.
+					vscode.postMessage({ type: "askResponse", askResponse: "messageResponse", text, images })
 				}
-			})
-			.finally(() => {
-				isProcessingQueueRef.current = false
-			})
 
-		// Cleanup function to handle component unmount
-		return () => {
-			isProcessingQueueRef.current = false
-		}
-	}, [sendingDisabled, messageQueue, handleSendMessage, clineAsk])
+				handleChatReset()
+			}
+		},
+		[handleChatReset, markFollowUpAsAnswered, sendingDisabled], // messagesRef and clineAskRef are stable
+	)
 
 	const handleSetChatBoxMessage = useCallback(
 		(text: string, images: string[]) => {
@@ -724,18 +657,6 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 		[inputValue, selectedImages],
 	)
 
-	// Cleanup retry count map on unmount
-	useEffect(() => {
-		// Store refs in variables to avoid stale closure issues
-		const retryCountMap = retryCountRef.current
-		const isProcessingRef = isProcessingQueueRef
-
-		return () => {
-			retryCountMap.clear()
-			isProcessingRef.current = false
-		}
-	}, [])
-
 	const startNewTask = useCallback(() => vscode.postMessage({ type: "clearTask" }), [])
 
 	// This logic depends on the useEffect[messages] above to set clineAsk,
@@ -2018,9 +1939,18 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 
 			<QueuedMessages
 				queue={messageQueue}
-				onRemove={(index) => setMessageQueue((prev) => prev.filter((_, i) => i !== index))}
+				onRemove={(index) => {
+					if (messageQueue[index]) {
+						vscode.postMessage({ type: "removeQueuedMessage", text: messageQueue[index].id })
+					}
+				}}
 				onUpdate={(index, newText) => {
-					setMessageQueue((prev) => prev.map((msg, i) => (i === index ? { ...msg, text: newText } : msg)))
+					if (messageQueue[index]) {
+						vscode.postMessage({
+							type: "editQueuedMessage",
+							payload: { id: messageQueue[index].id, text: newText, images: messageQueue[index].images },
+						})
+					}
 				}}
 			/>
 			<ChatTextArea

+ 8 - 6
webview-ui/src/components/chat/QueuedMessages.tsx

@@ -1,17 +1,21 @@
-import React, { useState } from "react"
+import { useState } from "react"
 import { useTranslation } from "react-i18next"
-import Thumbnails from "../common/Thumbnails"
+
 import { QueuedMessage } from "@roo-code/types"
-import { Mention } from "./Mention"
+
 import { Button } from "@src/components/ui"
 
+import Thumbnails from "../common/Thumbnails"
+
+import { Mention } from "./Mention"
+
 interface QueuedMessagesProps {
 	queue: QueuedMessage[]
 	onRemove: (index: number) => void
 	onUpdate: (index: number, newText: string) => void
 }
 
-const QueuedMessages: React.FC<QueuedMessagesProps> = ({ queue, onRemove, onUpdate }) => {
+export const QueuedMessages = ({ queue, onRemove, onUpdate }: QueuedMessagesProps) => {
 	const { t } = useTranslation("chat")
 	const [editingStates, setEditingStates] = useState<Record<string, { isEditing: boolean; value: string }>>({})
 
@@ -108,5 +112,3 @@ const QueuedMessages: React.FC<QueuedMessagesProps> = ({ queue, onRemove, onUpda
 		</div>
 	)
 }
-
-export default QueuedMessages

+ 9 - 8
webview-ui/src/components/chat/__tests__/ChatView.spec.tsx

@@ -97,22 +97,23 @@ vi.mock("@src/components/welcome/RooCloudCTA", () => ({
 
 // Mock QueuedMessages component
 vi.mock("../QueuedMessages", () => ({
-	default: function MockQueuedMessages({
-		messages = [],
-		onRemoveMessage,
+	QueuedMessages: function MockQueuedMessages({
+		queue = [],
+		onRemove,
 	}: {
-		messages?: Array<{ id: string; text: string; images?: string[] }>
-		onRemoveMessage?: (id: string) => void
+		queue?: Array<{ id: string; text: string; images?: string[] }>
+		onRemove?: (index: number) => void
+		onUpdate?: (index: number, newText: string) => void
 	}) {
-		if (!messages || messages.length === 0) {
+		if (!queue || queue.length === 0) {
 			return null
 		}
 		return (
 			<div data-testid="queued-messages">
-				{messages.map((msg) => (
+				{queue.map((msg, index) => (
 					<div key={msg.id}>
 						<span>{msg.text}</span>
-						<button aria-label="Remove message" onClick={() => onRemoveMessage?.(msg.id)}>
+						<button aria-label="Remove message" onClick={() => onRemove?.(index)}>
 							Remove
 						</button>
 					</div>

+ 2 - 1
webview-ui/src/context/ExtensionStateContext.tsx

@@ -176,7 +176,8 @@ export const mergeExtensionState = (prevState: ExtensionState, newState: Extensi
 }
 
 export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
-	const [state, setState] = useState<ExtensionState & { organizationAllowList?: OrganizationAllowList }>({
+	const [state, setState] = useState<ExtensionState>({
+		apiConfiguration: {},
 		version: "",
 		clineMessages: [],
 		taskHistory: [],