mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-06 17:57:43 +02:00
[eric] browser: one safe capture helper; a half-ready webview no longer crashes the app
This commit is contained in:
@@ -7,7 +7,7 @@ import GridViewRoundedIcon from '@mui/icons-material/GridViewRounded';
|
||||
import OpenInFullRoundedIcon from '@mui/icons-material/OpenInFullRounded';
|
||||
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
|
||||
import { clearTiledCard, focusBrowserCard, focusViewCard } from '@/shared/state/dashboardLayoutSlice';
|
||||
import { getWebview } from '@/shared/browserRegistry';
|
||||
import { captureBrowserShot } from '@/shared/captureBrowserShot';
|
||||
import type { ClaudeTokens } from '@/shared/styles/claudeTokens';
|
||||
|
||||
// The desktop-redesign frame: the surfaces an agent is driving live INSIDE its chat. Each linked
|
||||
@@ -23,17 +23,8 @@ function useBrowserSnapshot(browserId: string, live: boolean): string | null {
|
||||
let dead = false;
|
||||
let timer = 0;
|
||||
const grab = async (): Promise<void> => {
|
||||
try {
|
||||
const wv = getWebview(browserId) as unknown as { capturePage?: () => Promise<{ toDataURL(): string }> } | undefined;
|
||||
if (wv?.capturePage) {
|
||||
// capturePage can hang on off-screen guests (Electron 42); race a timeout so the poll never wedges.
|
||||
const img = await Promise.race([
|
||||
wv.capturePage(),
|
||||
new Promise<null>((res) => window.setTimeout(() => res(null), 1200)),
|
||||
]);
|
||||
if (!dead && img) setShot(img.toDataURL());
|
||||
}
|
||||
} catch { /* snapshot is best-effort */ }
|
||||
const img = await captureBrowserShot(browserId);
|
||||
if (!dead && img) setShot(img);
|
||||
// Live sessions repaint forever; idle ones retry a few times so a webview that mounts late
|
||||
// (fullscreen hiding the canvas, load races) still yields one real frame instead of a blank.
|
||||
tries += 1;
|
||||
|
||||
@@ -43,7 +43,7 @@ import { openCardContextMenu } from '../desktop/CardContextMenu';
|
||||
import { extractLatestTodos } from '../desktop/agentTodos';
|
||||
import { extractLatestShowUi, extractPendingAskUi, freezeIfDone } from '@/app/pages/AgentChat/tool-ui/showUiPayload';
|
||||
import { useDragEndBackstops } from '../hooks/interaction/useDragEndBackstops';
|
||||
import { getWebview } from '@/shared/browserRegistry';
|
||||
import { captureBrowserShot } from '@/shared/captureBrowserShot';
|
||||
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
|
||||
import { QuestionForm } from '@/app/pages/AgentChat/shell/ApprovalBar';
|
||||
import AgentChat from '@/app/pages/AgentChat/AgentChat';
|
||||
@@ -754,13 +754,9 @@ const AgentCard: React.FC<Props> = ({
|
||||
}
|
||||
let cancelled = false;
|
||||
const capture = (): void => {
|
||||
const wv = getWebview(spawnedBrowserId);
|
||||
const p = wv?.capturePage?.();
|
||||
if (p && typeof (p as Promise<unknown>).then === 'function') {
|
||||
(p as Promise<{ toDataURL(): string }>)
|
||||
.then((img) => { if (!cancelled) setBrowserShot(img.toDataURL()); })
|
||||
.catch(() => undefined);
|
||||
}
|
||||
void captureBrowserShot(spawnedBrowserId).then((shot) => {
|
||||
if (!cancelled && shot) setBrowserShot(shot);
|
||||
});
|
||||
};
|
||||
capture();
|
||||
// Refresh while the agent is driving so the shot tracks the page; parked cards keep the last frame.
|
||||
|
||||
@@ -8,7 +8,7 @@ import { openSettingsCard, openWorkflowsApp } from '@/shared/state/dashboardLayo
|
||||
import SettingsIcon from '@mui/icons-material/Settings';
|
||||
import AppsRoundedIcon from '@mui/icons-material/AppsRounded';
|
||||
import { useAppDispatch } from '@/shared/hooks';
|
||||
import { getWebview } from '@/shared/browserRegistry';
|
||||
import { captureBrowserShot } from '@/shared/captureBrowserShot';
|
||||
import { buildDockEntries, CardRect, DockEntry } from './dockEntries';
|
||||
import type { AgentSession } from '@/shared/state/agentsSlice';
|
||||
import type {
|
||||
@@ -111,13 +111,10 @@ function DesktopDock({
|
||||
hoverTimer.current = window.setTimeout(() => {
|
||||
setHovered({ id: entry.id, top });
|
||||
if (entry.browserId) {
|
||||
const wv = getWebview(entry.browserId);
|
||||
const capture = wv?.capturePage?.();
|
||||
if (capture && typeof (capture as Promise<unknown>).then === 'function') {
|
||||
(capture as Promise<{ toDataURL(): string }>)
|
||||
.then((img) => setLiveShot({ id: entry.id, dataUrl: img.toDataURL() }))
|
||||
.catch(() => undefined);
|
||||
}
|
||||
const entryId = entry.id;
|
||||
void captureBrowserShot(entry.browserId).then((shot) => {
|
||||
if (shot) setLiveShot({ id: entryId, dataUrl: shot });
|
||||
});
|
||||
}
|
||||
}, 220);
|
||||
},
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { getWebview } from './browserRegistry';
|
||||
|
||||
// One place for "get me a picture of a browser card, best effort". Four callers were hand-rolling
|
||||
// it and three got it wrong in a different way, because capturePage fails in two shapes: it THROWS
|
||||
// synchronously on a webview that is attached but not yet dom-ready (unguarded, that one took the
|
||||
// whole dashboard down through AgentCard's ErrorBoundary), and it never settles at all on a guest
|
||||
// the compositor is not drawing, so the timeout is not optional either.
|
||||
const DEFAULT_SHOT_TIMEOUT_MS = 1200;
|
||||
|
||||
/** A data URL of the card's live page, or null. Never throws, never hangs. */
|
||||
export async function captureBrowserShot(browserId: string, timeoutMs: number = DEFAULT_SHOT_TIMEOUT_MS): Promise<string | null> {
|
||||
try {
|
||||
const wv = getWebview(browserId);
|
||||
if (!wv?.capturePage) return null;
|
||||
const image = await Promise.race([
|
||||
wv.capturePage(),
|
||||
new Promise<null>((resolve) => { window.setTimeout(() => resolve(null), timeoutMs); }),
|
||||
]);
|
||||
return image && !image.isEmpty() ? image.toDataURL() : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user