feat(desktop): lay the Pull request tab out as a fixed header over Conversation, Checks, and Files changed tabs

Merged
DA
2d ago
+668-249

Follows #3239 (merged). This is the layout the developer chose for the agent's Pull request tab: "a tabbed approach below the header info of the PR … a fixed section and then tabs beneath, reminiscent of stage-cli." It's modelled on stage-cli's pull-request-layout.tsx and pull-request-header.tsx. The developer approved the screenshots below in their inbox.

Summary

  • Fixed header that stays put while the tab under it scrolls:

    • the bar: state pill on the left, GitHub and ⋯ on the right;
    • the title and #number, then base ← head;
    • who opened it and when, and the reviewers' facepile (requested reviewers over standings from reviews, as in #3239);
    • while the PR is open, the one-line merge status.

    It draws at once from the published summary (control.pullRequests) and fills in as the pull request read lands, with #3239's skeletons and per-part Retry.

  • Tab row under the header, replacing the Overview | Files changed Segmented (a form control). It's an underlined secondary row in Luke's look, with role="tablist"/tab/tabpanel and arrow keys moving along it. Each tab is a glyph, a name and a quiet count:

    • Conversation (MessagesSquare): the description, then the comments, reviews and review threads. Counts entries + earlier entries left on GitHub + threads.
    • Checks (CircleCheck): the checks list. From the first frame it wears the published checks summary as its glyph in its colour (✕ red, ● amber, ✓ green, nothing for none), the same rule as the pill's mark, which costs no extra GitHub request. The number joins it once the checks read lands.
    • Files changed (FileDiff): AgentChangesTab mounted whole, its own FileViewBar and tree, counted +N −M from the published summary. I agreed with the Pierre diffs agent to leave its bar untouched, so its Diff settings button lands there without a move.
  • Reads: the header's reads start when the tab shows: the pull request, and the conversation, because its reviews give each reviewer's standing in the facepile. That way a tab opened on Files changed still shows who approved. Checks reads when its tab is first shown. Refresh (⋯) re-reads the header's reads, plus the checks if their tab has been shown. Each tab stays drawn once shown, so switching back reads nothing again.

  • Failures: only a read that says there's no pull request yet takes the header and tabs away. A failed read says so in its own part with its own Retry; the tab row and Files changed stay and read on their own. This replaces #3239's single centred failure for the case where nothing is held.

  • Kept: the per-agent view memory (pullRequestViews in use-side-panel.ts), now conversation | checks | files (it's held for the window's life, so nothing stored needs migrating). The PR chip opens Conversation and the Changes chip opens Files changed.

  • Narrow panel: below 520px, tabs you're not on keep only their glyph and count. Their name is visually hidden but stays readable to a screen reader.

Screenshots

These come from a throwaway harness that mounts the tab alone. The harness can't run the Pierre diff renderer, so the thread preview and the Files changed diff show "The diff could not be shown." In the app they draw the diff.

Conversation, 620px, darkChecks, 620px, lightFiles changed, 620px, dark
Reads still out, 400px, light (Checks already shows the published ✕)Conversation, 400px, dark

Evidence

  • pnpm check:affected: exit 0 (before the rebase onto main; after it, the touched desktop tests and the desktop typecheck pass again).
  • pnpm exec vitest run apps/desktop/src/renderer/tasks/agent-pull-request-tab.test.ts apps/desktop/src/renderer/desktop/side-panel.test.ts: 16 passed.

New tests, each of which fails without this change:

  • "opened on its files, as the Changes chip opens it, the header stands above them with where each reviewer stands, and switching between the tabs draws each as it was".
  • "where nothing of the pull request is held and its reads fail, each part says so with its own Retry, and Files changed still reads on its own".
  • "each tab counts what Luke holds of it: the conversation its comments, reviews, and threads; the checks how they went as published from the first frame, and how many once read; the files the lines published". This includes the arrow keys moving along the row.

The existing tab tests now choose the Checks tab before asserting on checks. The progressive test checks that the Checks skeleton appears only once that tab is shown.

Invariants

  • ./scripts/check.sh green. (CI)
  • JSON Schema goldens unchanged.
  • Every behavior change has, in this PR, a test that fails without it. No change-detector test.
  • Gateway envelope goldens unchanged.
  • No new as type assertion.
  • No Effect.run* outside the edges.
  • No new raw async primitive.
  • No discarded Effect.
  • Test count: +1 renderer test (the old switch test is replaced by the header-over-files test, the all-failed test is rewritten, and the tab-counts test is new).
  • No hand-rolled file replaced.
  • No AGENTS.md sentence names the changed parts.
  • PRIVACY.md unchanged: the same reads, now started per tab.
  • Deps unchanged.
  • Barrel rule unaffected.

Notes

  • Three follow-up chains build on this branch: Conversation filters and reactions, Checks extras, and labels / reviewers / stack nav. So the header, the tab row and how the Conversation and Checks panes mount should stay stable.