Repository navigation
Summary popover: real blur under the glass in see-through windows - #30302
Conversation
… see-through Chromium page Leo: "I don't like the non-glassness". Over a translucent window or a painting, Chromium drew the popover's backdrop blur over the page's own pixels, so the chat under it stayed sharp and read through the tint. The covered siblings now blur their own pixels inside the popover's rounded rect (an SVG filter per element, ui/glass/seeThroughBlur.ts), and the popover drops the Chromium underlay it no longer needs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Owned by an agent session · session |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-140531107322b17b78f0
…s blocks The narrow pane's Summary popover is Session Info floating over the chat (acpmux-session-info, overlay). It sits beside the chat column, so it names what it covers: the stage's own blocks (chatUnderGlass.ts), never the whole column with the composer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-140531107322b17b78f0
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-140531107322b17b78f0
There was a problem hiding this comment.
2 issues found across 6 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="webviews/src/ui/glass/seeThroughBlur.ts">
<violation number="1" location="webviews/src/ui/glass/seeThroughBlur.ts:175">
P2: `release` clears `data-glass-under` and `--glass-under-filter` on a covered element even when another open surface now owns that element. Two surfaces covering the same block will therefore drop each other's blur when one closes. Only remove the attribute and property if the current value still points to this surface's filter id.</violation>
<violation number="2" location="webviews/src/ui/glass/seeThroughBlur.ts:236">
P2: The hole is only recomputed on resize, transition, or animation events, so scrolling that moves a covered element without resizing the surface leaves the blur misplaced. Listen for scroll events in capture phase on the document and call `kick` for them, and remove that listener in the detach function.</violation>
</file>
Reply with feedback, questions, or to request a fix.
View guided diff | Turn on auto-fix | Re-trigger cubic
| resize.observe(parent); | ||
| surface.addEventListener("transitionrun", kick); | ||
| surface.addEventListener("animationstart", kick); | ||
| view.addEventListener("resize", kick); |
There was a problem hiding this comment.
P2: The hole is only recomputed on resize, transition, or animation events, so scrolling that moves a covered element without resizing the surface leaves the blur misplaced. Listen for scroll events in capture phase on the document and call kick for them, and remove that listener in the detach function.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At webviews/src/ui/glass/seeThroughBlur.ts, line 236:
<comment>The hole is only recomputed on resize, transition, or animation events, so scrolling that moves a covered element without resizing the surface leaves the blur misplaced. Listen for scroll events in capture phase on the document and call `kick` for them, and remove that listener in the detach function.</comment>
<file context>
@@ -0,0 +1,262 @@
+ resize.observe(parent);
+ surface.addEventListener("transitionrun", kick);
+ surface.addEventListener("animationstart", kick);
+ view.addEventListener("resize", kick);
+ return () => {
+ if (frame) view.cancelAnimationFrame(frame);
</file context>
| if (!entry) return; | ||
| entry.filter.remove(); | ||
| target.removeAttribute(UNDER_ATTRIBUTE); | ||
| target.style.removeProperty(UNDER_FILTER_PROPERTY); |
There was a problem hiding this comment.
P2: release clears data-glass-under and --glass-under-filter on a covered element even when another open surface now owns that element. Two surfaces covering the same block will therefore drop each other's blur when one closes. Only remove the attribute and property if the current value still points to this surface's filter id.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At webviews/src/ui/glass/seeThroughBlur.ts, line 175:
<comment>`release` clears `data-glass-under` and `--glass-under-filter` on a covered element even when another open surface now owns that element. Two surfaces covering the same block will therefore drop each other's blur when one closes. Only remove the attribute and property if the current value still points to this surface's filter id.</comment>
<file context>
@@ -0,0 +1,262 @@
+ if (!entry) return;
+ entry.filter.remove();
+ target.removeAttribute(UNDER_ATTRIBUTE);
+ target.style.removeProperty(UNDER_FILTER_PROPERTY);
+ covered.delete(target);
+ };
</file context>
…low the covered blocks - A window without CSS.supports, ResizeObserver, MutationObserver or animation frames (a test DOM) attaches nothing. - A surface hidden by CSS (the Summary card under Find) covers nothing, so the Find bar and the transcript are not blurred under an invisible card. - The loop also restarts when a covered block resizes or its parent's children change (a banner or Find bar coming in), not only on the surface and its parent. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-140531107322b17b78f0
|
cmux status (one comment, edited in place; each line is updated by the job that owns it) Review App of this head (app.zip, kept 1 day)
Stale: these images are of Gallery before/after for
States
Pick other states with a Shipped in:
|
There was a problem hiding this comment.
1 issue found across 2 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="webviews/src/ui/glass/seeThroughBlur.ts">
<violation number="1" location="webviews/src/ui/glass/seeThroughBlur.ts:191">
P2: `watched` retains every candidate and parent for the entire attachment, but `measure` never unobserves elements that leave `candidates`; repeated block replacement accumulates observer targets and detached DOM references until close. Track the current candidate/parent set and unobserve stale targets during measurement.</violation>
</file>
Reply with feedback, questions, or to request a fix.
View guided diff | Turn on auto-fix | Re-trigger cubic
| const watched = new Set<Element>(); | ||
| const watch = (element: Element, children: boolean) => { | ||
| if (watched.has(element)) return; | ||
| watched.add(element); |
There was a problem hiding this comment.
P2: watched retains every candidate and parent for the entire attachment, but measure never unobserves elements that leave candidates; repeated block replacement accumulates observer targets and detached DOM references until close. Track the current candidate/parent set and unobserve stale targets during measurement.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At webviews/src/ui/glass/seeThroughBlur.ts, line 191:
<comment>`watched` retains every candidate and parent for the entire attachment, but `measure` never unobserves elements that leave `candidates`; repeated block replacement accumulates observer targets and detached DOM references until close. Track the current candidate/parent set and unobserve stale targets during measurement.</comment>
<file context>
@@ -166,6 +179,19 @@ export function attachSeeThroughBlur(surface: HTMLElement, candidates: CoveredCa
+ const watched = new Set<Element>();
+ const watch = (element: Element, children: boolean) => {
+ if (watched.has(element)) return;
+ watched.add(element);
+ resize.observe(element);
+ if (children) mutations.observe(element, { childList: true });
</file context>
… underlay over the composer - The Summary popover tags what it covers data-glass-under="summary"; summary.css switches those filters off under the same rules that hide the card (Find, wherever its bar is; review; inspect), with no observer needed to see it. - The composer never takes a filter (it would re-anchor its fixed menus), so a surface over it (a short pane's Session Info) keeps its underlay. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-140531107322b17b78f0
There was a problem hiding this comment.
4 issues found across 6 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="webviews/test/see-through-blur.test.ts">
<violation number="1" location="webviews/test/see-through-blur.test.ts:148">
P2: This test uses `.dock` as the composer stand-in, but `attach` passes all siblings as candidates, so `.dock` is blurred when it overlaps the popover. The test never checks that, so the composer-no-filter rule is untested. Exclude the dock from the candidates in the helper so the stand-in matches the composer's role.</violation>
</file>
<file name="webviews/src/ui/glass/seeThroughBlur.ts">
<violation number="1" location="webviews/src/ui/glass/seeThroughBlur.ts:242">
P2: The composer-overlap decision is not re-measured when the dock changes size or children. Observe each `keepUnderlayOver` element (and its child mutations) so `data-glass-over` is restored whenever the composer stack grows or shrinks.</violation>
<violation number="2" location="webviews/src/ui/glass/seeThroughBlur.ts:243">
P2: This overlap test reads zero rects for `display: contents` elements. Outside float mode (reviewing, inspecting, fresh view, diff), `.acpmux-dock` has no box, so an overlay over the in-flow composer never restores its underlay. The keepUnderlayOver target needs elements that have a box, such as the dock's children, or the caller's selector needs changing.</violation>
<violation number="3" location="webviews/src/ui/glass/seeThroughBlur.ts:299">
P3: `options` is now a `useLayoutEffect` dependency, so an inline object literal would re-attach the blur on every render. Document that `options` must be stable too, as `candidates` already is.</violation>
</file>
Reply with feedback, questions, or to request a fix.
View guided diff | Turn on auto-fix | Re-trigger cubic
| detach?: () => void; | ||
| }; | ||
| const popover = document.getElementById("popover")!; | ||
| target.detach = target.attachSeeThroughBlur(popover, (e) => e.parentElement!.children, { |
There was a problem hiding this comment.
P2: This test uses .dock as the composer stand-in, but attach passes all siblings as candidates, so .dock is blurred when it overlaps the popover. The test never checks that, so the composer-no-filter rule is untested. Exclude the dock from the candidates in the helper so the stand-in matches the composer's role.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At webviews/test/see-through-blur.test.ts, line 148:
<comment>This test uses `.dock` as the composer stand-in, but `attach` passes all siblings as candidates, so `.dock` is blurred when it overlaps the popover. The test never checks that, so the composer-no-filter rule is untested. Exclude the dock from the candidates in the helper so the stand-in matches the composer's role.</comment>
<file context>
@@ -133,11 +133,23 @@ await requireBrowserLane("see-through-blur.test.ts", async () => {
+ detach?: () => void;
+ };
+ const popover = document.getElementById("popover")!;
+ target.detach = target.attachSeeThroughBlur(popover, (e) => e.parentElement!.children, {
+ owner,
+ keepUnderlayOver: dock ? () => document.querySelectorAll(".dock") : undefined,
</file context>
| target.detach = target.attachSeeThroughBlur(popover, (e) => e.parentElement!.children, { | |
| target.detach = target.attachSeeThroughBlur(popover, (e) => [...e.parentElement!.children].filter((c) => !c.classList.contains("dock")), { |
| let unblurrable = false; | ||
| if (shown && keepUnderlayOver) | ||
| for (const element of keepUnderlayOver(surface)) | ||
| if (overlaps(rect, element.getBoundingClientRect())) unblurrable = true; |
There was a problem hiding this comment.
P2: This overlap test reads zero rects for display: contents elements. Outside float mode (reviewing, inspecting, fresh view, diff), .acpmux-dock has no box, so an overlay over the in-flow composer never restores its underlay. The keepUnderlayOver target needs elements that have a box, such as the dock's children, or the caller's selector needs changing.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At webviews/src/ui/glass/seeThroughBlur.ts, line 243:
<comment>This overlap test reads zero rects for `display: contents` elements. Outside float mode (reviewing, inspecting, fresh view, diff), `.acpmux-dock` has no box, so an overlay over the in-flow composer never restores its underlay. The keepUnderlayOver target needs elements that have a box, such as the dock's children, or the caller's selector needs changing.</comment>
<file context>
@@ -225,6 +237,11 @@ export function attachSeeThroughBlur(surface: HTMLElement, candidates: CoveredCa
+ let unblurrable = false;
+ if (shown && keepUnderlayOver)
+ for (const element of keepUnderlayOver(surface))
+ if (overlaps(rect, element.getBoundingClientRect())) unblurrable = true;
+ surface.toggleAttribute(OVER_ATTRIBUTE, !unblurrable);
const keep = new Set(under.map((entry) => entry.target));
</file context>
| } | ||
| let unblurrable = false; | ||
| if (shown && keepUnderlayOver) | ||
| for (const element of keepUnderlayOver(surface)) |
There was a problem hiding this comment.
P2: The composer-overlap decision is not re-measured when the dock changes size or children. Observe each keepUnderlayOver element (and its child mutations) so data-glass-over is restored whenever the composer stack grows or shrinks.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At webviews/src/ui/glass/seeThroughBlur.ts, line 242:
<comment>The composer-overlap decision is not re-measured when the dock changes size or children. Observe each `keepUnderlayOver` element (and its child mutations) so `data-glass-over` is restored whenever the composer stack grows or shrinks.</comment>
<file context>
@@ -225,6 +237,11 @@ export function attachSeeThroughBlur(surface: HTMLElement, candidates: CoveredCa
}
+ let unblurrable = false;
+ if (shown && keepUnderlayOver)
+ for (const element of keepUnderlayOver(surface))
+ if (overlaps(rect, element.getBoundingClientRect())) unblurrable = true;
+ surface.toggleAttribute(OVER_ATTRIBUTE, !unblurrable);
</file context>
| } | ||
|
|
||
| /** attachSeeThroughBlur for a React surface: attached while `active` and mounted. `candidates` must be | ||
| * stable (module-level constants). */ |
There was a problem hiding this comment.
P3: options is now a useLayoutEffect dependency, so an inline object literal would re-attach the blur on every render. Document that options must be stable too, as candidates already is.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At webviews/src/ui/glass/seeThroughBlur.ts, line 299:
<comment>`options` is now a `useLayoutEffect` dependency, so an inline object literal would re-attach the blur on every render. Document that `options` must be stable too, as `candidates` already is.</comment>
<file context>
@@ -279,15 +296,16 @@ export function attachSeeThroughBlur(surface: HTMLElement, candidates: CoveredCa
/** attachSeeThroughBlur for a React surface: attached while `active` and mounted. `candidates` must be
- * stable (a module-level function). */
+ * stable (module-level constants). */
export function useSeeThroughBlur(
surface: RefObject<HTMLElement | null>,
</file context>
| * stable (module-level constants). */ | |
| * stable, and so must `options` (module-level constants). */ |
…mposer's size - A surface hidden with visibility keeps its holes, so the Summary's blur is back the moment Find closes with no event needed; summary.css switches its filters off meanwhile. - The elements a surface keeps its underlay over (the composer) are observed too, so the overlap follows the composer growing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-140531107322b17b78f0
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 abackdrop-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 getsfilter: url(#…), an SVG filter that:feGaussianBlurwith a primitive subregion, at the surface's own backdrop blur radius);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:data-glass-under="summary", and summary.css switches their filters off under the same rules.CSS.supportsor the observers gets nothing attached.Other options I tried and rejected:
Cost and cleanup. Nothing in WebKit. In Chromium, while a surface is open, there is:
<svg>with one filter per covered block;ResizeObserveron the surface, its parent and the covered blocks;MutationObserveron the blocks' parent (direct children only, never the subtree, so no per-token work);requestAnimationFramefollow 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.
Painting behind the chat.
Opaque window (unchanged path, for regression):
I viewed the after images in all three states, in light and dark, including zoomed crops of the popover. The captures are at 392db36. 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)::has()rule releases its filters and gets them back.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