cmux-next: liquid glass lens primitive and Glass setting
This PR is the primitive, the Debug toggle and the host feed. Adoption on the surfaces (composer, bubbles, queue card, menus and popovers, hover/link/Updated cards, jump pill) follows right after in its own PR; the gallery entry ui.liquid-glass shows them all.
Liquid glass with a real lens: the wallpaper bends at each glass rim, with a thin specular rim and an optional chromatic fringe (Spectrum). One shared web primitive, .ui-glass.
The lens is off by default. Leo (2026-10-21): "the lens thing we just need to see and feel... we can put it as a toggle and later promote to default". Today's glass (Plain Glass) is unchanged for everyone. In DEV and NIGHTLY builds, the Debug Settings choice glass.lens (Glass and Overlays > Liquid Glass: Plain Glass, Lens, Spectrum) turns it on, and Toggle Liquid Glass Lens in the command palette or the Debug menu flips Plain and Lens live. It lives in the build's debug-tunables.json, outside the settings schema, so there is no Settings row and no new default; Release never activates Debug Settings and stays Plain. A Settings > Appearance control comes back when the lens is promoted.
Design: plans/cmux-next/glass-lens-DESIGN.md. WebKit can't refract a backdrop (backdrop-filter: url() parses but renders nothing; probed on macOS 27), and the wallpaper is drawn natively behind transparent web views. So the host sends each page a still of the wallpaper and its rect (cmuxTheme.backdrop), and one WebGL canvas per page lenses that copy under every visible .ui-glass.
- Web (
webviews/src/ui/glass/): tokens--glass-tint,--glass-blur,--glass-frost,--glass-refraction,--glass-rim,--glass-dispersion; class.ui-glass; the lens (lens.ts), which does nothing in Plain, under Reduce Transparency, with no still, or with no glass on screen, and whose rAF loop runs only after scroll, resize, DOM or animation changes. Gallery entryui.liquid-glassover a generated wallpaper. - Host: the Debug choice
glass.lens(GlassLensMode.tunable); effective mode is Plain under Reduce Transparency or Low Power Mode, sent asdata-glass.GlassBackdropFeedper web view (PageWebView, AgentPaneView, AgentActivityHostView) sends the art as drawn (JPEG, ≤1024 px, off the main thread, cached), its aspect-filled rect, blur and tint;nullfor video, no art or translucent art.
Media
The PR head built on a fleet mini (cmux-austin-mini-0), the gallery's glass transcript in a cmux NEXT browser tab over a colorful wallpaper:
| Lens | Plain Glass | Spectrum |
|---|---|---|
![]() | ![]() | ![]() |
Video, scrolling under the lens (stepped captures, not real time):
Frame time
Same mini (M4, 60 Hz display, no 120 Hz mini in the fleet), PR head app build, 400 turns (402 glass surfaces, 9 on screen), constant 1800 px/s scroll, 6 runs x 6 s per mode:
| Mode | p50 | p95 | p99 | dropped |
|---|---|---|---|---|
| Lens | 17 ms | 18 ms | 19 ms | 0.14% |
| Plain | 17 ms | 17 ms | 19 ms | 0.09% |
| Spectrum | 17 ms | 18 ms | 19 ms | 0.23% |
60.0 fps in every mode; drops are mostly each run's first frame. This counts requestAnimationFrame cadence, not compositor-side misses, at 1x scale.
Changelog
Added: An opt-in Liquid Glass lens for glass surfaces in DEV and NIGHTLY builds (Toggle Liquid Glass Lens in the palette or the Debug menu); the default glass is unchanged.
Observed / Not run
Observed:
bun test test/glass-state.test.ts: 13 pass.CMUX_BROWSER_TESTS=1 bun test test/glass-lens.test.ts: 10 pass in Chromium and WebKit (stripes displaced at the rim, not at the center or outside; no canvas in Plain or under Reduce Transparency; one redraw per move, then the loop stops; canvas removed with the last glass).- Gallery captures over the fixture wallpaper in all modes, both engines: the rim bends and magnifies the wallpaper; text under the composer and a menu frosts.
- Scrolling 120 glass bubbles one step per frame, headless WebKit: p50 17 ms, p95 21 ms (plain: p50 17 ms). On a fleet mini in the app: see Frame time.
- Two independent review passes (heads a885dbeb, 55af3817).
- Fleet at the merged head 6acb9842aec (the final head 048ea3cc4d8 only adds the regenerated action exports on top):
cmux-ci compile-check --apppassed;GlassLensTests,GlassBackdropTests,PaletteRankCatalogTests,ActionContractTests,ActionSurfaceParityTests,PaletteMenuVocabularyTestspass (aws-m4pro-7). - Settings exports regenerated on the fleet at that head are byte-identical to base;
defaults_guard.py check-stamppasses (no new default). - Gallery accent/glass invariant scan of the Inspector, Summary card, Updated (changelog update surfaces) and Chief inspector entries: 32 stages, 0 opaque-glass findings.
bun run check: one type error intest/editor-find-narrow.test.tsx, unchanged here and red on base (from #22872).- Webview shard reds: each failing file compared head vs base locally; the same counts fail on base (chief-context-copy, composer-history, composer-location-strip, composer-mention, composer-suggested-prompt, quiet-access-chip, settings summaries over 60 chars), the rest pass on both.
prepush-gate.py: no new violations.
Not run:
- The native wallpaper feed in the app (the mini run used the gallery's fixture still), a Retina 2x display and 120 Hz.
- Flipping the toggle in a running NIGHTLY build (covered by GlassLensTests through the tunable store and by the compile; not clicked by hand).
Leo-ask: ask-0e7ea215d7
Leo-ask: ask-b5bfc7b208
Agent session: cmux16
🤖 Generated with Claude Code


