DEV Community

wUltima Project
wUltima Project

Posted on

I built a Chrome extension that shows GitHub PR status in the tab title

I keep 5–10 GitHub PR tabs open at any given time. The problem: I can't tell which ones need my attention without clicking into each one.

So I built a small Chrome extension that solves exactly this — it prefixes the tab title with the current PR status, updated every 120 seconds.

What it looks like

Symbol Meaning
🟢 Approved — at least one approval, no pending changes
🔴 Changes requested
🟡 Awaiting review
🟣 Merged
Closed
💬N N total comments

Example tab title: 🟢 💬3 | Fix login bug · Pull Request #42 · acme/app

How it works

The extension has two main parts: a content script and a service worker.

The content script runs on every github.com/*/*/pull/* page. It reads the PR number from the URL, sends a message to the service worker, and applies the status prefix to document.title.

function parseURL() {
  const m = location.pathname.match(/^\/([^/]+)\/([^/]+)\/pull\/(\d+)/);
  if (!m) return null;
  return { owner: m[1], repo: m[2], number: parseInt(m[3], 10) };
}
Enter fullscreen mode Exit fullscreen mode

The service worker handles the GitHub API call and caches results for 60 seconds to avoid hammering the API:

async function fetchPR(owner, repo, number, token) {
  const base = `https://api.github.com/repos/${owner}/${repo}/pulls/${number}`;
  const [pull, reviews] = await Promise.all([
    ghFetch(base, token),
    ghFetch(`${base}/reviews?per_page=100`, token)
  ]);
  const state = computeState(pull, reviews);
  const comments = (pull.comments || 0) + (pull.review_comments || 0);
  return { state, comments };
}
Enter fullscreen mode Exit fullscreen mode

Handling GitHub's SPA navigation

GitHub is a single-page app — navigating between PRs doesn't reload the page. I handle this with three event listeners plus a 1-second interval fallback:

document.addEventListener('turbo:load', update);
document.addEventListener('pjax:end', update);
document.addEventListener('visibilitychange', () => {
  if (document.visibilityState === 'visible') update();
});

// Fallback for SPA URL changes that don't fire events
let lastHref = location.href;
setInterval(() => {
  if (location.href !== lastHref) {
    lastHref = location.href;
    update();
  }
}, 1000);
Enter fullscreen mode Exit fullscreen mode

Review state logic

The trickiest part: computing the "real" review state from GitHub's API. The API returns all reviews including dismissed ones and multiple reviews from the same person. I keep only the latest verdict per reviewer:

function computeState(pull, reviews) {
  if (pull.merged_at) return 'merged';
  if (pull.state === 'closed') return 'closed';

  const sorted = [...reviews].sort((a, b) =>
    Date.parse(a.submitted_at) - Date.parse(b.submitted_at)
  );

  const byUser = {};
  for (const r of sorted) {
    if (r.state === 'DISMISSED') continue;
    if (r.state === 'APPROVED' || r.state === 'CHANGES_REQUESTED') {
      byUser[r.user.login] = r.state;
    }
  }

  const verdicts = Object.values(byUser);
  if (verdicts.includes('CHANGES_REQUESTED')) return 'changes_requested';
  if (verdicts.includes('APPROVED')) return 'approved';
  return 'open';
}
Enter fullscreen mode Exit fullscreen mode

Security notes

  • Token stored in chrome.storage.local (not synced to Google account)
  • host_permissions restricted to github.com/*/*/pull/* only
  • Input validation on all message parameters before URL interpolation
  • No backend, no telemetry, no external servers

Stack

  • Manifest V3
  • Content script + service worker
  • GitHub REST API (fine-grained PAT, Pull requests: Read-only)
  • chrome.storage.local for token persistence
  • ~300 lines total

Get the source

The full source code is available for €5 on Gumroad:
👉 https://wultimaprojects.gumroad.com/l/xghli

Includes README with setup instructions and PAT generation guide.

Happy to answer questions or take feedback in the comments.

Top comments (0)