Skip to content

Instantly share code, notes, and snippets.

@levibostian
Last active November 2, 2025 13:29
Show Gist options
  • Save levibostian/b8b398ab3bcd7fd461ad6a38d1fdb68e to your computer and use it in GitHub Desktop.
Save levibostian/b8b398ab3bcd7fd461ad6a38d1fdb68e to your computer and use it in GitHub Desktop.
tampermonkey github pull request mark file as Viewed keyboard shortcut. Found it in https://github.com/orgs/community/discussions/10197, but modified it over time because I found some bugs.
// ==UserScript==
// @name GitHub PR Review Mode Improvements. Credit: https://github.com/orgs/community/discussions/10197#discussioncomment-14710611
// @namespace http://tampermonkey.net/
// @version 1.0
// @description Automatically advances to next file after marking a file as viewed, outlines the active file, and focuses the "Viewed" checkbox for keyboard navigation
// @author You
// @match https://github.com/*
// ==/UserScript==
(function() {
'use strict';
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();
})();
// ==UserScript==
// @name GitHub PR review keyboard shortcut
// @version 0.3
// @description Mark file as "viewed" on GitHub PR UI when hovering and pressing 'Escape' key
// @match https://github.com/*
// @author dvdvdmt, nbolton
// @source https://github.com/orgs/community/discussions/10197
// ==/UserScript==
(function() {
'use strict';
if (window.disposeMarkAsViewedByEscape) {
window.disposeMarkAsViewedByEscape();
}
window.disposeMarkAsViewedByEscape = start();
function start() {
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}
function isInEditableArea(el) {
if (!el) return false;
// If the target or any ancestor is input/textarea/select
if (el.closest && el.closest('input, textarea, select')) return true;
// If the target or ancestor has contentEditable=true (or is contentEditable)
const editable = el.closest && el.closest('[contenteditable]');
if (editable) {
const ce = editable.getAttribute('contenteditable');
// treat empty string or "true" as editable; also fallback to isContentEditable
if (ce === '' || ce === 'true' || editable.isContentEditable) return true;
}
// Some GitHub editors use role="textbox"
if (el.closest && el.closest('[role="textbox"]')) return true;
return false;
}
function markFileAsViewed() {
console.debug("Marking file as viewed");
const fileElement = document.querySelector(`[id^="diff-"]:hover`);
if (!fileElement){
console.debug("No file element under cursor");
return;
}
const buttons = [...fileElement.querySelectorAll('button')];
if (buttons.length === 0) {
console.debug("No buttons found in file element");
return;
}
// GitHub sometimes localizes the label; we attempt to match the visible label
const checkbox = buttons.find(btn => btn.textContent && btn.textContent.trim() === 'Viewed');
if (!checkbox) {
console.debug("No 'Viewed' checkbox/button in file element");
return;
}
checkbox.click();
}
function handleKeyDown(event) {
// Only act on plain Escape without modifiers
if (event.key !== 'Escape' || event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) {
return;
}
// If focus is inside any editable input/textarea/contentEditable, do nothing
if (isInEditableArea(event.target)) {
return;
}
markFileAsViewed();
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment