Merge pull request #81920 from NousResearch/bb/hud-frost-backing

HUD: only frost the part of the window the transcript is behind
This commit is contained in:
brooklyn! 2026-08-08 13:16:04 -05:00 committed by GitHub
commit de1f370f9c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 21 additions and 9 deletions

View File

@ -30,8 +30,14 @@ const TYPING_SELECTOR = '[data-slot="composer-rich-input"]:focus'
* read as sitting down to use it. Queried live rather than tracked from
* document.activeElement, which stays put when the window is blurred and would
* latch the frost on forever once the user had ever typed here.
*
* `backing` is the veto over both of those. Because the frost is the window and
* not the sheet, it is only ever right when the sheet covers the window; short
* of that the excess is frost over empty space. Gating the caller's `engaged`
* alone would not do it focus turns the frost on by itself, which is how a
* brand new thread still frosted its whole empty window.
*/
export function useHudGlass(rootRef: RefObject<HTMLElement | null>, engaged: boolean): void {
export function useHudGlass(rootRef: RefObject<HTMLElement | null>, engaged: boolean, backing: boolean): void {
useEffect(() => {
const root = rootRef.current
const setVibrancy = window.hermesDesktop?.hud?.setVibrancy
@ -43,7 +49,7 @@ export function useHudGlass(rootRef: RefObject<HTMLElement | null>, engaged: boo
let on: boolean | null = null
const apply = () => {
const next = engaged || root.querySelector(TYPING_SELECTOR) !== null
const next = backing && (engaged || root.querySelector(TYPING_SELECTOR) !== null)
if (on !== next) {
on = next
@ -60,5 +66,5 @@ export function useHudGlass(rootRef: RefObject<HTMLElement | null>, engaged: boo
root.removeEventListener('focusin', apply)
root.removeEventListener('focusout', apply)
}
}, [engaged, rootRef])
}, [backing, engaged, rootRef])
}

View File

@ -9,7 +9,7 @@ import { useI18n } from '@/i18n'
import { chatMessageText } from '@/lib/chat-messages'
import { closeHud } from '@/store/hud'
import { $activeSessionAwaitingInput } from '@/store/prompts'
import { $busy, $messages, $messagesEmpty } from '@/store/session'
import { $busy, $messages } from '@/store/session'
import { WiredPane } from '../contrib/wiring'
import { titlebarButtonClass } from '../shell/titlebar'
@ -156,10 +156,6 @@ export function HudShell() {
const { t } = useI18n()
const [recent, holdBand] = useRecentActivity()
const held = useHudHeld()
// Nothing to back yet. Vibrancy is the window's whole content view, so on a
// fresh thread it frosts the empty space above the bar into a blank slab —
// the transcript's backing showing up before the transcript does.
const empty = useStore($messagesEmpty)
// Main holds the session id on this window's behalf, so leaving HUD mode can
// hand the app window back whatever conversation ended up here.
@ -223,6 +219,13 @@ export function HudShell() {
// carve-out (styles.css): a band with nothing to scroll stays part of the
// window's drag region, so a short conversation never blocks moving the HUD.
const [scrollable, setScrollable] = useState(false)
// Whether the sheet reaches the top of the window. Gates the frost, which is
// native vibrancy and therefore the WINDOW's content view — it fills the whole
// rectangle and nothing in the page can clip it to the sheet. Whenever the
// sheet is shorter than the window, the difference is frost over empty space:
// a grey slab hanging under the bar with nothing in it, worst on a fresh
// thread where the sheet is zero and the slab is the entire window.
const [filled, setFilled] = useState(false)
const rootRef = useRef<HTMLDivElement | null>(null)
useEffect(() => {
@ -274,6 +277,9 @@ export function HudShell() {
: box.bottom - rows[0].getBoundingClientRect().top)
root.style.setProperty('--hud-band-height', `${Math.max(0, Math.round(span))}px`)
// The sheet is capped at the window (`min(100%, …)`), so reaching the
// window's height is the same question as covering it.
setFilled(span >= window.innerHeight)
// …and the bar's real height, which is what the thread has to clear.
// --composer-measured-height would be the obvious source, but it is a
@ -302,7 +308,7 @@ export function HudShell() {
}
}, [])
useHudGlass(rootRef, (recent || held) && !empty)
useHudGlass(rootRef, recent || held, filled)
useHudClickThrough(rootRef)
// Force the HOST layers transparent. index.html's pre-paint script writes an