Skip to content

fix(web): align pull request filter values

MacroscopeApp / Macroscope - UI Consistency failed Aug 30, 2026 in 5m 45s

UI Consistency: 1 issue found

  • apps/web/src/components/pullRequest/PullRequestRow.tsx — the new PullRequestRowLabels chip renders 20px tall (leading-4 + py-px + 1px borders) inside a meta line whose other segments cap at 16px, so a labeled row measures ~58px against the [contain-intrinsic-block-size:54px] the row declares for content-visibility:auto skipping. Offscreen labeled rows are under-estimated, drifting the scrollbar and shifting rows as they paint. Suggested fix: drop py-px and use leading-3.5 so the chip matches the 16px line, or raise the intrinsic block size to match.

Previously reported findings that this revision resolves: the Filters/Sort trigger aria-label Label-in-Name mismatches, the hand-rebuilt outline trigger (now renders through Button variant="outline"), the project label truncation and unavailable marker lost when projects moved onto the shared option list, the h-8 override on the author search InputGroup, the MenuSubTrigger double auto-margin, the MenuItem close-on-click clear action, and the project test cases that no longer reached the radio group.

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.

Methodology

  • Read the full head revision of PullRequestListFilters.tsx, PullRequestRow.tsx, pullRequestList.logic.ts, PullRequestListFilters.test.tsx, and the changed regions of routes/_chat.pull-requests.tsx rather than the diff alone (the branch was force-pushed, so prior-run commits are no longer reachable for a delta review).
  • Cross-checked the primitives the new composition depends on: ui/menu.tsx (MenuSubTrigger chevron ms-auto, MenuPopup width-defaulting contract, MenuRadioItem/MenuCheckboxItem close-on-click defaults), ui/button.tsx (size/variant geometry, --control-icon-color), ui/badge.tsx, ProjectFavicon.tsx, and pullRequestPresentation.tsx (PullRequestMetaLine separator contract, PullRequestDiffStat zero-state, PullRequestActorAvatar sizing).
  • Height math for the finding: row py-2 (16) + title text-sm line box (20) + mt-0.5 (2) + meta line (16) = the declared 54px; the new chip raises the meta line to 20px.
  • CI on the head commit (Check, Test) is green, so typing and the updated filter-menu tests were not re-litigated here.

Considered and not reported

  • labelDotColor and the coloured-dot markup remain duplicated between PullRequestListFilters.tsx and PullRequestSummaryTab.tsx; already raised on earlier revisions and unchanged, so not repeated.
  • The author search input now allows ArrowDown/Escape through to the menu's composite root and uses size="compact" instead of an h-8 wrapper — the earlier keyboard and sizing findings are addressed.
  • The project Icon is an inline component recreated per render, but the module-level favicon cache and atom reuse make the remount visually inert, so it is not reported as a regression.

🔍 Agent activity — 20 tool calls

💭 I'm checking the current PR diff at the head commit to see what remains, comparing against prior flagged issues—noticing the label filter dropdown and search input have changed, with the search now using a compact size instead of the old wrapper. The keydown handler now only stops propagation for non-ArrowDown/Escape keys, which is a partial fix to a previously flagged issue about blocking navigation keys like ArrowUp, Home, and End—since Home/End should stay blocked for text editing but ArrowUp being blocked means users can't wrap back to the last item from the input, a minor issue I'd let pass since the author already responded to the original flag. I'm also revisiting the MenuSubTrigger layout for the label chevron, checking whether the truncation and spacing issue from an earlier review was properly addressed. Several other previously flagged issues look resolved: the Filters trigger now shows visible "Filters" text instead of relying on an aria-label, the Sort trigger conditionally sets aria-label only when there's no visible label, and the outlined button variant is now properly passed through render props. But looking at the project filter's unavailable state, I notice the old tooltip explaining why a project was unavailable seems to have been dropped entirely, and unavailable projects are no longer disabled—just marked with a small "· Unavailable" label, which changes the interaction model more than I'd expect from a simple refactor. I want to check whether the Tooltip import is now unused since I don't see it referenced elsewhere in the diff. Let me pull up the actual current file to verify.

1. browse_code — apps/web/src/components/pullRequest/PullRequestListFilters.tsx

2. browse_code — apps/web/src/components/pullRequest/PullRequestRow.tsx

