diff --git a/frontend/src/app/pages/AgentChat/bubbles/MessageBubble.tsx b/frontend/src/app/pages/AgentChat/bubbles/MessageBubble.tsx index 3358b95b..f84dbead 100644 --- a/frontend/src/app/pages/AgentChat/bubbles/MessageBubble.tsx +++ b/frontend/src/app/pages/AgentChat/bubbles/MessageBubble.tsx @@ -23,6 +23,7 @@ import { estimateRenderedTextHeight, oversizedCharThreshold, RECHECK_VISIBILITY_ import { THINKING_LABELS } from '../thinkingLabels'; import { AgentMessage, retryLastUserMessage } from '@/shared/state/agentsSlice'; import { openSettingsModal } from '@/shared/state/settingsSlice'; +import { fetchSubscriptionStatus } from '@/shared/state/subscriptionsSlice'; import { shallowEqual } from 'react-redux'; import { useAppDispatch, useAppSelector } from '@/shared/hooks'; import { useClaudeTokens } from '@/shared/styles/ThemeContext'; @@ -209,8 +210,8 @@ function parseOpenSwarmError(text: string, ctx?: OverflowContext): OpenSwarmErro if (/No active subscription|Subscription canceled|Subscription past_due|Invalid.*token|Missing bearer token/i.test(text)) { return { kind: 'auth', - title: 'Subscription issue', - detail: "We can't find an active OpenSwarm subscription. Check your billing status.", + title: 'Connection needs a refresh', + detail: "This model's account isn't active right now. Reconnect it in Settings, or switch to a different model.", ctaLabel: 'Open Settings', ctaAction: 'settings', }; @@ -1050,6 +1051,15 @@ const MessageBubble: React.FC = React.memo(({ message, editing = false, o } }, [message.id, openswarmError?.kind]); + // A run that failed on a subscription/connection error means the card may be + // showing a stale "Connected" (the optimistic mark, or a token that went stale + // mid-session); re-pull the real 9Router/cloud status so it flips to Reconnect. + React.useEffect(() => { + if (openswarmError?.kind === 'auth') { + dispatch(fetchSubscriptionStatus()); + } + }, [message.id, openswarmError?.kind, dispatch]); + React.useEffect(() => { if (editing) setEditText(rawText); }, [editing, rawText]); diff --git a/frontend/src/app/pages/Settings/sections/subscription/subscriptionConnect.ts b/frontend/src/app/pages/Settings/sections/subscription/subscriptionConnect.ts index c63447c8..25671357 100644 --- a/frontend/src/app/pages/Settings/sections/subscription/subscriptionConnect.ts +++ b/frontend/src/app/pages/Settings/sections/subscription/subscriptionConnect.ts @@ -197,9 +197,26 @@ function runAuthCodeFlow(ctx: ConnectCtx) { try { body = await r.json(); } catch {} succeeded = r.ok && !!body?.success; } catch {} - // 9Router /providers lags /exchange; an immediate fetchStatus would clobber the UI. - if (succeeded) markConnected(providerId); + // /providers lags /exchange by a few seconds, so confirm an actually-active + // node within a short window instead of trusting bare HTTP success. A handoff + // that returns success but never lands a usable node must NOT show "Connected" + // (that was the card-says-connected-but-every-run-fails bug); it resets so the + // user sees Connect/retry. + let confirmed = false; + if (succeeded) { + for (let i = 0; i < 6 && !confirmed; i++) { + try { + const sr = await fetch(`${API_BASE}/agents/subscriptions/status`); + const sd = await sr.json(); + const connections = sd.providers?.connections || []; + confirmed = connections.some((p: any) => p.provider === providerId && (p.isActive || p.testStatus === 'active')); + } catch {} + if (!confirmed) await new Promise((r) => setTimeout(r, 1500)); + } + } + if (confirmed) markConnected(providerId); setConnecting(null); + fetchStatus(); refreshPickerModels(); };