Skip to content

fix(web): Cool Darker glass survives past the composer and the window losing focus - #93

Merged
NoahHendrickson merged 4 commits into
customfrom
t3code/start-mac-dev-app
Aug 15, 2026
Merged

fix(web): Cool Darker glass survives past the composer and the window losing focus#93
NoahHendrickson merged 4 commits into
customfrom
t3code/start-mac-dev-app

Conversation

@NoahHendrickson

Copy link
Copy Markdown
Owner

Three defects in the Cool Darker wallpaper glass that all presented as "the theme is wrong" rather than as the layering bugs they were.

The composer rendered as a flat grey slab

Its vessel and context chips carried a backdrop-filter, and a filter cannot coexist with native vibrancy. The NSVisualEffectView sits behind the window, outside anything the page compositor owns, so Chromium flattens the region to composite the filter and the desktop is lost — the element keeps its brightness and shows none of the wallpaper.

Measured through the running app, as a channel spread (max − min):

surface spread
sidebar 25
stage beside the composer 14–20
composer 3

Both filters are now cleared explicitly, stated as a rule rather than deleted so re-adding one is deliberate. Removing them is safe because the transcript is masked to end above the composer, which took over the blur's original job of destroying text passing underneath.

The window went opaque whenever it lost focus

NSVisualEffectView follows the window's active state by default and drops to a flat inactive fill. The CSS behind it stays translucent either way, so it read as the whole app going opaque rather than as a window losing focus. The window is built with visualEffectState: "active" now — construction-only, and Electron requires it paired with vibrancy.

The material was the ceiling on everything else

under-window is the darkest sampler macOS offers and returned a flat ~49 grey, so the stage and composer alphas were both arithmetically correct and had almost nothing to show. fullscreen-ui returns ~99 and carries colour. It is named once in ForkGlassState.ts because the window's construction options and the runtime call have to agree and neither file can import the other.

Its trade: under-window samples the desktop picture alone, so the glass ignored other apps moving beneath the window. The behind-window materials sample what is actually there, so the glass now reacts as windows pass under.

Retuning

Alphas are not portable across materials, so these were reset by holding the desktop's contribution constant rather than the alpha — 15% of 99 is 14.8 where 30% of 49 was 14.7. Sidebar 88%; stage 95% easing to 85% across its bottom third, where the cutoff mask has already ended the transcript and there is no long-form text to protect.

The composer is white washes over that stage — 6% tray, the same 6% compounded for the ring, 12% chips — with the input darkening back toward the column and the focus ring taking an alpha instead of the palette's opaque #444a4f. Following Figma 322:6306 for the tray, ring and chips and 322:6316 for the recessed input. The composer also sits 32px off the bottom now, via the spacer that already owned that gap.

Guards

Rewritten alongside, several of them inverted:

  • No backdrop-filter may survive anywhere in the glass set.
  • The stage is read as gradient stops rather than one fill, and the rule that clears bg-background on the inset is pinned — a background-image alone composites over that opaque utility fill and the glass silently does nothing.
  • The grain utility's repeat and 128px size must be overridden, which is what made the gradient tile down the column.
  • "Never more transparent than the sidebar" now applies to the reading area alone, since the lower stops sit below the mask.

350/350 fork guards, typecheck clean in apps/web and apps/desktop.

Verification

Verified in the running desktop app across the session, with values measured from screenshots rather than eyeballed — the arithmetic in the CSS comments is the measured composite, not an estimate. Not attached here: before/after images live in the working session. Requires a restart to see the desktop-side changes.

Model: Claude Opus 5 (1M context), harness: Claude Code.

🤖 Generated with Claude Code

… losing focus

The glass palette had three defects that all presented as "the theme is
wrong" rather than as the layering bugs they were.

The composer rendered as a flat grey slab in an otherwise translucent
window. Its vessel and context chips carried a backdrop-filter, and a
filter cannot coexist with native vibrancy: the NSVisualEffectView sits
behind the window, outside anything the page compositor owns, so Chromium
flattens the region to composite the filter and the desktop is lost.
Measured through the app, the wallpaper reached the sidebar at a channel
spread of 25 and the stage beside the composer at 14-20, against 3 inside
it. Both filters are now cleared explicitly. Removing them is safe because
the transcript is masked to end above the composer, which took over the
blur's original job of hiding text passing underneath.

The window went opaque whenever it lost focus, because the material
follows the window's active state by default and the CSS behind it stays
translucent either way. It is built with visualEffectState "active" now.

The material itself was the ceiling on everything else. "under-window" is
the darkest sampler macOS offers and returned a flat ~49 grey, so stage
and composer alphas were both arithmetically correct and had almost
nothing to show. "fullscreen-ui" returns ~99 and carries colour. It is
named once in ForkGlassState.ts because the window's construction options
and the runtime call have to agree and neither file can import the other.