💭 I'm checking whether the disabled logic for unavailable items in PullRequestFilterRadioGroup matches how it worked before, particularly for host options, and want to diff against the merge-base version to confirm. Then I notice the submenu trigger renders each option's Icon, and for project options this Icon is a new inline function component created every render, which would cause ProjectFavicon to remount instead of update — potentially causing a visible flash, though it might be too minor to flag. I also want to verify the MenuSubTrigger implementation in menu.tsx to confirm the chevron and flex-1 fix work correctly with MenuSubPopup's className support, and check how PullRequestRowLabels renders its dot indicator. Looking closer, I see the dot was actually removed — it's now just a chip with the name. The labelDotColor duplication I flagged before now exists in only two files instead of three, since the row component no longer uses it, though it still duplicates the helper in PullRequestSummaryTab.tsx, which is a new duplicate from this PR — but since I've already raised similar concerns twice and the author responded, repeating it might just be noise. I want to check PullRequestSummaryTab.tsx around line 68 to confirm. I'm also checking that the changedLines conditional render sits properly inside its flex container, and that PullRequestRowLabels is now properly guarded at its call site within PullRequestMetaLine's child-counting logic — that looks fine. Finally, I want to look at how CompactFilterMenu is used in the actual route code, so let me pull up those relevant sections.

3. browse_code — apps/web/src/routes/_chat.pull-requests.tsx

4. browse_code — apps/web/src/components/ui/menu.tsx

💭 I'm confirming the chevron placement works since the label's flex-1 absorbs the free space, pushing the chevron flush against the value. Then I'm checking how the submenu renders project icons via ProjectFavicon for each option. I notice the previous project group passed shrink-0 alongside the size class on the favicon, but the radio item's version drops that shrink-0. Since ProjectFavicon might render an <img> rather than an svg, it wouldn't be caught by the [&_svg]:shrink-0 rule on MenuRadioItem, so I'm working through whether the flex layout (with the truncating label having min-w-0) would actually cause the image to shrink before the text does, given how flex-shrink distributes across replaced elements with intrinsic sizing. I decide this is probably fine given how the image's minimum size resolves, so I move on to scanning for other real issues: the unavailable marker text change in PullRequestFilterRadioGroup looks resolved, the MenuCheckboxItem grid layout and empty-value radio group both look fine, and I note that the search input inside the scrollable submenu popup will scroll away with the list, which is a minor issue worth flagging. I also check that the submenu trigger displaying the current value as text is accessible, then focus on a possible autofocus conflict in PullRequestAuthorFilter where the input's autoFocus might clash with Base UI's own focus management of the first menu item, though the author seems to have handled keyboard behavior already. Finally I'm looking at whether the Filters button's icon actually picks up the --control-icon-color variable when active, checking how Button variants color icons in general. Then I trace through the CompactFilterMenu's MenuTrigger, confirming that when outlined, passing undefined className is fine since it merges properly with the render element's props, and that removing size="icon" and aria-label from the Filters trigger doesn't break header layout expectations. Now I'm comparing the PullRequestRowLabels chip styling against the similar "matched in the description" chip to check for visual duplication or inconsistency between them. Looking at the changed-lines display now, I'm checking whether showing both total lines and the PullRequestDiffStat's plus/minus counts is redundant, and whether PullRequestDiffStat already handles the zero case internally (returning null) versus previously rendering a literal zero -- that would determine if this is actually a behavior change. I also want to check MenuPopup and MenuSubPopup width classes -- confirming the explicit width class suppresses the min-width default, and that submenu popups use independent width rules from their parents, so nesting shouldn't cause conflicts. Then I'm tracing through the filter radio submenu trigger icon rendering, the filter count badge styling, and moving on to verify the test file, the sort/group behavior in the pull requests route, and the threadSort import path resolves correctly. I'm now checking how displayGroups handles non-updated sort orders, where it collapses everything into a single "others" group with an empty label. Now I'm checking whether the list rendering code uses that empty label to draw a header block, which could produce an odd blank header, along with reviewing the menuFiltered variable and the empty-state logic around lines 1230-1480 of the route file.

5. browse_code — apps/web/src/routes/_chat.pull-requests.tsx

6. browse_code — apps/web/src/components/pullRequest/pullRequestPresentation.tsx

