From 4097709b0ca59bb24658dfb0fe65f2ba7be7b3be Mon Sep 17 00:00:00 2001 From: teknium1 <127238744+teknium1@users.noreply.github.com> Date: Fri, 2 Oct 2026 00:39:44 -0700 Subject: [PATCH] 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. --- .../right-rail/preview-guest-offscreen.ts | 16 ++++++--- .../app/chat/right-rail/preview-pane.test.tsx | 34 +++++++++++++++++++ .../src/app/chat/right-rail/preview-pane.tsx | 6 ++-- 3 files changed, 50 insertions(+), 6 deletions(-) diff --git a/apps/desktop/src/app/chat/right-rail/preview-guest-offscreen.ts b/apps/desktop/src/app/chat/right-rail/preview-guest-offscreen.ts index 981ec163c7..2f6b94e84d 100644 --- a/apps/desktop/src/app/chat/right-rail/preview-guest-offscreen.ts +++ b/apps/desktop/src/app/chat/right-rail/preview-guest-offscreen.ts @@ -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, tabId?: string): void { +/** Reports and mutes the guest as it goes off screen. Returns a `dom-ready` + * listener the pane must attach to its ``: 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, 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) + const noteGuestReady = useCallback(() => setAttachedId(guestId(webviewRef.current)), [webviewRef]) useEffect(() => { const id = guestId(webviewRef.current) @@ -66,7 +72,7 @@ export function usePreviewGuestOffscreen(webviewRef: RefObject { const webview = webviewRef.current @@ -88,5 +94,7 @@ export function usePreviewGuestOffscreen(webviewRef: RefObject { 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 + + await act(async () => { + rendered = render( + + + + ) + }) + + 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) { diff --git a/apps/desktop/src/app/chat/right-rail/preview-pane.tsx b/apps/desktop/src/app/chat/right-rail/preview-pane.tsx index 94b9223d68..79449cb987 100644 --- a/apps/desktop/src/app/chat/right-rail/preview-pane.tsx +++ b/apps/desktop/src/app/chat/right-rail/preview-pane.tsx @@ -276,7 +276,7 @@ export function PreviewPane({ const lastRestartEventRef = useRef('') const previewContentRef = useRef(null) const webviewRef = useRef(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,