fix: ensure user messages are sent with save button for file editing operations

- Modified handlePrimaryButtonClick in ChatView to properly handle user input during file editing operations
- For tool operations (file editing), the function now checks for current input from both the text parameter and inputValue state
- This ensures that user messages typed while the agent is working are sent along with the save action
- Added comprehensive tests for QueuedMessages component functionality
- All existing tests continue to pass

Fixes #6479
This commit is contained in:
Roo Code 2025-07-31 10:32:59 +00:00
parent e13083e532
commit 2640abc186
2 changed files with 203 additions and 10 deletions

View file

@ -723,19 +723,42 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
case "use_mcp_server":
case "resume_task":
case "mistake_limit_reached":
// Only send text/images if they exist
if (trimmedInput || (images && images.length > 0)) {
vscode.postMessage({
type: "askResponse",
askResponse: "yesButtonClicked",
text: trimmedInput,
images: images,
})
// For tool operations (like file editing), check if we have current input or queued messages
if (clineAsk === "tool") {
// Get current input from the text area (this includes any text typed while agent was working)
const currentInput = text?.trim() || inputValue.trim()
const currentImages = images || selectedImages
// Send the save action with any current input
if (currentInput || (currentImages && currentImages.length > 0)) {
vscode.postMessage({
type: "askResponse",
askResponse: "yesButtonClicked",
text: currentInput,
images: currentImages,
})
} else {
vscode.postMessage({ type: "askResponse", askResponse: "yesButtonClicked" })
}
// Clear input state after sending
setInputValue("")
setSelectedImages([])
} else {
vscode.postMessage({ type: "askResponse", askResponse: "yesButtonClicked" })
// For other operations, use the original logic
if (trimmedInput || (images && images.length > 0)) {
vscode.postMessage({
type: "askResponse",
askResponse: "yesButtonClicked",
text: trimmedInput,
images: images,
})
// Clear input state after sending
setInputValue("")
setSelectedImages([])
} else {
vscode.postMessage({ type: "askResponse", askResponse: "yesButtonClicked" })
}
}
break
case "completion_result":
@ -752,7 +775,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
setClineAsk(undefined)
setEnableButtons(false)
},
[clineAsk, startNewTask],
[clineAsk, startNewTask, inputValue, selectedImages, setInputValue, setSelectedImages],
)
const handleSecondaryButtonClick = useCallback(

View file

@ -0,0 +1,170 @@
import React from "react"
import { render, screen, fireEvent } from "@testing-library/react"
import { vi } from "vitest"
import QueuedMessages from "../QueuedMessages"
import { QueuedMessage } from "@roo-code/types"
// Mock react-i18next
vi.mock("react-i18next", () => ({
useTranslation: () => ({
t: (key: string) => key,
}),
}))
// Mock the Mention component
vi.mock("../Mention", () => ({
Mention: ({ text }: { text: string }) => <span data-testid="mention">{text}</span>,
}))
// Mock the Thumbnails component
vi.mock("../common/Thumbnails", () => ({
default: ({ images }: { images: string[] }) => <div data-testid="thumbnails">{images.length} images</div>,
}))
// Mock the Button component
vi.mock("@src/components/ui", () => ({
Button: ({ children, onClick, ...props }: any) => (
<button onClick={onClick} {...props}>
{children}
</button>
),
}))
describe("QueuedMessages", () => {
const mockOnRemove = vi.fn()
const mockOnUpdate = vi.fn()
beforeEach(() => {
vi.clearAllMocks()
})
it("renders nothing when queue is empty", () => {
const { container } = render(<QueuedMessages queue={[]} onRemove={mockOnRemove} onUpdate={mockOnUpdate} />)
expect(container.firstChild).toBeNull()
})
it("renders queued messages", () => {
const queue: QueuedMessage[] = [
{
id: "1",
text: "Test message 1",
images: [],
},
{
id: "2",
text: "Test message 2",
images: ["image1.png"],
},
]
render(<QueuedMessages queue={queue} onRemove={mockOnRemove} onUpdate={mockOnUpdate} />)
expect(screen.getByTestId("queued-messages")).toBeInTheDocument()
expect(screen.getByText("queuedMessages.title")).toBeInTheDocument()
expect(screen.getAllByTestId("mention")).toHaveLength(2)
})
it("calls onRemove when delete button is clicked", () => {
const queue: QueuedMessage[] = [
{
id: "1",
text: "Test message",
images: [],
},
]
render(<QueuedMessages queue={queue} onRemove={mockOnRemove} onUpdate={mockOnUpdate} />)
const deleteButton = screen.getByRole("button")
fireEvent.click(deleteButton)
expect(mockOnRemove).toHaveBeenCalledWith(0)
})
it("enters edit mode when message is clicked", () => {
const queue: QueuedMessage[] = [
{
id: "1",
text: "Test message",
images: [],
},
]
render(<QueuedMessages queue={queue} onRemove={mockOnRemove} onUpdate={mockOnUpdate} />)
const messageElement = screen.getByTestId("mention").parentElement
fireEvent.click(messageElement!)
expect(screen.getByRole("textbox")).toBeInTheDocument()
})
it("calls onUpdate when edit is saved", () => {
const queue: QueuedMessage[] = [
{
id: "1",
text: "Test message",
images: [],
},
]
render(<QueuedMessages queue={queue} onRemove={mockOnRemove} onUpdate={mockOnUpdate} />)
// Enter edit mode
const messageElement = screen.getByTestId("mention").parentElement
fireEvent.click(messageElement!)
// Edit the text
const textarea = screen.getByRole("textbox")
fireEvent.change(textarea, { target: { value: "Updated message" } })
// Save by pressing Enter
fireEvent.keyDown(textarea, { key: "Enter" })
expect(mockOnUpdate).toHaveBeenCalledWith(0, "Updated message")
})
it("cancels edit when Escape is pressed", () => {
const queue: QueuedMessage[] = [
{
id: "1",
text: "Test message",
images: [],
},
]
render(<QueuedMessages queue={queue} onRemove={mockOnRemove} onUpdate={mockOnUpdate} />)
// Enter edit mode
const messageElement = screen.getByTestId("mention").parentElement
fireEvent.click(messageElement!)
// Edit the text
const textarea = screen.getByRole("textbox")
fireEvent.change(textarea, { target: { value: "Updated message" } })
// Cancel by pressing Escape
fireEvent.keyDown(textarea, { key: "Escape" })
// Should not call onUpdate and should exit edit mode
expect(mockOnUpdate).not.toHaveBeenCalled()
expect(screen.queryByRole("textbox")).not.toBeInTheDocument()
})
it("renders thumbnails for messages with images", () => {
const queue: QueuedMessage[] = [
{
id: "1",
text: "Message with images",
images: ["image1.png", "image2.png"],
},
]
render(<QueuedMessages queue={queue} onRemove={mockOnRemove} onUpdate={mockOnUpdate} />)
// Check that images are rendered (the actual Thumbnails component renders img elements)
const images = screen.getAllByRole("img")
expect(images).toHaveLength(2)
expect(images[0]).toHaveAttribute("src", "image1.png")
expect(images[1]).toHaveAttribute("src", "image2.png")
})
})