From f8ca4d32f4e0cedc10806b133f618cae1baa760b Mon Sep 17 00:00:00 2001 From: Sannidhya Date: Thu, 12 Feb 2026 18:01:09 +0530 Subject: [PATCH] fix: cancel backend auto-approval timeout when auto-approve is toggled off mid-countdown When a user disables auto-approve while a followup countdown timer is running, only the visual timer was removed but the backend setTimeout in Task.ts continued running and eventually auto-submitted the answer. Root cause: ChatView.tsx was not passing the onFollowUpUnmount callback to ChatRow, so FollowUpSuggest's cleanup function could not notify the backend to cancel its pending timeout. Fix: Add handleFollowUpUnmount callback in ChatView that sends cancelAutoApproval message to the backend, and pass it as onFollowUpUnmount prop to ChatRow. Now when FollowUpSuggest's useEffect cleanup fires (due to autoApprovalEnabled changing to false), it calls onCancelAutoApproval -> handleFollowUpUnmount -> cancelAutoApproval message -> task.cancelAutoApprovalTimeout() clearing the backend timer. --- webview-ui/src/components/chat/ChatView.tsx | 8 ++ .../chat/__tests__/FollowUpSuggest.spec.tsx | 98 +++++++++++++++++++ 2 files changed, 106 insertions(+) diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index 6d82071512f..fbd7db07436 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -1442,6 +1442,12 @@ const ChatViewComponent: React.ForwardRefRenderFunction { + vscode.postMessage({ type: "cancelAutoApproval" }) + }, []) + const itemContent = useCallback( (index: number, messageOrGroup: ClineMessage) => { const hasCheckpoint = modifiedMessages.some((message) => message.say === "checkpoint_saved") @@ -1459,6 +1465,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction { expect(screen.getByText(/3s/)).toBeInTheDocument() }) }) + + describe("auto-approve toggle off mid-countdown", () => { + it("should call onCancelAutoApproval when autoApprovalEnabled changes to false during countdown", async () => { + const { rerender } = renderWithTestProviders( + , + defaultTestState, + ) + + // Should show countdown initially + expect(screen.getByText(/3s/)).toBeInTheDocument() + + // Advance timer partially + await act(async () => { + vi.advanceTimersByTime(1000) + }) + + // Countdown should be at 2s + expect(screen.getByText(/2s/)).toBeInTheDocument() + + // Clear mock to track calls from the toggle-off + mockOnCancelAutoApproval.mockClear() + + // User toggles auto-approve off + rerender( + + + + + , + ) + + // Countdown should disappear + expect(screen.queryByText(/\d+s/)).not.toBeInTheDocument() + + // onCancelAutoApproval should have been called to cancel the backend timeout + expect(mockOnCancelAutoApproval).toHaveBeenCalled() + + // Advance timer past original timeout - nothing should happen + await act(async () => { + vi.advanceTimersByTime(5000) + }) + + // onSuggestionClick should NOT have been called + expect(mockOnSuggestionClick).not.toHaveBeenCalled() + }) + + it("should call onCancelAutoApproval when alwaysAllowFollowupQuestions changes to false during countdown", async () => { + const { rerender } = renderWithTestProviders( + , + defaultTestState, + ) + + // Should show countdown initially + expect(screen.getByText(/3s/)).toBeInTheDocument() + + // Clear mock to track calls from the toggle-off + mockOnCancelAutoApproval.mockClear() + + // User disables follow-up question auto-approval + rerender( + + + + + , + ) + + // Countdown should disappear + expect(screen.queryByText(/\d+s/)).not.toBeInTheDocument() + + // onCancelAutoApproval should have been called to cancel the backend timeout + expect(mockOnCancelAutoApproval).toHaveBeenCalled() + }) + }) })