Fast native code review app for GitHub, Bitbucket and Forgejo https://revue.cleangame.dev/
  • Rust 97.2%
  • JavaScript 1.7%
  • CSS 0.6%
  • Dockerfile 0.2%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Richard Jelínek 0c2224696a
All checks were successful
check / check (push) Has been skipped
check / web-probe (push) Has been skipped
server / publish (push) Successful in 16m31s
Release 0.44.1
2026-11-13 17:19:05 +00:00
.claude Session start: Forgejo issues and GlitchTip reports too; clear the build above 5 GB 2026-10-28 21:36:57 +01:00
.forgejo/workflows Nix: install Revue's release binary, with nothing to compile 2026-11-13 09:32:14 +01:00
assets Notifications: a page lists every notification of the session by day, with filters; the bell's list highlights each one under the pointer, with lines between them, and leaves out "Copied" confirmations 2026-11-13 17:07:07 +01:00
crates Deps: base64 0.23 in the app, the vault and the server 2026-11-12 12:39:19 +01:00
deploy/web CI: no warnings of our own in the build logs 2026-11-12 22:23:12 +01:00
docs Docs: Zed is back under Coming soon in Editors until it's in Zed's extensions 2026-11-13 17:27:11 +01:00
editors/zed Editors: the JetBrains plugin moves to jetbrains-plugins 2026-11-12 01:18:34 +01:00
packaging Packaging: nix-revue's revue-bin.nix header no longer gets its version filled into the comment 2026-11-13 13:38:25 +01:00
scripts CI: the hosted web probe runs in a job of its own, and fails on a blank or throwing page 2026-10-27 15:10:06 +01:00
server Website: the downloads page is one card, with a button for your system and its package manager beside it 2026-11-12 23:13:59 +01:00
src Notifications: a page lists every notification of the session by day, with filters; the bell's list highlights each one under the pointer, with lines between them, and leaves out "Copied" confirmations 2026-11-13 17:07:07 +01:00
tests Demo: answers editors' questions, so the JetBrains plugin and Zed can show its made-up pull requests 2026-11-12 12:14:31 +01:00
vendor Notifications: a page lists every notification of the session by day, with filters; the bell's list highlights each one under the pointer, with lines between them, and leaves out "Copied" confirmations 2026-11-13 17:07:07 +01:00
web Web: the app builds for the browser and revue serve serves it 2026-10-26 11:45:45 +01:00
.env.example GitHub: sign-ins made on the web account renew in the app 2026-11-09 09:33:36 +01:00
.gitignore Web: a Docker image, a Compose example behind Caddy and a hosting guide 2026-10-26 11:57:10 +01:00
.mcp.json GitHub MCP server as a task too; no shell scripts, in CLAUDE.md 2026-10-16 18:55:54 +02:00
build.rs Windows: revue.exe's Properties show what it is and its version 2026-10-19 17:52:14 +02:00
Cargo.lock Release 0.44.1 2026-11-13 17:19:05 +00:00
Cargo.toml Release 0.44.1 2026-11-13 17:19:05 +00:00
CHANGELOG.groups Changelog: the macOS shortcut fix in 0.41.0 reads as one 2026-11-12 10:19:52 +01:00
CHANGELOG.md Release 0.44.1 2026-11-13 17:19:05 +00:00
CLAUDE.md Changelog: only what users care about, corrected on every release 2026-11-12 00:28:50 +01:00
default.nix Install: Nix users without flakes can build and install Revue with nix-env or callPackage 2026-11-13 13:32:02 +01:00
flake.lock Revue: native GPUI GitHub code review app 2026-10-12 09:01:05 +02:00
flake.nix Install: Nix users without flakes can build and install Revue with nix-env or callPackage 2026-11-13 13:32:02 +01:00
global.env Edit pull requests, conflicts, merge commits ignored, and Revue's server 2026-10-16 09:24:58 +02:00
LICENSE-APACHE License: MIT OR Apache-2.0 2026-10-12 13:17:59 +02:00
LICENSE-MIT License: MIT OR Apache-2.0 2026-10-12 13:17:59 +02:00
package.nix Install: Nix users without flakes can build and install Revue with nix-env or callPackage 2026-11-13 13:32:02 +01:00
README.md Docs: README names the NUR package 2026-11-13 13:39:32 +01:00
rust-analyzer.toml GitHub MCP server as a task too; no shell scripts, in CLAUDE.md 2026-10-16 18:55:54 +02:00
rust-toolchain.toml Build with Rust 1.99.1 2026-11-12 10:14:30 +01:00
Taskfile.yml Install: Revue's release binary is on NUR as nur.repos.revue.revue-bin 2026-11-13 13:37:15 +01:00

