mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-18 02:35:41 +02:00
[eric] browser: unwedge off-screen webview suspend (capturePage hang) + glow-leak liveness gate
This commit is contained in:
@@ -39,19 +39,21 @@ function cardIntersectsViewport(card: BrowserCardPosition, vp: Viewport, marginP
|
||||
return card.x < vx + vw && card.x + card.width > vx && card.y < vy + vh && card.y + card.height > vy;
|
||||
}
|
||||
|
||||
function sessionIsWorking(s: { status?: string } | undefined): boolean {
|
||||
return !!s && (s.status === 'running' || s.status === 'waiting_approval');
|
||||
}
|
||||
|
||||
function agentNeedsLive(browserId: string, card: BrowserCardPosition): boolean {
|
||||
if (getActivity(browserId)) return true;
|
||||
const state = store.getState();
|
||||
const glow = state.dashboardLayout.glowingBrowserCards[browserId];
|
||||
if (glow && !glow.fading) return true;
|
||||
const sessions = state.agents.sessions as Record<string, any>;
|
||||
// A glow holds the card live only while its SOURCE session is still working: a stuck glow (chat unmounted at finish never fades it) must not pin a renderer forever.
|
||||
const glow = state.dashboardLayout.glowingBrowserCards[browserId];
|
||||
if (glow && !glow.fading && sessionIsWorking(sessions[glow.sourceId])) return true;
|
||||
for (const s of Object.values(sessions)) {
|
||||
if (s.browser_id === browserId && (s.status === 'running' || s.status === 'waiting_approval')) return true;
|
||||
}
|
||||
if (card.spawned_by) {
|
||||
const parent = sessions[card.spawned_by];
|
||||
if (parent && (parent.status === 'running' || parent.status === 'waiting_approval')) return true;
|
||||
if (s.browser_id === browserId && sessionIsWorking(s)) return true;
|
||||
}
|
||||
if (card.spawned_by && sessionIsWorking(sessions[card.spawned_by])) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -128,9 +130,10 @@ export function useWebviewSuspend(
|
||||
if (isSuspended(id)) continue;
|
||||
if (cardIntersectsViewport(card, vpRef.current, SUSPEND_MARGIN_PX)) 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 captureCard(id, card);
|
||||
// The capture await yielded; conditions may have changed under us.
|
||||
if (!dataUrl || cardIntersectsViewport(card, vpRef.current, SUSPEND_MARGIN_PX) || mustStayLive(id, card)) continue;
|
||||
if (cardIntersectsViewport(card, vpRef.current, SUSPEND_MARGIN_PX) || mustStayLive(id, card)) continue;
|
||||
dispatch(suspendBrowserCard({ browserId: id, dataUrl }));
|
||||
}
|
||||
|
||||
@@ -142,7 +145,7 @@ export function useWebviewSuspend(
|
||||
for (const [id, card] of candidates) {
|
||||
if (countLive() <= MAX_LIVE_WEBVIEWS) break;
|
||||
const dataUrl = await captureCard(id, card);
|
||||
if (!dataUrl || mustStayLive(id, card)) continue;
|
||||
if (mustStayLive(id, card)) continue;
|
||||
dispatch(suspendBrowserCard({ browserId: id, dataUrl }));
|
||||
}
|
||||
}
|
||||
@@ -160,19 +163,25 @@ function distFromCenter(card: BrowserCardPosition, vp: Viewport): number {
|
||||
return dx * dx + dy * dy;
|
||||
}
|
||||
|
||||
async function captureCard(id: string, card: BrowserCardPosition): Promise<string | null> {
|
||||
// capturePage on an already-off-screen webview can HANG forever (Electron 42/Viz stops producing frames for unpainted guests), and one hung await used to wedge the whole suspend pass, silently disabling suspension for every card. Bound it hard.
|
||||
const CAPTURE_TIMEOUT_MS = 1500;
|
||||
|
||||
async function captureCard(id: string, card: BrowserCardPosition): Promise<string> {
|
||||
const wv = getWebview(id, card.activeTabId);
|
||||
if (!wv) return null;
|
||||
if (!wv) return '';
|
||||
try {
|
||||
if (wv.isLoading()) return null;
|
||||
if (wv.isLoading()) return '';
|
||||
const url = wv.getURL();
|
||||
if (!url || url === 'about:blank') return null;
|
||||
const image = await wv.capturePage();
|
||||
if (image.isEmpty()) return null;
|
||||
if (!url || url === 'about:blank') return '';
|
||||
const image = await Promise.race([
|
||||
wv.capturePage(),
|
||||
new Promise<null>((resolve) => setTimeout(() => resolve(null), CAPTURE_TIMEOUT_MS)),
|
||||
]);
|
||||
if (!image || image.isEmpty()) return '';
|
||||
return image.getSize().width > SNAPSHOT_MAX_W
|
||||
? image.resize({ width: SNAPSHOT_MAX_W, quality: 'good' }).toDataURL()
|
||||
: image.toDataURL();
|
||||
} catch {
|
||||
return null;
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
clearTurnLabel,
|
||||
} from '../state/agentsSlice';
|
||||
import { streamStart, streamDelta, streamEnd, clearStreamingForSession } from '../state/streamingSlice';
|
||||
import { addBrowserCardFromBackend, markBrowserCardEnding, keepBrowserCardOpen, placeBesideCard, placeBelowCard, setBrowserCardPosition, setGlowingBrowserCards, GRID_GAP, WORKFLOW_CARD_GAP, openWorkflowsApp, openWorkflowMonitor } from '../state/dashboardLayoutSlice';
|
||||
import { addBrowserCardFromBackend, markBrowserCardEnding, keepBrowserCardOpen, placeBesideCard, placeBelowCard, setBrowserCardPosition, setGlowingBrowserCards, fadeGlowingBrowserCards, clearGlowingBrowserCards, GRID_GAP, WORKFLOW_CARD_GAP, openWorkflowsApp, openWorkflowMonitor } from '../state/dashboardLayoutSlice';
|
||||
import { upsertOutput } from '../state/outputsSlice';
|
||||
import { fetchSettings } from '../state/settingsSlice';
|
||||
import { displaySessionName } from '../state/sessionDisplay';
|
||||
@@ -384,6 +384,14 @@ class WebSocketManager {
|
||||
store.dispatch(trackAgentNotification(session_id));
|
||||
}
|
||||
|
||||
// Fade this session's browser glows on the terminal transition HERE, not only in AgentChat's effect: a collapsed chat is unmounted at finish, and a never-faded glow pins the browser's renderer (exempt from suspend + the webview cap) forever.
|
||||
const newStatus = data.status ?? data.session?.status;
|
||||
const wasWorking = prevStatus === 'running' || prevStatus === 'waiting_approval';
|
||||
if (session_id && wasWorking && (newStatus === 'completed' || newStatus === 'stopped' || newStatus === 'error')) {
|
||||
store.dispatch(fadeGlowingBrowserCards(session_id));
|
||||
setTimeout(() => store.dispatch(clearGlowingBrowserCards(session_id)), 600);
|
||||
}
|
||||
|
||||
// Fire a native notification when an agent terminates while the window is hidden. Skips sub-agents and browser-agents (the parent's own completion is what the user cares about) and only fires on a real transition from a non-terminal state.
|
||||
const TERMINAL = new Set(['completed', 'error']);
|
||||
const NON_TERMINAL = new Set(['running', 'waiting_approval', undefined, null, '']);
|
||||
|
||||
Reference in New Issue
Block a user