feat(service,features,desktop): one Pull request tab per agent, its overview and its files
Summary
Implements the plan Add PR tab. Each agent's Changes tab becomes its Pull request tab. An Overview | Files changed switch in the tab's bar moves between the two views. Stage-cli splits a pull request page into overview and files the same way. The developer chose combining over two separate tabs.
Bar: a pill showing whether the PR is open, a draft, merged or closed, then the switch. On Overview, the bar also has Open on GitHub and a ⋯ menu (Refresh pull request, Copy link). Files changed keeps its own file bar under this one.
Overview, top to bottom:
- The title and #number, then
base ← head. - Who opened it and when (or when it merged), and the reviewers with their review state on their faces.
- Merge status, only while the PR is open or a draft: ready, unstable, blocked, behind, conflicts, draft, or still checking.
- Checks: check runs and commit statuses together, failures first, then running, passed and skipped. Each row shows its App and how long it ran, and opens its page.
- The description, drawn from GitHub's own rendering (
body_html). Details are under GitHub's rendering and images below. - The conversation: comments and reviews in time order, also drawn from GitHub's rendering, and never cut. The newest 100 are kept whole. Any older ones are left out, and the conversation says how many: "N earlier comments on GitHub", which opens the PR. Neither stage-cli nor ReviewStage/stage cuts comments.
Files changed is the old Changes view. The "Review comments" fold under the files is removed, because the Overview's conversation now shows what reviews said, in full. The split diff from #2921 is now its own PR, #3121.
Opening the tab:
- The PR chip at an agent box's foot opens the tab on Overview; its right-click offers Open on GitHub.
- The Changes chip opens it on Files changed.
- The "+", ⌘K and the plan summary list each pull request once, under Pull requests.
The tab keeps the Changes tab's stored kind (changes), so it still survives a relaunch, pins to the sidebar, and keeps its link. Existing pins and links keep working. Each view reads only the first time it is shown. After that, both stay mounted and the hidden one is not re-read, so switching back and forth makes no new GitHub calls.
Alignment with the panel's top row. Measured in Playwright on the real agent page and side panel:
- The left edges of the pill, the merge box, the checks list, comments and the file bar's first crumb line up with the left edge of a tab chip in the strip above (
--column-inset). - All text and icons line up with a tab's icon (
--content-inset). A box's 1px border counts toward the inset, so the alignment is exact to the pixel. - The last control in each bar ends where the top row's last control ends.
| Overview, dark | Overview, light | Files changed, dark |
|---|---|---|
![]() | ![]() | ![]() |
These come from a throwaway harness (not committed) that mounts the real DesktopAgentPage with its side panel and stylesheet, using synthetic data. In the files screenshot the file tree reads "could not be shown" only because the harness doesn't build the tree's lazily loaded bundle.
GitHub's rendering and images (14204c6e7)
This is ported from ReviewStage/stage.
Text. The overview asks GitHub for application/vnd.github.full+json on the PR, its comments and its reviews, so it gets body_html, GitHub's own rendering:
- @mentions and #123 references are links, emoji are drawn, and task lists and
<details>look as they do on GitHub. - The HTML is never inserted as raw HTML. It goes through the transcript's existing markdown pipeline: Streamdown's HTML parser, then its sanitizer, which uses GitHub's own allowlist, then its link hardening.
- Before sanitizing, a small step (
@luke/ui/github-html) turns GitHub's highlighted code blocks and mermaid diagrams into ordinary fenced code blocks, so they get Luke's own code highlighting. The same step removes heading permalink icons and GitHub's wrapper elements. - Scripts are stripped.
Images. Images are read by the service, never by your Mac: GET /api/agents/{id}/github-image?src=.
- Uploads (
github.com/user-attachments/assets/<id>,…/<owner>/<repo>/assets/<id>,private-user-images…): the service asks for a freshly signed URL by rendering a one-line image of that asset through GitHub's markdown API, using the App's token for the agent's repository. This is the same trick ReviewStage/stage uses. It then fetches the image from that URL with no credential. The signed URL is used only if it points at that same asset, and it never reaches the window. - Public images on
camo.githubusercontent.comoruser-images.githubusercontent.comare fetched as-is. - Any other image is shown as a link.
- Limits: PNG, JPEG, WebP, GIF or SVG, up to 8 MB.
- Display: the window draws each image from a
data:URL through the picture cache transcripts already use, so the window's CSP doesn't change. The images are in the tab, which session replay already leaves out of the recording.
Cost: one upload costs one markdown-API request (re-signing) plus one fetch of the image. A camo image costs no API request.
| Overview with GitHub's rendering, dark | light |
|---|---|
![]() | ![]() |
Cut down (1984e110d)
- No character budget. The overview keeps the newest 100 entries whole and counts the earlier ones.
newestThatFit,MAX_WORDS_CHARSand the description'scutflag are gone. The 4.5 MB response limit is only at risk for a PR with 100 enormous comments; if it is hit, the tab shows its read failure and Retry. - Simpler cleanup of GitHub's HTML. Three rules are left: remove a heading's permalink, and turn a highlighted block or a
<pre lang>(a diagram's source) into a fenced code block. The heading and snippet unwrapping and the diagram data lookup are gone; those wrapper elements are harmless. - Checks aren't sorted twice. The tab lists them in the order the service ranks them.
- The split diff moved to its own PR, #3121.
Tab icon (b3338aab4)
The tab, and its row in "+", ⌘K and the plan summary, shows the PR's own mark for its state: open, draft, merged or closed in GitHub's colours, and red when its checks failed.
GitHub API vs gh, and rate limits
The overview reads the GitHub REST API through the hosted service, on the Luke GitHub App's installation token for the agent's one repository. That is the same path the files view already uses.
- Installation token: each installation has its own budget, at least 5,000 requests an hour and up to 12,500 (15,000 on Enterprise Cloud). It doesn't touch your personal budget. (docs)
gh: it would spend the developer's own 5,000 an hour, shared with every PAT, OAuth app and GitHub App acting as them. It also needsghinstalled and signed in on every Mac, and the hosted service can't run it.- Cost: one overview read is about 5–6 requests, roughly 800 reads an hour per installation at the minimum limit. Reads happen only when a view is first shown, on Refresh, and when the agent finishes, never on a timer.
Wiring
GET /api/agents/{id}/overview → GATEWAY_METHOD.CODING_AGENTS_OVERVIEW → ACT_KIND.CODING_AGENTS_OVERVIEW → CodingAgentsControl.readOverview. The rewrite table was regenerated. Naming people against workspace members is shared between the overview and files reads (github-people.ts). One read hook, useAgentRead, serves both views. Which view each agent's tab shows lives in the side panel (pullRequestViews, kept in memory only), and both chips open the tab through one helper, showPullRequest.
PRIVACY.md: the "A coding agent's changes" paragraph is replaced by "A coding agent's pull request". It describes both views' reads, the rendered words, and how images are read; nothing is stored. Please confirm the wording, since it's policy text.
Evidence
- Tests on this head:
vitest run apps/desktop: 1,490 tests passed.- Earlier on the branch:
apps/web,packages/service(all shards, re-run on their own after the sandbox ran out of memory),packages/host, andpackages/features.
- Lint and typecheck:
biome check,oxlint,knip,discarded-effect, andtscfor desktop, web, luke and coder. All clean. - New or changed tests:
- Service overview read: the GitHub read, naming, bounds, and keeping a failed check when the list is cut to 100.
- The tab: the overview page; the switch, which reads Files only once it's shown and reads nothing again when switching back; empty, merged and failure states.
showPullRequest: the PR chip opens Overview, and the Changes chip opens Files changed.- The chip's right-click offers GitHub.
- Tests that named the tab "Changes: …" now expect "Pull request: …".
- The "review comments fold" test is removed along with the fold.
- Bundle budget re-recorded (
LUKE_UPDATE_BUNDLE_BUDGET=1, a61e5228d). When recorded, this PR (then including #2921's split diff, since moved to #3121) added 5,333 gzipped bytes torenderer.js: 754,742 onmainat 8461461c4, 760,075 here.mainhad already used 4.3% of the 5% slack, so this ended up 5 bytes over the ceiling.plan-editor.jsandpcm-tee-worklet.jsare re-recorded at the sizesmainbuilds them to. - JSON Schema goldens: five new files (the overview answer and its view, plus the image params, view and type), recorded with
LUKE_UPDATE_FIXTURES=1. No golden that existed before this PR changed.
Notes
- Checked by the developer in the app against this PR's Vercel Preview (
--preview). The tab-icon change came after that check. - Follow-ups:
- Write actions: merge, close, draft/ready, reviewers, labels.
- GitHub's line comments in reviews (
/pulls/{n}/comments), which #2921's agent had also offered as a follow-up. - On pull requests with more than 1,000 comments, reading the newest ones backwards from GitHub's
Linkheader. - Commit authors are still shown only in Files changed.




