mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-31 12:19:48 +02:00
[eric] split: extract invoke/create/compact/default tool bubbles + shell dispatch
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
import React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Collapse from '@mui/material/Collapse';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
|
||||
export const AgentResponseBody: React.FC<{ open: boolean; markdown: string }> = ({ open, markdown }) => {
|
||||
const c = useClaudeTokens();
|
||||
return (
|
||||
<Collapse in={open}>
|
||||
<Box
|
||||
sx={{
|
||||
borderTop: `1px solid ${c.border.subtle}`,
|
||||
px: 1.5,
|
||||
py: 1.25,
|
||||
maxHeight: 400,
|
||||
overflowY: 'auto',
|
||||
overflowX: 'hidden',
|
||||
color: c.text.secondary,
|
||||
fontFamily: c.font.sans,
|
||||
fontSize: '0.78rem',
|
||||
lineHeight: 1.65,
|
||||
overflowWrap: 'anywhere',
|
||||
wordBreak: 'break-word',
|
||||
'& p': { m: 0, mb: 0.75, '&:last-child': { mb: 0 } },
|
||||
'& h1, & h2, & h3, & h4': {
|
||||
color: c.text.primary, fontFamily: c.font.sans,
|
||||
mt: 1, mb: 0.5, '&:first-of-type': { mt: 0 },
|
||||
},
|
||||
'& h1': { fontSize: '0.88rem' }, '& h2': { fontSize: '0.84rem' },
|
||||
'& h3': { fontSize: '0.8rem' }, '& h4': { fontSize: '0.78rem' },
|
||||
'& strong': { color: c.text.primary, fontWeight: 600 },
|
||||
'& a': { color: c.accent.primary, textDecoration: 'none', '&:hover': { textDecoration: 'underline' } },
|
||||
'& ul, & ol': { pl: 2, mb: 0.75, mt: 0 },
|
||||
'& li': { mb: 0.2 },
|
||||
'& blockquote': {
|
||||
m: 0, mb: 0.75, pl: 1, ml: 0,
|
||||
borderLeft: `2px solid ${c.border.subtle}`,
|
||||
color: c.text.tertiary, fontStyle: 'italic',
|
||||
},
|
||||
'& code': {
|
||||
bgcolor: c.bg.secondary, px: 0.4, py: 0.15,
|
||||
borderRadius: 0.5, fontSize: '0.72rem', fontFamily: c.font.mono,
|
||||
},
|
||||
'& pre': {
|
||||
bgcolor: c.bg.secondary, borderRadius: 1, p: 1,
|
||||
overflow: 'auto', fontSize: '0.72rem', fontFamily: c.font.mono,
|
||||
m: 0, mb: 0.75,
|
||||
},
|
||||
'& pre code': { bgcolor: 'transparent', p: 0 },
|
||||
'& hr': { border: 'none', borderTop: `1px solid ${c.border.subtle}`, my: 0.75 },
|
||||
'&::-webkit-scrollbar': { width: 5 },
|
||||
'&::-webkit-scrollbar-track': { background: 'transparent' },
|
||||
'&::-webkit-scrollbar-thumb': { background: c.border.medium, borderRadius: 3 },
|
||||
}}
|
||||
>
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={{
|
||||
a: ({ children, ...props }) => (
|
||||
<a {...props}>{children}</a>
|
||||
),
|
||||
}}
|
||||
>
|
||||
{markdown}
|
||||
</ReactMarkdown>
|
||||
</Box>
|
||||
</Collapse>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,168 @@
|
||||
import React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Collapse from '@mui/material/Collapse';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
|
||||
import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline';
|
||||
import BlockIcon from '@mui/icons-material/Block';
|
||||
import { AgentMessage } from '@/shared/state/agentsSlice';
|
||||
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
import { getToolLabel } from './toolLabels';
|
||||
import BrowserAgentInlineFeed from './BrowserAgentInlineFeed';
|
||||
import { GoogleServiceIcon } from './GoogleServiceIcon';
|
||||
import { ElapsedTimer, formatElapsed } from './toolBubbleChrome';
|
||||
import { useTermColors } from './toolColorize';
|
||||
import { ParsedResult } from './toolResultParsing';
|
||||
import { McpToolInfo, getMcpShortAction } from './mcpToolName';
|
||||
import { McpResultCard } from './McpResultCard';
|
||||
|
||||
interface CompactMcpBubbleProps {
|
||||
call: AgentMessage;
|
||||
input: any;
|
||||
sessionId?: string;
|
||||
isPending: boolean;
|
||||
isStreaming: boolean;
|
||||
isDenied: boolean;
|
||||
isError: boolean;
|
||||
result: AgentMessage | null;
|
||||
mcpInfo: McpToolInfo;
|
||||
toolName: string;
|
||||
resultSummary: string | null;
|
||||
resultElapsedMs: number | null;
|
||||
showTimer: boolean;
|
||||
showBody: boolean;
|
||||
toggle: () => void;
|
||||
parsedResult: ParsedResult | null;
|
||||
isBrowserAgent: boolean;
|
||||
selectAttrs: Record<string, string>;
|
||||
}
|
||||
|
||||
export const CompactMcpBubble: React.FC<CompactMcpBubbleProps> = ({
|
||||
call, input, sessionId, isPending, isStreaming, isDenied, isError, result,
|
||||
mcpInfo, toolName, resultSummary, resultElapsedMs, showTimer, showBody, toggle, parsedResult,
|
||||
isBrowserAgent, selectAttrs,
|
||||
}) => {
|
||||
const c = useClaudeTokens();
|
||||
const tc = useTermColors();
|
||||
|
||||
const shortAction = mcpInfo.isMcp ? getMcpShortAction(mcpInfo) : toolName;
|
||||
const mcpVerbLabel = (() => {
|
||||
const lbl = getToolLabel(toolName, call.id);
|
||||
return result && !isDenied ? lbl.past : lbl.present;
|
||||
})();
|
||||
const serviceLabel = mcpInfo.isMcp ? mcpVerbLabel : shortAction;
|
||||
const ServiceIcon = mcpInfo.isMcp && mcpInfo.service
|
||||
? <GoogleServiceIcon service={mcpInfo.service} size={14} />
|
||||
: null;
|
||||
|
||||
return (
|
||||
<Box {...selectAttrs} sx={{ my: 0 }}>
|
||||
<Box
|
||||
onClick={toggle}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: showBody ? 'flex-start' : 'center',
|
||||
gap: 0.75,
|
||||
px: 1.5,
|
||||
py: 0.6,
|
||||
cursor: 'pointer',
|
||||
borderBottom: showBody ? `1px solid ${c.border.subtle}` : 'none',
|
||||
'&:hover': { bgcolor: 'rgba(0,0,0,0.02)' },
|
||||
}}
|
||||
>
|
||||
{ServiceIcon}
|
||||
<Typography
|
||||
sx={{
|
||||
color: c.accent.primary,
|
||||
fontSize: '0.78rem',
|
||||
fontWeight: 600,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
{serviceLabel}
|
||||
</Typography>
|
||||
{resultSummary && !isError && (
|
||||
<Typography
|
||||
sx={{
|
||||
color: c.text.secondary,
|
||||
fontSize: '0.74rem',
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
...(showBody
|
||||
? { whiteSpace: 'normal', wordBreak: 'break-word' }
|
||||
: { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }),
|
||||
}}
|
||||
>
|
||||
{resultSummary}
|
||||
</Typography>
|
||||
)}
|
||||
{!resultSummary && !showTimer && <Box sx={{ flex: 1 }} />}
|
||||
{showTimer && (
|
||||
<>
|
||||
<Box sx={{ flex: 1 }} />
|
||||
<ElapsedTimer startTime={call.timestamp} />
|
||||
</>
|
||||
)}
|
||||
{isDenied && (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.3 }}>
|
||||
<BlockIcon sx={{ fontSize: 12, color: c.status.error }} />
|
||||
<Typography sx={{ color: c.status.error, fontSize: '0.68rem', fontWeight: 500 }}>denied</Typography>
|
||||
</Box>
|
||||
)}
|
||||
{result && !isDenied && (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.4 }}>
|
||||
{isError && (
|
||||
<ErrorOutlineIcon sx={{ fontSize: 12, color: c.status.error }} />
|
||||
)}
|
||||
{resultElapsedMs != null && (
|
||||
<Typography sx={{ fontSize: '0.63rem', fontFamily: c.font.mono, color: c.text.tertiary }}>
|
||||
{formatElapsed(resultElapsedMs)}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
<IconButton size="small" sx={{ color: c.text.tertiary, p: 0.15, flexShrink: 0 }}>
|
||||
{showBody ? <ExpandLessIcon sx={{ fontSize: 16 }} /> : <ExpandMoreIcon sx={{ fontSize: 16 }} />}
|
||||
</IconButton>
|
||||
</Box>
|
||||
|
||||
<Collapse in={showBody}>
|
||||
<Box
|
||||
sx={{
|
||||
bgcolor: tc.TERM_BG,
|
||||
maxHeight: '60vh',
|
||||
overflowY: 'auto',
|
||||
overflowX: 'hidden',
|
||||
'&::-webkit-scrollbar': { width: 5 },
|
||||
'&::-webkit-scrollbar-track': { background: 'transparent' },
|
||||
'&::-webkit-scrollbar-thumb': { background: tc.SCROLLBAR_THUMB, borderRadius: 3 },
|
||||
}}
|
||||
>
|
||||
{isBrowserAgent && sessionId && (
|
||||
<BrowserAgentInlineFeed
|
||||
parentSessionId={sessionId}
|
||||
browserId={input?.browser_id}
|
||||
/>
|
||||
)}
|
||||
{parsedResult && parsedResult.type === 'mcp' ? (
|
||||
<McpResultCard parsed={parsedResult} compact />
|
||||
) : parsedResult ? (
|
||||
<pre style={{
|
||||
margin: 0, padding: '8px 12px', whiteSpace: 'pre-wrap', wordBreak: 'break-word',
|
||||
fontFamily: c.font.mono, fontSize: '0.73rem', lineHeight: 1.5, color: tc.OUTPUT_COLOR,
|
||||
}}>
|
||||
{parsedResult.type === 'text' ? parsedResult.content : ''}
|
||||
</pre>
|
||||
) : null}
|
||||
{!parsedResult && isPending && !isStreaming && !isBrowserAgent && (
|
||||
<Box sx={{ px: 1.5, py: 1 }}>
|
||||
<Box sx={{ width: 8, height: 2, bgcolor: tc.PROMPT_COLOR, animation: 'tool-pulse 1s ease-in-out infinite', borderRadius: 1 }} />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Collapse>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,176 @@
|
||||
import React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
|
||||
import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline';
|
||||
import BlockIcon from '@mui/icons-material/Block';
|
||||
import CallSplitIcon from '@mui/icons-material/CallSplit';
|
||||
import { AgentMessage } from '@/shared/state/agentsSlice';
|
||||
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
import { ElapsedTimer, formatElapsed } from './toolBubbleChrome';
|
||||
import { AgentResponseBody } from './AgentResponseBody';
|
||||
|
||||
interface CreateAgentBubbleProps {
|
||||
call: AgentMessage;
|
||||
input: any;
|
||||
isPending: boolean;
|
||||
isDenied: boolean;
|
||||
isError: boolean;
|
||||
resultElapsedMs: number | null;
|
||||
expanded: boolean;
|
||||
showTimer: boolean;
|
||||
toggle: () => void;
|
||||
accentRgb: string;
|
||||
createAgentResponse: string;
|
||||
createAgentSessionId: string | null;
|
||||
handleRevealAgent: (e: React.MouseEvent) => void;
|
||||
bubbleRef: React.RefObject<HTMLDivElement>;
|
||||
selectAttrs: Record<string, string>;
|
||||
}
|
||||
|
||||
export const CreateAgentBubble: React.FC<CreateAgentBubbleProps> = ({
|
||||
call, input, isPending, isDenied, isError, resultElapsedMs, expanded, showTimer,
|
||||
toggle, accentRgb, createAgentResponse, createAgentSessionId, handleRevealAgent, bubbleRef, selectAttrs,
|
||||
}) => {
|
||||
const c = useClaudeTokens();
|
||||
const taskPrompt = input?.prompt || input?.task || input?.message || '';
|
||||
const taskLabel = taskPrompt
|
||||
? taskPrompt.length > 40 ? taskPrompt.slice(0, 40) + '…' : taskPrompt
|
||||
: 'Sub-agent';
|
||||
const hasResponse = !!createAgentResponse;
|
||||
|
||||
return (
|
||||
<Box ref={bubbleRef} {...selectAttrs} sx={{ maxWidth: '85%', my: 0.5 }}>
|
||||
<Box
|
||||
sx={{
|
||||
'--glow-rgb': accentRgb,
|
||||
bgcolor: c.bg.elevated,
|
||||
border: `1px solid ${
|
||||
isPending ? c.accent.primary : isDenied ? c.status.error + '60' : c.border.subtle
|
||||
}`,
|
||||
borderRadius: 2,
|
||||
overflow: 'hidden',
|
||||
animation: isPending ? 'border-glow 2s ease-in-out infinite' : 'none',
|
||||
transition: 'border-color 0.3s, box-shadow 0.3s',
|
||||
} as any}
|
||||
>
|
||||
<Box
|
||||
onClick={toggle}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 0.75,
|
||||
px: 1.5,
|
||||
py: 0.75,
|
||||
cursor: hasResponse ? 'pointer' : 'default',
|
||||
'&:hover': hasResponse ? { bgcolor: 'rgba(0,0,0,0.02)' } : {},
|
||||
}}
|
||||
>
|
||||
<CallSplitIcon sx={{ fontSize: 15, color: c.accent.primary, flexShrink: 0 }} />
|
||||
<Typography
|
||||
sx={{
|
||||
color: c.accent.primary,
|
||||
fontSize: '0.8rem',
|
||||
fontWeight: 600,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
CreateAgent
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
bgcolor: `${c.accent.primary}14`,
|
||||
borderRadius: 1,
|
||||
px: 0.75,
|
||||
py: 0.15,
|
||||
maxWidth: 180,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
noWrap
|
||||
sx={{
|
||||
fontSize: '0.72rem',
|
||||
fontWeight: 500,
|
||||
color: c.text.secondary,
|
||||
fontFamily: c.font.sans,
|
||||
}}
|
||||
>
|
||||
{taskLabel}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{!hasResponse && !showTimer && <Box sx={{ flex: 1 }} />}
|
||||
|
||||
{hasResponse && createAgentResponse && !expanded && (
|
||||
<Typography
|
||||
noWrap
|
||||
sx={{
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
fontSize: '0.73rem',
|
||||
color: c.text.tertiary,
|
||||
fontFamily: c.font.sans,
|
||||
}}
|
||||
>
|
||||
{createAgentResponse.slice(0, 100)}{createAgentResponse.length > 100 ? '…' : ''}
|
||||
</Typography>
|
||||
)}
|
||||
{expanded && <Box sx={{ flex: 1 }} />}
|
||||
|
||||
{isDenied && (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.3 }}>
|
||||
<BlockIcon sx={{ fontSize: 13, color: c.status.error }} />
|
||||
<Typography sx={{ color: c.status.error, fontSize: '0.7rem', fontWeight: 500 }}>denied</Typography>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{hasResponse && !isDenied && (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
||||
{isError && (
|
||||
<ErrorOutlineIcon sx={{ fontSize: 13, color: c.status.error }} />
|
||||
)}
|
||||
{resultElapsedMs != null && (
|
||||
<Typography sx={{ fontSize: '0.65rem', fontFamily: c.font.mono, color: c.text.tertiary }}>
|
||||
{formatElapsed(resultElapsedMs)}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{showTimer && <ElapsedTimer startTime={call.timestamp} />}
|
||||
|
||||
{createAgentSessionId && (
|
||||
<Tooltip title="Reveal on dashboard" arrow>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={handleRevealAgent}
|
||||
sx={{
|
||||
color: c.accent.primary,
|
||||
p: 0.25,
|
||||
flexShrink: 0,
|
||||
'&:hover': { bgcolor: `${c.accent.primary}18` },
|
||||
}}
|
||||
>
|
||||
<CallSplitIcon sx={{ fontSize: 15, transform: 'rotate(180deg)' }} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{hasResponse && (
|
||||
<IconButton size="small" sx={{ color: c.text.tertiary, p: 0.25, flexShrink: 0 }}>
|
||||
{expanded ? <ExpandLessIcon sx={{ fontSize: 18 }} /> : <ExpandMoreIcon sx={{ fontSize: 18 }} />}
|
||||
</IconButton>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<AgentResponseBody open={expanded && hasResponse} markdown={createAgentResponse} />
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,299 @@
|
||||
import React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Collapse from '@mui/material/Collapse';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
|
||||
import TerminalIcon from '@mui/icons-material/Terminal';
|
||||
import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline';
|
||||
import BlockIcon from '@mui/icons-material/Block';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import { AgentMessage } from '@/shared/state/agentsSlice';
|
||||
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
import { getToolLabelWithInput } from './toolLabels';
|
||||
import BrowserAgentInlineFeed from './BrowserAgentInlineFeed';
|
||||
import { GoogleServiceIcon } from './GoogleServiceIcon';
|
||||
import { ElapsedTimer, formatElapsed } from './toolBubbleChrome';
|
||||
import { useTermColors, colorizeInput, colorizeOutput } from './toolColorize';
|
||||
import { ParsedResult } from './toolResultParsing';
|
||||
import { McpToolInfo } from './mcpToolName';
|
||||
import { McpResultCard } from './McpResultCard';
|
||||
|
||||
interface DefaultToolBubbleProps {
|
||||
call: AgentMessage;
|
||||
input: any;
|
||||
sessionId?: string;
|
||||
mcpCompact: boolean;
|
||||
isPending: boolean;
|
||||
isStreaming: boolean;
|
||||
isDenied: boolean;
|
||||
isError: boolean;
|
||||
result: AgentMessage | null;
|
||||
mcpInfo: McpToolInfo;
|
||||
toolName: string;
|
||||
inputSummary: string;
|
||||
formattedInput: string;
|
||||
promptPrefix: string;
|
||||
resultSummary: string | null;
|
||||
resultElapsedMs: number | null;
|
||||
showTimer: boolean;
|
||||
showBody: boolean;
|
||||
toggle: () => void;
|
||||
parsedResult: ParsedResult | null;
|
||||
isBrowserAgent: boolean;
|
||||
accentRgb: string;
|
||||
selectAttrs: Record<string, string>;
|
||||
}
|
||||
|
||||
export const DefaultToolBubble: React.FC<DefaultToolBubbleProps> = ({
|
||||
call, input, sessionId, mcpCompact, isPending, isStreaming, isDenied, isError, result,
|
||||
mcpInfo, toolName, inputSummary, formattedInput, promptPrefix, resultSummary, resultElapsedMs,
|
||||
showTimer, showBody, toggle, parsedResult, isBrowserAgent, accentRgb, selectAttrs,
|
||||
}) => {
|
||||
const c = useClaudeTokens();
|
||||
const tc = useTermColors();
|
||||
|
||||
return (
|
||||
<Box {...selectAttrs} sx={{ maxWidth: mcpCompact ? '100%' : '85%', my: mcpCompact ? 0 : 0.5 }}>
|
||||
<Box
|
||||
sx={{
|
||||
'--glow-rgb': accentRgb,
|
||||
bgcolor: mcpCompact ? 'transparent' : c.bg.elevated,
|
||||
border: mcpCompact ? 'none' : `1px solid ${
|
||||
isPending || isStreaming
|
||||
? c.accent.primary
|
||||
: isDenied
|
||||
? c.status.error + '60'
|
||||
: c.border.subtle
|
||||
}`,
|
||||
borderRadius: mcpCompact ? 0 : 2,
|
||||
overflow: 'hidden',
|
||||
animation: (isPending || isStreaming) && !mcpCompact ? 'border-glow 2s ease-in-out infinite' : 'none',
|
||||
transition: 'border-color 0.3s, box-shadow 0.3s',
|
||||
} as any}
|
||||
>
|
||||
<Box
|
||||
onClick={toggle}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 0.75,
|
||||
px: 1.5,
|
||||
py: mcpCompact ? 0.6 : 0.75,
|
||||
cursor: isStreaming ? 'default' : 'pointer',
|
||||
borderBottom: mcpCompact && showBody ? `1px solid ${c.border.subtle}` : 'none',
|
||||
'&:hover': isStreaming ? {} : { bgcolor: 'rgba(0,0,0,0.02)' },
|
||||
}}
|
||||
>
|
||||
{mcpInfo.isMcp && mcpInfo.service
|
||||
? <GoogleServiceIcon service={mcpInfo.service} size={mcpCompact ? 14 : 15} />
|
||||
: (() => {
|
||||
const n = toolName.toLowerCase();
|
||||
if (n.includes('search') || n === 'grep' || n === 'glob')
|
||||
return <SearchIcon sx={{ fontSize: mcpCompact ? 14 : 15, color: c.accent.primary, flexShrink: 0 }} />;
|
||||
return <TerminalIcon sx={{ fontSize: mcpCompact ? 14 : 15, color: c.accent.primary, flexShrink: 0 }} />;
|
||||
})()
|
||||
}
|
||||
<Typography
|
||||
sx={{
|
||||
color: c.accent.primary,
|
||||
fontSize: mcpCompact ? '0.78rem' : '0.8rem',
|
||||
fontWeight: 600,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
{(() => {
|
||||
const { present, past } = getToolLabelWithInput(toolName, input, call.id);
|
||||
return result && !isDenied ? past : present;
|
||||
})()}
|
||||
</Typography>
|
||||
{mcpInfo.isMcp && (
|
||||
<Typography
|
||||
sx={{
|
||||
color: c.text.tertiary,
|
||||
fontSize: '0.65rem',
|
||||
opacity: 0.7,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
{mcpInfo.serverSlug}
|
||||
</Typography>
|
||||
)}
|
||||
{inputSummary && !isStreaming && (
|
||||
<Typography
|
||||
noWrap
|
||||
sx={{
|
||||
color: c.text.tertiary,
|
||||
fontSize: '0.75rem',
|
||||
fontFamily: c.font.mono,
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
}}
|
||||
>
|
||||
{inputSummary}
|
||||
</Typography>
|
||||
)}
|
||||
{!inputSummary && <Box sx={{ flex: 1 }} />}
|
||||
{isStreaming && <Box sx={{ flex: 1 }} />}
|
||||
{isDenied && (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.3 }}>
|
||||
<BlockIcon sx={{ fontSize: 13, color: c.status.error }} />
|
||||
<Typography sx={{ color: c.status.error, fontSize: '0.7rem', fontWeight: 500 }}>
|
||||
denied
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
{result && !isDenied && (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
||||
{isError && (
|
||||
<>
|
||||
<ErrorOutlineIcon sx={{ fontSize: 13, color: c.status.error }} />
|
||||
{resultSummary && (
|
||||
<Typography sx={{ color: c.status.error, fontSize: '0.7rem', fontWeight: 500 }}>
|
||||
{resultSummary}
|
||||
</Typography>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{resultElapsedMs != null && (
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: '0.65rem',
|
||||
fontFamily: c.font.mono,
|
||||
color: c.text.tertiary,
|
||||
}}
|
||||
>
|
||||
{formatElapsed(resultElapsedMs)}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
{showTimer && <ElapsedTimer startTime={call.timestamp} />}
|
||||
|
||||
{!isStreaming && (
|
||||
<IconButton size="small" sx={{ color: c.text.tertiary, p: mcpCompact ? 0.15 : 0.25, flexShrink: 0 }}>
|
||||
{showBody ? (
|
||||
<ExpandLessIcon sx={{ fontSize: mcpCompact ? 16 : 18 }} />
|
||||
) : (
|
||||
<ExpandMoreIcon sx={{ fontSize: mcpCompact ? 16 : 18 }} />
|
||||
)}
|
||||
</IconButton>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Collapse in={showBody}>
|
||||
<Box
|
||||
sx={{
|
||||
bgcolor: tc.TERM_BG,
|
||||
borderTop: `1px solid ${tc.TERM_BORDER}`,
|
||||
maxHeight: 500,
|
||||
overflow: 'auto',
|
||||
'&::-webkit-scrollbar': { width: 5 },
|
||||
'&::-webkit-scrollbar-track': { background: 'transparent' },
|
||||
'&::-webkit-scrollbar-thumb': {
|
||||
background: tc.SCROLLBAR_THUMB,
|
||||
borderRadius: 3,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<pre
|
||||
style={{
|
||||
margin: 0,
|
||||
padding: '8px 12px 0',
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-word',
|
||||
fontFamily: c.font.mono,
|
||||
fontSize: '0.73rem',
|
||||
lineHeight: 1.5,
|
||||
}}
|
||||
>
|
||||
<span style={{ color: tc.PROMPT_COLOR, fontWeight: 600, userSelect: 'none' }}>
|
||||
{promptPrefix}
|
||||
</span>
|
||||
{isStreaming ? (
|
||||
<span style={{ color: tc.CMD_COLOR }}>{call.content?.input ?? ''}</span>
|
||||
) : (
|
||||
colorizeInput(toolName, formattedInput, tc)
|
||||
)}
|
||||
{isStreaming && (
|
||||
<span
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
width: 2,
|
||||
height: '1em',
|
||||
background: c.accent.primary,
|
||||
marginLeft: 2,
|
||||
verticalAlign: 'text-bottom',
|
||||
animation: 'blink-cursor 0.8s step-end infinite',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</pre>
|
||||
|
||||
{isBrowserAgent && sessionId && (
|
||||
<BrowserAgentInlineFeed
|
||||
parentSessionId={sessionId}
|
||||
browserId={input?.browser_id}
|
||||
/>
|
||||
)}
|
||||
|
||||
{parsedResult && parsedResult.type === 'mcp' ? (
|
||||
<McpResultCard parsed={parsedResult} />
|
||||
) : parsedResult ? (
|
||||
<pre
|
||||
style={{
|
||||
margin: 0,
|
||||
padding: '4px 12px 8px',
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-word',
|
||||
fontFamily: c.font.mono,
|
||||
fontSize: '0.73rem',
|
||||
lineHeight: 1.5,
|
||||
}}
|
||||
>
|
||||
{parsedResult.type === 'bash' ? (
|
||||
<>
|
||||
{parsedResult.stdout.trim() &&
|
||||
colorizeOutput(toolName, parsedResult.stdout, tc)}
|
||||
{parsedResult.stderr.trim() && (
|
||||
<>
|
||||
{parsedResult.stdout.trim() && '\n'}
|
||||
<span style={{ color: tc.STDERR_COLOR }}>{parsedResult.stderr}</span>
|
||||
</>
|
||||
)}
|
||||
{!parsedResult.stdout.trim() && !parsedResult.stderr.trim() && (
|
||||
<span style={{ color: tc.DIM_COLOR, fontStyle: 'italic' }}>(no output)</span>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{parsedResult.isError ? (
|
||||
<span style={{ color: tc.STDERR_COLOR }}>{parsedResult.content || '(empty)'}</span>
|
||||
) : (
|
||||
colorizeOutput(toolName, parsedResult.content, tc)
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</pre>
|
||||
) : null}
|
||||
|
||||
{!parsedResult && isPending && !isStreaming && !isBrowserAgent && (
|
||||
<Box sx={{ px: 1.5, pb: 1, pt: 0.5 }}>
|
||||
<Box
|
||||
sx={{
|
||||
width: 8,
|
||||
height: 2,
|
||||
bgcolor: tc.PROMPT_COLOR,
|
||||
animation: 'tool-pulse 1s ease-in-out infinite',
|
||||
borderRadius: 1,
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Collapse>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,181 @@
|
||||
import React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
|
||||
import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline';
|
||||
import BlockIcon from '@mui/icons-material/Block';
|
||||
import CallSplitIcon from '@mui/icons-material/CallSplit';
|
||||
import { AgentMessage } from '@/shared/state/agentsSlice';
|
||||
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
import { ElapsedTimer, formatElapsed } from './toolBubbleChrome';
|
||||
import { AgentResponseBody } from './AgentResponseBody';
|
||||
import { InvokeAgentParsed } from './agentToolParsing';
|
||||
|
||||
interface InvokeAgentBubbleProps {
|
||||
call: AgentMessage;
|
||||
input: any;
|
||||
isPending: boolean;
|
||||
isDenied: boolean;
|
||||
isError: boolean;
|
||||
resultElapsedMs: number | null;
|
||||
expanded: boolean;
|
||||
showTimer: boolean;
|
||||
toggle: () => void;
|
||||
accentRgb: string;
|
||||
invokeAgentParsed: InvokeAgentParsed | null;
|
||||
invokedSessionId: string | null;
|
||||
handleRevealAgent: (e: React.MouseEvent) => void;
|
||||
bubbleRef: React.RefObject<HTMLDivElement>;
|
||||
selectAttrs: Record<string, string>;
|
||||
}
|
||||
|
||||
export const InvokeAgentBubble: React.FC<InvokeAgentBubbleProps> = ({
|
||||
call, input, isPending, isDenied, isError, resultElapsedMs, expanded, showTimer,
|
||||
toggle, accentRgb, invokeAgentParsed, invokedSessionId, handleRevealAgent, bubbleRef, selectAttrs,
|
||||
}) => {
|
||||
const c = useClaudeTokens();
|
||||
const agentName = invokeAgentParsed?.agentName || input?.session_id || 'Agent';
|
||||
const responsePreview = invokeAgentParsed?.response || '';
|
||||
const costLabel = invokeAgentParsed?.cost ? `$${invokeAgentParsed.cost}` : null;
|
||||
const hasResponse = !!invokeAgentParsed;
|
||||
|
||||
return (
|
||||
<Box ref={bubbleRef} {...selectAttrs} sx={{ maxWidth: '85%', my: 0.5 }}>
|
||||
<Box
|
||||
sx={{
|
||||
'--glow-rgb': accentRgb,
|
||||
bgcolor: c.bg.elevated,
|
||||
border: `1px solid ${
|
||||
isPending ? c.accent.primary : isDenied ? c.status.error + '60' : c.border.subtle
|
||||
}`,
|
||||
borderRadius: 2,
|
||||
overflow: 'hidden',
|
||||
animation: isPending ? 'border-glow 2s ease-in-out infinite' : 'none',
|
||||
transition: 'border-color 0.3s, box-shadow 0.3s',
|
||||
} as any}
|
||||
>
|
||||
<Box
|
||||
onClick={toggle}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 0.75,
|
||||
px: 1.5,
|
||||
py: 0.75,
|
||||
cursor: hasResponse ? 'pointer' : 'default',
|
||||
'&:hover': hasResponse ? { bgcolor: 'rgba(0,0,0,0.02)' } : {},
|
||||
}}
|
||||
>
|
||||
<CallSplitIcon sx={{ fontSize: 15, color: c.accent.primary, flexShrink: 0 }} />
|
||||
<Typography
|
||||
sx={{
|
||||
color: c.accent.primary,
|
||||
fontSize: '0.8rem',
|
||||
fontWeight: 600,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
InvokeAgent
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
bgcolor: `${c.accent.primary}14`,
|
||||
borderRadius: 1,
|
||||
px: 0.75,
|
||||
py: 0.15,
|
||||
maxWidth: 180,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
noWrap
|
||||
sx={{
|
||||
fontSize: '0.72rem',
|
||||
fontWeight: 500,
|
||||
color: c.text.secondary,
|
||||
fontFamily: c.font.sans,
|
||||
}}
|
||||
>
|
||||
{agentName}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{!hasResponse && !showTimer && <Box sx={{ flex: 1 }} />}
|
||||
|
||||
{hasResponse && responsePreview && !expanded && (
|
||||
<Typography
|
||||
noWrap
|
||||
sx={{
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
fontSize: '0.73rem',
|
||||
color: c.text.tertiary,
|
||||
fontFamily: c.font.sans,
|
||||
}}
|
||||
>
|
||||
{responsePreview.slice(0, 100)}{responsePreview.length > 100 ? '…' : ''}
|
||||
</Typography>
|
||||
)}
|
||||
{expanded && <Box sx={{ flex: 1 }} />}
|
||||
|
||||
{isDenied && (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.3 }}>
|
||||
<BlockIcon sx={{ fontSize: 13, color: c.status.error }} />
|
||||
<Typography sx={{ color: c.status.error, fontSize: '0.7rem', fontWeight: 500 }}>denied</Typography>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{hasResponse && !isDenied && (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
||||
{isError && (
|
||||
<ErrorOutlineIcon sx={{ fontSize: 13, color: c.status.error }} />
|
||||
)}
|
||||
{resultElapsedMs != null && (
|
||||
<Typography sx={{ fontSize: '0.65rem', fontFamily: c.font.mono, color: c.text.tertiary }}>
|
||||
{formatElapsed(resultElapsedMs)}
|
||||
</Typography>
|
||||
)}
|
||||
{costLabel && (
|
||||
<Typography sx={{ fontSize: '0.63rem', fontFamily: c.font.mono, color: c.text.tertiary }}>
|
||||
{costLabel}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{showTimer && <ElapsedTimer startTime={call.timestamp} />}
|
||||
|
||||
{invokedSessionId && (
|
||||
<Tooltip title="Reveal on dashboard" arrow>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={handleRevealAgent}
|
||||
sx={{
|
||||
color: c.accent.primary,
|
||||
p: 0.25,
|
||||
flexShrink: 0,
|
||||
'&:hover': { bgcolor: `${c.accent.primary}18` },
|
||||
}}
|
||||
>
|
||||
<CallSplitIcon sx={{ fontSize: 15, transform: 'rotate(180deg)' }} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{hasResponse && (
|
||||
<IconButton size="small" sx={{ color: c.text.tertiary, p: 0.25, flexShrink: 0 }}>
|
||||
{expanded ? <ExpandLessIcon sx={{ fontSize: 18 }} /> : <ExpandMoreIcon sx={{ fontSize: 18 }} />}
|
||||
</IconButton>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<AgentResponseBody open={expanded && hasResponse} markdown={responsePreview} />
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user