Summary popover: real blur under the glass in see-through windows

Merged
TE
6d ago
+674-1

Leo, on the Summary popover in a translucent window: "I don't like the non-glassness"

Spec: design/overlay-surfaces.md (legibility comes from blur, tint and contrast, never from an opaque fill; Reduce Transparency keeps its system fallback).

The defect

In a see-through window (opacity below 1, or a painting behind the chat), the narrow-pane Summary popover (Session Info, placement="overlay") was a tint with no blur. The transcript under it stayed sharp and ran into the popover's own rows. The cause is Chromium: it draws a backdrop-filter's blurred copy over the page's own pixels, not in their place, so on a clear page the sharp text shows through. The existing stopgap (a 70% theme underlay) only dimmed it. WebKit replaces in place, and an opaque window's backdrop already covers what is under it.

The fix

The page blurs itself under the glass (webviews/src/ui/glass/seeThroughBlur.ts). While a floating glass surface is open in a see-through Chromium page, each element it covers gets filter: url(#…), an SVG filter that:

  • blurs that element's own pixels inside the surface's rounded rect (feGaussianBlur with a primitive subregion, at the surface's own backdrop blur radius);
  • mixes blurred and sharp by the surface's opacity, so it fades in and out with the popover;
  • cuts to the rounded rect and leaves everything outside it untouched.

Then the surface drops its underlay (--glass-underlay: 0%), so no opaque fill is involved. glass.css applies the filter only in a see-through Chromium page, outside the lens and Reduce Transparency. Opaque windows, WebKit and Reduce Transparency behave exactly as before.

Wired into Session Info's overlay popover and the floating Summary card. Both cover the chat stage's blocks (chatUnderGlass.ts), never the composer: a filter there would re-anchor the composer's fixed menus. Edge cases:

  • Over the composer (a short pane's Session Info), the surface keeps its underlay.
  • While CSS hides the Summary card (Find, wherever its bar is; review; inspect), its blocks carry data-glass-under="summary", and summary.css switches their filters off under the same rules.
  • A hidden surface covers nothing.
  • A test DOM without CSS.supports or the observers gets nothing attached.

Other options I tried and rejected:

  • An alpha-boosting backdrop filter: either opaque, or it still leaves sharp text, with banding.
  • Native glass under the CEF view: it cannot blur the page's own transcript.
  • A CEF compositor patch: out of scope.

Cost and cleanup. Nothing in WebKit. In Chromium, while a surface is open, there is:

  • one hidden <svg> with one filter per covered block;
  • a ResizeObserver on the surface, its parent and the covered blocks;
  • a MutationObserver on the blocks' parent (direct children only, never the subtree, so no per-token work);
  • a requestAnimationFrame follow loop that reads every rect first, writes only changes, and stops after 3 unchanged frames. Resize, transition and animation events restart it.

Closing the surface (React layout-effect cleanup) disconnects the observer, removes the listeners, cancels the frame, and removes the attributes, filters and svg.

Limit. In a window set to no blur, the desktop or painting behind the window is not blurred under the popover, because the page cannot see it. The transcript, which is what collided with the popover's text, is blurred. Native blur under the popover's rect is a possible follow-up.

Before / after

Fresh HOME, fake harness, the striped backdrop window, the chat narrowed by a browser on its right.

Translucent window (opacity 0.55, no blur). Before: "Add retries with backoff…", "View changes" and "+29 −0" read through the popover. After: the transcript under it is blurred away and the stripes read as colour.

LightDark
Beforebefore lightbefore dark
Afterafter lightafter dark

Painting behind the chat.

LightDark
Beforebefore lightbefore dark
Afterafter lightafter dark

Opaque window (unchanged path, for regression):

LightDark
after lightafter dark

I viewed the after images in all three states, in light and dark, including zoomed crops of the popover. The captures are at 392db364. Later commits change only the Find/review/inspect, composer-overlap and test-DOM cases, none of which these scenes draw.

Tests

webviews/test/see-through-blur.test.ts (browser lane):

  • Real Chromium pixels: the bars under the popover lose more than 92% of their contrast, the bars outside are unchanged, and detach restores everything.
  • The underlay goes from 70% to 0%.
  • An opaque page gets no filter.
  • A surface hidden by a :has() rule releases its filters and gets them back.
  • The Summary's filters go off when a Find bar opens deep inside the transcript.
  • The underlay comes back over the composer.
  • WebKit attaches nothing.
  • Geometry unit tests.

Also run on the fleet: the glass suites (chromium-glass-underlay, glass-never-opaque, glass-lens, popup-page-root-glass), the overlay-glass and glass-never-opaque lints, tsc, lint, fmt, browser-lane and effect-cleanup checks.

Risk: low to medium. Rendering only, Chromium see-through pages only. The filter is scoped to the covered blocks while a floating glass is open.

🤖 Generated with Claude Code

Agent session: agent session