From ab0c265d7e3bb8258a75c1aaf8e01cd7e6cf29f8 Mon Sep 17 00:00:00 2001 From: ciregenz Date: Wed, 20 May 2026 02:06:26 -0700 Subject: [PATCH] chat: dismissable MCP suggestion banner + auto-clear on activate + jump to /actions on unknown_server MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds an × button to the integration-suggestion banner so it doesn't linger after the user dismisses it. On successful MCP activate, clears the banner so the user gets visual confirmation the click did something. On unknown_server response (not yet connected), navigate to /actions so the user can finish OAuth there. --- .../src/app/pages/AgentChat/AgentChat.tsx | 40 ++++++++++++++++++- frontend/src/shared/state/agentsSlice.ts | 8 ++++ 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/pages/AgentChat/AgentChat.tsx b/frontend/src/app/pages/AgentChat/AgentChat.tsx index 977c4429..cae5190d 100644 --- a/frontend/src/app/pages/AgentChat/AgentChat.tsx +++ b/frontend/src/app/pages/AgentChat/AgentChat.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useRef, useMemo, useState, useCallback } from 'react'; -import { useParams } from 'react-router-dom'; +import { useNavigate, useParams } from 'react-router-dom'; import Box from '@mui/material/Box'; import Typography from '@mui/material/Typography'; import Chip from '@mui/material/Chip'; @@ -37,6 +37,7 @@ import { fetchSession, AgentMessage, clearSessionMessages, + clearMcpSuggestions, } from '@/shared/state/agentsSlice'; import { fetchModes } from '@/shared/state/modesSlice'; import { createSessionWs } from '@/shared/ws/WebSocketManager'; @@ -167,6 +168,7 @@ const AgentChat: React.FC = ({ sessionId: sessionIdProp, onClose }; const { id: routeId } = useParams<{ id: string }>(); const id = sessionIdProp || routeId; + const navigate = useNavigate(); const dispatch = useAppDispatch(); const session = useAppSelector((state) => (id ? state.agents.sessions[id] : undefined)); const modesMap = useAppSelector((state) => state.modes.items); @@ -1015,8 +1017,32 @@ const AgentChat: React.FC = ({ sessionId: sessionIdProp, onClose borderRadius: 1.5, border: `1px solid ${c.border.medium}`, bgcolor: c.bg.secondary, + position: 'relative', }}> - + id && dispatch(clearMcpSuggestions({ sessionId: id }))} + sx={{ + position: 'absolute', + top: 6, + right: 8, + width: 20, + height: 20, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + fontSize: '1rem', + lineHeight: 1, + color: c.text.muted, + cursor: 'pointer', + borderRadius: 0.75, + '&:hover': { color: c.text.primary, bgcolor: c.bg.elevated }, + }} + > + × + + Looks like this might need an integration @@ -1056,8 +1082,18 @@ const AgentChat: React.FC = ({ sessionId: sessionIdProp, onClose 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 straight to Actions + // so the user can finish OAuth. Nothing here + // can do it on their behalf. + navigate('/actions'); + } else if (id) { + // Activation succeeded; clear the banner so the user + // gets visual confirmation the click did something. + dispatch(clearMcpSuggestions({ sessionId: id })); } } catch (e: any) { setActivateError(e?.message || 'Activation failed'); diff --git a/frontend/src/shared/state/agentsSlice.ts b/frontend/src/shared/state/agentsSlice.ts index 0d41fe68..218b83dd 100644 --- a/frontend/src/shared/state/agentsSlice.ts +++ b/frontend/src/shared/state/agentsSlice.ts @@ -1068,6 +1068,8 @@ const agentsSlice = createSlice({ ? existing.pending_approvals : s.pending_approvals ?? [], tool_group_meta: { ...existing?.tool_group_meta, ...s.tool_group_meta }, + mcp_suggestions: existing?.mcp_suggestions ?? [], + mcp_suggestions_is_vague: existing?.mcp_suggestions_is_vague ?? false, }; if (activeStatuses.has(s.status) && !state.trackedNotificationIds.includes(s.id)) { state.trackedNotificationIds.push(s.id); @@ -1257,6 +1259,12 @@ const agentsSlice = createSlice({ ...session, pending_approvals: session.pending_approvals ?? existing?.pending_approvals ?? [], tool_group_meta: session.tool_group_meta ?? existing?.tool_group_meta ?? {}, + // mcp_suggestions live in client state only (the backend never + // returns them in the session payload). Preserve them across + // refresh so the suggestion banner stays put until the user + // dismisses it or activates one. + mcp_suggestions: existing?.mcp_suggestions ?? [], + mcp_suggestions_is_vague: existing?.mcp_suggestions_is_vague ?? false, }; }) .addCase(fetchSession.rejected, (state, action) => {