💭 I'm checking that the change-size guard correctly hides the line count when additions and deletions are both zero, confirming that matches the existing null-check behavior in the diff stat component. Now I'm looking at how row labels style their borders and background with muted, low-opacity colors. I notice two chip-like elements on the same meta line use different surface treatments — one has a background fill, the other is just an outline — which feels like a minor inconsistency worth checking against how the summary tab styles its own label chips, since a prior pass flagged similar duplication there. I want to see if the author only partially addressed that feedback by dropping the dot but keeping the chip itself. Moving on, I'm confirming the author filter's submenu trigger falls back to "Anyone" when no value is set, which looks fine, and the radio group's "Unavailable" text rendering also looks correct. But I'm now wondering whether disabling an option when it's unavailable could cause a problem if the currently selected project becomes unavailable after the fact — that interaction with the submenu's "current" state needs a closer look. I'm also noticing that unavailable is typed as an optional string but the code checks truthiness in one place and !== undefined in another — meaning an empty-string reason would count as disabled but wouldn't get the visual styling or tooltip that a real reason would trigger, which is a small inconsistency worth flagging. Separately, the label filter's "N selected" text has no truncation, but since the string is always short, this shouldn't matter. Now I'm looking at whether the project icon component gets recreated inline on every render inside the radio submenu, which could cause the favicon component to unmount and remount unnecessarily and flash its fallback icon each time — I want to trace whether projectOptions is actually memoized or built fresh on every parent render. Since the whole filters menu re-renders on polling/list updates, this inline icon recreation would cause repeated remounts and refetches of favicons — a real regression compared to before, when the favicon element was rendered directly and preserved by React's reconciliation. I want to confirm this by checking whether ProjectFavicon holds internal fetch state. I'm also noticing the className passed to ProjectFavicon dropped the shrink-0 modifier that was there previously, which combined with a flex layout and truncated title text could cause the favicon to visually shrink or distort for long project names.

7. browse_code — apps/web/src/components/ProjectFavicon.tsx

8. browse_code — apps/web/src/components/pullRequest/PullRequestListFilters.test.tsx

💭 I'm tracing how findLabeledGroup recursively drills into the Project submenu, following the render tree from the MenuSub trigger down through the nested PullRequestFilterRadioGroup until it locates the MenuRadioGroup with the matching label. Checking the "no change on reselecting same project" test: the onValueChange guard compares next against current value, so reselecting the same project skips the callback, while switching to "all" correctly triggers onProject with undefined values. Now I'm considering ProjectFavicon's remount behavior — since a module-level cache stores previously loaded favicon sources, even if the Icon component remounts due to identity changes, the initial displayed state pulls from that cache, avoiding any visible flash after the first load. Since the projectOptions array and its Icon components are recreated every render of PullRequestFiltersMenu, React treats each Icon as a new element type and remounts the subtree, which could retrigger useAssetUrlState's request logic on every re-render if the menu polls frequently. But since submenu content only mounts when open and only the current selection's Icon is rendered, the practical impact is probably small — I want to check whether useAssetUrlState actually fires a network request on mount before deciding if this is worth flagging as a moderate issue.

9. git_tools — export function useAssetUrlState

💭 I'm wondering if remounting causes the atom to reset and briefly show a loading state, which could make the favicon flash to the fallback folder icon before settling on the correct value. Actually none of my findings so far feel truly ironclad, so I want to dig into more concrete UI regressions. Checking the filter submenu trigger's icon sizing (looks fine given explicit size classes), the label submenu span's missing truncate class (probably not an issue), and now looking at whether the Filters trigger button properly applies icon color styling to its child SVG. That existing --control-icon-color behavior checks out fine, and the count badge styling seems aesthetically consistent inside the button. Accessibility-wise, the badge announcement seems okay too. Now I'm looking at how CompactFilterMenu is being reused as a permanent outlined Sort control in the pull-requests header, noting the trailing chevron icon that other menu triggers like Filters don't have — a minor inconsistency, but I'm wondering if there's actually a duplicate-control problem here worth digging into further. I want to check whether sortMenu sits in the same row as the CompactFilterMenu instances for state and involvement, and verify the header stays consistent across compact and wide layouts. I should also look at menuFiltered's definition and review the relevant sections of the route file to confirm.

10. browse_code — apps/web/src/routes/_chat.pull-requests.tsx

11. browse_code — apps/web/src/routes/_chat.pull-requests.tsx

💭 Looking at the control row elements—search input, sort menu, filters menu, refresh control—I want to verify the button heights match across variants, so I'm checking button.tsx to confirm the outline size defaults align with the compact and non-compact refresh control.

12. browse_code — apps/web/src/components/ui/button.tsx

13. browse_code — apps/web/src/routes/_chat.pull-requests.tsx

