feat(desktop,service,features): reviewer and label pickers in the Pull request tab's header
Summary
This is F2 of the "Reviewers, labels and stack navigation" stack. F1 (#3268) has merged, so this now targets main, squashed to one commit on it. It adds pickers to add and remove requested reviewers (users and teams the repository allows) and labels (the repository's labels). Every write goes through W1's member write path (#3272): it is made as the member who pressed it, on their own GitHub sign-in, and never on the Luke GitHub App.
- Reviewer picker: a person-plus button beside the reviewer faces, shown only while the PR is open or a draft. Its menu lists the teams that reach the repository, then its collaborators, with the PR author left out. Anyone currently requested is checked, and choosing an item requests a review or withdraws the request.
- Label picker: a tag button at the end of the labels row. Its menu lists the repository's labels, each with its colour dot, and the ones on the PR are checked. Choosing one adds or removes it. It's offered on merged and closed PRs too, as GitHub allows that.
- What the pickers offer comes from a new read,
GET /api/agents/{id}/review-options: the repository's labels, its collaborators (named against the workspace's members) and its teams.- It uses the App's installation token, as the overview reads do, and is read once, when a picker first opens. It is read again only after a failure.
- Each list is paged up to 300 entries. If the App may not read one of them (for example a user-owned repo's teams, or a 403), that list is empty, a warning is logged, and the rest still show.
- Writes:
pull-request-write-wire.ts's action union gainsrequest-review,remove-review-request(a{ kind: user | team, name }reviewer),add-labelandremove-label(a label name).coding-agents-app.ts'sactionWritedispatches them throughmemberPullRequestWrite.- They are REST writes on the member's token (
setReviewRequested,setLabelledingithub/pull-request-writes.ts). GraphQL has no mutation that removes a requested reviewer;requestReviewswithunion: falsereplaces the whole set, which races with other changes. So all four writes use REST, with GitHub's refusals told apart exactly asgithubWrite's are (401 → sign in again; other statuses → GitHub's own words). github-app.tsgainsgithubSendRequestnext togithubPost, for the POST and DELETE.
- The PR read now also carries
requestedTeams(fromrequested_teamsin the same REST read, so no extra request), which the reviewer picker uses to check teams. - After a write, the tab re-reads exactly as W1's pill does, and GitHub's refusal shows on W1's line under the bar. While a change is out, the pickers are disabled. The pill only shows "…" for its own actions now (
pull-request-writes.tsx).
Screenshots
From a throwaway harness, redrawn after moving the header's CSS onto the shared --luke-* roles (#3524 removed the old aliases; this also fixes the F1 label and stack rules already on main): the tab with a fake window.luke and the built styles.css. One team and one user are requested, and the repository offers 2 teams, 4 people and 5 labels.
| Dark | Light |
|---|---|
![]() | ![]() |
![]() | ![]() |
![]() | ![]() |
GitHub requests
- Opening a picker the first time: 3 REST listings on the installation token, in parallel (
/labels,/collaborators,/teams), one page each up to 100 entries. None after that for the life of the tab. - Each change: 1 REST write on the member's token.
- The header read: unchanged, with
requested_teamscoming from the same response.
Wire
- New answer schema:
codingAgentReviewOptionsAnswerSchema, plus its view schema. codingAgentOverviewPullRequestAnswerSchema: gains an optionalrequestedTeams.pullRequestActionRequestSchema: gains four branches.- Goldens: re-recorded for those, and for
codingAgentPullRequestActionParamsSchema, which embeds the action schema. - Plumbing: new gateway method and act
codingAgents.reviewOptions, threaded through compose, operator, IPC andCodingAgentsControl.readReviewOptions. - Rewrites: new route shell
apps/web/server/routes/agents/review-options.ts, withapi-rewrites.jsonandvercel.jsonregenerated bypnpm --filter @luke/web functions:rewrites.
Evidence
pnpm check:affectedruns out of memory in this sandbox, so I ran the per-package fallback:tscfor@luke/desktop,@luke/service,@luke/features,@luke/hostand@luke/gateway: pass../scripts/oxlint.sh: 0 errors.pnpm knip: pass.pnpm discarded-effect: pass../scripts/repository-checks.sh: pass, including the rewrites--checkand the api-callers scan.
- Tests:
coding-agents-app.test.ts: the new and W1 suites, 10 tests, pass.agent-pull-request-tab.test.ts: 29 pass.packages/host/src, IPC acts and Vercel functions: 179 pass.- Wire goldens: 336 pass.
node scripts/build.mjsinapps/desktop: within the bundle budget.
New tests:
- Service: a teammate requests a team review, withdraws a user's request, adds a label and removes one, each as a REST write with the teammate's own bearer (method, path and body checked). A 422 from GitHub comes back in its words. A reviewer kind the wire doesn't define is refused before reaching GitHub. The options read returns labels and named collaborators, with teams empty on a 403, all on an installation token and never a member's.
- Desktop: the reviewer picker reads the options only when first opened, lists teams then people (author excluded, requested ones checked), and requests and withdraws reviews. The label picker removes and adds labels. A merged PR offers no reviewer picker.
Invariants
-
./scripts/check.shgreen. (CI) - JSON Schema goldens: only the new schemas and the extended action union and overview answer changed.
- Every behavior change has a test that fails without it.
- No new
astype assertion. - No Effect run outside the edges; no raw async primitives; no discarded Effect.
- No member token logged or answered. Writes never fall back to the App.
Notes
- Shared action union:
pull-request-write-wire.tsandactionWritekeep both this PR's four actions and main's review-submit and thread reply/resolve actions. The reviewer and label writes go by REST on owner, repo and number; the rest go through the node id. - Follow-ups:
- The picker has no search box yet; long lists scroll.
- Requested teams aren't drawn in the header's facepile; only the picker shows them.
- Collaborators are only a proxy for who can be requested. GitHub refuses anyone it won't accept, in its own words.
Made with Luke
Summary by cubic
Adds reviewer and label pickers to the Pull request tab's header. The reviewer picker offers the repository's teams and collaborators (the author left out) on open and draft PRs; the label picker offers the repository's labels and also works on merged and closed PRs. The header's labels, stack, and pickers now draw with the shared --luke-* design roles, since the legacy token aliases were removed in #3524.
- A new read,
GET /api/agents/{id}/review-options, fetches the repository's labels, collaborators, and teams once, when a picker first opens, on the App's installation token, paged up to 300 entries each. A listing the App may not read (403) comes back empty with a logged warning; any other failure fails the read, and the picker reads it again. - Four new write actions —
request-review,remove-review-request,add-label, andremove-label— go out as REST calls on the member's own GitHub token, since GraphQL has no mutation that removes a requested reviewer. A 401 asks for a fresh sign-in; other refusals show GitHub's own words on the existing write line. - The pickers disable while a change is in flight and until the tab re-reads after it lands. The PR read now carries the requested teams, so each picker checks what already stands.





