Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
20 kB · 617 lines
JavaScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618// PR Review Cockpit — renderer.//// Owns the <webview> grid, focus mode, sorting, and the focused-PR toolbar.//// PERF DESIGN (documented for future me):// - Every PR gets a tile, but a tile's <webview> src is LAZY: we only attach// the real github.com src the first time a tile becomes visible/live. Until// then the tile shows a clickable placeholder. This keeps startup cheap even// with 30+ PRs.// - In GRID mode we load webviews eagerly-on-first-render but capped: the first// N tiles auto-load, the rest wait for a click (see EAGER_LOAD_COUNT). This// is a balance — enough live previews to be useful, not 50 Chromium tabs.// - In FOCUS mode only the focused webview is live. The other PRs collapse to// header-only "mini-strips" in the left rail (their <webview> body is hidden// via CSS but the element — and thus its loaded state — is preserved, so// switching focus back is instant if it was loaded before).
"use strict";
const EAGER_LOAD_COUNT = 6; // how many grid tiles auto-load their webview
const stage = document.getElementById("stage");const meta = document.getElementById("meta");const statusEl = document.getElementById("status");const backBtn = document.getElementById("backBtn");const focusToolbar = document.getElementById("focusToolbar");const focusLabel = document.getElementById("focusLabel");const colsSelect = document.getElementById("colsSelect");const sortSelect = document.getElementById("sortSelect");const jsInput = document.getElementById("jsInput");const jsResult = document.getElementById("jsResult");const reviewBody = document.getElementById("reviewBody");const approveBtn = document.getElementById("approveBtn");const reqChangesBtn = document.getElementById("reqChangesBtn");const commentBtn = document.getElementById("commentBtn");
const state = { prs: [], loaded: new Set(), // PR numbers whose webview src is attached tiles: new Map(), // PR number -> { el, header, webview, placeholder } focused: null, // focused PR number or null cols: 3, sort: "priority",};
// --- helpers -----------------------------------------------------------------
// A short "what are we looking at" label for the top-right meta area.function scopeLabel(config) { if (!config) return ""; const who = config.author ? `${config.repo} · @${config.author}` : `${config.repo} · all open`; return config.onlyToday ? `${who} · started today` : who;}
function setStatus(msg, isError = false) { statusEl.textContent = msg || ""; statusEl.classList.toggle("error", !!isError); if (msg && !isError) { clearTimeout(setStatus._t); setStatus._t = setTimeout(() => { if (statusEl.textContent === msg) statusEl.textContent = ""; }, 3500); }}
// --- settledness -------------------------------------------------------------// A short human label + css-class for "where is this PR in its lifecycle?"// Order matters: the first matching rule wins.function settledness(pr) { const ci = pr.ciStatus; const rd = pr.reviewDecision; const mg = pr.mergeable;
if (pr.isDraft) return { label: "draft", cls: "draft" }; if (mg === "CONFLICTING") return { label: "conflicting", cls: "conflicting" }; if (rd === "CHANGES_REQUESTED") return { label: "changes req", cls: "changes-req" }; if (ci === "fail") return { label: "ci failing", cls: "ci-failing" }; if (rd === "APPROVED" && mg === "MERGEABLE" && ci === "ok") return { label: "ready to merge", cls: "ready-merge" }; if ( mg === "MERGEABLE" && (ci === "ok" || ci === "run") && rd === "REVIEW_REQUIRED" ) return { label: "ready for review", cls: "ready-review" }; if (mg === "UNKNOWN") return { label: "stale?", cls: "stale" }; return { label: "open", cls: "open" };}
// --- review priority ---------------------------------------------------------// Higher score = should be reviewed sooner. Tiebreak by updatedAt desc.function reviewPriority(pr) { const teammate = !pr.isMine; const ci = pr.ciStatus; const rd = pr.reviewDecision; const mg = pr.mergeable; const mergeableCi = ci === "ok" || ci === "run";
if (teammate && !pr.isDraft && mg === "MERGEABLE" && mergeableCi && rd === "REVIEW_REQUIRED") return 100; // their PR is waiting on review if (teammate && !pr.isDraft && mg === "MERGEABLE" && ci === "fail") return 90; // teammate, mergeable, CI red (e.g. security) if (pr.isMine && !pr.isDraft && mg === "MERGEABLE" && ci === "ok") return 80; // my ready-to-land if (pr.isMine && !pr.isDraft && ci === "fail") return 70; // my CI red — fix if (!pr.isDraft && (mg === "CONFLICTING" || rd === "CHANGES_REQUESTED")) return 60; // needs work if (pr.isDraft) return 30; if (mg === "UNKNOWN") return 10; // stale return 40;}
function updatedDesc(a, b) { // newer updatedAt first; missing timestamps sort last return (b.updatedAt || "").localeCompare(a.updatedAt || "");}
function sortPRs(prs, mode) { const arr = [...prs]; if (mode === "priority") { arr.sort( (a, b) => reviewPriority(b) - reviewPriority(a) || updatedDesc(a, b) ); } else if (mode === "newest") { arr.sort((a, b) => b.number - a.number); } else if (mode === "status") { const rank = { fail: 0, run: 1, none: 2, ok: 3 }; arr.sort( (a, b) => (rank[a.ciStatus] ?? 9) - (rank[b.ciStatus] ?? 9) || b.number - a.number ); } else if (mode === "draft-last") { arr.sort( (a, b) => Number(a.isDraft) - Number(b.isDraft) || b.number - a.number ); } return arr;}
// --- status hue injection into the PR webview --------------------------------// We inject CSS into each PR's <webview> so the live GitHub page itself takes on// its CI-status hue — the WHOLE pr reads as green/yellow/red, not just the tile// chrome. mix-blend-mode:color shifts hue while preserving luminance, so text// stays readable. Bump TINT_ALPHA for a stronger shift.
const TINT_ALPHA = 0.16; // 0..1 — how hard the PR page takes its status hue
const STATUS_HUE = { ok: { c: "#3fb950", glow: "rgba(63,185,80,0.14)" }, run: { c: "#d29922", glow: "rgba(210,153,34,0.14)" }, fail: { c: "#f85149", glow: "rgba(248,81,73,0.16)" }, none: { c: "#6e7681", glow: "rgba(110,118,129,0.10)" },};
function statusTintCSS(ciStatus) { const s = STATUS_HUE[ciStatus] || STATUS_HUE.none; // neutral gets only a frame — a gray "color" blend would desaturate the page. const hueLayer = ciStatus === "none" ? "" : `html::before{content:"";position:fixed;inset:0;pointer-events:none;` + `z-index:2147483646;background:${s.c};mix-blend-mode:color;` + `opacity:${TINT_ALPHA};}`; const frame = `html::after{content:"";position:fixed;inset:0;pointer-events:none;` + `z-index:2147483647;box-shadow:inset 0 0 0 2px ${s.c},inset 0 0 20px ${s.glow};}`; return hueLayer + frame;}
// Attach a dom-ready handler that (re)injects the tint on every load/navigation.function injectStatusTint(webview, ciStatus) { webview.addEventListener("dom-ready", async () => { try { if (webview._tintKey) { await webview.removeInsertedCSS(webview._tintKey).catch(() => {}); } webview._tintKey = await webview.insertCSS(statusTintCSS(ciStatus)); } catch { /* webview not ready / navigated away — ignore */ } });}
// --- tile construction -------------------------------------------------------
function makeTile(pr) { const el = document.createElement("div"); el.className = "tile"; el.classList.add(`st-${pr.ciStatus}`); // color-code the whole tile by CI status el.dataset.number = String(pr.number);
const header = document.createElement("div"); header.className = `tile-header st-${pr.ciStatus}`;
const num = document.createElement("span"); num.className = "num"; num.textContent = `#${pr.number}`;
const title = document.createElement("span"); title.className = "title"; title.textContent = pr.title; title.title = pr.title;
const badges = document.createElement("span"); badges.className = "badges";
// settledness pill — the headline "where is this PR?" marker. const settle = settledness(pr); const pill = document.createElement("span"); pill.className = `pill settle-${settle.cls}`; pill.textContent = settle.label; badges.appendChild(pill);
// author login (+ MINE marker for my own PRs) const author = document.createElement("span"); author.className = "badge author" + (pr.isMine ? " mine" : ""); author.textContent = pr.isMine ? `MINE · ${pr.authorLogin}` : pr.authorLogin; author.title = pr.authorLogin; badges.appendChild(author);
if (pr.approved) { const a = document.createElement("span"); a.className = "badge"; a.textContent = "✓ approved"; badges.appendChild(a); }
header.append(num, title, badges); header.addEventListener("click", () => enterFocus(pr.number));
const body = document.createElement("div"); body.className = "tile-body";
// webview element (created up front, src attached lazily) const webview = document.createElement("webview"); webview.setAttribute("partition", "persist:github"); // shared login session webview.setAttribute("allowpopups", "true"); body.appendChild(webview); injectStatusTint(webview, pr.ciStatus); // hue-shift the live PR page by status
const placeholder = document.createElement("div"); placeholder.className = "placeholder"; const big = document.createElement("div"); big.className = "big"; big.textContent = `#${pr.number}`; const hint = document.createElement("div"); hint.textContent = "click to load PR"; placeholder.append(big, hint); placeholder.addEventListener("click", (e) => { e.stopPropagation(); loadWebview(pr.number); }); body.appendChild(placeholder);
el.append(header, body);
const rec = { el, header, webview, placeholder }; state.tiles.set(pr.number, rec); return rec;}
function loadWebview(number) { const rec = state.tiles.get(number); const pr = state.prs.find((p) => p.number === number); if (!rec || !pr) return; if (!state.loaded.has(number)) { rec.webview.setAttribute("src", pr.url); state.loaded.add(number); } rec.placeholder.classList.add("hidden");}
// --- rendering ---------------------------------------------------------------
function render() { stage.style.setProperty("--cols", String(state.cols));
if (state.focused == null) { renderGrid(); } else { renderFocus(); }}
function renderGrid() { stage.className = "grid"; backBtn.classList.add("hidden"); focusToolbar.classList.add("hidden"); stage.replaceChildren();
const sorted = sortPRs(state.prs, state.sort); sorted.forEach((pr, i) => { const rec = state.tiles.get(pr.number) || makeTile(pr); rec.el.classList.remove("mini", "focused"); // restore placeholder visibility for tiles not yet loaded if (!state.loaded.has(pr.number)) { rec.placeholder.classList.remove("hidden"); } stage.appendChild(rec.el); // eager-load the first few for instant usefulness if (i < EAGER_LOAD_COUNT) loadWebview(pr.number); });}
function renderFocus() { stage.className = "focus"; backBtn.classList.remove("hidden"); focusToolbar.classList.remove("hidden"); stage.replaceChildren();
const rail = document.createElement("div"); rail.className = "rail"; const mainPane = document.createElement("div"); mainPane.className = "main-pane";
const sorted = sortPRs(state.prs, state.sort); for (const pr of sorted) { const rec = state.tiles.get(pr.number) || makeTile(pr); if (pr.number === state.focused) { rec.el.classList.add("focused"); rec.el.classList.remove("mini"); mainPane.appendChild(rec.el); loadWebview(pr.number); } else { rec.el.classList.add("mini"); rec.el.classList.remove("focused"); rail.appendChild(rec.el); } }
stage.append(rail, mainPane);
const focusedPr = state.prs.find((p) => p.number === state.focused); focusLabel.textContent = focusedPr ? `#${focusedPr.number} · ${focusedPr.title}` : "";}
// --- focus mode transitions --------------------------------------------------
function enterFocus(number) { state.focused = number; render();}
function exitFocus() { state.focused = null; render();}
function focusedWebview() { if (state.focused == null) return null; const rec = state.tiles.get(state.focused); return rec ? rec.webview : null;}
// --- toolbar wiring ----------------------------------------------------------
document.getElementById("openExtBtn").addEventListener("click", async () => { const pr = state.prs.find((p) => p.number === state.focused); if (!pr) return; await window.cockpit.openExternal(pr.url);});
document.getElementById("reloadBtn").addEventListener("click", () => { const wv = focusedWebview(); if (wv) { try { wv.reload(); } catch (e) { setStatus("Reload failed: " + e.message, true); } }});
async function runJs() { const wv = focusedWebview(); if (!wv) return; const code = jsInput.value.trim(); if (!code) return; jsResult.textContent = "running…"; try { const result = await wv.executeJavaScript(code, false); let out; try { out = JSON.stringify(result); } catch { out = String(result); } jsResult.textContent = "→ " + (out ?? "undefined"); } catch (e) { jsResult.textContent = "✗ " + e.message; }}
document.getElementById("runJsBtn").addEventListener("click", runJs);jsInput.addEventListener("keydown", (e) => { if (e.key === "Enter") runJs();});
// --- real GitHub review actions (via gh CLI, not the webview) ----------------// These POST publicly to GitHub, so every one asks for an explicit confirm that// names the action + PR number first. The buttons disable while a request is in// flight so a double-click can't fire twice.
const REVIEW_BTNS = [approveBtn, reqChangesBtn, commentBtn];
function setActionsBusy(busy) { for (const b of REVIEW_BTNS) b.disabled = busy; reviewBody.disabled = busy;}
// kind: "approve" | "request-changes" | "comment"async function fireReviewAction(kind) { if (state.focused == null) return; const number = state.focused; const body = reviewBody.value.trim();
// request-changes and comment require a non-empty body. if ((kind === "request-changes" || kind === "comment") && !body) { setStatus( kind === "comment" ? "Comment needs a body — type it in the review field first." : "Request-changes needs a body — type it in the review field first.", true ); reviewBody.focus(); return; }
const verb = kind === "approve" ? "Approve" : kind === "request-changes" ? "Request changes on" : "Comment on"; // Real, public GitHub action — require an explicit confirm. const bodyNote = body ? `\n\nBody:\n${body}` : ""; if (!window.confirm(`${verb} #${number}?${bodyNote}`)) return;
setActionsBusy(true); setStatus(`${verb} #${number}…`); let res; try { res = await window.cockpit.prAction({ kind, number, body }); } catch (e) { setStatus(`Action failed: ${e.message}`, true); setActionsBusy(false); return; } setActionsBusy(false);
if (!res || !res.ok) { setStatus(`Action failed: ${(res && res.error) || "unknown"}`, true); return; } setStatus(`${verb} #${number} ✓ ${res.output || "done"}`); if (kind !== "approve" || body) reviewBody.value = "";}
approveBtn.addEventListener("click", () => fireReviewAction("approve"));reqChangesBtn.addEventListener("click", () => fireReviewAction("request-changes"));commentBtn.addEventListener("click", () => fireReviewAction("comment"));
backBtn.addEventListener("click", exitFocus);document.addEventListener("keydown", (e) => { if (e.key === "Escape" && state.focused != null) exitFocus();});
colsSelect.addEventListener("change", () => { state.cols = Number(colsSelect.value); render();});sortSelect.addEventListener("change", () => { state.sort = sortSelect.value; render();});
document.getElementById("refreshBtn").addEventListener("click", refresh);
// --- GitHub auth: sign in once (Path B) + import from Chrome (Path A) ---------
// Reload every tile whose webview is live (after a login changes the session).function reloadAllWebviews() { for (const [number, rec] of state.tiles) { if (state.loaded.has(number) && rec.webview) { try { rec.webview.reload(); } catch { /* ignore a webview that isn't ready */ } } }}
// Path B — open a full-window GitHub login in the SAME persist:github session// the tiles share, so signing in once here authenticates (and persists for)// every tile and every future launch.function openGithubLogin() { if (document.getElementById("loginOverlay")) return;
const overlay = document.createElement("div"); overlay.id = "loginOverlay";
const bar = document.createElement("div"); bar.className = "login-bar";
const titleEl = document.createElement("span"); titleEl.className = "login-title"; titleEl.textContent = "Sign in to GitHub";
const hint = document.createElement("span"); hint.className = "login-hint"; hint.textContent = "This login is shared by every PR tile and is remembered across restarts.";
const spacer = document.createElement("span"); spacer.className = "spacer";
const done = document.createElement("button"); done.textContent = "✓ Done — reload PRs"; done.addEventListener("click", () => { overlay.remove(); reloadAllWebviews(); setStatus("Signed in — reloaded tiles."); });
bar.append(titleEl, hint, spacer, done);
const wv = document.createElement("webview"); wv.className = "login-webview"; wv.setAttribute("partition", "persist:github"); wv.setAttribute("allowpopups", "true"); wv.setAttribute("src", "https://github.com/login");
overlay.append(bar, wv); document.body.appendChild(overlay);}
document.getElementById("signinBtn").addEventListener("click", openGithubLogin);
// Path A — pull the existing github.com login out of Chrome via the main// process (gated at runtime by the macOS keychain prompt). Degrades gracefully// if the Chrome-import glue isn't wired into main.js yet.document.getElementById("syncBtn").addEventListener("click", async () => { if (!window.cockpit || typeof window.cockpit.syncGithub !== "function") { setStatus("Chrome import isn't available in this build.", true); return; } setStatus("Importing your GitHub login from Chrome…"); let res; try { res = await window.cockpit.syncGithub(); } catch (e) { setStatus("Chrome import failed: " + e.message, true); return; } if (!res || !res.ok) { setStatus("Chrome import failed: " + ((res && res.error) || "unknown"), true); return; } setStatus( `Imported ${res.set}/${res.total} cookies from Chrome “${res.profile}”. Reloading…` ); reloadAllWebviews();});
// --- data load ---------------------------------------------------------------
async function refresh() { setStatus("Loading PRs via gh…"); const res = await window.cockpit.listPRs(); if (!res.ok) { setStatus(res.error, true); meta.textContent = res.config ? scopeLabel(res.config) : ""; return; }
// Reset tile cache (PRs may have changed). Keep it simple: rebuild. state.tiles.clear(); state.loaded.clear(); state.focused = null; state.prs = res.prs;
meta.textContent = `${scopeLabel(res.config)} · ${res.prs.length} PRs`; setStatus(res.prs.length ? "" : "No open PRs found."); render();}
// In the horizontal band (grid mode), translate a vertical mouse wheel into// sideways scroll so mouse users can move through columns (trackpads already// swipe horizontally; hold Shift or scroll horizontally to bypass).stage.addEventListener( "wheel", (e) => { if (state.focused != null) return; // focus mode scrolls normally if (e.shiftKey || e.deltaY === 0) return; stage.scrollLeft += e.deltaY; e.preventDefault(); }, { passive: false });
// bootrefresh();