[eric] subscriptions: confirm active node before Connected (claude+codex), re-check on run-fail, honest copy

(cherry picked from commit 3ce44f314b3b51e48692d5f47df38d5bc43ec0f6)
This commit is contained in:
ciregenz
2026-06-19 00:25:54 -07:00
parent f0cb676afb
commit 6193863408
2 changed files with 31 additions and 4 deletions
@@ -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<Props> = 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]);
@@ -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();
};