Skip to content

Instantly share code, notes, and snippets.

@bregenspan
Created October 15, 2025 13:49
Show Gist options
  • Save bregenspan/bf0e9706df5b675112a7e954aba6f9ae to your computer and use it in GitHub Desktop.
Save bregenspan/bf0e9706df5b675112a7e954aba6f9ae to your computer and use it in GitHub Desktop.
Script to make reviewing large PRs in GitHub more pleasant
// @ts-check
/** GitHub PR review mode UX improvements for
reviewing large PRs. Suitable for converting
to a bookmarklet or using as a user script
that is activated on the PR review page.
Automatically advances to next file after marking
a file as viewed, outlines the active file, and
allows for keyboard use to mark file as viewed.
*/
(() => {
const CLASSNAME_ACTIVE = "br-reviewmode-active";
const STYLE_ACTIVE = `outline: 3px blue solid;`;
/** @typedef {Object} Selectors
* @property {string} attribute - Name of attribute that changes when a file is marked as viewed
* @property {string} button - Selector for the "Not Viewed" button/checkbox within a diff
* @property {string} diff - Selector for the entire diff container for a single not-yet-reviewed file
* @property {string} filesChangedContainer - Selector for top-level container around all file diff views
*/
/** New Github PR Files Changed review page (late 2025 preview)
* @type {Selectors}
*/
const SELECTORS_NEW = {
attribute: "aria-pressed",
filesChangedContainer: 'div[data-hpc="true"]', // brittle, fix this
button: 'button[aria-label="Not Viewed"]',
diff: `div[role=region][class*="Diff-module"]:has(button[aria-label="Not Viewed"])`,
};
/** Legacy review PR Files Changed review page (opted out of late 2025 preview)
* @type {Selectors}
*/
const SELECTORS_LEGACY = {
attribute: "data-file-user-viewed",
filesChangedContainer: 'div[data-target="diff-layout.mainContainer"]',
button: 'input[type="checkbox"]:not(:checked)',
diff: `.js-details-container[data-details-container-group="file"]:has(input[type="checkbox"]:not(:checked))`,
};
const selectors = document.querySelector(
SELECTORS_LEGACY.filesChangedContainer
)
? SELECTORS_LEGACY
: SELECTORS_NEW;
const filesChangedContainer = document.querySelector(
selectors.filesChangedContainer
);
if (!filesChangedContainer) {
console.warn("ReviewMode: could not find 'Files changed' container");
return;
}
const style = document.createElement("style");
style.innerHTML = `.${CLASSNAME_ACTIVE} { ${STYLE_ACTIVE} }`;
document.body.appendChild(style);
/** @type {HTMLElement | null} */
let lastDiff;
function advanceDiff() {
lastDiff?.classList.remove(CLASSNAME_ACTIVE);
/** @type {HTMLElement | null} */
const nextDiff = document.querySelector(selectors.diff);
if (!nextDiff) {
// No more to review
return;
}
nextDiff.scrollIntoView({ block: "center" });
nextDiff.classList.add(CLASSNAME_ACTIVE);
/** @type {HTMLElement | null} */
const nextButton = nextDiff.querySelector(selectors.button);
nextButton?.focus();
lastDiff = nextDiff;
}
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (
mutation.type === "attributes" &&
mutation.attributeName === selectors.attribute
) {
advanceDiff();
}
});
});
observer.observe(filesChangedContainer, {
attributes: true,
subtree: true,
});
advanceDiff();
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment