mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-07 08:26:51 +00:00
fix: queue messages when button approval is pending (tool/command/mcp)
Fixes #10675 - Messages now queue when approval buttons are shown for: - tool (file edits: Save/Reject) - command (command execution: Run Command/Reject) - browser_action_launch (browser actions: Approve/Reject) - use_mcp_server (MCP server usage: Approve/Reject) - command_output (command running: Proceed While Running/Kill Command) Previously, messages sent during these states were lost because they were sent as askResponse but the backend expected button clicks. Now messages are properly queued and processed after the approval interaction completes.
This commit is contained in:
parent
d689de34f8
commit
ec5cf17d19
2 changed files with 200 additions and 2 deletions
|
|
@ -62,6 +62,16 @@ export const MAX_IMAGES_PER_MESSAGE = 20 // This is the Anthropic limit.
|
|||
|
||||
const isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0
|
||||
|
||||
// Ask types that display approval buttons where typed messages should be queued
|
||||
// rather than sent directly as askResponse (which would be lost/ignored).
|
||||
const BUTTON_APPROVAL_ASK_TYPES: ClineAsk[] = [
|
||||
"tool", // File edits: Save/Reject
|
||||
"command", // Command execution: Run Command/Reject
|
||||
"browser_action_launch", // Browser actions: Approve/Reject
|
||||
"use_mcp_server", // MCP server usage: Approve/Reject
|
||||
"command_output", // Command running: Proceed While Running/Kill Command
|
||||
]
|
||||
|
||||
const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewProps> = (
|
||||
{ isHidden, showAnnouncement, hideAnnouncement },
|
||||
ref,
|
||||
|
|
@ -587,7 +597,13 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
// - Task is busy (sendingDisabled)
|
||||
// - API request in progress (isStreaming)
|
||||
// - Queue has items (preserve message order during drain)
|
||||
if (sendingDisabled || isStreaming || messageQueue.length > 0) {
|
||||
// - Waiting for button approval (tool/command/browser/mcp/command_output)
|
||||
const isWaitingForButtonApproval =
|
||||
clineAskRef.current !== undefined &&
|
||||
BUTTON_APPROVAL_ASK_TYPES.includes(clineAskRef.current) &&
|
||||
enableButtons
|
||||
|
||||
if (sendingDisabled || isStreaming || messageQueue.length > 0 || isWaitingForButtonApproval) {
|
||||
try {
|
||||
console.log("queueMessage", text, images)
|
||||
vscode.postMessage({ type: "queueMessage", text, images })
|
||||
|
|
@ -643,7 +659,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
handleChatReset()
|
||||
}
|
||||
},
|
||||
[handleChatReset, markFollowUpAsAnswered, sendingDisabled, isStreaming, messageQueue.length], // messagesRef and clineAskRef are stable
|
||||
[handleChatReset, markFollowUpAsAnswered, sendingDisabled, isStreaming, messageQueue.length, enableButtons], // messagesRef and clineAskRef are stable
|
||||
)
|
||||
|
||||
const handleSetChatBoxMessage = useCallback(
|
||||
|
|
|
|||
|
|
@ -1081,6 +1081,188 @@ describe("ChatView - Message Queueing Tests", () => {
|
|||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it("queues messages when tool approval buttons are shown (tool state) - Issue #10675", async () => {
|
||||
const { getByTestId, container } = renderChatView()
|
||||
|
||||
// Hydrate state with a tool ask (file edit approval)
|
||||
mockPostMessage({
|
||||
clineMessages: [
|
||||
{
|
||||
type: "say",
|
||||
say: "task",
|
||||
ts: Date.now() - 2000,
|
||||
text: "Initial task",
|
||||
},
|
||||
{
|
||||
type: "ask",
|
||||
ask: "tool",
|
||||
ts: Date.now(),
|
||||
text: JSON.stringify({ tool: "editedExistingFile", path: "test.txt", diff: "some diff" }),
|
||||
partial: false, // Buttons are shown when not partial
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
// Wait for buttons to become enabled (indicates enableButtons = true)
|
||||
await waitFor(() => {
|
||||
const buttons = container.querySelectorAll("button")
|
||||
const saveButton = Array.from(buttons).find((btn) => btn.textContent?.includes("chat:save.title"))
|
||||
expect(saveButton).toBeTruthy()
|
||||
expect(saveButton).not.toHaveAttribute("disabled")
|
||||
})
|
||||
|
||||
// Clear message calls before simulating user input
|
||||
vi.mocked(vscode.postMessage).mockClear()
|
||||
|
||||
// Simulate user typing and sending a message while Save/Reject buttons are shown
|
||||
const chatTextArea = getByTestId("chat-textarea")
|
||||
const input = chatTextArea.querySelector("input")! as HTMLInputElement
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.change(input, { target: { value: "additional context for later" } })
|
||||
fireEvent.keyDown(input, { key: "Enter", code: "Enter" })
|
||||
})
|
||||
|
||||
// Verify that the message was queued, not sent as askResponse
|
||||
await waitFor(() => {
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith({
|
||||
type: "queueMessage",
|
||||
text: "additional context for later",
|
||||
images: [],
|
||||
})
|
||||
})
|
||||
|
||||
// Verify it was NOT sent as askResponse (which would cause the message to be lost)
|
||||
expect(vscode.postMessage).not.toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: "askResponse",
|
||||
askResponse: "messageResponse",
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it("queues messages when command approval buttons are shown (command state)", async () => {
|
||||
const { getByTestId, container } = renderChatView()
|
||||
|
||||
// Hydrate state with a command ask (command approval)
|
||||
mockPostMessage({
|
||||
clineMessages: [
|
||||
{
|
||||
type: "say",
|
||||
say: "task",
|
||||
ts: Date.now() - 2000,
|
||||
text: "Initial task",
|
||||
},
|
||||
{
|
||||
type: "ask",
|
||||
ask: "command",
|
||||
ts: Date.now(),
|
||||
text: "npm install",
|
||||
partial: false, // Buttons are shown when not partial
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
// Wait for buttons to become enabled (indicates enableButtons = true)
|
||||
await waitFor(() => {
|
||||
const buttons = container.querySelectorAll("button")
|
||||
const runButton = Array.from(buttons).find((btn) => btn.textContent?.includes("chat:runCommand.title"))
|
||||
expect(runButton).toBeTruthy()
|
||||
expect(runButton).not.toHaveAttribute("disabled")
|
||||
})
|
||||
|
||||
// Clear message calls before simulating user input
|
||||
vi.mocked(vscode.postMessage).mockClear()
|
||||
|
||||
// Simulate user typing and sending a message while Run Command/Reject buttons are shown
|
||||
const chatTextArea = getByTestId("chat-textarea")
|
||||
const input = chatTextArea.querySelector("input")! as HTMLInputElement
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.change(input, { target: { value: "wait, let me think about this" } })
|
||||
fireEvent.keyDown(input, { key: "Enter", code: "Enter" })
|
||||
})
|
||||
|
||||
// Verify that the message was queued, not sent as askResponse
|
||||
await waitFor(() => {
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith({
|
||||
type: "queueMessage",
|
||||
text: "wait, let me think about this",
|
||||
images: [],
|
||||
})
|
||||
})
|
||||
|
||||
// Verify it was NOT sent as askResponse
|
||||
expect(vscode.postMessage).not.toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: "askResponse",
|
||||
askResponse: "messageResponse",
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it("queues messages when command is running (command_output state) - Issue #10675", async () => {
|
||||
const { getByTestId, container } = renderChatView()
|
||||
|
||||
// Hydrate state with a command_output ask (command running)
|
||||
mockPostMessage({
|
||||
clineMessages: [
|
||||
{
|
||||
type: "say",
|
||||
say: "task",
|
||||
ts: Date.now() - 2000,
|
||||
text: "Initial task",
|
||||
},
|
||||
{
|
||||
type: "ask",
|
||||
ask: "command_output",
|
||||
ts: Date.now(),
|
||||
text: "Running sleep 60...",
|
||||
partial: false,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
// Wait for buttons to become enabled (indicates enableButtons = true)
|
||||
await waitFor(() => {
|
||||
const buttons = container.querySelectorAll("button")
|
||||
const proceedButton = Array.from(buttons).find((btn) =>
|
||||
btn.textContent?.includes("chat:proceedWhileRunning.title"),
|
||||
)
|
||||
expect(proceedButton).toBeTruthy()
|
||||
expect(proceedButton).not.toHaveAttribute("disabled")
|
||||
})
|
||||
|
||||
// Clear message calls before simulating user input
|
||||
vi.mocked(vscode.postMessage).mockClear()
|
||||
|
||||
// Simulate user typing and sending a message while Proceed While Running/Kill Command buttons are shown
|
||||
const chatTextArea = getByTestId("chat-textarea")
|
||||
const input = chatTextArea.querySelector("input")! as HTMLInputElement
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.change(input, { target: { value: "clarifying message for later" } })
|
||||
fireEvent.keyDown(input, { key: "Enter", code: "Enter" })
|
||||
})
|
||||
|
||||
// Verify that the message was queued, not sent as askResponse
|
||||
await waitFor(() => {
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith({
|
||||
type: "queueMessage",
|
||||
text: "clarifying message for later",
|
||||
images: [],
|
||||
})
|
||||
})
|
||||
|
||||
// Verify it was NOT sent as askResponse (which would cause the message to be lost)
|
||||
expect(vscode.postMessage).not.toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: "askResponse",
|
||||
askResponse: "messageResponse",
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("ChatView - Context Condensing Indicator Tests", () => {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue