From 9784d0773953378b93b0a1e3761f4f8261412c6a Mon Sep 17 00:00:00 2001 From: Hannes Rudolph Date: Wed, 10 Sep 2025 16:59:57 -0600 Subject: [PATCH] fix: preserve chat input when queued messages are sent (#7861) When a queued message is sent, the chat input was being cleared even if the user had typed new text. This fix: - Adds a fromQueue parameter to handleSendMessage to track when messages are sent from the queue - Checks if invoke sendMessage corresponds to a queued message to determine fromQueue status - Only clears the input when fromQueue is false, preserving user input for queued messages - Adds comprehensive tests to verify the behavior Fixes #7861 --- webview-ui/src/components/chat/ChatView.tsx | 20 +- .../ChatView.queuedMessages.spec.tsx | 213 ++++++++++++++++++ 2 files changed, 229 insertions(+), 4 deletions(-) create mode 100644 webview-ui/src/components/chat/__tests__/ChatView.queuedMessages.spec.tsx diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index e0528da24c..be9a8680c7 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -589,13 +589,14 @@ const ChatViewComponent: React.ForwardRefRenderFunction { + (text: string, images: string[], fromQueue: boolean = false) => { text = text.trim() if (text || images.length > 0) { - if (sendingDisabled) { + if (sendingDisabled && !fromQueue) { try { console.log("queueMessage", text, images) vscode.postMessage({ type: "queueMessage", text, images }) @@ -648,7 +649,11 @@ const ChatViewComponent: React.ForwardRefRenderFunction + queuedMsg.text === message.text && + JSON.stringify(queuedMsg.images || []) === JSON.stringify(message.images || []), + ) + handleSendMessage(message.text ?? "", message.images ?? [], isFromQueue) break case "setChatBoxMessage": handleSetChatBoxMessage(message.text ?? "", message.images ?? []) @@ -846,6 +857,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction ({ + vscode: { + postMessage: vi.fn(), + }, +})) + +vi.mock("use-sound", () => ({ + default: () => [vi.fn()], +})) + +// Mock the extension state hook +vi.mock("@src/context/ExtensionStateContext", async () => { + const actual = await vi.importActual("@src/context/ExtensionStateContext") + return { + ...actual, + useExtensionState: vi.fn(() => ({ + clineMessages: [], + taskHistory: [], + apiConfiguration: { apiProvider: "test" }, + messageQueue: [], + mode: "code", + customModes: [], + setMode: vi.fn(), + })), + } +}) + +// Now import components after all mocks are set up +import ChatView from "../ChatView" +import { ExtensionStateContextProvider, useExtensionState } from "@src/context/ExtensionStateContext" +import { vscode } from "@src/utils/vscode" + +// Set up global mock +;(global as any).acquireVsCodeApi = () => ({ + postMessage: vi.fn(), + getState: () => ({}), + setState: vi.fn(), +}) + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { retry: false }, + mutations: { retry: false }, + }, +}) + +const renderChatView = () => { + return render( + + + + + , + ) +} + +describe("ChatView - Queued Messages", () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it("should preserve input text when processing queued messages", async () => { + // Mock the state with a queued message + const mockUseExtensionState = useExtensionState as any + mockUseExtensionState.mockReturnValue({ + clineMessages: [], + taskHistory: [], + apiConfiguration: { apiProvider: "test" }, + messageQueue: [ + { + id: "queue-1", + text: "Queued message", + images: [], + timestamp: Date.now(), + }, + ], + mode: "code", + customModes: [], + setMode: vi.fn(), + }) + + const { container } = renderChatView() + + // Find the textarea + const textarea = container.querySelector("textarea") as HTMLTextAreaElement + expect(textarea).toBeTruthy() + + // User types new text while message is queued + await userEvent.type(textarea, "New text typed by user") + expect(textarea.value).toBe("New text typed by user") + + // Simulate backend processing the queued message by sending invoke message + const invokeMessage = new MessageEvent("message", { + data: { + type: "invoke", + invoke: "sendMessage", + text: "Queued message", + images: [], + }, + }) + window.dispatchEvent(invokeMessage) + + // Wait for any async operations + await waitFor(() => { + // The input should still contain the user's typed text + expect(textarea.value).toBe("New text typed by user") + }) + + // Verify the queued message was sent + expect(vscode.postMessage).toHaveBeenCalledWith( + expect.objectContaining({ + type: expect.stringMatching(/newTask|askResponse/), + }), + ) + }) + + it("should clear input when sending a regular message (not from queue)", async () => { + // Mock the state with no queued messages + const mockUseExtensionState = useExtensionState as any + mockUseExtensionState.mockReturnValue({ + clineMessages: [], + taskHistory: [], + apiConfiguration: { apiProvider: "test" }, + messageQueue: [], // No queued messages + mode: "code", + customModes: [], + setMode: vi.fn(), + }) + + const { container } = renderChatView() + + // Find the textarea + const textarea = container.querySelector("textarea") as HTMLTextAreaElement + expect(textarea).toBeTruthy() + + // User types text + await userEvent.type(textarea, "Regular message") + expect(textarea.value).toBe("Regular message") + + // Simulate backend sending invoke message for a non-queued message + const invokeMessage = new MessageEvent("message", { + data: { + type: "invoke", + invoke: "sendMessage", + text: "Different message not in queue", + images: [], + }, + }) + window.dispatchEvent(invokeMessage) + + // Wait for any async operations + await waitFor(() => { + // The input should be cleared since this is not a queued message + expect(textarea.value).toBe("") + }) + }) + + it("should handle messages with images correctly", async () => { + // Mock the state with a queued message with image + const mockUseExtensionState = useExtensionState as any + mockUseExtensionState.mockReturnValue({ + clineMessages: [], + taskHistory: [], + apiConfiguration: { apiProvider: "test" }, + messageQueue: [ + { + id: "queue-2", + text: "Message with image", + images: ["data:image/png;base64,abc123"], + timestamp: Date.now(), + }, + ], + mode: "code", + customModes: [], + setMode: vi.fn(), + }) + + const { container } = renderChatView() + + // Find the textarea + const textarea = container.querySelector("textarea") as HTMLTextAreaElement + expect(textarea).toBeTruthy() + + // User types new text + await userEvent.type(textarea, "User typing while image message queued") + expect(textarea.value).toBe("User typing while image message queued") + + // Simulate backend processing the queued message with image + const invokeMessage = new MessageEvent("message", { + data: { + type: "invoke", + invoke: "sendMessage", + text: "Message with image", + images: ["data:image/png;base64,abc123"], + }, + }) + window.dispatchEvent(invokeMessage) + + // Wait for any async operations + await waitFor(() => { + // The input should still contain the user's typed text + expect(textarea.value).toBe("User typing while image message queued") + }) + }) +})