Skip to content

feat(web): refresh pull request details

MacroscopeApp / Macroscope - UI Consistency failed Aug 16, 2026 in 9m 33s

UI Consistency: 2 issues found

Two layout regressions introduced by changed lines:

  • apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx (~L1636-1641): the tab nav padding is now state-dependent (condensed ? "py-1.5" : "py-2"), but the scroll compensation still refunds only foldHeight - condensedRow height, so the chrome shrinks ~4px more than is compensated and the content jumps up mid-scroll on collapse. Fix: keep the nav padding constant (py-2) or include the nav in the measured chromeDelta.
  • apps/web/src/components/pullRequest/PullRequestSummaryTab.tsx (L199): MetaRow swapped py-1.5 for min-h-8; the Reviewers and Labels cells are flex-wrap, so wrapped pills now sit flush against neighbouring rows. Fix: keep py-1.5 alongside min-h-8 (single-line geometry is unchanged).

Both findings were posted as inline review comments with suggested fixes.

Details

Note

Your check run agent prompt is: .macroscope/check-run-agents/ui-consistency.md
More information about how Check Run Agents work can be found in our Help Center.

Checked and found consistent (no findings):

  • Tab bar migrated to the shared SegmentedTabList/SegmentedTab primitives; aria-pressed semantics preserved via Toggle pressed, tabs no longer need the tabIndex guard because they sit outside the folded region.
  • PullRequestFiltersMenu now renders MenuTrigger through Button size="icon" variant="outline", dropping the locally reconstructed size/border/hover classes. The filtered state correctly re-routes the primitive's icon indirection ([--control-icon-color:currentColor]) instead of setting text-foreground, and ListFilterIcon carries no text-* class so the variable still applies.
  • _chat.pull-requests.tsx header migration to WorkspacePageHeader preserves drag-region (gated on isElectron), the WCO inset (reserveNativeControls={!rightPanelOpen}) and COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS, which the shared component applies internally; the removed import has no remaining consumers. WorkspacePageContainer className="gap-4" overrides only the container gap.
  • New PullRequestRefreshControl uses named primitive sizes/variants (icon-sm/ghost compact, icon/outline in flow) rather than call-site geometry, matching the neighbouring filters button.
  • ps-[8.5px] on the checkout trigger matches the existing leading-icon button convention (GitActionsControl, OpenInPicker), so it is not an ad-hoc padding override.
  • delay-50 is a valid Tailwind v4 bare numeric delay value.
  • Ghost changes: GhostBar's default bg-muted-foreground/15 now applies uniformly (removed per-call /10 overrides); PullRequestDetailGhost is rendered in the same full-panel slot in both consumers (PullRequestDetailPanel early return and ChatView), so h-full/flex-col geometry is appropriate. PullRequestTimelineGhost, DiffPanelLoadingState, Badge and ArrowUpRightIcon all remain used, so no dead imports.
  • The early PullRequestDetailGhost return sits after every hook call, so hook order is stable.
  • Accessibility: condensed/expanded chrome rows are mutually inert, so the new <h1> in the condensed row never duplicates the fold's <h1>; the aria-hidden PullRequestActorAvatar is paired with an sr-only login.
  • isStackedPullRequestBase returns false when no default ref is known, and listRefs sorts current/default refs onto the first page, so limit: 2 is a safe read for the stacked indicator; unit tests cover local and remote default forms.
  • primaryAction reordering does not lose an action: the draft toggle remains in the menu when conflicts take the primary slot, and conflicting is still gated on state === "open".

🔍 Agent activity — 1 tool call

