Skip to content
This repository was archived by the owner on May 15, 2026. It is now read-only.

Commit ec5cf17

Browse files
committed
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.
1 parent d689de3 commit ec5cf17

2 files changed

Lines changed: 200 additions & 2 deletions

File tree

webview-ui/src/components/chat/ChatView.tsx

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,16 @@ export const MAX_IMAGES_PER_MESSAGE = 20 // This is the Anthropic limit.
6262

6363
const isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0
6464

65+
// Ask types that display approval buttons where typed messages should be queued
66+
// rather than sent directly as askResponse (which would be lost/ignored).
67+
const BUTTON_APPROVAL_ASK_TYPES: ClineAsk[] = [
68+
"tool", // File edits: Save/Reject
69+
"command", // Command execution: Run Command/Reject
70+
"browser_action_launch", // Browser actions: Approve/Reject
71+
"use_mcp_server", // MCP server usage: Approve/Reject
72+
"command_output", // Command running: Proceed While Running/Kill Command
73+
]
74+
6575
const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewProps> = (
6676
{ isHidden, showAnnouncement, hideAnnouncement },
6777
ref,
@@ -587,7 +597,13 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
587597
// - Task is busy (sendingDisabled)
588598
// - API request in progress (isStreaming)
589599
// - Queue has items (preserve message order during drain)
590-
if (sendingDisabled || isStreaming || messageQueue.length > 0) {
600+
// - Waiting for button approval (tool/command/browser/mcp/command_output)
601+
const isWaitingForButtonApproval =
602+
clineAskRef.current !== undefined &&
603+
BUTTON_APPROVAL_ASK_TYPES.includes(clineAskRef.current) &&
604+
enableButtons
605+
606+
if (sendingDisabled || isStreaming || messageQueue.length > 0 || isWaitingForButtonApproval) {
591607
try {
592608
console.log("queueMessage", text, images)
593609
vscode.postMessage({ type: "queueMessage", text, images })
@@ -643,7 +659,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
643659
handleChatReset()
644660
}
645661
},
646-
[handleChatReset, markFollowUpAsAnswered, sendingDisabled, isStreaming, messageQueue.length], // messagesRef and clineAskRef are stable
662+
[handleChatReset, markFollowUpAsAnswered, sendingDisabled, isStreaming, messageQueue.length, enableButtons], // messagesRef and clineAskRef are stable
647663
)
648664

649665
const handleSetChatBoxMessage = useCallback(

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

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1081,6 +1081,188 @@ describe("ChatView - Message Queueing Tests", () => {
10811081
}),
10821082
)
10831083
})
1084+
1085+
it("queues messages when tool approval buttons are shown (tool state) - Issue #10675", async () => {
1086+
const { getByTestId, container } = renderChatView()
1087+
1088+
// Hydrate state with a tool ask (file edit approval)
1089+
mockPostMessage({
1090+
clineMessages: [
1091+
{
1092+
type: "say",
1093+
say: "task",
1094+
ts: Date.now() - 2000,
1095+
text: "Initial task",
1096+
},
1097+
{
1098+
type: "ask",
1099+
ask: "tool",
1100+
ts: Date.now(),
1101+
text: JSON.stringify({ tool: "editedExistingFile", path: "test.txt", diff: "some diff" }),
1102+
partial: false, // Buttons are shown when not partial
1103+
},
1104+
],
1105+
})
1106+
1107+
// Wait for buttons to become enabled (indicates enableButtons = true)
1108+
await waitFor(() => {
1109+
const buttons = container.querySelectorAll("button")
1110+
const saveButton = Array.from(buttons).find((btn) => btn.textContent?.includes("chat:save.title"))
1111+
expect(saveButton).toBeTruthy()
1112+
expect(saveButton).not.toHaveAttribute("disabled")
1113+
})
1114+
1115+
// Clear message calls before simulating user input
1116+
vi.mocked(vscode.postMessage).mockClear()
1117+
1118+
// Simulate user typing and sending a message while Save/Reject buttons are shown
1119+
const chatTextArea = getByTestId("chat-textarea")
1120+
const input = chatTextArea.querySelector("input")! as HTMLInputElement
1121+
1122+
await act(async () => {
1123+
fireEvent.change(input, { target: { value: "additional context for later" } })
1124+
fireEvent.keyDown(input, { key: "Enter", code: "Enter" })
1125+
})
1126+
1127+
// Verify that the message was queued, not sent as askResponse
1128+
await waitFor(() => {
1129+
expect(vscode.postMessage).toHaveBeenCalledWith({
1130+
type: "queueMessage",
1131+
text: "additional context for later",
1132+
images: [],
1133+
})
1134+
})
1135+
1136+
// Verify it was NOT sent as askResponse (which would cause the message to be lost)
1137+
expect(vscode.postMessage).not.toHaveBeenCalledWith(
1138+
expect.objectContaining({
1139+
type: "askResponse",
1140+
askResponse: "messageResponse",
1141+
}),
1142+
)
1143+
})
1144+
1145+
it("queues messages when command approval buttons are shown (command state)", async () => {
1146+
const { getByTestId, container } = renderChatView()
1147+
1148+
// Hydrate state with a command ask (command approval)
1149+
mockPostMessage({
1150+
clineMessages: [
1151+
{
1152+
type: "say",
1153+
say: "task",
1154+
ts: Date.now() - 2000,
1155+
text: "Initial task",
1156+
},
1157+
{
1158+
type: "ask",
1159+
ask: "command",
1160+
ts: Date.now(),
1161+
text: "npm install",
1162+
partial: false, // Buttons are shown when not partial
1163+
},
1164+
],
1165+
})
1166+
1167+
// Wait for buttons to become enabled (indicates enableButtons = true)
1168+
await waitFor(() => {
1169+
const buttons = container.querySelectorAll("button")
1170+
const runButton = Array.from(buttons).find((btn) => btn.textContent?.includes("chat:runCommand.title"))
1171+
expect(runButton).toBeTruthy()
1172+
expect(runButton).not.toHaveAttribute("disabled")
1173+
})
1174+
1175+
// Clear message calls before simulating user input
1176+
vi.mocked(vscode.postMessage).mockClear()
1177+
1178+
// Simulate user typing and sending a message while Run Command/Reject buttons are shown
1179+
const chatTextArea = getByTestId("chat-textarea")
1180+
const input = chatTextArea.querySelector("input")! as HTMLInputElement
1181+
1182+
await act(async () => {
1183+
fireEvent.change(input, { target: { value: "wait, let me think about this" } })
1184+
fireEvent.keyDown(input, { key: "Enter", code: "Enter" })
1185+
})
1186+
1187+
// Verify that the message was queued, not sent as askResponse
1188+
await waitFor(() => {
1189+
expect(vscode.postMessage).toHaveBeenCalledWith({
1190+
type: "queueMessage",
1191+
text: "wait, let me think about this",
1192+
images: [],
1193+
})
1194+
})
1195+
1196+
// Verify it was NOT sent as askResponse
1197+
expect(vscode.postMessage).not.toHaveBeenCalledWith(
1198+
expect.objectContaining({
1199+
type: "askResponse",
1200+
askResponse: "messageResponse",
1201+
}),
1202+
)
1203+
})
1204+
1205+
it("queues messages when command is running (command_output state) - Issue #10675", async () => {
1206+
const { getByTestId, container } = renderChatView()
1207+
1208+
// Hydrate state with a command_output ask (command running)
1209+
mockPostMessage({
1210+
clineMessages: [
1211+
{
1212+
type: "say",
1213+
say: "task",
1214+
ts: Date.now() - 2000,
1215+
text: "Initial task",
1216+
},
1217+
{
1218+
type: "ask",
1219+
ask: "command_output",
1220+
ts: Date.now(),
1221+
text: "Running sleep 60...",
1222+
partial: false,
1223+
},
1224+
],
1225+
})
1226+
1227+
// Wait for buttons to become enabled (indicates enableButtons = true)
1228+
await waitFor(() => {
1229+
const buttons = container.querySelectorAll("button")
1230+
const proceedButton = Array.from(buttons).find((btn) =>
1231+
btn.textContent?.includes("chat:proceedWhileRunning.title"),
1232+
)
1233+
expect(proceedButton).toBeTruthy()
1234+
expect(proceedButton).not.toHaveAttribute("disabled")
1235+
})
1236+
1237+
// Clear message calls before simulating user input
1238+
vi.mocked(vscode.postMessage).mockClear()
1239+
1240+
// Simulate user typing and sending a message while Proceed While Running/Kill Command buttons are shown
1241+
const chatTextArea = getByTestId("chat-textarea")
1242+
const input = chatTextArea.querySelector("input")! as HTMLInputElement
1243+
1244+
await act(async () => {
1245+
fireEvent.change(input, { target: { value: "clarifying message for later" } })
1246+
fireEvent.keyDown(input, { key: "Enter", code: "Enter" })
1247+
})
1248+
1249+
// Verify that the message was queued, not sent as askResponse
1250+
await waitFor(() => {
1251+
expect(vscode.postMessage).toHaveBeenCalledWith({
1252+
type: "queueMessage",
1253+
text: "clarifying message for later",
1254+
images: [],
1255+
})
1256+
})
1257+
1258+
// Verify it was NOT sent as askResponse (which would cause the message to be lost)
1259+
expect(vscode.postMessage).not.toHaveBeenCalledWith(
1260+
expect.objectContaining({
1261+
type: "askResponse",
1262+
askResponse: "messageResponse",
1263+
}),
1264+
)
1265+
})
10841266
})
10851267

10861268
describe("ChatView - Context Condensing Indicator Tests", () => {

0 commit comments

Comments
 (0)