The problem
Reviewing merge requests is where my day goes now. Agents raise them faster than people can read them, and the review is the work. My queue was split across GitLab and GitHub, each with its own tabs, filters and notion of what needs me.
I’d already solved this once, for myself, as a widget in my Linux desktop shell. It worked well and nobody else could use it, because it only ran inside that shell.
What I built
Emarii is a desktop app for macOS and Windows that reviews GitLab merge requests and GitHub pull requests in one list. Right-click a row to approve, request changes, review or merge. Ask for an AI review and you get a draft you edit before anything is posted. A loop re-reviews watched merge requests as they change.
It’s a Rust port of the reviewer half of my original widget (QML and Python), so it works without that shell. The Python backend — 1,515 lines — is vendored in the repo as the behaviour reference, and the port order was strict: each module landed with its parity test before the next started.
At the top sits Ready for you: finished work on a merge request, waiting for a person to check it. It gathers three producers — Emarii’s own reviews, MR Glue’s cards, and any other tool that finishes something on a merge request while nobody’s watching.
How it works
- The backend is a Rust crate with no Tauri dependency, ever. A CLI wraps the same crate and prints exactly the JSON the original Python did.
- A golden parity suite puts fake
gh,glabandclaudebinaries on the path that replay recorded API responses, runs both the Rust CLI and the original Python, and diffs the JSON. There are 125 unit tests beside it. - Merges are pinned to the commit that was checked. A push between the check and the merge makes the merge fail.
- At work, Emarii also shows the state of MR Glue, a merge-request healer in the team’s agent harness. MR Glue fixes conflicts and review feedback on your own merge requests and, when a fix is too big or untested to push by itself, leaves a proposal and waits. Emarii reads its state and shows it on the row. A button press is written the same way the desktop tray writes one, so MR Glue has exactly one input path and can’t tell Emarii’s click from the tray’s.

- The front end is plain HTML, CSS and JavaScript with no build step. Anything worth testing there is a pure decision in its own module, tested with
node --test.
Decisions worth noting
It never reads a GitHub or GitLab token. Both CLIs authenticate from their own keyrings and the app shells out to them. No telemetry: merge request content goes only to the configured review command.
No orphaned children. Stop, Quit and a crash all leave zero claude processes behind. On macOS that’s a process group; the Windows equivalent is on the list.
Vanilla JS over the planned Svelte. The port plan specified Svelte and Vite. I chose plain JavaScript to reach a working demo sooner, and wrote down that it’s a small conversion if the component set outgrows it.
MR Glue’s state shows up where you’re already looking. Before this, the two disagreed in front of the person: one merge request had a conflict fix sitting ready to apply, while its Emarii row showed a red “conflicts” chip and “never reviewed”, with no sign anything had touched it. Now the row says what MR Glue did, or what it’s waiting on.
Where it fits
Emarii is my own tool, built for my review queue first; it isn’t part of any work codebase. I also use it at work, where it reads a local notification folder, which is how MR Glue’s cards and other tools’ finished reviews reach it. Its AI review runs through Claude Code, the same harness Loop dispatches to.
Outcome
Emarii is at v0.5.4. Windows installers come from CI; the macOS universal build is made by hand until the CI job re-signs after lipo, which strips the bundle signature. Getting Windows right took finding an ordinary config bug: bundle targets were pinned to dmg and app, neither of which exists on Windows, so the job compiled the whole app and then found no installer to upload. v0.5.3 was the first release with a real Windows installer.