mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-12 04:37:44 +02:00
[aidan] bug: browser card fade + keep on agent end
This commit is contained in:
@@ -26,6 +26,7 @@ import {
|
||||
setBrowserCardSize,
|
||||
removeBrowserCard,
|
||||
resumeBrowserCard,
|
||||
cancelBrowserCardEnding,
|
||||
addBrowserTab,
|
||||
removeBrowserTab,
|
||||
setActiveBrowserTab,
|
||||
@@ -219,6 +220,7 @@ const BrowserCard: React.FC<Props> = ({
|
||||
const browserAgentSession = useAppSelector(selectBrowserAgentSession);
|
||||
|
||||
const suspendedSnap = useAppSelector((state) => state.dashboardLayout.suspendedBrowserCards[browserId]);
|
||||
const endingState = useAppSelector((state) => state.dashboardLayout.endingBrowserCards[browserId]);
|
||||
|
||||
// Arm the Windows webview crash-safety marker synchronously, before React commits
|
||||
// the <webview> below. Cleared on dom-ready; a leftover marker next launch tells
|
||||
@@ -236,6 +238,7 @@ const BrowserCard: React.FC<Props> = ({
|
||||
const [tabLocalStates, setTabLocalStates] = useState<Record<string, TabLocalState>>({});
|
||||
// Electron webviews can't trigger OS platform auth; preload sends "passkey-detected" and we explain via modal.
|
||||
const [passkeyDialogOpen, setPasskeyDialogOpen] = useState(false);
|
||||
const [crashedTabs, setCrashedTabs] = useState<Set<string>>(new Set());
|
||||
const updateTabLocal = useCallback((tabId: string, update: Partial<TabLocalState>) => {
|
||||
setTabLocalStates((prev) => {
|
||||
const existing = prev[tabId] ?? { loading: false, canGoBack: false, canGoForward: false };
|
||||
@@ -269,6 +272,17 @@ const BrowserCard: React.FC<Props> = ({
|
||||
if (suspendedSnap) initializedTabs.current.clear();
|
||||
}, [suspendedSnap]);
|
||||
|
||||
// Spawned cards get marked "ending" by WebSocketManager when the parent agent
|
||||
// finishes; show the fade pill for ~3s, then dispatch the real remove. Keep
|
||||
// clears the flag and the cleanup below cancels the pending remove.
|
||||
useEffect(() => {
|
||||
if (!endingState) return;
|
||||
const timer = setTimeout(() => {
|
||||
dispatch(removeBrowserCard(browserId));
|
||||
}, 3000);
|
||||
return () => clearTimeout(timer);
|
||||
}, [endingState, browserId, dispatch]);
|
||||
|
||||
const tabIdKey = tabs.map((t) => t.id).join(',');
|
||||
useEffect(() => {
|
||||
if (!isElectron) return;
|
||||
@@ -342,6 +356,20 @@ const BrowserCard: React.FC<Props> = ({
|
||||
updateTabLocal(tabId, { loading: false });
|
||||
onNavigate();
|
||||
onTitleUpdate();
|
||||
setCrashedTabs((prev) => {
|
||||
if (!prev.has(tabId)) return prev;
|
||||
const next = new Set(prev);
|
||||
next.delete(tabId);
|
||||
return next;
|
||||
});
|
||||
};
|
||||
const onProcessGone = () => {
|
||||
setCrashedTabs((prev) => {
|
||||
if (prev.has(tabId)) return prev;
|
||||
const next = new Set(prev);
|
||||
next.add(tabId);
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const onFaviconUpdate = (e: any) => {
|
||||
@@ -366,6 +394,8 @@ const BrowserCard: React.FC<Props> = ({
|
||||
wv.addEventListener('page-favicon-updated', onFaviconUpdate);
|
||||
wv.addEventListener('ipc-message', onIpcMessage as any);
|
||||
wv.addEventListener('new-window', onNewWindow as any);
|
||||
wv.addEventListener('render-process-gone', onProcessGone as any);
|
||||
wv.addEventListener('crashed', onProcessGone as any);
|
||||
|
||||
cleanups.push(() => {
|
||||
unregisterWebview(browserId, tabId);
|
||||
@@ -377,6 +407,8 @@ const BrowserCard: React.FC<Props> = ({
|
||||
wv.removeEventListener('page-favicon-updated', onFaviconUpdate);
|
||||
wv.removeEventListener('ipc-message', onIpcMessage as any);
|
||||
wv.removeEventListener('new-window', onNewWindow as any);
|
||||
wv.removeEventListener('render-process-gone', onProcessGone as any);
|
||||
wv.removeEventListener('crashed', onProcessGone as any);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1119,31 +1151,104 @@ const BrowserCard: React.FC<Props> = ({
|
||||
</Box>
|
||||
)
|
||||
) : (
|
||||
tabs.map((tab) => (
|
||||
<webview
|
||||
key={tab.id}
|
||||
ref={(el: any) => {
|
||||
if (el) webviewMap.current.set(tab.id, el as unknown as WebviewElement);
|
||||
else webviewMap.current.delete(tab.id);
|
||||
}}
|
||||
data-tab-id={tab.id}
|
||||
src="about:blank"
|
||||
{...({ allowpopups: 'true' } as any) /* React drops boolean-valued unknown attrs, so string it stays; @types/react wrongly says boolean */}
|
||||
useragent={chromeUserAgent}
|
||||
{...(webviewPreloadPath ? { preload: webviewPreloadPath } : {})}
|
||||
webpreferences="plugins=yes, autoplayPolicy=no-user-gesture-required, backgroundThrottling=no" /* throttling: guests get occlusion-suspended on their own even with the host's disable-renderer-backgrounding, freezing agent JS when the window is covered */
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
border: 'none',
|
||||
visibility: tab.id === activeTabId ? 'visible' : 'hidden',
|
||||
zIndex: tab.id === activeTabId ? 1 : 0,
|
||||
}}
|
||||
/>
|
||||
))
|
||||
<>
|
||||
{tabs.map((tab) => (
|
||||
<webview
|
||||
key={tab.id}
|
||||
ref={(el: any) => {
|
||||
if (el) webviewMap.current.set(tab.id, el as unknown as WebviewElement);
|
||||
else webviewMap.current.delete(tab.id);
|
||||
}}
|
||||
data-tab-id={tab.id}
|
||||
src="about:blank"
|
||||
{...({ allowpopups: 'true' } as any) /* React drops boolean-valued unknown attrs, so string it stays; @types/react wrongly says boolean */}
|
||||
useragent={chromeUserAgent}
|
||||
{...(webviewPreloadPath ? { preload: webviewPreloadPath } : {})}
|
||||
webpreferences="plugins=yes, autoplayPolicy=no-user-gesture-required, backgroundThrottling=no" /* throttling: guests get occlusion-suspended on their own even with the host's disable-renderer-backgrounding, freezing agent JS when the window is covered */
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
border: 'none',
|
||||
visibility: tab.id === activeTabId ? 'visible' : 'hidden',
|
||||
zIndex: tab.id === activeTabId ? 1 : 0,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
<Fade in={!!endingState && !crashedTabs.has(activeTabId)} timeout={{ enter: 200, exit: 220 }} unmountOnExit>
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
zIndex: 5,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 1.25,
|
||||
bgcolor: c.bg.surface,
|
||||
}}
|
||||
>
|
||||
<Typography sx={{ color: c.text.primary, fontSize: '0.95rem', fontWeight: 500 }}>
|
||||
{endingState?.status === 'error' ? 'Task ended with an error.' : 'Task done.'}
|
||||
</Typography>
|
||||
<Button
|
||||
onClick={() => dispatch(cancelBrowserCardEnding(browserId))}
|
||||
sx={{
|
||||
textTransform: 'none',
|
||||
fontSize: '0.82rem',
|
||||
fontWeight: 600,
|
||||
bgcolor: c.accent.primary,
|
||||
color: '#fff',
|
||||
borderRadius: `${c.radius.md}px`,
|
||||
px: 2.25,
|
||||
py: 0.6,
|
||||
'&:hover': { bgcolor: c.accent.hover || c.accent.primary },
|
||||
}}
|
||||
>
|
||||
Keep
|
||||
</Button>
|
||||
</Box>
|
||||
</Fade>
|
||||
<Fade in={crashedTabs.has(activeTabId)} timeout={{ enter: 200, exit: 220 }} unmountOnExit>
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
zIndex: 6,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 1.25,
|
||||
bgcolor: c.bg.surface,
|
||||
}}
|
||||
>
|
||||
<Typography sx={{ color: c.text.primary, fontSize: '0.95rem', fontWeight: 500 }}>
|
||||
This page stopped responding.
|
||||
</Typography>
|
||||
<Button
|
||||
onClick={() => webviewMap.current.get(activeTabId)?.reload()}
|
||||
startIcon={<RefreshIcon sx={{ fontSize: '1rem' }} />}
|
||||
sx={{
|
||||
textTransform: 'none',
|
||||
fontSize: '0.82rem',
|
||||
fontWeight: 600,
|
||||
bgcolor: c.accent.primary,
|
||||
color: '#fff',
|
||||
borderRadius: `${c.radius.md}px`,
|
||||
px: 2.25,
|
||||
py: 0.6,
|
||||
'&:hover': { bgcolor: c.accent.hover || c.accent.primary },
|
||||
}}
|
||||
>
|
||||
Reload
|
||||
</Button>
|
||||
</Box>
|
||||
</Fade>
|
||||
</>
|
||||
)
|
||||
) : null}
|
||||
<Dialog
|
||||
|
||||
@@ -96,6 +96,8 @@ export interface DashboardLayoutState {
|
||||
pendingFocusNoteId: string | null;
|
||||
/** Transient: snapshot stand-ins for off-screen webviews; never rides the layout PUT. */
|
||||
suspendedBrowserCards: Record<string, { dataUrl: string; capturedAt: number }>;
|
||||
/** Transient: spawned cards that are about to be removed; surfaces the fade + Keep pill. */
|
||||
endingBrowserCards: Record<string, { status: 'completed' | 'error'; at: number }>;
|
||||
}
|
||||
|
||||
const initialState: DashboardLayoutState = {
|
||||
@@ -113,6 +115,7 @@ const initialState: DashboardLayoutState = {
|
||||
pendingFocusBrowserId: null,
|
||||
pendingFocusNoteId: null,
|
||||
suspendedBrowserCards: {},
|
||||
endingBrowserCards: {},
|
||||
};
|
||||
|
||||
interface LayoutPayload {
|
||||
@@ -635,6 +638,21 @@ const dashboardLayoutSlice = createSlice({
|
||||
removeBrowserCard(state, action: PayloadAction<string>) {
|
||||
delete state.browserCards[action.payload];
|
||||
delete state.suspendedBrowserCards[action.payload];
|
||||
delete state.endingBrowserCards[action.payload];
|
||||
},
|
||||
|
||||
markBrowserCardEnding(
|
||||
state, action: PayloadAction<{ browserId: string; status: 'completed' | 'error' }>,
|
||||
) {
|
||||
if (!state.browserCards[action.payload.browserId]) return;
|
||||
state.endingBrowserCards[action.payload.browserId] = {
|
||||
status: action.payload.status,
|
||||
at: Date.now(),
|
||||
};
|
||||
},
|
||||
|
||||
cancelBrowserCardEnding(state, action: PayloadAction<string>) {
|
||||
delete state.endingBrowserCards[action.payload];
|
||||
},
|
||||
|
||||
suspendBrowserCard(state, action: PayloadAction<{ browserId: string; dataUrl: string }>) {
|
||||
@@ -959,6 +977,7 @@ const dashboardLayoutSlice = createSlice({
|
||||
state.initialized = false;
|
||||
state.pendingFocusNoteId = null;
|
||||
state.suspendedBrowserCards = {};
|
||||
state.endingBrowserCards = {};
|
||||
},
|
||||
|
||||
},
|
||||
@@ -1068,6 +1087,8 @@ export const {
|
||||
removeBrowserCard,
|
||||
suspendBrowserCard,
|
||||
resumeBrowserCard,
|
||||
markBrowserCardEnding,
|
||||
cancelBrowserCardEnding,
|
||||
pasteBrowserCard,
|
||||
updateBrowserCardUrl,
|
||||
addBrowserTab,
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
clearTurnLabel,
|
||||
} from '../state/agentsSlice';
|
||||
import { streamStart, streamDelta, streamEnd, clearStreamingForSession } from '../state/streamingSlice';
|
||||
import { addBrowserCardFromBackend, removeBrowserCard, setBrowserCardPosition, setGlowingBrowserCards, GRID_GAP } from '../state/dashboardLayoutSlice';
|
||||
import { addBrowserCardFromBackend, markBrowserCardEnding, setBrowserCardPosition, setGlowingBrowserCards, GRID_GAP } from '../state/dashboardLayoutSlice';
|
||||
import { upsertOutput } from '../state/outputsSlice';
|
||||
import { getAuthToken } from '../config';
|
||||
import { notifyAgentCompletion } from '../notifications';
|
||||
@@ -500,6 +500,8 @@ class WebSocketManager {
|
||||
// sub-agent: the parent reuses the same browser_id for its next step,
|
||||
// so deleting on sub-agent completion strands BrowserAgent(browser_id)
|
||||
// on a dead card. 'stopped' skipped to allow inspect-after-manual-stop.
|
||||
// Mark the card as ending instead of removing immediately so the card
|
||||
// shows a fade + Keep pill; BrowserCard owns the 3s timer to remove.
|
||||
if (
|
||||
session_id &&
|
||||
(data.status === 'completed' || data.status === 'error') &&
|
||||
@@ -508,7 +510,9 @@ class WebSocketManager {
|
||||
const browserCards = store.getState().dashboardLayout.browserCards;
|
||||
for (const card of Object.values(browserCards)) {
|
||||
if (card.spawned_by === session_id) {
|
||||
store.dispatch(removeBrowserCard(card.browser_id));
|
||||
store.dispatch(markBrowserCardEnding({
|
||||
browserId: card.browser_id, status: data.status,
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -723,11 +727,15 @@ class WebSocketManager {
|
||||
// Auto-delete browsers spawned by this agent when it finishes
|
||||
// normally or errors out. We intentionally skip 'stopped' , the
|
||||
// user may want to inspect the browser after manually stopping.
|
||||
// Mark for removal so BrowserCard renders a fade + Keep pill;
|
||||
// the card itself runs the 3s timer to dispatch removeBrowserCard.
|
||||
if (closedStatus === 'completed' || closedStatus === 'error') {
|
||||
const browserCards = store.getState().dashboardLayout.browserCards;
|
||||
for (const card of Object.values(browserCards)) {
|
||||
if (card.spawned_by === session_id) {
|
||||
store.dispatch(removeBrowserCard(card.browser_id));
|
||||
store.dispatch(markBrowserCardEnding({
|
||||
browserId: card.browser_id, status: closedStatus,
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user