diff --git a/src/mergify.js b/src/mergify.js
index d3f89fb..305a57e 100644
--- a/src/mergify.js
+++ b/src/mergify.js
@@ -61,6 +61,48 @@ export function resetForNavigation() {
lastPullRequestUrl = null;
}
+function injectRowIntoMergeBox() {
+ // New merge box — the mergebox-partial may be inside discussion-timeline-actions
+ // or a separate element in the page (GitHub layout varies)
+ const mergeBoxPartial = document.querySelector(
+ '[data-testid="mergebox-partial"]',
+ );
+ if (mergeBoxPartial) {
+ const mergeBoxContainer =
+ mergeBoxPartial.querySelector(".border.rounded-2");
+ if (mergeBoxContainer) {
+ mergeBoxContainer.appendChild(buildMergifyRow());
+ debug("Mergify section injected inside merge box container");
+ return true;
+ }
+ }
+
+ let detailSection = document.querySelector(
+ "div[class=discussion-timeline-actions]",
+ );
+ if (detailSection) {
+ const mergeBoxContainer = detailSection.querySelector(
+ ".merge-pr .border.rounded-2",
+ );
+ if (mergeBoxContainer) {
+ mergeBoxContainer.appendChild(buildMergifyRow());
+ debug("Mergify section injected inside merge-pr container");
+ return true;
+ }
+ debug("Merge box container not found yet, waiting for render");
+ return false;
+ }
+
+ // Classic merge box
+ detailSection = document.querySelector(".mergeability-details");
+ if (detailSection) {
+ detailSection.insertBefore(buildMergifyRow(), detailSection.firstChild);
+ debug("Mergify section injected in classic merge box");
+ return true;
+ }
+ return false;
+}
+
async function _tryInject() {
if (!isGitHubPullRequestPage()) {
// SPA-navigated away from a PR (e.g., back to /pulls). The
@@ -81,70 +123,48 @@ async function _tryInject() {
lastPullRequestUrl = currentUrl;
}
- const hasMergifyConfiguration = await getMergifyConfigurationStatus();
- if (!isMergifyEnabledOnTheRepo(hasMergifyConfiguration)) {
+ // Try synchronous Mergify-enabled detection first (Mergify app icon in
+ // DOM, or cached repo result). Only fall back to the /search fetch when
+ // we have no synchronous signal — that fetch is the single biggest
+ // contributor to first-row latency on cold loads.
+ let mergifyEnabled = isMergifyEnabledOnTheRepo(false);
+ if (!mergifyEnabled) {
+ const hasMergifyConfiguration = await getMergifyConfigurationStatus();
+ mergifyEnabled = isMergifyEnabledOnTheRepo(hasMergifyConfiguration);
+ }
+ if (!mergifyEnabled) {
debug("Mergify is not enabled on the repo");
return;
}
const _data = getPullRequestData();
- await renderMergifyContext({
+ const contextPayload = {
org: _data.org,
repo: _data.repo,
number: Number.parseInt(_data.pull, 10),
subpath: _data.subpath,
- });
+ };
const existingRow = document.querySelector("#mergify");
if (existingRow) {
updateMergifyRow(existingRow);
+ void renderMergifyContext(contextPayload);
return;
}
- // Fetch queue state before first render (only once per PR page visit)
- await fetchQueueStateIfNeeded();
+ // Inject the row immediately with state derived synchronously from the
+ // DOM. The queue-state and stack-context fetches run in parallel in the
+ // background — when fetchQueueStateIfNeeded resolves, we re-derive the
+ // button state and swap it in if it changed.
+ const injected = injectRowIntoMergeBox();
- // New merge box — the mergebox-partial may be inside discussion-timeline-actions
- // or a separate element in the page (GitHub layout varies)
- const mergeBoxPartial = document.querySelector(
- '[data-testid="mergebox-partial"]',
- );
- if (mergeBoxPartial) {
- const mergeBoxContainer =
- mergeBoxPartial.querySelector(".border.rounded-2");
- if (mergeBoxContainer) {
- mergeBoxContainer.appendChild(buildMergifyRow());
- debug("Mergify section injected inside merge box container");
- scheduleQueueStatePoll();
- return;
- }
- }
+ void renderMergifyContext(contextPayload);
+ void fetchQueueStateIfNeeded().then(() => {
+ const row = document.querySelector("#mergify");
+ if (row) updateMergifyRow(row);
+ });
- let detailSection = document.querySelector(
- "div[class=discussion-timeline-actions]",
- );
- if (detailSection) {
- // Fallback: look for merge box inside discussion-timeline-actions
- const mergeBoxContainer = detailSection.querySelector(
- ".merge-pr .border.rounded-2",
- );
- if (mergeBoxContainer) {
- mergeBoxContainer.appendChild(buildMergifyRow());
- debug("Mergify section injected inside merge-pr container");
- scheduleQueueStatePoll();
- } else {
- debug("Merge box container not found yet, waiting for render");
- }
- return;
- }
- // Classic merge box
- detailSection = document.querySelector(".mergeability-details");
- if (detailSection) {
- detailSection.insertBefore(buildMergifyRow(), detailSection.firstChild);
- debug("Mergify section injected in classic merge box");
- scheduleQueueStatePoll();
- return;
- }
+ if (injected) scheduleQueueStatePoll();
}
function tryInject() {
diff --git a/src/queue.js b/src/queue.js
index 508ff30..f81d939 100644
--- a/src/queue.js
+++ b/src/queue.js
@@ -4,6 +4,10 @@ import { getPullRequestData, isGitHubPullRequestPage } from "./dom.js";
import { getLogoSvg, parseSvg } from "./logo.js";
import { resetStackState } from "./stacks.js";
+const QUEUE_ICON_SVG = ``;
+
+const LOGS_ICON_SVG = ``;
+
export const BUTTONS = [
{
command: "refresh",
@@ -365,7 +369,9 @@ export function buildQueueButton(state) {
case "dequeuing":
btn = buildMergeBoxButton(
state === "queuing" ? "queue" : "dequeue",
- state === "queuing" ? "Queuing…" : "Dequeuing…",
+ state === "queuing"
+ ? "Adding to merge queue…"
+ : "Removing from merge queue…",
"Waiting for Mergify to process…",
true,
"secondary",
@@ -374,7 +380,7 @@ export function buildQueueButton(state) {
case "queued":
btn = buildMergeBoxButton(
"dequeue",
- "Dequeue",
+ "Remove from merge queue",
"Remove the pull request from the merge queue",
false,
"danger",
@@ -384,7 +390,7 @@ export function buildQueueButton(state) {
default:
btn = buildMergeBoxButton(
"queue",
- "Queue",
+ "Add to merge queue",
"Add the pull request to the merge queue",
false,
"primary",
@@ -411,76 +417,72 @@ export function buildMergifyRow() {
const row = document.createElement("div");
row.id = "mergify";
+ row.className =
+ "bgColor-muted borderColor-muted border-top rounded-bottom-2";
row.style.cssText =
- "padding:10px 16px;border-top:1px solid var(--borderColor-default, #30363d);display:flex;align-items:center;gap:10px;";
+ "display:flex;align-items:center;gap:8px;padding:var(--base-size-16,16px)!important;";
- const logo = document.createElement("div");
- logo.style.cssText = "flex-shrink:0;display:flex;";
- const svgEl = parseSvg(getLogoSvg());
- svgEl.setAttribute("width", "20");
- svgEl.setAttribute("height", "20");
- logo.appendChild(svgEl);
- row.appendChild(logo);
+ if (state !== "merged" && state !== "closed") {
+ const buttons = document.createElement("div");
+ buttons.style.cssText = "display:flex;gap:6px;";
+ buttons.appendChild(buildQueueButton(state));
+ for (const btn of BUTTONS) {
+ buttons.appendChild(
+ buildMergeBoxButton(
+ btn.command,
+ btn.label,
+ btn.tooltip,
+ false,
+ "secondary",
+ ),
+ );
+ }
+ row.appendChild(buttons);
+ }
const info = document.createElement("div");
info.style.cssText =
- "flex:1;display:flex;align-items:center;gap:6px;font-size:13px;";
-
- const label = document.createElement("span");
- label.style.fontWeight = "600";
- label.textContent = "Mergify";
- info.appendChild(label);
-
- function appendDot() {
- const dot = document.createElement("span");
- dot.style.color = "var(--fgColor-muted, #7d8590)";
- dot.textContent = "·";
- info.appendChild(dot);
- }
+ "display:flex;align-items:center;gap:12px;font-size:14px;margin-left:auto;";
- function appendLink(href, text) {
- appendDot();
+ function appendLink(href, text, iconSvg) {
const link = document.createElement("a");
link.href = href;
link.target = "_blank";
link.rel = "noopener noreferrer";
- link.textContent = text;
link.style.cssText =
- "color:var(--fgColor-accent, #58a6ff);text-decoration:none;font-size:12px;";
+ "color:var(--fgColor-accent, #58a6ff);text-decoration:none;display:inline-flex;align-items:center;gap:4px;";
+ link.appendChild(parseSvg(iconSvg));
+ link.appendChild(document.createTextNode(text));
info.appendChild(link);
}
- appendLink(getMergeQueueLink(), "queue");
- appendLink(getEventLogLink(), "logs");
-
- row.appendChild(info);
+ appendLink(getMergeQueueLink(), "queue", QUEUE_ICON_SVG);
+ appendLink(getEventLogLink(), "logs", LOGS_ICON_SVG);
if (state === "merged") {
const status = document.createElement("span");
- status.style.cssText =
- "color:var(--fgColor-muted, #7d8590);font-size:13px;";
+ status.style.color = "var(--fgColor-muted, #7d8590)";
status.textContent = getMergedMessage();
- row.appendChild(status);
- } else if (state !== "closed") {
- const buttons = document.createElement("div");
- buttons.style.cssText = "display:flex;gap:6px;";
-
- buttons.appendChild(buildQueueButton(state));
+ info.appendChild(status);
+ }
- for (const btn of BUTTONS) {
- buttons.appendChild(
- buildMergeBoxButton(
- btn.command,
- btn.label,
- btn.tooltip,
- false,
- "secondary",
- ),
- );
- }
+ const brand = document.createElement("a");
+ brand.href = "https://dashboard.mergify.com";
+ brand.target = "_blank";
+ brand.rel = "noopener noreferrer";
+ brand.title = "Open Mergify dashboard";
+ brand.style.cssText =
+ "display:flex;align-items:center;gap:6px;margin-left:8px;color:var(--fgColor-default, #e6edf3);text-decoration:none;font-weight:600;";
+ const svgEl = parseSvg(getLogoSvg());
+ svgEl.setAttribute("width", "20");
+ svgEl.setAttribute("height", "20");
+ brand.appendChild(svgEl);
+ const brandText = document.createElement("span");
+ brandText.textContent = "Mergify";
+ brand.appendChild(brandText);
+ info.appendChild(brand);
- row.appendChild(buttons);
- }
+ row.appendChild(info);
return row;
}