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.
This commit is contained in:
Jinjing
2026-07-05 00:43:47 -07:00
parent 6677de1807
commit 39a47a42e7
4 changed files with 99 additions and 12 deletions
@@ -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<PRComment[]>([])
const [commentsLoading, setCommentsLoading] = useState(false)
const commentsRef = useRef<PRComment[]>([])
const [commentsSelectionClearRequest, setCommentsSelectionClearRequest] =
useState<PRCommentsListSelectionClearRequest | null>(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<void> => {
if (provider === 'gitlab') {
@@ -2802,9 +2818,14 @@ export default function ChecksPanel(): React.JSX.Element {
const resolveSelectedThreadsAfterLaunch = useCallback(
async (resolution: NonNullable<ChecksAgentComposerState['commentResolution']>) => {
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<void> => {
@@ -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}
@@ -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<RightPanelCommentSubmitResult>
@@ -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]
@@ -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>): 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: {
<PRCommentsList
comments={props.comments}
commentsLoading={false}
selectionContextKey="review:42"
selectionContextKey={props.contextKey ?? 'review:42'}
selectionClearRequest={props.clearRequest}
onResolveSelectedCommentsWithAI={props.onResolveSelectedCommentsWithAI ?? vi.fn()}
/>
</TooltipProvider>
@@ -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 })]
@@ -65,7 +65,7 @@ export function usePRCommentsListSelection(
selectionContextKey: string | undefined,
clearRequest?: PRCommentsListSelectionClearRequest | null
): PRCommentsListSelection {
const lastClearRequestTokenRef = useRef<number | null>(clearRequest?.token ?? null)
const lastClearRequestTokenRef = useRef<number | null>(null)
const [selectionState, setSelectionState] = useState<PRCommentsListSelectionState>(() =>
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,