From 154bd44d97c46d0c1f7950b0609cd65ac1ce686e Mon Sep 17 00:00:00 2001 From: Hamhire Hu Date: Tue, 8 Sep 2026 21:43:26 +0800 Subject: [PATCH 1/3] feat(comments): reference a comment into the agent conversation A reviewer''s comment is usually a claim -- is this actually a problem, does this path really run, was it handled elsewhere -- and settling it means reading the code the comment points at. That is what the agent is for, but the only way to hand it one was to paste the text into the chat box, dropping the anchor and the authorship on the way. The reference button attaches the comment as implicit context for the next question, so "is this right?" is a complete question. No IPC change was needed: agent:ask already carries referencedContext for the diff selection, and the two compose, since asking whether a comment is right about a selected region needs both. Cross-pane state goes in a store for the same reason the diff selection does: the comment surfaces and ChatPane are siblings with no common owner below App. Both comment surfaces get it, identically -- toggle to release, a chip with a clear button, released on send, cleared on PR switch. DraftZone does not: a draft is a comment not yet written by anyone else, so there is no claim to investigate. The glyph is the existing ShareIcon rather than something agent-flavoured, because that is already what "reference" looks like in this app (the finding reference button); one gesture should not have two icons. The prompt says what to do with the comment, not just what it says. Without that instruction a model tends to paraphrase the comment back instead of checking it. Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 1 + CHANGELOG.zh-CN.md | 1 + .../src/components/features/chat/ChatPane.tsx | 38 ++++++- .../features/chat/components/ChatInputBar.tsx | 25 +++++ .../features/pr/tabs/comments/CommentItem.tsx | 38 +++++++ .../inline-comments/InlineCommentZone.tsx | 41 ++++++- .../src/renderer/src/i18n/locales/de-DE.json | 5 + .../src/renderer/src/i18n/locales/en-US.json | 5 + .../src/renderer/src/i18n/locales/ja-JP.json | 5 + .../src/renderer/src/i18n/locales/zh-CN.json | 5 + .../src/stores/comment-reference-store.ts | 105 ++++++++++++++++++ .../styles/features/diff/comment-zone.scss | 33 ++++-- .../src/styles/features/pr/comments.scss | 19 ++++ 13 files changed, 310 insertions(+), 11 deletions(-) create mode 100644 apps/desktop/src/renderer/src/stores/comment-reference-store.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 7642b774..7d650d87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and the versioning follows [Semantic Versioning](https://semver.org/). ### ✨ Added +- A comment can now be handed to the agent to investigate: click the reference button on any comment and ask about it, and the agent gets the comment, its author and the code it points at, then checks the claim against the actual code. - The code editor is now Monaco 0.56.0 (from 0.55.1), which also drops the outdated sanitizer copy behind most of the open dependency advisories. - The bundled review engine is now pr-agent 0.45.0 (from 0.39.0). Two long-standing local fixes are no longer needed — a single-line file change is rendered correctly upstream now, and a binary file no longer has to be worked around — and its YAML handling is more tolerant of imperfect model output. - Mentions now render as a pill instead of blending into the surrounding text, so it is obvious at a glance when someone is named — on the activity page, in the inline diff comments, in drafts, and in the PR description alike. diff --git a/CHANGELOG.zh-CN.md b/CHANGELOG.zh-CN.md index 8f0c9dc8..8faa77f9 100644 --- a/CHANGELOG.zh-CN.md +++ b/CHANGELOG.zh-CN.md @@ -9,6 +9,7 @@ ### ✨ 新增 +- 现在可以把评论交给 Agent 调查:在任意评论上点击引用按钮再提问,Agent 会拿到评论原文、作者及其指向的代码,对照真实代码核查该说法是否成立。 - 代码编辑器升级至 Monaco 0.56.0(原 0.55.1),同时替换掉了此前多数依赖安全告警所指向的过时清理库副本。 - 内置评审引擎升级至 pr-agent 0.45.0(原 0.39.0)。两处长期存在的本地修补不再需要——单行文件变更在上游已渲染正确,二进制文件也无需再绕开——其 YAML 解析对不规范的模型输出也更宽容。 - @提及 改为胶囊标签展示,不再淹没在正文里,一眼即可看出点到了谁——活动页、内联 diff 评论、草稿与 PR 描述一致生效。 diff --git a/apps/desktop/src/renderer/src/components/features/chat/ChatPane.tsx b/apps/desktop/src/renderer/src/components/features/chat/ChatPane.tsx index df25119c..4c85572a 100644 --- a/apps/desktop/src/renderer/src/components/features/chat/ChatPane.tsx +++ b/apps/desktop/src/renderer/src/components/features/chat/ChatPane.tsx @@ -14,6 +14,12 @@ import { ChatIcon, TrashIcon, ConfirmModal, PaneLoading } from '../../common'; import { useChatRunStore } from '../../../stores/chat-run-store'; import { useDraftsForPr } from '../../../stores/drafts-store'; import { useFindingClosuresForPr } from '../../../stores/finding-closures-store'; +import { + commentReferenceStore, + commentReferenceLabel, + formatCommentReference, + useCommentReference, +} from '../../../stores/comment-reference-store'; import { formatReferencedContext, selectionStore, @@ -160,6 +166,7 @@ export function ChatPane({ // On PR switch, clear the reference state and reset the detached state to avoid cross-PR residue. useEffect(() => { setRefFinding(null); + commentReferenceStore.clear(); setScopeDetached(false); }, [prLocalId]); // On switching to another commit (or clearing the view scope), reset the detached state: the newly selected commit becomes the implicit scope again. @@ -172,9 +179,19 @@ export function ChatPane({ // Diff selection (belonging to the current PR): used for the input bar's "N lines selected" badge + carrying the selected code as implicit context into a question. const { selection: diffSelection, ignored: selectionIgnored } = useDiffSelection(prLocalId); - // When not ignored, format the selection into a reference string; shared by /ask and natural-language questions. Ignored / no selection → undefined (this message carries no reference). + // Comment referenced from a comment surface (activity panel / inline diff zone), carried as implicit context so the + // question can be about the comment without restating it. + const commentRef = useCommentReference(prLocalId); + // Implicit context for this message: the diff selection and the referenced comment are independent sources and + // compose — asking "is this comment right about the code I selected?" needs both. Each block is self-describing, so + // concatenating them needs no framing. Undefined when neither is present (the message carries no reference). const referencedContext = - diffSelection && !selectionIgnored ? formatReferencedContext(diffSelection) : undefined; + [ + diffSelection && !selectionIgnored ? formatReferencedContext(diffSelection) : null, + commentRef ? formatCommentReference(commentRef) : null, + ] + .filter(Boolean) + .join('\n\n') || undefined; // The effective scope for this PR's chat commands: follows the commit selected in the Diff view, unless the user has detached (scopeDetached). // Only one scope may be in effect at a time — when a Diff selection exists it takes precedence (finer-grained), the commit scope is suspended and its chip @@ -500,6 +517,10 @@ export function ChatPane({ undefined, tool === 'ask' ? undefined : (effectiveScope ?? undefined), ); + // Release the referenced comment once carried: a reference is per-question, so leaving it attached would + // silently prepend the same comment to every later message. The diff selection is deliberately *not* + // released here — it stays visible in the editor, so keeping it is what the user sees and expects. + if (tool === 'ask') commentReferenceStore.clear(); }} onAgentAsk={(q) => { if (refFinding) { @@ -512,6 +533,7 @@ export function ChatPane({ return; } void actions.handleAgentAsk(q, referencedContext); + commentReferenceStore.clear(); }} onCancel={hasMyActive || agentRunningHere ? actions.handleStopAll : undefined} onSetReviewStatus={onSetReviewStatus} @@ -537,6 +559,18 @@ export function ChatPane({ } : null } + // Referenced comment: chip showing ": " + clear. Attached from a comment's reference button; + // carried as implicit context with the next question, and released on send (see onSend). + commentChip={ + commentRef + ? { + label: commentReferenceLabel(commentRef), + onClear: () => { + commentReferenceStore.clear(); + }, + } + : null + } // Single-commit scope chip: shown when a commit is selected in the view (the selected state comes from the view); click to toggle enabled/disabled — // when disabled (scopeDetached) commands revert to the full PR and the chip greys out; switching to another commit or switching PR resets it to enabled. // Only one scope at a time: when a Diff selection exists it yields to the selection chip (hides this chip), auto-restored after the selection is cleared. diff --git a/apps/desktop/src/renderer/src/components/features/chat/components/ChatInputBar.tsx b/apps/desktop/src/renderer/src/components/features/chat/components/ChatInputBar.tsx index 06d5979a..839296bb 100644 --- a/apps/desktop/src/renderer/src/components/features/chat/components/ChatInputBar.tsx +++ b/apps/desktop/src/renderer/src/components/features/chat/components/ChatInputBar.tsx @@ -12,6 +12,7 @@ import { FileTreeIcon, SendIcon, StopIcon, + ShareIcon, } from '../../../common'; import { useChatInput } from '../hooks/useChatInput'; import { useTextareaAutosizeDrag } from '../hooks/useTextareaAutosizeDrag'; @@ -55,6 +56,8 @@ interface ChatInputBarProps { onToggleSelection: () => void; /** Re-review reference chip: shows "re-review " + clear when a finding is referenced; null = not rendered. */ referenceChip?: { label: string; onClear: () => void } | null; + /** Referenced-comment chip: shows ": " + clear when a comment is referenced; null = not rendered. */ + commentChip?: { label: string; onClear: () => void } | null; /** * Single-commit scope chip: follows the commit selected in the Diff view, showing "short SHA · subject". Shown whenever there is * a selection, click to **toggle enable/disable** (disabling does not remove the chip, this session's commands revert to the whole @@ -89,6 +92,7 @@ export function ChatInputBar({ selectionIgnored, onToggleSelection, referenceChip, + commentChip, commitScopeChip, }: ChatInputBarProps) { const { t } = useTranslation(); @@ -287,6 +291,27 @@ export function ChatInputBar({ )} + {/* Referenced comment chip: the comment is carried as implicit context for this question. Its own chip rather + than sharing the re-review one — that reference changes what the run *does* (produces a verdict), while + this only adds context, and the two can be attached at once. */} + {commentChip && ( + <> +