Chrome MV3 extension (TypeScript) that hides files in a GitHub pull request's Files changed tab based on regexes, and optionally auto-marks them as Viewed.
npm install
npm run build # or: npm run watchThen in Chrome: chrome://extensions → enable Developer mode →
Load unpacked → select the dist/ folder.
Alternatively, grab the zipped dist/ from the
latest release,
unzip it, and load that folder unpacked — no build step required.
Click the toolbar icon (or the ⚙ on the on-page pill). Patterns are one regex
per line; blank lines and # comments are ignored.
- Plain lines compile with the
iflag:\.spec\.[jt]sx?$ - Use
/pattern/flagsfor explicit control:/\.md$/(case-sensitive) gandyare stripped, since they make.test()stateful- Patterns are tested against the repo-relative path, e.g.
src/foo/bar.spec.ts
The options page has a live Test a path box that shows which rule matched.
The PR header shows totals for the whole diff (+1,713 −61). With this on, the
extension subtracts the filtered files and shows the remainder, keeping the
original one hover away in the tooltip. Clicking Show on the pill, or
switching the feature off, restores the original numbers.
Counts come from the screen-reader span GitHub renders alongside the visible
figure (Lines changed: 297 additions & 0 deletions) — unformatted integers,
so no locale parsing. The visible spans are a fallback.
One caveat worth knowing: the figure is computed by subtracting the files we have actually seen. On a virtualized PR, a filtered file that has never rendered hasn't been subtracted yet, so the number starts at the true total and only becomes more accurate as you scroll. It never overstates the reduction.
GitHub consolidated the diff routes under /changes in Dec 2025; /files and
/commits/:sha still resolve but redirect there, and the new experience became
the default for everyone in Jan 2026. Rather than track tab names, the extension
activates on any /pull/:n/* page where a diff root is present, and stands down
elsewhere (logging the reason). npm test covers /files, /changes,
/commits/:sha, and the Conversation tab.
Large PRs may use GitHub's experimental virtualization, where off-screen files aren't in the DOM at all. Hiding still works — the MutationObserver re-applies as rows are recycled in — but a file can only be auto-marked Viewed once it has been rendered at least once. If you need every match marked on a huge PR, scroll through, or turn virtualization off in the PR banner.
Console logging is off by default; enable it in the options. When on,
everything is prefixed [pr-diff-filter]. On a Files-changed
page you should see one line like:
[pr-diff-filter] v1.0.0 loaded on /acme/web/pull/42/files {…}
[pr-diff-filter] 37 files via path-attr, 6 matched [ … ]
If you see nothing at all, the content script never ran. In order of likelihood:
- The tab was open before you loaded the extension. Content scripts only inject on navigation — reload the tab.
- You pointed Load unpacked at the repo root instead of
dist/. chrome://extensionsis showing an Errors button on the card. Open it.
If you see no file containers resolved, the DOM changed. The warning includes
selector hit counts. probe.js in this repo prints a fuller report — paste it
into the DevTools console on the PR page.
There's also an inspection handle. Because content scripts run in an isolated world, you must switch the DevTools console context dropdown (top-left of the console, usually reading "top") to PR Diff Filter first, then:
__prdf.probe() // selector hit counts
__prdf.files() // resolved [{el, path}] plus which strategy found them
__prdf.rules() // compiled regexes
__prdf.rerun() // force a passEvery pass computes the complete set of elements that should carry a hide class,
then reconciles the DOM against it. Anything hidden by an earlier pass that the
current pass no longer resolves gets released. This matters because the diff
loads progressively: an early pass sees one file and may resolve a container too
coarsely, and without reconciliation that mistake would persist forever even as
later passes got it right. npm test asserts the self-heal directly.
These use the same class and the same regex call, so if the sidebar hides and the diff stays visible, the regex is fine — the diff container just isn't being resolved. Two reports cover that case:
[pr-diff-filter] matched in the file tree but no diff container resolved: [ … ]
[pr-diff-filter] N element(s) marked hidden but still displayed. First one: <div …
The first means discovery missed the file. The second means we hid the wrong
element — we found something, marked it, and it made no visual difference.
__prdf.explain() prints a row per file with the resolved container, its
computed display, its box size, and whether a Viewed control was found.
Anchored on the file header, not on a path attribute. In the current GitHub
diff view data-file-path lives on the "Expand all lines" button, which is
absent on files with no collapsible context — a brand-new file has no expand
button and therefore no path attribute anywhere in its markup. Anchoring on
[data-diff-header-wrapper] (with fallbacks) is what makes discovery complete.
The path is read from the header in this order: a path attribute, then the file
name link, then aria-label="Expand all lines: <path>". GitHub wraps the
displayed file name in bidi marks (‎), so all extracted text is stripped
of U+200E/U+200F/U+202A–E — otherwise every $-anchored rule silently
fails to match.
The container grows from the header: climb while the parent holds exactly one header (reliable, since every file has one), then extend across following siblings for layouts where the header and diff body are siblings.
The Viewed control in the current UI is a toggle <button aria-pressed>
labelled "Not Viewed", not a checkbox. State comes from aria-pressed, never
from the label text — the word "Viewed" appears in both states.
npm test includes a fixture built from real captured GitHub markup, covering a
file with an expand button and a new file without one.
Rather than hardcoding a container selector, the primary strategy finds any node
carrying data-file-path / data-tagsearch-path / data-path, then climbs to
the outermost ancestor still describing exactly one distinct path — stopping at
#files / [data-testid=diff-view] / main so a single-file PR can't swallow
the page. This derives the container from the data we need anyway, so it survives
class and data-testid renames. Named selectors (copilot-diff-entry,
div.js-file.file, …) are only a fallback for markup with no path attributes.
The climb is deliberately asymmetric. It always walks through a transparent wrapper (a parent whose only element child is the node), since hiding one of those can't hide anything else. It only uses the "parent describes one path" rule when two or more files are currently rendered — with a single file rendered, every ancestor up to the diff root describes one path, and trusting that blanks the entire pane.
The Viewed checkbox is likewise found by selector first, then by locating the literal text "Viewed" in the file header and walking to the nearest checkbox.
npm test runs both strategies against jsdom fixtures, including a
no-js-*-hooks fixture and a single-file PR.
- Viewed is server state. Auto-marking sends a POST to GitHub per file. It's private to you, but it does mutate real review state, and GitHub resets it when a new commit touches the file. Clicks are staggered (~150ms) rather than fired in a burst.
- Marking a file Viewed makes GitHub collapse it natively, so
hidemode plus auto-Viewed is belt-and-braces. If you'd rather keep the file headers visible as a reminder, switch mode to collapse. - If you manually un-View a file, the extension won't re-mark it in that page session.
- Hidden files never load their diffs, which meaningfully speeds up large PRs.
- The pill in the bottom-right shows the filtered count with a Show/Re-hide escape hatch.
GitHub's diff DOM changes without notice. content.ts matches several
generations of markup (copilot-diff-entry, div.js-file.file,
[data-testid=...]) and reads the path from any of data-file-path,
data-tagsearch-path, data-path, or the file link's title. File-tree
hiding is best-effort: it correlates rows via their #diff-<hash> anchor and
falls back to matching the row label. If GitHub ships a rewrite, the selector
constants at the top of content.ts are the only thing you should need to touch.
Add your host to content_scripts[0].matches in src/manifest.json, e.g.
https://github.mycorp.com/*/*/pull/*. No host_permissions entry is
needed — the extension never makes a network request of its own; it only
reads the page DOM and clicks GitHub's own "Viewed" toggle.
Replace the background block with { "scripts": ["background.js"] } and add a
browser_specific_settings.gecko.id. The rest is compatible.