feat(web): refresh pull request details#7148

merged7
main
ui-refresh/pull-requests
openedAug 16, 2026byMmaria
View on GitHubTwo-way sync enabledPR last imported 3d ago

What changed

  • Refreshes Pull Request list and detail geometry, filters, sticky metadata, branch actions, stacked-base state, and loading skeletons.
  • Keeps expanded detail spacious while using a compact header only after scroll.
  • Makes repository, PR, branch-copy, checkout, conflict, reviewer, label, and timeline controls consistent at narrow and wide widths.

Screenshots

Direct parent on the left; this PR on the right. Same viewport and copied application state.

BeforeAfter
Pull Requests before Pull Requests after
## Why

This is a large visual review on its own. Separating it keeps workspace primitives below and terminal, composer, and tool-call behavior out of the diff.

Validation

  • Pull Request filter and detail logic tests
  • Pull Request link tests
  • Affected-package typechecks
  • Changed-file lint and formatting checks

Stack order

  1. #7153 workspace and navigation
  2. #7147 Usage
  3. #7148 Pull Requests, this PR
  4. #7149 terminal
  5. #7150 composer
  6. #7151 tool lifecycle projection
  7. #7152 tool activity UI

Built with GPT-5.6-sol in the Codex harness.


Note

Medium Risk Large UI refactor across PR detail, list, and VCS/ref parsing; conflict action ordering and new stacked-PR heuristics affect merge/checkout flows, though logic is covered by added tests.

Overview Pull request detail and list get a visual and interaction pass: shared WorkspacePageHeader / WorkspacePageContainer, filter and refresh controls aligned with other workspace pages, and the right-panel toggle only when a PR is selected (with a clearer unavailable tooltip).

PullRequestDetailPanel drops the chromeVariant prop and always collapses header metadata on scroll, with tighter top chrome and segmented tab toggles. Resolve conflicts becomes the primary header action when merge conflicts exist (ahead of draft-ready / merge). The repository name can open the repo root via new changeRequestRepositoryUrl. Stacked PRs are inferred from local VCS refs (isStackedPullRequestBase) and shown with a layers icon on the base branch. Loading uses a full-panel PullRequestDetailGhost instead of per-tab ghosts inside the scroll area.

PanelLayoutControls wraps disabled toggles so tooltips still work on unavailable terminal/right-panel buttons. pullRequestHandoffLabels no longer exposes resolve/conflicts strings (conflicts are handled in the header). Summary meta rows use a fixed label column grid.

Reviewed by Cursor Bugbot for commit eee6a0458dc34f61e7727095e6d4ba361eb2b776. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Refresh pull request detail panel with conflict resolution, stacked PR indicators, and usage page redesign

  • Overhauls PullRequestDetailPanel: chrome always collapses on scroll, conflict resolution is promoted to a primary destructive button, tab nav switches to a segmented ToggleGroup, and the repository label becomes a clickable external link.
  • Adds isStackedPullRequestBase helper and a VCS refs query to detect stacked PRs, showing a LayersIcon indicator in the header when the base branch is not the repo default.
  • Redesigns the Usage page with responsive breadcrumb controls (segmented toggles on desktop, selects on mobile), fixed provider order with session counts, and a simplified Totals section without detail subtext.
  • Adds per-provider sessions tracking to mergeUsage so providers with sessions but zero spend appear in the provider list.
  • Adds changeRequestRepositoryUrl to derive a repository root URL from a PR URL across GitHub, GitLab, Bitbucket, and Azure DevOps.
  • PanelLayoutControls tooltips stay accessible when toggles are disabled; the right-panel toggle on the pull requests route requires an active PR selection.
  • Behavioral Change: pullRequestHandoffLabels no longer returns resolve or resolveConflicts keys; callers that depended on those entries will receive only fix-related labels.

Macroscope summarized 75f0625.

Synced from GitHub

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

βš™οΈ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d67f22f2-1d20-4028-bca6-35583948dbe8

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • πŸ” Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❀️ Share

Comment @coderabbitai help to get the list of available commands.