Retuned around the brighter material by holding the desktop's
contribution constant rather than the alpha: sidebar 88%, stage 95%
easing to 85% across its bottom third, where the cutoff mask has already
ended the transcript. The composer is white washes over that — 6% tray,
the same 6% compounded for the ring, 12% chips — with the input darkening
back toward the column and the focus ring taking an alpha instead of the
palette's opaque fill. The composer also sits 32px off the bottom.

Guards were rewritten alongside, several of them inverted: no
backdrop-filter may survive in the glass set, the stage is read as
gradient stops rather than one fill, and the reading area alone answers to
"never more transparent than the sidebar" now that the lower stops sit
below the mask.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@cursor

cursor Bot commented Aug 15, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XL labels Aug 15, 2026

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

REQUEST_CHANGES — thermo-nuclear code quality review.

The glass/cutoff direction is right (mask the transcript instead of frosting the composer; share the vibrancy material; keep the material active when unfocused). The structure does not clear the approval bar.

Blockers / high-priority structure

  1. Stop growing ChatView.tsx (~6.6k) with new fork shell stamping — put --fork-composer-inset in apps/web/src/custom/ (or a tiny fork-owned wrapper). Inline fences are last resort per .fork/AGENTS.md.
  2. The cutoff mask model is buried in opaque midpoints (* 0.28 / * 0.6) and a 7-stop top fade, duplicated across -webkit-mask/mask. Name the stops or collapse the ramp.
  3. The glass retune in theme.custom.palettes.css is mostly essay (~180 comment lines vs ~27 declarations), and the same narrative is triplicated across YAML / CSS / guards. One intent home; short outcome assertions.

Also delete the identity VIBRANCY_MATERIAL = FORK_VIBRANCY_MATERIAL alias, and keep new guards short/loud rather than re-litigating design history.

What looks fine: unconditional fork-timeline-cutoff in MessagesTimeline, shared FORK_VIBRANCY_MATERIAL + visualEffectState: "active" in the desktop fork layer, and dual vendor mask declarations.

Open in Web View Automation 

Sent by Cursor Automation: Thermo-nuclear PR review

Comment thread apps/web/src/components/ChatView.tsx
Comment thread apps/web/src/theme.custom.css
Comment thread apps/web/src/theme.custom.palettes.css Outdated
Comment thread apps/desktop/src/fork/ForkSidebarVibrancy.ts Outdated
Comment thread apps/web/src/__fork_guards__/forkCoolDarkerSidebarVibrancy.test.ts

@NoahHendrickson NoahHendrickson left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found one blocking cross-browser issue. I am posting this as a comment review because GitHub does not allow the PR author account to request changes on its own PR. Local verification: 350/350 fork guards, 20/20 focused timeline tests, web typecheck, and desktop typecheck all pass.

Comment thread apps/web/src/theme.custom.css Outdated
@github-actions

github-actions Bot commented Aug 15, 2026

Copy link
Copy Markdown

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

Provider Metric Main baseline This PR Impact PR ceiling
Codex Total thread wire 11.3 KiB 11.3 KiB −1 B (−0.0%) 15.1 KiB
Codex Thread snapshot wire 5.5 KiB 5.5 KiB −2 B (−0.0%) 7.3 KiB
Codex Live turn WebSocket wire 5.9 KiB 5.9 KiB +1 B (+0.0%) 7.8 KiB
Codex Live turn WebSocket decoded 49.7 KiB 49.7 KiB 0 B (0.0%) 66.4 KiB
Codex Live turn messages 16 16 0 (0.0%) 21
Claude Total thread wire 11.3 KiB 11.3 KiB +6 B (+0.1%) 15.1 KiB
Claude Thread snapshot wire 5.5 KiB 5.5 KiB 0 B (0.0%) 7.3 KiB
Claude Live turn WebSocket wire 5.9 KiB 5.9 KiB +6 B (+0.1%) 7.8 KiB
Claude Live turn WebSocket decoded 50.6 KiB 50.6 KiB 0 B (0.0%) 66.4 KiB
Claude Live turn messages 16 16 0 (0.0%) 21

Baseline: 017c026 · PR result: 7c84345 · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 94.6 KiB
  • Claude decoded thread snapshot: 95.4 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

@NoahHendrickson

Copy link
Copy Markdown
Owner Author

Code review

Reviewed all 11 files / ~700 lines. I read every hunk with surrounding CSS/TSX/Electron context and ran the three touched guard suites — 57 passed.

What checks out

  • composerOverlayHeight is in scope, and the chat column really is the nearest common ancestor of the scroller and the docked overlay, so --fork-composer-inset inherits correctly.
  • The mask layer stack (sizes / positions / order) matches upstream's, and the --topbar-scroll-fade-height it folds in is declared on the same element by .chat-timeline-scroll-fade.
  • theme.custom.css is unlayered, so it beats index.css's @layer components regardless of specificity.
  • background-color / background-repeat / background-size do cover all three declarations surface-grain sets.
  • Electron re-applies visual_effect_state_ inside setVibrancy, so the runtime toggle keeps "active".

