Skip to content

Instantly share code, notes, and snippets.

@ldthorne
Created October 23, 2025 23:19
Show Gist options
  • Select an option

  • Save ldthorne/c57db0d37e83af3fe977e2e0d10d53cd to your computer and use it in GitHub Desktop.

Select an option

Save ldthorne/c57db0d37e83af3fe977e2e0d10d53cd to your computer and use it in GitHub Desktop.
// ==UserScript==
// @name GitHub Merge Queue Tab
// @version 1.0.0
// @description Adds a "Merge queue" tab to GitHub repository navigation if merge queue is enabled
// @author @ldthorne
// @match https://github.com/*/*
// @grant GM_xmlhttpRequest
// @connect github.com
// @run-at document-end
// ==/UserScript==
(function () {
"use strict";
// Check if we're on a repository page
const pathParts = window.location.pathname.split("/").filter((p) => p);
if (pathParts.length < 2) return;
const excludedPages = [
"settings",
"users",
"orgs",
"marketplace",
"topics",
"search",
"notifications",
];
if (excludedPages.includes(pathParts[0])) return;
const owner = pathParts[0];
const repo = pathParts[1];
const repoKey = `${owner}/${repo}`;
// Flag to prevent concurrent checks
let isChecking = false;
let hasAddedTab = false;
let __isMergeQueueEnabled = null;
// Check if merge queue is enabled
async function isMergeQueueEnabled() {
if (__isMergeQueueEnabled !== null) {
return __isMergeQueueEnabled;
}
try {
// Try accessing the merge queue page directly (fastest check)
const mergeQueueUrl = `/${owner}/${repo}/queue`;
const pageResponse = await fetch(mergeQueueUrl, {
method: "HEAD",
credentials: "include",
});
// If we get a 200, the page exists
if (pageResponse.ok) {
__isMergeQueueEnabled = true;
return true;
}
// Cache false result to avoid repeated checks
__isMergeQueueEnabled = false;
return false;
} catch (error) {
console.log("Error checking merge queue status:", error);
return false;
}
}
// Function to add the merge queue tab
async function addMergeQueueTab() {
// Prevent concurrent execution
if (isChecking || hasAddedTab) return;
// Find the navigation container
const nav = document.querySelector('nav[aria-label="Repository"]');
if (!nav) return;
// Check if tab already exists
if (document.querySelector("[data-merge-queue-tab]")) {
hasAddedTab = true;
return;
}
// Set flag to prevent concurrent checks
isChecking = true;
try {
// Check if merge queue is enabled
const isEnabled = await isMergeQueueEnabled();
if (!isEnabled) {
hasAddedTab = true; // Don't keep trying
return;
}
// Find the tab list
const tabList = nav.querySelector("ul");
if (!tabList) return;
const mergeQueueUrl = `/${owner}/${repo}/queue`;
// Check if we're currently on the merge queue page
const isActive = window.location.pathname.includes("/queue/") || window.location.pathname.endsWith("/queue");
// Create the tab list item
const tabItem = document.createElement("li");
tabItem.setAttribute("data-merge-queue-tab", "true");
tabItem.className = "d-inline-flex";
// Create the link
const link = document.createElement("a");
link.href = mergeQueueUrl;
link.className = `UnderlineNav-item ${isActive ? "selected" : ""}`;
link.setAttribute("data-tab-item", "merge-queue-tab");
link.setAttribute("data-turbo-frame", "repo-content-turbo-frame");
if (isActive) {
// find other active tab, remove the aria-current attribute from it
const activeTabs = tabList.querySelectorAll(".selected");
activeTabs.forEach((tab) => {
tab.removeAttribute("aria-current");
tab.classList.remove("selected");
});
link.setAttribute("aria-current", "page");
}
// Add icon
const iconSvg = document.createElementNS(
"http://www.w3.org/2000/svg",
"svg"
);
iconSvg.setAttribute("aria-hidden", "true");
iconSvg.setAttribute("height", "16");
iconSvg.setAttribute("viewBox", "0 0 16 16");
iconSvg.setAttribute("width", "16");
iconSvg.setAttribute(
"class",
"octicon octicon-stack UnderlineNav-octicon d-none d-sm-inline"
);
iconSvg.innerHTML = `<path d="M7.122.392a1.75 1.75 0 0 1 1.756 0l5.25 3.045c.54.313.872.89.872 1.514V7.25a.75.75 0 0 1-1.5 0V5.677L8.75 8.432v6.384a1
1 0 0 1-1.502.865L.872 12.563A1.75 1.75 0 0 1 0 11.049V4.951c0-.624.332-1.2.872-1.514L7.122.392ZM7.125 1.69l4.63 2.685L7 7.133 2.245 4.375l4.63-2.685a.25.25
0 0 1 .25 0ZM1.5 11.049V5.677l5.75 3.368v6.384L1.872 12.563a.25.25 0 0 1-.372-.215Zm13.5.177v2.225a.75.75 0 0 1-1.5 0v-2.225a.75.75 0 0 1 1.5 0Z"/>`;
// Add text
const text = document.createElement("span");
text.textContent = "Merge queue";
text.setAttribute("data-content", "Merge queue");
link.appendChild(iconSvg);
link.appendChild(text);
tabItem.appendChild(link);
// Insert after Pull requests tab
const pullRequestsTab = Array.from(tabList.children).find((li) =>
li.textContent.includes("Pull requests")
);
if (pullRequestsTab) {
pullRequestsTab.after(tabItem);
} else {
tabList.appendChild(tabItem);
}
console.log("Merge queue tab added successfully");
hasAddedTab = true;
} finally {
isChecking = false;
}
}
// Add the tab initially
addMergeQueueTab();
// Debounce function to limit observer calls
let observerTimeout;
function debouncedAddTab() {
clearTimeout(observerTimeout);
observerTimeout = setTimeout(() => {
if (!hasAddedTab) {
addMergeQueueTab();
}
}, 500);
}
// Watch for navigation changes (only if we haven't added the tab yet)
const observer = new MutationObserver(() => {
if (!hasAddedTab) {
debouncedAddTab();
} else {
// Once tab is added, stop observing
observer.disconnect();
}
});
// Only observe the main content area, not the entire body
const mainContent = document.querySelector('main') || document.body;
observer.observe(mainContent, {
childList: true,
subtree: true,
});
// Listen for popstate/navigation events
window.addEventListener("popstate", () => {
// Reset flags on navigation to allow tab to be added on new page
hasAddedTab = false;
isChecking = false;
__isMergeQueueEnabled = null;
setTimeout(addMergeQueueTab, 100);
});
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment