Skip to content

test(web): follow project filter submenu

MacroscopeApp / Macroscope - UI Consistency failed Aug 30, 2026 in 6m 35s

UI Consistency: 4 issues found

Four consistency issues were found in the changed web UI code; inline comments were posted on each.

  • apps/web/src/routes/_chat.pull-requests.tsx (~L1745-1748): the new outlined mode of CompactFilterMenu reconstructs Button's outline variant on a bare MenuTrigger, next to a Filters trigger that is a real Button variant="outline". It loses the focus-visible ring, cursor-pointer, coarse-pointer hit target, --control-radius, popover/dark surfaces, and is h-8 where Button default is h-9 sm:h-8, so the two controls in the same row differ in height on narrow viewports.
  • apps/web/src/components/pullRequest/PullRequestListFilters.tsx (L277-279): the author submenu's search input stops propagation of every non-Escape key, blocking ArrowDown/ArrowUp/Home/End from reaching the Base UI menu's composite root. With autoFocus on open, the author list is unreachable by keyboard.
  • apps/web/src/components/pullRequest/PullRequestListFilters.tsx (L269): InputGroup className="h-8" overrides the wrapper height while InputGroupInput keeps its own h-8.5 sm:h-7.5, so the field overflows its bordered box on mobile. The Input size contract (or the input-targeting pattern used by PullRequestSearchInput) should own this.
  • apps/web/src/components/pullRequest/PullRequestRow.tsx (L18-21): labelDotColor is now a third copy, and the dot-plus-name label chip is duplicated with drifting geometry across PullRequestRow, PullRequestListFilters, and PullRequestSummaryTab; the shared treatment should live in pullRequestPresentation.tsx.

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.

Files examined

  • apps/web/src/components/pullRequest/PullRequestListFilters.tsx and its test
  • apps/web/src/components/pullRequest/PullRequestRow.tsx
  • apps/web/src/components/pullRequest/pullRequestList.logic.ts
  • apps/web/src/routes/_chat.pull-requests.tsx
  • Context: components/ui/{menu,button,input,input-group,combobox}.tsx, pullRequestPresentation.tsx, PullRequestSummaryTab.tsx, ProjectFavicon.tsx

Checks that passed

  • MenuSubPopup consumers pass explicit width utilities (w-80, w-72, min-w-56, min-w-72), so MenuPopup's min-w-32 defaulting contract is respected.
  • The project submenu retains disabled state and the tooltip reason for unavailable projects via PullRequestFilterRadioGroup; ProjectFavicon keeps shrink-0 through its own cn merge.
  • No environment-scoped action was introduced without explicit scope; PullRequestChecksPopover and row actions still receive entry.environmentId.
  • No global CSS, theme variant, or scroll/mask ownership was touched.

Lower-priority observations (not posted)

  • collectPullRequestListFacets and the new largest/smallest sort are new behavior with no focused tests, while pullRequestList.logic.test.ts covers the neighbouring helpers.
  • The project submenu builds its Icon as an inline arrow component inside render, giving ProjectFavicon a new component identity on every render; the module-level favicon cache keeps the cost small.
  • Sorting by change size uses asynchronously arriving diff stats, so rows can reorder as counts stream in.

🔍 Agent activity — 22 tool calls

💭 I want to look at the actual code, focusing on PullRequestListFilters.tsx, PullRequestRow.tsx, pullRequestList.logic.ts, and _chat.pull-requests.tsx to understand the changes.

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

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

💭 I'm checking that the trigger button still works fine with default size plus visible "Filters" text and an accessible label, and confirming the unavailable project options still show their disabled state with tooltips as before. I notice the ProjectFavicon icon lost its shrink-0 class in the refactor — minor but worth flagging. The "Unavailable" badge also seems to have been dropped in favor of just opacity plus tooltip, which could read as a regression though it's arguably an intentional simplification. I'm also spotting that the Icon component for projects gets redefined inline inside the map on every render, causing unnecessary remounts of the favicon — a small performance concern. Now I want to check the MenuSubTrigger for author/label filters and its margin-auto styling to see if something more significant changed there. I'm now tracing through the search input inside the author filter menu, since typing there stops keydown propagation for all keys except Escape — meaning arrow key navigation from the input to menu items likely gets blocked. That looks like a real keyboard accessibility issue, so I want to check whether other searchable menus in the codebase handle this differently. Continuing through the smaller details: the filter count logic spreading selected labels looks fine, the radio item handling for "Anyone" with an empty value looks correct, and the sub-popup width follows the existing contract. I'm flagging that disabled menu items used for empty states ("No authors found") might be better as non-interactive elements, and noting there's a duplicated label color helper showing up in PullRequestRow. I'm treating that duplication as a real finding — the same labelDotColor logic and label-dot markup appears in both PullRequestListFilters and PullRequestRow, which should be extracted into a shared helper rather than repeated, so I want to check whether pullRequestPresentation already has something like a label chip renderer. I'm also looking at how PullRequestRow now wraps PullRequestDiffStat to show "N lines" and hide it when zero, whereas before it always rendered, so I want to check the row's prior behavior for that stat. That's a minor code-quality nit rather than a UI issue really. Next I'm checking CompactFilterMenu's outlined trigger, since it looks like it hand-rolls button-outline styling classes instead of using the primitive, which matches the pattern of a raw element recreating Button behavior — I want to compare it against how the trigger was previously implemented before flagging it as a concrete finding.

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

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

