mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-17 18:25:42 +02:00
[eric] browser: delete the orphaned BrowserAgentInlineFeed component (missed in the action-feed removal commit)
This commit is contained in:
@@ -1,536 +0,0 @@
|
|||||||
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';
|
|
||||||
import LanguageIcon from '@mui/icons-material/Language';
|
|
||||||
import OpenInNewIcon from '@mui/icons-material/OpenInNew';
|
|
||||||
import TouchAppOutlinedIcon from '@mui/icons-material/TouchAppOutlined';
|
|
||||||
import KeyboardOutlinedIcon from '@mui/icons-material/KeyboardOutlined';
|
|
||||||
import CameraAltOutlinedIcon from '@mui/icons-material/CameraAltOutlined';
|
|
||||||
import ArticleOutlinedIcon from '@mui/icons-material/ArticleOutlined';
|
|
||||||
import AccountTreeOutlinedIcon from '@mui/icons-material/AccountTreeOutlined';
|
|
||||||
import CodeOutlinedIcon from '@mui/icons-material/CodeOutlined';
|
|
||||||
import BuildOutlinedIcon from '@mui/icons-material/BuildOutlined';
|
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
|
||||||
import { shallowEqual } from 'react-redux';
|
|
||||||
import { useAppSelector, useAppDispatch } from '@/shared/hooks';
|
|
||||||
import { AgentMessage, AgentSession, fetchBrowserAgentChildren, handleApproval } from '@/shared/state/agentsSlice';
|
|
||||||
import type { StreamingMessage } from '@/shared/state/streamingSlice';
|
|
||||||
import { useClaudeTokens, useThemeMode } from '@/shared/styles/ThemeContext';
|
|
||||||
import type { RootState } from '@/shared/state/store';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
parentSessionId: string;
|
|
||||||
browserId?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface FeedEntry {
|
|
||||||
type: 'thought' | 'action' | 'result' | 'system';
|
|
||||||
text: string;
|
|
||||||
actionTool?: string;
|
|
||||||
sessionLabel?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatMessage(msg: AgentMessage): FeedEntry | null {
|
|
||||||
if (msg.role === 'user') return null;
|
|
||||||
|
|
||||||
if (msg.role === 'assistant' && typeof msg.content === 'string') {
|
|
||||||
const trimmed = msg.content.trim();
|
|
||||||
if (!trimmed) return null;
|
|
||||||
return { type: 'thought', text: trimmed };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (msg.role === 'tool_call') {
|
|
||||||
const content =
|
|
||||||
typeof msg.content === 'string'
|
|
||||||
? (() => { try { return JSON.parse(msg.content); } catch { return {}; } })()
|
|
||||||
: msg.content;
|
|
||||||
const tool = content?.tool || content?.name || '?';
|
|
||||||
const input = content?.input || {};
|
|
||||||
let brief = '';
|
|
||||||
switch (tool) {
|
|
||||||
case 'BrowserNavigate':
|
|
||||||
brief = `Navigate → ${input.url || '...'}`;
|
|
||||||
break;
|
|
||||||
case 'BrowserClick':
|
|
||||||
brief = `Click ${input.selector || '...'}`;
|
|
||||||
break;
|
|
||||||
case 'BrowserType': {
|
|
||||||
const txt = (input.text || '').slice(0, 40);
|
|
||||||
const ellipsis = (input.text || '').length > 40 ? '…' : '';
|
|
||||||
brief = `Type "${txt}${ellipsis}" into ${input.selector || '...'}`;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'BrowserScreenshot':
|
|
||||||
brief = 'Screenshot';
|
|
||||||
break;
|
|
||||||
case 'BrowserGetText':
|
|
||||||
brief = 'Read page text';
|
|
||||||
break;
|
|
||||||
case 'BrowserGetElements':
|
|
||||||
brief = `Inspect elements${input.selector ? ` (${input.selector})` : ''}`;
|
|
||||||
break;
|
|
||||||
case 'BrowserEvaluate':
|
|
||||||
brief = `Evaluate JS`;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
brief = `${tool}(${JSON.stringify(input).slice(0, 60)})`;
|
|
||||||
}
|
|
||||||
return { type: 'action', text: brief, actionTool: tool };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (msg.role === 'tool_result') {
|
|
||||||
const content =
|
|
||||||
typeof msg.content === 'string'
|
|
||||||
? (() => { try { return JSON.parse(msg.content); } catch { return { text: msg.content }; } })()
|
|
||||||
: msg.content;
|
|
||||||
const toolName = content?.tool_name || '';
|
|
||||||
const elapsed = content?.elapsed_ms;
|
|
||||||
const text = content?.text || '';
|
|
||||||
|
|
||||||
if (toolName === 'BrowserScreenshot') {
|
|
||||||
return { type: 'result', text: `Screenshot captured${elapsed ? ` (${elapsed}ms)` : ''}` };
|
|
||||||
}
|
|
||||||
const preview = text.length > 120 ? text.slice(0, 120) + '…' : text;
|
|
||||||
return { type: 'result', text: `${preview}${elapsed ? ` (${elapsed}ms)` : ''}` };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (msg.role === 'system') {
|
|
||||||
return { type: 'system', text: typeof msg.content === 'string' ? msg.content : '' };
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
type SvgIconComponent = typeof OpenInNewIcon;
|
|
||||||
|
|
||||||
function getActionIcon(tool?: string): SvgIconComponent {
|
|
||||||
switch (tool) {
|
|
||||||
case 'BrowserNavigate': return OpenInNewIcon;
|
|
||||||
case 'BrowserClick': return TouchAppOutlinedIcon;
|
|
||||||
case 'BrowserType': return KeyboardOutlinedIcon;
|
|
||||||
case 'BrowserScreenshot': return CameraAltOutlinedIcon;
|
|
||||||
case 'BrowserGetText': return ArticleOutlinedIcon;
|
|
||||||
case 'BrowserGetElements': return AccountTreeOutlinedIcon;
|
|
||||||
case 'BrowserEvaluate': return CodeOutlinedIcon;
|
|
||||||
default: return BuildOutlinedIcon;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface FeedColors {
|
|
||||||
thought: string;
|
|
||||||
thoughtIcon: string;
|
|
||||||
result: string;
|
|
||||||
error: string;
|
|
||||||
errorIcon: string;
|
|
||||||
scrollThumb: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const darkFeedColors: FeedColors = {
|
|
||||||
thought: '#a0aab8',
|
|
||||||
thoughtIcon: '#555b6e',
|
|
||||||
result: '#555b6e',
|
|
||||||
error: '#ff8787',
|
|
||||||
errorIcon: '#ff8787',
|
|
||||||
scrollThumb: '#2a2d3e',
|
|
||||||
};
|
|
||||||
|
|
||||||
const lightFeedColors: FeedColors = {
|
|
||||||
thought: '#555550',
|
|
||||||
thoughtIcon: '#9e9c95',
|
|
||||||
result: '#9e9c95',
|
|
||||||
error: '#c03030',
|
|
||||||
errorIcon: '#c03030',
|
|
||||||
scrollThumb: '#ccc9c0',
|
|
||||||
};
|
|
||||||
|
|
||||||
// Stable ref keeps shallowEqual happy when there are no browser sessions yet.
|
|
||||||
const EMPTY_STREAMING: Record<string, StreamingMessage> = Object.freeze({}) as Record<string, StreamingMessage>;
|
|
||||||
|
|
||||||
// Factory, one selector PER FEED: a module-level createSelector has a cache of 1 shared by every mounted feed, so two feeds with different args thrash it and every render recomputes (and returns a fresh array identity, which defeats all downstream memoization).
|
|
||||||
const makeSelectBrowserSessions = () => createSelector(
|
|
||||||
[(state: RootState) => state.agents.sessions,
|
|
||||||
(_: RootState, parentSessionId: string) => parentSessionId,
|
|
||||||
(_: RootState, __: string, browserId?: string) => browserId],
|
|
||||||
(sessions, parentSessionId, browserId) =>
|
|
||||||
Object.values(sessions).filter(
|
|
||||||
(s): s is AgentSession =>
|
|
||||||
s.mode === 'browser-agent' &&
|
|
||||||
s.parent_session_id === parentSessionId &&
|
|
||||||
(!browserId || s.browser_id === browserId),
|
|
||||||
),
|
|
||||||
// Same members = same array identity: ANY session update rebuilds the sessions dict, and without this every unrelated agent:status re-ran formatMessage over the whole feed history.
|
|
||||||
{ memoizeOptions: { resultEqualityCheck: shallowEqual } },
|
|
||||||
);
|
|
||||||
|
|
||||||
const BrowserAgentInlineFeed: React.FC<Props> = ({ parentSessionId, browserId }) => {
|
|
||||||
const c = useClaudeTokens();
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const { mode } = useThemeMode();
|
|
||||||
const fc = mode === 'dark' ? darkFeedColors : lightFeedColors;
|
|
||||||
const scrollRef = useRef<HTMLDivElement>(null);
|
|
||||||
const fetchedForSession = useRef<string | null>(null);
|
|
||||||
|
|
||||||
const selectBrowserSessions = useMemo(makeSelectBrowserSessions, []);
|
|
||||||
const browserSessions = useAppSelector((state) =>
|
|
||||||
selectBrowserSessions(state, parentSessionId, browserId),
|
|
||||||
);
|
|
||||||
// Subscribe only to this feed's sessions; reading the full streaming dict re-renders on every char from every agent.
|
|
||||||
const browserSessionIds = useMemo(
|
|
||||||
() => browserSessions.map((s) => s.id).sort().join(','),
|
|
||||||
[browserSessions],
|
|
||||||
);
|
|
||||||
const streamingBySession = useAppSelector(
|
|
||||||
(state) => {
|
|
||||||
if (!browserSessionIds) return EMPTY_STREAMING;
|
|
||||||
const out: Record<string, StreamingMessage> = {};
|
|
||||||
for (const id of browserSessionIds.split(',')) {
|
|
||||||
const entry = state.streaming.bySession[id];
|
|
||||||
if (entry) out[id] = entry;
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
},
|
|
||||||
shallowEqual,
|
|
||||||
);
|
|
||||||
|
|
||||||
// A child that arrived only through the trimmed session-list poll carries its message_count but no messages; fetch the full children so its history renders instead of showing a blank feed. Keyed by the unhydrated-children set (not one-shot per parent) so a NEW child appearing mid-run still hydrates, while the same set never refetches (no loop).
|
|
||||||
const unhydratedKey = browserSessions.length === 0
|
|
||||||
? `${parentSessionId}:empty`
|
|
||||||
: browserSessions.filter((s) => (s.message_count ?? 0) > 0 && s.messages.length === 0).map((s) => s.id).sort().join(',');
|
|
||||||
useEffect(() => {
|
|
||||||
if (!unhydratedKey.endsWith(':empty') && unhydratedKey === '') return;
|
|
||||||
if (fetchedForSession.current === unhydratedKey) return;
|
|
||||||
fetchedForSession.current = unhydratedKey;
|
|
||||||
dispatch(fetchBrowserAgentChildren(parentSessionId))
|
|
||||||
.unwrap()
|
|
||||||
.catch(() => { fetchedForSession.current = null; });
|
|
||||||
}, [unhydratedKey, parentSessionId, dispatch]);
|
|
||||||
|
|
||||||
const sessionsWithHistoricalEntries = useMemo(() => {
|
|
||||||
return browserSessions.map((session) => {
|
|
||||||
const entries: FeedEntry[] = [];
|
|
||||||
for (const msg of session.messages) {
|
|
||||||
const entry = formatMessage(msg);
|
|
||||||
if (!entry) continue;
|
|
||||||
// Retries emit the same status line back-to-back ("Picking up what I learned..." x4); one
|
|
||||||
// row carries the information, the repeats were pure noise in the transcript.
|
|
||||||
const prev = entries[entries.length - 1];
|
|
||||||
if (prev && prev.type === entry.type && prev.text === entry.text) continue;
|
|
||||||
entries.push(entry);
|
|
||||||
}
|
|
||||||
return { session, entries };
|
|
||||||
});
|
|
||||||
}, [browserSessions]);
|
|
||||||
|
|
||||||
const sessionsWithEntries = sessionsWithHistoricalEntries.map(({ session, entries }) => {
|
|
||||||
const stream: StreamingMessage | undefined = streamingBySession[session.id];
|
|
||||||
if (stream?.role === 'assistant' && stream.content) {
|
|
||||||
return { session, entries: [...entries, { type: 'thought' as const, text: stream.content }] };
|
|
||||||
}
|
|
||||||
return { session, entries };
|
|
||||||
});
|
|
||||||
|
|
||||||
const totalMessages = browserSessions.reduce(
|
|
||||||
(n, s) => n + s.messages.length + (streamingBySession[s.id] ? 1 : 0),
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
|
|
||||||
const isStuckToBottom = useRef(true);
|
|
||||||
|
|
||||||
const handleScroll = useCallback(() => {
|
|
||||||
const el = scrollRef.current;
|
|
||||||
if (!el) return;
|
|
||||||
isStuckToBottom.current = el.scrollHeight - el.scrollTop - el.clientHeight < 30;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isStuckToBottom.current && scrollRef.current) {
|
|
||||||
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
|
||||||
}
|
|
||||||
}, [totalMessages]);
|
|
||||||
|
|
||||||
if (browserSessions.length === 0) return null;
|
|
||||||
|
|
||||||
const showLabels = sessionsWithEntries.length > 1;
|
|
||||||
const accentColor = c.accent.primary;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
ref={scrollRef}
|
|
||||||
onScroll={handleScroll}
|
|
||||||
onWheel={(e) => {
|
|
||||||
// Block wheel only while feed can still scroll; at boundaries let parent chat take over.
|
|
||||||
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;
|
|
||||||
if (!atTop && !atBottom) e.stopPropagation();
|
|
||||||
}}
|
|
||||||
sx={{
|
|
||||||
maxHeight: 300,
|
|
||||||
overflowY: 'auto',
|
|
||||||
px: 1.5,
|
|
||||||
py: 1,
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
gap: 0.25,
|
|
||||||
scrollbarWidth: 'thin',
|
|
||||||
scrollbarColor: `${fc.scrollThumb} transparent`,
|
|
||||||
'&::-webkit-scrollbar': { width: 4 },
|
|
||||||
'&::-webkit-scrollbar-thumb': {
|
|
||||||
background: fc.scrollThumb,
|
|
||||||
borderRadius: 2,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{sessionsWithEntries.map(({ session, entries }, si) => (
|
|
||||||
<Box key={session.id} sx={{ display: 'flex', flexDirection: 'column', gap: 0.25 }}>
|
|
||||||
{showLabels && (
|
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, mt: si > 0 ? 1 : 0, mb: 0.25 }}>
|
|
||||||
<LanguageIcon sx={{ fontSize: 12, color: accentColor, opacity: 0.7 }} />
|
|
||||||
<Typography
|
|
||||||
sx={{
|
|
||||||
fontSize: '0.625rem',
|
|
||||||
fontWeight: 600,
|
|
||||||
color: accentColor,
|
|
||||||
opacity: 0.8,
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
letterSpacing: '0.04em',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{session.browser_id || `Browser ${si + 1}`}
|
|
||||||
</Typography>
|
|
||||||
<SessionStatusChip status={session.status} />
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!showLabels && entries.length === 0 && session.status === 'running' && (
|
|
||||||
<Typography
|
|
||||||
sx={{
|
|
||||||
fontSize: '0.6875rem',
|
|
||||||
color: c.text.tertiary,
|
|
||||||
fontStyle: 'italic',
|
|
||||||
fontFamily: c.font.mono,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Starting browser agent...
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{entries.map((entry, i) => (
|
|
||||||
<EntryRow key={i} entry={entry} accentColor={accentColor} fc={fc} />
|
|
||||||
))}
|
|
||||||
|
|
||||||
{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.75rem',
|
|
||||||
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
|
|
||||||
sx={{
|
|
||||||
width: 6,
|
|
||||||
height: 6,
|
|
||||||
borderRadius: '50%',
|
|
||||||
bgcolor: accentColor,
|
|
||||||
animation: 'ba-feed-pulse 1.4s ease-in-out infinite',
|
|
||||||
'@keyframes ba-feed-pulse': {
|
|
||||||
'0%, 100%': { opacity: 0.3, transform: 'scale(0.8)' },
|
|
||||||
'50%': { opacity: 1, transform: 'scale(1.2)' },
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
))}
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Memoized: the feed re-renders on every streamed token, and un-memoized rows re-render the ENTIRE lazy-loaded history per token (the "browser use = hella lag" bug).
|
|
||||||
const EntryRow = React.memo<{ entry: FeedEntry; accentColor: string; fc: FeedColors }>(({ entry, accentColor, fc }) => {
|
|
||||||
const c = useClaudeTokens();
|
|
||||||
|
|
||||||
if (entry.type === 'thought') {
|
|
||||||
return (
|
|
||||||
<Box sx={{ display: 'flex', gap: 0.5, alignItems: 'flex-start', minWidth: 0 }}>
|
|
||||||
<SmartToyOutlinedIcon
|
|
||||||
sx={{ fontSize: 10, color: fc.thoughtIcon, mt: '3px', flexShrink: 0 }}
|
|
||||||
/>
|
|
||||||
<Typography
|
|
||||||
sx={{
|
|
||||||
fontSize: '0.6875rem',
|
|
||||||
color: fc.thought,
|
|
||||||
lineHeight: 1.45,
|
|
||||||
wordBreak: 'break-word',
|
|
||||||
fontFamily: c.font.mono,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{entry.text}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (entry.type === 'action') {
|
|
||||||
const ActionIcon = getActionIcon(entry.actionTool);
|
|
||||||
return (
|
|
||||||
<Box sx={{ display: 'flex', gap: 0.5, alignItems: 'flex-start', minWidth: 0 }}>
|
|
||||||
<ActionIcon sx={{ fontSize: 11, color: accentColor, mt: '2px', flexShrink: 0 }} />
|
|
||||||
<Typography
|
|
||||||
sx={{
|
|
||||||
fontSize: '0.6875rem',
|
|
||||||
fontFamily: c.font.mono,
|
|
||||||
color: accentColor,
|
|
||||||
lineHeight: 1.45,
|
|
||||||
wordBreak: 'break-word',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{entry.text}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (entry.type === 'result') {
|
|
||||||
return (
|
|
||||||
<Box sx={{ display: 'flex', gap: 0.5, alignItems: 'flex-start', minWidth: 0, pl: 1.25 }}>
|
|
||||||
<Typography
|
|
||||||
sx={{
|
|
||||||
fontSize: '0.625rem',
|
|
||||||
fontFamily: c.font.mono,
|
|
||||||
color: fc.result,
|
|
||||||
lineHeight: 1.45,
|
|
||||||
wordBreak: 'break-word',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
↳ {entry.text}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (entry.type === 'system') {
|
|
||||||
return (
|
|
||||||
<Box sx={{ display: 'flex', gap: 0.5, alignItems: 'center', minWidth: 0 }}>
|
|
||||||
<ErrorOutlineIcon sx={{ fontSize: 10, color: fc.errorIcon, flexShrink: 0 }} />
|
|
||||||
<Typography
|
|
||||||
sx={{
|
|
||||||
fontSize: '0.6875rem',
|
|
||||||
fontFamily: c.font.mono,
|
|
||||||
color: fc.error,
|
|
||||||
lineHeight: 1.45,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{entry.text}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
|
|
||||||
const SessionStatusChip: React.FC<{ status: string }> = ({ status }) => {
|
|
||||||
const c = useClaudeTokens();
|
|
||||||
if (status === 'running') {
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
width: 6,
|
|
||||||
height: 6,
|
|
||||||
borderRadius: '50%',
|
|
||||||
bgcolor: c.status.success,
|
|
||||||
animation: 'ba-feed-pulse 1.4s ease-in-out infinite',
|
|
||||||
'@keyframes ba-feed-pulse': {
|
|
||||||
'0%, 100%': { opacity: 0.3, transform: 'scale(0.8)' },
|
|
||||||
'50%': { opacity: 1, transform: 'scale(1.2)' },
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (status === 'completed') {
|
|
||||||
return <CheckCircleOutlineIcon sx={{ fontSize: 10, color: c.status.success }} />;
|
|
||||||
}
|
|
||||||
if (status === 'error') {
|
|
||||||
return <ErrorOutlineIcon sx={{ fontSize: 10, color: c.status.error }} />;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default React.memo(BrowserAgentInlineFeed);
|
|
||||||
Reference in New Issue
Block a user