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

Co-authored-by: Sannidhya <sann@Sannidhyas-MacBook-Pro.local>
This commit is contained in:
SannidhyaSah 2026-02-12 21:08:11 +05:30 • committed by daniel-lxs
parent 67e568f6bb
commit 2130a83024
No known key found for this signature in database
GPG key ID: 21C74479048B3AA6
2 changed files with 106 additions and 0 deletions

View file

@ -1297,6 +1297,12 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
vscode.postMessage({ type: "askResponse", askResponse: "objectResponse", text: JSON.stringify(response) }) 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( const itemContent = useCallback(
(index: number, messageOrGroup: ClineMessage) => { (index: number, messageOrGroup: ClineMessage) => {
const hasCheckpoint = modifiedMessages.some((message) => message.say === "checkpoint_saved") const hasCheckpoint = modifiedMessages.some((message) => message.say === "checkpoint_saved")
@ -1342,6 +1348,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
isStreaming={isStreaming} isStreaming={isStreaming}
onSuggestionClick={handleSuggestionClickInRow} // This was already stabilized onSuggestionClick={handleSuggestionClickInRow} // This was already stabilized
onBatchFileResponse={handleBatchFileResponse} onBatchFileResponse={handleBatchFileResponse}
onFollowUpUnmount={handleFollowUpUnmount}
isFollowUpAnswered={messageOrGroup.isAnswered === true || messageOrGroup.ts === currentFollowUpTs} isFollowUpAnswered={messageOrGroup.isAnswered === true || messageOrGroup.ts === currentFollowUpTs}
isFollowUpAutoApprovalPaused={isFollowUpAutoApprovalPaused} isFollowUpAutoApprovalPaused={isFollowUpAutoApprovalPaused}
editable={ editable={
@ -1372,6 +1379,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
isStreaming, isStreaming,
handleSuggestionClickInRow, handleSuggestionClickInRow,
handleBatchFileResponse, handleBatchFileResponse,
handleFollowUpUnmount,
currentFollowUpTs, currentFollowUpTs,
isFollowUpAutoApprovalPaused, isFollowUpAutoApprovalPaused,
enableButtons, enableButtons,

View file

@ -592,4 +592,102 @@ describe("FollowUpSuggest", () => {
expect(screen.getByText(/3s/)).toBeInTheDocument() 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()
})
})
}) })