Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save warrenseine/6e9d82d593137ae49a0681fa05a4ebb9 to your computer and use it in GitHub Desktop.

Select an option

Save warrenseine/6e9d82d593137ae49a0681fa05a4ebb9 to your computer and use it in GitHub Desktop.
Open GitHub pull requests in Linear
// ==UserScript==
// @name Open GitHub pull requests in Linear
// @namespace http://tampermonkey.net/
// @version 2026-10-15
// @description Open GitHub pull requests in Linear
// @author Warren Seine
// @match https://github.com/*
// @match https://linear.app/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=github.com
// @run-at document-start
// @grant none
// ==/UserScript==
(function() {
'use strict';
// Only these organisations review in Linear. Pull requests anywhere else stay
// on GitHub.
const OWNERS = new Set(['heypearl-ai']);
// The conversation and the diff, e.g. /owner/repo/pull/123 or …/123/changes.
// Commits and checks have no Linear counterpart, so those tabs stay on GitHub.
const PULL_REQUEST = /^\/([^/]+)\/([^/]+)\/pull\/(\d+)(?:\/(?:changes|files))?\/?$/;
// Append ?stay to a pull request URL to read it on GitHub instead.
const STAY = 'stay';
function reviewedInLinear(url) {
const parts = PULL_REQUEST.exec(url.pathname);
return url.origin === 'https://github.com' && parts && OWNERS.has(parts[1].toLowerCase()) ? parts : null;
}
if (document.location.origin === 'https://linear.app') {
/*
* Linear links out to GitHub for what it does not show itself, through
* noreferrer links the GitHub side cannot recognise. Mark those links so
* following one does not bounce straight back to Linear.
*/
function markStay(href) {
try {
const url = new URL(href, document.location.href);
if (!reviewedInLinear(url) || url.searchParams.has(STAY)) return null;
url.search += `${url.search ? '&' : '?'}${STAY}`;
return url.href;
} catch (error) {
return null;
}
}
// contextmenu runs before the browser menu reads the link, so "Open link in
// new tab" picks up the mark too.
for (const type of ['click', 'auxclick', 'contextmenu']) {
document.addEventListener(type, event => {
const link = event.target.closest && event.target.closest('a[href]');
const href = link && markStay(link.href);
if (href) link.href = href;
}, true);
}
// Keyboard shortcuts click a link that is never attached to the page, so
// no event reaches the document.
const click = HTMLElement.prototype.click;
HTMLAnchorElement.prototype.click = function() {
const href = markStay(this.href);
if (href) this.href = href;
return click.call(this);
};
return;
}
// A fragment points at something inside the pull request: a comment, a
// thread, a line. Linear has no matching anchor, so the reader would land at
// the top of the review rather than at what they clicked.
const ANCHORED = () => document.location.hash.length > 1;
// GitHub drops the query and the fragment as the reader moves around, and the
// reader would then be redirected out of the page they deliberately opened.
// So the decision to stay is remembered for the pull request, for as long as
// the tab lives.
const REMEMBERED = 'github-pull-request-stay:';
let last = null;
function remember(key) {
// Storage is unavailable in some browser configurations; the query and
// fragment checks still hold on their own there.
try {
sessionStorage.setItem(key, '1');
} catch (error) {
/* ignore */
}
}
function remembered(key) {
try {
return sessionStorage.getItem(key) !== null;
} catch (error) {
return false;
}
}
function redirect() {
const href = document.location.href;
if (href === last) return;
last = href;
const parts = reviewedInLinear(document.location);
if (!parts) return;
const [, owner, repo, number] = parts;
const key = `${REMEMBERED}${owner}/${repo}/${number}`.toLowerCase();
if (new URLSearchParams(document.location.search).has(STAY) || ANCHORED()) {
remember(key);
return;
}
if (remembered(key)) return;
// Linear resolves a review from the GitHub path and redirects to its own
// URL. Replace rather than assign: the pull request drops out of the
// history, so going back does not redirect again.
document.location.replace(`https://linear.app/review/${owner}/${repo}/pull/${number}`);
}
// GitHub opens pull requests by client-side navigation as often as by page
// load, so watch the URL instead of checking it once.
setInterval(redirect, 300);
redirect();
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment