Skip to content

Commit 9784d07

Browse files
committed
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
1 parent 3b1a6d1 commit 9784d07

File tree

2 files changed

+229
-4
lines changed

2 files changed

+229
-4
lines changed

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

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -589,13 +589,14 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
589589
* Handles sending messages to the extension
590590
* @param text - The message text to send
591591
* @param images - Array of image data URLs to send with the message
592+
* @param fromQueue - Whether this message is being sent from the queue (should not clear input)
592593
*/
593594
const handleSendMessage = useCallback(
594-
(text: string, images: string[]) => {
595+
(text: string, images: string[], fromQueue: boolean = false) => {
595596
text = text.trim()
596597

597598
if (text || images.length > 0) {
598-
if (sendingDisabled) {
599+
if (sendingDisabled && !fromQueue) {
599600
try {
600601
console.log("queueMessage", text, images)
601602
vscode.postMessage({ type: "queueMessage", text, images })
@@ -648,7 +649,11 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
648649
vscode.postMessage({ type: "askResponse", askResponse: "messageResponse", text, images })
649650
}
650651

651-
handleChatReset()
652+
// Only reset the chat if this is not a queued message being processed
653+
// When fromQueue is true, we preserve the current input value
654+
if (!fromQueue) {
655+
handleChatReset()
656+
}
652657
}
653658
},
654659
[handleChatReset, markFollowUpAsAnswered, sendingDisabled], // messagesRef and clineAskRef are stable
@@ -809,7 +814,13 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
809814
handleChatReset()
810815
break
811816
case "sendMessage":
812-
handleSendMessage(message.text ?? "", message.images ?? [])
817+
// Check if this message matches any queued message to determine if it's from the queue
818+
const isFromQueue = messageQueue.some(
819+
(queuedMsg) =>
820+
queuedMsg.text === message.text &&
821+
JSON.stringify(queuedMsg.images || []) === JSON.stringify(message.images || []),
822+
)
823+
handleSendMessage(message.text ?? "", message.images ?? [], isFromQueue)
813824
break
814825
case "setChatBoxMessage":
815826
handleSetChatBoxMessage(message.text ?? "", message.images ?? [])
@@ -846,6 +857,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
846857
handleSetChatBoxMessage,
847858
handlePrimaryButtonClick,
848859
handleSecondaryButtonClick,
860+
messageQueue,
849861
],
850862
)
851863

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
import React from "react"
2+
import { render, waitFor } from "@testing-library/react"
3+
import userEvent from "@testing-library/user-event"
4+
import { vi } from "vitest"
5+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
6+
import "@testing-library/jest-dom"
7+
8+
// Mock dependencies before importing components
9+
vi.mock("@src/utils/vscode", () => ({
10+
vscode: {
11+
postMessage: vi.fn(),
12+
},
13+
}))
14+
15+
vi.mock("use-sound", () => ({
16+
default: () => [vi.fn()],
17+
}))
18+
19+
// Mock the extension state hook
20+
vi.mock("@src/context/ExtensionStateContext", async () => {
21+
const actual = await vi.importActual("@src/context/ExtensionStateContext")
22+
return {
23+
...actual,
24+
useExtensionState: vi.fn(() => ({
25+
clineMessages: [],
26+
taskHistory: [],
27+
apiConfiguration: { apiProvider: "test" },
28+
messageQueue: [],
29+
mode: "code",
30+
customModes: [],
31+
setMode: vi.fn(),
32+
})),
33+
}
34+
})
35+
36+
// Now import components after all mocks are set up
37+
import ChatView from "../ChatView"
38+
import { ExtensionStateContextProvider, useExtensionState } from "@src/context/ExtensionStateContext"
39+
import { vscode } from "@src/utils/vscode"
40+
41+
// Set up global mock
42+
;(global as any).acquireVsCodeApi = () => ({
43+
postMessage: vi.fn(),
44+
getState: () => ({}),
45+
setState: vi.fn(),
46+
})
47+
48+
const queryClient = new QueryClient({
49+
defaultOptions: {
50+
queries: { retry: false },
51+
mutations: { retry: false },
52+
},
53+
})
54+
55+
const renderChatView = () => {
56+
return render(
57+
<ExtensionStateContextProvider>
58+
<QueryClientProvider client={queryClient}>
59+
<ChatView isHidden={false} showAnnouncement={false} hideAnnouncement={vi.fn()} />
60+
</QueryClientProvider>
61+
</ExtensionStateContextProvider>,
62+
)
63+
}
64+
65+
describe("ChatView - Queued Messages", () => {
66+
beforeEach(() => {
67+
vi.clearAllMocks()
68+
})
69+
70+
it("should preserve input text when processing queued messages", async () => {
71+
// Mock the state with a queued message
72+
const mockUseExtensionState = useExtensionState as any
73+
mockUseExtensionState.mockReturnValue({
74+
clineMessages: [],
75+
taskHistory: [],
76+
apiConfiguration: { apiProvider: "test" },
77+
messageQueue: [
78+
{
79+
id: "queue-1",
80+
text: "Queued message",
81+
images: [],
82+
timestamp: Date.now(),
83+
},
84+
],
85+
mode: "code",
86+
customModes: [],
87+
setMode: vi.fn(),
88+
})
89+
90+
const { container } = renderChatView()
91+
92+
// Find the textarea
93+
const textarea = container.querySelector("textarea") as HTMLTextAreaElement
94+
expect(textarea).toBeTruthy()
95+
96+
// User types new text while message is queued
97+
await userEvent.type(textarea, "New text typed by user")
98+
expect(textarea.value).toBe("New text typed by user")
99+
100+
// Simulate backend processing the queued message by sending invoke message
101+
const invokeMessage = new MessageEvent("message", {
102+
data: {
103+
type: "invoke",
104+
invoke: "sendMessage",
105+
text: "Queued message",
106+
images: [],
107+
},
108+
})
109+
window.dispatchEvent(invokeMessage)
110+
111+
// Wait for any async operations
112+
await waitFor(() => {
113+
// The input should still contain the user's typed text
114+
expect(textarea.value).toBe("New text typed by user")
115+
})
116+
117+
// Verify the queued message was sent
118+
expect(vscode.postMessage).toHaveBeenCalledWith(
119+
expect.objectContaining({
120+
type: expect.stringMatching(/newTask|askResponse/),
121+
}),
122+
)
123+
})
124+
125+
it("should clear input when sending a regular message (not from queue)", async () => {
126+
// Mock the state with no queued messages
127+
const mockUseExtensionState = useExtensionState as any
128+
mockUseExtensionState.mockReturnValue({
129+
clineMessages: [],
130+
taskHistory: [],
131+
apiConfiguration: { apiProvider: "test" },
132+
messageQueue: [], // No queued messages
133+
mode: "code",
134+
customModes: [],
135+
setMode: vi.fn(),
136+
})
137+
138+
const { container } = renderChatView()
139+
140+
// Find the textarea
141+
const textarea = container.querySelector("textarea") as HTMLTextAreaElement
142+
expect(textarea).toBeTruthy()
143+
144+
// User types text
145+
await userEvent.type(textarea, "Regular message")
146+
expect(textarea.value).toBe("Regular message")
147+
148+
// Simulate backend sending invoke message for a non-queued message
149+
const invokeMessage = new MessageEvent("message", {
150+
data: {
151+
type: "invoke",
152+
invoke: "sendMessage",
153+
text: "Different message not in queue",
154+
images: [],
155+
},
156+
})
157+
window.dispatchEvent(invokeMessage)
158+
159+
// Wait for any async operations
160+
await waitFor(() => {
161+
// The input should be cleared since this is not a queued message
162+
expect(textarea.value).toBe("")
163+
})
164+
})
165+
166+
it("should handle messages with images correctly", async () => {
167+
// Mock the state with a queued message with image
168+
const mockUseExtensionState = useExtensionState as any
169+
mockUseExtensionState.mockReturnValue({
170+
clineMessages: [],
171+
taskHistory: [],
172+
apiConfiguration: { apiProvider: "test" },
173+
messageQueue: [
174+
{
175+
id: "queue-2",
176+
text: "Message with image",
177+
images: ["data:image/png;base64,abc123"],
178+
timestamp: Date.now(),
179+
},
180+
],
181+
mode: "code",
182+
customModes: [],
183+
setMode: vi.fn(),
184+
})
185+
186+
const { container } = renderChatView()
187+
188+
// Find the textarea
189+
const textarea = container.querySelector("textarea") as HTMLTextAreaElement
190+
expect(textarea).toBeTruthy()
191+
192+
// User types new text
193+
await userEvent.type(textarea, "User typing while image message queued")
194+
expect(textarea.value).toBe("User typing while image message queued")
195+
196+
// Simulate backend processing the queued message with image
197+
const invokeMessage = new MessageEvent("message", {
198+
data: {
199+
type: "invoke",
200+
invoke: "sendMessage",
201+
text: "Message with image",
202+
images: ["data:image/png;base64,abc123"],
203+
},
204+
})
205+
window.dispatchEvent(invokeMessage)
206+
207+
// Wait for any async operations
208+
await waitFor(() => {
209+
// The input should still contain the user's typed text
210+
expect(textarea.value).toBe("User typing while image message queued")
211+
})
212+
})
213+
})

0 commit comments

Comments
 (0)