mirror of
https://github.com/stablyai/orca.git
synced 2026-10-21 00:03:46 +00:00
Batch conversation replies instead of posting per-comment
Combine N unresolvable comments into a single PR conversation reply to avoid timeline spam. Review threads still get one nested reply each. Refactor helper functions to pr-comment-fixing-reply-body.ts and change replyAsConversation(comment, body) → replyAsConversation(body).
This commit is contained in:
@@ -79,10 +79,10 @@ import {
|
||||
buildPRCommentsResolutionPrompt,
|
||||
isResolvablePRCommentGroup
|
||||
} from '../pr-comments-resolution-prompt'
|
||||
import { buildPRCommentConversationReplyBody } from './pr-comment-fixing-reply-body'
|
||||
import {
|
||||
acknowledgePRCommentsAfterAiLaunch,
|
||||
attachPRReviewReplyParent,
|
||||
buildPRCommentConversationReplyBody,
|
||||
canPostPRReviewThreadReply,
|
||||
checksPanelReviewStableKey,
|
||||
clearPendingPRCommentAiAck,
|
||||
@@ -3095,12 +3095,13 @@ export default function ChecksPanel(): React.JSX.Element {
|
||||
{ value0: activeReview.provider === 'gitlab' ? 'MR' : 'PR' }
|
||||
),
|
||||
// Why: only GitHub with a resolved PR target posts fixing replies; other providers
|
||||
// must not be promised a reply the ack never sends.
|
||||
// must not be promised a reply the ack never sends. Review threads get one reply
|
||||
// each, conversation comments share one — so the copy stays deliberately vague.
|
||||
description:
|
||||
githubTarget && activeReview.provider === 'github'
|
||||
? translate(
|
||||
'auto.components.right.sidebar.ChecksPanel.ed3f79c031',
|
||||
'Review the prompt before starting an agent. After launch, Orca posts a fixing reply on each selected comment and resolves host threads when possible.'
|
||||
'Review the prompt before starting an agent. After launch, Orca replies to the selected comments and resolves host threads when possible.'
|
||||
)
|
||||
: translate(
|
||||
'auto.components.right.sidebar.ChecksPanel.abf59262fb',
|
||||
@@ -3234,8 +3235,9 @@ export default function ChecksPanel(): React.JSX.Element {
|
||||
return false
|
||||
}
|
||||
},
|
||||
// Why: CodeRabbit / review-summary / conversation comments have no nested-reply API.
|
||||
replyAsConversation: async (comment, body) => {
|
||||
// Why: CodeRabbit / review-summary / conversation comments have no nested-reply
|
||||
// API, so the ack sends one combined body for all of them.
|
||||
replyAsConversation: async (body) => {
|
||||
if (!githubTarget) {
|
||||
return false
|
||||
}
|
||||
@@ -3243,7 +3245,7 @@ export default function ChecksPanel(): React.JSX.Element {
|
||||
const result = await addPRConversationComment(
|
||||
githubTarget.repoPath,
|
||||
githubTarget.prNumber,
|
||||
buildPRCommentConversationReplyBody(comment.author, body),
|
||||
body,
|
||||
{
|
||||
repoId: githubTarget.repoId,
|
||||
prRepo: githubTarget.prRepo
|
||||
|
||||
@@ -4,18 +4,21 @@ import type { PRCommentGroup } from '@/lib/pr-comment-groups'
|
||||
import {
|
||||
acknowledgePRCommentsAfterAiLaunch,
|
||||
attachPRReviewReplyParent,
|
||||
buildPRCommentConversationReplyBody,
|
||||
canPostPRReviewThreadReply,
|
||||
checksPanelReviewStableKey,
|
||||
clearPendingPRCommentAiAck,
|
||||
formatPRCommentMentionHandle,
|
||||
getPRCommentGroupReplyTarget,
|
||||
PR_COMMENT_ACK_MAX_CONCURRENCY,
|
||||
PR_COMMENT_AI_FIXING_REPLY,
|
||||
resolvePRReviewReplyThreadId,
|
||||
setPendingPRCommentAiAck,
|
||||
takePendingPRCommentAiAck
|
||||
} from './pr-comments-ai-launch-ack'
|
||||
import {
|
||||
buildPRCommentBatchConversationReplyBody,
|
||||
buildPRCommentConversationReplyBody,
|
||||
formatPRCommentMentionHandle,
|
||||
PR_COMMENT_AI_FIXING_REPLY
|
||||
} from './pr-comment-fixing-reply-body'
|
||||
|
||||
function comment(overrides: Partial<PRComment> = {}): PRComment {
|
||||
return {
|
||||
@@ -123,8 +126,58 @@ describe('buildPRCommentConversationReplyBody', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('buildPRCommentBatchConversationReplyBody', () => {
|
||||
it('returns an empty body for an empty selection', () => {
|
||||
expect(buildPRCommentBatchConversationReplyBody([])).toBe('')
|
||||
})
|
||||
|
||||
it('keeps the plain @-reply for a single target', () => {
|
||||
expect(
|
||||
buildPRCommentBatchConversationReplyBody([comment({ author: 'coderabbitai[bot]' })])
|
||||
).toBe(`@coderabbitai ${PR_COMMENT_AI_FIXING_REPLY}`)
|
||||
})
|
||||
|
||||
it('lists every target once with author, kind, and a short label', () => {
|
||||
const body = buildPRCommentBatchConversationReplyBody([
|
||||
comment({
|
||||
author: 'coderabbitai[bot]',
|
||||
body: '<!-- meta -->\n## Nitpick comments (2)\n\ndetails',
|
||||
url: 'https://github.com/acme/widgets/pull/42#pullrequestreview-999'
|
||||
}),
|
||||
comment({
|
||||
author: 'alice',
|
||||
body: 'Please rename this variable.',
|
||||
url: 'https://github.com/acme/widgets/pull/42#issuecomment-9'
|
||||
}),
|
||||
comment({ author: 'bob', body: 'Off by one.', path: 'src/a.ts', line: 12 })
|
||||
])
|
||||
|
||||
expect(body).toBe(
|
||||
[
|
||||
'Fixing:',
|
||||
'- @coderabbitai: review summary — Nitpick comments (2)',
|
||||
'- @alice: comment — Please rename this variable.',
|
||||
'- @bob: comment on src/a.ts:12 — Off by one.',
|
||||
'',
|
||||
'Will be in the next commit.'
|
||||
].join('\n')
|
||||
)
|
||||
})
|
||||
|
||||
it('truncates long bodies instead of quoting the whole comment', () => {
|
||||
const body = buildPRCommentBatchConversationReplyBody([
|
||||
comment({ author: 'alice', body: 'a'.repeat(200) }),
|
||||
comment({ author: 'bob', body: '' })
|
||||
])
|
||||
|
||||
expect(body).toContain(`- @alice: comment — ${'a'.repeat(71)}…`)
|
||||
expect(body).toContain('- @bob: comment\n')
|
||||
})
|
||||
})
|
||||
|
||||
describe('getPRCommentGroupReplyTarget', () => {
|
||||
it('uses the latest reply in a thread when present', () => {
|
||||
// Why: GitHub's replies endpoint keys off the top-level review comment id.
|
||||
it('uses the thread root even when later replies exist', () => {
|
||||
const group: PRCommentGroup = {
|
||||
kind: 'thread',
|
||||
threadId: 'T1',
|
||||
@@ -134,7 +187,7 @@ describe('getPRCommentGroupReplyTarget', () => {
|
||||
comment({ id: 12, threadId: 'T1', path: 'a.ts', body: 'latest' })
|
||||
]
|
||||
}
|
||||
expect(getPRCommentGroupReplyTarget(group).id).toBe(12)
|
||||
expect(getPRCommentGroupReplyTarget(group).id).toBe(10)
|
||||
})
|
||||
|
||||
it('falls back to the root when there are no replies', () => {
|
||||
@@ -173,7 +226,6 @@ describe('attachPRReviewReplyParent', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('resolvePRReviewReplyThreadId', () => {
|
||||
it('prefers the parent threadId', () => {
|
||||
expect(
|
||||
@@ -281,7 +333,26 @@ describe('acknowledgePRCommentsAfterAiLaunch', () => {
|
||||
})
|
||||
|
||||
expect(result).toEqual({ resolved: 0, replied: 1, skipped: 0, failed: 0 })
|
||||
expect(replyAsConversation).toHaveBeenCalledWith(group.comment, PR_COMMENT_AI_FIXING_REPLY)
|
||||
expect(replyAsConversation).toHaveBeenCalledTimes(1)
|
||||
expect(replyAsConversation).toHaveBeenCalledWith(`@alice ${PR_COMMENT_AI_FIXING_REPLY}`)
|
||||
})
|
||||
|
||||
it('posts no conversation comment when nothing in the selection needs one', async () => {
|
||||
const replyAsConversation = vi.fn()
|
||||
|
||||
await acknowledgePRCommentsAfterAiLaunch({
|
||||
groups: [openThread('T1'), openThread('T2', 11)],
|
||||
deps: {
|
||||
isStillCurrent: () => true,
|
||||
isThreadStillResolvable: () => true,
|
||||
resolveThread: vi.fn().mockResolvedValue(true),
|
||||
canReply: true,
|
||||
replyInThread: vi.fn().mockResolvedValue(true),
|
||||
replyAsConversation
|
||||
}
|
||||
})
|
||||
|
||||
expect(replyAsConversation).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('posts a conversation @-reply for CodeRabbit review summaries', async () => {
|
||||
@@ -303,17 +374,17 @@ describe('acknowledgePRCommentsAfterAiLaunch', () => {
|
||||
|
||||
expect(result).toEqual({ resolved: 0, replied: 1, skipped: 0, failed: 0 })
|
||||
expect(replyInThread).not.toHaveBeenCalled()
|
||||
expect(replyAsConversation).toHaveBeenCalledWith(group.comment, PR_COMMENT_AI_FIXING_REPLY)
|
||||
expect(replyAsConversation).toHaveBeenCalledWith(`@coderabbitai ${PR_COMMENT_AI_FIXING_REPLY}`)
|
||||
})
|
||||
|
||||
// Why: the reported bug — a selection of only unresolvable CodeRabbit summaries
|
||||
// produced replied=0 because the ack ran the resolve path and nothing else.
|
||||
it('replies to every unresolvable review summary in the selection', async () => {
|
||||
// Why: the reported bug — N selected summaries used to produce N near-identical
|
||||
// "Fixing." conversation comments; one combined post covers the whole set.
|
||||
it('combines every unresolvable review summary into one conversation comment', async () => {
|
||||
const replyAsConversation = vi.fn().mockResolvedValue(true)
|
||||
const resolveThread = vi.fn()
|
||||
|
||||
const result = await acknowledgePRCommentsAfterAiLaunch({
|
||||
groups: [codeRabbitReviewSummary(30), codeRabbitReviewSummary(31)],
|
||||
groups: [codeRabbitReviewSummary(30), codeRabbitReviewSummary(31), standalone(32)],
|
||||
deps: {
|
||||
isStillCurrent: () => true,
|
||||
isThreadStillResolvable: () => false,
|
||||
@@ -324,7 +395,19 @@ describe('acknowledgePRCommentsAfterAiLaunch', () => {
|
||||
}
|
||||
})
|
||||
|
||||
expect(result).toEqual({ resolved: 0, replied: 2, skipped: 0, failed: 0 })
|
||||
// Why: one host POST, so replied counts 1 — not one per selected comment.
|
||||
expect(result).toEqual({ resolved: 0, replied: 1, skipped: 0, failed: 0 })
|
||||
expect(replyAsConversation).toHaveBeenCalledTimes(1)
|
||||
expect(replyAsConversation.mock.calls[0]?.[0]).toBe(
|
||||
[
|
||||
'Fixing:',
|
||||
'- @coderabbitai: review summary — Nitpick comments (2)',
|
||||
'- @coderabbitai: review summary — Nitpick comments (2)',
|
||||
'- @alice: comment — Overall looks good; one nit.',
|
||||
'',
|
||||
'Will be in the next commit.'
|
||||
].join('\n')
|
||||
)
|
||||
expect(resolveThread).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -348,7 +431,35 @@ describe('acknowledgePRCommentsAfterAiLaunch', () => {
|
||||
|
||||
expect(result).toEqual({ resolved: 1, replied: 2, skipped: 0, failed: 0 })
|
||||
expect(replyInThread).toHaveBeenCalledWith(thread.root, PR_COMMENT_AI_FIXING_REPLY)
|
||||
expect(replyAsConversation).toHaveBeenCalledWith(summary.comment, PR_COMMENT_AI_FIXING_REPLY)
|
||||
expect(replyAsConversation).toHaveBeenCalledWith(`@coderabbitai ${PR_COMMENT_AI_FIXING_REPLY}`)
|
||||
})
|
||||
|
||||
// Why: nestable threads keep one reply each; only the conversation half is batched.
|
||||
it('replies once per thread and once for the whole conversation set', async () => {
|
||||
const replyInThread = vi.fn().mockResolvedValue(true)
|
||||
const replyAsConversation = vi.fn().mockResolvedValue(true)
|
||||
|
||||
const result = await acknowledgePRCommentsAfterAiLaunch({
|
||||
groups: [
|
||||
openThread('T1', 10),
|
||||
codeRabbitReviewSummary(30),
|
||||
openThread('T2', 11),
|
||||
standalone(31)
|
||||
],
|
||||
deps: {
|
||||
isStillCurrent: () => true,
|
||||
isThreadStillResolvable: () => true,
|
||||
resolveThread: vi.fn().mockResolvedValue(true),
|
||||
canReply: true,
|
||||
replyInThread,
|
||||
replyAsConversation
|
||||
}
|
||||
})
|
||||
|
||||
expect(result).toEqual({ resolved: 2, replied: 3, skipped: 0, failed: 0 })
|
||||
expect(replyInThread).toHaveBeenCalledTimes(2)
|
||||
expect(replyAsConversation).toHaveBeenCalledTimes(1)
|
||||
expect(replyAsConversation.mock.calls[0]?.[0]).toContain('Fixing:')
|
||||
})
|
||||
|
||||
it('counts a failed conversation reply instead of silently reporting success', async () => {
|
||||
@@ -367,7 +478,7 @@ describe('acknowledgePRCommentsAfterAiLaunch', () => {
|
||||
expect(result).toEqual({ resolved: 0, replied: 0, skipped: 0, failed: 1 })
|
||||
})
|
||||
|
||||
it('replies to the latest message in a multi-comment thread', async () => {
|
||||
it('replies to the thread root in a multi-comment thread', async () => {
|
||||
const replyInThread = vi.fn().mockResolvedValue(true)
|
||||
const group: PRCommentGroup = {
|
||||
kind: 'thread',
|
||||
@@ -389,7 +500,7 @@ describe('acknowledgePRCommentsAfterAiLaunch', () => {
|
||||
})
|
||||
|
||||
expect(result).toEqual({ resolved: 0, replied: 1, skipped: 0, failed: 0 })
|
||||
expect(replyInThread).toHaveBeenCalledWith(group.replies[0], PR_COMMENT_AI_FIXING_REPLY)
|
||||
expect(replyInThread).toHaveBeenCalledWith(group.root, PR_COMMENT_AI_FIXING_REPLY)
|
||||
})
|
||||
|
||||
it('skips when reply is unavailable and resolve cannot run', async () => {
|
||||
@@ -458,20 +569,54 @@ describe('acknowledgePRCommentsAfterAiLaunch', () => {
|
||||
expect(result).toEqual({ resolved: 0, replied: 3, skipped: 3, failed: 0 })
|
||||
})
|
||||
|
||||
it('counts a failed resolve on a group whose reply also failed', async () => {
|
||||
// Why: resolving after a failed reply closes the thread with no ack at all.
|
||||
it('leaves a thread open when its fixing reply failed', async () => {
|
||||
const resolveThread = vi.fn().mockResolvedValue(true)
|
||||
|
||||
const result = await acknowledgePRCommentsAfterAiLaunch({
|
||||
groups: [openThread('T1')],
|
||||
deps: {
|
||||
isStillCurrent: () => true,
|
||||
isThreadStillResolvable: () => true,
|
||||
resolveThread: vi.fn().mockResolvedValue(false),
|
||||
resolveThread,
|
||||
canReply: true,
|
||||
replyInThread: vi.fn().mockResolvedValue(false),
|
||||
replyAsConversation: vi.fn()
|
||||
}
|
||||
})
|
||||
|
||||
expect(result).toEqual({ resolved: 0, replied: 0, skipped: 0, failed: 2 })
|
||||
expect(result).toEqual({ resolved: 0, replied: 0, skipped: 0, failed: 1 })
|
||||
expect(resolveThread).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('does not resolve batched conversation groups when the combined post failed', async () => {
|
||||
const resolveThread = vi.fn()
|
||||
const summaryThread: PRCommentGroup = {
|
||||
kind: 'thread',
|
||||
threadId: 'T_summary',
|
||||
root: comment({
|
||||
id: 30,
|
||||
threadId: 'T_summary',
|
||||
isResolved: false,
|
||||
url: 'https://github.com/acme/widgets/pull/42#pullrequestreview-999'
|
||||
}),
|
||||
replies: []
|
||||
}
|
||||
|
||||
const result = await acknowledgePRCommentsAfterAiLaunch({
|
||||
groups: [summaryThread],
|
||||
deps: {
|
||||
isStillCurrent: () => true,
|
||||
isThreadStillResolvable: () => true,
|
||||
resolveThread,
|
||||
canReply: true,
|
||||
replyInThread: vi.fn(),
|
||||
replyAsConversation: vi.fn().mockResolvedValue(false)
|
||||
}
|
||||
})
|
||||
|
||||
expect(result).toEqual({ resolved: 0, replied: 0, skipped: 0, failed: 1 })
|
||||
expect(resolveThread).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('reports a resolvable thread as skipped when the panel context moved', async () => {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import type { PRComment } from '../../../../shared/types'
|
||||
import { getPRCommentGroupRoot, type PRCommentGroup } from '@/lib/pr-comment-groups'
|
||||
import { isResolvablePRCommentGroup } from '../pr-comments-resolution-prompt'
|
||||
|
||||
/** Posted when a selected review comment is sent to AI. */
|
||||
export const PR_COMMENT_AI_FIXING_REPLY = 'Fixing. Will be in the next commit'
|
||||
import {
|
||||
buildPRCommentBatchConversationReplyBody,
|
||||
PR_COMMENT_AI_FIXING_REPLY
|
||||
} from './pr-comment-fixing-reply-body'
|
||||
|
||||
export type PRCommentAiLaunchAckCounts = {
|
||||
resolved: number
|
||||
@@ -25,27 +26,10 @@ export type PRCommentAiLaunchAckDeps = {
|
||||
*/
|
||||
replyInThread: (comment: PRComment, body: string) => Promise<boolean>
|
||||
/**
|
||||
* Top-level PR conversation comment (issues/.../comments), used for review
|
||||
* summaries and other comments that have no nested-reply endpoint (e.g. CodeRabbit).
|
||||
* One top-level PR conversation comment (issues/.../comments) covering every selected
|
||||
* comment with no nested-reply endpoint (review summaries, CodeRabbit, plain comments).
|
||||
*/
|
||||
replyAsConversation: (comment: PRComment, body: string) => Promise<boolean>
|
||||
}
|
||||
|
||||
/**
|
||||
* GitHub App logins carry a `[bot]` suffix that does not resolve as an @-mention
|
||||
* (`@coderabbitai[bot]` renders literally; `@coderabbitai` reaches the bot).
|
||||
*/
|
||||
export function formatPRCommentMentionHandle(author: string | undefined): string {
|
||||
return (author ?? '').replace(/\[bot\]$/i, '').trim()
|
||||
}
|
||||
|
||||
/** Top-level conversation reply body, addressed to the comment author. */
|
||||
export function buildPRCommentConversationReplyBody(
|
||||
author: string | undefined,
|
||||
body: string
|
||||
): string {
|
||||
const handle = formatPRCommentMentionHandle(author)
|
||||
return handle ? `@${handle} ${body}` : body
|
||||
replyAsConversation: (body: string) => Promise<boolean>
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -72,12 +56,10 @@ export function canPostPRReviewThreadReply(
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefer the latest message in a thread so the fixing reply sits after existing replies.
|
||||
* Reply to the thread root: GitHub's replies endpoint keys off the top-level review
|
||||
* comment id, and a later reply's id can 404 there.
|
||||
*/
|
||||
export function getPRCommentGroupReplyTarget(group: PRCommentGroup): PRComment {
|
||||
if (group.kind === 'thread' && group.replies.length > 0) {
|
||||
return group.replies.at(-1)!
|
||||
}
|
||||
return getPRCommentGroupRoot(group)
|
||||
}
|
||||
|
||||
@@ -174,10 +156,22 @@ async function mapWithBoundedConcurrency<T, R>(
|
||||
return results
|
||||
}
|
||||
|
||||
type BatchedConversationReply = {
|
||||
counts: PRCommentAiLaunchAckCounts
|
||||
handled: ReadonlySet<PRCommentGroup>
|
||||
replyOk: boolean
|
||||
}
|
||||
|
||||
const NO_BATCHED_CONVERSATION_REPLY: BatchedConversationReply = {
|
||||
counts: EMPTY_ACK_COUNTS,
|
||||
handled: new Set(),
|
||||
replyOk: false
|
||||
}
|
||||
|
||||
/**
|
||||
* After launching an agent for selected review feedback:
|
||||
* post a fixing reply (nested when possible, else conversation @-reply), then
|
||||
* resolve host threads when possible.
|
||||
* post a fixing reply (one nested reply per review thread, one combined conversation
|
||||
* comment for everything that has no nested-reply endpoint), then resolve host threads.
|
||||
*
|
||||
* Why: replies use a snapshotted GitHub target and must not abort just because
|
||||
* the checks-panel async key churned mid-launch. isStillCurrent only gates
|
||||
@@ -190,13 +184,22 @@ export async function acknowledgePRCommentsAfterAiLaunch(args: {
|
||||
groups: readonly PRCommentGroup[]
|
||||
deps: PRCommentAiLaunchAckDeps
|
||||
}): Promise<PRCommentAiLaunchAckCounts> {
|
||||
// Why: conversation comments have no replies endpoint, so one post per selected item
|
||||
// would spam the PR timeline with near-identical "Fixing." comments.
|
||||
const conversationGroups = args.deps.canReply
|
||||
? args.groups.filter(
|
||||
(group) => !canPostPRReviewThreadReply(getPRCommentGroupReplyTarget(group))
|
||||
)
|
||||
: []
|
||||
const batch = await postBatchedConversationReply(conversationGroups, args.deps)
|
||||
|
||||
const perGroup = await mapWithBoundedConcurrency(
|
||||
args.groups,
|
||||
PR_COMMENT_ACK_MAX_CONCURRENCY,
|
||||
(group) => acknowledgePRCommentGroup(group, args.deps)
|
||||
(group) => acknowledgePRCommentGroup(group, args.deps, batch)
|
||||
)
|
||||
|
||||
return perGroup.reduce<PRCommentAiLaunchAckCounts>(
|
||||
return [batch.counts, ...perGroup].reduce<PRCommentAiLaunchAckCounts>(
|
||||
(totals, counts) => ({
|
||||
resolved: totals.resolved + counts.resolved,
|
||||
replied: totals.replied + counts.replied,
|
||||
@@ -207,32 +210,48 @@ export async function acknowledgePRCommentsAfterAiLaunch(args: {
|
||||
)
|
||||
}
|
||||
|
||||
async function postBatchedConversationReply(
|
||||
groups: readonly PRCommentGroup[],
|
||||
deps: PRCommentAiLaunchAckDeps
|
||||
): Promise<BatchedConversationReply> {
|
||||
if (groups.length === 0) {
|
||||
return NO_BATCHED_CONVERSATION_REPLY
|
||||
}
|
||||
const replyOk = await deps.replyAsConversation(
|
||||
buildPRCommentBatchConversationReplyBody(groups.map(getPRCommentGroupReplyTarget))
|
||||
)
|
||||
return {
|
||||
// Why: one host POST covers the whole set, so the toast must count it once.
|
||||
counts: { ...EMPTY_ACK_COUNTS, replied: replyOk ? 1 : 0, failed: replyOk ? 0 : 1 },
|
||||
handled: new Set(groups),
|
||||
replyOk
|
||||
}
|
||||
}
|
||||
|
||||
async function acknowledgePRCommentGroup(
|
||||
group: PRCommentGroup,
|
||||
deps: PRCommentAiLaunchAckDeps
|
||||
deps: PRCommentAiLaunchAckDeps,
|
||||
batch: BatchedConversationReply
|
||||
): Promise<PRCommentAiLaunchAckCounts> {
|
||||
const counts = { ...EMPTY_ACK_COUNTS }
|
||||
const replyTarget = getPRCommentGroupReplyTarget(group)
|
||||
const threadId =
|
||||
group.kind === 'thread' && isResolvablePRCommentGroup(group) ? group.threadId : null
|
||||
|
||||
if (deps.canReply) {
|
||||
// Why: prefer nested review-thread replies; fall back to a top-level conversation
|
||||
// comment for review summaries / issue comments (CodeRabbit, etc.) that have no
|
||||
// replies endpoint.
|
||||
const repliedOk = canPostPRReviewThreadReply(replyTarget)
|
||||
? await deps.replyInThread(replyTarget, PR_COMMENT_AI_FIXING_REPLY)
|
||||
: await deps.replyAsConversation(replyTarget, PR_COMMENT_AI_FIXING_REPLY)
|
||||
if (repliedOk) {
|
||||
counts.replied += 1
|
||||
} else {
|
||||
counts.failed += 1
|
||||
const replyOk = batch.handled.has(group)
|
||||
? batch.replyOk
|
||||
: await postInThreadFixingReply(group, deps, counts)
|
||||
// Why: resolving a thread whose fixing reply never landed closes it silently —
|
||||
// the user was promised an ack, so leave it open and let the toast report the failure.
|
||||
if (!replyOk) {
|
||||
return counts
|
||||
}
|
||||
} else if (threadId == null || !deps.isThreadStillResolvable(threadId)) {
|
||||
counts.skipped += 1
|
||||
}
|
||||
|
||||
if (threadId == null || !deps.isThreadStillResolvable(threadId)) {
|
||||
if (!deps.canReply) {
|
||||
counts.skipped += 1
|
||||
}
|
||||
return counts
|
||||
}
|
||||
|
||||
@@ -253,6 +272,24 @@ async function acknowledgePRCommentGroup(
|
||||
return counts
|
||||
}
|
||||
|
||||
/** Mutates counts in place; returns whether the nested reply landed. */
|
||||
async function postInThreadFixingReply(
|
||||
group: PRCommentGroup,
|
||||
deps: PRCommentAiLaunchAckDeps,
|
||||
counts: PRCommentAiLaunchAckCounts
|
||||
): Promise<boolean> {
|
||||
const replyOk = await deps.replyInThread(
|
||||
getPRCommentGroupReplyTarget(group),
|
||||
PR_COMMENT_AI_FIXING_REPLY
|
||||
)
|
||||
if (replyOk) {
|
||||
counts.replied += 1
|
||||
} else {
|
||||
counts.failed += 1
|
||||
}
|
||||
return replyOk
|
||||
}
|
||||
|
||||
/** Durable pending ack payload so dialog close / re-renders cannot drop it before launch. */
|
||||
let pendingAiCommentAck: unknown = null
|
||||
|
||||
|
||||
@@ -10142,7 +10142,7 @@
|
||||
"653c105ecc": "More PR actions",
|
||||
"f316a8ca2b": "No unresolved comments selected.",
|
||||
"d00ebdc402": "Resolve {{value0}} Comments With AI",
|
||||
"ed3f79c031": "Review the prompt before starting an agent. After launch, Orca posts a fixing reply on each selected comment and resolves host threads when possible.",
|
||||
"ed3f79c031": "Review the prompt before starting an agent. After launch, Orca replies to the selected comments and resolves host threads when possible.",
|
||||
"f273f2271c": "Started the agent. Marked {{value0}} resolved, replied to {{value1}}, skipped {{value2}}, failed {{value3}}.{{value4}}",
|
||||
"aa95b81a3a": "Started the agent. Marked {{value0}} resolved, replied to {{value1}}, skipped {{value2}}, failed {{value3}}.",
|
||||
"495b2f8c4b": "Started the agent, but could not resolve or reply on the selected comments.",
|
||||
|
||||
@@ -10069,7 +10069,7 @@
|
||||
"653c105ecc": "Más acciones de PR",
|
||||
"f316a8ca2b": "No hay comentarios sin resolver seleccionados.",
|
||||
"d00ebdc402": "Resolver comentarios de {{value0}} con IA",
|
||||
"ed3f79c031": "Revisa el prompt antes de iniciar un agente. Tras el lanzamiento, Orca publica una respuesta de corrección en cada comentario seleccionado y resuelve los hilos en el host cuando es posible.",
|
||||
"ed3f79c031": "Revisa el prompt antes de iniciar un agente. Tras el lanzamiento, Orca responde a los comentarios seleccionados y resuelve los hilos en el host cuando es posible.",
|
||||
"f273f2271c": "Agente iniciado. Marcados como resueltos: {{value0}}; respondidos: {{value1}}; omitidos: {{value2}}; con error: {{value3}}.{{value4}}",
|
||||
"aa95b81a3a": "Agente iniciado. Marcados como resueltos: {{value0}}; respondidos: {{value1}}; omitidos: {{value2}}; con error: {{value3}}.",
|
||||
"495b2f8c4b": "Agente iniciado, pero no se pudieron resolver ni responder los comentarios seleccionados.",
|
||||
|
||||
@@ -10069,7 +10069,7 @@
|
||||
"653c105ecc": "その他の PR 操作",
|
||||
"f316a8ca2b": "未解決のコメントが選択されていません。",
|
||||
"d00ebdc402": "{{value0}} のコメントを AI で解決",
|
||||
"ed3f79c031": "agent を開始する前にプロンプトを確認してください。開始後、Orca は選択した各コメントに修正の返信を投稿し、可能な場合はホスト上のスレッドを解決します。",
|
||||
"ed3f79c031": "agent を開始する前にプロンプトを確認してください。開始後、Orca は選択したコメントに返信し、可能な場合はホスト上のスレッドを解決します。",
|
||||
"f273f2271c": "agent を開始しました。{{value0}} 件を解決済み、{{value1}} 件に返信、{{value2}} 件をスキップ、{{value3}} 件が失敗しました。{{value4}}",
|
||||
"aa95b81a3a": "agent を開始しました。{{value0}} 件を解決済み、{{value1}} 件に返信、{{value2}} 件をスキップ、{{value3}} 件が失敗しました。",
|
||||
"495b2f8c4b": "agent を開始しましたが、選択したコメントの解決または返信ができませんでした。",
|
||||
|
||||
@@ -10069,7 +10069,7 @@
|
||||
"653c105ecc": "더 많은 PR 작업",
|
||||
"f316a8ca2b": "선택한 해결되지 않은 댓글이 없습니다.",
|
||||
"d00ebdc402": "AI로 {{value0}} 댓글 해결",
|
||||
"ed3f79c031": "에이전트를 시작하기 전에 프롬프트를 확인하세요. 시작 후 Orca가 선택한 각 댓글에 수정 답글을 게시하고 가능한 경우 호스트 스레드를 해결합니다.",
|
||||
"ed3f79c031": "에이전트를 시작하기 전에 프롬프트를 확인하세요. 시작 후 Orca가 선택한 댓글에 답글을 남기고 가능한 경우 호스트 스레드를 해결합니다.",
|
||||
"f273f2271c": "agent를 시작했습니다. {{value0}}개 해결됨, {{value1}}개 답글, {{value2}}개 건너뜀, {{value3}}개 실패.{{value4}}",
|
||||
"aa95b81a3a": "agent를 시작했습니다. {{value0}}개 해결됨, {{value1}}개 답글, {{value2}}개 건너뜀, {{value3}}개 실패.",
|
||||
"495b2f8c4b": "agent를 시작했지만 선택한 댓글을 해결하거나 답글할 수 없습니다.",
|
||||
|
||||
@@ -10069,7 +10069,7 @@
|
||||
"653c105ecc": "更多 PR 操作",
|
||||
"f316a8ca2b": "未选择未解决的评论。",
|
||||
"d00ebdc402": "使用 AI 解决 {{value0}} 评论",
|
||||
"ed3f79c031": "启动 agent 前请检查提示词。启动后,Orca 会在每条选中的评论下发布修复回复,并在可能时解决主机上的讨论串。",
|
||||
"ed3f79c031": "启动 agent 前请检查提示词。启动后,Orca 会回复选中的评论,并在可能时解决主机上的讨论串。",
|
||||
"f273f2271c": "已启动智能体。已标记 {{value0}} 个为已解决,回复 {{value1}} 个,跳过 {{value2}} 个,失败 {{value3}} 个。{{value4}}",
|
||||
"aa95b81a3a": "已启动智能体。已标记 {{value0}} 个为已解决,回复 {{value1}} 个,跳过 {{value2}} 个,失败 {{value3}} 个。",
|
||||
"495b2f8c4b": "已启动智能体,但无法解决或回复选中的评论。",
|
||||
|
||||
Reference in New Issue
Block a user