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:
Jinjing
2026-08-02 17:19:34 -07:00
parent 2b8adee369
commit 786c82c32a
8 changed files with 259 additions and 75 deletions
@@ -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
+1 -1
View File
@@ -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.",
+1 -1
View File
@@ -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.",
+1 -1
View File
@@ -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 を開始しましたが、選択したコメントの解決または返信ができませんでした。",
+1 -1
View File
@@ -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를 시작했지만 선택한 댓글을 해결하거나 답글할 수 없습니다.",
+1 -1
View File
@@ -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": "已启动智能体,但无法解决或回复选中的评论。",