feat(panel,ui): copyable branch chips and a polished Pull request header
Summary
The developer asked to make branch names copyable on click, as stage-cli does, and to improve the design of the rest of the Pull request tab's header. This is a polish, not a re-layout: every control stays in its row and keeps working as before (the state menu, inline title edit, merge control, pickers, stack nav, fold on scroll, narrow-width rules).
Copyable branches (pull-request-branches.tsx)
- Base and head are compact mono chips:
@luke/uiChippill, compact (20px, the status pill's size), withChipName mono. A long name is cut in its middle so its last words stay whole (luke/a-very-long-branch-name-that-…the-header). The full name is in the tooltip. - A click copies the name through the app's usual copy feedback (
useCopyFeedback/CopyGlyph, as in Files changed's path copy). The chip's glyph is the branch at rest, the copy glyph on hover (mouse only) or keyboard focus, and the check for 1.2s once copied, with the tooltip saying "Copied". - Each chip is a real button named "Copy branch ".
Header polish
-
Pill position: the title row aligns to its top, and the pill is centred in the first line's height, so it stands level with the first line of a wrapped title, as does the merge control.
-
Title wrapping: the space before
#Nnow sits inside the title's no-wrap end, so#N(and the pencil) always wrap with the last word and never stand alone. -
Meta row: consistent 16px gaps with a thin hairline between groups (branches · author/time · reviewers and pickers · labels). A hairline that would start a wrapped line is clipped. The row now starts at the pill's edge.
-
Author and time: one phrase, "[face] You opened 3h ago", and once merged "[face] You opened · merged 1h ago".
-
Reviewers: the people glyph is gone. Each face's state badge is a filled disc in its tone with the glyph cut out in the ground colour: approved a green ✓, changes requested a red ✕, requested an amber dot, commented and dismissed grey. The badges are spaced so the
+Ncount no longer overlaps them. Hover cards are unchanged. -
Pickers: both read as a pair, "+ Reviewer" and "+ Label", in a new quiet
subtleButton look (muted ink, medium weight). They keep their accessible names "Edit reviewers" / "Edit labels". Their order is unchanged: they stand beside each other, and labels close the line, as the existing test holds. -
Folded Review press: below 650px the Review press at the tab row's end hid its word but kept the labelled button's padding and the gap before its empty words, so it stood as a wide box with its glyph off-centre. It's now
@luke/ui'sFoldingButton, folded by the row's--luke-fold: a square icon button the control's height, glyph centred, the same hover and open fill, with the hint "Review" and its accessible name. The pending-review count now rides the glyph's corner, whether the word is drawn or folded.FoldingButtongained a fullername,aria-haspopup/aria-expanded/aria-controlsand aref, so a press that opens a popup can be one. The header's merge control has no folded icon form (a narrow header draws its shorter words), so it doesn't have this bug. -
Full-width header: in a side panel the header and its row of tabs no longer keep to the reading measure (
--reading-room), so on a wide panel the merge control and Review stand at the panel's edge, at the tab strip's own--column-inset. The views under them (Conversation, Checks, Files changed) keep their measure. The fold on scroll and the narrow-width rules are unchanged, and their tests pass.
@luke/ui additions (variants, as AGENTS.md asks)
ChipNamecut={CHIP_CUT.MIDDLE}(words alone): keeps the name's last few characters, starting after a/ - _ . :join, and gives way from the end of the start.BUTTON_VARIANT.SUBTLE: a quiet button in the muted ink. The catalog draws it with the other variants.FoldingButton:name, the popup attributes andref(above).
Not in this PR: a fork head shown as owner:branch. The wire doesn't carry the head's owner, so that needs an optional field on the overview read and a service change, which would need a Preview run. It will follow as its own small PR. A stage-cli style merge control will also follow, stacked on this one.
Screenshots
These come from a harness that mounts the real AgentPullRequestTab with fixture reads over the built styles.css. Linux has no SF, so Geist and JetBrains Mono stand in for the system fonts. Avatars don't load in the sandbox, so faces show initials, and bots show their robot glyph.
Open, 900px
| Light | Dark | |
|---|---|---|
| Before | ![]() | ![]() |
| After | ![]() | ![]() |
Open, 600px
| Light | Dark | |
|---|---|---|
| Before | ![]() | ![]() |
| After | ![]() | ![]() |
Merged, 900px
| Light | Dark | |
|---|---|---|
| Before | ![]() | ![]() |
| After | ![]() | ![]() |
Merged, 600px
| Light | Dark | |
|---|---|---|
| Before | ![]() | ![]() |
| After | ![]() | ![]() |
Long wrapping title and branch, 900px
| Light | Dark | |
|---|---|---|
| Before | ![]() | ![]() |
| After | ![]() | ![]() |
Long wrapping title and branch, 600px
| Light | Dark | |
|---|---|---|
| Before | ![]() | ![]() |
| After | ![]() | ![]() |
Branch chip hovered, and its copied state
| Light | Dark | |
|---|---|---|
| Hover, 900px | ![]() | ![]() |
| Copied, 900px | ![]() | ![]() |
| Hover, 600px | ![]() | ![]() |
| Copied, 600px | ![]() | ![]() |
Folded Review press, 600px (hovered, and with 2 pending comments)
| Light | Dark | |
|---|---|---|
| Before, hovered | ![]() | ![]() |
| After, hovered | ![]() | ![]() |
| Before, 2 pending | ![]() | ![]() |
| After, 2 pending | ![]() | ![]() |
Full-width header in a side panel, 1400px and 900px
The PR here is ready to merge, so the old merge control stands at the row's end. The stacked #4159 replaces it.
| Light | Dark | |
|---|---|---|
| Before, 1400px | ![]() | ![]() |
| After, 1400px | ![]() | ![]() |
| Before, 900px | ![]() | ![]() |
| After, 900px | ![]() | ![]() |
Evidence
pnpm check:affected: lint passed after fixes.@luke/panel#typecheckwas killed for memory (137) in the sandbox, so I ran each piece separately:tscfor@luke/ui,@luke/panel,@luke/catalog: passvitest run --project ui --project catalog: 88 files, 636 tests passvitest run --project panel: 265 files pass. The one failure on the first run (touch-hover.test.ts, a hover reveal outside@media (hover: hover)) is fixed, andsrc/styles/reruns green../scripts/oxlint.sh: 0 errors.pnpm knip: clean../scripts/repository-checks.sh: pass.cd apps/desktop && node scripts/build.mjs: builds within the bundle budget.
- New tests (+7):
agent-pull-request-tab.test.ts: each branch chip is a tabbable button named "Copy branch …". A click copies the name, the glyph says Copied, the name stays, and the check goes after its moment (onTestClock). The number stays with the title's last word. The author and time read as one phrase, open or merged.pull-request-header-narrow.test.ts: the title row aligns its pill and controls to the first line, wide and narrow; in a side panel at 900 and 1400px the header and the tab row never pad to the reading measure (fails without the fix).chip.test.ts: a long name cut in its middle keeps its whole text and its end; a short one isn't cut.folding-button.test.ts: a folding button that opens a popup says so and whether it's open, is named in full, and hands over its button.pull-request-tabs-narrow.test.ts: at 600px Review folds as every folding button does (updated from "its word is hidden").
- All 93 existing
agent-pull-request-tabtests, the people tests and the narrow-header tests pass unchanged.
Notes
- Desktop, panel and
@luke/uionly: no service change, so no Preview run. - Branch is
luke/…rather than a Conventional Branch prefix, because the service wakes agents onluke/branches.















































