feat(desktop,service,features): load the Pull request tab's Overview progressively

Merged
DA
2d ago
+1979-741

Was stacked on #3221. #3221 has merged, so this is rebased onto main and targets it. It also takes in #3206 and #3208.

Summary

The agent's Pull request tab Overview used to show a spinner until one readOverview answered. That one read waited on the slowest of: reach + token, then the rendered pull request, then checks, every page of comments and reviews, and the GraphQL threads, then naming people. This PR splits it into three reads that each land on their own:

ReadRoute · gateway method · actWhat it answersBounds
Pull request itselfGET /api/agents/{id}/overview/pull-request · codingAgents.overviewPullRequestdescription (body_html), branches, created/merged, mergeability, author, writtenAs, requested reviewersnames ≤ 255 chars, requested ≤ 50
ChecksGET /api/agents/{id}/overview/checks · codingAgents.overviewCheckscheck runs + commit statuses, failures first≤ 100
ConversationGET /api/agents/{id}/overview/conversation · codingAgents.overviewConversationcomments, reviews and threads, plus each reviewer's standing from their reviews≤ 100 entries (rest counted in earlier), ≤ 100 threads, ≤ 50 comments per thread, ≤ 50 reviewers
  • Draw at once what Luke holds. The bar (pill, GitHub, ⋯) and the title + #number draw from control.pullRequests on the first frame. Each word is replaced by the overview's own as its read lands: state and title from the pull request read, the checks summary from the checks read.
  • A section per read. The head (branches, people, merge line), Checks, Description and Conversation each show a skeleton while their read is out. A read that failed shows a failure with its own Retry, which re-reads only that part. Refresh (⋯) re-reads all three, and each section stays drawn until its new answer lands. If nothing is held and every read failed, one centred failure retries all three. If any read answers that there's no pull request, the tab says "No pull request yet." The tab still reads only once the Overview is shown (useAgentRead, three times).
  • Reviewers are now the requested reviewers from the pull request read, laid over each reviewer's standing from the conversation read. A requested reviewer shows as requested, as GitHub draws a re-request. This is the same rule reviewersOf applied on the service before.
  • Checks no longer wait on the pull request read. They're asked by refs/pull/N/head (/commits/refs%2Fpull%2FN%2Fhead/check-runs and /status), not by the head sha the pull request read used to supply. I checked that GitHub answers both endpoints with that ref (against #3221 itself: same sha as pulls/3221.head.sha).
  • The single overview route, gateway method and act are removed, not kept beside the new ones: #3030 isn't in any release yet (0.16.0 was cut before it).

Screenshots

These come from a throwaway harness that holds each read open, answers it, or fails it. They were taken before the rebase onto #3208, so the opener still shows as the agent rather than as luke-agent[bot].

Nothing landed yet: bar and title from what Luke holdsChecks landedChecks and conversation landed
All landed (dark)All landed (light)Checks read failed, with its own Retry (light)

GitHub requests, before and after

These are typical counts: one page each of comments and reviews, the published answer warm (it's cached per instance for 30 s, and the tab reads it anyway).

Before: one routeAfter: pull requestAfter: checksAfter: conversation
Reach + repository token (agentPullRequestReach: installation, listing mint, repositories page, repository mint)4444
GET pulls/N (rendered)11––
check-runs + status2–2–
comments pages + reviews pages + GraphQL threads3––3
Total10567 (18 in all)
  • Splitting adds only the reach + token, twice more (+8), which the brief allows. Every other GitHub read happens exactly once, as before. (Naming people reads only the DB now that #3208 dropped appBot. Before the rebase, the split also repeated that one read.)
  • Time to first section is better beyond the parallelism. Before, the checks and the conversation waited on the pull request read. Now each route's path is reach → its own reads.

How this fits with #3235 (per-instance cache of the repository reach and token, from main): #3235 changes only github-app.ts, under agentPullRequestReach. Its reach and token caches are Effect Caches, and Cache.get joins an in-flight lookup. So the three routes landing on one instance at once do one installation read, listing and mint between them, and none while warm. Each instance's first cold mint still happens once per instance. On a warm instance the extra reaches cost 0 GitHub requests, which brings the split back to the single read's own 6 (10 − 4). #3235 has since merged. Its test of the token cache read the single overview route, so here it asks the three overview routes at once, twice, and still sees one installation read, listing, listing mint and repository mint. That shows the cache joining the three concurrent routes.

JSON Schema goldens

Re-recorded with LUKE_UPDATE_FIXTURES=1 through hosted-wire-schemas.test.ts:

  • removed coding-agent-wire-codingAgentOverviewAnswerSchema.json and coding-agent-view-codingAgentOverviewAnswerViewSchema.json, because their schemas are gone;
  • added the …OverviewPullRequestAnswer…, …OverviewChecksAnswer… and …OverviewConversationAnswer… wire and view goldens. Between them they hold the old answer's fields, except that checks moves to the checks answer, reviewers (standings only) and conversation/earlier move to the conversation answer, and the pull request answer gains requested: GitHubPerson[] (≤ 50).

Evidence

  • pnpm check:affected: exit 0 (lint, knip, discarded-effect, typechecks, every affected vitest project).
  • pnpm exec vitest run packages/service/src/github/pull-request-overview.test.ts: 8 passed.
  • pnpm exec vitest run apps/desktop/src/renderer/tasks/agent-pull-request-tab.test.ts: 10 passed.
  • pnpm --filter @luke/web functions:rewrites regenerated vercel.json and apps/web/server/api-rewrites.json for the three routes.
  • Screenshots come from a throwaway harness (not committed) that mounts AgentPullRequestTab alone and holds each read open, answers it, or fails it. They're below.

New tests, each of which fails without this change:

  • Renderer: "the bar and the title stand from what Luke holds before any read lands; each section is a skeleton until its own read lands, and is drawn the moment it does, whatever the order" (reads held open and answered checks → conversation → pull request).
  • Renderer: "a section whose read failed says so with a Retry that reads it alone, the others standing; Refresh reads all three again".
  • Renderer: "where nothing of the pull request is held and every read failed, one failure offers a retry that reads all three again".
  • Service: "each read stands on its own: the pull request's read failing leaves its checks, asked by its head ref, and its conversation read" (GitHub answers pulls/N with 503).
  • Host: the gateway test calls the three new methods.

The existing renderer and service tests were moved onto the three reads with the same assertions. The one exception: reviewers are now checked as requested on the pull request plus standings on the conversation.

Invariants

  • ./scripts/check.sh green. (CI) Locally pnpm check:affected is green.
  • JSON Schema goldens changed, and the diff is explained above.
  • Every behavior change has, in this PR, a test that fails without it. No change-detector test added.
  • Gateway envelope goldens unchanged.
  • No new as type assertion.
  • No Effect.run* outside the edges.
  • No new raw async primitive in source. (The renderer test holds reads open with Promise.withResolvers, and a test body is its own edge.)
  • No discarded Effect.
  • Test count: +5 renderer tests (one old "no PR / retry" test split into "no PR" plus four new ones, the stale-badge test among them), +1 service test.
  • No hand-rolled file replaced.
  • No AGENTS.md sentence names the changed parts.
  • PRIVACY.md unchanged: the same data is read from GitHub, now in three asks.
  • Deps unchanged.
  • Barrel rule unaffected.

Notes

  • Rebased over #3206 and #3208. Naming no longer reads appBot, and the tab no longer passes agent to the people chips, matching main.
  • Next in the stack: the stage-cli-style tabbed layout (fixed header, then Conversation / Checks / Files changed tabs).