Kilo Code Agent Manager 中的紧凑 PR Checks:按状态分桶、去重与持久化的实现方案
【免费下载链接】kilocodeKilo is the all-in-one agentic engineering platform. Build, ship, and iterate faster with the most popular open source coding agent.项目地址: https://gitcode.com/GitHub_Trending/ki/kilocode
本文围绕 Kilo Code 仓库中的规划文档 plans/pr-checks-compact-plan.md 展开,讲解 Agent Manager 的 PR 面板如何将多达数十条的 GitHub 检查(checks)从“每行一条”的原始列表,重构为按状态分桶、按严重度排序、可折叠且失败永不隐藏的紧凑视图。读完后,你可以掌握一条完整的 UI 数据链路:宿主侧ghGraphQL 数据如何解析与去重、纯函数分组模块如何组织桶序与自然排序、组件与 CSS 如何呈现,以及展开状态如何在组件重挂载(remount)之后仍然保持。
目标:35 条全绿检查不该占 35 行
规划文档开篇给出了一条核心设计原则:
A green PR with 35 checks must not cost 35 rows. Show signal, hide noise, keep everything expandable, and never hide a failure.
重构前的现状是 PRChecks.tsx 按gh返回的原始顺序逐条渲染 check,没有分组、排序或去重。当 PR 挂着几十个矩阵化 job(如test (linux)、test (windows)、unit (windows, 10/6))时,面板被大量绿色行淹没,真正失败的检查反而要靠滚动才能找到。
文档明确把 GitHub CLI(cli/cli仓库的pkg/cmd/pr/checks/aggregate.go与output.go)列为参考实现,并提炼出 8 条规则:
- 按状态桶(status bucket)分组,而不是按 app 或 workflow 分组;
- 桶的固定顺序为 failure、pending、cancelled、skipped、success;
- 桶内用“数值感知的自然序”(numeric-aware natural sort)按名称排序;
- failure 与 pending 组默认展开,success 与 skipped 组默认折叠;
- 使用计数标签,如
1 failing check、27 successful checks; - 矩阵 job 保持为独立行,不合并;
- 按“check 标识 + 最新 startedAt”去重,剔除陈旧的 rerun 记录;
- 汇总行用 tally 形式(
5 pending · 1 failing),而不是X of Y。
预期效果(来自规划文档的 Expected result 小节):
Checks 5 pending · 1 failing [Fix with Kilo] v 1 failing check Vercel - docs [link] v 5 pending checks test (linux) [link] typecheck [link] > 29 successful checks全绿时则收敛为一行折叠的35 successful checks。
数据入口:宿主侧解析、状态映射与去重
PR 面板的数据由 VS Code 扩展宿主侧从gh的 GraphQL 结果解析而来,核心逻辑在 am-pr-utils.ts。规划文档中“Host data”一节提出的三条要求,都可以在这份源码中找到对应实现:
状态映射:TIMED_OUT / STARTUP_FAILURE 算失败,EXPECTED 算 pending
checkStatus()负责把 GitHub 的状态枚举折叠为面板使用的 5 值类型CheckStatus(定义见 pr-types.ts 第 6 行):
SUCCESS、NEUTRAL→success(规划文档特别指出NEUTRAL应保留为成功,避免“中性结论”的检查被误读为异常);FAILURE、ERROR、ACTION_REQUIRED、TIMED_OUT、STARTUP_FAILURE→failure;PENDING、QUEUED、IN_PROGRESS、REQUESTED、WAITING、EXPECTED→pending;SKIPPED→skipped;CANCELLED、STALE→cancelled;- 无法识别的未知状态兜底为
pending,保证“不确定的东西不会伪装成绿色”。
按 check 标识 + 最新 startedAt 去重
checks()函数(am-pr-utils.ts 第 110–156 行)实现了文档第 7 条规则。它的做法是:
- 为每个原始 item 计算去重键:commit status 用
status:<context>,check run 用run:<name>:<workflowName>:<event>——这就是文档所说的“check 标识”(check identity); - 计算每个 item 的
started时间戳:有startedAt就用它;没有的话,仍处于活动状态(PENDING/QUEUED 等)的记录取+Infinity,其余取-Infinity,保证活动中的重跑总是压过陈旧的完成记录; - 相同键保留
started最大的一条,最后按原始index排序输出,保持用户熟悉的相对顺序。
这样,一次失败后重跑成功的 workflow 不会留下两条记录;反之,一次陈旧的成功 rerun 也不会覆盖新的失败。
汇总语义保持不变
规划文档强调“Keep aggregate counts unchanged. Grouping is a view concern only.”。summarize()(am-pr-utils.ts 第 158–167 行)在去重之后计算total / passed / failed / pending / status,其中 skipped 不计入 total、cancelled计入 failed——这些聚合值继续供徽章(badge)与上层编排使用,分组只发生在视图层,不改动这一层。
纯分组模块:DOM-free、可单测
分组逻辑独立为 pr-check-groups.ts——这正是规划文档“Pure grouping module”一节要求新增的文件。整个模块只有 3 个导出函数和一个桶类型,没有任何 DOM 依赖:
export type CheckBucket = "failure" | "pending" | "cancelled" | "skipped" | "success" const ORDER: CheckBucket[] = ["failure", "pending", "cancelled", "skipped", "success"] const SORT = new Intl.Collator(undefined, { numeric: true, sensitivity: "base" }) export function groups(checks: PRCheck[]): CheckGroup[] { return ORDER.flatMap((bucket) => { const values = checks .filter((check) => check.status === bucket) .slice() .sort((a, b) => SORT.compare(a.name, b.name) || SORT.compare(a.url ?? "", b.url ?? "")) return values.length > 0 ? [{ bucket, checks: values }] : [] }) }几个实现细节值得注意:
- 单一共享的 Collator:
Intl.Collator(undefined, { numeric: true })在模块级创建一次(实际还加了sensitivity: "base"),numeric: true让unit (windows, 2/6)排在unit (windows, 10/6)前面——这是文档第 3 条“natural numeric-aware name comparison”的直接落地,也是矩阵 job 命名(1/6…10/6)排序正确的前提; - 空桶被剔除:
flatMap中空桶返回[],所以一个全绿 PR 只产生一个success组; - 二级排序键:名称相同时按
url排序,保证同名不同链接的 job 顺序稳定; - 默认展开规则
expands(bucket):除success与skipped外一律默认展开,即 failure/pending/cancelled 三个“需要行动”的桶默认打开——落实了“never hide a failure”; - 计数数据
counts(checks):直接复用groups(),为本地化摘要提供每个桶的数量。
对应的单测 pr-check-groups.test.ts 覆盖了规划文档“Verification”小节的三项要求:桶顺序["failure", "pending", "skipped", "success"]、自然排序(2/6在10/6前)、以及expands()对各桶的默认展开断言。
组件层:PRChecks.tsx 的分组渲染
PRChecks.tsx 消费上述纯模块。与文档对照,几个关键实现点如下:
- SectionHeading 承载 tally:
count()memo 用counts()取各桶数量,按tallyLabel本地化后以分隔符连接。这里有一个额外的信号优先处理:只要存在任何非 success 的桶,汇总行就只显示信号桶(如5 pending · 1 failing),全绿时才退化为35 successful checks。样式上通过am-pr-checks-count-${status}类(定义于 pr-panel.css 第 184–191 行)给失败/等待状态着不同颜色; - 组标题可点击展开:每组是一个
<button>,aria-expanded反映当前展开态,chevron 图标随状态切换,符合文档“Keep group headings visually below the main Checks section heading”与“one status signal per row”的意图规则; - 行内不再重复状态文字:每行只剩图标 + 名称 + 可选时长 + 外链按钮,状态完全由
data-status驱动图标着色(CSS 中.am-pr-panel-check-item[data-status="failure"] .am-pr-check-icon等规则),即文档所说“Remove duplicated per-row status words. Improve density by removing duplicate content, not by shrinking type.”; Fix with Kilo保持在检查组之上:checkFeedback()生成的修复建议按钮渲染在组列表之前,对应文档“KeepFix with Kiloabove the check groups”;- 内滚动的行阈值:
data-scrollable={checks.length > 12}在超过约 12 行时才给容器加滚动类([data-scrollable="true"]样式见 pr-panel.css 第 847 行),落实“Add an inner scrollbar only after a sensible row-count threshold is reached”; - 长名称与窄面板:名称走
am-pr-check-name的省略号截断,外链收进带 Tooltip 的小按钮,避免在窄面板中换行挤掉控件。
矩阵 job 按文档要求保持独立行:分组只发生在桶维度,桶内部逐条列出,不做任何合并。
展开状态持久化:跨 remount 存活
文档的“Persistence”一节要求把 checks 区的展开态与每个桶的用户覆盖态存进 pr-comment-state.ts,并按 worktree 为键。该模块的注释解释了动机:任何一次短暂拿不到gh状态的轮询、worktree 重选、侧边栏切换都会重挂载 PR 面板,组件局部状态会随 remount 死亡。
CommentState接口为此扩展了两个字段(pr-comment-state.ts 第 36–37 行):
checksOpen: boolean // Checks 整区是否展开 checkGroups: Record<string, boolean> // 每个桶的用户覆盖PRChecks.tsx 中的解析优先级是三级回落:
const groupOpen = (bucket) => state()?.checkGroups[bucket] ?? localGroups()[bucket] ?? expands(bucket)即“持久化用户覆盖 > 无 worktreeId 时的局部信号 > 模块默认展开规则”。切换组时,有worktreeId就写patchCommentState(持久化),否则退回写局部信号。toggleOpen对 Checks 整区做同样处理。这样就实现了文档的两条要求:“Make every collapse reversible in one click and preserve it across remounts”——用户在 worktree A 折叠的 success 组,切走再回来时依旧折叠。
本地化与样式约束
规划文档还给出了一批“意图规则”(UI preferences),约束实现不得另起炉灶:
- 复用 kilo-ui 组件与既有
pr-panel.css类,不新增内联样式或裸 hex 色值——PRChecks.tsx 中确实只从@kilocode/kilo-ui引入 Button/Icon/Spinner/Tooltip,图标全部来自图标注册表(circle-check、circle-x-outline、stop、circle-ban-sign、chevron-down等); - 所有用户可见文案——组标题(
1 failing check/27 successful checks这类带{{count}}占位符的 one/other 复数形式)、行内状态标签、汇总分隔符、外链 Tooltip——都走useLanguage()的t()与agentManager.pr.checks.*键(见 PRChecks.tsx 中GROUP_KEYS/TALLY_KEYS两张键表),并需补进 Agent Manager 的全部 locale; - 样式取值“从现有样式表与设计令牌中取具体值,而不是发明固定值”,例如滚动阈值 12 与组标题的字号层级都锚定在既有
pr-panel.css的规则上。
验证清单与明确不做的事
规划文档的 Verification 一节给出可执行清单,可作为后续维护该功能的回归基线:
- 单测覆盖分组、自然排序、状态映射、去重(pr-check-groups.test.ts 已覆盖前三项的分组侧);
- 测试断言成功组默认折叠、失败组默认展开、展开态在 remount 后保持;
- 在
packages/kilo-vscode/下运行bun run compile、bun run test:unit、bun run lint、bun run knip以及 Agent Manager 架构测试; - 用同一视口与代表性 check 数据截取前后对比截图。
文档的 Out of scope 一节同样值得引用,它划定了这条方案的边界,避免读者误期待:
- 必需检查徽章(required badges)——需要额外一次 GraphQL 请求;
- 按 workflow 分组——GitHub 在 Checks 页签用它,但 merge box 场景不用;
- 把矩阵 job 合并成一行——保持逐行,牺牲少量行数换取定位失败的精度。
小结:一条“视图关切”的完整链路
这套紧凑 PR checks 方案值得借鉴的地方在于职责切分得非常干净:去重与状态折叠在宿主侧 am-pr-utils.ts 完成(数据关切);桶序、自然排序、默认展开、计数在 DOM-free 的 pr-check-groups.ts 完成(可单测的逻辑关切);渲染、截断、内滚动阈值在 PRChecks.tsx 与 pr-panel.css(呈现关切);展开态持久化按 worktree 键控在 pr-comment-state.ts(状态关切)。聚合计数始终不随分组变化,失败与等待永远不会被默认隐藏——这正是文档开篇那句原则在每一层代码里的具体投影。
【免费下载链接】kilocodeKilo is the all-in-one agentic engineering platform. Build, ship, and iterate faster with the most popular open source coding agent.项目地址: https://gitcode.com/GitHub_Trending/ki/kilocode
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考