apps/web/src/routes/_chat.pull-requests.tsxline 1947Outdated Β· 8c3da41Open thread
……@@ -1909,19 +1914,43 @@ function PullRequestsColumn({
1944+ onRefresh: () => void;
1945+}) {
1946+ return (
1947+ <Button
MmacroscopeappBoton line 1947Aug 16, 2026

🟑 Medium routes/_chat.pull-requests.tsx:1947

While refreshing is true, repeated clicks still invoke onRefresh, so duplicate invalidations start the list, partitions, stats, and detail refresh cascade concurrently. Disable the Button with disabled={refreshing} or otherwise guard the click, matching the existing empty-state refresh controls.

βœ… Resolved in 89fac2a9dc591dd8d696ec484495ed574ac41650

apps/web/src/components/pullRequest/pullRequestDetail.logic.tslines 83–83Outdated Β· 8c3da41Open thread
……@@ -73,6 +74,15 @@ export function pullRequestActionMenuHasGroup(
80+): boolean {
81+ const defaultRef = refs.find((refName) => refName.isDefault);
82+ if (!defaultRef) return false;
83+ return defaultRef.name !== baseBranch && !defaultRef.name.endsWith(`/${baseBranch}`);
MmacroscopeappBoton lines 83–83Aug 16, 2026

🟑 Medium pullRequest/pullRequestDetail.logic.ts:83

isStackedPullRequestBase treats origin/feature/main as matching base branch main, so distinct branches are reported as non-stacked and the stacked-PR indicator is hidden. The unrestricted endsWith check matches nested branch names; strip only the remote-name prefix before comparing with baseBranch.

βœ… Resolved in 89fac2a9dc591dd8d696ec484495ed574ac41650

Synced from GitHub

Thread transfer impact

βœ… Thread transfer remains within every enforced ceiling.

ℹ️ The exact PR base did not have a successful artifact. Baseline uses the latest successful main measurement shown below.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire12.6 KiB12.6 KiB+23 B (+0.2%)15.1 KiBβœ…
CodexThread snapshot wire6.3 KiB6.3 KiB+1 B (+0.0%)7.3 KiBβœ…
CodexLive turn WebSocket wire6.3 KiB6.4 KiB+22 B (+0.3%)7.8 KiBβœ…
CodexLive turn WebSocket decoded51.8 KiB51.8 KiB0 B (0.0%)66.4 KiBβœ…
CodexLive turn messages16160 (0.0%)21βœ…
ClaudeTotal thread wire12.6 KiB12.6 KiBβˆ’2 B (βˆ’0.0%)15.1 KiBβœ…
ClaudeThread snapshot wire6.3 KiB6.3 KiBβˆ’8 B (βˆ’0.1%)7.3 KiBβœ…
ClaudeLive turn WebSocket wire6.3 KiB6.3 KiB+6 B (+0.1%)7.8 KiBβœ…
ClaudeLive turn WebSocket decoded52.7 KiB52.7 KiB0 B (0.0%)66.4 KiBβœ…
ClaudeLive turn messages16160 (0.0%)21βœ…

Baseline: 3c06655 Β· PR result: eee6a04 Β· Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 99.9 KiB
  • Claude decoded thread snapshot: 100.6 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

apps/web/src/lib/openPullRequestLink.tsline 130Outdated Β· 8c3da41Open thread
……@@ -118,6 +118,17 @@ export function parseChangeRequestUrl(targetUrl: string): ChangeRequestLink | nu
127+ url.search = "";
128+ url.hash = "";
129+ return url.toString();
130+}
CcursorBoton line 130Aug 16, 2026

Repo link lowercases the opened path

Low Severity

changeRequestRepositoryUrl rebuilds the repository path from parseChangeRequestUrl, which lowercases the captured path for identity matching. The opened URL therefore loses the host’s original casing and can 404 on case-sensitive GitLab or similar installs, while the pull request number still uses the original detail.url.

Reviewed by Cursor Bugbot for commit 8c3da41600c7ab9da4c3c561ce9057bbdbc48943. Configure here.

……@@ -73,6 +74,15 @@ export function pullRequestActionMenuHasGroup(
81+ const defaultRef = refs.find((refName) => refName.isDefault);
82+ if (!defaultRef) return false;
83+ return defaultRef.name !== baseBranch && !defaultRef.name.endsWith(`/${baseBranch}`);
84+}
CcursorBoton line 84Aug 16, 2026

Stacked check over-matches branch suffixes

Low Severity

isStackedPullRequestBase treats any default ref that merely ends with /${baseBranch} as the same branch. That is meant for origin/main versus main, but it also matches unrelated names such as release/1.0 versus 1.0, so a stacked pull request can lose the stacked indicator.

Reviewed by Cursor Bugbot for commit 8c3da41600c7ab9da4c3c561ce9057bbdbc48943. Configure here.

Synced from GitHub

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable β€” This PR introduces new features including stacked pull request detection (with LayersIcon indicators), clickable repository links that open externally, and changes the priority logic for action buttons. The scope of new user-facing behavior and UI restructuring warrants human review.

No code changes detected at eee6a04. Prior analysis still applies.

You can add or adjust custom eligibility rules. Learn more.

MmacroscopeappBotreviewed Β· 2 inline threadsAug 16, 2026
Synced from GitHub

Reviewed the pull-request chrome rework against the shared primitive/Tailwind ownership rules. The primitive migrations look sound (SegmentedTab* for the tabs, Button behind MenuTrigger render in the filters menu with --control-icon-color carrying the pressed tone, WorkspacePageHeader/WorkspacePageContainer keeping drag-region, WCO inset and the collapsed-sidebar titlebar inset, ps-[8.5px] matching the existing leading-icon button call sites). Two layout regressions introduced by changed lines are noted inline.

Posted via Macroscope β€” UI Consistency

apps/web/src/components/pullRequest/PullRequestDetailPanel.tsxlines 1638–1641Outdated Β· 8c3da41Open thread
……@@ -1650,147 +1630,114 @@ export function PullRequestDetailPanel({
1638+ className={cn(
1639+ "col-span-2 flex min-w-0 items-center gap-1 overflow-x-auto border-t border-border/60 px-4 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden",
1640+ condensed ? "py-1.5" : "py-2",
1641+ )}
MmacroscopeappBoton lines 1638–1641Aug 16, 2026

pullRequest/PullRequestDetailPanel.tsx:1641

The tab nav's padding now changes with the chrome state (py-2 β†’ py-1.5), but the scroll compensation below still refunds only foldHeight - condensedRow height. The chrome loses 4px more than that when it collapses, so the content under the reader jumps up ~4px mid-scroll β€” exactly what the compensation exists to prevent. Keeping the nav height constant is the smallest fix; otherwise this row has to be measured into chromeDelta too.

suggestion
1 className="col-span-2 flex min-w-0 items-center gap-1 overflow-x-auto border-t border-border/60 px-4 py-2 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"

No longer relevant as of 0437edb65b7dc3483cfa26bb49e9366d6d2e60b0

apps/web/src/components/pullRequest/PullRequestSummaryTab.tsxlines 199–199Outdated Β· 8c3da41Open thread
……@@ -196,12 +196,12 @@ function MetaRow({
198198 return (
199- <div className="flex items-center gap-2 py-1.5 text-xs">
200- <span className="flex w-24 shrink-0 items-center gap-1.5 text-muted-foreground">
199+ <div className="grid min-h-8 grid-cols-[6rem_minmax(0,1fr)] items-center gap-2 text-xs">
MmacroscopeappBoton lines 199–199Aug 16, 2026

pullRequest/PullRequestSummaryTab.tsx:199

Trading py-1.5 for min-h-8 drops the row's vertical padding, and both the Reviewers and Labels cells are flex-wrap β€” once they wrap, the pills sit flush against the neighbouring rows. Keeping py-1.5 alongside min-h-8 preserves the 32px single-line rhythm the ghost mirrors and restores the gap when a row wraps.

suggestion
1 <div className="grid min-h-8 grid-cols-[6rem_minmax(0,1fr)] items-center gap-2 py-1.5 text-xs">

No longer relevant as of 0437edb65b7dc3483cfa26bb49e9366d6d2e60b0

MmacroscopeappBotreviewed Β· 1 inline threadAug 16, 2026
Synced from GitHub

One new finding on the conflict primary action. The two findings from the previous run (tab-nav padding vs. scroll compensation, and MetaRow losing its vertical padding) are still open on the current head and are not repeated here.

Posted via Macroscope β€” UI Consistency

apps/web/src/components/pullRequest/PullRequestDetailPanel.tsxlines 1393–1427Outdated Β· 89fac2aOpen thread
MmacroscopeappBoton lines 1393–1427Aug 16, 2026

pullRequest/PullRequestDetailPanel.tsx:1393

This button replaced the conflict row's only consumer of handoffLabels.resolve, so the shared label contract (pullRequestHandoffLabels, "names where a pull-request task will land, without letting each surface guess independently") now has an unused entry while this surface hardcodes its own copy. Since the click still hands off to a thread, the thread context loses the "in this thread" destination the contract exists to state. Reusing the shared label keeps the two consistent.

suggestion
1 {handoff === "conflicts" ? "Preparing..." : handoffLabels.resolve}

No longer relevant as of b45d74ed7e0e01be81d283140f980943dfa14849

apps/web/src/lib/openPullRequestLink.tslines 126–130Outdated Β· 1dd5875Open thread
……@@ -118,6 +118,22 @@ export function parseChangeRequestUrl(targetUrl: string): ChangeRequestLink | nu
127+ /^(.*?)(?:\/pull\/\d+|\/-\/merge_requests\/\d+|\/pull-requests\/\d+|\/pullrequest\/\d+)(?:\/|$)/iu.exec(
128+ url.pathname,
129+ )?.[1];
130+ if (!repositoryPath) return null;
MmacroscopeappBoton lines 126–130Aug 16, 2026

🟑 Medium lib/openPullRequestLink.ts:126

changeRequestRepositoryUrl returns https://gitlab.example/group for a valid nested GitLab URL such as .../group/pull/123/repo/-/merge_requests/42, so the repository link opens the namespace page instead of the repository. The provider-agnostic regex matches /pull/123 before reaching GitLab’s /-/merge_requests/42 marker; extract the GitLab repository path first, then fall back to the other provider patterns.

βœ… Resolved in 0437edb65b7dc3483cfa26bb49e9366d6d2e60b0

apps/web/src/components/HeaderFilterMenu.tsxline 47Outdated Β· 0437edbOpen thread
……@@ -37,8 +40,16 @@ export function HeaderFilterMenu<Value extends string>({
44+ key={option.value}
45+ value={option.value}
46+ disabled={option.disabledReason !== undefined}
47+ title={option.disabledReason}
MmacroscopeappBoton line 47Aug 16, 2026

🟑 Medium components/HeaderFilterMenu.tsx:47

Disabled options expose disabledReason only through title, but MenuRadioItem applies pointer-events-none and disabled items cannot receive focus. As a result, users see unavailable options with no way to discover why they are disabled; render the reason in visible/accessibly focusable UI or attach the tooltip to a non-disabled wrapper.

βœ… Resolved in 2dd5525de4577b23ff331860af354f54ead0a154

apps/web/src/components/pullRequest/PullRequestDetailPanel.tsxlines 1159–1159Outdated Β· 5cc755cOpen thread
……@@ -1131,38 +1141,22 @@ export function PullRequestDetailPanel({
11631157 <Button
11641158 aria-label="More pull request actions"
1165- className="size-6"
1159+ className="order-last size-6"
MmacroscopeappBoton lines 1159–1159Aug 16, 2026

pullRequest/PullRequestDetailPanel.tsx:1159

order-last paints the overflow menu at the end of this action row while it stays first in the DOM, so keyboard focus reaches "More pull request actions" before the Check out, primary action and Collapse controls that are drawn ahead of it β€” visual order and focus order no longer match. Moving the <Menu> block to the end of this flex row (and dropping order-last) keeps the two in sync without changing the layout.

No longer relevant as of 2dd5525de4577b23ff331860af354f54ead0a154

MmacroscopeappBotreviewed Β· 1 inline threadAug 16, 2026
Synced from GitHub

One finding on the refreshed detail-panel chrome: the actions row now sets order-last on the overflow menu, which desynchronises visual order from DOM/focus order. The previously reported dead pullRequestHandoffLabels.resolve entry is still open and has not been re-flagged here.

Posted via Macroscope β€” UI Consistency

apps/web/src/components/pullRequest/PullRequestDetailPanel.tsxlines 1159–1159Outdated Β· b73193dOpen thread
……@@ -1131,38 +1141,22 @@ export function PullRequestDetailPanel({
11631157 <Button
11641158 aria-label="More pull request actions"
1165- className="size-6"
1159+ className="order-last size-6"
MmacroscopeappBoton lines 1159–1159Aug 16, 2026

pullRequest/PullRequestDetailPanel.tsx:1159

order-last paints this trigger at the end of the actions row while it stays first in the DOM, so keyboard focus reaches "More pull request actions" before Check out and the primary action, and assistive tech reads the row in an order that no longer matches what is shown (it would also land after the panel-collapse button when onClose is passed). Moving the whole <Menu> block to after the primary-action button and dropping order-last keeps visual, DOM and focus order the same.

No longer relevant as of 2dd5525de4577b23ff331860af354f54ead0a154

apps/web/src/routes/_chat.pull-requests.tsxline 1648Outdated Β· b73193dOpen thread
……@@ -1627,37 +1639,18 @@ function CompactFilterMenu<Value extends string>({
1645+ value: option.value,
1646+ label: option.label,
1647+ icon: <option.Icon aria-hidden className="size-3.5" />,
1648+ ...(option.unavailable ? { disabledReason: option.unavailable } : {}),
MmacroscopeappBoton line 1648Aug 16, 2026

🟠 High routes/_chat.pull-requests.tsx:1648

An unavailable host with unavailable: "" is re-enabled, allowing selection of a source already known to be unreadable. This truthiness check omits disabledReason for empty strings; preserve definedness by checking option.unavailable !== undefined.

βœ… Resolved in 2dd5525de4577b23ff331860af354f54ead0a154

MmacroscopeappBotreviewed Β· 1 inline threadAug 16, 2026
Synced from GitHub

UI consistency review: one finding on the new segmented tab bar. Previously flagged items that are still open (order-last on the overflow-menu trigger, and the hardcoded "Resolve conflicts" label bypassing pullRequestHandoffLabels) are not repeated here.

Posted via Macroscope β€” UI Consistency

apps/web/src/components/pullRequest/PullRequestDetailPanel.tsxlines 1641–1644Outdated Β· c4b280bOpen thread
……@@ -1650,147 +1630,115 @@ export function PullRequestDetailPanel({
1641+ <ToggleGroup
1642+ size="segmented"
1643+ variant="segmented"
1644+ value={[tab]}
MmacroscopeappBoton lines 1641–1644Aug 16, 2026

pullRequest/PullRequestDetailPanel.tsx:1644

The segmented track treatment (rounded-lg bg-muted/45 p-0.5) is hand-written here, making this the fourth call site carrying the same three classes β€” UsagePage repeats them verbatim on each of its three segmented ToggleGroups. That is durable geometry for the segmented variant, not contextual layout, so it belongs in the primitive: ToggleGroup already branches on variant === "segmented" for its gap, and the radius, surface, and inset padding can join that branch. Call sites would then pass only variant="segmented" size="segmented" plus any local layout classes, and the track stays consistent if it is ever retuned.

Cross-file fix, so no inline suggestion: add the track classes to the segmented branch in apps/web/src/components/ui/toggle-group.tsx and drop the className here.

No longer relevant as of c4b280b249755ab463d28edee1532e856ae838a5

MmacroscopeappBotreviewed Β· 2 inline threadsAug 16, 2026
Synced from GitHub

UI consistency review of the pull-request surface refresh. Two call-site/primitive ownership notes below; everything else (shared WorkspacePageHeader/WorkspacePageContainer adoption, HeaderFilterMenu reuse, the MenuTrigger render={<Button/>} migration in PullRequestListFilters, and the ghost mirroring the loaded chrome) looks consistent with the shared component system.

Posted via Macroscope β€” UI Consistency

apps/web/src/components/pullRequest/PullRequestDetailPanel.tsxlines 1336–1336Outdated Β· c4b280bOpen thread
……@@ -1339,16 +1333,10 @@ export function PullRequestDetailPanel({
1349- <ChevronDownIcon className="size-3 text-muted-foreground" />
1350- </>
1351- )}
1336+ <Button size="xs" variant="outline" className="ps-[8.5px]">
MmacroscopeappBoton lines 1336–1336Aug 16, 2026

pullRequest/PullRequestDetailPanel.tsx:1336

ps-[8.5px] overrides the xs size's own inline-start padding at one call site, so the two size="xs" icon-leading buttons in this row no longer share a geometry: the neighbouring Resolve conflicts / Merge buttons keep the primitive's px-[calc(--spacing(2)-1px)]. The primitive already accounts for leading glyphs via [&_svg]:-mx-0.5.

Dropping the override keeps the row uniform; if the optical compensation is genuinely wanted for icon-leading buttons, it belongs in the Button size contract rather than here.

suggestion
1 <Button size="xs" variant="outline">

No longer relevant as of 2dd5525de4577b23ff331860af354f54ead0a154

apps/web/src/components/pullRequest/PullRequestDetailPanel.tsxlines 1642–1776Outdated Β· c4b280bOpen thread
MmacroscopeappBoton lines 1642–1776Aug 16, 2026

pullRequest/PullRequestDetailPanel.tsx:1642

This repeats the segmented track treatment (rounded-lg bg-muted/45 p-0.5) that UsagePage already sets on each of its three ToggleGroups, so with this call site the same durable geometry is spelled out in four places. ToggleGroup already special-cases variant === "segmented" (for the gap) but owns only the items, not the surface they sit in β€” a call site that forgets the string gets a visually different segmented control.

Consider moving the track classes into the segmented branch of ToggleGroup and leaving only contextual layout here.

No longer relevant as of 2dd5525de4577b23ff331860af354f54ead0a154

……@@ -1407,7 +1391,19 @@ export function PullRequestDetailPanel({
1403+ >
1404+ <MoreHorizontalIcon className="size-4" />
1405+ </MenuTrigger>
1406+ </Menu>
CcursorBoton line 1406Aug 16, 2026

Actions menu wraps unrelated controls

Medium Severity

The overflow actions Menu now wraps the checkout Menu, badges, and primary action buttons, with MenuTrigger after MenuPopup. Those controls used to be siblings in a fragment. Nesting an independent menu and non-menu actions under the overflow root ties them into the same menu tree and can break open/dismiss and focus behavior for checkout and the β‹― menu.

Reviewed by Cursor Bugbot for commit 2dd5525de4577b23ff331860af354f54ead0a154. Configure here.

CcursorBotreviewed Β· 1 inline threadAug 16, 2026
Synced from GitHub

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit ac244453615ce03ede0d2b210249f038c9b980e6. Configure here.

……@@ -1157,201 +1172,171 @@ export function PullRequestDetailPanel({
1313- <MenuSeparator />
1314- ) : null}
1315- </>
1316- ) : null}
CcursorBoton line 1316 (old)Aug 16, 2026

Unreachable conflict menu action

Low Severity

The overflow menu still gates Resolve conflicts with conflicting && primaryAction !== "resolve", but open conflicting PRs now always set primaryAction to resolve. That branch can no longer render, so the menu path is dead leftover from the old priority order.

Additional Locations (1)

Reviewed by Cursor Bugbot for commit ac244453615ce03ede0d2b210249f038c9b980e6. Configure here.

MmacroscopeappBotreviewed Β· 2 inline threadsAug 16, 2026
Synced from GitHub

Two consistency findings in the changed UI code: the actions-row Menu restructure now nests the Check out menu inside another Menu root, and the new inline disabledReason in HeaderFilterMenu renders an unbounded host-error sentence where the sibling filter menu keeps it in a capped tooltip. Details inline.

Posted via Macroscope β€” UI Consistency

apps/web/src/components/pullRequest/PullRequestDetailPanel.tsxlines 1181–1193Outdated Β· ac24445Open thread
MmacroscopeappBoton lines 1181–1193Aug 16, 2026

pullRequest/PullRequestDetailPanel.tsx:1181

Moving the trigger last does fix visual/DOM order, but wrapping the whole actions row in this <Menu> also puts the Check out <Menu> (line 1346) β€” plus the auto-merge Badge and the primary-action Button β€” inside the actions menu's React context. Base UI derives submenu behaviour from that context (MenuSub in ui/menu.tsx is just MenuPrimitive.SubmenuRoot, the same root component), so the Check out menu is now treated as a nested menu rather than an independent one: nested menus default to openOnHover and share the parent's floating tree, focus restoration and Escape handling. Nothing else in apps/web/src nests a Menu root outside a popup.

Keeping the overflow menu self-contained β€” Menu β†’ MenuTrigger β†’ MenuPopup β€” and rendering that whole block after the primary-action button keeps visual, DOM and focus order aligned without changing menu semantics.

No longer relevant as of 46c5c1ba3bca374dbff131076a47e5d063865899

apps/web/src/components/HeaderFilterMenu.tsxlines 55–55Outdated Β· ac24445Open thread
……@@ -38,8 +41,22 @@ export function HeaderFilterMenu<Value extends string>({
52+ {option.label}
53+ </span>
54+ {option.disabledReason !== undefined ? (
55+ <span className="ms-auto text-xs text-muted-foreground">
MmacroscopeappBoton lines 55–55Aug 16, 2026

disabledReason is fed the host's full failure sentence (e.g. "GitHub CLI (gh) is required to browse change requests on this host. Install it from https://cli.github.com/ and reload."), and this span has no width cap, so one unreadable host stretches the popup far past its min-w-40. The pill/menu row for the same option data keeps that string inside a max-w-80 tooltip (PullRequestListFilters.tsx:179-184), so the two treatments of one string also diverge. Capping the width lets the reason wrap inside the item instead of widening the menu.

suggestion
1 <span className="ms-auto max-w-56 text-xs text-muted-foreground">

Posted via Macroscope β€” UI Consistency

Removed from this PR when the shared filter control moved down to #7147 with its first consumer.

apps/web/src/routes/_chat.pull-requests.tsxline 1197Outdated Β· 986c2d5Open thread
……@@ -1181,6 +1184,17 @@ function PullRequestsRouteView() {
1194+ // A closed panel keeps its tabs so reopening does not discard work. Those retained tabs are
1195+ // history, though, not a current selection: without this check they leave the toggle looking
1196+ // available after the selected pull request has been cleared.
1197+ const rightPanelAvailable = activePullRequestSurface !== null || linkedSelectionMatchesSurface;
MmacroscopeappBoton line 1197Aug 17, 2026

🟑 Medium routes/_chat.pull-requests.tsx:1197

Closing the panel clears the linked selection, so rightPanelAvailable becomes false even when selectedPullRequestSurface still contains a retained tab. The layout toggle is then disabled and the user cannot reopen the panel; base availability on the retained selected surface instead.

βœ… Resolved in 46c5c1ba3bca374dbff131076a47e5d063865899

MmacroscopeappBotreviewed Β· 1 inline threadAug 17, 2026
Synced from GitHub

One finding: the newly added rightPanelUnavailableLabel can never be shown, because its tooltip trigger is the disabled Toggle itself.

Posted via Macroscope β€” UI Consistency

MmacroscopeappBoton outdated line 95Aug 17, 2026

chat/PanelLayoutControls.tsx:99

This label can't be reached, so the route's "Select a pull request first" never renders: the tooltip trigger is the Toggle, and while it is disabled toggleVariants' base disabled:pointer-events-none (plus the native disabled attribute Base UI puts on it) keeps hover and focus from ever opening the popup. The branch only runs when rightPanelAvailable is false, i.e. exactly when the control is disabled.

The established pattern here for explaining a disabled control is to hang the trigger on a wrapper element rather than on the disabled control β€” see GitActionsControl.tsx:1768-1785, where a disabled MenuItem sits inside a <span> trigger, and the menu rows that use data-disabled:pointer-events-auto. Rendering TooltipTrigger as a wrapping <span className="flex shrink-0" /> around the Toggle (leaving the Toggle disabled) keeps the disabled semantics and makes the reason visible.

No longer relevant as of 7d39c806200f28ca98850d29da23777149fed7cf

MmacroscopeappBotreviewed Β· 2 inline threadsAug 17, 2026
Synced from GitHub

UI consistency review of the pull-request surface refresh. Two small consistency findings on changed lines in PullRequestDetailPanel.tsx; the shared-primitive migrations elsewhere (segmented ToggleGroup for the tabs, Button for the filters trigger and refresh control, WorkspacePageHeader/WorkspacePageContainer, the span-wrapped tooltip trigger in PanelLayoutControls) all line up with the existing contracts.

Posted via Macroscope β€” UI Consistency

apps/web/src/components/pullRequest/PullRequestDetailPanel.tsxlines 1284–1284Outdated Β· 929b283Open thread
……@@ -1185,29 +1200,103 @@ export function PullRequestDetailPanel({
1281+ disabled={handoff !== null}
1282+ onClick={startResolveConflicts}
1283+ >
1284+ <TriangleAlertIcon className="size-3" />
MmacroscopeappBoton lines 1284–1284Aug 17, 2026

pullRequest/PullRequestDetailPanel.tsx:1284

This size="xs" button pins its glyph to size-3 while the Check out button now beside it in the same action row uses size-3.5, so two peers of the same size variant show differently sized icons. Matching the neighbour (or dropping the override entirely and letting the xs size contract pick size-4/sm:size-3.5) keeps the row uniform.

suggestion
1 <TriangleAlertIcon className="size-3.5" />

No longer relevant as of 696a2e10e7ace22568c0c6b8a62d571ab5854f69

apps/web/src/components/pullRequest/PullRequestDetailPanel.tsxlines 1673–1673Outdated Β· 929b283Open thread
……@@ -1660,60 +1636,71 @@ export function PullRequestDetailPanel({
1670+ render={
1671+ <button
1672+ type="button"
1673+ className="grid w-fit min-w-0 max-w-full shrink cursor-pointer rounded px-1 py-0.5 text-left outline-none transition-colors hover:bg-accent/45 hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring"
MmacroscopeappBoton lines 1673–1673Aug 17, 2026

pullRequest/PullRequestDetailPanel.tsx:1673

The rewritten copy-branch button keeps outline-none but drops focus-visible:ring-offset-1 focus-visible:ring-offset-background, so its keyboard focus ring now paints flush against the branch text, while every control around it β€” Button, Toggle, and this button's previous class string β€” draws the same ring with a background-coloured offset. Restoring the offset pair keeps one focus treatment across the row.

suggestion
1 className="grid w-fit min-w-0 max-w-full shrink cursor-pointer rounded px-1 py-0.5 text-left outline-none transition-colors hover:bg-accent/45 hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background"

No longer relevant as of 696a2e10e7ace22568c0c6b8a62d571ab5854f69

MmacroscopeappBotreviewed Β· 1 inline threadAug 17, 2026
Synced from GitHub

One consistency finding: the disabled-toggle tooltip fix landed on the right-panel control only, so the two sibling controls in PanelLayoutControls now compose their tooltips differently and only one of them can actually show its "unavailable" explanation. Everything else in this PR lines up with the shared system as far as I can tell β€” the filters trigger now inherits Button's icon geometry and routes its active tone through --control-icon-color, the tab strip uses the segmented ToggleGroup (track treatment owned by the primitive), the header migration matches the electron={isElectron} / reserveNativeControls pattern used by Settings, Usage and ChatView, and the detail ghost mirrors the chrome/summary geometry it stands in for (min-h-8 rows matching MetaRow).

Posted via Macroscope β€” UI Consistency

……@@ -60,41 +62,39 @@ export const PanelLayoutControls = memo(function PanelLayoutControls({
6062 </Tooltip>
6163 ) : null}
MmacroscopeappBoton outdated line 63Aug 17, 2026

chat/PanelLayoutControls.tsx:65

This wrapper is the right fix for a tooltip on a disabled control, but it is now the only one: the terminal control just above (lines 41–62) still renders its trigger as the Toggle, so while terminalAvailable is false, toggleVariants' base disabled:pointer-events-none plus the native disabled attribute keep hover and focus from ever opening "Terminal drawer is unavailable" β€” exactly the case that branch exists for. Two sibling controls in one component with the same need and two different compositions.

Consider hanging the terminal trigger on the same wrapper so both unavailable states explain themselves identically:

diff
1 <Tooltip>
2- <TooltipTrigger
3- render={
4- <Toggle
5- className="shrink-0 [-webkit-app-region:no-drag]"
6- pressed={terminalOpen}
7- onPressedChange={onToggleTerminal}
8- aria-label="Toggle terminal drawer"
9- variant="ghost"
10- size="sm"
11- disabled={!terminalAvailable}
12- >
13- <PanelBottomIcon className="size-4" />
14- </Toggle>
15- }
16- />
17+ <TooltipTrigger render={<span className="flex shrink-0" />}>
18+ <Toggle
19+ className="shrink-0 [-webkit-app-region:no-drag]"
20+ pressed={terminalOpen}
21+ onPressedChange={onToggleTerminal}
22+ aria-label="Toggle terminal drawer"
23+ variant="ghost"
24+ size="sm"
25+ disabled={!terminalAvailable}
26+ >
27+ <PanelBottomIcon className="size-4" />
28+ </Toggle>
29+ </TooltipTrigger>
30 <TooltipPopup side="bottom">

The new test could then cover both triggers with the same assertion.

No longer relevant as of a7586658ecd31f5b053350cdbcfd8d2d2f2ffcf5

MmacroscopeappBotreviewed Β· 1 inline threadAug 17, 2026
Synced from GitHub

One finding: the chrome fold's grid-row transition was dropped, so re-expanding the metadata snaps the content area down instead of easing. Everything else in this pass looks consistent with the shared primitives β€” the tooltip triggers now wrap both disabled panel toggles, the filters trigger and refresh controls go through Button, the tab strip uses the segmented ToggleGroup contract (track geometry owned by the primitive), and MetaRow/PullRequestDetailGhost share one row rhythm.

Posted via Macroscope β€” UI Consistency

apps/web/src/components/pullRequest/PullRequestDetailPanel.tsxlines 1560–1560Outdated Β· fb06af7Open thread
……@@ -1480,120 +1476,100 @@ export function PullRequestDetailPanel({
1579- : "grid-rows-[1fr] transition-[grid-template-rows] duration-200 ease-out motion-reduce:transition-none",
1580- )}
1581- >
1560+ <div className={cn("col-span-2 grid", condensed ? "grid-rows-[0fr]" : "grid-rows-[1fr]")}>
MmacroscopeappBoton lines 1560–1560Aug 17, 2026

pullRequest/PullRequestDetailPanel.tsx:1560

Removing the eased track makes reopening the chrome snap. grid-rows-[0fr]/[1fr] now flips with no transition-[grid-template-rows], so when the reader scrolls back to top < 4 the fold's full height reappears in one frame and the content area below jumps down by it. Collapsing does have to stay instant β€” the scrollTop compensation refunds foldHeight - condensedRow and would fight an animated track β€” but the reopen path deliberately gets no refund, which is why that direction used to ease.

Restoring the transition on the expanding branch (and the mirrored one on the condensed row's track at line 1479, so the two rows still trade height in one motion) keeps the geometry it had before.

suggestion
1 <div
2 className={cn(
3 "col-span-2 grid",
4 condensed
5 ? "grid-rows-[0fr]"
6 : "grid-rows-[1fr] transition-[grid-template-rows] duration-200 ease-out motion-reduce:transition-none",
7 )}
8 >

No longer relevant as of e942ef87286e84e130ba58d6ad5685ade4e2f5b6

MmacroscopeappBotreviewed Β· 1 inline threadAug 19, 2026
Synced from GitHub

One finding: the chrome restructure kept the collapse mechanism intact but removed the notes that explain its non-obvious parts. Everything else in this revision looks consistent with the shared primitives β€” the segmented track now lives in ToggleGroup, the filter trigger and refresh control go through Button, the disabled panel toggles get real tooltip triggers, and the copy-branch focus ring keeps its offset.

Posted via Macroscope β€” UI Consistency

apps/web/src/components/pullRequest/PullRequestDetailPanel.tsxlines 436–443Outdated Β· fe40b2aOpen thread
MmacroscopeappBoton lines 436–443Aug 19, 2026

pullRequest/PullRequestDetailPanel.tsx:436

The collapse machinery survived this restructure unchanged, but its explanations did not: chromeStateByTab and the compensationRef layout effect (lines 432-447), the foldHeight - condensedRow refund (line 1856), and the deliberately one-directional track animation (grid-rows-[0fr] with no transition on collapse, eased only on reopen β€” lines 1570-1572) are all still here with nothing left saying why.

The asymmetric track is the risky one: it now reads as an oversight, so the next reader is likely to "fix" it by letting collapse animate too, which puts the animated track back in a fight with the scrollTop write and reintroduces the mid-scroll content jump the refund exists to prevent. Since the mechanism itself did not change, consider restoring the two notes that cover it β€” why collapse is instant while reopening eases, and why the refund is the fold's height minus the condensed row's.

No longer relevant as of 6fe8428ca433ad6d4151f6757be25b190638c33e

MmacroscopeappBotreviewed Β· 1 inline threadAug 19, 2026
Synced from GitHub

One follow-up on the chrome-collapse machinery. Everything else in this round β€” the tooltip wrappers in PanelLayoutControls, the segmented ToggleGroup tabs now taking their track from the primitive, the Button-based filter/refresh controls, and the ghost's geometry (it matches the loaded panel row for row) β€” reads consistent with the shared component system.

Posted via Macroscope β€” UI Consistency

apps/web/src/components/pullRequest/PullRequestDetailPanel.tsxlines 431–436Outdated Β· 6fe8428Open thread
……@@ -430,26 +429,13 @@ export function PullRequestDetailPanel({
445- // compensation: collapse only once the reader has scrolled at least the fold's height,
446- // then give that height back to `scrollTop` before the next paint β€” the content under
447- // their eyes does not move, and the collapse itself is the only thing that changes.
436+ const condensed = chromeCondensed;
MmacroscopeappBoton lines 431–436Aug 19, 2026

pullRequest/PullRequestDetailPanel.tsx:436

Two of the four rationales dropped from the collapse machinery are back (the one-way track at 1567-1574 and the refund at 1858), but the two here are still gone while the code they explained is unchanged:

  • chromeStateByTab (431-435): nothing left says why chrome state is remembered per tab β€” only the active tab emits scroll, and a tab too short to scroll has to remember "expanded" or it is stranded under a chrome it has no scrollbar to reopen.
  • the compensationRef layout effect (440-447): nothing says the scrollTop write exists so the content under the reader does not leap when the fold's height returns to the scrollport.

Both are non-obvious scroll-compensation constraints, so a line of why above each would keep the next reader from taking them for removable state.

No longer relevant as of 5ea3cfaeeae3981a36e3a757ac6e71c8789c7e77

MmacroscopeappBotreviewed Β· 1 inline threadAug 19, 2026
Synced from GitHub

One consistency issue found in the reworked pull request chrome. Everything previously raised (segmented track classes now live in the ToggleGroup primitive, the disabled-toggle tooltip wrappers, the copy-branch focus ring, the xs icon sizing, MetaRow padding, action-row DOM order, and the restored collapse rationale comments) is addressed at this head.

Posted via Macroscope β€” UI Consistency

apps/web/src/components/pullRequest/PullRequestDetailPanel.tsxlines 1513–1517Outdated Β· 02917c5Open thread
……@@ -1507,104 +1500,105 @@ export function PullRequestDetailPanel({
1514+ <PullRequestActorAvatar actor={detail.author} className="shrink-0" />
1515+ <span className="sr-only">{detail.author?.login ?? "ghost"}</span>
1516+ <span className="shrink-0">{formatRelativeTimeLabel(detail.updatedAt)}</span>
1517+ </span>
MmacroscopeappBoton lines 1513–1517Aug 19, 2026

pullRequest/PullRequestDetailPanel.tsx:1517

The condensed chrome shows the author as a bare avatar: PullRequestActorAvatar renders aria-hidden markup, so the login only exists in the sr-only span and a sighted reader has no way to find out who it is. This surface's convention is the opposite β€” CommentAuthor in PullRequestSummaryTab ("The avatar carries the attribution alone; who it is arrives on hover") and PullRequestActorLabel both hang a tooltip off the avatar, and the expanded fold below uses the labelled form.

Wrapping the avatar in the same tooltip trigger keeps one attribution treatment and drops the need for the separate sr-only node.

suggestion
1 <span className="flex min-w-0 shrink items-center gap-1.5 overflow-hidden text-xs text-muted-foreground">
2 <Tooltip>
3 <TooltipTrigger
4 render={
5 <span
6 className="shrink-0 rounded-full"
7 aria-label={detail.author?.login ?? "ghost"}
8 />
9 }
10 >
11 <PullRequestActorAvatar actor={detail.author} />
12 </TooltipTrigger>
13 <TooltipPopup side="top">{detail.author?.login ?? "ghost"}</TooltipPopup>
14 </Tooltip>
15 <span className="shrink-0">{formatRelativeTimeLabel(detail.updatedAt)}</span>
16 </span>

No longer relevant as of 1088b8114cca06cba3fc0b18e926a5a7582c3195

Fixed in 1088b8114. The condensed author avatar now uses the shared tooltip treatment with an accessible login label.

MmacroscopeappBotreviewed Β· 1 inline threadAug 19, 2026
Synced from GitHub

One consistency finding in this revision. Everything else in the restructure lines up with the shared primitives: the segmented tab group now takes its track from the segmented variant, the filters trigger renders through Button, the panel header goes through WorkspacePageHeader/WorkspacePageContainer, both disabled panel toggles hang their tooltip on a wrapper (matching ConnectionsSettings), and the detail ghost mirrors the new chrome and MetaRow grid.

Posted via Macroscope β€” UI Consistency

apps/web/src/components/pullRequest/PullRequestDetailPanel.tsxlines 1296–1296Outdated Β· 210047dOpen thread
……@@ -1194,29 +1211,103 @@ export function PullRequestDetailPanel({
1293+ onClick={startResolveConflicts}
1294+ >
1295+ <TriangleAlertIcon className="size-3.5" />
1296+ {handoff === "conflicts" ? "Preparing..." : "Resolve conflicts"}
MmacroscopeappBoton lines 1296–1296Aug 19, 2026

pullRequest/PullRequestDetailPanel.tsx:1296

This revision deletes the overflow-menu fallback that used handoffLabels.resolveConflicts, so with the conflict row already gone pullRequestHandoffLabels has two entries (resolve, resolveConflicts) that nothing renders, while this button hardcodes its own copy. The contract exists to name where a hand-off lands "without letting each surface guess independently", and the sibling hand-offs in this same panel (fixFindings in the menu, fixFinding/fixCheck passed to the tabs) still read from it β€” so in thread context this is now the one hand-off that does not say the work stays in the open thread.

Smallest fix either way: render handoffLabels.resolveConflicts here, or drop the two entries (and their assertions in pullRequestDetail.logic.test.ts) if the short copy is deliberate for this compact row, so the contract stops advertising labels no surface uses.

No longer relevant as of f1c8120882a1c7ad951abf7f8e9d12bb06ac1f2e

Fixed on the latest head by removing the dead resolve handoff labels and keeping the compact visible copy.

MmacroscopeappBotreviewed Β· 1 inline threadAug 19, 2026
Synced from GitHub

One finding: the new stacked-base indicator has no user-facing explanation. Everything else in this pass looks consistent with the shared primitives β€” the tooltip triggers on the disabled panel toggles are now wrapped uniformly, the segmented track lives in ToggleGroup rather than the call site, the filter/refresh controls go through Button, and the header/container geometry comes from WorkspacePageHeader/WorkspacePageContainer.

Posted via Macroscope β€” UI Consistency

apps/web/src/components/pullRequest/PullRequestDetailPanel.tsxlines 1520–1534Outdated Β· 210047dOpen thread
……@@ -1507,104 +1500,105 @@ export function PullRequestDetailPanel({
1531+ </span>
1532+ }
1533+ />
1534+ <TooltipPopup side="top">{detail.baseBranch}</TooltipPopup>
MmacroscopeappBoton lines 1520–1534Aug 19, 2026

pullRequest/PullRequestDetailPanel.tsx:1534

The new LayersIcon is the only place "stacked pull request" is said, and it is said on a bare <svg> inside a tooltip trigger whose popup repeats just the branch name β€” so a sighted reader gets an unexplained glyph, and the label sits on an element with no role rather than on the trigger. Its neighbour in this same row (PullRequestBaseFreshnessWarning) does the opposite: the label is on the interactive element and the meaning is spelled out in the layer it opens.

Folding the meaning into the tooltip that is already here is the smallest fix (the expanded copy of this row at ~1683 needs the same change).

suggestion
1 <TooltipPopup side="top">
2 {isStackedPullRequest
3 ? `Stacked on ${detail.baseBranch}`
4 : detail.baseBranch}
5 </TooltipPopup>

No longer relevant as of f1c8120882a1c7ad951abf7f8e9d12bb06ac1f2e

Fixed on the latest head. The tooltip now explains the glyph as β€œStacked on ”.

MmacroscopeappBotreviewed Β· 1 inline threadAug 19, 2026
Synced from GitHub

One finding on the rebuilt detail-panel chrome: the top row's crossfading layers lost their sm:text-xs step. Everything else checked out β€” the panel-layout tooltip triggers, the filters trigger migration to Button, the segmented tab group, and the MetaRow/ghost geometry all line up with the shared primitives.

Posted via Macroscope β€” UI Consistency

apps/web/src/components/pullRequest/PullRequestDetailPanel.tsxlines 1120–1120Outdated Β· f1c8120Open thread
……@@ -1101,34 +1105,47 @@ export function PullRequestDetailPanel({
1117- "col-start-1 row-start-1 flex min-w-0 items-center gap-1 text-sm text-muted-foreground transition-opacity sm:text-xs motion-reduce:transition-none",
1118- // Sequenced, not simultaneous: the leaving layer clears quickly before the
1119- // arriving one lands, so no frame shows both texts superimposed at half opacity.
1120+ "col-start-1 row-start-1 flex min-w-0 items-center gap-1 text-sm text-muted-foreground transition-[opacity,transform] ease-out motion-reduce:transform-none motion-reduce:transition-none",
MmacroscopeappBoton lines 1120–1120Aug 19, 2026

pullRequest/PullRequestDetailPanel.tsx:1120

Both crossfade layers of the top row dropped sm:text-xs while keeping text-sm, so the repository/number line (and the condensed title layer, line 1176, which carries the identical string) now renders at 14px on desktop inside a row that shrank from h-11 to h-7 β€” while every other line of this chrome (the condensed meta row, the branch pair, the tab bar) stays 12px. text-sm … sm:text-xs is the coarse-pointer/desktop pairing the control primitives themselves use (Button xs, Select xs, Badge), so the desktop step is worth keeping here too.

suggestion
1 "col-start-1 row-start-1 flex min-w-0 items-center gap-1 text-sm text-muted-foreground transition-[opacity,transform] ease-out sm:text-xs motion-reduce:transform-none motion-reduce:transition-none",

The same addition applies to the condensed layer at line 1176.

No longer relevant as of 1088b8114cca06cba3fc0b18e926a5a7582c3195

Fixed in 1088b8114. Both crossfade layers keep sm:text-xs so the compact desktop header stays at the intended scale.

Added 7 commits

  • feat(web): refresh pull request details15a93c1
  • fix(web): label condensed pull request authors4ea57ac
  • docs(web): explain pull request scroll anchoring975f561
  • fix(web): clarify pull request handoffsa739de0
  • docs(web): preserve pull request scroll intenteb77848
  • fix(web): keep pull request header compacteee6a04
  • fix(web): separate pull request page primitivesffcaa7b