Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions webview-ui/src/components/chat/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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={
Expand Down Expand Up @@ -1489,6 +1496,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
isStreaming,
handleSuggestionClickInRow,
handleBatchFileResponse,
handleFollowUpUnmount,
currentFollowUpTs,
isFollowUpAutoApprovalPaused,
enableButtons,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
})
})
})
Loading