mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-30 05:24:50 +02:00
[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:
@@ -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,
|
||||
|
||||
@@ -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"),
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user