A tiny native macOS menu bar app that watches your GitHub pull requests so you stop alt-tabbing to github.com. It shows, in a dropdown from the menu bar:
- Needs my review: PRs where you were requested as a reviewer.
- My open PRs: your PRs across any org, with CI status (✅ / ❌ / 🟡) and review decision (👍 approved / ✋ changes requested).
- New on my repos: PRs opened by other people on repos you own.
It polls GitHub every 60 seconds and fires a desktop notification when something changes on a PR you care about:
- a new PR lands on one of your repos,
- someone reviews or comments on your PR,
- CI passes / fails / starts running,
- a review decision flips (approved / changes requested).
Clicking a PR in the menu (or clicking a notification) opens it in your browser.
Install
Grab the latest PR Widget <version>.dmg from the
Releases page, open it,
and drag PR Widget into Applications.
Every pushed v* tag builds a fresh DMG in CI and attaches it to the release.
Opening an unsigned app on macOS
PR Widget is ad-hoc signed but not notarized by Apple, so macOS Gatekeeper blocks it on first launch with a message like "PR Widget can't be opened because Apple cannot check it for malicious software." This is expected — here's how to run it anyway.
macOS Sequoia (15) and later — right-click → Open no longer works for un-notarized apps; you have to approve it in Settings:
- Try to open PR Widget once (double-click). You'll get the blocked dialog — click Done.
- Open → System Settings → Privacy & Security.
- Scroll to the Security section. You'll see "PR Widget was blocked to protect your Mac." Click Open Anyway.
- Confirm with Open Anyway and authenticate with Touch ID / your password. The app launches and macOS remembers the choice.
macOS Sonoma (14) and earlier:
- In Applications, right-click (or Control-click) PR Widget → Open.
- In the dialog, click Open again. macOS remembers the choice from then on.
Terminal fallback (any version). If the app still won't open, strip the quarantine flag it picked up from being downloaded:
xattr -dr com.apple.quarantine "/Applications/PR Widget.app"Then launch it normally. This is only needed once, right after installing.
Why the warning? The app isn't code-signed with a paid Apple Developer ID or notarized, so Gatekeeper can't verify a known developer. Building from source with
./run.shsidesteps this entirely.
Build & run
./run.shThat builds the app, packages it into PR Widget.app, ad-hoc code-signs it
(so notifications are allowed), and launches it. Look at the top-right of your
menu bar for the pull-request icon.
To rebuild after code changes:
./run.sh --rebuildFirst launch: connect GitHub
On first launch it asks for a GitHub Personal Access Token. Create one at:
- Classic token → https://github.com/settings/tokens/new
- Scopes:
repoandread:org
- Scopes:
- Fine-grained token → https://github.com/settings/personal-access-tokens/new
- Repository access: all repos you want watched
- Permissions: Pull requests → Read, Contents → Read, Metadata → Read
Paste it into the prompt and hit Save. The token is stored in your macOS Keychain (not in any file). You can change it later from the menu → Set GitHub Token….
Notifications
The first successful sync is silent (it just records a baseline), so you don't get a flood of alerts for every currently-open PR. From then on you'll only be notified about changes.
If macOS asks whether "PR Widget" may send notifications, allow it. If you never granted permission, the app falls back to AppleScript notifications so you still get alerts.
The window
Launching opens a Pull Requests window (also: menu bar → Open PR Window, ⌘O). While it's open the app becomes a normal app: it gets a Dock icon and shows up in ⌘-Tab. Close the window and it drops back to menu-bar-only.
The window is a split view: the PR list on the left, a code-review pane on the right.
Left (list):
- Filter dropdown (All / Needs review / My PRs / New on my repos) + search.
- Dismiss a PR: hover a row and click the ✕ (or right-click → Dismiss). It hides from the list, but reappears automatically if it gets new activity (a new comment, review, or CI change), since that's when you'd want another look.
- Click the ↗︎ on a row (or right-click → Open in Browser) to open it on GitHub.
- Gear menu (settings): Set GitHub Token, Refresh, Launch at Login, Show dismissed, Restore all dismissed.
Right (review pane): select a PR to load it:
- PR description and the conversation (reviews + comments).
- The full diff, per file, color-coded, with line numbers. Big files are capped (open on GitHub for the rest).
- Review inline: existing review threads appear anchored to their lines, with a Resolve / Unresolve button and a Reply box.
- Comment on a line: hover a line, click the blue +, type, and post.
Use Suggestion to drop a
```suggestionblock seeded with the line. - Submit a review: write a body, pick Comment / Approve / Request changes, and submit (⌘↩).
These are live actions against GitHub: submitting a review, posting a comment, or resolving a thread happens immediately on the real PR.
Window size/position is remembered between launches.
Run at login
Use the gear menu → Launch at Login in the window (or the menu bar item of
the same name). It registers via macOS's modern SMAppService, so it also shows
up under System Settings → General → Login Items.
Project layout
| File | What it does |
|---|---|
Sources/PRWidget/main.swift |
Entry point; runs as a menu-bar accessory app. |
AppDelegate.swift |
Status item, menu, 60s polling loop, token prompt. |
GitHubClient.swift |
One GraphQL request → login + all three PR buckets. |
Models.swift |
PR model + GraphQL decoding. |
StateStore.swift |
Diffs each refresh against the last to produce events. |
NotificationManager.swift |
Desktop notifications (UserNotifications + osascript fallback). |
Keychain.swift |
Stores the token in the macOS Keychain. |
build.sh / run.sh |
Build, bundle, sign, launch. |
Notes / limits
- Search is capped at 50 PRs per bucket (GitHub API limit per page).
- "New on my repos" covers repos you own. Watching a whole org's repos would need an extra query, easy to add if you want it.
- Polling interval is 60s; change
refreshIntervalinAppDelegate.swift.