feat(ui): PR picker becomes a persistent excluder, newest-first
Build & push image / build-and-push (push) Successful in 13s
CI / test (push) Successful in 10m43s

Invert the PR scope from opt-in to exclusion: untick a PR to drop it
from the comparison; the excluded set persists in localStorage and new
PRs are included automatically as they arrive. The list is now reverse
chronological (last run/report first) with the date shown per PR, the
footer states the total count so truncation fears are checkable at a
glance, and the scrollable list is pinned with min-height:0 for
robustness.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-03 05:55:32 -04:00
parent 7fce78a664
commit e381c0ad41
2 changed files with 54 additions and 40 deletions
+47 -35
View File
@@ -29,7 +29,7 @@
#modal .mhead { display:flex; gap:8px; align-items:center; padding:10px 12px; border-bottom:1px solid var(--line); }
#modal .mhead b { margin-right:auto; white-space:nowrap; }
#modal .mhead input { flex:1; min-width:80px; width:auto; }
#mlist { overflow:auto; padding:6px 0; }
#mlist { flex:1 1 auto; min-height:0; overflow:auto; padding:6px 0; }
#mlist label.item { display:flex; gap:10px; align-items:center; padding:6px 14px; cursor:pointer; font-size:13px; }
#mlist label.item:hover { background:#1d212b; }
#mlist .note { margin-left:auto; color:var(--mut); font-size:12px; }
@@ -76,7 +76,7 @@
<div class="f"><label>from</label><input type="date" id="from"></div>
<div class="f"><label>to</label><input type="date" id="to"></div>
<div class="f"><label>repo</label><select id="repo"></select></div>
<div class="f"><label>PRs</label><button id="prbtn" onclick="openModal('pr')" title="Limit the whole comparison to a set of PRs — every model is scored only on runs/findings from them.">all PRs ▾</button></div>
<div class="f"><label>PRs</label><button id="prbtn" onclick="openModal('pr')" title="Exclude PRs from the comparison (persists in this browser). New PRs are included automatically as they arrive.">all PRs ▾</button></div>
<div class="f"><label>models shown</label><button id="modelsbtn" onclick="openModal('models')" title="Hide models from the scoreboard entirely (e.g. retired ones). Persists in this browser.">all ▾</button></div>
<div class="f"><label>provider</label><select id="provider"></select></div>
<div class="f"><label>model</label><select id="model"></select></div>
@@ -152,14 +152,23 @@ function loadHidden(){ try { return new Set(JSON.parse(localStorage.getItem("grt
let HIDDEN = loadHidden();
function saveHidden(){ localStorage.setItem("grt-hidden", JSON.stringify([...HIDDEN].sort())); }
// PRs the comparison is scoped to (repo#pr keys). Empty = every PR counts.
let SELPRS = new Set();
// PRs excluded from the comparison (repo#pr keys), persisted like HIDDEN.
// Exclusion (not opt-in) so new PRs count automatically as they arrive.
function loadXPRs(){ try { return new Set(JSON.parse(localStorage.getItem("grt-xprs") || "[]")); } catch { return new Set(); } }
let EXPRS = loadXPRs();
function saveXPRs(){ localStorage.setItem("grt-xprs", JSON.stringify([...EXPRS].sort())); }
// excluded PRs actually present in the current data (EXPRS may hold stale keys)
function excludedCount(){
const seen = new Set();
for (const r of [...RUNS, ...ROWS]){ const k = prKey(r); if (EXPRS.has(k)) seen.add(k); }
return seen.size;
}
// ---- picker modal (shared by the PR scope and the model hider) ----
// ---- picker modal (shared by the PR excluder and the model hider) ----
let modalKind = null;
function openModal(kind){
modalKind = kind;
document.getElementById("mtitle").textContent = kind==="pr" ? "compare only these PRs" : "models shown";
document.getElementById("mtitle").textContent = kind==="pr" ? "PRs compared" : "models shown";
document.getElementById("msearch").value = "";
document.getElementById("modalback").style.display = "block";
document.getElementById("modal").style.display = "flex";
@@ -172,20 +181,23 @@ function closeModal(){
document.getElementById("modal").style.display = "none";
}
// The full item list, rebuilt from the raw data on every open/refresh so it
// always has every PR / model. checked = in the comparison.
// always has every PR / model. checked = counted; untick to exclude.
function modalItems(){
const allModels = uniq([...RUNS.map(r=>r.model), ...ROWS.map(r=>r.model)]);
if (modalKind === "pr"){
const byPR = new Map();
const byPR = new Map(), last = new Map();
for (const r of [...RUNS, ...ROWS]){
const k = prKey(r);
if (!byPR.has(k)) byPR.set(k, new Set());
if (r.model) byPR.get(k).add(r.model);
const t = r.created_at || r.reported_at || "";
if (t > (last.get(k) || "")) last.set(k, t);
}
return [...byPR.keys()].sort((a,b)=>{
const [ra,pa] = splitPR(a), [rb,pb] = splitPR(b);
return ra===rb ? pb-pa : ra.localeCompare(rb); // newest PR first within a repo
}).map(k => ({ value:k, label:k, note:`${byPR.get(k).size}/${allModels.length} models`, checked:SELPRS.has(k) }));
// reverse chronological: most recently reviewed PR first
return [...byPR.keys()].sort((a,b)=> (last.get(b)||"").localeCompare(last.get(a)||""))
.map(k => ({ value:k, label:k,
note:`${byPR.get(k).size}/${allModels.length} models · ${(last.get(k)||"").slice(0,10)}`,
checked:!EXPRS.has(k) }));
}
return allModels.map(m => ({ value:m, label:m, note:HIDDEN.has(m)?"hidden":"", checked:!HIDDEN.has(m) }));
}
@@ -194,8 +206,9 @@ function visibleModalItems(){
return modalItems().filter(it => !q || it.label.toLowerCase().includes(q));
}
function fillModal(){
const items = modalItems(), vis = visibleModalItems();
const list = document.getElementById("mlist"); list.innerHTML = "";
for (const it of visibleModalItems()){
for (const it of vis){
const lab = document.createElement("label"); lab.className = "item";
const cb = document.createElement("input"); cb.type = "checkbox"; cb.checked = it.checked;
cb.onchange = ()=> modalToggle(it.value, cb.checked);
@@ -204,12 +217,14 @@ function fillModal(){
lab.append(cb, name, note);
list.appendChild(lab);
}
const off = items.filter(it=>!it.checked).length;
const counts = vis.length===items.length ? `all ${items.length}` : `${vis.length} of ${items.length}`;
document.getElementById("mfoot").textContent = modalKind==="pr"
? (SELPRS.size ? `${SELPRS.size} PR${SELPRS.size===1?"":"s"} selected — only these are compared` : "none selected — every PR counts")
: (HIDDEN.size ? `${HIDDEN.size} hidden — excluded from the scoreboard (persists in this browser)` : "all models shown");
? `showing ${counts} PRs · ` + (off ? `${off} excluded — stays excluded as new PRs arrive (persists in this browser)` : "none excluded — every PR counts, new ones included automatically")
: `showing ${counts} models · ` + (off ? `${off} hidden — excluded from the scoreboard (persists in this browser)` : "all models shown");
}
function modalToggle(v, on){
if (modalKind === "pr"){ if (on) SELPRS.add(v); else SELPRS.delete(v); }
if (modalKind === "pr"){ if (on) EXPRS.delete(v); else EXPRS.add(v); saveXPRs(); }
else {
if (on) HIDDEN.delete(v); else { HIDDEN.add(v); if (selModel===v) selModel=null; }
saveHidden();
@@ -217,14 +232,14 @@ function modalToggle(v, on){
fillModal(); render();
}
// all/none apply to the search-filtered items, so you can e.g. type a repo
// name and select all its PRs at once.
// name and exclude/restore all its PRs at once.
function modalSetAll(on){
for (const it of visibleModalItems()){
if (modalKind === "pr"){ if (on) SELPRS.add(it.value); else SELPRS.delete(it.value); }
if (modalKind === "pr"){ if (on) EXPRS.delete(it.value); else EXPRS.add(it.value); }
else if (on) HIDDEN.delete(it.value);
else { HIDDEN.add(it.value); if (selModel===it.value) selModel=null; }
}
if (modalKind !== "pr") saveHidden();
if (modalKind === "pr") saveXPRs(); else saveHidden();
fillModal(); render();
}
@@ -269,7 +284,6 @@ function opt(sel, vals, label){
if (vals.includes(cur)) sel.value = cur;
}
function prKey(o){ return o.repo + "#" + o.pr; }
function splitPR(k){ const i = k.lastIndexOf("#"); return [k.slice(0,i), +k.slice(i+1)]; }
function buildFacets(){
opt(document.getElementById("repo"), uniq([...RUNS.map(r=>r.repo), ...ROWS.map(r=>r.repo)]), "all repos");
opt(document.getElementById("provider"), uniq([...RUNS.map(r=>r.provider), ...ROWS.map(r=>r.provider)]), "all providers");
@@ -299,7 +313,6 @@ function filters(){
return {
from: document.getElementById("from").value,
to: document.getElementById("to").value,
prs: SELPRS,
repo: document.getElementById("repo").value,
provider: document.getElementById("provider").value,
model: document.getElementById("model").value,
@@ -309,13 +322,13 @@ function filters(){
};
}
function dateOK(ts, f){ const d = (ts||"").slice(0,10); return (!f.from || d >= f.from) && (!f.to || d <= f.to); }
// prOK gates a run/row on the PR picker: no selection = every PR counts,
// regardless of which models ran it.
function prOK(o, f){ return !f.prs.size || f.prs.has(prKey(o)); }
// prOK drops runs/rows from excluded PRs; everything else (including PRs that
// arrive after the exclusions were set) counts.
function prOK(o){ return !EXPRS.has(prKey(o)); }
// run-level filters only (date/repo/provider/model/pr) — severity/lens/search are finding-level.
function runMatch(r, f){
return dateOK(r.created_at, f) && (!f.repo || r.repo===f.repo) &&
(!f.provider || r.provider===f.provider) && (!f.model || r.model===f.model) && prOK(r, f);
(!f.provider || r.provider===f.provider) && (!f.model || r.model===f.model) && prOK(r);
}
function gradeMatch(row, g){
if (!g) return true;
@@ -332,7 +345,7 @@ function rowMatch(row, f){
if (f.lens && row.lens!==f.lens) return false;
if (!gradeMatch(row, f.grade)) return false;
if (f.q && !((row.title||"")+" "+(row.file||"")+" "+(row.repo||"")).toLowerCase().includes(f.q)) return false;
if (!prOK(row, f)) return false;
if (!prOK(row)) return false;
return true;
}
@@ -346,12 +359,11 @@ function aggregate(f){
const get = m => { if(!M.has(m)) M.set(m, {model:m, provider:"", runs:0, minutes:0, inTok:0, outTok:0,
findings:new Set(), confirmed:new Map(), fp:new Map(), ungraded:new Set()}); return M.get(m); };
const prsSeen = new Set();
for (const r of RUNS){ if(!runMatch(r,f)) continue; prsSeen.add(prKey(r)); const m=get(r.model); m.runs++; m.minutes += (r.duration_secs||0)/60;
for (const r of RUNS){ if(!runMatch(r,f)) continue; const m=get(r.model); m.runs++; m.minutes += (r.duration_secs||0)/60;
m.inTok += r.input_tokens||0; m.outTok += r.output_tokens||0; if(r.provider) m.provider=r.provider; }
const rows = ROWS.filter(r => rowMatch(r, f) && !HIDDEN.has(r.model));
for (const r of rows){ prsSeen.add(prKey(r)); const m=get(r.model); if(r.provider) m.provider=m.provider||r.provider;
for (const r of rows){ const m=get(r.model); if(r.provider) m.provider=m.provider||r.provider;
m.findings.add(r.finding_id);
if (r.graded && r.is_real === true){ m.confirmed.set(r.finding_id, r.severity || ""); }
else if (r.graded && r.is_real === false){ m.fp.set(r.finding_id, rawToSevKey(r.raw_severity)); }
@@ -378,7 +390,7 @@ function aggregate(f){
ptsPerRun: m.runs>0 ? points/m.runs : null,
confirmedPct: findings>0 ? confirmed/findings*100 : null };
}).filter(m => (m.runs>0 || m.findings>0) && !HIDDEN.has(m.model));
return { models: out, rows, prsSeen };
return { models: out, rows };
}
const COLS = [
@@ -395,7 +407,7 @@ const COLS = [
function render(){
const f = filters();
const { models, rows, prsSeen } = aggregate(f);
const { models, rows } = aggregate(f);
models.sort((a,b)=>{
let x=a[sortKey], y=b[sortKey];
if (sortKey==="model"||sortKey==="provider"){ x=x||""; y=y||""; return sortAsc ? x.localeCompare(y) : y.localeCompare(x); }
@@ -428,14 +440,14 @@ function render(){
mb.appendChild(tr);
}
// picker buttons reflect current scope
const exN = excludedCount();
document.getElementById("prbtn").textContent =
SELPRS.size ? `${SELPRS.size} PR${SELPRS.size===1?"":"s"}` : "all PRs ▾";
exN ? `${exN} PR${exN===1?"":"s"} excluded` : "all PRs ▾";
document.getElementById("modelsbtn").textContent =
HIDDEN.size ? `${HIDDEN.size} hidden ▾` : "all ▾";
const tot = models.reduce((a,m)=>({runs:a.runs+m.runs, min:a.min+m.minutes, find:a.find+m.findings, conf:a.conf+m.confirmed, pts:a.pts+m.points}), {runs:0,min:0,find:0,conf:0,pts:0});
const prNote = f.prs.size
? ` · <b>scoped to ${prsSeen.size} PR${prsSeen.size===1?"":"s"}</b>` : "";
const prNote = exN ? ` · <b>${exN} PR${exN===1?"":"s"} excluded</b>` : "";
document.getElementById("summary").innerHTML =
`${models.length} models · ${tot.runs} runs · ${tot.min.toFixed(0)} min · ${tot.find} findings · ${tot.conf} confirmed · ${tot.pts.toFixed(0)} pts` +
prNote +
@@ -464,7 +476,7 @@ function esc(s){ return (s==null?"":String(s)).replace(/[&<>]/g, m=>({"&":"&amp;
function resetFilters(){
for (const id of ["from","to","q"]) document.getElementById(id).value="";
for (const id of ["repo","provider","model","lens","grade"]) document.getElementById(id).value="";
SELPRS.clear(); // hidden models are a persistent preference, not a filter — reset leaves them
// excluded PRs and hidden models are persistent preferences, not filters — reset leaves them
selModel = null; render();
}