Skip to content

fix(web): address keybindings review findings

MacroscopeApp / Macroscope - UI Consistency failed Aug 25, 2026 in 5m 39s

UI Consistency: 2 issues found

Two findings in apps/web/src/components/settings/KeybindingsSettings.tsx:

  • Accessible shortcut text lost (KeybindingKeyControl, ~line 818-828): the deleted sr-only formatShortcutLabel(...) span was the only text representation of an assigned binding. The pill button sets aria-label, which excludes the inner KeybindingPill glyphs from the accessible name, so assistive tech can no longer read what a shortcut is set to. Fix: include the formatted shortcut in the button's aria-label (re-import formatShortcutLabel) or restore the sr-only span.
  • Interactive control as heading content (NewKeybindingSettingsRow, ~line 1178-1183): SettingsRow renders title inside an <h3>, so the add row emits a heading whose entire content is the command combobox (no static heading text). Fix: give the add row a plain-text title and move NewKeybindingCommandSelect into the control cluster, or add a non-heading title option to SettingsRow.

Both were posted as inline review comments.

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.

Verification performed:

  • Confirmed the three findings from the prior run are resolved: the pill/capture Input now uses size="compact" (and border-primary/70 bg-primary/5 legitimately lands on the wrapper span, which owns the border in ui/input.tsx); WhenClauseControl collapsed to a single inline treatment with no dead variant/className pass-throughs; the browser-notice tooltip trigger now carries aria-label="Browser keybinding limitations", matching UnknownWhenVariableWarning / KeybindingConflictWarning.
  • Checked primitive contracts used by the diff: Badge supports variant="outline" + size="sm"; Button size="compact"/icon-sm and Input size="compact" exist; SettingsRow accepts ReactNode title/description/control; Popover/Select popups are portaled, so nesting their triggers inside <h3>/<p> does not create invalid DOM.
  • Styled PopoverTrigger class strings (the WhenClauseControl trigger) match established repo practice (GitActionsControl, PullRequestReactions, ThemeColorPicker, etc.), so that was not flagged; the raw pill <button> and the size-7 override on the row menu button are pre-existing code moved unchanged.
  • ScrollArea, InfoIcon, and formatShortcutLabel imports were removed with no remaining references in the file; only KeybindingsSettings.logic.test.ts exists and it is unaffected by the layout refactor.
  • Mobile/sm layout re-checked: SettingsRow's stacked/grid layout replaces the removed min-w-[680px] horizontally scrolled grid, and the control cluster fits typical narrow widths, so no overflow regression was reported.

🔍 Agent activity — 22 tool calls

💭 Checking the diff between the prior commit and head, it looks like all three previous findings—the missing accessible name on the tooltip trigger, the unused variant on WhenClauseControl, and the className-vs-size mismatch on the input—have been resolved. Now I want to look over the current state more broadly for any new issues. I'm checking whether Badge actually supports a size prop, and whether SettingsRow's title/description props accept ReactNode. I also want to verify that removing ScrollArea for horizontal scrolling on narrow widths is safe now that rows use SettingsRow.

1. browse_code — apps/web/src/components/settings/settingsLayout.tsx

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

💭 Let me check the current version of KeybindingsSettings.tsx to see the relevant sections.

3. browse_code — apps/web/src/components/settings/KeybindingsSettings.tsx

💭 Looking further at lines 1100-1300 and the panel section, I'm checking whether SettingsRow correctly nests interactive elements — the title heading now contains a Select button for NewKeybindingSettingsRow, which is fine structurally, and the WhenClauseControl button inside the description paragraph is also valid since buttons are phrasing content. I'm noting a few pre-existing patterns that were moved unchanged: KeybindingRowMenu's raw Button with overridden size classes, and the pill button reimplementing a Button. I'm also flagging that the sr-only shortcut label span was removed, so I need to check whether KeybindingPill still exposes accessible text for the shortcut value elsewhere. The layout containers for both row types look fine on inspection. The rounded-none overrides on SettingsRow feel like minor, repeated styling choices but not significant enough to flag. I'm also noting the divide-y styling used in KeybindingsList and whether it conflicts with anything being removed. Beyond that, I'm checking WhenClauseControl's trigger — it lost some border and width styling in favor of a quieter inline look, which seems intentional as part of the redesign, and its focus-visible styling still relies on the ring so accessibility should be preserved. I'm also verifying that BrowserKeybindingNotice's tooltip trigger properly has an aria-label alongside its focusable span. Now let me look further into the file around lines 1100-1300, and check the KeybindingPill and KeybindingConflictWarning components as well.

