Skip to content

feat(web): enable full-text message content search in sidebar

MacroscopeApp / Macroscope - UI Consistency failed Aug 15, 2026 in 6m 21s

UI Consistency: 5 issues found

Five UI consistency/regression findings were posted as inline review comments.

  • apps/web/src/components/status/StatusPage.tsx (line 287): the non-Electron header uses an undefined workspace-topbar class, so the topbar loses its height, shrink-0, and vertical centering that every other page applies via explicit h-[var(--workspace-topbar-height)] min-h-[...] flex items-center utilities.
  • apps/web/src/components/ChatView.tsx (line 6752): ThreadHandoffDialog is rendered without providerInstanceEntries, so the dialog's From/To ProviderInstanceIcon glyphs can never render in the app (only the unit test supplies entries).
  • apps/web/src/components/Sidebar.tsx (line 1601, plus SidebarThreadTooltip): provider instance badge and accent color were removed from thread rows and the tooltip, so multiple instances of one driver are indistinguishable in the sidebar while the model picker still badges them; opacity-60 moved to the wrapper also dims the badge, which a removed comment explicitly guarded.
  • apps/web/src/components/Sidebar.tsx (lines 853, 1961): change-request (PR) state moved from the shared threadChangeRequestSnapshotsAtom + resolveDisplayedThreadPr to local useState + resolveThreadPr, losing state across remounts and diverging from ChatView, which still uses the snapshot path.
  • apps/web/src/components/Sidebar.tsx (line 1713): new search-snippet role labels use dark-only palette shades (text-blue-400/text-emerald-400), unlike the light/dark pairs used elsewhere in the same file, producing low contrast in light theme.

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 and files examined (in-scope patterns apps/web/src/**/*.ts|tsx|css):

  • Verified workspace-topbar is not defined as a class or @utility anywhere (only --workspace-topbar-height in index.css) and that it appears solely in the new StatusPage.tsx at the PR head, not at the merge base.
  • Compared new controls against apps/web/src/components/ui/*: the handoff dialog, status cards, and tab strip reuse Dialog/Textarea/Spinner/Button/Card/Empty/ScrollArea; no Progress primitive exists, so the hand-rolled rate-limit bar is acceptable; the tab close action uses Button size="icon-micro" variant="ghost-muted".
  • Checked repo conventions before flagging: ModelListRow's pre-existing raw "New" pill (so the new "Handoff" pill is consistent), DesktopUpdateStatusIcon's raw overlay dot (so the SidebarTrigger unread counter span is consistent), widespread raw <label> usage (so the handoff dialog's label is consistent), and RightPanelTabs' ScrollArea hideScrollbars tab strip (so WorkspaceTabs follows the existing viewport contract, including the [data-slot="scroll-area-viewport"] wheel target).
  • Confirmed ModelPickerContent's new allowHandoff default only relaxes locked-provider behavior for the composer picker; all other consumers pass lockedProvider={null}.
  • Confirmed the sidebar DnD PointerSensor keeps a 6px activation distance, so row clicks are preserved despite every row becoming draggable.
  • Cross-checked merge base (20a7042) vs head (725cf20) for the sidebar instance-badge and PR-snapshot code paths to confirm both are removals introduced by this PR rather than pre-existing gaps.

🔍 Agent activity — 1 tool call

💭 Looking at the UI consistency issues in the WorkspaceTabs component, I'm seeing that it uses raw button elements for the tab trigger and "New thread" tab instead of the standard Button component, though the close button does follow the pattern with the ghost-muted variant. The tab structure itself might be exempt from this since tabs have different geometry requirements than standard buttons. Moving through the other files, StatusPage has a custom progressbar div that could be a violation if a Progress primitive exists in the codebase. The ThreadHandoffDialog looks consistent with its use of Dialog, Button, Textarea, and Spinner components. In sidebar.tsx, there's a raw span badge for unread notifications that might need a Badge primitive if one exists. ModelListRow has a "Handoff" span with inline styling that appears to recreate a Badge component, which would be a finding if the primitive is available elsewhere in the component library. I'm also noticing a theming issue in sidebar.tsx where hardcoded colors like text-blue-400 and text-emerald-400 are used without light mode variants, creating a contrast problem. The existing sidebar code uses text-blue-600 dark:text-blue-400, so this inconsistency is a legitimate concern. Additionally, the diff removes the provider instance badge display and accent color styling from the sidebar, which represents a visual regression in how multi-instance providers are presented. Looking at the broader pattern, this PR appears to be based on an older revision of the codebase — it's reverting recent improvements like resolveDisplayedThreadPr and removing threadChangeRequestSnapshotsAtom. The comments explaining constraints around badge sizing and glyph behavior are also stripped out. Since the diff is against the merge base, I should verify whether these features actually exist on main to confirm this is a stale branch issue that's reverting current functionality. The Handoff badge in ModelListRow follows the same pattern as the existing New badge—a raw span element, which is consistent with the codebase convention, so that's fine. Now I need to check the badge primitive in ui/badge.tsx and look at how the sidebar handles its unread badge, which appears to be a positioned notification dot. I also noticed that StatusPage doesn't have a Progress primitive, so the hand-rolled progress bar is acceptable. Let me examine badge.tsx to see what's available. Looking at the unread badge styling in SidebarTrigger, I'm noticing the current implementation uses a raw span with destructive badge semantics, but the sizing (h-5 min-w-5) doesn't align well with the Badge primitive's small variant. I'm checking if ProviderInstanceIcon has an existing badge pattern I can reference, since it has badge-related props like showBadge and badgeContent that might already solve this overlay counter problem. I'm also scanning the codebase for similar overlay counter patterns — things like SidebarUpdatePill or existing dot indicators with absolute positioning and rounded-full styling. On the accessibility side, the aria-label handling looks correct since it's destructured before spreading props, though there's a redundant sr-only span alongside the aria-label that follows a pre-existing pattern. But the bigger issue is in Sidebar.tsx where the changes swap out providerEntry and showInstanceBadge for driverKind in the tooltip, which removes the accent-colored instance badge and instance name display, and the thread resolution logic shifts from resolveDisplayedThreadPr to resolveThreadPr with local useState state instead of what was there before. Now I need to verify whether resolveThreadPr actually exists at the merge base and whether the PR is deleting usage of exported helpers that end up unused — if the function was renamed or removed, the import would fail at typecheck. The overlay badge pattern matches what's used elsewhere in the codebase, so that's consistent. Looking at the reverts now—Sidebar.tsx still imports and uses resolveThreadPr at the current HEAD, and hasUnseenCompletion is also imported, so the PR's version should compile fine. But I need to check what happened with resolveDisplayedThreadPr and nextThread— those were introduced on main and the PR reverts Sidebar back to the older resolveThreadPr plus local useState instead of the snapshot-based approach with the shared atom. This is actually a behavioral regression. The snapshot system was designed to keep the PR badge stable when branches mismatch and persist state across sidebar remounts via the atom. Reverting to local useState means the PR status indicator will flicker or disappear, and state won't persist when the sidebar remounts. That's a concrete regression in an in-scope file. I'd flag that. The provider instance badge removal in SidebarThreadTooltip and rows also strips out the accent color and instance badge—a visual regression for multi-instance setups since shouldShowInstanceBadge is still being used elsewhere in ProviderModelPicker and ModelPickerSidebar. Let me verify the diff actually shows these as removals (checking for "-" lines on providerEntry and showInstanceBadge). Yes, confirmed. Now checking the other UI changes: visibleSettledThreads → settledThreads in the routeThread lookup is just logic, so I'll skip that. Looking at WorkspaceTabs.tsx, I need to verify that ScrollArea actually supports the hideScrollbars and scrollFade props, and that the Button variants size="icon-micro" and variant="ghost-muted" exist. Also checking whether the onWheel handler on the inner div properly targets the ScrollArea viewport with that data-slot selector. For ChatHeader, when tabs are enabled the breadcrumb gets replaced by WorkspaceTabs, but there's a potential hydration issue where useTabsEnabled defaults to true before the client hydrates, which could cause a flash. I should compare this against the useLegacySidebarEnabled pattern to see how they handle the same problem. The ScrollArea component with hideScrollbars removes the scrollbar visually, and in WorkspaceTabs it combines with scrollFade to use Base UI's overflow masks for a fade effect at the edges. The wheel handler targets the viewport element using a data attribute selector to properly detect scroll interactions. Looking at StatusPage.tsx, there's an unused statusProviders dependency in a useCallback that the linter might flag, though it's not a UI issue. The progress bar uses the bg-primary class and has proper accessibility attributes with role="progressbar" and aria-label. In ThreadHandoffDialog.tsx, the Spinner inside a Button has an mr-2 class that adds extra spacing on top of the button's built-in gap, which is a minor aesthetic choice. More notably, the component uses raw <label htmlFor> elements with inline classes instead of the Label primitive from the UI library. There's also a concrete bug I'm spotting: ChatView.tsx renders ThreadHandoffDialog without passing the providerInstanceEntries prop, so sourceEntry and targetEntry are always null. This means the provider instance icons that should display in the dialog never actually render in the real app — they only show up in tests because the test harness passes the entries directly. This is a real visual gap worth flagging across both files. Now I need to check how the Label primitive is used and whether Textarea pairs with it properly in other dialogs to understand the labeling pattern. I'm noticing the Label primitive doesn't get much use in the codebase—just a couple of instances, mostly internal. Raw <label> elements with muted small text seem to be the more common pattern here, so I should search through the web app source to confirm what's actually being used. The <label> pattern is standard across the codebase, so that's not a real issue. Now I'm looking more closely at Sidebar.tsx to identify actual UI and consistency problems, particularly around the drag interactions and search result rows to verify the color scheme is working correctly. I should also check how the droppable zones are styled—they're using the right design tokens like bg-sidebar-row-hover, ring-foreground/15, and scale-[1.01]—but I need to verify the content alignment matches expectations. I'm noticing the snippet colors use text-blue-400 and text-emerald-400, while the existing sidebar code applies text-blue-600 dark:text-blue-400. This creates a contrast problem in light mode where blue-400 and emerald-400 don't have enough contrast against the light sidebar background. This is a theming consistency issue—raw palette colors being used without proper light mode variants. Let me search the repo to see if text-emerald-400 appears elsewhere and whether raw palette colors are being applied inconsistently without dark mode support. I'm noticing there's a color inconsistency across the components—the command palette uses text-blue-400 and text-emerald-400 on dark surfaces, and the sidebar is copying that pattern, but the sidebar's own convention is text-blue-600 dark:text-blue-400, which creates a mismatch in light mode. Beyond that, there are two more significant issues: the sidebar provider instance badge has been removed entirely, which breaks the ability to distinguish between multiple instances of the same provider (something the model picker still does), and the opacity styling has shifted from dimming just the icon to dimming the entire wrapper, so the badge no longer stays saturated while the glyph fades. The PR status handling has regressed too—it switched from using a snapshot-based atom to local state, which causes badges to flicker when the git branch doesn't match, and since ChatView still relies on the snapshot approach, the sidebar and chat header can now show different PR states for the same thread. For ChatHeader, when tabs are enabled, the breadcrumb and project favicon path get replaced, but there's a potential drag-region handling issue: the tabs themselves have the no-drag class set, but the scroll wrapper might not be properly configured for the workspace topbar's drag region. There's also an accessibility gap—WorkspaceTabs uses plain divs and buttons without proper tab semantics like role="tablist", role="tab", or aria-selected, relying instead on a data-active-tab attribute, which violates the guideline about preserving accessibility and interaction semantics. Actually, thinking about it more—these are navigation tabs that control routes, not in-page tab panels, so WAI-ARIA's role="tablist" requirement doesn't strictly apply here; they're more like a list of links. I'll skip flagging that. There's also a concrete issue with ThreadHandoffDialog: the providerInstanceEntries prop is never passed from ChatView, so the provider icons in the dialog never actually render—the component was designed with them but the real app doesn't provide the data. Looking at the Spinner inside Button, there's potential double spacing since the Button already has gap-2 and the Spinner adds mr-2. And in StatusPage, the Card components are overriding the before: pseudo-element's border radius at the call site to match contextual layout changes—it's repeated a couple times across the Codex and Claude cards. I should also check how useTabsEnabled handles the hydration state compared to useLegacySidebarEnabled, and verify the unread count logic in AppSidebarLayout where it's appending the count to what looks like screen-reader text. I'm noticing that the before:rounded-[calc(var(--radius-xl)-1px)] styling on Card in StatusPage is only applied in the new version, appearing twice, while the Card primitives keep this internal. I should check how UsagePage handles Card styling to see if there's a consistent pattern across pages, so I'm going to search for Card className usage in the web app. I'm checking how UsagePage.tsx handles card styling to see if StatusPage's repeated utility overrides are a pattern worth consolidating—if both pages use the same Card component with identical styling, a shared variant or local constant would be cleaner than duplicating the same five utilities twice. Looking at how UsagePage structures its panels without Card components, and checking whether StatusPage is duplicating a shared page shell pattern that other pages like UsagePage and PullRequestsPage already have for their headers. I'm noticing an inconsistency in how the StatusPage header is styled compared to other pages—it uses a workspace-topbar class while Usage, Settings, and ProjectSettings all apply explicit flexbox and height utilities. ChatView seems to mix both approaches, so I need to search for where that class is actually defined to understand what's going on. I'm noticing that UsagePage.tsx references the workspace-topbar class in two different places—once explicitly at line 171 and again at line 514. This suggests there might be multiple header components or states within UsagePage, possibly including a loading or empty state. But when I search for where workspace-topbar is actually defined as a CSS class, it doesn't appear in index.css, which is puzzling. I'm noticing a discrepancy between the grep results and what's actually in the file at those line numbers — the grep output shows class names at line 514, but viewing that same line shows a closing brace instead, which is strange since both searches were run against the same commit. I've confirmed that .workspace-topbar is only used in the new StatusPage but isn't actually defined anywhere in the CSS—there's no utility class or variable for it beyond the --workspace-topbar-height variable. This means the header on the web version collapses to just text height without any flex layout or centering, since the class that should control its styling doesn't exist. I've confirmed that workspace-topbar is undefined—that's a solid finding. Now I need to check the Sidebar's provider-icon opacity change and the instance badge removal, verify the PR-status revert, and see if removing providerEntry from the tooltip leaves any unused variables that would trigger linting issues. I should also check whether providerEntry is still being used in SidebarThreadRow for the models or accent styling, so I'll look at those specific line ranges. I've found a regression where the instance badge and accent color are missing from sidebar rows and tooltips for the same provider — the tooltip provider line and row glyph both dropped these properties, making multiple instances indistinguishable in the sidebar even though the model picker still badges them correctly. The deleted comments revealed this was intentional, explaining the badge geometry constraints and the offset matching logic, but it looks like this exception was unintentionally removed. Now I need to check if the providerEntry variable is still being used in SidebarThreadRow around line 955, since it might be an unused variable causing a lint error. I see that providerEntry is still being used, so I need to check the ChatView handoff dialog to see if it's missing the providerInstanceEntries reference. So the issue is that providerInstanceEntries isn't being passed to the dialog, which is why the provider icons don't show up. ChatView likely has access to this data through a derived variable, probably something like deriveProviderInstanceEntries, so I need to find where that's computed and use it to pass the correct prop. ChatView doesn't pass instance entries to ProviderModelPicker, so I need to either derive them from providerStatuses using the helper function or remove the icon rendering logic entirely. This is a consistency issue—the provider glyphs in this dialog never actually display, unlike everywhere else in the UI that shows provider icons. Now checking if providerStatuses is available in ChatView—yes, it's used in functions like getModelDisabledReason. Looking at ModelPickerContent, the isLocked logic depends on allowHandoff, which defaults to true when undefined. Since ProviderModelPicker passes this through as optional, the locked-provider restrictions that previously prevented cross-provider selection are now globally disabled unless a caller explicitly sets allowHandoff={false}. I need to grep for callers that actually pass this prop, because this could be a behavior regression—surfaces without handoff support like PullRequestThreadDialog or drafts might now allow unrestricted provider switching when they shouldn't. ChatComposer is the only consumer that passes a non-null lockedProvider and doesn't set allowHandoff, so it defaults to true and always enables the handoff path—which aligns with the intended behavior. Other consumers pass lockedProvider={null}, so the default is appropriate. There's a consistency issue in ModelPickerContent though: it computes isCrossProvider based on whether the model matches the locked provider, but when allowHandoff is false, those cross-provider rows are still being calculated even though they shouldn't be displayed. Actually, when allowHandoff is false, cross-provider models get filtered out entirely, so the badge wouldn't show anyway. Looking at ChatHeader and WorkspaceTabs, there's an unused isWorking prop declared but never referenced—a minor lint issue. The draftId prop is passed through correctly for determining draft status. I'm noticing some key accessibility and behavior patterns in WorkspaceTabs that might be worth flagging, particularly around how the active tab click triggers a menu. The rename input commits on blur, and ChatHeader passes onCancelRename separately, but WorkspaceTabs declares it in the props interface without actually destructuring or using it—so Escape-to-cancel rename is handled through onRenameKeyDown from ChatHeader instead. That works fine, but onCancelRename ends up being dead code. The bigger issue is that tab renaming is now squeezed into a max-width tab (around 12rem), which is cramped but workable. There's also a real accessibility gap with the ScrollArea's hidden scrollbars and horizontal keyboard navigation, but I'm skipping that for now. In the Sidebar, renderThreadRow is creating props on every render instead of memoizing them, which is a performance concern. More critically, DraggableThreadRow spreads listeners onto every <li> element and applies grab cursor styling, which is a behavioral regression I need to address. The drag listeners are being attached to all thread rows without a proper drag handle, and I'm realizing that useSortable is being called outside of a SortableContext — only the pinned rows are wrapped in one. In dnd-kit v6, useSortable relies on context from SortableContext, and when it's missing, it falls back to a default context and logs a warning, so dragging might still technically work but the sorting data won't be available. The PointerSensor is configured with an 8-pixel distance constraint, so clicking still works fine. The drag modifiers have shifted from restrictToVerticalAxis and restrictToFirstScrollableAncestor to snapCenterToCursor on the DragOverlay, which is reasonable. But there's an accessibility issue: the <li> element spreads keyboard listeners from the sensor but isn't focusable and lacks tabIndex or a proper role, breaking keyboard navigation for drag operations. Looking at the previous implementation, SortablePinnedThreadRow also didn't apply dnd-kit attributes, so keyboard dragging wasn't supported before either—this isn't a new regression. Now I need to check the sensor configuration to understand the click-versus-drag risk across all rows. I'm setting the cursor to grab for all thread rows as a UX improvement since they're primarily meant to be clicked to open, and this aligns with the drag-to-shelves feature already in the sidebar. The DroppableThreadZone and DroppableShelfHeader components are rendering list items with selection attributes, and the snoozed shelf header now appears whenever there's an active drag, even with zero snoozed items. There's a minor UX regression where legacy server pins are now draggable through SortablePinnedThreadRow, but dropping them silently does nothing instead of clearly communicating they can't be reordered — it degrades gracefully enough that I'm not flagging it. Let me verify the unread indicator in AppSidebarLayout is properly hidden with the sr-only span. Looking at how SidebarChromeHeader passes unreadCount to SidebarTrigger on line 62, then checking the button styling in sidebar.tsx where the badge span sits inside a Button with size-related utility classes and pointer-coarse pseudo-element handling. Now I'm consolidating the key issues to report: the workspace-topbar class missing from StatusPage.tsx causes the web header to lose its layout properties, ThreadHandoffDialog in ChatView needs the provider instance entries passed so the From/To icons actually render, there's a regression in the sidebar instance badge and accent color styling that needs reverting, and there's a divergence between how the PR snapshot state is handled in the sidebar versus ChatView where one uses local component state while the other still relies on snapshot resolution. I also need to flag the color contrast issue on line 1713 where the "You:"/"Agent:" labels use dark-only palette colors that don't work in light theme — they should use conditional classes like the rest of the file does. For posting inline comments, I need to verify which lines are actually in the diff hunks so the line numbers reference the PR head file correctly. I'm debating whether to use a single comment at line 1601 to cover both the opacity change at 1597 and the icon size adjustment, or split them across multiple comments. I think one comment makes sense since they're adjacent changes. I also need to account for the tooltip modifications in the SidebarThreadTooltip component around line 333, where I'm adding the provider name display, and fold that context into the same comment rather than creating separate inline notes. There are also some other added lines I'm tracking—line 853 in Sidebar.tsx with the resolveThreadPr call, and the useState hook for change request state around lines 1959-1978. I'm preparing to post a code review with inline comments across several files, noting specific line numbers and issues like missing providerInstanceEntries in ChatView and useState-related concerns in Sidebar. For the first issue in StatusPage, I'm updating the class string to add flexbox layout properties and height constraints using CSS variables. For the Sidebar issue at line 1713, I'm refining the className conditional to include proper dark mode color variants, splitting it across multiple lines for readability with 18-space indentation. For issue #3 at line 1601, restoring the full functionality would require multiple changes across the component—bringing back the accentColor, showBadge, and badgeClassName props along with the showInstanceBadge logic—so I'm describing the needed fix rather than providing a diff. Issues #2 and #4 also need multi-hunk changes that aren't suitable for inline diffs. The overall status is failure with five UI consistency issues identified: the topbar styling, color contrast in the sidebar, icon badge configuration, missing entry derivation, and incomplete badge display logic.

1. modify_pr — Reviewed the changed web UI surfaces (apps/web/src/**) for shared-primitive usage, Tailwind/CSS ownership, theming, and visual/behavioral preservation. Five concrete findings, all on changed lines: 1. apps/web/src/components/status/StatusPage.tsx — the browser (non-Electron) header uses an undefined workspace-topbar class, so the topbar has no height, flex, or vertical centering. 2. apps/web/src/components/ChatView.tsx — ThreadHandoffDialog is rendered without providerInstanceEntries, so the dialog's From/To provider glyphs can never render in the app. 3. apps/web/src/components/Sidebar.tsx — provider instance badge/accent color was dropped from thread rows and the details tooltip, so multiple instances of the same provider are no longer distinguishable in the sidebar (the model picker still badges them). 4. apps/web/src/components/Sidebar.tsx — change-request (PR) state moved back to local component state + resolveThreadPr, diverging from ChatView, which still uses the shared snapshot atom + resolveDisplayedThreadPr. 5. apps/web/src/components/Sidebar.tsx — new search-snippet role labels use dark-only palette shades, unlike the rest of this file which pairs light/dark values. Everything else checked out: new controls reuse Button/Card/Dialog/Textarea/ScrollArea/Empty, the new pill and overlay-count markup follow existing local patterns (ModelListRow's "New" badge, DesktopUpdateStatusIcon's dot), and the tab strip reuses the ScrollArea hideScrollbars/scrollFade contract already used by RightPanelTabs.


Agent Credits: 494 credits