[eric] dashboard: minimizing a browser card actually frees its renderer (it used to keep a live one)

This commit is contained in:
ciregenz
2026-07-31 01:16:06 -07:00
parent 2572f9ff45
commit ee287534d0
@@ -10,6 +10,7 @@ import { getWebview } from '@/shared/browserRegistry';
import { getActivity, isAnyBrowserBusy } from '@/shared/browserCommandHandler';
import { isKeepAliveBrowser } from '@/shared/browserFocus';
import { captureTabCapsule } from '@/shared/browserStateCapsule';
import { getMinimizedShot } from '../../desktop/minimizedShots';
const isElectron = typeof navigator !== 'undefined' && navigator.userAgent.includes('Electron');
@@ -83,6 +84,12 @@ function cardIsAudible(browserId: string, card: BrowserCardPosition): boolean {
return false;
}
// Parked in the minimize rail: the card renders off-canvas behind a frozen still, so a live renderer
// sitting behind it is pure waste (same call DashboardViewCard already makes for app previews).
function isMinimized(browserId: string): boolean {
return !!store.getState().dashboardLayout.minimizedCards[browserId];
}
// A card we must never snapshot-swap: an agent is driving it, it's in the keep-alive set (recently used), or it's playing audio. Suspending destroys the webContents (sessionStorage, playback), the things we're preserving.
function mustStayLive(browserId: string, card: BrowserCardPosition): boolean {
// The shield class marks a live card/marquee drag: suspending a browser MID-DRAG unmounts the
@@ -107,6 +114,7 @@ export function useWebviewSuspend(
) {
const dispatch = useAppDispatch();
const suspended = useAppSelector((s) => s.dashboardLayout.suspendedBrowserCards);
const minimized = useAppSelector((s) => s.dashboardLayout.minimizedCards);
const vpRef = useRef<Viewport>({ panX, panY, zoom, vpW: 1200, vpH: 800 });
// Window resize changes the viewport without touching pan/zoom/cards; tick so the evaluation below reruns, or a shrunken window never suspends anything.
@@ -146,7 +154,7 @@ export function useWebviewSuspend(
budget--;
continue;
}
if (budget <= 0) continue;
if (budget <= 0 || minimized[id]) continue;
const bigEnough = card.width * zoom >= RESUME_MIN_CARD_PX;
if (bigEnough && cardIntersectsViewport(card, vpRef.current, RESUME_MARGIN_PX)) {
dispatch(resumeBrowserCard(id));
@@ -156,15 +164,18 @@ export function useWebviewSuspend(
const timer = setTimeout(async () => {
const isSuspended = (id: string) => !!store.getState().dashboardLayout.suspendedBrowserCards[id];
// Read live, not off the effect's closure: this re-runs after an await, and the user can restore a card mid-capture.
const wantsPark = (id: string, card: BrowserCardPosition): boolean =>
isMinimized(id) || !cardIntersectsViewport(card, vpRef.current, SUSPEND_MARGIN_PX);
await refreshVisibleFrames(browserCards, isSuspended, vpRef.current);
for (const [id, card] of Object.entries(browserCards)) {
if (isSuspended(id)) continue;
if (cardIntersectsViewport(card, vpRef.current, SUSPEND_MARGIN_PX)) continue;
if (!wantsPark(id, card)) continue;
if (mustStayLive(id, card)) continue;
// An empty dataUrl still suspends (placeholder renders): a card whose capture hangs/fails must not keep its renderer alive forever.
const dataUrl = await captureForSuspend(id, card);
// The capture await yielded; conditions may have changed under us.
if (cardIntersectsViewport(card, vpRef.current, SUSPEND_MARGIN_PX) || mustStayLive(id, card)) continue;
if (!wantsPark(id, card) || mustStayLive(id, card)) continue;
dispatch(suspendBrowserCard({ browserId: id, dataUrl }));
}
@@ -183,7 +194,7 @@ export function useWebviewSuspend(
}, SETTLE_MS);
return () => clearTimeout(timer);
}, [browserCards, suspended, panX, panY, zoom, viewportRef, dispatch, resizeTick]);
}, [browserCards, suspended, minimized, panX, panY, zoom, viewportRef, dispatch, resizeTick]);
}
function distFromCenter(card: BrowserCardPosition, vp: Viewport): number {
@@ -219,6 +230,7 @@ async function refreshVisibleFrames(
if (isAnyBrowserBusy()) return;
for (const [id, card] of Object.entries(cards)) {
if (isSuspended(id)) continue;
if (isMinimized(id)) continue;
if (!cardIntersectsViewport(card, vp, 0)) continue;
const prev = lastFrames.get(id);
if (prev && Date.now() - prev.at < FRAME_TTL_MS) continue;
@@ -232,12 +244,14 @@ async function captureForSuspend(id: string, card: BrowserCardPosition): Promise
for (const tab of card.tabs ?? []) {
await captureTabCapsule(getWebview(id, tab.id), tab.id);
}
const live = await captureCard(id, card);
// A minimized card is already parked off-canvas and will never paint again, so asking it for a frame
// just burns the capture timeout; the shot frozen on the way in is the only real one it has.
const live = isMinimized(id) ? '' : await captureCard(id, card);
if (live) {
rememberFrame(id, live);
return live;
}
return lastFrames.get(id)?.dataUrl ?? '';
return lastFrames.get(id)?.dataUrl ?? getMinimizedShot(id) ?? '';
}
async function captureCard(id: string, card: BrowserCardPosition): Promise<string> {