feat(desktop,service,features): labels, compact reviewers, and stack navigation in the Pull request tab's header

Merged
DA
4d ago
+1403-48

Summary

This is F1 of the "Reviewers, labels and stack navigation" stack. It adds labels, compact reviewers and stack navigation to the Pull request tab's fixed header. Everything is read-only. The reviewer and label pickers come next in F2.

  • Labels sit under the title as pills in their GitHub colours (pull-request-labels.tsx), using stage-cli's colour maths.
    • Light appearance: a solid fill. The text is #24292f or white, chosen by Rec. 709 lightness at GitHub's 0.453 threshold. The edge is 15% darker, so a white label still shows on a white ground.
    • Dark appearance: a 12.5% tint with a 25% edge, and the text in the label colour. For a colour too dark to read on a dark ground, the text and edge are lifted toward white until the text's lightness reaches 0.6. stage-cli doesn't do this.
    • A colour GitHub spells any other way gets a quiet pill.
  • Reviewers are compact, as in stage-cli's header: a people glyph, 3 faces, then +N.
    • The author is left out and bots go last.
    • A "changes requested" stays standing when that reviewer is asked again, as stage-cli does. Anyone else asked again shows as requested.
    • Until the reviews arrive, the requested reviewers show as requested, with no spinner.
  • Stack navigation: a Stack 3/5 button beside the branches (pull-request-stack.tsx). Its menu lists the whole stack tip first, with drafts marked; the current PR is checked and can't be pressed.
    • The stack is worked out stage-cli's way (packages/service/src/github/pull-request-stack.ts): open, same-repository PRs only, an edge only where the PR is the sole one targeting that branch, the whole chain walked both ways, cycles refused, and at most 30 entries. A PR from a fork is never in a stack.
  • Opening a stacked PR: the service names, for each stacked PR, the newest non-archived agent in the workspace whose published pull request is that number. It finds this in the database (listCodingAgentsOnPullRequest), with no GitHub call. The header then opens:
    • that agent's Pull request tab on its own task, whether that is this task or another, through the window's Luke link opener (LINK_PLACE.CHANGES), which routes to showPanelTab(planId, { changes });
    • the agent's own page, for an agent on no task;
    • GitHub, where no agent owns the PR.

Screenshots

From a throwaway harness: the tab with a fake window.luke and the built styles.css, five labels, three reviewers asked plus two standings, and a stack of four.

DarkLight
Header, darkHeader, light
Stack menu, darkStack menu, light

GitHub requests (measured against the test double)

ReadBeforeAfter
Header read, PR open or draft1 REST (GET /pulls/{n})1 REST + ⌈open PRs / 100⌉ GraphQL (OpenPullRequests, at most 5, run in parallel with the REST read). This repository has about 117 open PRs, so that's 2.
Header read, PR merged or closed (by its last published state)1 REST1 REST, no extra request
Labels and reviewersn/a0 extra, both from the existing REST read (reviewer states from the reviews the conversation read already fetches)

The GraphQL query lists only number title url isDraft headRefName baseRefName isCrossRepository per node, at 1 point a page. Every open PR has to be read, because a PR left out could be the second on a branch, which unmakes an edge. So the read follows endCursor up to 5 pages, and a listing cut short means no stack rather than a false one. If a read fails or GitHub answers only with errors, the stack is left out and a warning is logged; the header still draws.

Wire

codingAgentOverviewPullRequestAnswerSchema gains two optional keys, so older services and desktops still agree:

  • labels: name and colour each, at most 100.
  • stack: number, title, url, draft, and an optional agent ({ id, taskId | null }), at most 30.

That accounts for the two JSON Schema golden diffs. They add these keys and nothing else.

Evidence

  • pnpm check:affected: typecheck was OOM-killed in this sandbox (exit 137), so I ran the per-package fallback:
    • tsc for @luke/desktop, @luke/service and @luke/features: pass.
    • ./scripts/oxlint.sh: 0 warnings, 0 errors.
    • pnpm knip: pass.
    • pnpm discarded-effect: pass.
    • ./scripts/repository-checks.sh: pass.
  • pnpm exec vitest run on the Pull request tab, the Changes tab, packages/service/src/github/ and the wire goldens: 15 files, 454 tests passed.
  • LUKE_UPDATE_FIXTURES=1 pnpm exec vitest run packages/features/src/hosted-wire-schemas.test.ts: re-recorded the two overview pull request goldens.
  • node scripts/build.mjs in apps/desktop: passes, and the bundle stays within budget (not re-recorded).

New tests:

  • pull-request-stack.test.ts (8): chain walked both ways, no stack, shared base, shared head, fork, cycle, current PR missing from the listing, bound.
  • pull-request-overview.test.ts (+3): labels and a stack named by the agents that published each PR (a newer agent on the same branch and a stranger workspace's agent name nothing); a second child on page 2 unmaking an edge, and a forked PR in no stack; no stack for a merged PR, a refused GraphQL read, or an unasked one.
  • agent-pull-request-tab.test.ts (+4): labels' colours, compact reviewers before and after the reviews land, the stack menu's order and its three routes (CHANGES on this task and on another, AGENT for an agent on no task, GitHub for no agent), and no stack.

Invariants

  • ./scripts/check.sh green. (CI)
  • JSON Schema goldens changed only by the two optional keys above.
  • Every behavior change has, in this PR, the test that fails without it; no change-detector test added.
  • Gateway envelope goldens unchanged.
  • No new as type assertion.
  • No Effect.runPromise/runSync/runFork outside the edges.
  • No new raw async primitives.
  • No discarded Effect.
  • Test count: +15 (8 stack, 3 service overview, 4 desktop).
  • No hand-rolled file replaced.
  • No AGENTS.md sentence names a changed part.
  • PRIVACY.md unchanged.
  • Package deps unchanged.
  • No Node-reaching module behind a renderer door.

Notes

  • Requested teams are not shown, matching stage-cli. F2's picker adds them.
  • An agent is matched by the pull request it published, as last read. A PR whose agent hasn't had its published answer read yet opens on GitHub.
  • The stack read is skipped for a PR whose last published state was merged or closed. A PR reopened since then gets its stack back once that short-lived answer is re-read.

Made with Luke


Devin Review


Summary by cubic

Makes the Pull request tab's fixed header read-only: labels under the title, reviewers as three faces plus a count, and Stack n/m navigation to the rest of the stack.

Stack navigation

  • The stack is worked out from GraphQL reads of the repository's open pull requests, page by page up to five, run in parallel with the header read and only when the PR is open or draft; a pull request from a fork stands in no stack.
  • Each stacked PR is named by the workspace agent that published that pull request, found in the database; opening one goes to that agent's Pull request tab on its own task, to its page when it has no task, or to GitHub when no agent owns it.
  • A Stack 3/5 button sits beside the branches; its menu lists the stack tip first, marks drafts, and disables the current PR.

Label and reviewer styling

  • Labels use GitHub's colors: solid fill with text chosen by Rec. 709 lightness at GitHub's 0.453 threshold in light mode; in dark mode a 12.5% tint, words in the label color, lifted toward white when too dark to read, with the edge lifted the same way. Invalid colors draw a quiet pill.
  • Reviewers drop the author, put bots last, and let a "changes requested" stand when that reviewer is asked again.

The wire gains two optional keys (labels, stack) so older services and desktops still agree.