Skip to content

fix(web): scope migrated tooltips to their own hover surfaces

MacroscopeApp / Macroscope - UI Consistency failed Aug 16, 2026 in 6m 36s

UI Consistency: 1 inline finding, 2 migration gaps

Prior findings on the earlier head are all addressed. One new issue and two migration gaps remain.

  • apps/web/src/components/DiffPanel.tsx (line 692): the "Remote only" indicator loses its accessible name — title was the only text and the CheckIcon is aria-hidden, so nothing is exposed when the tooltip is closed. Inline comment posted with a role="img" aria-label suggestion matching the repo's other icon-only tooltip triggers.
  • apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx (line 1414): <Badge title="The host will merge this on its own…"> still renders a native tooltip (useRender spreads title onto the span) in the same header row this PR converted. Reported in the review body; the line is outside the diff so no inline comment was possible.
  • apps/web/src/components/pullRequest/PullRequestListFilters.tsx (lines 166, 383): <MenuRadioItem title={…}> keeps the native tooltip for the same "unavailable host" reason that was converted in _chat.pull-requests.tsx. The new lint rule only matches lowercase intrinsic elements, so component-forwarded title props are a blind spot.

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.

Verified as resolved since the last run: DiffPanel head-ref trigger no longer wraps the base-ref Combobox; PullRequestDetailPanel branch tooltip no longer encloses PullRequestBaseFreshnessWarning; the pairing-link expiry trigger is scoped to the expiry label instead of the whole <p> containing AccessScopeSummary; CompactFilterMenu anchors the tooltip on a wrapper <span> rather than the data-disabled:pointer-events-none MenuRadioItem.

Other checks performed with no findings: Tooltip (Base UI Root) renders no DOM, so none of the wrappers change layout; call-site classes and aria-label/aria-pressed/disabled/pointer handlers are preserved on every converted trigger; nested TooltipTrigger → PopoverTrigger/MenuTrigger composition in ThemeColorPicker.tsx and DraftHeroHeadline.tsx follows existing precedent (Sidebar.tsx, PreviewMoreMenu.tsx); tooltip positioner z-[140] sits above menu/popover/combobox popups (z-[130]), the dialog surface (z-50) and the toast viewport (fixed z-100), so the new tooltips inside dialogs, popovers and the update toast paint on top; the title removals in ChatView.tsx and ConnectionStatusDot.tsx are safe (visible label / pre-existing tooltip). Remaining title= occurrences elsewhere in apps/web/src are heading/section props on custom components, not DOM tooltips, so the newly enabled rule does not break lint.


🔍 Agent activity — 1 tool call

💭 I'm checking whether the code changes in the current PR head address the four issues that were flagged in the earlier commits, so I need to compare the diffs to see what's actually changed. Looking at the DiffPanel.tsx modifications, I'm noticing the wrapping behavior still seems to be present around line 585. Examining the specific changes across multiple files — DiffPanel, PullRequestDetailPanel, ConnectionsSettings, and the chat pull-requests file — I can see that the tooltip triggers have been restructured to wrap only the relevant content (the ref labels and icons) rather than larger components like the Combobox or disabled menu items. This appears to address all four of the previous findings. Now I'm reviewing the current state for any new issues that might have emerged. The main concern is whether wrapping a MenuRadioItem inside a TooltipTrigger span could interfere with Base UI's menu keyboard navigation or item registration through its composite list context. The wrapper span with className="block" around menu items might also affect layout or highlight behavior. Since the TooltipPopup only renders for unavailable options, the tooltip will anchor on the wrapper for all items but only display content conditionally. I'm also noticing that some buttons had their title attributes replaced with tooltips, which is generally fine, but there's a potential accessibility issue: spans with only aria-hidden icons now lack an accessible name since Base UI Tooltip sets aria-describedby on the trigger rather than providing an accessible name. This could be a regression for elements like the "Remote only" span in DiffPanel. I should check the tooltip component implementation and also look at how ThemeColorPicker handles nested trigger composition with Base UI's render prop system. Now I'm examining the actual files to see where titles were removed without replacements. Looking at ChatView.tsx around line 6350 where title="Scroll to end" was stripped—need to verify if there's already a tooltip in place. Also checking ConnectionStatusDot to see how it's handling the tooltip text attribute. The ConnectionStatusDot component already had a tooltip, so removing the redundant title attribute was the right call. Now I'm moving on to check ChatView.tsx around line 6350. I'm noticing an accessibility issue with the DiffPanel's "Remote only" tooltip — it's nested inside a ComboboxItem within a popup, and while the tooltip positioning works fine with the z-index layering, the accessible name is getting lost since the icon is hidden from screen readers and the span no longer contains visible text. The tooltip's aria attributes need to be set up properly to preserve the accessible label. There's also a concern about keyboard navigation — if the Base UI Tooltip.Trigger is rendering as a non-focusable span rather than a button, keyboard users won't be able to access the tooltip at all, which is a regression from the previous implementation. Now I need to check the other components for similar issues. The ThemeColorPicker has both a tooltip and popover trigger on the same element, which could cause z-index conflicts — the tooltip at z-[140] might overlap the popover depending on how Base UI handles the stacking. And in _chat.pull-requests.tsx, I need to verify that wrapping a MenuRadioItem in a span doesn't break its layout or interaction patterns. Looking at the other files: FilePreviewPanel uses a tooltip trigger on a span in the breadcrumb which should be fine, and ChangedFilesTree wraps the tooltip directly around the file path element, which is also straightforward. But DraftHeroHeadline is more complex — it's nesting a TooltipTrigger around a MenuTrigger, which means both triggers are trying to compose their props together. This could cause conflicts with aria attributes, and there's also a z-index concern where the tooltip might stay visible on top of the menu popup when it opens. I need to check what z-index values the menu and tooltip popups are using to see if there's an overlap issue. Then there are tooltips inside other popovers and panels like in ServerUpdateProgress, PullRequestChecksPopover, and the various pull request tabs, which should mostly work but might have similar layering issues depending on how the popovers are structured. For PullRequestActorLabel, it was previously just a span with a title attribute and now uses a Tooltip. The key question is where it's being used — if it's inside a button or link, that's fine since the tooltip trigger renders as a span by default. But I should verify it's not creating nested interactive elements, especially if it's used in list rows where the entire row might be clickable. I also need to check the z-index situation with ThemeColorPicker and DraftHeroHeadline, plus look into something about PullRequestDetailPanel around line 1490 regarding freshness. Now I'm checking the remaining component issues, specifically looking at DraftHeroHeadline and ThemeColorPicker where nested triggers might be causing problems. I need to understand how Base UI's Tooltip behaves when it's wrapping other interactive elements like MenuTrigger or PopoverTrigger — particularly whether the tooltip closes automatically when those inner triggers are activated. Rather than trying to trace through the Base UI source, I should search the codebase to see if there are already examples of Tooltip wrapping MenuTrigger or PopoverTrigger compositions, which would tell me if this is an established pattern that works. I'm not finding any existing patterns for wrapping Tooltip around MenuTrigger or PopoverTrigger, so I'll need to do a broader search using git grep with context to see how these components are typically combined. I'm noticing the nesting pattern where the root component (Menu or Popover) wraps the Tooltip, which then contains the TooltipTrigger with a render prop for the trigger element. This same structure appears in both Sidebar and PreviewMoreMenu, so I should follow that precedent in ThemeColorPicker and DraftHeroHeadline. Now I need to check the actual implementations of those two components to verify the structure is correct, and then scan through the remaining changed files for any other potential issues with the tooltip nesting or other problems. These patterns align with what's already established. The DraftHeroHeadline MenuRadioItem has a tooltip that appears above the menu popup when hovering, which can overlap other items—this matches the previous behavior with title tooltips, so it's acceptable. Let me check the current state of the pull requests chat file. I'm concerned about wrapping MenuRadioItem in a span element within MenuRadioGroup — the item relies on grid classes and direct child positioning for proper width, and while a block-level span wrapper shouldn't break the layout, I need to verify that adding an extra element between the group and its menu items won't cause accessibility or styling issues. Base UI's composite navigation uses refs from context so nesting depth is fine, and the arrow-key navigation registers items by DOM order on mount, which should work correctly. The tooltip trigger wrapping the item is also harmless since no popup renders for available options, and the data-highlighted styling on the menu item itself won't be affected by hovering the wrapper span. Now I'm checking the remaining files I haven't verified yet: ServerUpdateAction, ProjectSettingsPanel, BrowserDeviceToolbar, ChangedFilesTree, ComposerPreviewAnnotationCards, FilePreviewPanel, PullRequestChecksPopover, PullRequestCodeTab, PullRequestSummaryTab, and a few others. Looking at potential issues: PullRequestActorLabel was changed from using a title attribute to a Tooltip, and the trigger span preserves the className, which is fine. For ProjectContentSearchDialog, the SearchOptionButton has a Toggle inside the TooltipTrigger, which is also acceptable. But there's a z-index concern — if the dialog has a z-index of 150 and the tooltip portal is at 140, tooltips inside the dialog would render behind the backdrop and be invisible. I need to check what z-index the dialog actually uses. I need to check the dialog.tsx file to see where the z-index value is coming from. The z-index layering looks good with the dialog below the tooltip. I'm checking the SearchOptionButton in ProjectContentSearchDialog—it's now wrapped but still has the aria-label on the Toggle component, so that's preserved. Now I need to scan through the remaining files to catch any accessibility issues, particularly where aria-labels or accessible names were lost when replacing title attributes, like in the DiffPanel's "Remote only" button and similar cases. Checking a few more components where tooltips wrap triggers — FilePreviewPanel breadcrumb, PullRequestChecksPopover, PullRequestCodeTab, and PullRequestSummaryTab all look fine with the span/paragraph render props preserved. One thing to note: BrowserDeviceToolbar has a disabled button inside the tooltip trigger, and disabled buttons don't fire pointer events in some browsers, so the tooltip won't actually display when disabled — though the old title attribute had similar limitations. ThreadPreviewMiniPlayer has a resize handle button with a tooltip, and since the tooltip trigger adds its own pointer event handlers, I need to make sure those don't interfere with the existing onPointerDown stopPropagation logic that prevents the mini player drag from triggering. For the Sidebar, the discard button is wrapped in a tooltip but has pointer-events-none until the row hovers, which should work fine since the tooltip will only become interactive when the row is hovered. The main question now is whether TooltipTrigger adds accessibility attributes like tabIndex or role when rendering non-button elements through the render prop — if it renders a <span> by default but still applies button semantics, that could affect keyboard navigation. the pattern is established in the codebase. Let me check the DiffPanel "Remote only" case and ComposerPreviewAnnotationCards test for any accessible-name issues, then look more carefully at a few remaining spots to see if there's a legitimate new finding. Looking at ConnectionsSettings with the pairing expiry tooltip wrapping formatted text inside a paragraph—I need to verify what else is in that container to understand the full context. Now I'm checking whether any remaining title attributes on intrinsic elements would trigger the new lint rule that's set to error across the repo, which could break the build if any slip through. The grep results show that most remaining title= props are component-specific, but I need to verify a couple of instances in PullRequestListFilters where title is applied to what might be intrinsic elements like buttons — if they are, the new lint rule would catch them. I also spotted title attributes in PullRequestDetailPanel (which is in the changed files), ConnectionsSettings, ProjectSettingsPanel, and ChatView that need checking to see if they're on intrinsic elements or component props. Let me look at those specific lines to determine which ones would actually trigger the lint error. The lint rule only flags intrinsic elements in lowercase, so these MenuRadioItem components pass without issue. But I'm noticing the PR handled a similar pattern inconsistently — it switched _chat.pull-requests.tsx to use a Tooltip wrapper, yet left PullRequestListFilters.tsx with a direct title attribute on the MenuRadioItem. These are essentially the same UI concept implemented differently across the codebase. The real issue is that MenuRadioItem forwards the title prop through to the DOM, creating a native tooltip that the PR's own rule is designed to eliminate — but the rule doesn't catch it because it only checks intrinsic elements. This is a coverage gap the PR directly made relevant by converting the sibling call site. I should check a couple other instances to see if this pattern appears elsewhere. I'm noticing that the Badge component with a title attribute is forwarding it to a DOM span, which keeps the native tooltip behavior intact. Meanwhile, QRCodeSvg renders its title inside the SVG element itself. The issue is that Badge's native title tooltip is still present in PullRequestDetailPanel.tsx—a file the PR modified—while the PR converted all the other titles in that same file. This creates an inconsistency where one title attribute was overlooked in the header row region that was otherwise updated. The lint rule doesn't catch this because it only checks lowercase intrinsic elements, so the component prop silently passes through. It's a legitimate finding worth flagging as low-severity: the migration is incomplete, and the rule has a blind spot for component props that forward title to the DOM. Leaving one native browser tooltip amid styled tooltips in the same toolbar creates a visible inconsistency in delay, styling, and positioning. Now I'm looking more carefully at the actual behavioral changes in the new code to see if there are any regressions—starting with the ThreadPreviewMiniPlayer resize handle and how TooltipTrigger renders the button. Checking the pointer events and drag behavior: the resize handle has cursor-nwse-resize and the Tooltip popup positioner is pointer-events-none, so there's no interference. During a drag, the tooltip may stay open and follow the anchor, whereas previously the native title would suppress during drag—minor issue. The handle's onPointerDown likely calls setPointerCapture and preventDefault(), which shouldn't conflict with Base UI's tooltip trigger since it doesn't typically attach its own onPointerDown. Looking at the DiffPanel "Remote only" case: the accessible name was previously provided by the title attribute on a generic span element, but I need to verify how that name is now exposed with the new Tooltip component. The fix is straightforward: remove aria-hidden and add aria-label="Remote only" to match the pattern used elsewhere in the codebase. But scanning the diff reveals a similar issue in ComposerPreviewAnnotationCards TargetStat—the title attribute that previously labeled the icon-count pair is gone, leaving screen reader users with just the number and no context about what it represents. Looking at how the Frame component is being used with lucide icons — they're rendered without aria-hidden attributes, and since lucide-react doesn't add aria attributes by default, the SVGs end up unlabeled and potentially exposed to screen readers when they shouldn't be. I'm deciding whether this is worth flagging as a violation. The previous runs found four issues that got fixed, so I should focus on genuinely new problems rather than weak ones. Let me dig into a few more spots to see if there are stronger accessibility issues — like checking the ProjectContentSearchDialog's SearchOptionButton and whether its Toggle component properly forwards props through the TooltipTrigger's render mechanism. Looking at the sidebar draft row, the button's pointer-events-none state on non-hover is fine. The PullRequestDetailPanel's copy button now combines the action label with the branch name in its tooltip, which makes sense. The PullRequestChecksPopover's tooltip inside the popover row is also fine — the z-index layering works out. I'm moving through the remaining components like ChangedFilesTree and FilePreviewPanel to see if anything else stands out. For the breadcrumbs in FilePreviewPanel, I need to check whether they're wrapped in clickable buttons, since that would affect how the tooltip nesting works. Let me also look for the "Remote only" aria-label pattern in PullRequestCodeTab to reference in my notes. Looking at the ComposerPreviewAnnotationCards.test.tsx file which is in scope, the test verifies that the tooltip trigger appears exactly twice in the markup. Then I'm checking how ServerUpdateProgress from ServerUpdateAction.tsx is being used, particularly in toast notifications. The issue is that tooltips inside toasts have a z-index conflict — the tooltip popup sits at z-140 while toasts are positioned at z-9999, which would render the tooltip behind the toast. This pattern already exists in the codebase since toast.tsx itself uses tooltips on line 125. I need to trace where ServerUpdateProgress is actually rendered to understand the full scope of this z-index problem. I'm thinking through the z-index layering issue with ServerUpdateProgress rendered in a toast description. The toast has a high calculated z-index, and a tooltip popup inside it has z-140, but since they're in separate stacking contexts—the toast in its portal and the tooltip in another portal appended to the body—the tooltip might not necessarily render behind the toast despite the lower z value. But wait, if the tooltip is positioned above the message row with side="top", it could overlap the toast card itself, getting hidden behind it. Though I notice toast.tsx already uses Tooltip inside toasts successfully (around line 125 on a button), so tooltips in toasts must be working somehow. Let me check if they're using a different side prop or some other approach to handle this. There's precedent for tooltips appearing inside toasts with different z-index values, though whether they actually render visibly is hard to verify. The real issue is that ServerUpdateProgress's message truncation tooltip is a new tooltip inside a toast, and if tooltips get hidden behind the toast card, the message becomes unreadable — that's a regression compared to the native title attribute, which always renders on top at the OS level. Both the toast viewport and tooltip positioner are portaled to document.body, so I need to check the actual z-index values to see if the tooltip would be obscured by the toast card. The toast viewport's fixed positioning with z-100 creates a stacking context that contains its children, so the toast cards' z-9999 values are scoped within it. The tooltip positioner is a separate portal with z-140, which sits above the viewport entirely, so tooltips like ServerUpdateProgress will render on top of toasts. Now I'm looking at the remaining issues, starting with the DiffPanel's "Remote only" label that's lost its accessible name. For the DiffPanel issue, the previous title attribute on a span exposed an accessible name to screen readers, but now there's nothing. Looking at the repo's own patterns in the same PR family, I can see they use aria-label on icons within tooltip triggers—like the TriangleAlertIcon example. That's a concrete, actionable fix. The TargetStat issue is similar: the count is visible but the icon lacks a label, so screen readers announce just "1" with no context. Before, the title gave "1 region". Both follow the same pattern and need the same solution—adding aria-label to the trigger span. I'll post both as separate comments since they're the same class of regression. Now I'm thinking through the implementation details. Adding aria-label to a generic span doesn't work reliably unless the element has a role. Looking at how the codebase handles this—ThreadStatusIndicators and PullRequestCodeTab both use role="img" with aria-label on the icon itself. For DiffPanel's CheckIcon, I should suggest role="img" aria-label="Remote only" on the icon to match that pattern. For TargetStat, applying role="img" aria-label={tooltipText} to the trigger span would replace the content announcement with the full label, which is exactly what we want—"1 region" instead of just "1". But I need to check whether Base UI's TooltipTrigger already handles this accessibility concern. I should verify whether PullRequestActorLabel inside a MenuItem in PullRequestReviewerPicker causes any tooltip interference issues, and whether the tooltip behavior change from title attributes is intentional. Let me also check ProjectSettingsPanel for similar patterns. Looking at Sidebar.tsx, the "Woke" buttons inside SidebarThreadRow are nested buttons with tooltip triggers, which mirrors the existing nested button structure, so that's fine. In ChangedFilesTree, the tooltip wrapping is straightforward. I need to examine ComposerPreviewAnnotationCards more closely to see if there are leftover title attributes on nearby elements and whether TargetStat call sites properly handle aria-hidden icons. I'm weighing whether the DiffPanel issue is significant enough to flag as a failure, and considering whether I should also mention the leftover title attribute. The TargetStat problem with unlabeled icons and visible counts is minor—losing the title removes context for the number, but I could include it as a secondary comment if needed. Since comments have to be inline per file, I'll probably post the DiffPanel finding as the main one since it's the strongest case. Looking at the Badge component in PullRequestDetailPanel—it's a pre-existing native tooltip in the same file and toolbar row that the PR is converting away from. The Badge spreads props onto a span, so the title does forward to the DOM. Since the PR makes native titles a lint error and this one is in the same header being migrated, it's fair to flag as a low-severity note—the patch makes this pre-existing behavior directly relevant. I'll mention it briefly as a suggestion to avoid seeming like scope creep, but first I need to confirm Badge actually forwards the title attribute. I found a concrete example: the Badge component with a title prop in PullRequestDetailPanel.tsx renders a native tooltip on a span element, which the new lint rule doesn't catch because the component name is capitalized. Now I'm checking the other changed files to see if there are more component-forwarded title props I should account for in my comment. I'm identifying the components that lost their native tooltips in the changed files—specifically the Badge in PullRequestDetailPanel and MenuRadioItem in PullRequestListFilters. I'm planning to post inline comments flagging the accessibility issues, starting with the "Remote only" indicator in DiffPanel that lost its accessible name. For the Badge component, I need to note the leftover native title attribute that's still being forwarded to the DOM, which is inconsistent with the migration pattern. I'm debating whether to mention PullRequestListFilters too since that file wasn't changed, but I'll keep the comment focused on the changed file and just note that component prop forwarding is a blind spot for the lint rule. On TargetStat's aria-label, I'm leaning toward skipping it to avoid noise—the count is still visible as text, whereas DiffPanel shows nothing but an aria-hidden icon, making that a clearer case to flag. For the Tooltip regression, I need to verify: when the popup is closed, there's no accessible description for screen reader users who don't hover. The old title attribute was exposed as an accessible description, so information is genuinely lost here. The regression stands. Now I need to figure out the correct line numbers for the review comments—they have to be on the RIGHT side of the diff and within the actual changed hunks. DiffPanel's Tooltip block spans lines 688-697, so I can comment on line 692 where the aria-hidden icon is. But PullRequestDetailPanel line 1414 is unchanged context far from any hunks, so GitHub won't allow a comment there. I'll mention line 1414 in the review body instead of trying to attach it to the wrong diff line. As for the check result, I have findings—the accessible-name loss is a regression, even if minor. I need to decide between failure and neutral state. The instructions are clear: the check defaults to failure, and "All clear" only applies when there are no findings. Since I have findings, I should go with failure. Let me verify the diff line numbers to make sure I can attach an inline comment to a valid location. The hunk around line 688-697 contains the CheckIcon element, so I can comment on that line and suggest removing aria-hidden while adding a proper label. Writing the suggestion... Formatting the suggestion as multi-line JSX to stay within prettier's line-length limit, then I'll add a brief note about the Badge issue to the review body and consider whether to mention the PullRequestListFilters file that has similar patterns. Let me do a final check that this "Remote only" accessible name finding hasn't already been flagged in prior comments, then post the review.

1. modify_pr — ## UI Consistency The four issues flagged on the previous head are addressed (DiffPanel trigger now scopes to the head-ref text, the PR header tooltip no longer swallows PullRequestBaseFreshnessWarning, the pairing-expiry trigger no longer wraps AccessScopeSummary, and the compact filter menu anchors on a hoverable wrapper instead of the disabled MenuRadioItem). One new finding, inline: the "Remote only" indicator in DiffPanel.tsx loses its accessible name in the migration (the title was the only text, and the icon is aria-hidden). Also worth noting (no inline comment possible — the lines are outside the diff): the new t3code/no-native-title-tooltip rule only matches lowercase intrinsic elements, so title props that shared primitives forward straight to the DOM still render native tooltips. Two remain next to the surfaces this PR migrated: PullRequestDetailPanel.tsx:1414 (<Badge title="The host will merge this on its own…">, which useRender spreads onto a span, in the same header row that was converted) and PullRequestListFilters.tsx:166,383 (<MenuRadioItem title={option.unavailable}> — the twin of the CompactFilterMenu case converted in _chat.pull-requests.tsx). Converting those keeps hover behavior uniform; otherwise the rule silently leaves the pattern it was added to eliminate.


Agent Credits: 276 credits