4. browse_code — apps/web/src/components/settings/KeybindingsSettings.tsx

5. git_tools — function KeybindingPill|function KeybindingConflictWarning|function UnknownWhenVariableWarning

💭 I'm noticing an inconsistency in how the trigger components pass their icon children versus using the render prop with a self-closing span, and considering whether Base UI actually merges children into the rendered element in these cases. Now I'm looking at a bigger concern: SettingsRow wraps a Select trigger inside an h3 heading, which puts an interactive form control inside heading semantics. That's a genuine accessibility issue worth flagging, since screen readers would announce the heading with the combobox's current value nested inside it. I'm also weighing whether converting the previous table-like layout (divs styled as a grid with a header row for Command/Keybinding/When/Status) into repeated SettingsRow components turns every command name into an h3 heading. That means hundreds of headings polluting heading navigation and losing column semantics that screen reader users previously relied on — a real regression, though I'm unsure if it's too editorial to flag for a redesign PR. Then I trace how the pill-to-input swap in KeybindingKeyControl behaves inside the new justify-end flex layout — previously fixed-width grid columns kept things stable, but now growing the input and adding a Save button could shift the menu button, though since the container grows leftward while the menu stays anchored right, it seems mostly fine. I also note that autoFocus on NewKeybindingKeyInput defaults to false and appears unused, along with the className param on NewKeybindingCommandSelect. Now I'm checking the unused prop-passing issue further — earlier comments flagged variant/className/autoFocus as unnecessary pass-throughs, and while variant/className were removed from NewKeybindingWhen, autoFocus remains dangling on NewKeybindingKeyInput with no call site actually using it. I also spot a possible duplication where KeybindingsListProps extends KeybindingRowActions (which includes allRows), yet allRows seems to be passed alongside rows separately. I'm weighing whether re-flagging the leftover autoFocus prop counts as a duplicate of a comment already made — I'd lean toward not repeating it. Now I want to verify whether the Input primitive supports a "compact" size and whether className is applied to the wrapper span as previously noted. If the border/background highlight classes for the recording state land on the wrapper while the actual border is rendered on the inner input element, that styling would silently fail to apply — a real behavioral bug worth flagging, though I should check if this pattern predates the current change since it also existed in the moved NewKeybindingKeyInput component.

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

💭 The border styling on the wrapper span confirms the compact size fix works correctly. Now I'm checking the SettingsRow control layout for a keybinding row, where the input, save button, and menu together need roughly 20rem of space, so I'm verifying the grid column sizing accommodates that width properly. For the mobile layout, SettingsRow stacks title/description above the full-width control, which looks fine. I'm now wondering whether PopoverContent and SelectContent are portaled — since a positioned div rendered inside a <p> tag (from the description text) would create invalid HTML nesting and potential hydration issues, so I want to check popover.tsx and select.tsx to confirm they use a Portal.

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

💭 I'm noting that the PopoverTrigger in WhenClauseControl was changed from a bordered, visibly affordant style to a quiet inline trigger with just hover/focus states, which is a deliberate redesign but weakens visible affordance. I'm weighing whether this counts as recreating a Select-like primitive locally, though it seems more like a legitimate custom Popover trigger with a chevron rather than a disallowed reimplementation. I realize the missing cursor-pointer isn't a new regression since the previous trigger lacked it too, but since these classes were rewritten in this PR, the reconstruction pattern is still worth flagging if it deviates from convention. I want to check whether the repo typically uses render={<Button/>} for popover triggers rather than hand-rolling ghost-button styles, and verify the focus ring color token is consistent with other primitives. I should also check if there's an existing "ghost-muted" variant with "compact" sizing matching h-6 px-1 rounded-sm, and look at other similar quiet inline trigger patterns like the KeybindingRowMenu button, though that one is pre-existing moved code so I can skip it. Let me look at the button primitive file directly.

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

💭 I'm noticing the micro button variant includes cursor-pointer and coarse-pointer touch target styling, but the new WhenClauseControl PopoverTrigger recreates the ghost button styling manually and is missing the cursor-pointer class. Also that pointer-coarse hit target was missing before too, so that's not a new regression, though the trigger shifted from a bordered select-like control to a borderless smaller one, reducing touch affordance while a Button component with matching variants already exists in the codebase. I want to check whether other PopoverTriggers in the repo typically wrap a Button component or use custom classes like this one does, to judge if this is really worth flagging.

9. git_tools — PopoverTrigger

💭 Raw-styled PopoverTrigger with classes seems common elsewhere in the repo, so that's not a real finding. I'm now checking whether the WhenClauseControl trigger button, nested inside SettingsRow's description paragraph, could create invalid HTML nesting since a button inside a <p> tag isn't valid markup. The bigger issue I'm homing in on is that every binding row's command title becomes an <h3> heading, and in the add row that heading's sole content is an interactive Select combobox — that's a real semantic regression versus the prior plain-div design. The cleanest fix would be extending SettingsRow with a configurable heading level or title element instead of hardcoding <h3> everywhere. But before flagging that, I should check whether SettingsRow is already used elsewhere in the codebase for dynamic mapped lists — if so, this pattern would just be consistent existing practice rather than a new issue. Let me search for other places that map over SettingsRow.

10. git_tools — <SettingsRow

11. git_tools — SettingsRow

💭 I want to check whether ProviderSettingsPanel and ConnectionsSettings render SettingsRow inside a map with repeated data, or pass an interactive title, so let me look at ProviderSettingsPanel around lines 180-240.

12. browse_code — apps/web/src/components/settings/ProviderSettingsPanel.tsx

13. browse_code — apps/web/src/components/settings/ConnectionsSettings.tsx

💭 I'm weighing whether the new usage stretches SettingsRow beyond its original contract—especially the case where a Select gets wrapped inside the <h3> heading, which risks incorrect accessibility semantics since that primitive was designed for static, single-setting titles rather than repeated interactive rows. I should also check whether rows now overflow the card's rounded corners since square-cornered first/last rows inside a rounded container could clip oddly — worth viewing that container's styling.

14. browse_code — apps/web/src/components/settings/KeybindingsSettings.tsx

💭 I'm checking whether the settings section previously had a card wrapper with border and rounded corners before the header row and ScrollArea were removed, so I need to compare against the merge-base version around that region.

15. browse_code — apps/web/src/components/settings/KeybindingsSettings.tsx

💭 I'm thinking the ScrollArea removal is fine since the responsive rows now handle scrolling. I should double-check that ScrollArea isn't still referenced elsewhere in the file after removing its import, since that would break things. I also want to verify formatShortcutLabel and InfoIcon aren't used elsewhere after their imports were removed—need to grep for both.

16. git_tools — ScrollArea|formatShortcutLabel|InfoIcon|TriangleAlertIcon|useCallback

💭 I'm checking whether the row height change from the compact table to SettingsRow will cause issues with long keybinding lists—rows are now taller, which is just a design tradeoff. I'm also verifying that KeybindingSettingsRow correctly handles the "Always" case with no when clause, and confirming where useKeybindingRowEditor gets called for editing state. More critically, I've spotted an accessibility regression: the old layout had a screen-reader-only span exposing the formatted shortcut text, since the visible KbdGroup only shows symbols inside a button whose aria-label overrides its content. Removing that sr-only span means screen reader users can no longer learn what the current shortcut actually is for a row—the button just announces "Edit shortcut for..." without the value itself. The cleanest fix is either restoring an sr-only span or folding the formatted shortcut into the aria-label directly. I want to check where formatShortcutLabel is defined and what it outputs before proposing the exact fix.

17. git_tools — export function formatShortcutLabel

💭 I'm noting that formatShortcutLabel gives symbol glyphs on Mac (like ⌘K) versus plain text like "Ctrl+Shift+K" elsewhere, so the screen-reader value of that sr-only span was already marginal since VoiceOver reads ⌘ as "command" anyway. Still, removing that text representation of the keybinding could count as a real regression in some sense. Let me check whether any tests reference that sr-only span or the shortcut label text, particularly in the KeybindingsSettings tests.

18. git_tools — formatShortcutLabel|Edit shortcut for|sr-only

💭 No tests found. I should check whether there's a test file for KeybindingsSettings.tsx that references the removed "Command"/"Keybinding" table header columns, since that could break tests — let me search for related test files.

19. browse_code — apps/web/src/components/settings/Keybindings*