Findings

1. Stage gradient bottoms out below the sidebar on non-chat pagesapps/web/src/theme.custom.palettes.css:374

The 95%→85% gradient is justified by the chat-only cutoff mask, but it lands on [data-slot="sidebar-inset"], which is also the root surface for Settings, Usage, and the no-active-thread state. On those pages the bottom third of a scrolling text panel now sits at 85%, below the sidebar's 88%, with no mask ending anything. The rewritten guard only pins the top stop, so it can't catch this.

2. Cutoff ramp overlaps live content instead of the empty padapps/web/src/theme.custom.css:336

The ramp runs ~6rem above --fork-composer-inset, but contentInsetEndAdjustment already pads the transcript by exactly that inset — so the whole ramp lands on live content rather than the pad. At the default live-follow position the final ~80px (3–4 lines) of the newest streaming message renders at ~60–95% alpha, in every fork palette.

3. Manifest text no longer matches the shipped values.fork/customizations.yaml:1139

The manifest says the guard pins "every stage stop >= 80% and never below the sidebar's", but the guard now checks only the top stop, and the shipped bottom stop (85%) is below the sidebar's 88%. The same paragraph still carries the under-window-era note that "usable range is roughly 60-90%, 91% renders as no glass" while this PR ships a 95% top stop.

4. Guard loosened on a false premiseapps/web/src/__fork_guards__/forkCoolDarkTheme.test.ts:220

The guard went from "no backdrop-filter on the chips" to "must be vibrancy-scoped", on a comment claiming the chips carry a filter under glass. They don't — the block declares none, and the old guard passing is proof no chip frost ever existed. The same false claim appears in the palettes.css comment at line 543.


Findings 1 and 2 are the ones with user-visible consequences; 3 and 4 are docs/guard drift that will mislead the next change in this area.

🤖 Generated with Claude Code

…cut the essays

- ForkSidebarVibrancy called setVibrancy through a local alias of the shared
  constant. Uses FORK_VIBRANCY_MATERIAL directly.
- The timeline mask's tail midpoints were bare arithmetic repeated in both the
  prefixed and unprefixed declaration, so the ramp's shape could only be read by
  evaluating four expressions. Named as tail-early / tail-late / tail-end beside
  the edge and ramp tokens.
- Trimmed the glass comments and the guard prose by roughly two thirds. Kept the
  traps that cost round trips to find — bg-background under the gradient, the
  grain utility's repeat and 128px size, backdrop-filter versus the native
  material, alphas not being portable across materials — and dropped the Figma
  walkthroughs, the superseded-value history and the per-token restatements that
  the manifest already carries.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added size:L and removed size:XL labels Aug 15, 2026
Upstream's glass utilities carry backdrop-filter for the same reason the
composer did, and hit the same wall: a filter cannot sample the native
material, so a menu opened over the sidebar or the stage renders that
patch as flat grey and reads as a card sitting on the glass.

.dropdown-glass, .dialog-glass, .alert-glass, .chat-composer-glass and
.dialog-backdrop are cleared under the glass marker, and the first four
are made opaque in the same rule by overriding --glass-opacity to 100%.
Each computes its fill from that variable, so this keeps the dropdown's
denser popover tint and the alert's variant colour while removing the
see-through part that has nothing good left to show. Without the blur a
translucent menu leaks the text underneath instead, which is worse than
the grey card: these are transient overlays whose job is to be read.
.dialog-backdrop keeps its own 60% dim; only its blur goes, since that
one covers the whole window.

The guard reads the class list out of index.css rather than hardcoding
it, so a new `*-glass` utility upstream fails the test instead of
shipping a grey card.

Also drops the composer's bottom offset from 32px to 24px.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added size:XL and removed size:L labels Aug 15, 2026
The 95%->85% stage gradient landed on [data-slot="sidebar-inset"], which
also roots Settings, Usage and the empty state. Those pages have no
transcript and no cutoff mask, so the bottom third of a scrolling text
panel faded to 85% under an 88% sidebar with nothing ending it. The ramp
is now scoped to insets carrying .fork-timeline-cutoff and everything
else keeps the flat reading value; chat is unchanged.

Also re-syncs two pieces of drift the change surfaced: the manifest
described a guard constraint the guard had stopped enforcing and still
carried under-window-era alpha ranges next to a 95% top stop, and the
chip guard had been loosened to "must be vibrancy-scoped" on a comment
claiming the chips carry a filter under glass. They do not — the rule
declares none. It now matches the value instead of the property, which
accepts that rule and still fails on a real blur.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@NoahHendrickson
NoahHendrickson merged commit feaf628 into custom Aug 15, 2026
11 checks passed
@NoahHendrickson
NoahHendrickson deleted the t3code/start-mac-dev-app branch August 15, 2026 18:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant