From c139c704aff9ec18af630cf8a62a024ae68a19bb Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Sun, 21 Jun 2026 23:59:29 -0700 Subject: [PATCH] Add visible PR comment queue action (#6057) --- .../right-sidebar/checks-panel-content.tsx | 86 ++++++++++++++----- .../right-sidebar/pr-comment-presentation.ts | 16 +++- .../pr-comments-list-selection.test.tsx | 41 ++++++--- src/renderer/src/i18n/locales/en.json | 3 +- src/renderer/src/i18n/locales/es.json | 3 +- src/renderer/src/i18n/locales/ja.json | 3 +- src/renderer/src/i18n/locales/ko.json | 3 +- src/renderer/src/i18n/locales/zh.json | 3 +- tests/e2e/pr-comments-sidebar-cards.spec.ts | 28 +++++- 9 files changed, 141 insertions(+), 45 deletions(-) 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 5772c80a07a..b3dc98cc704 100644 --- a/src/renderer/src/components/right-sidebar/checks-panel-content.tsx +++ b/src/renderer/src/components/right-sidebar/checks-panel-content.tsx @@ -1497,12 +1497,7 @@ function CommentMoreMenu({ {hasQueue ? ( - { - event.preventDefault() - onQueueForAgent?.() - }} - > + onQueueForAgent?.()}> {translate( 'auto.components.right.sidebar.checks.panel.content.f8a2c91d04', @@ -1553,6 +1548,38 @@ function buildCopyText(comment: PRComment): string { return `File: ${location}\n\n${comment.body}` } +function QueueForAgentButton({ + className, + onQueueForAgent +}: { + className?: string + onQueueForAgent: () => void +}): React.JSX.Element { + const label = translate( + 'auto.components.right.sidebar.checks.panel.content.f8a2c91d04', + 'Queue for agent' + ) + // Why: always-visible row action, but ghost styling keeps it from reading as a card-level CTA. + return ( + + ) +} + function PRCommentActionBadge({ actionState, isQueued, @@ -1690,14 +1717,11 @@ function CommentRow({ {comment.author} ) - const authorLead = selectionControl ? ( - {selectionControl} - ) : ( - authorAvatar - ) + const queueButton = + !isReply && onQueueForAgent ? : null - const commentActions = !editing ? ( -
+ const hoverActions = !editing ? ( +
{showResolve && comment.threadId != null && onResolve && @@ -1735,9 +1759,22 @@ function CommentRow({
) : null + const commentActions = !editing ? ( +
+ {presentation.useCardLayout ? null : queueButton} + {hoverActions} +
+ ) : null + const cardMetaRow = presentation.useCardLayout && !isReply ? ( -
+
{relativeTime ? {relativeTime} : null} {automated ? ( @@ -1755,6 +1792,12 @@ function CommentRow({ isQueued={isQueued} presentation={presentation} /> + {onQueueForAgent ? ( + + ) : null}
) : null @@ -1762,7 +1805,8 @@ function CommentRow({ presentation.useCardLayout && !isReply ? ( <>
- {authorLead} + {selectionControl} + {authorAvatar} {authorName} {commentActions}
@@ -1770,7 +1814,8 @@ function CommentRow({ ) : ( <> - {authorLead} + {selectionControl} + {authorAvatar} {authorName} {relativeTime ? ( @@ -1803,13 +1848,13 @@ function CommentRow({ return (
-
+
diff --git a/src/renderer/src/components/right-sidebar/pr-comment-presentation.ts b/src/renderer/src/components/right-sidebar/pr-comment-presentation.ts index 26907e4f286..7b9a645b994 100644 --- a/src/renderer/src/components/right-sidebar/pr-comment-presentation.ts +++ b/src/renderer/src/components/right-sidebar/pr-comment-presentation.ts @@ -1,8 +1,7 @@ import type { PRCommentGroupActionState } from '@/lib/pr-comment-action-state' import { cn } from '@/lib/utils' -/** PR comment sidebar typography and layout variants. Tweak here or compare in - * docs/design/pr-comments-sidebar.html, then preview in-app via localStorage. */ +/** PR comment sidebar typography and layout variants. */ export type PRCommentPresentationVariant = 'flat' | 'cards' | 'focus' export const DEFAULT_PR_COMMENT_PRESENTATION_VARIANT: PRCommentPresentationVariant = 'cards' @@ -53,6 +52,8 @@ export type PRCommentPresentationClasses = { statusBadgeQueued: string commentHeaderPrimary: string commentHeaderMeta: string + /** Indents the card meta row when a selection checkbox precedes the avatar. */ + commentHeaderMetaWithSelection: string groupOpen: string groupQueued: string groupResolved: string @@ -164,6 +165,7 @@ export function getPRCommentPresentationClasses( 'shrink-0 rounded border border-ring/40 bg-accent px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-foreground', commentHeaderPrimary: 'flex min-w-0 items-center gap-1.5', commentHeaderMeta: '', + commentHeaderMetaWithSelection: '', groupOpen: 'border-l-2 border-l-status-success', groupQueued: 'ring-1 ring-ring/50', groupResolved: '' @@ -211,7 +213,7 @@ export function getPRCommentPresentationClasses( 'rounded-full border border-border bg-muted px-1.5 py-px text-[10px] font-semibold tabular-nums text-muted-foreground', audienceTabs: 'grid grid-cols-3 rounded-md border border-border bg-background p-0.5', audienceTab: - 'flex h-8 items-center justify-center gap-1 rounded-md px-1.5 text-[12px] font-medium text-muted-foreground transition-colors', + 'flex h-7 items-center justify-center gap-1 rounded-md px-1.5 text-[11px] font-medium text-muted-foreground transition-colors', audienceTabActive: 'bg-muted text-foreground shadow-xs', sectionTriageLabel: cn('px-3 pt-1', RESOLVED_SECTION_LABEL), statusBadgeOpen: @@ -225,7 +227,13 @@ export function getPRCommentPresentationClasses( metaIndent, 'flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1 text-[11px] text-muted-foreground' ), - groupOpen: 'border-l-2 border-l-status-success', + // Why: checkbox (16px) + gap-2 sits before the avatar row the meta row already indents past. + commentHeaderMetaWithSelection: cn( + isFocus ? 'pl-[3.5rem]' : 'pl-[3.25rem]', + 'flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1 text-[11px] text-muted-foreground' + ), + // Why: open state is conveyed by the status badge; a green card rail reads noisy in the sidebar. + groupOpen: '', groupQueued: 'ring-1 ring-ring/50', groupResolved: '' } 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 aec9a30f238..69d07fff0a2 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 @@ -214,7 +214,7 @@ describe('PRCommentsList comment resolution selection', () => { ) }) - it('lets a user queue one eligible comment thread for the agent from the row menu', () => { + it('lets a user queue one eligible comment thread for the agent from the visible row action', () => { const onResolveSelectedCommentsWithAI = vi.fn() renderList({ comments: [ @@ -231,10 +231,10 @@ describe('PRCommentsList comment resolution selection', () => { onResolveSelectedCommentsWithAI }) - clickMenuItem('Queue for agent') + clickButton('Queue for agent') - expect(hasButton('Send 1 queued comments')).toBe(true) - clickButton('Send 1 queued comments') + expect(hasButton('Send 1 queued comments to AI')).toBe(true) + clickButton('Send 1 queued comments to AI') expect(onResolveSelectedCommentsWithAI).toHaveBeenCalledTimes(1) const selectedGroups = onResolveSelectedCommentsWithAI.mock.calls[0]?.[0] as PRCommentGroup[] @@ -242,7 +242,7 @@ describe('PRCommentsList comment resolution selection', () => { expect(selectedGroups[0]?.kind === 'thread' ? selectedGroups[0].threadId : '').toBe('thread-1') }) - it('lets a user queue one standalone comment for the agent from the row menu', () => { + it('lets a user queue one standalone comment for the agent from the visible row action', () => { const onResolveSelectedCommentsWithAI = vi.fn() renderList({ comments: [ @@ -255,10 +255,10 @@ describe('PRCommentsList comment resolution selection', () => { onResolveSelectedCommentsWithAI }) - clickMenuItem('Queue for agent') + clickButton('Queue for agent') - expect(hasButton('Send 1 queued comments')).toBe(true) - clickButton('Send 1 queued comments') + expect(hasButton('Send 1 queued comments to AI')).toBe(true) + clickButton('Send 1 queued comments to AI') expect(onResolveSelectedCommentsWithAI).toHaveBeenCalledTimes(1) const selectedGroups = onResolveSelectedCommentsWithAI.mock.calls[0]?.[0] as PRCommentGroup[] @@ -269,16 +269,31 @@ describe('PRCommentsList comment resolution selection', () => { ) }) + it('keeps the overflow menu queue action available as a fallback', () => { + const onResolveSelectedCommentsWithAI = vi.fn() + renderList({ + comments: [comment({ id: 1, threadId: 'thread-1', path: 'src/a.ts', isResolved: false })], + onResolveSelectedCommentsWithAI + }) + + clickMenuItem('Queue for agent') + + expect(hasButton('Send 1 queued comments to AI')).toBe(true) + clickButton('Send 1 queued comments to AI') + + expect(onResolveSelectedCommentsWithAI).toHaveBeenCalledTimes(1) + }) + it('clears the queued comment list from the header action', () => { renderList({ comments: [comment({ id: 1, threadId: 'thread-1', path: 'src/a.ts', isResolved: false })] }) - clickMenuItem('Queue for agent') + clickButton('Queue for agent') - expect(hasButton('Send 1 queued comments')).toBe(true) + expect(hasButton('Send 1 queued comments to AI')).toBe(true) clickButton('Clear queued comments') - expect(hasButton('Send 1 queued comments')).toBe(false) + expect(hasButton('Send 1 queued comments to AI')).toBe(false) expect(container.querySelector('button[role="checkbox"]')).toBeNull() }) @@ -286,12 +301,12 @@ describe('PRCommentsList comment resolution selection', () => { renderList({ comments: [comment({ id: 1, threadId: 'thread-1', path: 'src/a.ts', isResolved: false })] }) - clickMenuItem('Queue for agent') + clickButton('Queue for agent') renderList({ comments: [comment({ id: 1, threadId: 'thread-1', path: 'src/a.ts', isResolved: true })] }) - expect(hasButton('Send 1 queued comments')).toBe(false) + expect(hasButton('Send 1 queued comments to AI')).toBe(false) }) }) diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index 8bbddbfeb20..0c6fdfd6a65 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -8832,7 +8832,7 @@ "084c516efb": "Copy commands", "5dc3af25c0": "Select comment", "d7a2f9c401": "Send unresolved {{value0}} comments", - "d91f2a6c39": "Send {{value0}} queued comments", + "d91f2a6c39": "Send {{value0}} queued comments to AI", "a6de3e5a20": "Clear queued comments", "49ea0937e4": "Add comment to resolve list", "9fecebb29d": "Add", @@ -8841,6 +8841,7 @@ "7c1f0a2b11": "Open", "e8b4c1a903": "Resolved · {{value0}}", "c3a8e5d710": "Needs review · {{value0}}", + "a7f0c7e8d1": "Queue", "8a621a2c4f": "Grouped", "b13f85d75c": "Timeline", "f5cf324efa": "Comment display options", diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json index 1ea5a2f70c3..59f5a099e83 100644 --- a/src/renderer/src/i18n/locales/es.json +++ b/src/renderer/src/i18n/locales/es.json @@ -8831,7 +8831,7 @@ "084c516efb": "Copy commands", "5dc3af25c0": "Seleccionar comentario", "d7a2f9c401": "Send unresolved {{value0}} comments", - "d91f2a6c39": "Enviar {{value0}} comentarios en cola", + "d91f2a6c39": "Enviar {{value0}} comentarios en cola a la IA", "a6de3e5a20": "Borrar comentarios en cola", "49ea0937e4": "Agregar comentario a la lista de resolución", "9fecebb29d": "Agregar", @@ -8841,6 +8841,7 @@ "7c1f0a2b11": "Open", "e8b4c1a903": "Resolved · {{value0}}", "c3a8e5d710": "Needs review · {{value0}}", + "a7f0c7e8d1": "Queue", "8a621a2c4f": "Grouped", "b13f85d75c": "Timeline", "f5cf324efa": "Comment display options", diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json index da06c34f4ec..212f6ec64aa 100644 --- a/src/renderer/src/i18n/locales/ja.json +++ b/src/renderer/src/i18n/locales/ja.json @@ -8831,7 +8831,7 @@ "084c516efb": "Copy commands", "5dc3af25c0": "コメントを選択", "d7a2f9c401": "Send unresolved {{value0}} comments", - "d91f2a6c39": "キュー内の {{value0}} 件のコメントを送信", + "d91f2a6c39": "キュー内の {{value0}} 件のコメントを AI に送信", "a6de3e5a20": "キュー内のコメントをクリア", "49ea0937e4": "コメントを解決リストに追加", "9fecebb29d": "追加", @@ -8841,6 +8841,7 @@ "7c1f0a2b11": "Open", "e8b4c1a903": "Resolved · {{value0}}", "c3a8e5d710": "Needs review · {{value0}}", + "a7f0c7e8d1": "Queue", "8a621a2c4f": "Grouped", "b13f85d75c": "Timeline", "f5cf324efa": "Comment display options", diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json index 7e1fb8ca95e..ceff3340a8e 100644 --- a/src/renderer/src/i18n/locales/ko.json +++ b/src/renderer/src/i18n/locales/ko.json @@ -8831,7 +8831,7 @@ "084c516efb": "Copy commands", "5dc3af25c0": "댓글 선택", "d7a2f9c401": "해결되지 않은 댓글 {{value0}}개 보내기", - "d91f2a6c39": "대기 중인 댓글 {{value0}}개 보내기", + "d91f2a6c39": "대기 중인 댓글 {{value0}}개를 AI로 보내기", "a6de3e5a20": "대기 중인 댓글 지우기", "49ea0937e4": "댓글을 해결 목록에 추가", "9fecebb29d": "추가", @@ -8841,6 +8841,7 @@ "7c1f0a2b11": "Open", "e8b4c1a903": "Resolved · {{value0}}", "c3a8e5d710": "Needs review · {{value0}}", + "a7f0c7e8d1": "Queue", "8a621a2c4f": "Grouped", "b13f85d75c": "Timeline", "f5cf324efa": "Comment display options", diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json index 100424111f5..6c1af075995 100644 --- a/src/renderer/src/i18n/locales/zh.json +++ b/src/renderer/src/i18n/locales/zh.json @@ -8831,7 +8831,7 @@ "084c516efb": "Copy commands", "5dc3af25c0": "选择评论", "d7a2f9c401": "Send unresolved {{value0}} comments", - "d91f2a6c39": "发送 {{value0}} 条已排队评论", + "d91f2a6c39": "发送 {{value0}} 条已排队评论给 AI", "a6de3e5a20": "清除已排队评论", "49ea0937e4": "将评论添加到解决列表", "9fecebb29d": "添加", @@ -8841,6 +8841,7 @@ "7c1f0a2b11": "Open", "e8b4c1a903": "Resolved · {{value0}}", "c3a8e5d710": "Needs review · {{value0}}", + "a7f0c7e8d1": "Queue", "8a621a2c4f": "Grouped", "b13f85d75c": "Timeline", "f5cf324efa": "Comment display options", diff --git a/tests/e2e/pr-comments-sidebar-cards.spec.ts b/tests/e2e/pr-comments-sidebar-cards.spec.ts index 46978c0a119..a045302269a 100644 --- a/tests/e2e/pr-comments-sidebar-cards.spec.ts +++ b/tests/e2e/pr-comments-sidebar-cards.spec.ts @@ -125,7 +125,9 @@ test.describe('PR comments sidebar cards view', () => { expect(positions[1]).toBeLessThan(positions[2]) }) - test('queues an open thread for the agent from the row menu', async ({ orcaPage }) => { + test('queues an open thread for the agent from the visible row action and menu fallback', async ({ + orcaPage + }) => { const { worktreeId } = await seedPRCommentsSidebarFixture(orcaPage) await openChecks(orcaPage, worktreeId) @@ -135,12 +137,32 @@ test.describe('PR comments sidebar cards view', () => { hasText: 'Please update this handler before merge.' }) await openThreadCard.hover() + const visibleQueueButton = openThreadCard.getByRole('button', { name: 'Queue for agent' }) + await expect(visibleQueueButton).toBeVisible() + await visibleQueueButton.click() + await expect(visibleQueueButton).toBeHidden() + await expect( + orcaPage.getByRole('button', { name: 'Send 1 queued comments to AI' }) + ).toBeVisible() + await expect(orcaPage.getByText('Queued', { exact: true })).toBeVisible() + + await orcaPage.getByRole('button', { name: 'Clear queued comments' }).click() + await expect( + orcaPage.getByRole('button', { name: 'Send 1 queued comments to AI' }) + ).toBeHidden() + await openThreadCard.hover() + await expect(visibleQueueButton).toBeVisible() + const actionsMenu = openThreadCard.getByRole('button', { name: 'More comment actions' }) await actionsMenu.evaluate((element) => (element as HTMLElement).focus()) await actionsMenu.press('Enter') - await orcaPage.getByRole('menuitem', { name: 'Queue for agent' }).click({ force: true }) + const queueMenuItem = orcaPage.getByRole('menuitem', { name: 'Queue for agent' }) + await queueMenuItem.click({ force: true }) + await expect(queueMenuItem).toBeHidden() - await expect(orcaPage.getByRole('button', { name: 'Send 1 queued comments' })).toBeVisible() + await expect( + orcaPage.getByRole('button', { name: 'Send 1 queued comments to AI' }) + ).toBeVisible() await expect(orcaPage.getByText('Queued', { exact: true })).toBeVisible() const queuedCard = orcaPage.getByTestId('pr-comment-group').filter({