diff --git a/apps/desktop/src/app/overlays/overlay-view.tsx b/apps/desktop/src/app/overlays/overlay-view.tsx index 5ff4810004..6830c20796 100644 --- a/apps/desktop/src/app/overlays/overlay-view.tsx +++ b/apps/desktop/src/app/overlays/overlay-view.tsx @@ -98,6 +98,10 @@ export function OverlayView({ 'relative flex h-full min-h-0 flex-col overflow-hidden rounded-xl border border-(--ui-stroke-secondary) bg-(--ui-chat-surface-background) shadow-md', rootClassName )} + // Marks the card as a RAISED surface for window glass: while the field + // behind it thins to show the desktop, this card stays near-opaque + // (see the [data-glass-raised] rules in styles.css). Inert otherwise. + data-glass-raised="" >
{headerContent && ( diff --git a/apps/desktop/src/store/translucency.test.ts b/apps/desktop/src/store/translucency.test.ts index 438f908ba8..8e73c5374f 100644 --- a/apps/desktop/src/store/translucency.test.ts +++ b/apps/desktop/src/store/translucency.test.ts @@ -105,6 +105,22 @@ describe('window translucency lever', () => { setTranslucencyMode('clear') expect(document.documentElement.hasAttribute('data-hermes-glass')).toBe(false) }) + + it('marks clear mode on so the overlay scrim can compensate', () => { + setTranslucency(50) + setTranslucencyMode('clear') + expect(document.documentElement.hasAttribute('data-hermes-clear')).toBe(true) + + setTranslucency(0) + expect(document.documentElement.hasAttribute('data-hermes-clear')).toBe(false) + + setTranslucency(50) + + if (GLASS_SUPPORTED) { + setTranslucencyMode('glass') + expect(document.documentElement.hasAttribute('data-hermes-clear')).toBe(false) + } + }) }) describe('glassSurfaceKeep', () => { diff --git a/apps/desktop/src/store/translucency.ts b/apps/desktop/src/store/translucency.ts index 33dd27920c..587617dd6e 100644 --- a/apps/desktop/src/store/translucency.ts +++ b/apps/desktop/src/store/translucency.ts @@ -86,6 +86,11 @@ const applyGlassSurfaces = (intensity: number, mode: TranslucencyMode): void => const root = document.documentElement const on = mode === 'glass' && intensity > 0 && GLASS_SUPPORTED && isChatWindow() + // Clear mode fades the whole window uniformly, so overlay text and the + // covered transcript blend; styles.css strengthens the overlay scrim while + // this attribute is present. Native opacity applies in every window kind, + // so no chat-window gate. + const clearOn = mode === 'clear' && intensity > 0 if (on) { root.setAttribute('data-hermes-glass', '') @@ -94,6 +99,12 @@ const applyGlassSurfaces = (intensity: number, mode: TranslucencyMode): void => root.removeAttribute('data-hermes-glass') root.style.removeProperty('--translucency-glass-keep') } + + if (clearOn) { + root.setAttribute('data-hermes-clear', '') + } else { + root.removeAttribute('data-hermes-clear') + } } if (typeof window !== 'undefined') { diff --git a/apps/desktop/src/styles.css b/apps/desktop/src/styles.css index 362f4985c6..5b57159241 100644 --- a/apps/desktop/src/styles.css +++ b/apps/desktop/src/styles.css @@ -562,18 +562,20 @@ Set on by store/translucency.ts; macOS chat windows only. Every chat window already carries an NSVisualEffectView (`vibrancy: 'sidebar'` in electron/main.ts) that WindowServer composites BELOW the web contents; - the page normally buries it under fully opaque surfaces. Glass thins - exactly the two field surfaces — the chat surface and the sidebar — to + the page normally buries it under fully opaque surfaces. + + ONE PAINTER: paints the glass tint exactly once, at `--translucency-glass-keep` (set inline from the intensity slider, floor - 30%) so the material shows through as a matte desktop blur, while cards, - popovers, the composer and every raised control keep their opaque fills - and text keeps full contrast. Unlike Clear, nothing on the page fades. - Surfaces that MASK in-page content are deliberately not thinned: the - editor surface backs the diff gutter's sticky line-number mask and the - active pane tab, and the find bar reads --ui-surface-background — - in-page transparency there reads as text bleeding through text, not as - glass. The two thinned surfaces already cover the window, so the pane - container behind them never shows. + 30%), and the field tokens (chat / sidebar / editor surface) go fully + transparent. The field surfaces NEST — body > pane container > chat + section > transcript wrapper all wear these tokens — so thinning the + tokens themselves stacks the tint once per layer and a session pane ends + up near-opaque (~0.93 at 60%) while the landing page, with fewer layers, + reads far clearer. With a single painter the field alpha is the same + number on every route. Raised content (cards, popovers, bubbles, the + composer) keeps its own opaque fills over full-contrast text; regions + still read distinct through their borders and the material itself. + Unlike Clear, nothing on the page fades. The boot script in index.html pins an opaque inline background on before first paint; it has to go transparent here or it sits behind the @@ -583,17 +585,69 @@ which does not composite page alpha. */ :root[data-hermes-glass] { background-color: transparent !important; + --ui-chat-surface-background: transparent; + --ui-sidebar-surface-background: transparent; + --ui-editor-surface-background: transparent; + --ui-terminal-surface-background: var(--ui-bg-chrome); + } + + :root[data-hermes-glass] body { + background: color-mix(in srgb, var(--ui-bg-chrome) var(--translucency-glass-keep, 100%), transparent); + } + + /* Surfaces that MASK sibling content must stay filled under glass — a + see-through mask reads as text bleeding through text, not as glass. The + diff panel's sticky line-number gutter wears the editor surface + (components/chat/diff-lines.tsx), and a dragged sidebar row wears the + sidebar surface precisely so the rows it passes over don't show through + (see the comment in sidebar/session-row.tsx). Both get an opaque fill + back. */ + :root[data-hermes-glass] [data-slot='file-diff-panel'] { + --ui-editor-surface-background: var(--ui-bg-chrome); + } + + :root[data-hermes-glass] .cursor-grabbing { + --ui-sidebar-surface-background: var(--ui-bg-sidebar); + } + + /* The inline user-message edit box wears the chat surface as a raised box + fill; give it the opaque chrome the token resolves to outside glass. */ + :root[data-hermes-glass] .composer-human-message-container { + --ui-chat-surface-background: var(--ui-bg-chrome); + } + + /* Overlay cards (settings, agents, cron, command center — everything on + OverlayView) sit ABOVE the field, so they run counter to it: near-opaque + over a glass field, never thinner than the field itself (max() covers + low slider values where the field keeps more than 94%). The card wears + the chat token and its internal nav asides wear the sidebar token, so + both come back as almost-solid fills and the two-tone layout survives. */ + :root[data-hermes-glass] [data-glass-raised] { --ui-chat-surface-background: color-mix( in srgb, - var(--ui-bg-chrome) var(--translucency-glass-keep, 100%), + var(--ui-bg-chrome) max(94%, var(--translucency-glass-keep, 100%)), transparent ); --ui-sidebar-surface-background: color-mix( in srgb, - var(--ui-bg-sidebar) var(--translucency-glass-keep, 100%), + var(--ui-bg-sidebar) max(94%, var(--translucency-glass-keep, 100%)), transparent ); - --ui-terminal-surface-background: var(--ui-bg-chrome); + --ui-editor-surface-background: var(--ui-bg-chrome); + } + + /* Clear mode (native window opacity) fades EVERYTHING in the window + uniformly, so when an overlay opens over a session two layers of text + fade by the same amount and fight through the overlay's light default + scrim (bg-black/22 + 2px blur). While clear is active, the scrim works + for a living: darken it and widen the blur so the covered transcript + drops away and the overlay's own text is the only line of type in front + of the desktop. Set on by store/translucency.ts whenever + mode='clear' and intensity > 0; !important because the utility classes + live in a later cascade layer. */ + :root[data-hermes-clear] [data-overlay-surface] { + background-color: rgb(0 0 0 / 0.55) !important; + backdrop-filter: blur(0.5rem) !important; } * {