Skip to content

fix(web): keep composer banners opaque

MacroscopeApp / Macroscope - UI Consistency failed Aug 23, 2026 in 4m 13s

UI Consistency: 1 issue found

apps/web/src/index.css (lines 919-923) — the new .chat-composer-banner-surface::before { --glass-opacity: 100%; } rule hardcodes the shared glass token for a single surface. This opts the front composer banner out of the user's Glass opacity setting (GlassAppearanceSync writes --glass-opacity on <html>) and makes it paint opaque next to surfaces intended to read as one continuous attached panel — .chat-composer-banner-stack-cap, ThreadSyncStatusPill (which drops its top border/radius to join the banner flush), and the composer main surface — all of which still use var(--glass-opacity). At any setting below 100% this produces a visible hard band at the seam.

Removing before:mask-none (restoring the drawer mask) already fixes the doubled translucent overlap on its own, so the smallest fix is to drop this rule and the chat-composer-banner-surface class from ComposerBannerStack.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.

Analysis performed:

  • Traced .chat-composer-drawer-surface::before (index.css:887-917) to confirm the mask restoration is coherent: each attached drawer's fill stops at --chat-composer-attachment-overlap, and the element below (sync pill, or the composer main surface) paints in that band.
  • Verified externalComposerDrawerAttached in ChatView.tsx:6416 is true whenever composerBannerItems.length > 0, so the composer splits into exact surfaces (index.css:1055-1111) and paints its own glass over the banner's masked overlap. The mask restoration therefore does not reintroduce the gap that #8000 (5427ca05) addressed with before:mask-none.
  • Confirmed the custom-property override on a pseudo-element resolves correctly for that pseudo-element's own var(--glass-opacity) reads in both the base rule and the nested @variant dark &::before rule, and that no cascade/layer conflict exists (banner rule follows the base rule in the same block, equal specificity, no competing declaration).
  • Checked adjacent glass consumers: .chat-composer-banner-stack-cap (index.css:789-817), ThreadSyncStatusPill.tsx, ComposerCommandMenu.tsx, ComposerStashMenu.tsx, the alert-glass stacked banners, and the html[data-theme-id] token bridges (index.css:1637-1674) — all continue to use var(--glass-opacity).
  • The removed before:mask-none test assertion was not treated as a finding: this is a small visual-only class migration, and the check rules do not require tests for that.

🔍 Agent activity — 15 tool calls

💭 I'm examining the PR's swap of before:mask-none for a new chat-composer-banner-surface class that sets --glass-opacity: 100% on its ::before, and I want to check the drawer surface styles in index.css to confirm these two approaches actually produce equivalent visual behavior.

1. git_tools — chat-composer-drawer-surface|chat-composer-banner-surface|glass-opacity|mask-none

2. browse_code — apps/web/src/components/chat/ComposerBannerStack.tsx

💭 I'm checking index.css around lines 760-960 to see how the drawer surface's ::before mask is set up.

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

💭 I'm checking whether setting --glass-opacity: 100% directly on the ::before pseudo-element affects its own property values through variable substitution, since custom properties apply to the element they're declared on. I trace the cascade order between the banner rule and the drawer rule to confirm no conflict since only one declares --glass-opacity, and the dark-variant nested rule resolves the same way since it references the same variable on its own pseudo-element. Actually, I realize the real fix concern: previously removing the mask let the fill extend into the overlap zone, stacking with the composer surface beneath and creating the doubled-opacity band users reported. Restoring the mask fixes the overlap, but forcing --glass-opacity: 100% on the banner makes it fully opaque regardless of the user's configured glass opacity setting, which could override their preference inconsistently. Now I'm noticing that this creates a visual mismatch: the opaque banner will no longer have the glass look while its backdrop blur becomes pointless, and the collapsed stack cap plus other stacked banners remain translucent — so an attached front banner would look solid next to translucent peeking elements above it. I check the test diff too — they dropped the old class assertion without adding one for the new class, which seems like a minor coverage gap rather than a real bug, so I'll set it aside. The bigger question is whether declaring a custom property on ::before actually works as intended here, though custom properties are generally inherited fine in this context. Then I want to confirm --glass-opacity isn't registered via @property, since that would change inheritance behavior — a quick check confirms it's just a plain custom property defined in :root, so nothing unusual there. Finally, I'm considering whether other components sharing this styling, like sync status pills or command menus, might be affected by the new class scoping, but it looks properly contained to the banner stack. I'm weighing whether a single-use global class is actually the smallest durable contract versus just a component-specific workaround dressed up as "reusable." I should also double check why the original banner's ::before had mask disabled — likely something to do with how it sits at the top of the layout, so removing that could have unintended effects. I'm tracing through the flex-col-reverse layout logic to confirm stacked banners render above the front banner, with the composer beneath overlapping it by 1rem via negative margin—so that seam is covered fine. Then I check the CSS rule targeting .chat-composer-drawer-slot::before—since the slot div itself doesn't carry the drawer-surface class, this selector likely has no effect and is probably dead code unrelated to what I'm reviewing. The real issue I want to flag is that setting --glass-opacity: 100% overrides the user's glass opacity just for this banner, breaking consistency with adjacent glass surfaces and making the backdrop blur pointless—though I need to verify this is actually the cause of timeline content bleeding through given how the mask and padding interact. I'm also checking the specificity ordering between the dark-mode nested rule and the new banner rule to make sure the intended override actually wins, then need to look at what happens under the @supports not (backdrop-filter) fallback. The bigger question is whether setting --glass-opacity: 100% on the pseudo-element truly guarantees full opacity when combined with color-mix, since that depends on whether --card itself is already translucent in some themes -- so I should check how --card is defined across the theme variants in index.css, and also account for how the --chat-composer-attached-tint gradient layer interacts on top of it. I don't see anything else broken with the dark mode overlay gradients or the class ordering, since CSS doesn't care about attribute order. Removing the mask-none assertion in the test doesn't seem to need extra test coverage for what's really just a visual class migration, but I want to look at ComposerBannerStack.test.tsx and check the git history for when before:mask-none was introduced to be sure.

4. git_tools

5. browse_code — apps/web/src/components/chat/ComposerBannerStack.test.tsx

💭 I'm looking into why before:mask-none was originally added in that commit—likely to keep the banner visually flush against the composer without a seam—and considering whether removing it might reintroduce that visual gap if the composer actually overlaps the banner's transparent edge.

6. git_tools — 5427ca0

💭 I'm trying to figure out whether restoring the mask reintroduces the bug that PR #8000 fixed—specifically whether the banner's translucent bottom edge would show timeline content through it if the composer below doesn't fully overlap that region. I'm checking whether the composer's layout actually covers the banner area given the negative margin overlap in the drawer slot. Rather than guessing further, I should just find where ComposerBannerStack is actually rendered relative to the composer to see whether the composer's glass surface fills that overlap zone.

7. git_tools — ComposerBannerStack|chat-composer-glass-shell|chat-composer-top-drawer

💭 I want to check ChatView's layout around lines 6740-6790, plus index.css around 1040-1130 to see how the glass-shell composer handles overlap with the banner above it.

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

9. browse_code — apps/web/src/components/ChatView.tsx

💭 I'm reasoning through the banner-composer overlap: the banner stack overlaps the composer via negative margin, and when a drawer is attached, the composer needs to paint its own glass surface in front of that overlap rather than sharing the shell's clipped backdrop. That confirms the mask on the banner's ::before is correct, since it avoids double glass rendering in the shared overlap region. Now I'm checking whether externalComposerDrawerAttached is actually true whenever banners are present, and if not, whether the shell's inset-0 ::before still covers the overlap correctly either way. I'm also probing for a possible regression around the --glass-opacity: 100% setting on the banner and whether it conflicts with the user's global glass opacity preference. Actually, comparing to the pre-fix behavior, the banner was 80% translucent everywhere except a slightly darker overlap band with the composer — so hardcoding full opacity fixes that band but makes the whole banner opaque, breaking visual consistency with its translucent siblings like the ThreadSyncStatusPill and stack cap. The more surgical fix would just be removing the mask override rather than forcing opacity to 100%. The comment justifies this as needed so banner text stays legible over timeline content, but other drawer surfaces with overlaid text stay at the shared glass-opacity variable, so this still looks like an inconsistency worth flagging. I want to check the custom theme definitions to see whether translucent surface colors there would actually still show timeline content through even at 100% glass-opacity, since color-mix on a translucent color stays translucent regardless of the opacity variable.

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

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

💭 I notice the theme override sets the surface variable on the composer drawer element, which gets inherited by the ::before pseudo-element—that part seems fine. Now I'm checking whether the banner surface class is properly covered by this rule too. Both rules appear to live in the same layer block, so ordering shouldn't cause a specificity conflict. I'm also confirming that the glass-opacity variable is properly visible for the ::before element's variable substitution since it's declared on the same element, and I don't see an issue there. The remaining thing to check is whether there's a real visual regression in dark mode around the seam gradient on that pseudo-element's background. The dark mode seam uses a gradient overlay that fades in and out around the boundary, sitting above an otherwise fully opaque fill — that seems fine. The backdrop-filter blur is still being applied on what's now an opaque surface, which is wasted work but harmless, aside from a possible extra compositing cost that's already baked in. The more interesting interaction is that in the overlap region, the banner's pseudo-element is masked to transparent, letting the timeline content show through the composer's translucent surface as expected, and the banner's padding accounts for that overlap so its text sits above it properly. Now I'm considering the ThreadSyncStatusPill, which also overlaps the banner by using a negative margin and a translucent background, and thinking through how that stacking interacts with everything else. The functional fix seems fine, but hardcoding full opacity does break the glass consistency and overrides the user's setting, which feels like the real concern here. I'm rechecking whether there's a more concrete, objective violation to flag rather than a stylistic judgment call — considering things like whether the surface class only applies to one banner variant, whether the pseudo-element rules properly combine without a content property of their own, and whether there's a specificity conflict with theme-scoped selectors that might override these opacity rules. Other drawer surfaces like ThreadSyncStatusPill and the command menu all keep the standard 80% opacity, so bumping the banner to 100% makes it inconsistent. Plus the stack-cap peeking above the front banner still uses the 80% variable, meaning it's directly continuous with a surface that no longer matches its opacity. I'm checking whether glass opacity is actually meant to be user-configurable via a root-level setting, which would make hardcoding --glass-opacity: 100% on the banner surface a real violation—it splits the glass system into adjusted and unadjusted surfaces, similar to the forbidden pattern of hardcoding foreground colors instead of using the shared token. I wonder if a lighter fix exists: rather than overriding the shared opacity token, maybe just removing before:mask-none (as the PR does) is sufficient, since the timeline content behind the banner would already be blurred and tinted consistently with other composer drawer surfaces. I should check the settings glass opacity range — if the minimum is quite low, text could become hard to read across all drawer surfaces, not just the banner. Let me look at __root.tsx around lines 160-180 and the settings slider to confirm.

12. browse_code — apps/web/src/routes/__root.tsx

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

💭 The glass opacity setting is meant to apply across menus, dialogs, and the composer, but this new rule hardcodes 100% opacity just for the composer banner surface, bypassing the user setting even at the default 80% level where there wasn't actually a legibility issue. I'm also noting that a test assertion was removed without being replaced with anything meaningful about the fix. Placing this as a single-variable override in global CSS versus keeping it as a Tailwind arbitrary class at the call site is a genuine judgment call -- the prior approach used a call-site override, so shifting it into global CSS represents a real direction change worth flagging, even though glass composition patterns can justify staying global. The concrete issue is that the opacity override makes the banner ignore the user's glass opacity setting entirely, causing it to appear fully opaque while adjacent elements sharing the same continuous glass system -- the stack cap, stacked alert banners, sync pill, and composer -- remain translucent, creating a visible mismatch at the seam. It's also a broader fix than needed, since removing the mask-none override alone would resolve the original overlap problem, and the blur effect becomes wasted since it's now hidden under solid fill. The smallest fix is likely dropping the surface class and its opacity rule while keeping the mask restoration, though I want to verify whether the mask removal alone truly resolves everything without reintroducing whatever issue prompted the original mask-none addition tied to keeping banners flush with the composer. That "not flush" bug was actually fixed by using mask-none, so reverting that fix could bring the regression back unless externalComposerDrawerAttached is reliably true whenever a banner renders. I need to check that variable carefully.

14. git_tools — externalComposerDrawerAttached

💭 Since banners always mean the shell is attached, the composer's main surface paints over the overlap area, so the mask restoration should keep things flush with #8000's fix — unless the composer's own top edge introduces a gap there, which I need to check. Actually, working through the negative margin math: the drawer-slot moves up exactly 1rem, landing right where the mask boundary starts, so it should stay flush. That makes me question why #8000 needed mask-none in the first place — maybe it was addressing a different edge case (like ThreadSyncStatusPill combinations), and this PR's mask-none was actually overcorrecting and causing the doubled band artifact instead. I'm now considering the case where both a banner and the pill are present together, since the adjacent-sibling selector for removing borders/radius seems to target the wrong element structurally, which could be part of the real issue. This gives me a solid, well-supported finding worth flagging as an inline comment. I'll target the added CSS lines around 919-923 in index.css, since that's where the mismatched opacity values were introduced, and note that removing the block (and the corresponding class in the TSX) would be the simplest fix. If they still want extra legibility, I'd suggest deriving it from the existing token rather than hardcoding a new value—something that applies consistently across all attached drawer surfaces instead of a one-off override. I lean toward treating this as a failing finding, since it's a real regression risk and default behavior is to fail when issues surface. I also verified the ThreadSyncStatusPill and banner stack can render simultaneously without conflict, and the stack cap spacing checks out at 80% offset. Now I'm putting together the review comment to submit. I'm confirming that apps/web/src/index.css falls within the include patterns for this check. Then I draft the actual explanation: hardcoding --glass-opacity: 100% on the banner's ::before pulls it out of the shared glass token system, so it no longer respects the user's Glass opacity setting from GlassAppearanceSync, and it ends up painting solid next to neighboring surfaces — the stack cap and the pill — that are meant to read as one continuous glass panel. Removing before:mask-none fixes the overlap, but I want the fuller fix: drop the redundant surface class and rule entirely, and if extra legibility over the timeline is genuinely needed, derive it from the shared opacity token rather than pinning one surface to 100%. I trim the wording down a bit for length, then confirm the diff line range (919–923, RIGHT side, added lines) before submitting the comment.

15. modify_pr — One finding on the new glass override in apps/web/src/index.css. Restoring the mask (dropping before:mask-none) looks right; pinning --glass-opacity to 100% for this one surface is the part that introduces a consistency regression.


Agent Credits: 108 credits