Просмотр исходного кода

fix: cancel backend auto-approval timeout when auto-approve is toggled off mid-countdown (#11439)

Co-authored-by: Sannidhya <[email protected]>
SannidhyaSah 11 часов назад
Родитель
Сommit
6c9ff49dd8

+ 8 - 0
webview-ui/src/components/chat/ChatView.tsx

@@ -1442,6 +1442,12 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 		vscode.postMessage({ type: "askResponse", askResponse: "objectResponse", text: JSON.stringify(response) })
 	}, [])
 
+	// Cancel backend auto-approval timeout when FollowUpSuggest's countdown effect cleans up.
+	// This is called when auto-approve is toggled off, a suggestion is clicked, or the component unmounts.
+	const handleFollowUpUnmount = useCallback(() => {
+		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<ChatViewRef, ChatViewPro
 					isStreaming={isStreaming}
 					onSuggestionClick={handleSuggestionClickInRow} // This was already stabilized
 					onBatchFileResponse={handleBatchFileResponse}
+					onFollowUpUnmount={handleFollowUpUnmount}
 					isFollowUpAnswered={messageOrGroup.isAnswered === true || messageOrGroup.ts === currentFollowUpTs}
 					isFollowUpAutoApprovalPaused={isFollowUpAutoApprovalPaused}
 					editable={
@@ -1489,6 +1496,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 			isStreaming,
 			handleSuggestionClickInRow,
 			handleBatchFileResponse,
+			handleFollowUpUnmount,
 			currentFollowUpTs,
 			isFollowUpAutoApprovalPaused,
 			enableButtons,

+ 98 - 0
webview-ui/src/components/chat/__tests__/FollowUpSuggest.spec.tsx

@@ -592,4 +592,102 @@ describe("FollowUpSuggest", () => {
 			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(
+				<FollowUpSuggest
+					suggestions={mockSuggestions}
+					onSuggestionClick={mockOnSuggestionClick}
+					ts={123}
+					onCancelAutoApproval={mockOnCancelAutoApproval}
+					isAnswered={false}
+				/>,
+				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(
+				<TestExtensionStateProvider value={{ ...defaultTestState, autoApprovalEnabled: false }}>
+					<TooltipProvider>
+						<FollowUpSuggest
+							suggestions={mockSuggestions}
+							onSuggestionClick={mockOnSuggestionClick}
+							ts={123}
+							onCancelAutoApproval={mockOnCancelAutoApproval}
+							isAnswered={false}
+						/>
+					</TooltipProvider>
+				</TestExtensionStateProvider>,
+			)
+
+			// 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(
+				<FollowUpSuggest
+					suggestions={mockSuggestions}
+					onSuggestionClick={mockOnSuggestionClick}
+					ts={123}
+					onCancelAutoApproval={mockOnCancelAutoApproval}
+					isAnswered={false}
+				/>,
+				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(
+				<TestExtensionStateProvider value={{ ...defaultTestState, alwaysAllowFollowupQuestions: false }}>
+					<TooltipProvider>
+						<FollowUpSuggest
+							suggestions={mockSuggestions}
+							onSuggestionClick={mockOnSuggestionClick}
+							ts={123}
+							onCancelAutoApproval={mockOnCancelAutoApproval}
+							isAnswered={false}
+						/>
+					</TooltipProvider>
+				</TestExtensionStateProvider>,
+			)
+
+			// Countdown should disappear
+			expect(screen.queryByText(/\d+s/)).not.toBeInTheDocument()
+
+			// onCancelAutoApproval should have been called to cancel the backend timeout
+			expect(mockOnCancelAutoApproval).toHaveBeenCalled()
+		})
+	})
 })