mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
Move message queue to the extension host (#7604)
Co-authored-by: roomote[bot] <219738659+roomote[bot]@users.noreply.github.com>
This commit is contained in:
parent
7e24600877
commit
5196c75017
18 changed files with 385 additions and 221 deletions
|
|
@ -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 },
|
||||
|
|
|
|||
|
|
@ -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,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",
|
||||
|
|
|
|||
|
|
@ -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,6 +465,26 @@ export const extensionBridgeEventSchema = z.discriminatedUnion("type", [
|
|||
instance: extensionInstanceSchema,
|
||||
timestamp: z.number(),
|
||||
}),
|
||||
|
||||
z.object({
|
||||
type: z.literal(ExtensionBridgeEventName.TaskUserMessage),
|
||||
instance: extensionInstanceSchema,
|
||||
timestamp: z.number(),
|
||||
}),
|
||||
|
||||
z.object({
|
||||
type: z.literal(ExtensionBridgeEventName.ModeChanged),
|
||||
instance: extensionInstanceSchema,
|
||||
mode: z.string(),
|
||||
timestamp: z.number(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal(ExtensionBridgeEventName.ProviderProfileChanged),
|
||||
instance: extensionInstanceSchema,
|
||||
providerProfile: z.object({ name: z.string(), provider: z.string().optional() }),
|
||||
timestamp: z.number(),
|
||||
}),
|
||||
|
||||
z.object({
|
||||
type: z.literal(ExtensionBridgeEventName.InstanceRegistered),
|
||||
instance: extensionInstanceSchema,
|
||||
|
|
@ -476,18 +500,6 @@ export const extensionBridgeEventSchema = z.discriminatedUnion("type", [
|
|||
instance: extensionInstanceSchema,
|
||||
timestamp: z.number(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal(ExtensionBridgeEventName.ModeChanged),
|
||||
instance: extensionInstanceSchema,
|
||||
mode: z.string(),
|
||||
timestamp: z.number(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal(ExtensionBridgeEventName.ProviderProfileChanged),
|
||||
instance: extensionInstanceSchema,
|
||||
providerProfile: z.object({ name: z.string(), provider: z.string().optional() }),
|
||||
timestamp: z.number(),
|
||||
}),
|
||||
])
|
||||
|
||||
export type ExtensionBridgeEvent = z.infer<typeof extensionBridgeEventSchema>
|
||||
|
|
|
|||
|
|
@ -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]),
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
src/core/message-queue/MessageQueueService.ts
Normal file
98
src/core/message-queue/MessageQueueService.ts
Normal file
|
|
@ -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,3 +1,3 @@
|
|||
export { readApiMessages, saveApiMessages } from "./apiMessages"
|
||||
export { type ApiMessage, readApiMessages, saveApiMessages } from "./apiMessages"
|
||||
export { readTaskMessages, saveTaskMessages } from "./taskMessages"
|
||||
export { taskMetadata } from "./taskMetadata"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,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
|
||||
|
|
|
|||
|
|
@ -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,129 +577,71 @@ 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()
|
||||
(text: string, images: string[]) => {
|
||||
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 }])
|
||||
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()
|
||||
return
|
||||
}
|
||||
} 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
|
||||
|
||||
// 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 })
|
||||
}
|
||||
// For direct sends, we could show an error to the user
|
||||
// but for now we'll just log it
|
||||
|
||||
handleChatReset()
|
||||
}
|
||||
},
|
||||
[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)
|
||||
|
||||
// Get current retry count
|
||||
const retryCount = retryCountRef.current.get(nextMessage.id) || 0
|
||||
|
||||
// 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])
|
||||
} else {
|
||||
console.error(`Message ${nextMessage.id} failed after ${MAX_RETRY_ATTEMPTS} attempts, discarding`)
|
||||
retryCountRef.current.delete(nextMessage.id)
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
isProcessingQueueRef.current = false
|
||||
})
|
||||
|
||||
// Cleanup function to handle component unmount
|
||||
return () => {
|
||||
isProcessingQueueRef.current = false
|
||||
}
|
||||
}, [sendingDisabled, messageQueue, handleSendMessage, clineAsk])
|
||||
|
||||
const handleSetChatBoxMessage = useCallback(
|
||||
(text: string, images: string[]) => {
|
||||
// Avoid nested template literals by breaking down the logic
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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: [],
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue