mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-06 08:18:39 +00:00
fix: queued messages with only images now properly sent to UI and LLM
Fixes EXT-639 where queued messages containing only images (no text)
would vanish after being dequeued without being shown in the UI or
sent to the LLM.
Root cause: processQueuedMessages called submitUserMessage which
sets askResponse values, but those values are only consumed when
there is a pending ask(). When called after tool execution (no
pending ask), the content was lost.
Fix: Changed processQueuedMessages to directly:
1. Show user feedback in UI via say("user_feedback", ...)
2. Add content to userMessageContent array for the next LLM request
3. Handle image-only messages with hasText || hasImages check
Added test to verify image-only queued messages work correctly.
This commit is contained in:
parent
17d3456e96
commit
ef66ce637c
2 changed files with 82 additions and 26 deletions
|
|
@ -4663,22 +4663,41 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
|
|||
}
|
||||
|
||||
/**
|
||||
* Process any queued messages by dequeuing and submitting them.
|
||||
* This ensures that queued user messages are sent when appropriate,
|
||||
* preventing them from getting stuck in the queue.
|
||||
* Process any queued messages by dequeuing and adding them to the current
|
||||
* user message content. This ensures that queued user messages are included
|
||||
* in the next LLM request, preventing them from getting stuck in the queue.
|
||||
*
|
||||
* @param context - Context string for logging (e.g., the calling tool name)
|
||||
* Unlike submitUserMessage (which sets askResponse values for pending asks),
|
||||
* this method directly adds content to userMessageContent, which is appropriate
|
||||
* when called after tool execution when there's no pending ask.
|
||||
*/
|
||||
public processQueuedMessages(): void {
|
||||
try {
|
||||
if (!this.messageQueueService.isEmpty()) {
|
||||
const queued = this.messageQueueService.dequeueMessage()
|
||||
if (queued) {
|
||||
setTimeout(() => {
|
||||
this.submitUserMessage(queued.text, queued.images).catch((err) =>
|
||||
console.error(`[Task] Failed to submit queued message:`, err),
|
||||
const text = (queued.text ?? "").trim()
|
||||
const images = queued.images ?? []
|
||||
const hasText = text.length > 0
|
||||
const hasImages = images.length > 0
|
||||
|
||||
if (hasText || hasImages) {
|
||||
// Show user feedback in the UI
|
||||
this.say("user_feedback", queued.text, queued.images).catch((err) =>
|
||||
console.error(`[Task] Failed to show queued message feedback:`, err),
|
||||
)
|
||||
}, 0)
|
||||
|
||||
// Add to userMessageContent for the next LLM request
|
||||
if (hasText) {
|
||||
this.userMessageContent.push({
|
||||
type: "text",
|
||||
text: `<user_message>\n${text}\n</user_message>`,
|
||||
})
|
||||
}
|
||||
if (hasImages) {
|
||||
this.userMessageContent.push(...formatResponse.imageBlocks(images))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
|
|
|
|||
|
|
@ -1861,20 +1861,63 @@ describe("Queued message processing after condense", () => {
|
|||
|
||||
// Make condense fast + deterministic
|
||||
vi.spyOn(task as any, "getSystemPrompt").mockResolvedValue("system")
|
||||
const submitSpy = vi.spyOn(task, "submitUserMessage").mockResolvedValue(undefined)
|
||||
const saySpy = vi.spyOn(task, "say").mockResolvedValue(undefined)
|
||||
|
||||
// Queue a message during condensing
|
||||
task.messageQueueService.addMessage("queued text", ["img1.png"])
|
||||
|
||||
// Use fake timers to capture setTimeout(0) in processQueuedMessages
|
||||
vi.useFakeTimers()
|
||||
await task.condenseContext()
|
||||
|
||||
// Flush the microtask that submits the queued message
|
||||
vi.runAllTimers()
|
||||
vi.useRealTimers()
|
||||
// Verify the message was shown in UI
|
||||
expect(saySpy).toHaveBeenCalledWith("user_feedback", "queued text", ["img1.png"])
|
||||
|
||||
expect(submitSpy).toHaveBeenCalledWith("queued text", ["img1.png"])
|
||||
// Verify the content was added to userMessageContent
|
||||
expect(task.userMessageContent.length).toBeGreaterThan(0)
|
||||
const textBlock = task.userMessageContent.find(
|
||||
(block) => block.type === "text" && (block as any).text?.includes("queued text"),
|
||||
)
|
||||
expect(textBlock).toBeDefined()
|
||||
|
||||
// Verify queue was emptied
|
||||
expect(task.messageQueueService.isEmpty()).toBe(true)
|
||||
})
|
||||
|
||||
it("processes image-only queued messages correctly", async () => {
|
||||
const provider = createProvider()
|
||||
const task = new Task({
|
||||
provider,
|
||||
apiConfiguration: apiConfig,
|
||||
task: "initial task",
|
||||
startTask: false,
|
||||
})
|
||||
|
||||
vi.spyOn(task as any, "getSystemPrompt").mockResolvedValue("system")
|
||||
const saySpy = vi.spyOn(task, "say").mockResolvedValue(undefined)
|
||||
|
||||
// Queue a message with ONLY images (no text) - this is the bug scenario
|
||||
// Images must be in data URL format for formatResponse.imageBlocks to parse them correctly
|
||||
const testImages = [
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
|
||||
"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRof",
|
||||
]
|
||||
task.messageQueueService.addMessage("", testImages)
|
||||
|
||||
await task.condenseContext()
|
||||
|
||||
// Verify the message was shown in UI (with empty text but images)
|
||||
expect(saySpy).toHaveBeenCalledWith("user_feedback", "", testImages)
|
||||
|
||||
// Verify image blocks were added to userMessageContent
|
||||
const imageBlocks = task.userMessageContent.filter((block) => block.type === "image")
|
||||
expect(imageBlocks.length).toBe(2)
|
||||
|
||||
// Verify no text block was added for empty text
|
||||
const textBlocks = task.userMessageContent.filter(
|
||||
(block) => block.type === "text" && (block as any).text?.includes("<user_message>"),
|
||||
)
|
||||
expect(textBlocks.length).toBe(0)
|
||||
|
||||
// Verify queue was emptied
|
||||
expect(task.messageQueueService.isEmpty()).toBe(true)
|
||||
})
|
||||
|
||||
|
|
@ -1898,29 +1941,23 @@ describe("Queued message processing after condense", () => {
|
|||
vi.spyOn(taskA as any, "getSystemPrompt").mockResolvedValue("system")
|
||||
vi.spyOn(taskB as any, "getSystemPrompt").mockResolvedValue("system")
|
||||
|
||||
const spyA = vi.spyOn(taskA, "submitUserMessage").mockResolvedValue(undefined)
|
||||
const spyB = vi.spyOn(taskB, "submitUserMessage").mockResolvedValue(undefined)
|
||||
const saySpyA = vi.spyOn(taskA, "say").mockResolvedValue(undefined)
|
||||
const saySpyB = vi.spyOn(taskB, "say").mockResolvedValue(undefined)
|
||||
|
||||
taskA.messageQueueService.addMessage("A message")
|
||||
taskB.messageQueueService.addMessage("B message")
|
||||
|
||||
// Condense in task A should only drain A's queue
|
||||
vi.useFakeTimers()
|
||||
await taskA.condenseContext()
|
||||
vi.runAllTimers()
|
||||
vi.useRealTimers()
|
||||
|
||||
expect(spyA).toHaveBeenCalledWith("A message", undefined)
|
||||
expect(spyB).not.toHaveBeenCalled()
|
||||
expect(saySpyA).toHaveBeenCalledWith("user_feedback", "A message", undefined)
|
||||
expect(saySpyB).not.toHaveBeenCalledWith("user_feedback", expect.anything(), expect.anything())
|
||||
expect(taskB.messageQueueService.isEmpty()).toBe(false)
|
||||
|
||||
// Now condense in task B should drain B's queue
|
||||
vi.useFakeTimers()
|
||||
await taskB.condenseContext()
|
||||
vi.runAllTimers()
|
||||
vi.useRealTimers()
|
||||
|
||||
expect(spyB).toHaveBeenCalledWith("B message", undefined)
|
||||
expect(saySpyB).toHaveBeenCalledWith("user_feedback", "B message", undefined)
|
||||
expect(taskB.messageQueueService.isEmpty()).toBe(true)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue