test(next): glass is never opaque, natively and in webviews; the open pane sidebar is glass

Merged
TE
15d ago
+352-4

The rule: glass is never opaque (Leo 2026-10-20: "we want a lot of glass... I hate opaque"). A surface that blurs what is behind it, or is declared glass, must let that backdrop show. Reduce Transparency is the one place glass turns solid. This PR adds one guard with a native side and a web side, modeled on the pane toolbar's no-raw-accent-lit-glyph (an ast-grep rule plus a webviews test). It also fixes the one existing violation the guard found.

The guard

  • Native: scripts/cmux-next/lint/ast-grep-rules/no-opaque-glass.yml. The pinned ast-grep rules run in scripts/cmux-next/prepush-gate.py (pre-push hook) and in CI as the step "Pushed commits add no violations" of the cmux-next checks job (.github/workflows/cmux-next.yml). It flags:

    • an opaque tintColor or layer fill on a view named glass or visual effect. Opaque means Palette.popupSurface, alpha 1 (withAlphaComponent(1), alpha: 1), or a solid system color.
    • isOpaque = true, or var isOpaque: Bool { true }, in a subclass of NSVisualEffectView, NSGlassEffectView or GlassPanelView.
    • an opaque .background(...) right before or after .glassEffect(...) in SwiftUI.

    The Reduce Transparency paths draw a separate plain view (OverlaySurface's .opaque material view, the shortcut chip's fill), so the rule doesn't match them. The reviewed escape is // glass-opaque-allow: <why>, added to the gate's ESCAPES.

  • Web: webviews/test/glass-never-opaque.test.ts, with the logic in webviews/scripts/check-glass-never-opaque.ts. CI runs it in the step "Test webviews" of web / react-apps-check (N) (ci-web.yml, via ci.yml on PRs and cmux-next-web.yml on feat-cmux-next pushes). A plain expect, so it fails the job. It scans every CSS file under webviews/src, skipping rules under @media (prefers-reduced-transparency: reduce) or [data-reduce-transparency] (not (...) and :not([...]) are still checked). It flags:

    • a rule with an active backdrop-filter, or a class named glass, whose background resolves opaque.
    • a --*glass* token set to an opaque color or a 100% share.

    "Opaque" counts only what the scan can prove: a literal color with no alpha, Canvas, a token whose every definition is opaque (the host's --cmux-*solid* and --cmux-popup-surface count), or a color-mix of two opaque colors. Anything it can't resolve passes, so a solid card that sets no blur (.ui-card) is never flagged. The escape is /* glass-opaque-allow: <why> */ on the line before. A second test runs the scan over a fixture, so it can't pass by finding nothing.

This complements #23133, which renders gallery stages in Chromium and catches computed opaque glass at runtime. This guard is static: it covers Swift, and CSS states the gallery doesn't render (the open sidebar below is one). The two PRs share no files.

Violations fixed

WhereBeforeAfter
webviews/src/agent-session/acpmux/styles.css:5, .acpmux-shell[data-sidebar=open] .acpmux-sidebar (the narrow pane's open session sidebar, over the transcript)background: var(--acpmux-base) under backdrop-filter: var(--surface-blur): an opaque fill, so the blur never showedbackground: color-mix(in srgb, var(--acpmux-base) var(--surface-text-min, 92%), transparent): the text floor desktop.css sets for translucent surfaces that carry text. Under Reduce Transparency, desktop.css makes it 100% and the blur none, so it is solid again

The native scan of Packages/macOS/CmuxNext/Sources finds nothing today, so no Swift file changes.

#23000's toast (CmuxNextFloating/ChiefPet/ChiefPetBubble.swift, which another session is reworking) isn't flagged and isn't touched, so it needs no exclusion.

Red, then green

Commit 1 (6a9d0a2bb27c11264b96e66c5b058cf9c86575ab) adds the guard only. Commit 2 (c3ac1f8f5eeee68ca8baf04372e2fd92a8c064de) adds the fix.

$ (cd webviews && bun scripts/check-glass-never-opaque.ts)   # at commit 1
glass is never opaque: agent-session/acpmux/styles.css:5: .acpmux-shell[data-sidebar=open] .acpmux-sidebar { background: var(--acpmux-base) }
exit=1

$ (cd webviews && bun scripts/check-glass-never-opaque.ts)   # at commit 2
Glass fills checked: none opaque.
exit=0
$ ast-grep scan -c scripts/cmux-next/lint/sgconfig.yml --filter no-opaque-glass Packages/macOS/CmuxNext/Sources
exit=0   (no matches, on both commits)

On a scratch Swift file, the native rule matched all 9 intended cases, including the escaped one (the gate drops that one through ESCAPES). It did not match Palette.glassTint, withAlphaComponent(min(a, 1)), (materialView as? GlassPanelView)?.tintColor = look?.veil..., a translucent fill on an effect view, fill.layer?.backgroundColor = Palette.popupSurface (not a glass view), materialView?.layer?.backgroundColor = Palette.popupSurface (the .opaque path), isOpaque in a plain NSView subclass, or .glassEffect().background(.ultraThinMaterial).

Changelog

  • Fixed: The agent pane's session sidebar, opened over a narrow chat, is frosted glass again instead of an opaque panel.

Observed / Not run

  • Observed: bun scripts/check-glass-never-opaque.ts red on commit 1 and green on commit 2 (above). The fixture cases match the test's expected list, checked by running the same fixture through the script. The ast-grep scan of the real sources and of the scratch file is as described above. python3 scripts/cmux-next/prepush-gate.py: "no new violations". scripts/web-lint.sh: passed.
  • Not run: bun test test/glass-never-opaque.test.ts locally (this host's guard refuses bun test), so CI's "Test webviews" step is its first run. No Swift build or test, since no Swift changed.

No capture: the only visible change is one CSS declaration in a webview (the narrow pane's open session sidebar fill goes from opaque --acpmux-base to the shared 92% --surface-text-min mix under its existing blur, and back to solid under Reduce Transparency). No gallery stage renders that open-sidebar state, and the Swift rule changes no app code.

Agent session: cosmic-gray

🤖 Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.


Summary by cubic

Enforces the standing rule that glass is never opaque, natively and in webviews, and fixes the one violation the new guard finds.

Old behavior: the agent pane's open session sidebar blurred the transcript underneath an opaque base fill, so the blur never showed. New behavior: the fill is a 92% translucent color-mix, so the backdrop shows through the blur; Reduce Transparency still makes it solid.

The guard has two sides, both wired into CI:

  • Native: an ast-grep rule (no-opaque-glass) in the pre-push gate and the cmux-next checks job flags opaque tints/fills or isOpaque on glass views, and opaque .background next to .glassEffect. A reviewed escape is // glass-opaque-allow: <why>.
  • Web: check-glass-never-opaque.ts scans every CSS file under webviews/src and flags an opaque fill on a blurred or glass-named surface, or an opaque --*glass* token, outside Reduce Transparency contexts. A fixture test ensures the scan can't pass vacuously. Escapes are /* glass-opaque-allow: <why> */.

Reduce Transparency paths are exempt on both sides since that's the one place glass is allowed to go solid.