diff --git a/src/renderer/src/components/right-sidebar/checks-panel-content.test.tsx b/src/renderer/src/components/right-sidebar/checks-panel-content.test.tsx index 307c20ecf76..5ad62f79f25 100644 --- a/src/renderer/src/components/right-sidebar/checks-panel-content.test.tsx +++ b/src/renderer/src/components/right-sidebar/checks-panel-content.test.tsx @@ -278,6 +278,33 @@ describe('PRCommentsList', () => { expect(markup).not.toContain('Add a PR comment') }) + it('shows resolve on open review threads', () => { + const comments: PRComment[] = [ + { + id: 2, + author: 'alice', + authorAvatarUrl: '', + body: 'Please address this before merge.', + createdAt: '2026-05-14T00:00:00Z', + url: 'https://github.com/acme/widgets/pull/42#discussion_r2', + threadId: 'thread-open', + path: 'src/a.ts', + isResolved: false + } + ] + + const markup = renderWithTooltips( + React.createElement(PRCommentsList, { + comments, + commentsLoading: false, + onResolve: () => true + }) + ) + + expect(markup).toContain('Resolve') + expect(markup).not.toContain('Unresolve') + }) + it('renders a more-actions menu on conversation comments', () => { const comments: PRComment[] = [ { 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 938ad6ff8bf..5772c80a07a 100644 --- a/src/renderer/src/components/right-sidebar/checks-panel-content.tsx +++ b/src/renderer/src/components/right-sidebar/checks-panel-content.tsx @@ -22,6 +22,7 @@ import { AlertTriangle, MoreHorizontal, Pencil, + SlidersHorizontal, Trash, X } from 'lucide-react' @@ -39,9 +40,18 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu' +import { + ContextMenu, + ContextMenuContent, + ContextMenuItem, + ContextMenuTrigger +} from '@/components/ui/context-menu' import { cn } from '@/lib/utils' import CommentMarkdown from '@/components/sidebar/CommentMarkdown' import { @@ -53,16 +63,24 @@ import { type PRCommentAudienceFilter } from '@/lib/pr-comment-audience' import { - getPRCommentGroupCount, getPRCommentGroupId, getPRCommentGroupRoot, groupPRComments, - isResolvedPRCommentGroup, - PR_COMMENT_OPEN_AUTHOR_CLASS, - PR_COMMENT_RESOLVED_AUTHOR_CLASS, - PR_COMMENT_RESOLVED_CONTAINER_CLASS, type PRCommentGroup } from '@/lib/pr-comment-groups' +import { + getPRCommentGroupActionState, + isPRCommentGroupQueueableForAI, + partitionPRCommentGroupsForTriage, + sortPRCommentGroupsForTimeline, + type PRCommentGroupActionState +} from '@/lib/pr-comment-action-state' +import { formatPrCommentRelativeTime } from '@/lib/pr-comment-time' +import { + getPRCommentPresentationClasses, + getPRCommentGroupSurfaceClasses, + type PRCommentPresentationClasses +} from './pr-comment-presentation' import type { PRInfo, PRCheckDetail, @@ -83,6 +101,16 @@ import { useAppStore } from '@/store' export const PullRequestIcon = GitPullRequest +type PRCommentsListDisplayMode = 'triage' | 'timeline' + +const PR_COMMENT_LIST_DISPLAY_MODES: PRCommentsListDisplayMode[] = ['triage', 'timeline'] + +function getPRCommentsListDisplayModeLabel(mode: PRCommentsListDisplayMode): string { + return mode === 'triage' + ? translate('auto.components.right.sidebar.checks.panel.content.8a621a2c4f', 'Grouped') + : translate('auto.components.right.sidebar.checks.panel.content.b13f85d75c', 'Timeline') +} + export const CHECK_ICON: Record> = { success: CircleCheck, failure: CircleX, @@ -1435,16 +1463,19 @@ export function isMutablePRConversationComment(comment: PRComment): boolean { function CommentMoreMenu({ comment, onStartEdit, - onDelete + onDelete, + onQueueForAgent }: { comment: PRComment onStartEdit?: () => void onDelete?: () => void | Promise + onQueueForAgent?: () => void }): React.JSX.Element | null { const hasGoToComment = Boolean(comment.url) const hasEdit = Boolean(onStartEdit) const hasDelete = Boolean(onDelete) - if (!hasGoToComment && !hasEdit && !hasDelete) { + const hasQueue = Boolean(onQueueForAgent) + if (!hasGoToComment && !hasEdit && !hasDelete && !hasQueue) { return null } @@ -1465,6 +1496,21 @@ function CommentMoreMenu({ + {hasQueue ? ( + { + event.preventDefault() + onQueueForAgent?.() + }} + > + + {translate( + 'auto.components.right.sidebar.checks.panel.content.f8a2c91d04', + 'Queue for agent' + )} + + ) : null} + {hasQueue && (hasGoToComment || hasEdit || hasDelete) ? : null} {hasGoToComment && ( window.api.shell.openUrl(comment.url)}> @@ -1507,6 +1553,39 @@ function buildCopyText(comment: PRComment): string { return `File: ${location}\n\n${comment.body}` } +function PRCommentActionBadge({ + actionState, + isQueued, + presentation +}: { + actionState: PRCommentGroupActionState + isQueued: boolean + presentation: PRCommentPresentationClasses +}): React.JSX.Element | null { + if (isQueued) { + return ( + + {translate('auto.components.right.sidebar.checks.panel.content.b4e8a1c902', 'Queued')} + + ) + } + if (actionState === 'open') { + return ( + + {translate('auto.components.right.sidebar.checks.panel.content.7c1f0a2b11', 'Open')} + + ) + } + if (actionState === 'resolved') { + return ( + + {translate('auto.components.right.sidebar.checks.panel.content.8987d5a3dd', 'Resolved')} + + ) + } + return null +} + /** A single comment row — used for both root and reply comments. */ function CommentRow({ comment, @@ -1514,26 +1593,32 @@ function CommentRow({ showResolve, showReply, selectionControl, - resolveSelectionAction, + actionState, + isQueued, replyDisabled, replyDisabledReason, + presentation, onResolve, onReply, onEditComment, - onDeleteComment + onDeleteComment, + onQueueForAgent }: { comment: PRComment isReply: boolean showResolve: boolean showReply?: boolean selectionControl?: React.ReactNode - resolveSelectionAction?: React.ReactNode + actionState: PRCommentGroupActionState + isQueued: boolean replyDisabled?: boolean replyDisabledReason?: string + presentation: PRCommentPresentationClasses onResolve?: (threadId: string, resolve: boolean) => boolean | Promise onReply?: (comment: PRComment) => void onEditComment?: (comment: PRComment, body: string) => Promise onDeleteComment?: (comment: PRComment) => void | Promise + onQueueForAgent?: () => void }): React.JSX.Element { const automated = isBotPRComment(comment) const canMutateComment = isMutablePRConversationComment(comment) @@ -1588,94 +1673,159 @@ function CommentRow({ const trimmedDraft = draft.trim() const canSaveEdit = !submittingEdit && trimmedDraft.length > 0 && trimmedDraft !== comment.body + const relativeTime = formatPrCommentRelativeTime(comment.createdAt, Date.now()) + + const authorAvatar = comment.authorAvatarUrl ? ( + {comment.author} + ) : ( +
+ ) + + const authorName = ( + + {comment.author} + + ) + const authorLead = selectionControl ? ( + {selectionControl} + ) : ( + authorAvatar + ) + + const commentActions = !editing ? ( +
+ {showResolve && + comment.threadId != null && + onResolve && + (actionState === 'open' || actionState === 'resolved') && ( + + )} + {showReply && onReply && ( + + )} + + +
+ ) : null + + const cardMetaRow = + presentation.useCardLayout && !isReply ? ( +
+ {relativeTime ? {relativeTime} : null} + {automated ? ( + + {translate('auto.components.right.sidebar.checks.panel.content.2ba0a32bdd', 'bot')} + + ) : null} + {comment.path ? ( + + {comment.path.split('/').pop()} + {formatLineRange(comment) && `:${formatLineRange(comment)}`} + + ) : null} + +
+ ) : null + + const authorLine = + presentation.useCardLayout && !isReply ? ( + <> +
+ {authorLead} + {authorName} + {commentActions} +
+ {cardMetaRow} + + ) : ( + <> + {authorLead} + {authorName} + {relativeTime ? ( + + {presentation.useCardLayout ? `· ${relativeTime}` : relativeTime} + + ) : null} + {automated && ( + + {translate('auto.components.right.sidebar.checks.panel.content.2ba0a32bdd', 'bot')} + + )} + {!isReply && comment.path && ( + + {comment.path.split('/').pop()} + {formatLineRange(comment) && `:${formatLineRange(comment)}`} + + )} + {!isReply ? ( + + ) : null} +
+ {commentActions} + + ) return (
- {selectionControl}
- {/* Author line: avatar + name + file badge aligned on center */} -
- {comment.authorAvatarUrl ? ( - {comment.author} - ) : ( -
- )} - - {comment.author} - - {automated && ( - - {translate('auto.components.right.sidebar.checks.panel.content.2ba0a32bdd', 'bot')} - - )} - {!isReply && comment.path && ( - - {comment.path.split('/').pop()} - {formatLineRange(comment) && `:${formatLineRange(comment)}`} - - )} -
- {!editing && resolveSelectionAction} - {!editing && ( -
- {showResolve && comment.threadId != null && onResolve && ( - - )} - {showReply && onReply && ( - - )} - - -
+
+ {authorLine}
{editing ? ( -
+