From 582793eba0fba90d9fdb1a29f793d2395a4488a2 Mon Sep 17 00:00:00 2001 From: Mohamed Aziz Mejri Date: Tue, 30 Sep 2025 00:19:49 +0100 Subject: [PATCH] Disable send button while approval is pending (#1368) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #912 This PR implements disabling send button while approval is pending and addresses issue #912 --- ## Summary by cubic Disable the chat send button while a proposal is awaiting approval, and re-enable it after approve or reject. Prevents accidental messages during pending changes. Addresses issue #912. - **New Features** - Track pending changes with isChangesPending based on the last assistant message’s approvalState. - Disable the send button when a proposal is pending (in addition to the existing empty input check). - Re-enable after approve/reject by refreshing the proposal and messages. - Added Playwright e2e tests for both approve and reject flows. --- > [!NOTE] > Disable the chat send button when a code proposal is pending approval and re-enable after approve/reject; add e2e coverage and update MCP flow. > > - **Frontend** > - `ChatInput.tsx`: Read `messages` from `chatMessagesAtom` and derive `disableSendButton` when the last assistant message (matching `proposal.messageId`) has no `approvalState` and `proposal.type === "code-proposal"`. > - Apply `disableSendButton` to the send button’s `disabled` condition (in addition to empty input/attachments). > - Ensure proposal/messages refresh after approve/reject. > - **Tests** > - Add Playwright tests `e2e-tests/chat_input.spec.ts` to verify send button disabled during pending proposal and re-enabled after approve or reject. > - Update `e2e-tests/mcp.spec.ts` to click "Approve" after granting consent. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit b9b47bd6f547449cc5cf1d39a00e4e7fb5de1bcd. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --------- Co-authored-by: Will Chen --- e2e-tests/chat_input.spec.ts | 50 +++++++++++++++++++++++++++++++ e2e-tests/mcp.spec.ts | 1 + src/components/chat/ChatInput.tsx | 16 ++++++++-- 3 files changed, 64 insertions(+), 3 deletions(-) create mode 100644 e2e-tests/chat_input.spec.ts diff --git a/e2e-tests/chat_input.spec.ts b/e2e-tests/chat_input.spec.ts new file mode 100644 index 0000000..3551812 --- /dev/null +++ b/e2e-tests/chat_input.spec.ts @@ -0,0 +1,50 @@ +import { test } from "./helpers/test_helper"; +import { expect } from "@playwright/test"; + +test("send button disabled during pending proposal", async ({ po }) => { + await po.setUp(); + + // Send a prompt that generates a proposal + await po.sendPrompt("Create a simple React component"); + + // Wait for proposal buttons to appear (ensuring proposal is rendered) + await expect(po.page.getByTestId("approve-proposal-button")).toBeVisible(); + + // Type something in the input to ensure it's not disabled due to empty input + await po.getChatInput().fill("test message"); + + // Check send button is disabled due to pending changes + const sendButton = po.page.getByRole("button", { name: "Send message" }); + await expect(sendButton).toBeDisabled(); + + // Approve the proposal + await po.approveProposal(); + + // Check send button is enabled again + await expect(sendButton).toBeEnabled(); +}); + +test("send button disabled during pending proposal - reject", async ({ + po, +}) => { + await po.setUp(); + + // Send a prompt that generates a proposal + await po.sendPrompt("Create a simple React component"); + + // Wait for proposal buttons to appear (ensuring proposal is rendered) + await expect(po.page.getByTestId("reject-proposal-button")).toBeVisible(); + + // Type something in the input to ensure it's not disabled due to empty input + await po.getChatInput().fill("test message"); + + // Check send button is disabled due to pending changes + const sendButton = po.page.getByRole("button", { name: "Send message" }); + await expect(sendButton).toBeDisabled(); + + // Reject the proposal + await po.rejectProposal(); + + // Check send button is enabled again + await expect(sendButton).toBeEnabled(); +}); diff --git a/e2e-tests/mcp.spec.ts b/e2e-tests/mcp.spec.ts index 3c2a72c..76499fa 100644 --- a/e2e-tests/mcp.spec.ts +++ b/e2e-tests/mcp.spec.ts @@ -43,6 +43,7 @@ test("mcp - call calculator", async ({ po }) => { // Make sure the tool call doesn't execute until consent is given await po.snapshotMessages(); await alwaysAllowButton.click(); + await po.page.getByRole("button", { name: "Approve" }).click(); await po.sendPrompt("[dump]"); await po.snapshotServerDump("all-messages"); diff --git a/src/components/chat/ChatInput.tsx b/src/components/chat/ChatInput.tsx index 7dafef8..8f1e0ba 100644 --- a/src/components/chat/ChatInput.tsx +++ b/src/components/chat/ChatInput.tsx @@ -79,7 +79,7 @@ export function ChatInput({ chatId }: { chatId?: number }) { const [showError, setShowError] = useState(true); const [isApproving, setIsApproving] = useState(false); // State for approving const [isRejecting, setIsRejecting] = useState(false); // State for rejecting - const [, setMessages] = useAtom(chatMessagesAtom); + const [messages, setMessages] = useAtom(chatMessagesAtom); const setIsPreviewOpen = useSetAtom(isPreviewOpenAtom); const [showTokenBar, setShowTokenBar] = useAtom(showTokenBarAtom); const [selectedComponent, setSelectedComponent] = useAtom( @@ -108,6 +108,14 @@ export function ChatInput({ chatId }: { chatId?: number }) { } = useProposal(chatId); const { proposal, messageId } = proposalResult ?? {}; + const lastMessage = messages.at(-1); + const disableSendButton = + lastMessage?.role === "assistant" && + !lastMessage.approvalState && + !!proposal && + proposal.type === "code-proposal" && + messageId === lastMessage.id; + useEffect(() => { if (error) { setShowError(true); @@ -214,7 +222,6 @@ export function ChatInput({ chatId }: { chatId?: number }) { setError((err as Error)?.message || "An error occurred while rejecting"); } finally { setIsRejecting(false); - // Keep same as handleApprove refreshProposal(); fetchChatMessages(); @@ -307,7 +314,10 @@ export function ChatInput({ chatId }: { chatId?: number }) { ) : (