Better Review
Gerrit-style review for GitHub pull requests in VS Code: patch sets, per-label votes, submit
requirements, one-shot Reply with local drafts, keyboard-first navigation, and no branch switching.
Status: early release (0.1). The specification is docs/DESIGN.md in the source repository.
What it does
- Change screen with patch sets reconstructed from GitHub's own evidence (force pushes, check
suites, reviews), numbered the same for every reviewer.
- Votes
Code-Review −2..+2 and Verified (from CI), mapped exactly onto GitHub review states,
with Gerrit's stickiness rules across trivial rebases.
- Submit requirements derived from branch protection and rulesets, next to GitHub's own merge state.
- Reply: drafts stay on your machine until you send them all at once; publishing is journaled and
resumable.
- Attention signals: a count badge on the activity bar icon and a notification when one of your
changes is reviewed or something else needs you, e.g. "@ada-l reviewed #42 engine: add throttle control:
Code-Review-1, 3 comments".
- Suggested changes, like Gerrit's Suggest edit and Apply fix: Suggest Edit in a comment puts the
commented lines in a GitHub
suggestion block for you to edit. Suggestions, also those written on
github.com, are shown as diffs, and Apply Suggestion applies one to the file in your checkout after a
preview, unless the lines have changed since. Reply warns when a suggestion cannot stay one on GitHub (left
side, file comment, another patch set, the review message) and posts it as a diff.
- Never touches your repository: code is read from a private sidecar object store that borrows your
clone's objects; no refs, config, hooks, stash or checkout in your work tree. The one exception is Apply
Suggestion, which you run and confirm; it edits the file in the editor and saves nothing.
- Keyboard-first:
]/[ next/previous file, n/p next/previous change, c comment, a reply,
r mark reviewed, Shift+M mark reviewed and open the next file, ? for all keys.
Requirements
- VS Code 1.140 or later
- GitHub CLI (
gh), signed in with gh auth login (VS Code's GitHub sign-in
is used as a fallback)
- git (developed against 2.55)
Settings
All settings live under betterReview.*; see the Settings UI ("Better Review") for descriptions.
Review caches, worktrees and drafts are stored in ~/.better-review (betterReview.storageRoot).
Behind a proxy, set betterReview.proxy (for example http://127.0.0.1:7890 or socks5://127.0.0.1:1080):
GitHub API requests, git fetches and gh all use it. Without it, API requests follow VS Code's http.proxy
and git and gh their own configuration.
Attention. When one of your changes is reviewed or commented on, its checks fail, it has a merge conflict
or it is ready to submit, or when your review is requested, a change you reviewed gets a new patch set or someone
replies to your comment, the Better Review icon in the activity bar counts the changes that need you and a
notification says who did what, with Open Change. Opening the change (or reviewing it) clears it. Each event is
announced once, across reloads and windows; a window without focus leaves its notifications to the next window you
focus, a change is announced at most every 10 minutes, and more than three at once come as one summary.
betterReview.attention.badge and betterReview.attention.notifications turn them off. While the badge is on, the
Files view shows its unreviewed-file count in its title bar description, because VS Code adds up the badges of all
views on one icon. The badge uses your theme's activity bar badge colour (extensions cannot make it red), and the
notifications are VS Code's own: VS Code has no API for the operating system's notification center.
betterReview.gh.path, betterReview.proxy, betterReview.storageRoot and betterReview.git.extraClones are
read from your user settings only: a repository's .vscode/settings.json cannot choose which executable runs,
where your traffic goes or which folders are used. In untrusted (Restricted Mode) workspaces, workspace values of the other settings that
pick hosts, LFS or GitHub writes (worktree.lfs, dashboard.host, files.syncViewedToGitHub, …) are
ignored too.
Security notes
- Review worktrees hold the pull request's code. A worktree file opened in the current window (
w)
runs under that window's workspace trust: language extensions may load configuration or tools the pull
request committed. Better Review shows a one-time notice before the first one opens in a trusted window.
- Do not put the storage root inside a folder you trust as a parent. A review worktree opened in a new
window (
Open Worktree in New Window) is trusted without VS Code's prompt when it lies inside such a
folder. Workspace trust is never set by Better Review.
- Credentials never appear on a command line. git asks for them only for the pull request's own host
(
gh auth git-credential, or a helper that answers only that host).
Development
npm install
npm run typecheck # tsc for the extension and the webview
npm run build # esbuild → dist/extension.js, dist/webview/change.js
npm test # vitest unit tests (test/unit)
npm run test:integration # downloads VS Code 1.143.1 and runs test/integration under mocha
BR_CLANGD=<path to clangd> npm run test:clangd # comment completion through your installed vscode-clangd and that clangd
npm run package # produces a .vsix
Press F5 ("Run Extension") to start an Extension Development Host.
Live tests
npm run test:live runs the real extension in VS Code 1.143.1 against real GitHub: it opens pull
requests on the private throwaway repository CGQAQ/better-review-live-test (the only repository live
tests may change), votes, replies and merges there, and checks every effect with independent gh api
reads. It needs gh signed in with the repo and workflow scopes; no token is ever read or printed (git
and the extension ask gh themselves). Neither npm test nor npm run test:integration runs these tests.
npm run live:seed # once: push the seed files (test/live/support/seed.ts) to main
npm run test:live # every test/live/**/*.live.test.ts
BR_LIVE_FILES=smoke npm run test:live # one file (a bare name means <name>.live.test.ts; globs and comma lists work)
BR_LIVE_FILES='votes/*.live.test.ts' npm run test:live # one area
BR_LIVE_WORKSPACE=clone npm run test:live # open a temp clone of the repo as the workspace (borrowing mode)
Each run gets an id (BR_LIVE_RUN, generated by default) and its own build (out/live/<runId>/), user data
directory, extensions directory and storage root (a temp directory; never ~/.better-review), so runs can go
in parallel. Everything is removed afterwards unless BR_LIVE_KEEP=1; the extension's log is kept in
out/live/<runId>.better-review.log. Other variables: BR_LIVE_SETTINGS (JSON merged into the instance's
user settings; polling is off by default), BR_LIVE_GREP, BR_LIVE_BAIL=1, BR_LIVE_TIMEOUT (ms per test),
BR_LIVE_KEEP_PRS=1 and BR_LIVE_MAX_DISPATCHES (bot pull requests per run, default 10).
The tests are grouped by area, one directory each. Run the areas one after another rather than all at once:
GitHub puts secondary rate limits on an account that creates many reviews, comments and pull requests at the
same time, and a whole-suite run needs more bot pull requests than one run's budget.
| Area |
Files |
Bot PRs |
Notes |
votes/ |
ladder, reply, replyextras, interrupt, outdated, headmoved, github, ownpr |
7 |
every vote step, dismissals, interrupted Replies (resume, discard, crash), github.com interop, own PR |
comments/ |
threads, patchsets, pending, vcflow, vdrift |
0 |
anchors, fallbacks, porting, imported pending reviews, the c flow in a real diff editor |
patchsets/ |
lifecycle, retarget, sidecar, polling, probe, reconstruct |
1 |
probe runs only with BR_PSV_PROBE=1 (read-only, earlier PRs); reconstruct needs the BR_PS_HANDOFF=<file> a lifecycle run wrote (best in clone mode); BR_PS_VOTES=b picks the second vote order; sidecar checks borrowing only with BR_LIVE_WORKSPACE=clone; polling waits up to 5 minutes when the test window has no focus |
labels/ |
labels, verified, requirements, dashboard |
7 |
the ruleset test skips itself (403 on this plan) |
submit/ |
merge, viewed, viewedPaths |
10 |
run merge (8 bot PRs) apart from the other two; BR_LIVE_SUBMIT_VETO2=1 adds the open design question "a new −2 after Submit anyway" (submit-2, pending by default) |
Some paths cannot run on this repository, by GitHub's rules or its plan, and are covered by unit tests only:
Quick approve's publish path and any Code-Review requirement met without "Submit anyway" (one human account
pushes every patch set, so its +2 never counts under user=non_uploader), votes from a second account or a
non-writer, re-apply and GitHub's stale-approval dismissal (branch protection), ruleset requirement rows, admin
merges and the merge queue. docs/DESIGN.md §0.4 lists what the live runs established about GitHub.
Layout: test/live/entry.ts wraps the real activate() and returns its Services (via the test hook
servicesForTests() in src/extension.ts) plus a recorder that answers or cancels the extension's dialogs;
test/live/support/ holds the helpers (gh.ts ground-truth gh api calls, truth.ts typed PR state,
repo.ts branches, clones and pull requests, ext.ts sessions, content, drafts, Reply, labels and submit
through the extension, seed.ts the repository's seed content). Branches are live/<area>/<runId>/<slug>
and pull request titles carry [live <area> <runId>]. Pull requests that need Code-Review±2 are opened by
github-actions[bot] (openBotPr, through the repository's open-pr workflow), since GitHub never lets
authors approve their own. The repository is private on a plan without rulesets, branch protection or
auto-merge (each answers HTTP 403 "Upgrade to GitHub Pro"), so those paths cannot be tested live.
Layout: src/model holds the frozen shared contracts, src/core the shared services, and each unit
(src/github, src/git, src/session, src/votes, src/comments, src/views, src/webviewHost)
exports one factory from its index.ts. Pure modules never import vscode, fs or child_process
and are tested with vitest against the fakes in test/fakes.
Releasing
.github/workflows/release.yml builds, type-checks, unit-tests and packages the extension, then attaches the
.vsix (and its .sha256) to a GitHub release and publishes it on the VS Code Marketplace. Bump version in
package.json, add a ## [x.y.z] section to CHANGELOG.md (it becomes the release notes), push, then push the
matching tag:
npm run --silent release:notes -- 0.2.0 # preview the notes: the section with its wrapped lines joined
git tag v0.2.0 && git push origin v0.2.0
GitHub shows every newline inside a release body's paragraphs as a line break, so scripts/release-notes.ts joins the
soft line breaks of each paragraph, as a CommonMark + GFM parser (micromark) finds them, and leaves code, tables, HTML,
headings, hard breaks and alert markers as written. It also appends the link and footnote definitions the section uses
from elsewhere in the file.
The tag must equal v + the package.json version, and a version with a hyphen (0.2.0-beta.1) becomes a GitHub
prerelease and is not published on the Marketplace, which takes x.y.z versions only. Running the workflow by hand
from the Actions tab with "dry run" on (the default) only builds and keeps the .vsix as a workflow artifact.
The Marketplace is published to by
Trusted Publishing: vsce publish --oidc trades the job's GitHub OIDC token for a Marketplace credential, so no token is stored (Azure DevOps
personal access tokens for all organizations stop working on December 1, 2026). It needs a policy on the CGQAQ
publisher's Trusted Publishing page at marketplace.visualstudio.com/manage:
repository owner CGQAQ, repository better-review, workflow release.yml, no environment. A new policy is
inactive until its first sign-in, which for a private repository must come within 48 hours: run the workflow from
the Actions tab with check publishing on, which only signs in (scripts/marketplace-signin.mjs) and publishes
nothing, or make a release.
gh workflow run release.yml --repo CGQAQ/better-review -f check_publishing=true # activate a new policy
A release run again (after a failed step) skips the GitHub release and the Marketplace version that exist already
(scripts/marketplace-has.mjs reads vsce show).
License
MIT © CGQAQ (see LICENSE)