💭 Let me check CompactFilterMenu and how the control row is laid out.

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

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

💭 I'm noticing the sort menu's trigger is a bare Base UI button hand-styled to mimic an outline button, sitting right next to another trigger that properly uses the Button component with variant="outline" — that inconsistency is worth flagging as a concrete finding. I should compare against Button's actual outline variant classes to check for mismatches in height, focus ring, hover states, and disabled behavior. Let me check the Button component's source.

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

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

💭 I should check pullRequestPresentation for PullRequestDiffStat and see if there's already a label chip or label color helper being used elsewhere, since duplicating one would be worth flagging.

9. git_tools — labelDotColor|label.color|labelColor|badgeColor",|hexColor

10. git_tools — PullRequestDiffStat|PullRequestActorAvatar

💭 I notice labelDotColor now exists in three files — the pre-existing one plus two new copies. I want to check the label chip markup in PullRequestSummaryTab against the new row chip to see if there's a shared component that should be reused instead, so I'll look at lines 60-80 and 600-650.

11. browse_code — apps/web/src/components/pullRequest/PullRequestSummaryTab.tsx

12. browse_code — apps/web/src/components/pullRequest/PullRequestSummaryTab.tsx

💭 I'm noticing the label chip pattern is now duplicated across three places with slightly inconsistent styling, plus the helper function tripled and labelDotColor copied twice. This looks like a case for extracting a shared PullRequestLabelChip component/renderer instead of repeating the markup and logic at each call site. Then looking at the sort menu trigger, CompactFilterMenu with outlined reconstructs Button's outline variant manually but diverges in multiple ways: wrong height on mobile (h-8 vs Button's h-9), missing focus-visible ring, missing pointer-coarse hit target, missing cursor-pointer, wrong border radius, transparent instead of bg-popover, and no dark-mode variants — plus a text-size mismatch relative to Button's responsive classes. Since this sits right next to a real outline Button in the same row, the fix is clearly to render the trigger through <Button variant="outline"> instead of hand-rolling the styles, and I'm also flagging the missing focus ring as a keyboard accessibility gap. Now I'm considering a second finding: the author submenu search input blocks propagation of all non-Escape keydown events, so arrow keys, Home, and End never reach the Base UI menu — combined with autofocus landing in the input on open, keyboard users get completely stuck and can't navigate to the radio items. I want to check whether there's an existing searchable-menu pattern elsewhere in the codebase, like PullRequestReviewerPicker, that handles this correctly so I can reference it.

13. git_tools — onKeyDown

💭 I wonder if there's an existing searchable combobox pattern in the repo I should reuse, so I'll check the ui folder for filterable pickers like a reviewer picker component.

14. browse_code — apps/web/src/components/ui/*.tsx

15. browse_code — In apps/web/src, is there an existing searchable/filterable picker inside a Menu or Popover (e.g. PullRequestReviewerPicker, command palette, combobox)? Show how the search input handles keyboard interaction with the menu (arrow keys, typeahead), including any use of Combobox/Command primitives from components/ui.

💭 I'm confirming Combobox is the canonical searchable picker in the codebase. The new author submenu blocks arrow-key navigation because it stops propagation of non-Escape keys, whereas ModelPickerContent lets arrow keys fall through since Base UI's Combobox handles them directly on the input itself. In a Menu, keyboard navigation is managed by an ancestor composite root, so stopping propagation on the input prevents ArrowDown from reaching the popup's handler — this is a genuine accessibility regression that would block keyboard users from reaching the author list. I'm also noting a third issue: the Project submenu's Icon prop is being recreated as a new inline arrow function on every render, inside a non-memoized array. Since it's a new component identity each time, React will unmount and remount the favicon subtree constantly, likely causing flicker or repeated fetches — I should check ProjectFavicon's internals to confirm the cost. I'm also weighing whether dropping the "Unavailable" badge (leaving only opacity and tooltip) counts as a real regression or an intentional simplification, since the underlying sorting behavior appears preserved. The explanatory comment about disabled rows reading as a "broken menu" got deleted, but that's product rationale rather than one of the browser/virtualizer/theme/Electron exceptions I'm supposed to protect, so it's a minor loss. I'm also noting that the favicon icon lost its shrink-0 class, which could let it compress next to long labels, and that the Button trigger removed size="icon" while keeping an aria-label that no longer matches its now-visible "Filters" text — a WCAG 2.5.3 label-in-name violation worth flagging. Ninth, I'm noticing PullRequestRow now shows both "N lines" and the diff stat, which seems redundant, and I want to check whether the diff stat component hides itself when totals are zero. Tenth, the facet query only runs while the filter menu is open, falling back to baseline entries otherwise, which seems intentional. Eleventh, I'm checking whether the checkbox menu item for label filtering closes on click by default. The "Clear selected labels" menu item closing the menu on click seems acceptable. I'm also noting a minor grid truncation concern in the checkbox item layout where nested min-width isn't controllable, but it's a small issue -- next I want to actually verify the diff stat component's zero-handling.

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

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

💭 ProjectFavicon already handles shrink-0 through its className merge, so that's fine to drop. Since the component holds its own displayed-src state seeded from the module-level cache, remounting on every parent render causes minimal flicker but does mean the asset URL subscription gets torn down and re-subscribed each time, which is a bit wasteful but probably tolerable. I'll skip the diff stat duplication—showing both the +/- counts and "N lines" isn't really a violation worth flagging. What matters more is whether the new interactive behavior—author search, label multi-select, sort, and the new collectPullRequestListFacets function—has test coverage, since the composition rules require focused tests for state transitions and behavior changes like this. Let me check the test files for facets and filter updates.

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

19. git_tools — collectPullRequestListFacets

💭 Checking the test at line 113 that verifies no change is emitted when reselecting the same project — even though the submenu's onChange now calls onProject unconditionally, the radio group's own guard (comparing against current value) filters out the duplicate, so the test still passes despite the removed guard upstream. I'm also noting that collectPullRequestListFacets is a new pure function with nontrivial state-filtering and merged-count logic but has no dedicated tests — worth flagging as a lower-priority suggestion rather than a hard requirement, since the review guidance is about not over-demanding tests for every change. Now I'm moving on to compile the actual list of inline findings to post, starting with the pull-requests route file. I'm also flagging that the compact filter menu in the pull-requests route recreates a button-outline look on a raw menu trigger instead of using the real Button component, which loses the focus ring, pointer cursor, larger touch target, border radius, and consistent height with the other filter button nearby. The fix is to render the trigger through Button with variant="outline", and since Button is already imported in this file, I'm working out whether I can express this as a self-contained diff across the affected lines. Separately, in the author search input for pull-request filters, the onKeyDown handler stops propagation for every key, which blocks arrow-key navigation from reaching the underlying menu list once the input autofocuses — making the author list effectively unreachable by keyboard. I'm drafting a narrower fix that only stops propagation for typed characters while letting navigation keys like arrows, Home, End, Enter, and Escape pass through, and checking whether stopPropagation is even necessary here in the first place. Also flagging a third issue: the label-dot-color chip rendering logic now appears in three separate places (originally in PullRequestSummaryTab, plus two new spots), which suggests this pattern should be extracted into a shared helper in the presentation module rather than duplicated further, even though this crosses file boundaries so there's no single diff to attach it to. I'm also considering the Filters trigger button—now with visible "Filters" text plus a count badge, but its aria-label still just says "Filter pull requests," creating a mismatch between the visible label and accessible name. That's a legitimate WCAG Label in Name concern worth noting as a minor inline comment alongside the other consistency findings. I'll skip flagging that aria-label point since the accessible name remains accurate, and reconsider whether my third finding is worth including given its low severity versus noise. I'm also weighing a separate issue: sorting by size uses additions plus deletions from stats that load asynchronously, so rows without stats yet sort as zero, causing jumpy reordering as data streams in — real but maybe outside scope for a consistency check. I'm also noting that grouping headers seem to disappear in some case worth tracing further. Checking a few more menu interaction details — checkbox items in submenus not closing on click, radio items in submenus closing as expected, autofocus potentially competing with default submenu focus behavior, and how an input group inside a menu popup behaves — all seem consistent enough with the existing pattern, though minor edge cases worth a quick second look. I'm also noting the InputGroup has a hardcoded height class overriding what should probably come from a size prop, so I want to check the input-group component for a proper size variant instead of the call-site override.

20. browse_code — apps/web/src/components/ui/input-group.tsx

💭 I'm noticing that the InputGroup wrapper doesn't set its own height, so forcing h-8 there while the inner Input might keep h-9 on mobile could cause clipping. I recall the existing PullRequestSearchInput handles this by targeting the inner input element directly with responsive height classes rather than sizing the wrapper. Since PullRequestReviewerPicker uses Input's size="compact" prop, applying h-8 to the InputGroup instead is really overriding the primitive's height at the wrong element rather than using the intended size variant — I should check what heights Input's size options actually produce.

21. browse_code — apps/web/src/components/ui/input.tsx

💭 I'm noticing that InputGroupInput defaults to a taller height than the InputGroup wrapper now specifies, so the input overflows the group's border by a couple pixels at both default and sm breakpoints—vertical centering breaks and text gets clipped by the rounded border. This is a case of a call-site class overriding a primitive's core height. The proper fix is to use the size="sm" contract on InputGroupInput, similar to how PullRequestSearchInput targets the input directly with breakpoint-specific heights, rather than the wrapper. So my fix suggestion is to drop the manual height override on the group and pass size="sm" to the input instead. That gives me four solid findings: the sort trigger duplicating Button styling, the author search input swallowing arrow keys and breaking keyboard nav, this InputGroup height mismatch, and the triplicated labelDotColor label-dot chip logic. Now I need to double check the diff line numbers so my review comments land correctly, since the condensed diff might not match the actual file's line numbering—I should verify against the real PR patch. I'm confirming which lines in each file fall within the actual diff hunks so my comments will be commentable on GitHub, checking the added blocks in the filters component, the row component, and the route file where the sort trigger duplication happens. I'm drafting my first review comment about the sort trigger issue — explaining how it loses the outline button's focus ring, hit target, and other styling since it isn't rendered through the actual Button component, and proposing a fix. Refining the fix to route through Button's outline variant while checking whether size "sm" matches the compact row height used elsewhere. Since the size prop needs to apply to two separate lines, I'll construct a full replacement spanning the whole InputGroup block, preserving exact indentation from the original file. Instead of a named constant, I'll inline the list of allowed navigation keys directly in the condition so the suggestion stays compact. Then I move on to the next comment about a duplicate labelDotColor/chip in PullRequestRow.tsx line 18. I'll keep my summary focused on the four main findings rather than adding a note about untested facet logic. Reconsidering comment 3's strength—three duplicated copies is solid grounds for flagging it per the "named primitive" consistency rule. Given the keyboard-navigation regression and the repeated primitive pattern are both concrete issues, I'll set the review outcome to failure rather than defaulting to pass. I also confirm line 1747 in the head file matches the outlined button styling I expected before finalizing the review post. Now I'm checking whether that region falls within the diff, and it does—the added lines around 1745-1748 confirm it. Turning to comment 2's suggested fix for lines 269-283, I need to match the exact indentation from the file view when proposing the replacement, changing the InputGroup wrapper and adding a size="sm" prop to the input since InputProps supports that option, though I'm weighing whether "sm" makes it too small compared to the default height. The simplest fix is just to drop the height override and let the primitive control its own sizing, mentioning size="sm" as an alternative if a more compact field is desired—keeping the suggestion to a single inline change on line 269. With that settled, I'm ready to post the review.

22. modify_pr — ## UI Consistency Four findings, all in the changed web UI code. The two blocking ones are the hand-rolled outline trigger in the control row and the author search field swallowing the menu's navigation keys. - apps/web/src/routes/_chat.pull-requests.tsx — the new outlined mode of CompactFilterMenu reconstructs Button's outline variant on a bare MenuTrigger, next to a Filters trigger that is a real Button variant="outline". - apps/web/src/components/pullRequest/PullRequestListFilters.tsx — the author submenu's search input stops propagation of every non-Escape key, so arrow keys never reach the Base UI menu and the autofocused field traps keyboard users. - apps/web/src/components/pullRequest/PullRequestListFilters.tsx — InputGroup className="h-8" overrides the wrapper height instead of using the Input size contract, leaving the inner input taller than its border box. - apps/web/src/components/pullRequest/PullRequestRow.tsx — labelDotColor plus the label-dot chip markup is now a third divergent copy of the same treatment.


Agent Credits: 263 credits

Token usage: 46 uncached input · 1,336,312 cache-read input · 184,835 cache-write input · 26,930 output
Agent Credits may also include non-token charges from external tools such as web research.