💭 The sort trigger's chevron icon already has a text color class, so the SVG color override rule doesn't apply to it, which is fine. I'm now checking how CompactFilterMenu passes its className through the MenuTrigger's render prop to the underlying Button. I confirm this pattern matches how the Filters menu handles the same thing, so it's consistent. Now I'm shifting to double-check the PullRequestRow's spacing and layout for the changed-lines count and metadata line. Redisplaying the diff stat as both a total and a breakdown feels redundant, but it's a product decision rather than a consistency bug. Checking focus handling in the author filter's search input within the menu—since ArrowDown is deliberately allowed to bubble up to move into the item list while ArrowUp is swallowed, and there's nothing above the input to navigate to anyway, this seems like reasonable, intentional behavior rather than something to flag. For the checkbox items, I'm noting that Base UI keeps the menu open by default on check, which works fine even without a "clear all" option since individual unchecking still functions. Skimming past a couple of renames and logic-only changes, I land on the sorted list rendering: when sorting by size, group headers disappear and everything merges into one unlabeled group using a key like "others"—I'm checking whether that key actually exists in the group's type union, since TypeScript would need it to. The array isn't explicitly typed as PullRequestGroup[] but inferred, and since the useMemo branches return different array types, TS likely infers a union type. I want to verify that mapping over a union of array types still works correctly for accessing key, label, and entries. Since map is generic, it usually triggers that error, but maybe the element types unify enough that TS widens them into one shape—it depends on whether the group key is a plain string or a literal union. Rather than speculate, I should check the actual PullRequestGroup type definition and confirm what label and key look like, so let me grep the logic file for that definition.

14. browse_code — apps/web/src/components/pullRequest/pullRequestList.logic.ts

💭 I'm checking whether "others" being a valid PullRequestGroupKey makes the two branch types compatible enough for TypeScript to resolve .map over the union array type, since TS can call union signatures only when they align closely. Since both .map methods are generic, calling on a union of X[] | Y[] likely triggers "not callable" unless the inferred union collapses—so I'm tracing where useMemo infers T from the two returned array literals and whether their element types (key, label, entries) actually widen to a common shape. Rather than reasoning through TypeScript's overload resolution rules further, I think checking the CI status of the PR would settle whether typecheck actually passes. This is somewhat outside my review's scope of UI consistency, so I'll just glance at the check runs for the head commit to avoid duplicating already-caught issues.

15. github_api_read_only — repos/{owner}/{repo}/commits/2af692e286197be5b70419a48c38a459c2f33264/check-runs

💭 Both CI checks passed, confirming no typecheck or test issues, so any remaining findings need to be genuine UI-consistency problems. I want to diff fcf21d4 against 2af692e to see exactly what changed most recently, since that's where new issues are most likely to appear.

16. git_tools — fcf21d4..2af692e

💭 Since the branch was force-pushed and prior commits are gone, I can only compare the merge base to the current head now. Looking at the submenu, I'm noticing that the inline Icon component for projects gets a fresh identity on every render, which means for a selected project rendering ProjectFavicon, React would unmount and remount it each time PullRequestFiltersMenu re-renders—worth digging into how significant that visual flicker actually is. Tracing the atom family behind the favicon's URL creation, remounts likely reuse a cached atom value rather than refetching, and the favicon image component also reads from a module-level cache for its initial paint, so there's probably no visible flash—just some unnecessary churn, not a strong violation. I also notice the aria-hidden prop passed down gets silently dropped since ProjectFavicon doesn't accept unknown props, though that matches prior behavior since the favicon wasn't aria-hidden before either. Moving on, I want to check whether the new PullRequestRowLabels chip is essentially reconstructing the existing Badge primitive rather than reusing it—both chips sit in the same meta line with slightly different surface treatments, which is exactly the kind of duplicated geometry the guidance warns about. I should pull up badge.tsx to compare its variants against what this new chip is doing.

17. browse_code — apps/web/src/components/ui/badge.tsx

18. git_tools — labelDotColor

