From 944fd5b7265bd5e7068bf67ca33d91b007092351 Mon Sep 17 00:00:00 2001 From: iris Date: Sat, 7 Nov 2026 10:46:02 +0100 Subject: [PATCH] swarm ui: a diff column on the pull requests card, and "approved" as its header MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The pull requests card gets a diff column, the lines a PR adds and removes (`+120 −8`), sortable on the total. The controller takes the counts from the forge's pull request list, so no call is added. The column that said "has all approvals" is now headed "approved". --- docs/2-handbook/swarm/ui.md | 5 +++-- .../packages/swarm-ui/src/pages/PrReportPage.tsx | 10 +++++++++- frontend/packages/swarm-ui/src/pages/pr-report.ts | 15 +++++++++++++++ .../packages/swarm-ui/test/pr-report.test.mjs | 11 +++++++++++ swarm-controller/src/forge/pr_report.rs | 8 ++++++++ 5 files changed, 46 insertions(+), 3 deletions(-) diff --git a/docs/2-handbook/swarm/ui.md b/docs/2-handbook/swarm/ui.md index 1fdd74d5..36ca935d 100644 --- a/docs/2-handbook/swarm/ui.md +++ b/docs/2-handbook/swarm/ui.md @@ -605,8 +605,9 @@ and base branch, and what a reviewer or merger filters on: decision, on the PR's current head, neither stale nor dismissed, and from a reviewer whose approval the rule accepts. A reviewer whose latest review asks for changes shows **changes requested**. -- **has all approvals**: the approvals meet the rule. A PR into a branch with no - protection rule has nothing to approve, so it has them all. +- **approved**: the approvals meet the rule. A PR into a branch with no + protection rule has nothing to approve, so it counts as approved. +- **diff**: the lines the PR adds and removes (`+120 −8`), as the forge counts them. - **conflicts**: the forge can't merge the PR as it stands. - **protected base**: the base branch has a protection rule. A stacked PR targets a feature branch, so filtering this to **no** lists them. diff --git a/frontend/packages/swarm-ui/src/pages/PrReportPage.tsx b/frontend/packages/swarm-ui/src/pages/PrReportPage.tsx index 9994e32b..0d8d1f1a 100644 --- a/frontend/packages/swarm-ui/src/pages/PrReportPage.tsx +++ b/frontend/packages/swarm-ui/src/pages/PrReportPage.tsx @@ -24,6 +24,8 @@ import { TableSkeleton } from "../ui/table/TableSkeleton.js"; import { ALL_REPOS, repoOptions } from "./issue-report-repo.js"; import { approvalsText, + diffSize, + diffText, hasAllApprovals, prReportUrl, yesNo, @@ -124,7 +126,7 @@ const COLUMNS: TableColumn[] = [ }, { key: "approved", - header: "has all approvals", + header: "approved", render: (r) => hasAllApprovals(r) ? : "no", sortBy: (r) => Number(hasAllApprovals(r)), @@ -156,6 +158,12 @@ const COLUMNS: TableColumn[] = [ filterValues: (r) => [r.ci ?? "none"], filterMode: "multiselect", }, + { + key: "diff", + header: "diff", + render: (r) => diffText(r), + sortBy: (r) => diffSize(r), + }, { key: "updated", header: "updated", diff --git a/frontend/packages/swarm-ui/src/pages/pr-report.ts b/frontend/packages/swarm-ui/src/pages/pr-report.ts index 6c11415f..6b8e8556 100644 --- a/frontend/packages/swarm-ui/src/pages/pr-report.ts +++ b/frontend/packages/swarm-ui/src/pages/pr-report.ts @@ -17,6 +17,9 @@ export interface PrReportRow { head_branch: string; created_unix: number | null; updated_unix: number | null; + /** Lines the PR adds and removes. */ + additions: number | null; + deletions: number | null; /** The forge's draft flag. */ wip: boolean; has_conflicts: boolean; @@ -52,6 +55,18 @@ export function hasAllApprovals(row: PrReportRow): boolean { return row.approvals >= row.required_approvals; } +/** The diff size cell: lines added and removed, `+120 −8`. A count the forge + * did not give reads as a dash. */ +export function diffText(row: PrReportRow): string { + if (row.additions === null && row.deletions === null) return "—"; + return `+${row.additions ?? 0} −${row.deletions ?? 0}`; +} + +/** What the diff column sorts on: lines changed in all. */ +export function diffSize(row: PrReportRow): number { + return (row.additions ?? 0) + (row.deletions ?? 0); +} + /** The approvals cell: how many of the rule's approvals the PR has, or a dash * when its base branch asks for none. */ export function approvalsText(row: PrReportRow): string { diff --git a/frontend/packages/swarm-ui/test/pr-report.test.mjs b/frontend/packages/swarm-ui/test/pr-report.test.mjs index 64fe22c6..a39648a2 100644 --- a/frontend/packages/swarm-ui/test/pr-report.test.mjs +++ b/frontend/packages/swarm-ui/test/pr-report.test.mjs @@ -3,6 +3,8 @@ import assert from "node:assert/strict"; import { test } from "node:test"; import { approvalsText, + diffSize, + diffText, hasAllApprovals, prReportUrl, yesNo, @@ -52,6 +54,15 @@ test("an unprotected base, or a rule asking for none, shows only what it has", ( ); }); +test("the diff reads as lines added and removed, and sorts on both", () => { + assert.equal(diffText({ additions: 120, deletions: 8 }), "+120 −8"); + assert.equal(diffText({ additions: 0, deletions: 3 }), "+0 −3"); + assert.equal(diffText({ additions: 5, deletions: null }), "+5 −0"); + assert.equal(diffText({ additions: null, deletions: null }), "—"); + assert.equal(diffSize({ additions: 120, deletions: 8 }), 128); + assert.equal(diffSize({ additions: null, deletions: null }), 0); +}); + test("a PR has all approvals when it meets the rule, and an unprotected base always does", () => { assert.equal(hasAllApprovals(row({})), false); assert.equal(hasAllApprovals(row({ approvals: 2 })), true); diff --git a/swarm-controller/src/forge/pr_report.rs b/swarm-controller/src/forge/pr_report.rs index 801572a8..3e59af59 100644 --- a/swarm-controller/src/forge/pr_report.rs +++ b/swarm-controller/src/forge/pr_report.rs @@ -41,6 +41,9 @@ pub struct PrReportRow { pub head_branch: String, pub created_unix: Option, pub updated_unix: Option, + /// Lines the PR adds and removes, as the forge counts them. + pub additions: Option, + pub deletions: Option, /// The forge's draft flag (setting a `WIP:` title is what sets it). pub wip: bool, /// The forge cannot merge the PR as it is: its head conflicts with the @@ -200,6 +203,8 @@ fn row( head_branch: head.r#ref.clone().unwrap_or_default(), created_unix: pr.created_at.as_ref().map(|t| t.unix_timestamp()), updated_unix: pr.updated_at.as_ref().map(|t| t.unix_timestamp()), + additions: pr.additions, + deletions: pr.deletions, wip: pr.draft.unwrap_or(false), has_conflicts: pr.mergeable == Some(false), targets_protected_branch: protection.is_some(), @@ -483,6 +488,8 @@ mod tests { "head": { "ref": "iris/thing", "sha": "h1" }, "created_at": "2026-11-07T08:00:00Z", "updated_at": "2026-11-07T09:00:00Z", + "additions": 120, + "deletions": 8, })) .expect("a pull request"); let status: CombinedStatus = serde_json::from_value(serde_json::json!({ @@ -504,6 +511,7 @@ mod tests { assert_eq!(r.author.as_deref(), Some("iris")); assert_eq!(r.requested_reviewers, ["argus"]); assert_eq!(r.updated_unix, Some(1_794_042_000)); + assert_eq!((r.additions, r.deletions), (Some(120), Some(8))); // The same PR into a branch no rule names has nothing to approve. let r = row("o/r", &pr, &[], &reviews, &status).expect("a row");