mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-29 19:29:48 +02:00
[eric] chat: MCP connect notice is ONE slim ephemeral bubble; inline table deleted, header/description stripped, 12s auto-clear (ENG-350), plus the ENG-345 scroll hooks
This commit is contained in:
@@ -383,6 +383,13 @@ const AgentChat: React.FC<AgentChatProps> = ({ sessionId: sessionIdProp, onClose
|
||||
const [activateError, setActivateError] = useState<string | null>(null);
|
||||
// Holds the last non-empty suggestions so the docked banner's exit fade renders them instead of going blank the instant the array is cleared.
|
||||
const mcpSnapshotRef = useRef<Array<{ id: string; title: string; description: string; reason?: string }>>([]);
|
||||
// The connect nudge is ephemeral (ENG-350): auto-clears locally after 12s; a real dismiss (the x) also persists.
|
||||
const mcpSuggestionCount = session?.mcp_suggestions?.length ?? 0;
|
||||
useEffect(() => {
|
||||
if (!id || mcpSuggestionCount === 0) return;
|
||||
const t = window.setTimeout(() => dispatch(clearMcpSuggestions({ sessionId: id })), 12000);
|
||||
return () => window.clearTimeout(t);
|
||||
}, [id, mcpSuggestionCount, dispatch]);
|
||||
const [mode, setMode] = useState('agent');
|
||||
const [model, setModel] = useState('opus-5');
|
||||
// Workflow build chat only: brief "this model now runs the workflow" notice when the user switches models, so the run-model change isn't silent.
|
||||
@@ -989,6 +996,8 @@ const AgentChat: React.FC<AgentChatProps> = ({ sessionId: sessionIdProp, onClose
|
||||
|
||||
const handleApprove = (requestId: string, updatedInput?: Record<string, any>, trustPattern?: boolean, alwaysAllow?: boolean) => {
|
||||
dispatch(handleApproval({ requestId, behavior: 'allow', updatedInput, trustPattern, setAlwaysAllow: alwaysAllow }));
|
||||
// Answering pins to bottom like a composer send, or the agent's next message lands below the fold (ENG-345).
|
||||
scrollToBottom();
|
||||
};
|
||||
|
||||
const handleDeny = (requestId: string, message?: string) => {
|
||||
@@ -1863,7 +1872,7 @@ const AgentChat: React.FC<AgentChatProps> = ({ sessionId: sessionIdProp, onClose
|
||||
}
|
||||
return (
|
||||
<Box key={item.id} data-window-item-id={item.id} ref={isLastVisibleItem ? lastVisibleItemRef : undefined}>
|
||||
<AskUiBubble pair={item} sessionId={session.id} isPending={isPending} suppressReveal={item.call.id === justStreamedId} />
|
||||
<AskUiBubble pair={item} sessionId={session.id} isPending={isPending} suppressReveal={item.call.id === justStreamedId} onAnswered={scrollToBottom} />
|
||||
{compactionChip}
|
||||
</Box>
|
||||
);
|
||||
@@ -2015,84 +2024,6 @@ const AgentChat: React.FC<AgentChatProps> = ({ sessionId: sessionIdProp, onClose
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
{/* Connect offer sits BELOW the latest reply (where the eye is), not at the top of the
|
||||
transcript where the auto-scroll-to-bottom buries it. Suggest-only; activation is the
|
||||
user's click through the gated MCPActivate endpoint. */}
|
||||
{(session.mcp_suggestions && session.mcp_suggestions.length > 0) && (
|
||||
<Box sx={{ mt: 1, mb: 1, px: 0.5, display: 'flex', flexDirection: 'column', gap: 0.5, overflowAnchor: 'none' }}>
|
||||
{session.mcp_suggestions.map((s) => (
|
||||
<Box key={s.id} sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Typography variant="caption" sx={{ color: c.text.secondary, flex: 1, minWidth: 0 }}>
|
||||
Connect{' '}
|
||||
<Box component="span" sx={{ color: c.text.primary, fontWeight: 500 }}>{s.title}</Box>
|
||||
{' '}so the agent can do this
|
||||
</Typography>
|
||||
<Typography
|
||||
component="button"
|
||||
variant="caption"
|
||||
disabled={activatingMcp === s.id}
|
||||
onClick={async () => {
|
||||
if (activatingMcp) return;
|
||||
setActivateError(null);
|
||||
setActivatingMcp(s.id);
|
||||
try {
|
||||
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
|
||||
const tok = (() => { try { return getAuthToken(); } catch { return ''; } })();
|
||||
if (tok) headers['Authorization'] = `Bearer ${tok}`;
|
||||
const r = await fetch(`${API_BASE}/mcp-meta/activate`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
server_name: s.id.toLowerCase().replace(/\s+/g, '-'),
|
||||
reason: s.reason || 'preflight suggestion',
|
||||
parent_session_id: session.id,
|
||||
}),
|
||||
});
|
||||
const body = await r.json().catch(() => ({} as any));
|
||||
if (!r.ok) {
|
||||
setActivateError(`Activation failed (${r.status})`);
|
||||
} else if (body?.status === 'unknown_server') {
|
||||
// Not yet connected; jump to Actions so the user can finish OAuth.
|
||||
openMarketplace('my-connectors');
|
||||
} else if (id) {
|
||||
dispatch(clearMcpSuggestions({ sessionId: id }));
|
||||
}
|
||||
} catch (e: any) {
|
||||
setActivateError(e?.message || 'Activation failed');
|
||||
} finally {
|
||||
setActivatingMcp(null);
|
||||
}
|
||||
}}
|
||||
sx={{
|
||||
border: 'none',
|
||||
background: 'none',
|
||||
p: 0,
|
||||
color: c.accent.primary,
|
||||
cursor: activatingMcp === s.id ? 'wait' : 'pointer',
|
||||
opacity: activatingMcp === s.id ? 0.5 : 1,
|
||||
'&:hover': { textDecoration: activatingMcp ? 'none' : 'underline' },
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
{activatingMcp === s.id ? 'Connecting…' : 'Connect'}
|
||||
</Typography>
|
||||
</Box>
|
||||
))}
|
||||
{activateError && (
|
||||
<Typography variant="caption" sx={{ display: 'block', color: c.status.error }}>
|
||||
{activateError}
|
||||
</Typography>
|
||||
)}
|
||||
<Box
|
||||
role="button"
|
||||
aria-label="Dismiss"
|
||||
onClick={() => id && dispatch(clearMcpSuggestions({ sessionId: id }))}
|
||||
sx={{ alignSelf: 'flex-start', color: c.text.muted, cursor: 'pointer', fontSize: '0.75rem', '&:hover': { color: c.text.secondary } }}
|
||||
>
|
||||
Dismiss
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
{/* The surfaces this agent is driving, live inside the chat: browser snapshots + built
|
||||
apps, each one click from popping out onto the canvas. */}
|
||||
{!isDraft && id && <InlineSurfaceEmbeds c={c} sessionId={id} fullscreen={fullscreenChat} />}
|
||||
@@ -2460,7 +2391,8 @@ const AgentChat: React.FC<AgentChatProps> = ({ sessionId: sessionIdProp, onClose
|
||||
<Box sx={{
|
||||
mx: 2,
|
||||
mb: 1,
|
||||
p: 1.5,
|
||||
py: 0.75,
|
||||
px: 1.25,
|
||||
borderRadius: 1.5,
|
||||
border: `1px solid ${c.border.medium}`,
|
||||
bgcolor: c.bg.secondary,
|
||||
@@ -2493,24 +2425,13 @@ const AgentChat: React.FC<AgentChatProps> = ({ sessionId: sessionIdProp, onClose
|
||||
>
|
||||
×
|
||||
</Box>
|
||||
<Typography variant="body2" sx={{ color: c.text.primary, fontWeight: 500, mb: 0.5, pr: 3 }}>
|
||||
Looks like this might need an integration
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ color: c.text.secondary, display: 'block', mb: 1 }}>
|
||||
Activating one of these will let the agent answer in a single round-trip.
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
|
||||
{display.map((s) => (
|
||||
<Box key={s.id} sx={{ flexBasis: '100%', display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 1 }}>
|
||||
<Box sx={{ flex: 1, minWidth: 0 }}>
|
||||
<Typography variant="caption" sx={{ color: c.text.primary, fontWeight: 500 }}>
|
||||
{s.title}
|
||||
<Typography variant="caption" sx={{ color: c.text.secondary }}>
|
||||
Connect <Box component="span" sx={{ color: c.text.primary, fontWeight: 500 }}>{s.title}</Box>
|
||||
</Typography>
|
||||
{s.reason && (
|
||||
<Typography variant="caption" sx={{ display: 'block', color: c.text.tertiary }}>
|
||||
{s.reason}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
<Typography
|
||||
component="button"
|
||||
|
||||
Reference in New Issue
Block a user