feat: the agent's branch and pull request on its tab (O4)
What
A coding agent's tab now shows what the agent produced, and lets the developer act on it without leaving Luke.
Server. GET /api/agents/{id}/pull-request answers what the agent published (coder-host/published.ts): the repository, the branch it pushed, and the pull request from it with its state (open, draft, merged, closed), its checks as one word (pending, passing, failing, none), and its additions, deletions, and changed files; null for a branch or a pull request it has not. Finding them: the agent reaches GitHub only through git and gh in its shell, so its own transcript rows are where the two are named, for this agent and no other (two agents on one repository have rows of their own, so there is no ambiguity to break). The branch is read off the commands it ran (a push, a branch cut, a head given to gh pr create, or any name under CODER.BRANCH_PREFIX), the newest naming winning; the pull request off any address of one on the agent's repository in its words or in any tool's answer, case aside. GitHub then says what stands, on the developer's own App token (github/pull-requests.ts, the same reach the checkout uses): the pull request by its number, else the newest from the branch, else whether the branch was pushed at all, with the head's check runs and commit statuses read together; a head whose checks the App may not read reads as none with a warning rather than failing the answer. The answer is kept per agent for 30 s (CODER.PUBLISHED_TTL) in an Effect Cache built once with the routes, a failed read for no time, so a tab reading beside every held page asks GitHub a few times a minute at most. No webhooks.
Desktop. In the agent tab's header row (model · effort · status), after the title: a pill for the pull request (the GitHub mark, #123, open green / draft grey / merged purple / closed red, with a check-status dot) that opens the pull request in the browser; or a branch chip when there's a branch and no pull request yet; and a ⋯ menu beside either with Open pull request, Copy branch name, Copy checkout command (git fetch origin <branch> && git switch <branch>), and View changes on GitHub (the pull request's Files tab, or the branch compared). After a turn finishes, a compact row at the end of the transcript: "Opened #123 · +210 −14 in 6 files" with Open. The window reads the endpoint as the tab comes on screen, as transcript pages land no closer than 15 s apart, and the moment a page says the agent ended (planning/use-agent-pull-request.ts).
Stop stays the header's own button, as agreed with the orchestrator; O2 owns moving it into the composer, and nothing here adds it to the ⋯ menu.
The end-of-turn notice (O5 merged first, so the wiring is here): a completed turn is announced as "Claude Opus 5.5 opened #123" when the pull request is found, "finished" otherwise, "failed" as before. Each turn end is queued from the ledger's synchronous path and a fiber on the notices' scope takes it, reads GET /api/agents/{id}/pull-request once, and posts; the unseen dot is set ahead of the read, and a failed turn is not read (main/agent-notices.ts).
After Devin's review: the first read after a turn ends is fresh rather than the running turn's kept answer (the cache key says whether the agent had ended); the transcript scan walks every page, not the first 200 rows; a pull request is the agent's only on the branch its own commands named, so a link in a file it read is never worn as its own, and an agent with no branch of its own published nothing; the developer's reach of the repository through the App is confirmed as a Start confirms it (repository-not-reachable, 403); the checkout command quotes a branch a shell would read into, and the compare address encodes one.
Also:
- The plan's action menu moved out of
plan-actions.tsxintodesktop/action-menu.tsx(no behaviour change) so the agent's ⋯ draws the same menu. - The rewrite generator (
function-rewrites.ts) learned thecoderservice that #1871 hand-added tovercel.json; regenerating had been dropping it silently.vercel-services.test.tsnow holds every service through a regeneration. githubReachingin the test fake takes an optional repository script, beside O0'speople.
Tests
Server: coder-host-published.test.ts holds the transcript scan alone (branch from push / switch -c / checkout -b / --head / prefix, HEAD names nothing, newest wins, a branch in a tool's answer names nothing; a PR address on another repository or in the plan's own words names nothing, case folds). coding-agents-app.test.ts answers the route over the scripted GitHub and PGlite: PR found and mapped with sizes and checks; a second ask inside the TTL reads GitHub no more, one past it sees draft, merged, and closed and the checks moving through pending, failing (a run or a status), none, and a 403 on check runs; branch with no PR → chip, branch never pushed → nothing, a PR opened from the branch later → found by head; rows naming neither ask GitHub nothing; another account's agent 404, POST 405. Plus the client, the host composer, the act router, and the wire goldens.
Desktop: agent-tab.test.ts renders the pill in every state and every check word, the chip, nothing before the service has said, and the exact menu labels; pressing items asserts the exact copied text and the exact URLs opened (PR, Files tab, compare page); the summary row's text and Open. use-agent-pull-request.test.ts drives the read moments on an injected clock: hidden reads nothing, shown reads once, pages inside the gap don't, past it do, the agent ending reads at once, a failed answer leaves what was drawn, another agent's tab starts fresh and ignores the first's late answer. coding-agent-model.test.ts covers the checkout command, the changes URL, the summary line, and the row's gate.
agent-notices.test.ts holds the notice: four agents ending together are announced in order as opened #123 / finished / finished / failed, only the completed ones read, the dots set ahead of the reads.
./scripts/check.sh exits 0. CI could not verify the Mac: verify.sh was waived for this work by Dean. The evidence is a headless Chrome render of the header in every state and of a finished transcript ending on the summary row, against the compiled stylesheet, in the Conductor workspace at .context/agent-pr-card.png (synced to the Mac while file sync is on).
Coordination
- O5 (#1899) merged first; the "opened #N" notice is wired here.
- O2 owns Stop and the composer; nothing here touches
agent-tab-foot.
🤖 Generated with Claude Code