💭 Looking at the diff to identify specific violations in the styling changes, particularly around the new segmented tabs and button classes like ps-[8.5px] which appears to be a custom padding override, and checking whether delay-50 is actually a valid Tailwind class since the standard delay values don't include 50. I also need to verify the Button component's render prop usage in the filters, check if the removed imports like COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS are properly handled elsewhere, and confirm that the GhostBar styling changes don't break the default appearance after removing bg-muted-foreground/10. I'm checking whether delay-50 is a valid Tailwind v4 class — it turns out that Tailwind v4 supports delay-<number> as a bare utility that generates transition-delay: <number>ms, so values like delay-150 work directly without needing arbitrary value syntax. For the [--control-icon-color:currentColor] change in the filters menu, the outline Button variant already sets --control-icon-color to --muted-foreground, but when filtered, setting it to currentColor makes the icon inherit the text color instead. Since the outline variant already applies text-foreground, the filtered icon will display in the foreground color — this aligns with the guidance about being careful with shared primitive color indirection. Now I need to check the actual implementation: verify the Tailwind version, look at the Button component's icon and icon-sm sizes, and examine the PullRequestDetailPanel to see how these changes are applied. I see that segmented-tabs was already in the codebase before this PR. Looking at the panel's changes, there's a Button component with an inline padding override using ps-[8.5px] — a magic value that breaks away from the primitive's standard spacing, which is worth flagging based on the guidelines for call-site class overrides. The icon sizing uses size-3.5 which does override the xs variant's default, but that's acceptable. For accessibility, the tabs have shifted from aria-pressed buttons to SegmentedTab components (which use Toggle with pressed state), and the condensed row's dynamic tabIndex logic is now handled by the separate always-visible nav — both changes are fine. Now checking the repository button implementation... it's a raw button element styled to look like a link, which violates the preference for core primitives like Button variant="link". More critically, it's missing the focus-visible:ring styling that other buttons in the file have, creating a keyboard accessibility regression — the browser's default outline remains but there's no custom focus indicator. I should verify if there's a global focus-visible style in the index.css that might cover this, and check whether similar external-link buttons elsewhere use the Button component or a shared pattern. Also need to look at the copy-branch button changes, particularly around the removal of the title attribute. The bigger concerns are potential unused imports — PullRequestTimelineGhost and DiffPanelLoadingState might no longer be referenced if PullRequestDetailGhost now handles all tabs — and the removal of the chromeVariant prop while keeping the collapse-compensation logic and scroll handler, though the grid-row transitions were stripped out. Checking which imports are still being used — Button is referenced in PullRequestRefreshControl, but TITLE_WIDTHS in PullRequestGhosts might be orphaned after the diff removed its usage in the detail ghost. Let me look at the actual file sections around those line ranges to verify. I need to check if PullRequestTimelineGhost and DiffPanelLoadingState are still being used elsewhere in the file before removing those imports from the panel. The imports are all still in use, so that's fine. Let me check the specific sections of the code where changes were made to see how they fit into the broader context. Looking at the specific row and column ranges to examine the data in those sections. Looking at the new repository button—it's a raw <button> element used for a link-like action, which matches the existing #number button pattern in the same file. This is an established convention here for inline text links within a text run, so it's not really a semantic issue. Though I notice neither button has a focus-visible ring, just the browser default. The repository button doesn't have shrink-0, but that's fine since it can truncate. The inert attribute on the row handles preventing focus even though the button lacks an explicit tabIndex guard. The top-row height reduction from h-11 to h-7 is intentional for a more compact layout, and the ghost matches that height. The bigger concern is the early return of the ghost before the panel renders—it now replaces the entire panel instead of sitting inside the scroll region. This is a layout change, though it's intentional since the ghost mimics the chrome structure. I need to verify that the early return at line 1068 comes after all hooks are called, since hooks can't be conditional. The return appears to be right before the JSX, so it should be fine, but I should double-check whether any hooks are invoked after that point. The tab nav moved outside the fold, and the scroll compensation logic still uses condensedRowRef and foldRef for measurements. The transition classes on the grid wrappers were removed, so collapse and expand are now instant rather than animated, though the inner content still has transitions. The tab nav padding changes based on the condensed state, which could affect spacing calculations. I need to check the scroll handler around lines 1600–1800 to see if the compensation math accounts for the padding change on the nav, since a 4px difference could cause a content jump when collapsing. I'm spotting a bug in the scroll compensation logic—when the nav condenses mid-scroll, the padding change adds 4px to the scrollport growth, but the chromeDelta calculation doesn't account for this extra padding shift, causing the content to jump by that amount. I'm also noticing the Button component at line 1710 has conflicting size specifications—it's setting h-7 explicitly while the size="xs" prop already defines responsive heights, and I need to check whether the explicit height is actually overriding the responsive behavior or if they're coexisting. Looking at the PullRequestListFilters component, the MenuTrigger Button migration from custom classes to the Button component's size="icon" variant looks solid, and the filtered state handling with the icon color change is working as intended—it's switching from a text-foreground class to currentColor when filtered, which maintains the visual distinction. Now I need to check the WorkspacePageHeader component to verify it's properly receiving the electron and reserveNativeControls props, and crucially, that it still includes the drag-region class for Electron window dragging and maintains the correct height styling. Looking at the container changes, the new gap-4 spacing needs to match the previous layout's max-width and padding setup. The refresh button behavior has shifted — it's now conditionally shown in the header only when condensed, otherwise it appears in the list area with different styling. Let me examine the actual route code to see these changes and check the PullRequestSummaryTab grid adjustments, which appear to be equivalent between the old and new implementations. The header spacing has been updated to use gap-3 with safe-area padding instead of the previous px-3 sm:px-5 and gap-1.5, which groups the search and refresh button together with gap-1.5. There's a potential issue with the refresh icon having an explicit size-4 class inside the button that might need attention. The refresh button uses size="icon" with variant="outline" next to the search input and filters menu, both maintaining consistent sizing with h-9 sm:h-8. The WorkspacePageContainer overrides the default gap-6 to gap-4 via tailwind-merge, and padding has been standardized to px-5 sm:px-6 instead of always px-5. Now I need to check how the PullRequestListFilters component was migrated. The ListFilterIcon is picking up the correct color styling through the Button's base selector since it lacks a text class, so it inherits the currentColor and applies the text-foreground from the outline variant. I'm checking whether the newly added Button import in PullRequestListFilters creates any import ordering issues with the linter, since it was appended after the input-group import and there might be a configured rule for sorted imports. Looking at the existing imports in PullRequestDetailPanel, they're not strictly alphabetized, so the project doesn't enforce import sorting. I can skip worrying about that. Now I need to examine the ps-[8.5px] button styling around line 1320–1400. I'm noticing a specific case where a Button component has a tiny padding override at just one call site—adding back 1px to compensate for the negative margin on its icon. It's technically the kind of thing the linting rules flag, but it's such a minimal utility class that it doesn't really fit the pattern of large override strings that should be caught. Let me search the codebase to see if this same ps-[8.5px] nudge appears elsewhere, which would suggest an established pattern rather than a one-off magic number. There's also a minor icon size inconsistency across buttons in the same row that I'm noting but skipping for now, and the destructive-outline variant's icon color handling looks correct since it properly inherits the foreground color. Now I'm moving into the behavioral checks—specifically verifying that the early return of PullRequestDetailGhost doesn't violate React's rules of hooks by having any hook calls after that point in the component. Now I'm noticing the heading structure issue — the condensed row now has an <h1> for the title, but the route page already has <h1>Pull Requests</h1> in the breadcrumb, creating two h1s in the document when the panel is open. Plus, the h1 sits inside a row that's aria-hidden and inert when expanded, so it gets hidden from the accessibility tree while the expanded state's title lives elsewhere. I need to check what heading element the expanded row uses for its title to understand the full semantic picture. I'm checking whether the fold element properly handles accessibility when condensed. The fold has an inert attribute that toggles based on the condensed state, which should remove it from the accessibility tree according to the HTML spec, but there's no explicit aria-hidden attribute backing it up. When not condensed, the condensed row itself gets inert={!condensed} to prevent its h1 from appearing in the tree. The panel's h1 does duplicate the page's "Pull Requests" heading, but that's a pre-existing issue. Now I need to trace through the navigation padding — the tab nav was previously nested inside the fold wrapper itself, not as a separate element. In the old structure, the nav lived inside the fold container, so when condensed, the tabs would fold away and the condensed row had its own compact tab buttons. Now the nav is persistent outside the fold, and the condensed row shows avatar/branches/diffstat instead. This means foldRef.scrollHeight no longer includes the nav, which is correct. When collapsing, the scrollport height change comes from the fold shrinking minus the condensed row height, plus the nav padding adjustment (py-2 to py-1.5 = 4px total). The compensation logic reduces scrollTop by the chrome delta to keep content visually stable as the container resizes. However, there's a 4px mismatch: the code reduces scrollTop by the chrome delta minus 4, which leaves the content 4px too high — a tiny but real jump introduced by the new nav padding change. This is worth flagging since the previous comments were careful about ensuring content doesn't move under the user's eyes. The simplest fix would be to keep the nav padding constant or account for the nav height delta in the measurement. Let me verify the other changes don't cause issues: the removed grid-row transitions mean expansion is now instant, which is fine. The avatar component has proper shrink constraints, the condensed row layout handles overflow correctly, and the tab navigation structure with aria labels and segmented controls looks sound. I should confirm the rightPanelUnavailableLabel prop exists on the shell component and that the linkedSelection fields are properly typed for the linkedSelectionMatchesSurface logic. I need to trace where the rightPanelUnavailableLabel prop is being used around line 1316 to verify it's actually supported by whatever component or hook is receiving it—likely useWorkspacePanelControls or PanelLayoutControls. I'm wrapping the rightPanelControl with WorkspacePageHeaderEdgeControl, which applies a negative margin offset, and the header's padding accounts for safe area insets on the right side. The header also incorporates the collapsed sidebar titlebar inset class. The drag-region class is now conditionally applied only in Electron environments rather than unconditionally, which is fine since it's harmless in browsers. The header uses safe-area-based padding instead of the previous fixed padding values, which aligns with the shared standard. There's a flex spacer in the column header that was already there before, and I need to verify that the collapsed sidebar titlebar inset class import was properly removed and isn't used elsewhere in the route file. The ps-[8.5px] pattern for icon-leading buttons is already established in the codebase, so that's not a concern. Looking at the remaining issues: there's a 4px jump in the nav padding versus scroll compensation, and I need to check whether PullRequestDetailGhost renders the full-panel ghost inside the panel's layout container or if it returns it directly. The ghost itself has the right styling to fill the panel slot, so that's fine. The ghost bars were simplified by removing the bg-muted-foreground/10 overrides and standardizing everything to the default bg-muted-foreground/15, which is a clean consistency improvement. The MenuTrigger with a render prop in PullRequestListFilters follows the same pattern used elsewhere in the codebase, so that's good. The refresh control moved into the content row when not condensed and into the header when condensed—both states are keyboard-accessible, and the non-condensed header no longer shows a refresh button since it appears in-flow instead. The PullRequestRefreshControl with size="icon" sits next to the filters menu and search input with a spinning animation when refreshing, which looks right. The outline variant for the in-flow refresh button matches the filters button styling, so there's good consistency there. In the condensed detail panel row, the avatar, updated time, branches, and diffstat are shown while the tabs move to a persistent location elsewhere. The condensed row has no focusable items except the freshness warning popover trigger, with inert={!condensed} handling the state properly. The nav wrapping the SegmentedTabList with role="group" is fine—nav with a group inside works well. The checks summary and conflicts badge were removed from the title row, and now the only conflict signal is the primary action button "Resolve conflicts," which is computed based on whether the PR is conflicting, in draft, or merged/closed. Now I'm checking the actual precedence logic for the primary action—previously conflicts only surfaced to users with merge permission, but now the "Resolve conflicts" button appears for everyone. I need to verify how conflicting is defined and whether a closed PR could report as conflicting, which would be odd. With conflicts taking priority, a draft PR that has merge conflicts won't show "Ready for review" as the main action anymore—it'll show "Resolve conflicts" instead. This means the draft toggle shifts into the menu since the condition for showing it evaluates to true. I'm thinking through what happens with the conflicts badge that was in the title row and the "Merge conflicts" badge row— those are now gone, but conflicts are still surfaced through the primary "Resolve conflicts" button. The checks summary used to appear in the condensed title row with a popover, but now it only shows up in the tab nav when you're on the summary tab, so on the Code or Timeline tabs you lose that visibility. That's an intentional info-density trade-off from the redesign, not really a regression. Now I'm considering the PullRequestDetailGhost early return—previously when loading, the chrome at the top with the repository and number would render with mostly empty content plus per-tab ghost loading states, but now the whole panel becomes a ghost instead. The early return happens after all hooks register but before the component renders further, which is fine since hook order stays consistent when the loading state toggles. The ghost's aria-label and role are appropriate. Looking at the markup, the ghost's top row height and tab row styling are close enough to the real chrome. But there's a spacing issue I'm noticing: the MetaRow used to have py-1.5 padding, now it relies on min-h-8 for height, which means if content wraps taller than 32px there's no vertical padding and wrapped text could touch the edges. I'm checking how MetaRow handles content wrapping, particularly looking at whether the reviewers list can use flex-wrap. I'm noticing a visual regression where wrapped label content in MetaRow is now taller than the intended 32px height, and without the previous vertical padding, the label pills are sitting flush against adjacent rows like the reviewers or comments sections. I should keep the padding to maintain proper spacing between rows. So the padding adjustment with py-1.5 and min-h-8 actually preserves the 32px row height for single-line content while restoring breathing room when labels wrap—I'll flag that as a suggestion. Now I need to check the isStackedPullRequestBase logic and whether the listRefs ordering guarantees are solid enough to trust the LayersIcon correctness, since the limit of 2 refs could miss the default ref if the current ref isn't positioned first. The limit:2 approach handles the normal case well, and isStackedPullRequestBase correctly returns false when there's no default branch information. The function also properly scopes its detection to each workspace root, so it works correctly across different worktrees. Now I'm noticing the LayersIcon uses an aria-label for "Stacked pull request" but lacks a role="img", which could cause screen readers to announce it incorrectly. However, this matches the existing pattern elsewhere in the file with ArrowLeftIcon, so it's consistent with how the codebase already handles this — I'll leave it as is. There's also a new branchRefsQuery that fires for every open PR detail, which is a performance consideration rather than a UI issue, so I'll skip that too. Looking at the nav padding and height compensation more carefully: the top row height is fixed at h-7 in both condensed and expanded states, but the padding changes from py-2 to py-1.5 when condensed. The actual height difference between states should account for both the fold height and the 4px padding change, but the current compensation only covers the fold height difference — that leaves a 4px content jump when collapsing. Expanding intentionally skips compensation since the reader starts at the top where scrollTop is already near zero. This looks like a genuine regression from the changed line, so I'll flag it. I should also check the MetaRow padding loss — it went from py-1.5 on each row to zero, which removes the 12px spacing that was between wrapped content before. That's worth flagging too. The new button in the raw repository lacks focus-visible styling, but the sibling button already has the same gap, so it's just following the established local pattern. The removed conflict Badge is fine. For PullRequestDetailGhost, I need to verify whether it's used anywhere else expecting content-only geometry, since it's now rendered as a full panel instead of inside the scroll region. Looking at the PullRequestDetailGhost component on line 6127 of ChatView — it's styled with full height, flex column layout, and overflow hidden, which might conflict with the scroll container's expectations. I need to check the context around lines 6100-6140 in ChatView.tsx to see how it's being used and whether the geometry is appropriate for its parent container. The TITLE_WIDTHS constant is still being used in the list and timeline ghosts, which is fine for now. The ChatView fits naturally into the same panel space as PullRequestDetailPanel with full height styling. The ghost state displays appropriately in thread context where checkout buttons and other detail-specific chrome aren't needed. I'm noticing a potential issue though — the early return for the loading state happens before the hooks run, which could be problematic. Looking at the hooks placement and checking for state-related issues — the early return with useEffect firing onStateChange during loading seems fine, and useState for titleScope is working as expected. There's a subtle concern about how the early return affects the previous rendering logic, but I need to examine that more carefully. Actually, there's a genuine UX regression here: during the loading state, the panel's close button and tab bar disappear entirely since the header doesn't render. Users can't collapse the panel from within it while data is loading — they'd have to use the toggle in the page header instead. It's a real interaction loss, though the ghost placeholder does mimic those controls to signal intent. I'd call this a minor finding worth noting: the panel becomes non-interactive during the pending state. Let me reconsider the scroll compensation math more carefully. When the fold collapses, the chrome shrinks by foldH - condH + 4 pixels. The compensation applies -chromeDelta, which is -(foldH - condH), so the scroll adjustment is 4px short of what's needed to keep content fixed. That means the new scroll position ends up 4px too large, causing the content to jump upward by 4px at collapse. The fix would be to adjust the compensation formula to account for that 4px offset. For the review, I'll suggest either removing the conditional padding on the nav so it stays constant, or including the nav height in the measured chrome delta. I'm preparing an inline comment for the className line where the padding toggles between py-1.5 and py-2, proposing to replace it with a fixed py-2 value. The suggestion targets lines 1638–1641 in the diff, which are part of the added hunk and therefore commentable. I'm also working on a second comment for PullRequestSummaryTab.tsx. For the MetaRow issue, I'm noticing that line 199 lost vertical padding when wrapped rows like Labels now touch their neighbors. I'm suggesting adding py-1.5 alongside the existing min-h-8 to restore spacing—with those combined, the single-row height works out to 32px minimum including padding, which keeps the ghost rows aligned properly. Now I'm checking the class ordering convention in this codebase. Looking at similar patterns like "flex min-h-10 items-center justify-between gap-3 border-t border-border/60 px-4 py-2", it seems padding classes come after gap and border utilities but before text utilities, so py-1.5 should slot in right before text-xs. On the check state, I'm weighing whether this warrants a failure. The 4px vertical compensation mismatch directly contradicts the module's documented invariant, which feels like a legitimate regression, but I'm uncertain if that alone justifies failing the PR given the instruction that the check defaults to failure only when there are no findings at all. For the MetaRow padding issue, I'm verifying it's a real problem—the labels and reviewers rows both use flex-wrap, so when content wraps in a narrow panel, adjacent rows would indeed be flush without vertical padding. That's a valid concern worth flagging as a suggestion. I'm also scanning the rest of the component to make sure I haven't missed anything else significant, and checking the condensed row implementation. Now I'm looking at the PullRequestActorAvatar to see if it already provides its own accessible name through alt text or aria-label, since there's also an sr-only span for "login"—if the avatar already has accessibility coverage, that span might be redundant. The avatar is properly hidden from screen readers with aria-hidden, so the sr-only login text handles accessibility correctly. I'm ready to submit the review with two inline comments targeting specific lines in the diff—the first at lines 1638–1641 in PullRequestDetailPanel.tsx where the nav block was added, and the second comment is cut off but appears to be for PullRequestSumma. Now I'm drafting the first comment about the tab nav's padding shift from py-2 to py-1.5 when condensed, which causes a 4px jump in the content below since the scroll compensation only accounts for foldHeight - condensedRow and doesn't capture the full chrome loss—keeping the nav height constant is the simplest fix. The second comment addresses how MetaRow replaced py-1.5 with min-h-8, removing vertical padding when content wraps, so restoring py-1.5 alongside min-h-8 maintains the 32px rhythm and preserves gaps when rows wrap. I'll mark this as a failure with the title "UI Consistency: 2 issues found" and include suggestion blocks for both comments.

1. modify_pr — 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.


Agent Credits: 379 credits