From 39a47a42e75eda70f3799d6b2cb3af4acb2194a2 Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Sun, 5 Jul 2026 00:43:47 -0700 Subject: [PATCH] Clear PR comment selection on AI launch and defer mismatching clears - Clear sent comments from selection queue when confirming AI resolution. - Keep selection clear requests pending until the matching review context is mounted, preventing them from being ignored on context changes. - Fix calculation of skipped threads when comment resolution is launched. --- .../components/right-sidebar/ChecksPanel.tsx | 29 ++++++++- .../right-sidebar/checks-panel-content.tsx | 9 ++- .../pr-comments-list-selection.test.tsx | 62 ++++++++++++++++++- .../pr-comments-list-selection.ts | 11 ++-- 4 files changed, 99 insertions(+), 12 deletions(-) diff --git a/src/renderer/src/components/right-sidebar/ChecksPanel.tsx b/src/renderer/src/components/right-sidebar/ChecksPanel.tsx index 5ccc1b9f4ec..0682c1b0790 100644 --- a/src/renderer/src/components/right-sidebar/ChecksPanel.tsx +++ b/src/renderer/src/components/right-sidebar/ChecksPanel.tsx @@ -49,6 +49,10 @@ import { PRCommentsList, PRTriageStrip } from './checks-panel-content' +import { + clearPRCommentsListSelection, + type PRCommentsListSelectionClearRequest +} from './pr-comments-list-selection' import { ENTRY_REFRESH_GRACE_MS, shouldEntryRefresh } from './checks-entry-refresh' import type { GitLabDiscussionResolveResult, @@ -435,6 +439,9 @@ export default function ChecksPanel(): React.JSX.Element { const [comments, setComments] = useState([]) const [commentsLoading, setCommentsLoading] = useState(false) const commentsRef = useRef([]) + const [commentsSelectionClearRequest, setCommentsSelectionClearRequest] = + useState(null) + const commentsSelectionClearTokenRef = useRef(0) const [emptyRefreshing, setEmptyRefreshing] = useState(false) const [isRefreshing, setIsRefreshing] = useState(false) const refreshInFlightRef = useRef(false) @@ -2789,6 +2796,15 @@ export default function ChecksPanel(): React.JSX.Element { ] ) + const clearSentCommentSelection = useCallback((reviewContextKey: string): void => { + clearPRCommentsListSelection(reviewContextKey) + commentsSelectionClearTokenRef.current += 1 + setCommentsSelectionClearRequest({ + contextKey: reviewContextKey, + token: commentsSelectionClearTokenRef.current + }) + }, []) + const refreshCommentsAfterBulkResolve = useCallback( async (provider: ChecksPanelReview['provider']): Promise => { if (provider === 'gitlab') { @@ -2802,9 +2818,14 @@ export default function ChecksPanel(): React.JSX.Element { const resolveSelectedThreadsAfterLaunch = useCallback( async (resolution: NonNullable) => { + clearSentCommentSelection(resolution.reviewContextKey) let resolved = 0 - let skipped = 0 + let skipped = Math.max( + 0, + resolution.selectedGroups.length - resolution.selectedThreadIds.length + ) let failed = 0 + let attemptedThreadCount = 0 if (resolution.selectedThreadIds.length === 0) { toast.success( translate( @@ -2816,9 +2837,10 @@ export default function ChecksPanel(): React.JSX.Element { } for (const threadId of resolution.selectedThreadIds) { if (asyncResultKeyRef.current !== resolution.reviewContextKey) { - skipped += resolution.selectedThreadIds.length - resolved - skipped - failed + skipped += resolution.selectedThreadIds.length - attemptedThreadCount break } + attemptedThreadCount += 1 const currentGroup = groupPRComments(commentsRef.current).find( (group) => group.kind === 'thread' && group.threadId === threadId ) @@ -2856,7 +2878,7 @@ export default function ChecksPanel(): React.JSX.Element { ) ) }, - [handleResolve, refreshCommentsAfterBulkResolve] + [clearSentCommentSelection, handleResolve, refreshCommentsAfterBulkResolve] ) const handleFixChecksWithAI = useCallback(async (): Promise => { @@ -3780,6 +3802,7 @@ export default function ChecksPanel(): React.JSX.Element { commentsDisabled={!canTargetPRComments} commentsDisabledReason={commentsDisabledReason} selectionContextKey={stateRequestKey} + selectionClearRequest={commentsSelectionClearRequest} resolveCommentsWithAIDisabled={Boolean(resolveCommentsWithAIDisabledReason)} resolveCommentsWithAIDisabledReason={resolveCommentsWithAIDisabledReason} onAddComment={pr ? handleAddPRComment : undefined} diff --git a/src/renderer/src/components/right-sidebar/checks-panel-content.tsx b/src/renderer/src/components/right-sidebar/checks-panel-content.tsx index 30a78f5391b..43ca52db9ea 100644 --- a/src/renderer/src/components/right-sidebar/checks-panel-content.tsx +++ b/src/renderer/src/components/right-sidebar/checks-panel-content.tsx @@ -94,7 +94,10 @@ import { RightPanelCommentComposer, type RightPanelCommentSubmitResult } from './right-panel-comment-composer' -import { usePRCommentsListSelection } from './pr-comments-list-selection' +import { + usePRCommentsListSelection, + type PRCommentsListSelectionClearRequest +} from './pr-comments-list-selection' import { translate } from '@/i18n/i18n' import { useActiveWorktree } from '@/store/selectors' import { useAppStore } from '@/store' @@ -2171,6 +2174,7 @@ export function PRCommentsList({ commentsDisabled, commentsDisabledReason, selectionContextKey, + selectionClearRequest, resolveCommentsWithAIDisabled, resolveCommentsWithAIDisabledReason, onAddComment, @@ -2186,6 +2190,7 @@ export function PRCommentsList({ commentsDisabled?: boolean commentsDisabledReason?: string selectionContextKey?: string + selectionClearRequest?: PRCommentsListSelectionClearRequest | null resolveCommentsWithAIDisabled?: boolean resolveCommentsWithAIDisabledReason?: string onAddComment?: (body: string) => Promise @@ -2212,7 +2217,7 @@ export function PRCommentsList({ addGroupToSelection, clearSelection, toggleGroupSelection - } = usePRCommentsListSelection(comments, selectionContextKey) + } = usePRCommentsListSelection(comments, selectionContextKey, selectionClearRequest) const visibleComments = React.useMemo( () => filterPRCommentsByAudience(comments, commentFilter), [commentFilter, comments] diff --git a/src/renderer/src/components/right-sidebar/pr-comments-list-selection.test.tsx b/src/renderer/src/components/right-sidebar/pr-comments-list-selection.test.tsx index 69d07fff0a2..61924494157 100644 --- a/src/renderer/src/components/right-sidebar/pr-comments-list-selection.test.tsx +++ b/src/renderer/src/components/right-sidebar/pr-comments-list-selection.test.tsx @@ -45,7 +45,10 @@ vi.mock('@/components/ui/dropdown-menu', () => ({ })) import type { PRComment } from '../../../../shared/types' import type { PRCommentGroup } from '@/lib/pr-comment-groups' -import { clearPRCommentsListSelection } from './pr-comments-list-selection' +import { + clearPRCommentsListSelection, + type PRCommentsListSelectionClearRequest +} from './pr-comments-list-selection' import { PRCommentsList } from './checks-panel-content' let container: HTMLDivElement @@ -79,7 +82,9 @@ function comment(overrides: Partial): PRComment { function renderList(props: { comments: PRComment[] + contextKey?: string onResolveSelectedCommentsWithAI?: (groups: PRCommentGroup[]) => void + clearRequest?: PRCommentsListSelectionClearRequest | null }): void { act(() => { root.render( @@ -87,7 +92,8 @@ function renderList(props: { @@ -297,6 +303,58 @@ describe('PRCommentsList comment resolution selection', () => { expect(container.querySelector('button[role="checkbox"]')).toBeNull() }) + it('clears sent standalone bot comments from the queue when the parent confirms launch', () => { + const comments = [ + comment({ + id: 1, + author: 'coderabbitai', + body: 'Review Change Stack. No actionable comments were generated.', + isBot: true + }) + ] + renderList({ comments }) + clickButton('Queue for agent') + + expect(hasButton('Send 1 queued comments to AI')).toBe(true) + + renderList({ + comments, + clearRequest: { contextKey: 'review:42', token: 1 } + }) + + expect(hasButton('Send 1 queued comments to AI')).toBe(false) + expect(container.querySelector('button[role="checkbox"]')).toBeNull() + }) + + it('keeps a clear request pending until its review context is mounted', () => { + const queuedComments = [ + comment({ + id: 1, + author: 'coderabbitai', + body: 'Review Change Stack. No actionable comments were generated.', + isBot: true + }) + ] + renderList({ comments: queuedComments, contextKey: 'review:42' }) + clickButton('Queue for agent') + + expect(hasButton('Send 1 queued comments to AI')).toBe(true) + + renderList({ + comments: [comment({ id: 2, body: 'Other review comment.' })], + contextKey: 'review:99', + clearRequest: { contextKey: 'review:42', token: 1 } + }) + renderList({ + comments: queuedComments, + contextKey: 'review:42', + clearRequest: { contextKey: 'review:42', token: 1 } + }) + + expect(hasButton('Send 1 queued comments to AI')).toBe(false) + expect(container.querySelector('button[role="checkbox"]')).toBeNull() + }) + it('exits selection mode when refresh leaves no eligible loaded threads', () => { renderList({ comments: [comment({ id: 1, threadId: 'thread-1', path: 'src/a.ts', isResolved: false })] diff --git a/src/renderer/src/components/right-sidebar/pr-comments-list-selection.ts b/src/renderer/src/components/right-sidebar/pr-comments-list-selection.ts index 01bfeddf103..71b51ad5b3f 100644 --- a/src/renderer/src/components/right-sidebar/pr-comments-list-selection.ts +++ b/src/renderer/src/components/right-sidebar/pr-comments-list-selection.ts @@ -65,7 +65,7 @@ export function usePRCommentsListSelection( selectionContextKey: string | undefined, clearRequest?: PRCommentsListSelectionClearRequest | null ): PRCommentsListSelection { - const lastClearRequestTokenRef = useRef(clearRequest?.token ?? null) + const lastClearRequestTokenRef = useRef(null) const [selectionState, setSelectionState] = useState(() => createSelectionState(selectionContextKey) ) @@ -77,13 +77,14 @@ export function usePRCommentsListSelection( }, [selectionContextKey]) useEffect(() => { - if (!clearRequest || clearRequest.token === lastClearRequestTokenRef.current) { + if ( + !clearRequest || + clearRequest.contextKey !== selectionContextKey || + clearRequest.token === lastClearRequestTokenRef.current + ) { return } lastClearRequestTokenRef.current = clearRequest.token - if (clearRequest.contextKey !== selectionContextKey) { - return - } const next = { contextKey: selectionContextKey, isSelectingForAI: false,