💭 The labelDotColor helper and the colored dot markup are now duplicated across the summary tab and the new filters module — I'd flagged this exact duplication earlier when there were three copies including one in the row, and while the row copy got removed, the filters copy remains and wasn't addressed. Since I've already flagged this duplication twice and the underlying issue is unchanged, re-raising it again would just be repetitive, so I should move on. Instead I'm going through the current head systematically to see what's genuinely new versus what's already been fixed or flagged: the row chip duplication, sort/filters trigger aria issues, the outlined button, project label truncation, and the unavailable pill are all either already addressed or already covered by my prior comments, so none of those warrant a new flag. Looking further, I notice a few items that could be new: the filter radio group's disabled logic actually predates this change so it's not new, the repeated group labels inside submenu popups are redundant but harmless, and I'm now checking the author submenu's InputGroup layout inside the popup for any real issue worth flagging. Checking button sizing consistency between the trigger and search input confirms they match. The compact menu paths and condensed header layout also look unchanged and fine, so nothing new seems to violate the design so far. I want to look more closely at the submenu's label/value spans for potential truncation or overflow issues in the flex layout. Since max-width clamps the automatic minimum size, truncation should work correctly, and the label span without min-w-0 is fine since labels are short fixed strings. The popup's min-w-72 comfortably fits the trigger row's icon, label, value, and chevron combined widths. Now I want to check the diff-stat block in PullRequestRow to see how PullRequestDiffStat behaves inside its flex container. Now I'm noticing that adding a labels chip to the meta line could actually increase row height slightly—the chip's 18px line height versus the surrounding 16px text-xs line means rows with labels might grow ~2px taller than the declared 54px intrinsic block size, which could cause scrollbar drift for offscreen rows. Let me work out the exact math for row height with padding and content included. Actually the pre-existing chip already inherits 16px line-height plus its border, landing at 18px, while the new chip's py-px and leading-4 push it to 20px — so there's a real drift in the intrinsic size estimate used for scroll virtualization. That's worth noting since the code has an explicit comment about preserving intentional exceptions for browser/virtualizer behavior, so I should be careful not to disturb that logic. Now I'm working through the actual row height math: column three stacks a time line and a stat line at roughly 32px total, while column two stacks the title and meta line at 40-42px, so the row content lands around 40-42px before padding, giving roughly 56-58px total — and I'm checking how that changes when labels are absent from the meta line. Doing the arithmetic more precisely, the declared 54px baseline lines up exactly with padding plus title plus a 16px meta line, but adding a 20px chip pushes it to 58px — a 4px mismatch that's common enough to cause scrollbar jitter with content-visibility:auto, since offscreen rows still report the old 54px height. That's a solid, defensible bug tied to the intentional height comment, and the cleanest fix is shrinking the chip to fit within 16px by dropping the border and padding or using a tighter line-height and smaller text size. Checking the existing "matched in description" chip, it also has a border, so it likely already produces an 18px height instead of the assumed 16 — meaning there's already a small pre-existing drift in search results, but it's isolated to that one case, whereas the new label chip appears on every labeled row and would be a much more pervasive issue. I trace through the math: 54 seems to line up almost exactly with padding (16) plus the title's 20px line height, plus 2px margin, plus the meta line's 16px — that's a strong signal the new label chip is adding roughly 4px extra to every labeled row's real height versus its declared containment size, which could cause scroll height drift and jumpy scrollbar behavior on long lists. I want to suggest sizing the chip down to match the meta line's 16px instead. I'm confirming the meta line's Tailwind text-xs computes to a 16px line-height, and the title's text-sm to 20px, so my 54px baseline calculation checks out. Now I'm reconsidering whether the right column's line count actually changed given how the diff stat conditionally renders. I'm also glancing at the author-search input to make sure it accepts a "compact" size prop—CI typecheck passing confirms it does. Given the scroll-estimate drift feels fairly minor, I'm weighing whether it's really significant enough to fail the check versus just flagging it inline. I'm now leaning toward reporting it as a finding, since the check defaults to failure whenever anything is found, but I want to double-check whether the intrinsic size value was chosen loosely or is a real documented invariant—so I'm looking at whether other rows already vary in height, like the environment label or checks popover.

19. git_tools — className=

💭 I'm confirming the checks icon and avatar both sit at 16px, so the meta line's height is 16px today, and the new 20px chip is the first thing to exceed it. The smallest fix is sizing the chip down to match the meta line rather than bumping the container's intrinsic block size. Working through the chip's actual height, I calculate 16px content plus padding plus border equals 20px, confirming rows with labels render at 58px against a declared 54px — a real discrepancy that compounds across a long list and causes scrollbar drift and row jitter. I'll flag this with a comment on the chip's className line in PullRequestRow.tsx, then check if there's another issue worth raising. I scan a few other spots — a potentially empty label name, unused imports, import ordering — but none of those seem worth flagging. I confirm cn was removed and is genuinely unused, while MenuGroupLabel, Tooltip, MenuSeparator, and MenuItem are all still used correctly. I decide to post one inline suggestion on line 20 of PullRequestRow.tsx, making sure to preserve exact indentation and only modify the diff lines when writing my fix. I fix the chip's height by swapping padding and line-height so it lands at exactly 20px, then draft the review comment explaining the sizing issue before showing the corrected code.

20. modify_pr — One finding: the new row label chip is taller than the meta line it joins, which puts labeled rows over the contain-intrinsic-block-size the row declares for skipped rows.


Agent Credits: 227 credits

Token usage: 28 uncached input · 1,261,008 cache-read input · 153,558 cache-write input · 22,633 output
Agent Credits may also include non-token charges from external tools such as web research.