💭 I'm weighing whether the hook extraction and prop rewiring here counts as behavior-changing composition that warrants new focused tests, even though there weren't component tests before this PR either — requiring them now might be asking for more than is fair given the baseline. Now I'm sorting through candidate findings: the add-binding row's heading has no text with an interactive control nested inside it, and every binding row becomes an h3, which seems like a real semantics regression from the migration. There's also a concrete accessibility loss where the pill button's aria-label now overrides the Kbd glyphs, hiding the shortcut text from assistive tech. I also notice an unused autoFocus prop on the new keybinding input that has no call site, but since I likely flagged something similar before, I'll leave it out or mention it only lightly. I'm also weighing whether KeybindingsList passing both rows and allRows as the same array matters, but that seems more like internal plumbing than a UI issue, so I'll skip it. More interesting is the shrunken hit target on the WhenClauseControl trigger — it went from a full-width h-7 bordered control to a small h-6 px-1 text trigger, which is a clear touch-target regression, and it's missing cursor-pointer too, so I want to check whether Tailwind's preflight or the repo's base styles already handle button cursor styling before flagging this. The simplest fix would be adding cursor-pointer back and considering a larger hit target, or just using the Button primitive with a ghost variant. Before finalizing though, I should check index.css for any global button cursor rule so I don't make a false claim.

20. git_tools — cursor-pointer|cursor: pointer

21. browse_code — apps/web/src/index.css

💭 I'm noting that the pill button cursor issue is pre-existing and unrelated to this change, so I shouldn't flag it. Now I need to weigh which findings are worth reporting without being noisy—comparing the Select-inside-h3 issue against the sr-only shortcut removal to decide what's actually worth surfacing. For the sr-only removal, I'm confirming this is a real regression: the old row exposed the current shortcut text via a hidden span for screen readers, but the new row drops that entirely for assigned bindings, leaving only visually-hidden glyphs inside an aria-labeled button. That means screen reader users can no longer perceive what shortcut is currently set. The fix is small — append the formatted shortcut text to the pill's aria-label using the existing formatting helper and binding data already in scope. I'm also weighing a second, more editorial finding: the add-row's command Select rendered inside an <h3> heading means the accessible heading name becomes whatever placeholder or value the combobox shows, injecting a bogus heading into screen-reader navigation. This feels worth flagging too, with a minimal fix being to give that row a real text heading and move the Select out of the heading itself. Then for the trigger button issue, I want to check whether other raw triggers in this file also lack cursor-pointer styling before deciding how strongly to call it out, since there seems to be a pre-existing pattern of that omission elsewhere. I'll also verify that KeybindingConflictWarning placement inside the control area works fine, and confirm the binding row's flex control layout inherits properly for the grid columns. The New row wraps its control in an extra flex-wrap container while the existing row doesn't, which is a minor inconsistency but not a real bug. On mobile, the existing-row control lacking flex-wrap could theoretically overflow at very narrow widths given the width sum of the input, save button, and menu, but it's borderline enough not to flag as a definite issue. Doing the math at a 320px width, the row's contents mostly fit within the available space, with only a marginal overflow risk when a conflict warning icon appears alongside an active edit state. That edge case is minor enough to skip, so I'll settle on just the two inline comments. I confirm the h3 heading structure for SettingsRow titles, including the keybinding variant with its icon-and-span layout inside the heading. Since the Select-inside-heading issue is a real semantic inconsistency, I'll include both findings and mark the check as a failure state rather than "all clear," then move on to figuring out the correct diff line numbers for the inline comments. I trace both target lines against the head file — the pill button's aria-label and the new keybinding row's Select title block — confirming both fall within added lines of the diff hunk, so I can safely anchor comments to those right-side line numbers before drafting the actual comment text. Since the fix needs both a diff and an import restore, that's multi-hunk territory, so I'll describe it in prose with inline code rather than a suggestion block instead. Moving to comment B on line 1179: the add row renders its title inside an <h3>, but that heading's only content ends up being the command combobox — so heading navigation lands on an entry with no static text, and every refresh drops an interactive control inside a heading. I want to suggest giving the add row a plain, non-heading container instead. Let me reconsider whether this is actually a real accessibility problem — an interactive control inside a heading is technically valid, and screen readers just announce it as a heading level with a control inside, so I should temper how strongly I phrase this before finalizing the review comments and failure state.

22. modify_pr — Reviewed the keybinding settings redesign against the shared primitive/Tailwind ownership rules. The three items from the previous run are resolved (size="compact" on the pill/capture Input, the collapsed single-treatment WhenClauseControl, and the labelled browser-notice tooltip trigger). Two remaining findings, both semantics/accessibility rather than styling: - The deleted sr-only shortcut text leaves assigned bindings with no accessible representation of their current key. - The add-binding row puts the command Select inside SettingsRow's <h3>, producing a heading with no static text.


Agent Credits: 200 credits