[eric] RequestHumanIntervention: show in BrowserAgentOverlay + parent chat inline feed + DynamicIsland with consistent amber UI, add

skip-with-reason text input, shorter intervention messages, register in BUILTIN_TOOLS, fix waiting_approval session selector,
  sticky-to-bottom scroll in inline feed
This commit is contained in:
ciregenz
2026-04-10 10:35:55 -07:00
parent a5fc29351e
commit 71f3eb640a
7 changed files with 359 additions and 113 deletions
+16 -3
View File
@@ -588,11 +588,17 @@ BROWSER_TOOLS_SCHEMA = [
"properties": {
"problem": {
"type": "string",
"description": "What obstacle was encountered.",
"description": (
"One short sentence describing the obstacle. Keep it under "
"15 words. Example: 'Login required — please sign in to X/Twitter.'"
),
},
"instruction": {
"type": "string",
"description": "What the user should do to resolve it.",
"description": (
"One short sentence telling the user what to do. Keep it under "
"15 words. Example: 'Log in with your credentials, then click Done.'"
),
},
},
"required": ["problem", "instruction"],
@@ -1078,7 +1084,14 @@ async def run_browser_agent(
decision = await _request_browser_approval(
session, tu.name, {"problem": problem, "instruction": instruction},
)
result_text = "User resolved the issue." if decision.get("behavior") != "deny" else "User declined to help."
if decision.get("behavior") != "deny":
result_text = "User resolved the issue. Continue with the task."
else:
user_message = decision.get("message", "").strip()
if user_message and user_message != "Skipped by user":
result_text = f"User skipped this intervention and said: \"{user_message}\"\nAddress what the user said and adapt your approach accordingly."
else:
result_text = "User skipped this intervention. Try a different approach or move on."
tool_results.append({
"type": "tool_result",
"tool_use_id": tu.id,
+1
View File
@@ -56,6 +56,7 @@ BUILTIN_TOOLS: list[BuiltinTool] = [
BuiltinTool(name="BrowserClickIndex", description="Click an element by its index from BrowserListInteractives (uses native mouse events)", category="browser_action"),
BuiltinTool(name="BrowserBatch", description="Run a sequence of browser actions in one tool call with URL-change abort guard", category="browser_action"),
BuiltinTool(name="ReportProgress", description="Record evaluation of previous action, working memory, and next goal (required before action tools)", category="browser_action"),
BuiltinTool(name="RequestHumanIntervention", description="Pause the browser agent and ask the user for help (login, captcha, 2FA, etc.)", category="browser_action"),
]
+18 -10
View File
@@ -10,6 +10,7 @@ import CloseIcon from '@mui/icons-material/Close';
import CheckIcon from '@mui/icons-material/Check';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
import PanToolOutlinedIcon from '@mui/icons-material/PanToolOutlined';
import { motion, AnimatePresence } from 'framer-motion';
import { useNavigate } from 'react-router-dom';
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
@@ -618,9 +619,16 @@ const CompactActionablePill: React.FC<{
const parsed = useMemo(() => parseMcpToolName(request.tool_name), [request.tool_name]);
const meta = useMcpToolMeta(parsed);
const icon = parsed.isMcp
? (meta.integration?.icon || null)
: getToolIcon(request.tool_name);
const isIntervention = request.tool_name === 'RequestHumanIntervention';
const interventionProblem = isIntervention
? ((request.tool_input as any)?.problem || 'Browser agent needs help')
: '';
const icon = isIntervention
? <PanToolOutlinedIcon />
: parsed.isMcp
? (meta.integration?.icon || null)
: getToolIcon(request.tool_name);
return (
<motion.div
@@ -648,7 +656,7 @@ const CompactActionablePill: React.FC<{
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
color: c.text.tertiary,
color: isIntervention ? '#f59e0b' : c.text.tertiary,
'& svg': { width: 12, height: 12 },
}}
>
@@ -658,7 +666,7 @@ const CompactActionablePill: React.FC<{
sx={{
fontSize: '0.68rem',
fontWeight: 600,
color: c.text.secondary,
color: isIntervention ? '#f59e0b' : c.text.secondary,
flex: 1,
overflow: 'hidden',
textOverflow: 'ellipsis',
@@ -666,7 +674,7 @@ const CompactActionablePill: React.FC<{
minWidth: 0,
}}
>
{parsed.displayName}
{isIntervention ? interventionProblem : parsed.displayName}
</Typography>
{remainingCount > 1 && (
<Typography
@@ -680,7 +688,7 @@ const CompactActionablePill: React.FC<{
+{remainingCount - 1}
</Typography>
)}
<Tooltip title="Approve" arrow>
<Tooltip title={isIntervention ? 'Done — continue' : 'Approve'} arrow>
<IconButton
size="small"
onClick={(e) => { e.stopPropagation(); onApprove(request.id); }}
@@ -689,14 +697,14 @@ const CompactActionablePill: React.FC<{
width: 18,
height: 18,
color: '#fff',
bgcolor: c.status.success,
'&:hover': { bgcolor: c.status.success, filter: 'brightness(0.85)' },
bgcolor: isIntervention ? '#f59e0b' : c.status.success,
'&:hover': { bgcolor: isIntervention ? '#d97706' : c.status.success, filter: isIntervention ? undefined : 'brightness(0.85)' },
}}
>
<CheckIcon sx={{ fontSize: 11 }} />
</IconButton>
</Tooltip>
<Tooltip title="Deny" arrow>
<Tooltip title={isIntervention ? 'Skip' : 'Deny'} arrow>
<IconButton
size="small"
onClick={(e) => { e.stopPropagation(); onDeny(request.id); }}
@@ -1,6 +1,12 @@
import React, { useEffect, useRef, useMemo } from 'react';
import React, { useEffect, useRef, useMemo, useCallback } from 'react';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import Button from '@mui/material/Button';
import IconButton from '@mui/material/IconButton';
import Tooltip from '@mui/material/Tooltip';
import CheckIcon from '@mui/icons-material/Check';
import CloseIcon from '@mui/icons-material/Close';
import PanToolOutlinedIcon from '@mui/icons-material/PanToolOutlined';
import SmartToyOutlinedIcon from '@mui/icons-material/SmartToyOutlined';
import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline';
@@ -15,7 +21,7 @@ import CodeOutlinedIcon from '@mui/icons-material/CodeOutlined';
import BuildOutlinedIcon from '@mui/icons-material/BuildOutlined';
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector, useAppDispatch } from '@/shared/hooks';
import { AgentMessage, AgentSession, fetchBrowserAgentChildren } from '@/shared/state/agentsSlice';
import { AgentMessage, AgentSession, fetchBrowserAgentChildren, handleApproval } from '@/shared/state/agentsSlice';
import { useClaudeTokens, useThemeMode } from '@/shared/styles/ThemeContext';
import type { RootState } from '@/shared/state/store';
@@ -197,8 +203,20 @@ const BrowserAgentInlineFeed: React.FC<Props> = ({ parentSessionId, browserId })
0,
);
// Sticky-to-bottom: auto-scroll to the latest content unless the user
// has manually scrolled up. Re-enable auto-scroll when the user scrolls
// back to the bottom (within a small threshold).
const isStuckToBottom = useRef(true);
const handleScroll = useCallback(() => {
const el = scrollRef.current;
if (!el) return;
// "At bottom" = within 30px of the bottom edge
isStuckToBottom.current = el.scrollHeight - el.scrollTop - el.clientHeight < 30;
}, []);
useEffect(() => {
if (scrollRef.current) {
if (isStuckToBottom.current && scrollRef.current) {
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
}
}, [totalMessages]);
@@ -211,6 +229,19 @@ const BrowserAgentInlineFeed: React.FC<Props> = ({ parentSessionId, browserId })
return (
<Box
ref={scrollRef}
onScroll={handleScroll}
onWheel={(e) => {
// Capture wheel events so the feed scrolls on hover without
// needing to click/focus first. Without this, the parent chat
// scroll container eats the wheel events.
const el = scrollRef.current;
if (!el) return;
const atTop = el.scrollTop <= 0 && e.deltaY < 0;
const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 1 && e.deltaY > 0;
// Only stop propagation when the feed has room to scroll in this
// direction. At boundaries, let the parent scroll naturally.
if (!atTop && !atBottom) e.stopPropagation();
}}
sx={{
maxHeight: 300,
overflowY: 'auto',
@@ -266,6 +297,79 @@ const BrowserAgentInlineFeed: React.FC<Props> = ({ parentSessionId, browserId })
<EntryRow key={i} entry={entry} accentColor={accentColor} fc={fc} />
))}
{/* Inline RequestHumanIntervention — matches the DynamicIsland
and BrowserAgentOverlay style (amber, hand icon, compact pill).
Same request_id → whichever surface the user responds from
first resolves the approval; the others auto-dismiss. */}
{session.pending_approvals?.filter(
(a) => a.tool_name === 'RequestHumanIntervention',
).map((intervention) => {
const problem = (intervention.tool_input as any)?.problem || 'Browser agent needs help';
return (
<Box
key={intervention.id}
sx={{
mt: 0.75,
mb: 0.5,
display: 'flex',
alignItems: 'center',
gap: 0.75,
px: 1,
py: 0.5,
borderRadius: '8px',
bgcolor: 'rgba(245,158,11,0.10)',
border: '1px solid rgba(245,158,11,0.25)',
}}
>
<PanToolOutlinedIcon sx={{ fontSize: 12, color: '#f59e0b', flexShrink: 0, mt: '2px' }} />
<Typography
sx={{
fontSize: '0.72rem',
fontWeight: 600,
color: '#f59e0b',
flex: 1,
minWidth: 0,
lineHeight: 1.4,
}}
>
{problem}
</Typography>
<Tooltip title="Done — continue" arrow>
<IconButton
size="small"
onClick={() => dispatch(handleApproval({ requestId: intervention.id, behavior: 'allow' }))}
sx={{
p: 0,
width: 18,
height: 18,
color: '#fff',
bgcolor: '#f59e0b',
'&:hover': { bgcolor: '#d97706' },
}}
>
<CheckIcon sx={{ fontSize: 11 }} />
</IconButton>
</Tooltip>
<Tooltip title="Skip" arrow>
<IconButton
size="small"
onClick={() => dispatch(handleApproval({ requestId: intervention.id, behavior: 'deny', message: 'User declined to help' }))}
sx={{
p: 0,
width: 18,
height: 18,
color: '#f59e0b',
border: '1px solid rgba(245,158,11,0.4)',
'&:hover': { bgcolor: 'rgba(245,158,11,0.1)' },
}}
>
<CloseIcon sx={{ fontSize: 11 }} />
</IconButton>
</Tooltip>
</Box>
);
})}
{!showLabels && session.status === 'running' && entries.length > 0 && (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, mt: 0.25 }}>
<Box
@@ -2,14 +2,18 @@ import React, { useState, useEffect, useRef, useCallback } from 'react';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import IconButton from '@mui/material/IconButton';
import Button from '@mui/material/Button';
import InputBase from '@mui/material/InputBase';
import Tooltip from '@mui/material/Tooltip';
import SendIcon from '@mui/icons-material/Send';
import SmartToyOutlinedIcon from '@mui/icons-material/SmartToyOutlined';
import PanToolOutlinedIcon from '@mui/icons-material/PanToolOutlined';
import StopIcon from '@mui/icons-material/Stop';
import OpenInFullIcon from '@mui/icons-material/OpenInFull';
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 } from '@/shared/state/agentsSlice';
import { AgentSession, AgentMessage, stopAgent, handleApproval } from '@/shared/state/agentsSlice';
import { useAppDispatch } from '@/shared/hooks';
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
@@ -59,13 +63,21 @@ const BrowserAgentOverlay: React.FC<Props> = ({ session, browserWidth, browserHe
const [confirmStop, setConfirmStop] = useState(false);
const [fadeOut, setFadeOut] = useState(false);
const [hidden, setHidden] = useState(false);
const [showSkipInput, setShowSkipInput] = useState(false);
const [skipReason, setSkipReason] = useState('');
const confirmTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
const fadeTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
const hideTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
const isRunning = session.status === 'running';
const isRunning = session.status === 'running' || session.status === 'waiting_approval';
const isDone = session.status === 'completed' || session.status === 'error' || session.status === 'stopped';
const intervention = session.pending_approvals?.find(
(a) => a.tool_name === 'RequestHumanIntervention',
);
const interventionProblem = (intervention?.tool_input as any)?.problem || 'The browser agent needs your help.';
const interventionInstruction = (intervention?.tool_input as any)?.instruction || '';
const prevSessionId = useRef(session.id);
useEffect(() => {
if (session.id !== prevSessionId.current) {
@@ -73,9 +85,19 @@ const BrowserAgentOverlay: React.FC<Props> = ({ session, browserWidth, browserHe
setFadeOut(false);
setHidden(false);
setConfirmStop(false);
setShowSkipInput(false);
setSkipReason('');
}
}, [session.id]);
// Reset skip input when intervention resolves
useEffect(() => {
if (!intervention) {
setShowSkipInput(false);
setSkipReason('');
}
}, [intervention]);
useEffect(() => {
if (isDone) {
fadeTimer.current = setTimeout(() => setFadeOut(true), 2000);
@@ -127,8 +149,10 @@ const BrowserAgentOverlay: React.FC<Props> = ({ session, browserWidth, browserHe
const expandedW = Math.min(Math.floor(browserWidth * 0.55), browserWidth - 24);
const expandedH = Math.min(Math.floor(browserHeight * 0.6), browserHeight - 24);
const panelW = expanded ? expandedW : collapsedW;
const panelH = expanded ? expandedH : collapsedH;
const panelW = intervention ? Math.min(340, browserWidth - 24) : expanded ? expandedW : collapsedW;
// When intervention is active, auto-size to fit content instead of a
// fixed height — otherwise the Done button gets clipped below the fold.
const panelH = intervention ? undefined : expanded ? expandedH : collapsedH;
if (hidden) return null;
@@ -141,12 +165,12 @@ const BrowserAgentOverlay: React.FC<Props> = ({ session, browserWidth, browserHe
bottom: 12,
right: 12,
width: panelW,
height: panelH,
...(panelH != null ? { height: panelH } : { maxHeight: Math.min(320, browserHeight - 24) }),
zIndex: 18,
borderRadius: '12px',
bgcolor: 'rgba(15, 15, 15, 0.88)',
backdropFilter: 'blur(16px)',
border: `1px solid ${accentColor}30`,
border: `1px solid ${intervention ? 'rgba(245,158,11,0.4)' : `${accentColor}30`}`,
boxShadow: `0 4px 24px rgba(0,0,0,0.5), 0 0 0 1px rgba(255,255,255,0.05)`,
display: 'flex',
flexDirection: 'column',
@@ -172,9 +196,12 @@ const BrowserAgentOverlay: React.FC<Props> = ({ session, browserWidth, browserHe
flexShrink: 0,
}}
>
<SmartToyOutlinedIcon sx={{ fontSize: 14, color: accentColor }} />
{intervention
? <PanToolOutlinedIcon sx={{ fontSize: 14, color: '#f59e0b' }} />
: <SmartToyOutlinedIcon sx={{ fontSize: 14, color: accentColor }} />
}
{isRunning && (
{isRunning && !intervention && (
<Box
sx={{
width: 6,
@@ -202,7 +229,7 @@ const BrowserAgentOverlay: React.FC<Props> = ({ session, browserWidth, browserHe
sx={{
fontSize: '0.7rem',
fontWeight: 600,
color: 'rgba(255,255,255,0.85)',
color: intervention ? '#f59e0b' : 'rgba(255,255,255,0.85)',
flex: 1,
overflow: 'hidden',
textOverflow: 'ellipsis',
@@ -211,7 +238,7 @@ const BrowserAgentOverlay: React.FC<Props> = ({ session, browserWidth, browserHe
>
{isDone
? session.status === 'completed' ? 'Done' : session.status === 'error' ? 'Error' : 'Stopped'
: 'Browser Agent'}
: intervention ? 'Needs Help' : 'Browser Agent'}
</Typography>
<Tooltip title={expanded ? 'Collapse' : 'Expand'} placement="top">
@@ -253,92 +280,182 @@ const BrowserAgentOverlay: React.FC<Props> = ({ session, browserWidth, browserHe
)}
</Box>
{/* Body — scrollable action log */}
<Box
ref={scrollRef}
sx={{
flex: 1,
overflowY: 'auto',
px: 1.25,
py: 0.75,
display: 'flex',
flexDirection: 'column',
gap: 0.5,
scrollbarWidth: 'thin',
scrollbarColor: 'rgba(255,255,255,0.12) transparent',
'&::-webkit-scrollbar': { width: 4 },
'&::-webkit-scrollbar-thumb': {
background: 'rgba(255,255,255,0.12)',
borderRadius: 2,
},
}}
>
{entries.length === 0 && isRunning && (
<Typography sx={{ fontSize: '0.68rem', color: 'rgba(255,255,255,0.35)', fontStyle: 'italic' }}>
Starting...
{/* Body — intervention prompt OR scrollable action log */}
{intervention ? (
<Box sx={{ flex: 1, px: 1.25, py: 1, display: 'flex', flexDirection: 'column', gap: 1 }}>
<Typography sx={{ fontSize: '0.72rem', color: 'rgba(255,255,255,0.8)', lineHeight: 1.5 }}>
{interventionProblem}
</Typography>
)}
{interventionInstruction && (
<Typography sx={{ fontSize: '0.68rem', color: 'rgba(255,255,255,0.45)', fontStyle: 'italic', lineHeight: 1.4 }}>
{interventionInstruction}
</Typography>
)}
<Typography sx={{ fontSize: '0.65rem', color: 'rgba(255,255,255,0.3)', lineHeight: 1.4 }}>
Resolve the issue in the browser above, then click Done.
</Typography>
{showSkipInput ? (
<Box sx={{ display: 'flex', gap: 0.5, mt: 'auto', pt: 0.5, alignItems: 'center' }}>
<InputBase
autoFocus
placeholder="Why? (optional)"
value={skipReason}
onChange={(e) => setSkipReason(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
dispatch(handleApproval({ requestId: intervention.id, behavior: 'deny', message: skipReason || 'Skipped by user' }));
}
if (e.key === 'Escape') setShowSkipInput(false);
}}
sx={{
flex: 1,
fontSize: '0.68rem',
color: 'rgba(255,255,255,0.8)',
bgcolor: 'rgba(255,255,255,0.08)',
borderRadius: '6px',
px: 1,
py: 0.3,
'& input::placeholder': { color: 'rgba(255,255,255,0.3)' },
}}
/>
<IconButton
size="small"
onClick={() => dispatch(handleApproval({ requestId: intervention.id, behavior: 'deny', message: skipReason || 'Skipped by user' }))}
sx={{
p: 0.4,
color: 'rgba(255,255,255,0.5)',
'&:hover': { color: 'rgba(255,255,255,0.8)' },
}}
>
<SendIcon sx={{ fontSize: 13 }} />
</IconButton>
</Box>
) : (
<Box sx={{ display: 'flex', gap: 0.75, mt: 'auto', pt: 0.5 }}>
<Button
size="small"
variant="contained"
onClick={() => dispatch(handleApproval({ requestId: intervention.id, behavior: 'allow' }))}
sx={{
bgcolor: '#f59e0b',
'&:hover': { bgcolor: '#d97706' },
textTransform: 'none',
fontSize: '0.68rem',
fontWeight: 600,
borderRadius: '6px',
px: 1.5,
py: 0.4,
color: '#000',
}}
>
Done — continue
</Button>
<Button
size="small"
variant="text"
onClick={() => setShowSkipInput(true)}
sx={{
color: 'rgba(255,255,255,0.4)',
'&:hover': { color: 'rgba(255,255,255,0.7)' },
textTransform: 'none',
fontSize: '0.68rem',
px: 1,
py: 0.4,
minWidth: 'auto',
}}
>
Skip
</Button>
</Box>
)}
</Box>
) : (
<Box
ref={scrollRef}
sx={{
flex: 1,
overflowY: 'auto',
px: 1.25,
py: 0.75,
display: 'flex',
flexDirection: 'column',
gap: 0.5,
scrollbarWidth: 'thin',
scrollbarColor: 'rgba(255,255,255,0.12) transparent',
'&::-webkit-scrollbar': { width: 4 },
'&::-webkit-scrollbar-thumb': {
background: 'rgba(255,255,255,0.12)',
borderRadius: 2,
},
}}
>
{entries.length === 0 && isRunning && (
<Typography sx={{ fontSize: '0.68rem', color: 'rgba(255,255,255,0.35)', fontStyle: 'italic' }}>
Starting...
</Typography>
)}
{entries.map((entry, i) => (
<Box key={i} sx={{ display: 'flex', gap: 0.5, alignItems: 'flex-start', minWidth: 0 }}>
{entry.type === 'thought' ? (
<>
<Box
sx={{
width: 4,
height: 4,
borderRadius: '50%',
bgcolor: 'rgba(255,255,255,0.25)',
flexShrink: 0,
mt: '5px',
}}
/>
<Typography
sx={{
fontSize: '0.68rem',
color: 'rgba(255,255,255,0.6)',
lineHeight: 1.4,
overflow: 'hidden',
display: '-webkit-box',
WebkitLineClamp: expanded ? 6 : 2,
WebkitBoxOrient: 'vertical',
wordBreak: 'break-word',
}}
>
{entry.text}
</Typography>
</>
) : (
<>
<Box
sx={{
width: 4,
height: 4,
borderRadius: '1px',
bgcolor: accentColor,
flexShrink: 0,
mt: '5px',
transform: 'rotate(45deg)',
}}
/>
<Typography
sx={{
fontSize: '0.68rem',
fontFamily: c.font.mono,
color: accentColor,
lineHeight: 1.4,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{entry.text}
</Typography>
</>
)}
</Box>
))}
</Box>
{entries.map((entry, i) => (
<Box key={i} sx={{ display: 'flex', gap: 0.5, alignItems: 'flex-start', minWidth: 0 }}>
{entry.type === 'thought' ? (
<>
<Box
sx={{
width: 4,
height: 4,
borderRadius: '50%',
bgcolor: 'rgba(255,255,255,0.25)',
flexShrink: 0,
mt: '5px',
}}
/>
<Typography
sx={{
fontSize: '0.68rem',
color: 'rgba(255,255,255,0.6)',
lineHeight: 1.4,
overflow: 'hidden',
display: '-webkit-box',
WebkitLineClamp: expanded ? 6 : 2,
WebkitBoxOrient: 'vertical',
wordBreak: 'break-word',
}}
>
{entry.text}
</Typography>
</>
) : (
<>
<Box
sx={{
width: 4,
height: 4,
borderRadius: '1px',
bgcolor: accentColor,
flexShrink: 0,
mt: '5px',
transform: 'rotate(45deg)',
}}
/>
<Typography
sx={{
fontSize: '0.68rem',
fontFamily: c.font.mono,
color: accentColor,
lineHeight: 1.4,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{entry.text}
</Typography>
</>
)}
</Box>
))}
</Box>
)}
</Box>
);
};
@@ -6,6 +6,8 @@ import Tooltip from '@mui/material/Tooltip';
import InputBase from '@mui/material/InputBase';
import LinearProgress from '@mui/material/LinearProgress';
import CircularProgress from '@mui/material/CircularProgress';
import Button from '@mui/material/Button';
import Fade from '@mui/material/Fade';
import LanguageIcon from '@mui/icons-material/Language';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
@@ -29,6 +31,7 @@ import {
type BrowserTab,
} from '@/shared/state/dashboardLayoutSlice';
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
import { handleApproval } from '@/shared/state/agentsSlice';
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
import {
registerWebview,
@@ -125,9 +128,9 @@ const BrowserCard: React.FC<Props> = ({
const sessions = state.agents.sessions;
const matches = Object.values(sessions).filter(
(s) => s.browser_id === browserId && s.mode === 'browser-agent'
&& (s.status === 'running' || s.status === 'completed' || s.status === 'error' || s.status === 'stopped'),
&& (s.status === 'running' || s.status === 'waiting_approval' || s.status === 'completed' || s.status === 'error' || s.status === 'stopped'),
);
return matches.find((s) => s.status === 'running') ?? matches[matches.length - 1] ?? null;
return matches.find((s) => s.status === 'running' || s.status === 'waiting_approval') ?? matches[matches.length - 1] ?? null;
});
const activity = useBrowserActivity(browserId);
File diff suppressed because one or more lines are too long