
【免费下载链接】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 仓库中的意图文档 intent-description-annotation-phase1深入讲解在 PR 描述PR Description中划选文本并发表评论这一功能的完整设计方案与落地实现读者将理解 plannotator 如何复用既有的 prose 批注引擎useAnnotationHighlighterCommentPopover把原本只读的 PR Overview 面板变成可批注、可计数、可随 Send Feedback 一起发给 agent 的评审面并掌握从划选到导出反馈 markdown 的完整调用链。一、问题背景PR Overview 是只读的评审意见只能丢进 diff 注释plannotator 的 PR 评审界面review editor中PR Overview 面板负责渲染 PR 描述description和评论时间线但两者都是只读的。评审者经常想对描述里的某段话提出具体意见——这个说法不对这里需要澄清——而此前没有任何机制能把这类反馈锚定到描述文本上只能退而求其次在 diff 行注释里绕圈子重述上下文。仓库中当时并存两套批注系统Code reviewCodeAnnotation锚定到 diff 行文件 行号 侧无法锚定 prose 文本Plan/annotateAnnotation锚定到渲染后 markdown 中选中的文本底层依赖 web-highlighterstartMeta/endMetaoriginalText。其引擎——useAnnotationHighlighterAnnotationToolbarCommentPopoverFloatingQuickLabelPicker——完全位于packages/ui没有任何 plan 专属依赖并且已经在第二处表面useHtmlAnnotation.ts作用于 iframe 之上被复用证明了它是表面无关surface-agnostic的。一个前置问题在决策阶段被识别PR 描述当时由 PRSummaryTab.tsx 使用的裁剪版MarkdownBody渲染只支持 5 种块类型heading、code、list-item、blockquote、hr没有表格/HTML 块/directive/alert 的真实渲染。而完整渲染器BlockRendererblocks/组件集与 plan 编辑器用的是同一个自研解析器parseMarkdownToBlocks会输出高亮器所依赖的data-block-idDOM 属性。完整的决策与后果分析记录在 ADR 004最终规格见 description-annotation-phase1 规格代码级验证结论见 synthesis 以及两份渲染器调研 SPIKE-renderer-migration 与 SPIKE-renderer-density-parameterization。二、核心设计决策复用 prose 引擎comment-only 模式意图文档给出的How部分可以归纳为七条设计决策全部已在当前仓库代码中落地用 prose 引擎不用CodeAnnotation。复用useAnnotationHighlighterCommentPopover不复用整个 planViewer它拖了约 50 个 plan 专属 props。升级渲染器。把 PR prose 的裁剪版MarkdownBody换成完整共享块渲染器从Viewer中抽出块分派逻辑形成小的共享组件RenderedMarkdown使 plan viewer 与 PR 面板共用同一个渲染器消除重复。这一步同时修好了 HTML/表格/alert 渲染并让文本变得可批注。comment-only 的交互风格有意比 plan/annotate 更简单划选文本后评论框直接打开没有工具栏、没有 quick-labels、没有删除红线选择器。这对应高亮器的comment模式划选 → 直接进CommentPopover。单一评论气泡 Ask AI。沿用 review editor 已在用的共享CommentPopover并把其 Ask AI 动作接到既有的无文件scope选择提问上。独立 store。新增一个Annotation[]prosestore提升到review-editor/App.tsx经由ReviewStateContext下发与既有的CodeAnnotation[]在内存中严格分离。在 Annotations 侧栏中以 PR description 分组呈现。选中和删除都从侧栏完成——高亮本身不附带任何操作入口。接入现有反馈管道零服务端改动。prose 批注计入totalAnnotationCount使 Send Feedback 出现并通过exportAnnotations追加到feedbackMarkdown走既有的/api/feedbackPOST——没有新端点。意图文档还明确了改动边界无 server、端点或 Pi-runtime 改动工作仅局限于packages/ui迁移 CSS与packages/review-editorwrapper、store、context、侧栏分组、计数/导出接线。三、实现剖析一AnnotatableDescription包装器PR 描述是一个直接的 DOM 容器没有 iframe所以可以直接挂载useAnnotationHighlighter。实现位于 AnnotatableDescription.tsx它是一个 memo 化的包装组件PR 面板用它替换原先直接渲染的RenderedMarkdownconst AnnotatableDescription React.memo(function AnnotatableDescription({ markdown, className, }: { markdown: string; className?: string; }) { 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, }); // ... });几个关键点mode: comment这是 comment-only 决策的直接落点。在该模式下划选完成时 hook 不弹工具栏而是直接设置commentPopover状态见下文 hook 分析AnnotationToolbar与FloatingQuickLabelPicker根本不渲染。React.memo包装器只在markdown描述正文或descriptionAnnotations变化时重渲染避免父组件无关重渲染时 React 去协调reconcile掉 web-highlighter 注入的mark标记。幂等的重应用组件内用一个useEffect对 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]);依赖数组里同时包含markdown当 PR 描述正文经 SSE 更新后applyAnnotations会重跑让高亮在新内容上重新绑定。规格文档把这一行为称为idempotent, so safe——重复调用无副作用。评论气泡经 portal 渲染到document.body并接入 Ask AI{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, )}askAIContext以kind: selection、label: PR description携带所划选文本这正是 HTML vieweruseHtmlAnnotation.ts路径使用的同一种无文件选择提问形态。四、实现剖析二useAnnotationHighlighter引擎内部引擎主体在 useAnnotationHighlighter.ts。理解它的几个核心机制就能理解 comment 模式为何开箱即用以及批注为何能跨重渲染存活。4.1 web-highlighter 生命周期与模式分派hook 在containerRef上构造 web-highlighter 实例排除button、.math-annotatable、.katex及.annotation-exclude等不可批注子树并把高亮渲染为mark classannotation-highlightconst highlighter new Highlighter({ $root: containerRef.current, exceptSelectors: [ .annotation-toolbar, button, .math-annotatable, .katex, ANNOTATION_EXCLUDED_SELECTOR, ], wrapTag: mark, style: { className: annotation-highlight }, });划选完成后web-highlighter 的CREATE事件按modeRef分派redline模式直接创建DELETION批注comment模式只设置commentPopover状态anchorEl、contextText截前 80 字符、selectedText全文、source与draftKey其余模式弹工具栏。comment 模式下用户提交时走handleCommentSubmit→createAnnotationFromSource(highlighter, source, AnnotationType.COMMENT, text, ...)最终调用onAddAnnotation回调把新批注交给宿主 store。批注对象由source.startMeta/source.endMetaweb-highlighter 的 DOM 位置序列化originalText所选文本引文 所在块的blockId/startOffset组成。其中blockId的求值依赖渲染器输出的data-block-id属性——这就是渲染器升级是批注前置条件的原因。4.2 幂等恢复applyAnnotations的三重回退applyAnnotationsInternal对每条批注按如下顺序尝试锚定任何一步成功即停止已高亮则跳过highlighter.getDoms(ann.id)非空、或 DOM 中已存在[data-bind-idann.id]直接return。这就是幂等的实现——memo 失效导致重跑 effect 也不会画重。meta 恢复ann.startMeta ann.endMeta时调highlighter.fromStore(...)若开启了verifyRestoredContent还会把画出的文本与originalText做空白归一化比对不匹配则拆掉错误高亮继续回退。文本搜索兜底findTextInDOM(originalText)在容器文本树中查找引文。搜索分三级单文本节点精确子串 → 跨节点拼接流 → 空白折叠 块边界补偿blockBoundaryOffsets处理浏览器选择串在块级元素之间插入空行的差异。找不到时该批注进入unanchored报告onRestoreReport并触发onRestoreMismatch什么都不画。对AnnotatableDescription而言这条回退链正是规格中风险项 #2SSE 刷新后文本锚可能失效的兜底v1 接受回退到文本搜索或丢弃。4.3 防御性细节空白引文与不可序列化选区hook 中大量注释引用的 #881 修复是 PR 描述这类真实 prose 面板特别受益的防御块边界双击会留下一个全空白的非折叠选区若放任不管会产生originalText: \n的隐形批注——它无法再锚定却会被计数和导出。引擎在CREATE事件和捕获阶段的pointerend守卫里双重拦截isBlankQuote并校验isSerializableRange边界可解析后才放行。这些守卫对所有使用方生效PR 描述面板无需自己处理。五、实现剖析三store、上下文与侧栏分组5.1 App 层 store 与处理器review-editor/App.tsx 中按规格新增了完整的状态面并与 plan 编辑器的小状态面规格引用的packages/editor/App.tsx对应行同构const [descriptionAnnotations, setDescriptionAnnotations] useStateAnnotation[]([]); const [selectedDescriptionAnnotationId, setSelectedDescriptionAnnotationId] useStatestring | null(null);配套处理器handleAddDescriptionAnnotation追加 选中 记录 undo 快照、handleSelectDescriptionAnnotation、handleDeleteDescriptionAnnotation过滤 store、修正选区并支持 undo 恢复选择。此外还有一条与 PR 元数据对齐的过滤visibleDescriptionAnnotations用proseAnnotationMatchesPr(a, prMetadata?.url)过滤保证切换 PR 时不会串批注。整个状态面经ReviewStateContext下发接口字段、provider value、useMemo依赖数组三处都要加——规格特别强调依赖数组漏项会导致 stale state并接入草稿恢复items.descriptionAnnotations合并与 Ask AI 入口handleAskAIForDescription。5.2 侧栏 PR description 分组ReviewSidebar.tsx 早已渲染第二种批注类型editorAnnotations带独立删除路径描述批注镜像该模式而非发明新合并逻辑新增 propsdescriptionAnnotations?: Annotation[]、selectedDescriptionAnnotationId、onSelectDescriptionAnnotation、onDeleteDescriptionAnnotation卡片经renderProseAnnotationCard渲染评论文本 引文originalText 作者 删除 选中态挂在editorAnnotations分组的同一位置两个计数器都要更新App 层的totalAnnotationCount决定 Send Feedback 是否出现侧栏的totalCount决定侧栏计数与空态。当前实现中后者为const totalCount annotations.length (editorAnnotations?.length ?? 0) (descriptionAnnotations?.length ?? 0) (commentAnnotations?.length ?? 0);卡片点击 →onSelectDescriptionAnnotation(id)→ hook 选中效果滚动到对应mark并加.focused类 2 秒见 hook 中scrollIntoView({ behavior: smooth, block: center })分支卡片删除 →onDeleteDescriptionAnnotation(id)→ 包装器的对账 effect 调removeHighlight(id)并从 store 移除。5.3 计数与导出把 prose 反馈送进 agent导出侧的接线在 exportFeedback.tsbuildProseFeedback负责把描述与评论批注拼进反馈文档其中描述部分以PR Description Feedback为标题输出——与意图文档中exportAnnotations(parseMarkdownToBlocks(prContext.body), …, PR Description Feedback, PR description)的语义一致按来源 PR description 而非file:line标注因为 prose 批注没有真实文件/行号exportAnnotations在没有行号时本就优雅降级。由于解析的是与渲染同一份prContext.body批注的blockId与导出用的块 id 天然对齐按blockId/startOffset排序也成立。App 层的最终计数const totalAnnotationCount allAnnotations.length visibleEditorAnnotations.length visibleDescriptionAnnotations.length visibleCommentAnnotations.length;整条链路走既有的/api/feedbackPOST无任何服务端新增。5.4 Ask AI复用无文件的 scope 选择提问意图文档要求把评论框的 Ask AI 接到既有的askAI({ scope: { kind: selection, label: PR description, text } })。规格验证后确认这不是新工作AskAIParams有一等公民的scope字段buildDefaultPromptuseAIChat.ts已经会构造带标签、无文件的selection提问Re: {label}Source:Selected text: … 问题这正是 HTML viewer 经CommentPopover的askAIContext喂入的形态。App 层只需一个handleAskAIForDescription(question)转发答案落在 AI 侧栏由于scope存在 question 上卡片自带其 PR description 上下文。规格还提到一个可选优化目前无文件提问在 AI tab 中归入 general 桶可以按question.scope?.label分组让 PR 描述的提问聚在独立标题下——nice-to-have不影响功能成立。六、实现剖析四高亮 CSS 迁移到共享主题.annotation-highlight系列样式含.deletion/.comment/.focused/:hover约 40 行原先只存在于 plan 编辑器的packages/editor/index.css。意图文档要求把它迁入共享的 packages/ui/theme.cssreview 编辑器加载的主题文件使两个编辑器共用。当前theme.css中可以看到.annotation-highlight、.annotation-highlight.comment、.annotation-highlight.focused、.annotation-highlight:hover等规则包括.light变体与 math 批注变体其中.focused依赖的--focus-highlight变量已由 review 编辑器加载的主题文件定义——所以迁移后 PR 面板的高亮视觉与 plan 编辑器完全一致。规格同时预警过渲染器从MarkdownBody换成RenderedMarkdown会改变面板间距/样式需要回填对齐。七、风险评估与验证清单规格把真实风险收敛到一条React 与 web-highlighter 标记的对抗——hook 把mark注入 React 拥有的 DOMRenderedMarkdown重渲染可能把它们冲掉。三重缓解均已在AnnotatableDescription中验证/落地React.memo包装器仅在正文/批注变化时重渲染避开无关协调useEffect按[descriptionAnnotations, markdown]重跑applyAnnotations而applyAnnotationsInternal幂等跳过已标记 id可以随意多次调用meta 失效时回退findTextInDOM(originalText)文本搜索。第二条低优先级风险是实时上下文刷新描述正文可经 SSE 变化批注的文本锚可能无法重新绑定v1 接受回退文本搜索或丢弃。既然 Ask AI 确认也是纯复用Phase 1 全部是复用——没有真正的新子系统标记持久化是唯一实打实的风险。规格给出的验证清单可作为手工验收用例划选描述文本 → 评论框立即打开无工具栏→ 添加评论 → 高亮持久卡片出现在 Annotations 侧栏 PR description 分组计数包含它Send Feedback 出现选中卡片 → 滚动/聚焦高亮删除卡片 → 高亮与条目一并消失从评论框 Ask AI → 答案显示在 AI 侧栏发送的反馈包含 PR Description Feedback 章节标记在面板重渲染 PR 上下文刷新后存活plan 编辑器与非 PR 评审不受影响。八、小结与延伸阅读Phase 1 的价值在于用一条零新子系统的路径打通了完整管道划选 → 评论 → 侧栏 → 发送。它依赖并验证了三件此前已存在的基础设施共享RenderedMarkdown输出data-block-id、表面无关的useAnnotationHighlightercomment 模式 幂等恢复 空白引文防御、以及既有反馈导出管道。PR 评论comment card 上的comment按钮被明确划入 Phase 2见 intent-comment-annotation-phase2。继续深入可阅读意图文档本体adr/intent-description-annotation-phase1-20260630-180000.md决策与后果adr/decisions/004-annotate-pr-description-and-comments-20260630-155000.md最终规格含复用映射表与预检发现adr/specs/description-annotation-phase1-20260630-171500.md核心实现AnnotatableDescription、useAnnotationHighlighter、ReviewSidebar、exportFeedback赞分享【免费下载链接】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 仓库中的架构决策记录 ADRPG Maker全系列加密档案解密技术解决方案RPG Maker全系列加密档案解密技术解决方案 RPG Maker Decrypter是一款专门用于解密RPG Maker XP、VX和VX Ace加密档案的Anarlog 就绪 PR 批量修复技能实战用 fix-ready-prs 一次性清理 CI 失败与 Bugbot 评审发现Anarlog 就绪 PR 批量修复技能实战用 fix ready prs 一次性清理 CI 失败与 Bugbot 评审发现 本文讲解 Anarlog 仓库内AI 应用人工智能语音本地部署桌面应用音频上一篇终极WebTTY安全指南如何安全地共享终端会话下一篇Lapdev预览URL功能揭秘如何安全共享HTTPS端点无需配置DNS创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考