[eric] keep browser agent overlay alive between sub-tasks by gating fade/hide on parent session status, show Waiting… instead of false

Done between tool calls
This commit is contained in:
ciregenz
2026-04-10 12:17:36 -07:00
parent 5e93f5a950
commit c9df3c1b49
3 changed files with 61 additions and 10 deletions
@@ -14,7 +14,7 @@ import CloseFullscreenIcon from '@mui/icons-material/CloseFullscreen';
import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline';
import { AgentSession, AgentMessage, stopAgent, handleApproval } from '@/shared/state/agentsSlice';
import { useAppDispatch } from '@/shared/hooks';
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
interface Props {
@@ -69,8 +69,22 @@ const BrowserAgentOverlay: React.FC<Props> = ({ session, browserWidth, browserHe
const fadeTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
const hideTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
// Check if the parent agent session is still running. If so, the overlay
// stays alive even when this browser-agent sub-task completes — the parent
// may send another BrowserAgent call momentarily. Only treat the overlay
// as "done" when both the browser-agent session AND the parent are terminal.
const parentStatus = useAppSelector((state) => {
if (!session.parent_session_id) return null;
return state.agents.sessions[session.parent_session_id]?.status ?? null;
});
const parentStillActive = parentStatus === 'running' || parentStatus === 'waiting_approval';
const isRunning = session.status === 'running' || session.status === 'waiting_approval';
const isDone = session.status === 'completed' || session.status === 'error' || session.status === 'stopped';
const browserDone = session.status === 'completed' || session.status === 'error' || session.status === 'stopped';
// Only truly "done" (fade + hide) when the parent is also finished.
// While the parent is still active, the overlay stays visible in a
// "waiting for next task" state between sub-tasks.
const isDone = browserDone && !parentStillActive;
const intervention = session.pending_approvals?.find(
(a) => a.tool_name === 'RequestHumanIntervention',
@@ -201,7 +215,7 @@ const BrowserAgentOverlay: React.FC<Props> = ({ session, browserWidth, browserHe
: <SmartToyOutlinedIcon sx={{ fontSize: 14, color: accentColor }} />
}
{isRunning && !intervention && (
{(isRunning || (browserDone && parentStillActive)) && !intervention && (
<Box
sx={{
width: 6,
@@ -238,7 +252,9 @@ const BrowserAgentOverlay: React.FC<Props> = ({ session, browserWidth, browserHe
>
{isDone
? session.status === 'completed' ? 'Done' : session.status === 'error' ? 'Error' : 'Stopped'
: intervention ? 'Needs Help' : 'Browser Agent'}
: intervention ? 'Needs Help'
: browserDone && parentStillActive ? 'Waiting…'
: 'Browser Agent'}
</Typography>
<Tooltip title={expanded ? 'Collapse' : 'Expand'} placement="top">
+40 -5
View File
@@ -1384,7 +1384,24 @@ const DashboardInner: React.FC<DashboardProps> = ({ dashboardId, isActive = true
};
}).filter(Boolean) as Array<{ key: string; path: string; labelX: number; labelY: number; label: string; fading: boolean }>;
const browserTethers = Object.entries(glowingBrowserCards).map(([browserId, { sourceId, fading, label }]) => {
// Build browser tethers from TWO sources and merge:
// 1. glowingBrowserCards — the short-lived "flash" when a browser is first assigned
// 2. Active browser-agent sessions — persistent as long as the agent runs
//
// Source #2 is the fix for tethers disappearing when the parent session
// completes a turn (which clears glowingBrowserCards even though the
// browser agent is still working). Source #1 covers the initial moment
// before the browser-agent session is fully created. Together they
// ensure the arrow is always visible when it should be.
type Anchor = { x: number; y: number; side: 'left' | 'right' | 'top' | 'bottom' };
function browserTether(
browserId: string,
sourceId: string,
fading: boolean,
label: string,
) {
const src = cards[sourceId];
const dst = browserCards[browserId];
if (!src || !dst) return null;
@@ -1405,7 +1422,6 @@ const DashboardInner: React.FC<DashboardProps> = ({ dashboardId, isActive = true
const srcCx = srcX + src.width / 2;
const dstCx = dstX + dst.width / 2;
type Anchor = { x: number; y: number; side: 'left' | 'right' | 'top' | 'bottom' };
const srcAnchors: Anchor[] = [
{ x: srcX + src.width, y: srcY + srcH * 0.54, side: 'right' },
{ x: srcX, y: srcY + srcH * 0.54, side: 'left' },
@@ -1466,14 +1482,33 @@ const DashboardInner: React.FC<DashboardProps> = ({ dashboardId, isActive = true
path: pathD,
labelX,
labelY,
label: label || '',
label,
fading,
};
}).filter(Boolean) as Array<{ key: string; path: string; labelX: number; labelY: number; label: string; fading: boolean }>;
}
// Source 1: glow-based (covers the initial flash before browser-agent session exists)
const glowTethers = new Map<string, ReturnType<typeof browserTether>>();
for (const [browserId, { sourceId, fading, label }] of Object.entries(glowingBrowserCards)) {
const t = browserTether(browserId, sourceId, fading, label || '');
if (t) glowTethers.set(browserId, t);
}
// Source 2: active browser-agent sessions (persistent — survives parent turn completion)
for (const s of sessionList) {
if (s.mode !== 'browser-agent') continue;
if (s.status !== 'running' && s.status !== 'waiting_approval') continue;
if (!s.browser_id || !s.parent_session_id) continue;
if (glowTethers.has(s.browser_id)) continue; // glow already covers this one
const t = browserTether(s.browser_id, s.parent_session_id, false, '');
if (t) glowTethers.set(s.browser_id, t);
}
const browserTethers = Array.from(glowTethers.values()).filter(Boolean) as Array<{ key: string; path: string; labelX: number; labelY: number; label: string; fading: boolean }>;
return [...agentTethers, ...browserTethers];
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [glowingAgentCards, glowingBrowserCards, cards, browserCards, expandedSessionIds, liveDragInfo, measuredHeightsTick]);
}, [glowingAgentCards, glowingBrowserCards, cards, browserCards, expandedSessionIds, liveDragInfo, measuredHeightsTick, sessionList]);
const dotSize = Math.max(1, 1.5 * canvas.zoom);
const dotSpacing = 24 * canvas.zoom;
File diff suppressed because one or more lines are too long