ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

plannotator PR 描述标注(Phase 1)实战指南:选中即评、复用现有注释引擎为 PR 描述接入 Agent 反馈管线

plannotator PR 描述标注(Phase 1)实战指南:选中即评、复用现有注释引擎为 PR 描述接入 Agent 反馈管线 【免费下载链接】plannotatorAnnotate and review coding agent plans and code diffs visually, share with your team, send feedback to agents with one click.项目地址https://gitcode.com/gh_mirrors/pl/plannotator点击查看免费下载导读本文基于 plannotator 仓库中的 ADR 004 及其配套规格文档adr/specs/description-annotation-phase1-20260630-171500.md2026-06-30 最终修订版、经代码验证并 Greenlit系统讲解「PR 描述PR description文本标注」这一功能的设计与落地评审者选中 PR 描述中的任意文字即可立即弹出评论框发表评论或对选中文字 Ask AI评论以「PR description」分组出现在 Annotations 侧边栏计入评审总数并在 Send Feedback 时随 diff 评论一起发送给 Agent。读完本文你将掌握 plannotator 复用文本锚定注释引擎useAnnotationHighlighterCommentPopover为只读 Markdown 表面接入完整「选中 → 评论 → 侧边栏 → 导出反馈」链路的架构思路、逐项实现方案、源码证据与风险应对。一、背景为什么 PR 描述需要可标注在 plannotator 的评审工作流中PR Overview 面板会把 PR 描述以只读形式渲染出来。评审者经常想针对描述的某一段文字发表意见——「这个说法有误」「这里需要澄清」——并希望这些反馈能和 diff 评论一起到达 Agent 手里而无需离开评审页面或重新打字。此前代码评审里没有任何机制可以标注散文内容ADRadr/decisions/004-annotate-pr-description-and-comments-20260630-155000.md。仓库里其实早已存在两套注释系统这正是本功能可以全部走「复用」路线的根本原因系统锚定方式适用场景CodeAnnotation代码评审锚定到 diff 行文件 行号 side无法锚定散文Annotationplan/annotate基于 web-highlighter锚定到渲染后 Markdown 中的选中文本startMeta/endMetaoriginalText可锚定散文引擎完全位于packages/ui无 plan 专属依赖关键事实来自 ADR 004Annotation引擎——useAnnotationHighlighterAnnotationToolbarCommentPopoverFloatingQuickLabelPicker——已经通过useHtmlAnnotation.ts在第二个表面iframe 上的 HTML viewer成功复用证明它是**表面无关surface-agnostic**的。因此 ADR 004 的决策是用散文引擎而不是CodeAnnotation来标注 PR 描述且只复用「hook 三个组件」不复用承载约 50 个 plan 专属 props 的整个 planViewer。配套的渲染器前置工作也已就绪描述现在通过共享的RenderedMarkdown渲染每个 block 都带data-block-idDOM 天然可标注见规格文档开头与packages/ui/components/RenderedMarkdown.tsx的注释——它复用共享BlockRenderer表格、HTML、callouts、代码与data-block-id一并获得却不拖入 plan Viewer 的 diagram/toolbar/lightbox 机制。二、需求定义评论专用、刻意更简单规格文档开篇就把需求讲得很「平实」在 PR 描述中选中文本 →评论框立即打开→ 输入评论或对它 Ask AI→ 评论出现在Annotations 侧边栏的「PR description」分组下和 diff 评论完全一样在那里选中/编辑/删除计入评审并在 Send Feedback 时发送给 Agent。这是评论专用comment-only没有工具栏、没有快速标签quick-labels、没有删除/划线选择器redline picker。刻意比 plan/annotate 更简单——这是与 plan/annotate 展示多选项工具栏的设计分歧也是整个 Phase 1 的灵魂。对应到useAnnotationHighlighter的comment模式选中文本后直接弹出CommentPopover跳过工具栏分支。规格与 ADR 都强调两个交互风格的设计意图描述走「选中即评」而每条 PR 评论的标注则走「卡片上的小按钮」路线Phase 2本文不展开以避免与卡片自身的点击/折叠处理器冲突——冲突被设计掉了因此不是运行时风险。三、整体流程一次完整的「选中 → 反馈」旅程规格文档给出的端到端流程如下本文按此骨架逐节展开实现细节在描述中选中文本 → web-highlighter 触发 → hook 处于 comment 模式 → 直接打开 CommentPopover无工具栏 → 用户输入评论或点击 Ask AI→ 提交 → 构建 AnnotationoriginalText startMeta/endMeta高亮文本加入 descriptionAnnotations store → 卡片出现在 Annotations 侧边栏的 PR description 分组totalAnnotationCount 递增 → 出现 Send Feedback → Send Feedback → feedbackMarkdown 包含 PR Description Feedback 小节 → 走既有 /api/feedback POST一句话概括和 diff 评论相同的生命周期只不过锚定在散文上合成报告adr/research/synthesis-description-annotation-20260630-174500.md的原话。整个 Phase 1 没有真正的新子系统——评论引擎、评论框、Ask AI、侧边栏、导出、计数全部是既有机制接到一个新表面上。四、实现方案逐项拆解对应规格 Build 部分4.1 在描述上挂载注释引擎AnnotatableDescription包装器PR 描述是直接的 DOM 容器不是 iframe因此可以直接挂载useAnnotationHighlighter。规格要求新建AnnotatableDescription包装器由PRSummaryTab在RenderedMarkdown的位置渲染它PRSummaryTab保持 props 驱动包装器通过useReviewState()拉取 store。该组件在仓库中已按规格落地packages/review-editor/components/AnnotatableDescription.tsx核心骨架为export const AnnotatableDescription React.memo(function AnnotatableDescription({ markdown, className }) { const { descriptionAnnotations, selectedDescriptionAnnotationId, onAddDescriptionAnnotation, onSelectDescriptionAnnotation, onAskAIForDescription } useReviewState(); const containerRef useRefHTMLDivElement(null); const hook useAnnotationHighlighter({ containerRef, annotations: descriptionAnnotations, onAddAnnotation: onAddDescriptionAnnotation, onSelectAnnotation: onSelectDescriptionAnnotation, selectedAnnotationId: selectedDescriptionAnnotationId, mode: comment, }); // 按 store 对账高亮应用新增幂等、移除已删除例如从侧边栏删除 const prevIdsRef useRefSetstring(new Set()); useEffect(() { const ids new Set(descriptionAnnotations.map(a a.id)); for (const id of prevIdsRef.current) { if (!ids.has(id)) hook.removeHighlight(id); } hook.applyAnnotations(descriptionAnnotations); prevIdsRef.current ids; }, [descriptionAnnotations, markdown]); return ( div ref{containerRef} RenderedMarkdown markdown{markdown} className{className} / {hook.commentPopover createPortal( CommentPopover anchorEl{hook.commentPopover.anchorEl} contextText{hook.commentPopover.contextText} initialText{hook.commentPopover.initialText} isGlobal{false} allowImages{false} onSubmit{hook.handleCommentSubmit} onClose{hook.handleCommentClose} onAskAI{onAskAIForDescription} askAIContext{{ kind: selection, label: PR description, text: hook.commentPopover.selectedText ?? hook.commentPopover.contextText }} /, document.body, )} /div ); });要点与规格完全一致containerRef只包住描述区的RenderedMarkdown不包ChecksDisclosure避免把无关区域纳入标注范围。只渲染CommentPopover由hook.commentPopoverhook.handleCommentSubmit/handleCommentClose驱动不渲染AnnotationToolbar或FloatingQuickLabelPicker——comment 模式直接跳过工具栏。变更后重放useEffect以[descriptionAnnotations, markdown]为依赖重跑对账逻辑让高亮在重渲染后存活详见「风险」一节。React.memo包裹只在markdown/descriptionAnnotations变化时重渲染避免父级无关重渲染导致 React 把 web-highlighter 注入的mark洗掉。仓库实现比规格更进一步对账逻辑不仅重放applyAnnotations还维护prevIdsRef集合主动removeHighlight那些已从 store 删除的 id——规格原文只要求「重放」落地方案把「侧边栏删除 → 高亮消失」也闭环了。4.2 comment 模式的底层行为选中即弹评论框comment模式的行为来自useAnnotationHighlighterpackages/ui/hooks/useAnnotationHighlighter.ts内部的 selection 处理分支if (effectiveMode redline) { createAnnotationFromSource(highlighter, source, AnnotationType.DELETION); window.getSelection()?.removeAllRanges(); } else if (effectiveMode comment) { pendingSourceRef.current source; setCommentPopover({ anchorEl: doms[0] as HTMLElement, contextText: source.text.slice(0, 80), selectedText: source.text, source, draftKey: commentDraftTargetKey(source, source.text), }); } else if (effectiveMode quickLabel) { setQuickLabelPicker({ ... }); } else { // Selection mode — show toolbar setToolbarState({ ... }); }可见comment模式以及redline、quickLabel都直接跳过最后那个「展示工具栏」的else分支——这正是规格与合成报告声称「modecomment→setCommentPopover(...)工具栏只在else分支」的代码级印证。此外hook 里Highlighter.event.CLICK会回调onSelectAnnotation(id)从而支持点击高亮选中该条注释。4.3 Store 与状态穿透descriptionAnnotations贯穿ReviewStateContext规格要求在App.tsx增加两个 state 与一组处理器镜像 plan 编辑器那套极简表面规格引用packages/editor/App.tsx:2857-2868, 2912const [descriptionAnnotations, setDescriptionAnnotations] useStateAnnotation[]([]); const [selectedDescriptionAnnotationId, setSelectedDescriptionAnnotationId] useStatestring|null(null); // Handlers: onAddDescriptionAnnotation(ann)追加 选中、onSelectDescriptionAnnotation(id)、onDeleteDescriptionAnnotation(id)仓库实现packages/review-editor/App.tsx与规格完全对应并增加了实现细节用descriptionAnnotationsRef/selectedDescriptionAnnotationIdRef两个 ref 同步 state供回调与历史记录使用App.tsx:372-377。handleAddDescriptionAnnotation会给 annotation盖印当前 PR 的prUrl{ ...ann, prUrl: prMetadata?.url }使其在「原地切换 PR」时仍能绑定到正确的 PR同时调用reviewHistory.record({ kind: description, ... })让描述注释也进入撤销/重做历史App.tsx:3338-3354。handleDeleteDescriptionAnnotation同样记录历史并处理选中态清理App.tsx:3370-3386。规格特别强调把 store handlers 加进ReviewState接口、provider 值对象以及它的 deps 数组——那是一个巨大的useMemo漏掉一个 dep 就等于拿到过期数据。仓库中ReviewStateContext.tsx的接口声明为packages/review-editor/dock/ReviewStateContext.tsxdescriptionAnnotations: Annotation[]; selectedDescriptionAnnotationId: string | null; onAddDescriptionAnnotation: (ann: Annotation) void; onSelectDescriptionAnnotation: (id: string | null) void; onDeleteDescriptionAnnotation: (id: string) void; onAskAIForDescription?: CommentAskAIHandler;4.4 侧边栏「PR description」分组ReviewSidebar早已支持渲染第二类注释editorAnnotations带自己的删除路径——规格的策略就是镜像这个既有模式而不是发明新的合并方案。仓库实现packages/review-editor/components/ReviewSidebar.tsx把descriptionAnnotations作为可选 props 接收在既有editorAnnotations区块旁渲染「PR description」分组ReviewSidebar.tsx:739-758并新增共享的散文注释卡片外壳renderProseAnnotationCardReviewSidebar.tsx:484-533展示 scope 标签PR description、引用的原文quoteline-clamp-2截断、评审者评论经renderInlineMarkdown渲染、作者与时间、复制/删除操作CommentActions卡片点击触发onSelect选中态通过isSelected高亮边框。这里正是规格「Preflight findings」第 1 条的落点EditorAnnotationCard的类型是EditorAnnotationlabel filePath装不下散文形态的Annotation所以需要这个轻量的DescriptionAnnotationCard——仓库最终用共享的renderProseAnnotationCard同时服务描述注释与 PR 评论注释两种类型renderDescriptionAnnotationCard/renderCommentAnnotationCard两个薄调用点。侧边栏计数同样按规格第 2 条处理totalCount把descriptionAnnotations?.length也计入ReviewSidebar.tsx:292保证侧边栏数字与空状态正确。4.5 计数与导出两个计数器 feedbackMarkdown规格第 5 节要求两处totalAnnotationCountApp.tsx:1712它门控「Send Feedback」是否出现与feedbackMarkdownApp.tsx:1704都要包含描述注释。仓库实现App.tsx:3775-3794进一步演进为const feedbackMarkdown useMemo(() { const parts: string[] []; if (allAnnotations.length 0) parts.push(exportReviewFeedback(allAnnotations, prMetadata, feedbackDiffContext, prReviewScopeLabel)); if (visibleEditorAnnotations.length 0) parts.push(exportEditorAnnotations(visibleEditorAnnotations).trim()); const prose buildProseFeedback(visibleDescriptionAnnotations, visibleCommentAnnotations, prContext?.body); if (prose) parts.push(prose); return parts.length 0 ? parts.join(\n\n) : exportReviewFeedback([], prMetadata, feedbackDiffContext, prReviewScopeLabel); }, [allAnnotations, prMetadata, feedbackDiffContext, prReviewScopeLabel, visibleEditorAnnotations, visibleDescriptionAnnotations, prContext?.body, visibleCommentAnnotations]); const totalAnnotationCount allAnnotations.length visibleEditorAnnotations.length visibleDescriptionAnnotations.length visibleCommentAnnotations.length;两个计数器App.tsx的totalAnnotationCount门控 Send Feedback / 退出警告 / 平台决策与ReviewSidebar.tsx的totalCount都包含描述注释另外feedbackMarkdown只有在有代码注释时才先渲染代码评审段避免exportReviewFeedback([])在描述注释前插入「No feedback provided.」造成自相矛盾。导出聚合规格原先设想直接在feedbackMarkdown里append exportAnnotations(parseMarkdownToBlocks(prContext.body), descriptionAnnotations, [], PR Description Feedback, PR description)仓库落地时把这段逻辑收敛到buildProseFeedbackpackages/review-editor/utils/exportFeedback.ts:404-435同时处理描述注释、PR 评论注释与 artifact 注释三类散文反馈且被「发给 Agent 的 feedbackMarkdown」与「GitHub review body 种子」共享确保两处永不漂移。规格「Preflight findings」第 3 条要求的\n\n前缀拼接与prContext?.body守卫也在buildProseFeedback的if (regularDescription.length 0 descriptionBody)判断中落实。exportAnnotations本身packages/ui/utils/parser.ts:1434按blockId/startOffset排序并以# {title}开头导出——散文注释同时携带两者而 block id 之所以能与导出对得上正是因为导出与渲染用的是同一份parseMarkdownToBlocks(prContext.body)解析结果规格第 3 条的关键洞察。exportAnnotations对没有行号的注释天然优雅降级标题参数PR Description Feedback、主题参数PR description让 Agent 明确知道反馈来自「PR 描述」而非某个file:lineADR 004 Consequences 也指出散文注释没有真实的 file/line导出按来源命名而非file:line。4.6 高亮 CSS 共享化.annotation-highlight及其.deletion/.comment/.focused/:hover变体原先生存在packages/editor/index.css:118-161。规格要求把这约 40 行迁入共享的packages/ui/theme.css让两个编辑器共用--focus-highlight变量在评审编辑器加载的 theme 文件里已定义plan 编辑器里可用因此.focused是安全的。仓库已按此落地packages/ui/theme.css:1346-1388基础高亮圆角 微内边距、.deletion--destructive底色 删除线、.comment暖色调背景 --accent底边、.focused基于--focus-highlight的oklch(from ...)强调背景/阴影/底边、:hover亮度增强以及.light模式下的柔和配色。这组样式是整个文本锚定标注系统plan 编辑器、HTML viewer、PR 描述共享的视觉语言。4.7 Ask AI零新增管道复用scope选择型提问规格第 7 节也是它修订版的最大变化确认Ask AI 根本不是新工作。AskAIParams拥有一等公民的scope字段buildDefaultPromptpackages/ui/hooks/useAIChat.ts:56-98已经能构建带标签、无文件的选择型提问if (params.scope?.kind selection) { const label params.scope.label ? Re: ${params.scope.label} : Re: selected text; const source params.scope.sourcePath ? \nSource: ${params.scope.sourcePath} : ; const selection params.scope.text ? \n\nSelected text:\n\\\\n${params.scope.text}\n\\\ : ; return ${label}${source}${selection}\n\n${params.prompt}; }即生成Re: PR descriptionSelected text: … 用户问题的三段式 prompt。这正是 HTML viewer 通过CommentPopover的askAIContext喂进去的机制HtmlViewer.tsx:1254-1258的askAIContext{{ kind: selection, label: Selected HTML, text: ... }}。因此规格给出的接线方式是在 App 中加handleAskAIForDescription用askAI({ prompt: question, scope: { kind: selection, label: PR description, text: selectedText } })——不带 filePath描述区的CommentPopover以onAskAI{handleAskAIForDescription}与askAIContext{{ kind: selection, label: PR description, text: selectedText }}接线镜像HtmlViewer.tsx的做法。仓库实现与此一致App.tsx:3390-3395AnnotatableDescription.tsx中askAIContext.text回退到contextText。回答会落到 AI 侧边栏。因为scope存储在AIQuestion上问题卡片自带「PR description」上下文规格里还留了一个可选增强目前无文件提问统一归到 generalAITab.tsx:75可加一个分支按question.scope?.label分组让 PR 描述类提问聚到自己的标题下——注意这是 nice-to-have不是功能跑通的必需项。五、复用地图整个 Phase 1 都是复用规格的「Reuse map」表格是理解本功能成本边界的最佳索引adr/research/SPIKE-renderer-migration-20260630-155500.md与SPIKE-renderer-density-parameterization-20260630-160500.md两篇 spike 也已验证渲染器迁移与密度参数化需求复用零改动选中 → 评论框useAnnotationHighlighter的mode: comment评论输入 Ask AICommentPopoveronAskAI/askAIContext消费端模板HtmlViewer.tsx:295-328仅CommentPopoverportal 部分store 形态editor/App.tsx:2857-2868, 2912侧边栏第二类型模式ReviewSidebar的editorAnnotations路径选中文本 Ask AIaskAI({ scope: { kind:selection, label, text } })useAIChat.ts:78-82与HtmlViewer相同导出exportAnnotations(blocks, anns, [], title, subject)合成报告adr/research/synthesis-description-annotation-20260630-174500.md的置信度检查表逐条对照代码确认了这些 load-bearing 主张comment 模式分支、applyAnnotationsInternal幂等跳过已标注 idgetDoms/[data-bind-id]检查再从 store 经findTextInDOM回退、点击高亮选中、侧边栏双类型、App 持有prContext用于导出、文件级选择 Ask AI 存在、高亮 CSS 与--focus-highlight就绪。六、核心风险与缓解React 与 web-highlighter 的 mark 之争规格明言整个 Phase 1 唯一真实的风险是React 重渲染 vs web-highlighter 注入的markhook 把mark注入 React 渲染的 DOM而RenderedMarkdown的一次重渲染可能把它们全部洗掉。规格给出三条全部经过验证的标准缓解手段React.memo化AnnotatableDescription只在markdown/descriptionAnnotations变化时重渲染避免父级无关重渲染带来的偶然 reconciliation已在前文组件代码中体现。渲染后重放useEffect(() hook.applyAnnotations(descriptionAnnotations), [descriptionAnnotations, markdown key])——applyAnnotationsInternal是幂等的先查highlighter.getDoms(ann.id)与[data-bind-id]已标注的直接跳过useAnnotationHighlighter.ts:1118-1124所以放心频繁调用。仓库实现还进一步做了双向对账对已删除的 id 调用removeHighlight。文本搜索回退若startMeta在 DOM 变化后不再解析applyAnnotations回退到findTextInDOM(originalText)按原文重新绑定。规格要求验证「mark 能在面板重渲染与一次 PR-context SSE tick 后存活」而残留风险live-context SSE 更新改变了描述文本导致注释锚点丢失频率低v1 接受回退到文本搜索或丢弃——这正是applyAnnotations的unanchored列表机制useAnnotationHighlighter.ts:1098-1100要处理的场景。七、锁定决策与验证清单规格「Decisions locked」一节锁定了以下边界全部在仓库实现中可印证评论专用、comment模式、只渲染CommentPopover显示在 Annotations 侧边栏的「PR description」分组下选中/删除都在侧边栏完成Ask AI 复用既有scope选择型提问同 HtmlViewer回答进 AI 侧边栏并带「PR description」上下文按scope.label分组为可选项先做描述Phase 1每条评论卡片上的按钮标注comments是 Phase 2无任何 server/endpoint/Pi-runtime 改动改动仅限packages/ui共享 CSS与packages/review-editor包装器、store、context、侧边栏分组、计数/导出行。规格给出的验证清单同时也是本文读者可以手动复现的功能验收点在描述中选中文本 → 评论框立即打开无工具栏→ 添加评论 → 高亮持续存在卡片出现在 Annotations 侧边栏「PR description」分组计数包含它出现「Send Feedback」选中卡片 → 滚动并聚焦到高亮删除卡片 → 高亮与条目一并消失从评论框 Ask AI → 回答出现在 AI 侧边栏发送的反馈包含「PR Description Feedback」小节高亮在面板重渲染 PR-context 刷新后存活plan 编辑器与非 PR 评审不受影响。八、源码定位速查表关注点仓库路径规格文档本文主体adr/specs/description-annotation-phase1-20260630-171500.md决策记录 ADR 004adr/decisions/004-annotate-pr-description-and-comments-20260630-155000.md意图文档adr/intent-description-annotation-phase1-20260630-180000.md合成/置信度报告adr/research/synthesis-description-annotation-20260630-174500.md渲染器 spike前置SPIKE-renderer-migration、SPIKE-renderer-density-parameterization描述标注包装器实现packages/review-editor/components/AnnotatableDescription.tsx注释引擎 hookcomment 模式packages/ui/hooks/useAnnotationHighlighter.tsAsk AI 默认 promptscope 选择型packages/ui/hooks/useAIChat.ts共享 Markdown 渲染器data-block-idpackages/ui/components/RenderedMarkdown.tsx侧边栏「PR description」分组packages/review-editor/components/ReviewSidebar.tsx导出/计数feedbackMarkdown、totalAnnotationCountpackages/review-editor/App.tsx散文反馈导出buildProseFeedbackpackages/review-editor/utils/exportFeedback.ts文本注释导出exportAnnotationspackages/ui/utils/parser.ts共享高亮 CSSpackages/ui/theme.css状态上下文ReviewState 接口packages/review-editor/dock/ReviewStateContext.tsx结语PR 描述标注 Phase 1 是「复用优先」工程实践的典型样本渲染器前置迁移让描述 DOM 携带data-block-id而可标注随后评论引擎、评论框、Ask AI、侧边栏分组、导出与计数全部是对既有生产机制的再接线唯一新代码只有包装器 一个 store 若干 context 字段 一个侧边栏分组 两条计数/导出行 迁移的 CSS。唯一真实风险React 与 web-highlighter 的 mark 之争有标准且已被 plan 编辑器验证过的缓解方案。如果你也想为仓库里其他只读 Markdown 表面文档、HTML 页面等接入同样的「选中即评 → 侧边栏 → 反馈」能力本文给出的模式与源码路径可以作为直接的可复用模板。赞分享【免费下载链接】plannotatorAnnotate and review coding agent plans and code diffs visually, share with your team, send feedback to agents with one click.项目地址https://gitcode.com/gh_mirrors/pl/plannotator点击查看免费下载相关推荐plannotator 架构决策 004为 PR 描述与评论接入 Agent 反馈注释管线plannotator 架构决策 004为 PR 描述与评论接入 Agent 反馈注释管线 导读 本文基于 plannotator 仓库中的架构决策记录 ADplannotator PR 描述批注实战用共享 Prose 批注引擎让评审者一行划选就能评论 PR 正文plannotator PR 描述批注实战用共享 Prose 批注引擎让评审者一行划选就能评论 PR 正文 本篇基于 plannotator 仓库中的意图文档PR标题[模块] 简明描述PR标题 模块 简明描述 变更类型 功能新增 Bug修复 性能优化 代码重构 文档更新 实现细节 详细说明实现方案、算法选择、技术难点 测试步骤 1. 2游戏开发图形学上一篇破解创意断裂难题SD-PPP工具的AI绘画与专业编辑协同创新方案下一篇Figma中文插件打破语言壁垒的设计效率工具创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表