Revue

A fast, native code review app for GitHub, Bitbucket Cloud and Forgejo, built with Rust and GPUI (Zed's GPU-accelerated UI framework, via gpui-kit). It doesn't use Electron or a webview.

Warning

This project is fully vibe coded. Its code, tests and docs are written by AI coding agents (Claude Code), directed by a developer rather than written by hand. Use it at your own risk.

Documentation: every feature and setting, with step-by-step guides, is at https://revue.cleangame.dev/docs, written in docs/ (start with docs/index.md).

Why

GitHub's review UI hides parts of the conversation ("Load more…", collapsed "hidden items"), loses context when commits are pushed, and makes reviewing a few new commits awkward. Revue is built around these goals:

  • No hidden conversations. It loads every thread, comment, review and commit, paginating each connection all the way through. Threads show inline in the diff and also in a threads panel. Threads that can't sit on a line in the current view (outdated, file-level, or written on other commits) show at the top of their file rather than disappearing.

  • Any range of commits. You can view:

    • all changes,
    • changes since your last review,
    • changes since your last visit,
    • a single commit (click it),
    • or any range of commits (shift-click).

    Range diffs hide files that only changed because the base branch was merged in. Existing threads are re-mapped onto older commits when their lines still exist there.

  • Drafts are never lost. Every pending comment and every half-written reply is saved to disk on each keystroke. If submitting fails, the drafts stay. Esc asks before throwing text away.

  • Comments written on any commit range still land. When you submit, Revue carries each comment's line forward to the PR head through the diff between the commit you commented on and the head. If the line has changed since, the comment becomes a file-level comment that quotes its original location. Nothing is dropped.

  • Fast:

    • The UI is GPU-rendered and the diff is virtualized.
    • Syntax highlighting uses tree-sitter in the background, for 35 languages.
    • REST calls are cached with ETags (a 304 doesn't count against the rate limit).
    • GraphQL results are cached on disk, so the inbox and PRs appear instantly at startup and then revalidate in the background.
    • PRs waiting on you are prefetched, and so is any PR you hover in the inbox.

Features

  • Inbox: review requested, re-review (shows a dot when there are new commits since your review), your PRs, and PRs you're involved in. Each row shows CI status, review decision and draft counts.

  • Tabs: several PRs open at once. They're restored on restart.

  • Repositories: browse any repository without a PR (Ctrl+G, or pick one in Ctrl+K). Its tab lists:

    • the history of any branch: click a commit for its diff, Shift+click another for the whole range, filter by message, author or hash;
    • the branches, each one's diff against the default branch, with Open pull request on those without one (the branch checked out in your local clone first, pushed with git push -u if it isn't yet, or a plain git push when it's ahead; never forced) and a link on those with one;
    • its pull requests, open, merged or closed, searchable;
    • its code at any commit.

    Clone on a repository's row clones it, signed in as the account it's listed for: its remote names that user (https://login@github.com/…), so git's credential helper uses that account for every fetch and push. It goes into the folder set in Settings → Storage (one per service, over a default; asked when none is set), and is then used for worktrees and the Code tab.

  • Diff:

    • Split and unified modes, word-level inline highlights, and collapsible files.
    • Viewed-file state syncs with GitHub and auto-collapses viewed files.
    • Progress bar for viewed files.
  • Comments:

    • Comment on a single line or on a range by dragging the gutter, or with the + button.
    • "Suggest change" inserts a ```suggestion block prefilled with the selected lines.
    • "Add to review" queues the comment as a draft; "Comment now" posts it straight away.
  • Threads:

    • Reply inline, and resolve or unresolve.
    • The threads panel's tabs list All threads or those on the files on screen (Visible); Show resolved and Show from others add those, and a search finds threads by text, file or person. ] and [ jump between open threads.
  • Conversation tab: the PR description and a comment box, then a timeline of comments, reviews, threads (with their diff-hunk context), force-pushes and commits, newest first. It's virtualized, with nothing collapsed.

  • Submit: Comment, Approve or Request changes, with a summary that is saved as you type.

Download

Revue's website, https://revue.cleangame.dev, has the latest version for each platform on its downloads page. It's served by Revue's own server (server/, see server/DEPLOY.md), and task server:dev runs it on your computer at http://localhost:7377.

Prebuilt builds are attached to every release:

Platform File Install
Linux x86_64 revue-vX.Y.Z-linux-x86_64.{deb,rpm,pkg.tar.zst} From the apt, dnf and pacman repositories (setup), or install the file
Linux x86_64 revue-vX.Y.Z-linux-x86_64.AppImage chmod +x, run it; it updates itself
Linux x86_64 revue-vX.Y.Z-linux-x86_64.tar.gz Extract, run task install (installs to ~/.local)
Windows x86_64 revue-vX.Y.Z-windows-x86_64-setup.exe Run the installer
Windows x86_64 revue-vX.Y.Z-windows-x86_64.zip Extract, run revue.exe
macOS (Apple Silicon + Intel) revue-vX.Y.Z-macos-universal.dmg Open, drag Revue to Applications. It isn't notarized, so the first time, right-click Revue.app → Open
macOS (Apple Silicon + Intel) revue-vX.Y.Z-macos-universal.zip Extract, move Revue.app to Applications (right-click → Open the first time, as above)

With Nix, nix profile install git+https://forgejo.cleangame.dev/thatrichan/nix-revue installs the release binary (also on NUR as nur.repos.revue.revue-bin), or add this repository's flake as an input to build from source (see below). Without flakes, nix-env -if https://forgejo.cleangame.dev/thatrichan/revue/archive/main.tar.gz builds it from source, and pkgs.callPackage "${src}/package.nix" { } adds it to a NixOS or home-manager configuration. On Windows, scoop bucket add revue https://forgejo.cleangame.dev/thatrichan/scoop-revue then scoop install revue; on macOS, brew tap thatrichan/revue https://forgejo.cleangame.dev/thatrichan/homebrew-revue then brew install --cask thatrichan/revue/revue.

Running

On NixOS, or anywhere with Nix:

nix run .                 # build & run the packaged app
nix profile install .     # install, including a desktop entry

From a checkout, every command is a Task task, the same on Linux, macOS and Windows (inside the Nix dev shell when there's Nix):

task dev                  # dev build of this checkout ("Revue Dev")
task dev -- https://github.com/owner/repo/pull/123   # open a PR directly
task dev:release          # try out a release build, changing nothing (below)
task server:dev           # Revue's server and website, at http://localhost:7377
task dev:demo             # the app on mock services, no accounts needed (below)
task --list               # the others: check, deploy:patch, shoot…

A release build uses the installed Revue's settings, sign-ins and data, so task dev:release runs it with --immutable: on a copy of them, in a temporary folder deleted on quit, with the credential store copied into memory. It doesn't install updates or register itself for links either. Renewed sign-in tokens are still saved for real, as the services stop taking the ones they replace. revue --immutable does the same for any build.

Without Nix you need Rust 1.90+ and the usual GPUI Linux dependencies (wayland, libxkbcommon, xcb, vulkan-loader, fontconfig, freetype).

Demo: no accounts needed

task dev:demo             # or: revue --demo

--demo runs Revue against mock GitHub, Bitbucket Cloud, Forgejo, Jira and Slack services on this computer (src/mock), signed in to all of them as a made-up person at a made-up company: pull requests with real diffs, review threads, CI checks, tickets and Slack threads, every name and line of it fictional. Everything you do (comments, reviews, reactions, new pull requests…) changes the mock and reads back. It runs on a new, empty settings and data folder, deleted on quit, never on Revue's own: none of your settings, sign-ins, credential store, caches, gh token or GITHUB_TOKEN is read. Nothing leaves the computer either: every request for anything but the mocks is refused, and git only works with local repositories (each mock repository is a real local clone, so worktrees, the Code tab and language servers work as with your own). The demo starts with the app's default look; REVUE_DEMO_SETTINGS (JSON) is laid over its settings, e.g. REVUE_DEMO_SETTINGS='{"dark": false}'.

Editors ask the demo too: revue --demo --pulls … (and every other editor question, and --pull-request) goes to the demo's window, not Revue's own, and with --headless and no demo running it's answered from mock services of its own. REVUE_DEMO=1 in an editor's environment does the same for the plain revue --pulls … its plugin runs.

task shoot -- OUTDIR --demo ::: STEPS… takes screenshots of the demo, and task shoot:site all of the website's (server/assets/screenshots), dark and light, in one go. task shoot:editors sets up the demo and a project for the JetBrains IDE and Zed ones, taken by hand, and prints the steps.

Tests against the mock services

The mocks are stateful fakes of each service's HTTP API that the app's real clients talk to, so a test exercises everything above the wire:

let services = crate::mock::Services::start();      // every service, seeded
cx.update(|cx| services.install(cx));               // the app signed in to each
// …drive the UI, then check what the service holds:
let w = services.github.state.lock();
assert_eq!(w.pr("tidewater/atlas-api", 214).unwrap().reviews.len(), 3);

For one service, mock::github::start(seed::github()) and a client from mock::github::client(&svc) (the same for bitbucket, forgejo, jira, slack). Make a service fail with svc.faults.add(Fault::new("/pulls/1/files", 502, "…").times(2)), sign its account out with svc.revoke(), or change what it holds (svc.state.lock(): someone else's comment, a push) to see the app pick it up. Examples: src/mock/tests.rs, src/ui/mock_flows.rs, and each service's own tests in src/mock/.

Opening pull requests from the browser

revue <PR link> opens a pull request, in the running Revue if there is one. Under Settings → Apps, Register as handler makes revue:// links open Revue (Windows and Linux; Revue.app declares them itself on macOS), and the bookmarklet shown there sends the pull request page you're on to Revue as revue://open?url=<link>.

To send someone a link that opens in Revue, use Copy Revue link (in the pull request's header and its right-click menu in lists). It's the pull request's address with Revue's server in front, e.g. https://revue.cleangame.dev/github.com/acme/web/pull/12, so you can also type one by hand. It opens Revue where it's installed and offers the page on the web where it isn't.

Authentication

Sign in with as many GitHub, Bitbucket and Forgejo accounts as you like, on the welcome screen or under Settings. Sign-ins are stored in ~/.config/revue/tokens/ (whose account, which server), so they sync along with the config folder; their tokens (and OAuth refresh tokens) never do. They stay on each computer, which signs in on its own: Atlassian, Forgejo and Slack refresh tokens work once, so two computers sharing them would sign each other out. By default they're in a file readable only by you (mode 0600) in the local data folder: ~/.local/share/revue/sign-ins.json on Linux, ~/Library/Application Support/revue/sign-ins.json on macOS, %LOCALAPPDATA%\revue\sign-ins.json on Windows.

Settings → Storage → Sign-ins can keep the tokens in the system's credential store instead (the switch is saved in that computer's settings.local.json): the login keychain on macOS, a file encrypted for your Windows user with DPAPI, the Secret Service (GNOME Keyring, KWallet…, through libsecret's secret-tool) on Linux. macOS may ask once after each update to let Revue use the keychain. Turned off, the tokens go back to the local file and the store's entry is deleted. Where no store can be used (a server without a desktop session, no secret-tool), the switch says so and the tokens stay in the local file.

Turn on system credentials to also use GITHUB_TOKEN / GH_TOKEN or the logged-in gh CLI. REVUE_GITHUB_TOKEN, when set, is used alone.

Every service can also be signed in to with the browser (OAuth), as an alternative to pasting a token; the token forms stay as they are. GitHub uses its device flow (you type a code on github.com). Bitbucket, Jira and Forgejo send the browser back to http://localhost:47392/callback (Atlassian) or http://127.0.0.1:47392/callback (Forgejo), and Revue renews their short-lived access tokens with the refresh token, saving both to the same files. The button only shows for a service whose OAuth app Revue knows, from these variables at run time or, else, when it was built. For local runs, copy .env.example to .env and fill it in; task dev loads it. Setup for each service is in docs/oauth-setup.md.

Variable For
REVUE_GITHUB_CLIENT_ID, REVUE_GITHUB_CLIENT_SECRET GitHub OAuth app (device flow enabled; the secret renews sign-ins made on the web account)
REVUE_BITBUCKET_CLIENT_ID, REVUE_BITBUCKET_CLIENT_SECRET Bitbucket OAuth consumer
REVUE_JIRA_CLIENT_ID, REVUE_JIRA_CLIENT_SECRET Atlassian OAuth 2.0 (3LO) app
REVUE_FORGEJO_CLIENT_IDS https://codeberg.org=<id>,https://git.example.com=<id>
REVUE_SLACK_CLIENT_ID (and, without PKCE, _SECRET, REVUE_SLACK_REDIRECT_URL) Slack app (user scopes)

For a Forgejo server not listed, the form asks for the client ID of an OAuth2 application made on that server (public, redirect URI http://127.0.0.1:47392/callback). Bitbucket only takes OAuth tokens on api.bitbucket.org, so for an OAuth-signed Bitbucket account the Code tab fetches a commit's files one by one through the API, for repositories of up to 2000 files. Larger ones need an API token sign-in to browse their code.

Each account has a Track switch: tracked accounts fill the pull request list; the others still open PR links. A PR opens with the account whose list it came from, or else the first account that can see it.

Bitbucket Cloud

Revue also reviews Bitbucket Cloud pull requests, side by side with GitHub ones. Add the account on the sign-in screen or in Settings → Bitbucket account, with your Atlassian account email and an API token with these scopes: read:user:bitbucket, read:pullrequest:bitbucket, write:pullrequest:bitbucket, read:repository:bitbucket, read:workspace:bitbucket. It's kept like the other tokens (see Authentication). You can use Bitbucket without a GitHub account.

Open a PR with its link (https://bitbucket.org/ws/repo/pull-requests/12) or from the inbox. A few things differ from GitHub:

  • Bitbucket can't search pull requests across repositories, so "Needs my review" and "All in my repos" cover the 30 most recently updated repositories of the workspaces listed under Settings → Watched workspaces. With none listed, Revue uses the repositories of your own open PRs.
  • Submitting a review posts its comments one by one, then the summary as a plain comment, then the approval or change request. If a comment fails, the ones already posted leave your drafts and the rest stay drafts.
  • "Viewed" marks on files stay in Revue (Bitbucket has no API for them).

Forgejo

Revue reviews pull requests on Codeberg or your own Forgejo server (Gitea works too, as it has the same API). Add the account on the sign-in screen or under Settings → Forgejo accounts, with the server's address and an access token (Settings → Applications on the server) with read:user, write:repository and write:issue. It's kept like the other tokens (see Authentication). You can add accounts on several servers.

Open a PR with its link (https://codeberg.org/owner/repo/pulls/12) or from the inbox. Some things work differently from GitHub:

  • "All in my repos" lists the open PRs of the 30 repositories your own PRs, review requests and reviews are in.
  • Forgejo comments sit on a single line, so a comment on a range of lines is posted on its last line. File-level comments go into the review summary.
  • A reply is posted as a one-comment review on the thread's line, the way Forgejo's website does it.
  • The API can't resolve conversations, so Revue only shows whether they're resolved. "Viewed" marks on files stay in Revue.
  • Commit ranges ("since my review", single commits) are diffed by Revue from the files' two versions, since Forgejo's API doesn't return patches for them.

Jira

Connect Jira under Settings → Jira with your site, Atlassian account email and a scoped API token ("Create API token with scopes", app Jira) with read:jira-work, write:jira-work and read:jira-user. Classic tokens without scopes aren't supported; Revue talks to Jira through Atlassian's API gateway. Links to tickets on that site then open in Revue instead of the browser, in a tab grouped with the PR you clicked them in (closing the PR closes them too), and the keys of its projects are linked everywhere. A ticket tab shows its status, type, priority, assignee, reporter, people fields such as Tester, epic or parent, sprint, labels, components, description and comments. You can comment (Markdown), move the ticket along its workflow (the transitions Jira offers from its current status) and assign it to yourself. The token is kept like the others (see Authentication).

Slack

Connect Slack under Settings → Accounts, like the other services (Sign in with Slack in the browser, or a pasted user token; the welcome screen offers it too, as an optional extra). The Submit review dialog then has Notify in Slack: Revue finds the Slack threads that link to the pull request (Slack's search, as you), you pick one and who to tag, and once the review is in it replies there as you: "@Ann I approved your pull request #12 …". The author is found by their email on the code host, or by Settings → Slack messages → People on Slack, or you pick them from the channel's members. A failed reply doesn't fail the review. A Slack threads tab (key 7) lists every thread linking to the pull request, with Open in Slack (in Slack's app when it's installed, Settings → Apps → Slack). Above them, Ask for a review requests reviews again after changes (those who reviewed start picked) on the code host and posts "@bob please review …" in a thread. In any comment, # and a letter suggest your Slack channels; the pick goes in as a link ([#dev](…/archives/C…)), and a #channel typed out becomes one when posted. A pull request opened from Revue can be announced in a channel you pick: the ticket's link, with the pull request and its reviewers tagged in its thread. Settings → Slack messages → Message formats changes the wording of every message (placeholders like {pr_link}, {reviewers}), with a preview, and sets their language apart from the app's. The preview draws :emoji:, the workspace's own too, flags names that aren't emoji ("did you mean :balloon:?"), and : suggests them as you type. Attach file… (or a drop) adds files to a message, a GIF on "approved" say, kept in the config folder (up to 10 MB each) and posted with the text as one message. Setup, scopes, formats and privacy: docs/slack.md. The token is kept like the others (see Authentication).

Web version

The same app runs in a browser: the library compiled to WebAssembly and drawn by GPUI's web platform, served by revue serve: on the computer whose accounts it uses, or hosted for many people, each with their own accounts.

task web:build      # the bundle, into target/web-dist (PROFILE=dev: quicker)
task web:serve      # revue serve --web-dir target/web-dist; open http://127.0.0.1:8421/
task web:check      # only whether the browser build compiles

task web:build builds in Docker, so it needs nothing but Docker: a rust:1-bookworm container with the wasm32-unknown-unknown target and the wasm-bindgen CLI of the version in Cargo.lock, kept in revue-web-* volumes with the build, so the next one only rebuilds what changed. With rustup's wasm target and that wasm-bindgen installed, task web:bundle does the same without Docker (task web:tools installs them on Debian or Ubuntu, as root). It runs on plain stable Rust, single-threaded: vendor/gpui-pre-web is GPUI's web platform without its multithreaded feature, whose wasm_thread needs nightly. Without --web-dir, revue serve looks for web/ next to its executable; with --demo (task web:serve -- --demo) it serves the demo's mock services and accounts instead. task web:probe -- <url> [seconds] [shot.png] loads a page in headless Chromium (with deno) and prints its console and a screenshot; task web:probe:hosted loads a signed-in hosted session's PR list that way, and fails when the page throws, draws nothing or doesn't load the PRs. What's left to do: docs/web-plan.md.

Hosting it for a team. deploy/web has a Docker image of revue serve with the bundle (task web:image) and a Compose example behind Caddy with HTTPS (task web:up). Registering the OAuth apps, every setting, backups, upgrades and the security model: docs/web-hosting.md.

A browser lends the page no fonts, so the bundle brings Noto Sans and Noto Sans Mono (assets/fonts, SIL Open Font License), fetched at start; a font chosen in Settings that the page doesn't have falls back to them. GPUI draws with WebGL 2; add ?webgpu to the address to try WebGPU first (where a browser's WebGPU misbehaves, the page stays blank and the console says "device lost").

How it talks to the services. GitHub, Bitbucket, Forgejo and Jira don't let other sites' pages call their APIs (Forgejo has CORS off by default), so the page asks revue serve, on its own origin, and the server sends the request on. Tokens never reach the browser: the server uses the accounts signed in on its computer (the desktop app's sign-ins, GITHUB_TOKEN or the gh CLI with system credentials on), hands the page a handle for each, and signs every request for the account the page names. Sign in, or out, with the desktop app there and reload the page.

Security. revue serve listens on 127.0.0.1:8421 and answers only to that address by name (no DNS rebinding). --bind <address>[:port] listens elsewhere, and then requires an access secret: made up and printed at start, or given with --secret / REVUE_SERVE_SECRET. The page asks for it once and keeps a session cookie (HttpOnly, SameSite=Strict); without it nothing but the sign-in page answers. It isn't an open proxy: requests go only to the hosts of its accounts (a signed one to its own account's origin; unsigned GETs only for avatars, answered only with PNG, JPEG, GIF or WebP), cookies and the page's Authorization header stay behind, and answers carry nosniff, DENY framing, no-store caching and a Content Security Policy. Only the page's own requests reach the proxy (not a link opened in the browser), and what it answers is sandboxed and served as a download, never a page of this origin. It speaks plain HTTP: beyond a network you trust, put it behind an HTTPS reverse proxy.

Hosted mode. revue serve --hosted (also chosen by --config <file.json>, or when REVUE_PUBLIC_URL or REVUE_CONFIG is set) serves the web version to many people instead: each browser signs in to its own accounts, and the server never reads its computer's sign-ins. The basics:

REVUE_PUBLIC_URL=https://revue.example.com \
REVUE_SESSION_KEY="$(openssl rand -hex 32)" \
REVUE_DATA_DIR=/var/lib/revue REVUE_TRUST_PROXY=1 \
REVUE_GITHUB_CLIENT_ID=… REVUE_GITHUB_CLIENT_SECRET=… \
REVUE_FORGEJO=https://codeberg.org REVUE_JIRA_SITES=acme.atlassian.net \
revue serve --hosted --web-dir target/web-dist

REVUE_PUBLIC_URL (with a path, such as https://example.com/revue, it serves everything under that path) and REVUE_SESSION_KEY (32+ characters; changing it signs everyone out) are required; it listens on REVUE_BIND (default 0.0.0.0:8421). A service with an OAuth app configured (REVUE_<GITHUB|BITBUCKET|JIRA>_CLIENT_ID and _CLIENT_SECRET, or <url>=<id>[:<secret>] in REVUE_FORGEJO) signs in with it, coming back to <public url>/auth/<service>/callback; any service also takes a pasted token, which the server checks. Forgejo servers and Jira sites are only those listed. Sessions (a cookie, 30 days by default, REVUE_SESSION_DAYS) and their tokens are kept in REVUE_DATA_DIR, encrypted with AES-256-GCM. Requests that change anything need the page's origin and the session's anti-forgery token, the Host must be the public address's (/health excepted), each session is rate limited (REVUE_RATE_LIMIT a minute), and the server reaches no private or loopback address unless REVUE_ALLOW_PRIVATE_HOSTS names its host. It's meant to run behind an HTTPS reverse proxy (with REVUE_TRUST_PROXY=1), which also shields it from slow and numerous clients. Every variable: revue serve --hosted --help; the design: docs/web-plan.md.

Not in the browser. Everything that needs the computer: local clones and worktrees, source checkouts (so the Code tab and Find in Files), language servers and code navigation, "Open in" apps, terminals and assistants, revue:// links, self-update and crash reports, the Revue account and Slack, and signing in with the local revue serve (its computer's accounts are used; the hosted mode has its own sign-in). These are hidden, or say "Not available in the browser". Diffs are syntax highlighted as on the desktop (the tree-sitter grammars are built for wasm32), but for C++, C#, Kotlin, Scala and Swift, shown plain: their grammars would be most of the page's download. Settings and each PR's state are kept in the page's localStorage, the HTTP cache in memory. With the local revue serve, OAuth tokens it renews are saved for the desktop app too, but a desktop app running at the same time may need to sign in again (refresh tokens are single-use).

Releasing

A release is made in one of two ways, and pushing a tag starts neither. Both run the same tasks from Taskfile.yml (Task):

  • From Forgejo: Actions → release → Run workflow on main, choosing patch, minor or major. .forgejo/workflows/release.yml runs task version:<bump> and task release:tag (commit "Release X.Y.Z", tag vX.Y.Z, push), then task release:build:<platform> and task release:upload in a container per platform on the Forgejo runner. Forgejo doesn't let a run's own token push, so the release commit and tag are pushed with the repository secret RELEASE_TOKEN: a personal access token (scope write:repository) of someone who may push to main.
  • From your computer: task deploy:patch (or deploy:minor, deploy:major) raises the version, then task build runs the three release:build:* tasks in parallel, each in the same Docker image as in CI. Only when all three succeed does it commit, tag, push and upload. The OAuth apps and a FORGEJO_ACCESS_TOKEN that can write releases come from .env.

Either way the files go to the Forgejo release for that tag, created with notes made from the commits since the previous tag.

  • Linux: built natively in rust:1-bookworm, so it needs glibc 2.36 or newer. The binary links only libc, libxcb and libxkbcommon; Wayland, Vulkan and fontconfig are loaded at runtime.
  • Windows: cross-compiled with cargo-xwin, which supplies clang-cl and the MSVC CRT and Windows SDK.
    • GPUI normally compiles its HLSL shaders with fxc.exe, and only when the build host is Windows. Revue therefore patches gpui-pre-windows (vendor/) to use shader bytecode precompiled with the real fxc.exe under Wine. Regenerate it with task windows-shaders.
    • The icon is embedded and no console window opens.
  • macOS: cross-compiled as a universal binary with cargo-zigbuild and the macOS SDK.
    • GPUI's runtime_shaders feature compiles the Metal shaders at runtime, and a patched gpui-pre-apple build script checks the target OS instead of the host's.
    • The output is packaged as Revue.app.

rust-toolchain.toml pins the toolchain the containers use.

CI (.forgejo/workflows/check.yml) runs task check:ci on every push: rustfmt, clippy --all-targets -D warnings, the tests and a release build. Its web-probe job, alongside, builds the browser version (task web:tools, task web:bundle) and runs task web:probe:hosted on it in headless Chromium; the screenshot is the run's hosted-pr-list artifact. Pushing doesn't run it; run task check:quick by hand before a push when you want the same check locally.

Keyboard

Key Action
Ctrl+K / Ctrl+L / Ctrl+O Search PRs and repositories, or open a PR by link or owner/repo#123
Ctrl+R / F5 Refresh
Ctrl+Tab, Ctrl+W Next PR tab, close tab
Ctrl+B / Ctrl+H Go to the pull request list
1 2 3 Files / Conversation / Commits
j / k Next / previous file
n / p Next / previous change
] / [ Next / previous open thread
v Toggle viewed and jump to the next file
a / r / l All changes / since your review / since last visit
, / . Step to older / newer single commit
c Comment on selected lines
s Split / unified
t Threads panel
z / Shift+Z Collapse / expand all files
Ctrl+Enter In a comment box: add to review or send reply. Otherwise: open submit
Esc Close an empty composer
Ctrl+Shift+T Reopen the tab closed last (again: the one before)
Ctrl+Shift+L Light / dark
Ctrl+/, F1 All shortcuts

Single-key shortcuts are turned off while a text field has focus.

Every shortcut can be changed in Settings → Keyboard shortcuts (click a key, press the new one; Esc cancels, Backspace removes it), with a second key, a reset per shortcut and for all. A key another shortcut already has where this one works is caught first: replace it there or swap the two. Only the changed shortcuts are stored, under keybindings in the settings (and in an export).

Keyboard layouts that type letters or other characters on the number row and punctuation keys (Czech, Slovak, German, French, Hungarian…) get the key in the same place instead of one they type only with Shift or AltGr: on a Czech keyboard Ctrl+é resets the text size (Ctrl+0 on US), ě š č… pick a PR's tabs, ) goes to the next thread. On Linux Revue knows these layouts by the name xkb reports (Czech, German…); on macOS and Windows GPUI matches the defaults by their place on a US keyboard itself. The dialog, the badges of "hold Alt", tooltips and Settings show the keys that work on the layout in use, and the keys picked in Settings always win.

Layout

src/
  main.rs          the desktop binary: runs the library (lib.rs)
  lib.rs           app bootstrap, --probe CLI for API diagnostics, `serve`
  web.rs           the browser build's entry (wasm), web_api.rs its server API
  serve/           `revue serve`: the web bundle and the proxy to the services
  github/          client (ETag + disk cache, tokio), GraphQL queries, models, API ops
  bitbucket/       Bitbucket Cloud API 2.0, mapped onto the same models
  forgejo/         Forgejo / Gitea API v1 (Codeberg or self-hosted), same models
  provider.rs      routes each PR operation to GitHub, Bitbucket or Forgejo
  diff.rs          hunk parsing, line mapping across commits, GitHub patch adapter
  review.rs        drafts → review comments (re-anchoring to the PR head)
  store.rs         drafts, reply drafts, settings, token (all on disk)
  ui/              app shell, inbox, PR view, threads/timeline, login, tests

cargo test runs the unit tests and a headless UI test. The UI test opens a real GPUI window and checks:

  • key dispatch,
  • that single-key shortcuts don't fire while you type,
  • that drafts persist,
  • that Esc never discards text.

Settings, sign-ins, favorites and each PR's drafts and viewed files live in ~/.config/revue on every platform ($XDG_CONFIG_HOME/revue when set): one folder to back up or sync between computers (Syncthing, a dotfiles repo). Older versions kept them in ~/.local/share/revue; they move over on first start. What Revue remembers rather than what you set (recent and open tabs, pinned tabs, the pull request list's filters, panel widths) is in state.json there, apart from settings.json. Machine-local state stays in ~/.local/share/revue, and the HTTP cache, source checkouts and settings-cache.json (language servers detected per repository, ones stopped by hand) in ~/.cache/revue. Dev builds (task dev) use revue-dev everywhere instead, so they never touch the installed app's accounts, drafts or settings.

Settings for one computer

Some settings belong to one machine: local clones, editor and terminal, fonts, text size, language servers. They can live in settings.local.json in the machine-local folder (~/.local/share/revue on Linux, see Settings → Storage → Settings files), which is never synced and beats the shared settings.json:

  • each setting (top-level key) in it replaces the shared one; objects (local_repos, lsp_choices…) merge key by key, so the shared entries still show unless overridden; null puts a setting back to its default;
  • a setting it has is saved there when changed, as what differs from the shared value; the shared file keeps its own value for your other computers. Everything else is saved to settings.json as before;
  • the This computer / Shared switch on a settings card moves its settings into the local file or back, keeping their current value.
{
  "text_size": 110,
  "local_repos": { "owner/repo": "/home/me/src/repo" }
}

Languages

Revue speaks English and Czech. Settings → Appearance → Language picks one, or System (the default) follows the computer: LANGUAGE, LC_ALL, LC_MESSAGES or LANG on Linux, the preferred languages on macOS and the display language on Windows; anything Revue doesn't have shows in English. The change applies at once; text already laid out in open fields and dialogs follows after a restart (the card offers one).

Translating

The English text in the code is the key (see src/i18n.rs):

  • tr!("Sign out") for plain text, trf!("Sign out {name}?", name = login) with named placeholders (a translation may reorder them), and trn!("{n} file", "{n} files", count) for counts;
  • tr_mark!("Appearance") marks text in tables and constants, translated where it's shown with i18n::tr.

Each language is a JSON file in assets/i18n/, English text → translation; plurals are an object of forms (Czech: one for 1, few for 2–4, other for the rest), keyed by the English plural. The "//" entry holds notes and the glossary for translators.

  • Missing strings: cargo test i18n lists every string the code translates that the catalog lacks (and entries no longer used), and checks that each translation keeps its {placeholders}. Debug builds also print i18n: no translation for … the first time such a string shows.
  • Another language: add assets/i18n/<code>.json, a variant to Language and Lang in src/i18n.rs with its plural rule in plural, and its code to Lang::from_code.

License

Licensed under either of Apache License 2.0 or MIT, at your option. vendor/gpui-pre-windows, vendor/gpui-pre-apple and vendor/gpui-pre-web are patched copies of GPUI crates and keep their Apache-2.0 license.