fix(desktop): report and mute a Browser guest that attaches after its session is hidden

The off-screen hook only re-ran when hidden/parked changed, and a guest has
no webContents id until it attaches. A page whose session was parked between
mount and attach was never reported hidden to main (so mouse back/reload
could still target it) and never muted. The pane now feeds the guest's
dom-ready to the hook, which re-reports and re-mutes for the attached id.
This commit is contained in:
teknium1
2026-10-02 00:39:44 -07:00
committed by Teknium
parent 248091cd83
commit 4097709b0c
3 changed files with 50 additions and 6 deletions

View File

@@ -15,7 +15,7 @@
*/
import { useStore } from '@nanostores/react'
import { type RefObject, useEffect } from 'react'
import { type RefObject, useCallback, useEffect, useState } from 'react'
import { usePaneVisible } from '@/components/pane-shell/pane-visibility'
import { $parkedTreePanes } from '@/components/pane-shell/tree/parked-panes'
@@ -55,10 +55,16 @@ function muteGuest(webview: OffscreenGuest): boolean {
}
}
export function usePreviewGuestOffscreen(webviewRef: RefObject<null | OffscreenGuest>, tabId?: string): void {
/** Reports and mutes the guest as it goes off screen. Returns a `dom-ready`
* listener the pane must attach to its `<webview>`: a guest has no id until it
* attaches, and one that attaches while its session is already hidden would
* otherwise never be reported or muted (the effects only re-run on change). */
export function usePreviewGuestOffscreen(webviewRef: RefObject<null | OffscreenGuest>, tabId?: string): () => void {
const hidden = !usePaneVisible()
const parkedPanes = useStore($parkedTreePanes)
const parked = Boolean(tabId) && parkedPanes.has(`${PREVIEW_TILE_PREFIX}:${tabId}`)
const [attachedId, setAttachedId] = useState<null | number>(null)
const noteGuestReady = useCallback(() => setAttachedId(guestId(webviewRef.current)), [webviewRef])
useEffect(() => {
const id = guestId(webviewRef.current)
@@ -66,7 +72,7 @@ export function usePreviewGuestOffscreen(webviewRef: RefObject<null | OffscreenG
if (id !== null) {
window.hermesDesktop?.setPreviewGuestHidden?.(id, hidden)
}
}, [hidden, webviewRef])
}, [attachedId, hidden, webviewRef])
useEffect(() => {
const webview = webviewRef.current
@@ -88,5 +94,7 @@ export function usePreviewGuestOffscreen(webviewRef: RefObject<null | OffscreenG
// The guest went away while parked; nothing to restore.
}
}
}, [parked, webviewRef])
}, [attachedId, parked, webviewRef])
return noteGuestReady
}

View File

@@ -945,6 +945,40 @@ describe('PreviewPane off-screen guest', () => {
expect(setPreviewGuestHidden).toHaveBeenLastCalledWith(41, false)
})
// A guest has no id until it attaches; one that attaches after its session
// was already parked must still be reported to main and muted.
it('reports and mutes a guest that attaches while its session is already hidden', async () => {
const setPreviewGuestHidden = vi.fn()
desktopWindow.hermesDesktop = { setPreviewGuestHidden } as unknown as Window['hermesDesktop']
act(() => setTreePaneParked('preview-tile:url:late', true))
let rendered!: ReturnType<typeof render>
await act(async () => {
rendered = render(
<PaneVisibleContext value={false}>
<PreviewPane tabId="url:late" target={target} />
</PaneVisibleContext>
)
})
let muted = false
const webview = rendered.container.querySelector('webview')!
Object.assign(webview, {
getWebContentsId: () => 52,
isAudioMuted: () => muted,
setAudioMuted: (next: boolean) => (muted = next)
})
await act(async () => {
webview.dispatchEvent(new Event('dom-ready'))
})
expect(setPreviewGuestHidden).toHaveBeenLastCalledWith(52, true)
expect(muted).toBe(true)
act(() => setTreePaneParked('preview-tile:url:late', false))
})
// A hidden session's kept page keeps running, but is not heard from the chat
// the user switched to; it comes back with the sound it had.
async function renderAudibleGuest(tabId: string, mutedBefore: boolean) {

View File

@@ -276,7 +276,7 @@ export function PreviewPane({
const lastRestartEventRef = useRef('')
const previewContentRef = useRef<HTMLDivElement | null>(null)
const webviewRef = useRef<PreviewWebview | null>(null)
usePreviewGuestOffscreen(webviewRef, tabId)
const noteGuestReady = usePreviewGuestOffscreen(webviewRef, tabId)
const previewServerRestart = useStore($previewServerRestart)
const consoleHeight = useStore(consoleState.$height)
const consoleOpen = useStore(consoleState.$open)
@@ -1361,6 +1361,7 @@ export function PreviewPane({
webview.addEventListener('page-title-updated', notePage)
// #101880: never let a guest reach the native print panel.
webview.addEventListener('dom-ready', armPrintGuard)
webview.addEventListener('dom-ready', noteGuestReady)
host.appendChild(webview)
webviewRef.current = webview
@@ -1378,10 +1379,11 @@ export function PreviewPane({
webview.removeEventListener('did-stop-loading', onStop)
webview.removeEventListener('page-title-updated', notePage)
webview.removeEventListener('dom-ready', armPrintGuard)
webview.removeEventListener('dom-ready', noteGuestReady)
webview.remove()
setAnnotate(session => (session.mode ? { ...endAnnotateMode(session), stack: emptyAnnotateStack() } : session))
}
}, [appendConsoleEntry, consoleState, copy, isRemoteHtml, isWebPreview, tabId, target.kind])
}, [appendConsoleEntry, consoleState, copy, isRemoteHtml, isWebPreview, noteGuestReady, tabId, target.kind])
// Steers the LIVE guest when the session opens a new URL (#120265): loadURL
// keeps the webview instance (JS state, cookies, form data, scroll, refs,