feat(panel,ui): copyable branch chips and a polished Pull request header

Merged
DA
5d ago
+721-146

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/ui Chip pill, compact (20px, the status pill's size), with ChipName 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 #N now 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 +N count no longer overlaps them. Hover cards are unchanged.

  • Pickers: both read as a pair, "+ Reviewer" and "+ Label", in a new quiet subtle Button 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's FoldingButton, 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. FoldingButton gained a fuller name, aria-haspopup/aria-expanded/aria-controls and a ref, 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)

  • ChipName cut={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 and ref (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

LightDark
Beforebefore open 900 lightbefore open 900 dark
Afterafter open 900 lightafter open 900 dark

Open, 600px

LightDark
Beforebefore open 600 lightbefore open 600 dark
Afterafter open 600 lightafter open 600 dark

Merged, 900px

LightDark
Beforebefore merged 900 lightbefore merged 900 dark
Afterafter merged 900 lightafter merged 900 dark

Merged, 600px

LightDark
Beforebefore merged 600 lightbefore merged 600 dark
Afterafter merged 600 lightafter merged 600 dark

Long wrapping title and branch, 900px

LightDark
Beforebefore long 900 lightbefore long 900 dark
Afterafter long 900 lightafter long 900 dark

Long wrapping title and branch, 600px

LightDark
Beforebefore long 600 lightbefore long 600 dark
Afterafter long 600 lightafter long 600 dark

Branch chip hovered, and its copied state

LightDark
Hover, 900pxhover light 900hover dark 900
Copied, 900pxcopied light 900copied dark 900
Hover, 600pxhover light 600hover dark 600
Copied, 600pxcopied light 600copied dark 600

Folded Review press, 600px (hovered, and with 2 pending comments)

LightDark
Before, hoveredbefore review lightbefore review dark
After, hoveredafter review lightafter review dark
Before, 2 pendingbefore pending lightbefore pending dark
After, 2 pendingafter pending lightafter pending dark

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.

LightDark
Before, 1400pxbefore 1400 lightbefore 1400 dark
After, 1400pxafter 1400 lightafter 1400 dark
Before, 900pxbefore 900 lightbefore 900 dark
After, 900pxafter 900 lightafter 900 dark

Evidence

  • pnpm check:affected: lint passed after fixes. @luke/panel#typecheck was killed for memory (137) in the sandbox, so I ran each piece separately:
    • tsc for @luke/ui, @luke/panel, @luke/catalog: pass
    • vitest run --project ui --project catalog: 88 files, 636 tests pass
    • vitest 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, and src/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 (on TestClock). 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-tab tests, the people tests and the narrow-header tests pass unchanged.

Notes

  • Desktop, panel and @luke/ui only: no service change, so no Preview run.
  • Branch is luke/… rather than a Conventional Branch prefix, because the service wakes agents on luke/ branches.