mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-31 04:09:44 +02:00
[eric] settings: a dead backend greeted a configured user as new and tried to sell them a subscription
This commit is contained in:
@@ -208,6 +208,7 @@ const SettingsLoader: React.FC<{ children: React.ReactNode }> = ({ children }) =
|
||||
const accentColor = useAppSelector((s) => s.settings.data.accent_color);
|
||||
const accentGradient = useAppSelector((s) => s.settings.data.accent_gradient);
|
||||
const loaded = useAppSelector((s) => s.settings.loaded);
|
||||
const settled = useAppSelector((s) => s.settings.settled);
|
||||
const allowExperimentalUpdates = useAppSelector((s) => s.settings.data.allow_experimental_updates);
|
||||
useEffect(() => {
|
||||
dispatch(fetchSettings());
|
||||
@@ -275,8 +276,8 @@ const SettingsLoader: React.FC<{ children: React.ReactNode }> = ({ children }) =
|
||||
if (!loaded) return;
|
||||
(window as any).openswarm?.setAllowPrerelease?.(allowExperimentalUpdates);
|
||||
}, [loaded, allowExperimentalUpdates]);
|
||||
// Hold paint until settings land so the user's theme renders first; Electron's ready-to-show relies on this.
|
||||
if (!loaded) return null;
|
||||
// Hold paint until the settings fetch SETTLES so the user's theme renders first; Electron's ready-to-show relies on this. Settling, not succeeding: a backend that never answers used to leave a blank window forever.
|
||||
if (!settled) return null;
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
|
||||
@@ -88,6 +88,8 @@ const AppShell: React.FC = () => {
|
||||
}, []);
|
||||
|
||||
const modelsLoaded = useAppSelector((s) => s.models.loaded);
|
||||
// The models list is marked loaded even when its fetch fails, so it alone can't tell "no model" from "couldn't ask". Settings is where the user's own key/sub lives, so the banner waits for it.
|
||||
const settingsKnown = useAppSelector((s) => s.settings.loaded);
|
||||
// "Connected" = the user's OWN model (key/sub/pro/custom), NOT a non-empty /models list: the free-trial Haiku is always in that list now, so a byProvider-length check would falsely read as connected and hide the out-of-runs banner.
|
||||
const hasModelConnected = useAppSelector(selectHasModelConnected);
|
||||
// While onboarding owns the window, the floating sidebar (and its hover-peek strip) must not exist; both out-z the overlay.
|
||||
@@ -147,12 +149,14 @@ const AppShell: React.FC = () => {
|
||||
// Hold the banner until the boot free-trial mint settles, else a brand-new user sees it flash red for the ~1-3s the trial takes to arm. (Offline shows immediately, it's its own signal.)
|
||||
const freeTrialArmSettled = useAppSelector((s) => s.settings.freeTrialArmSettled);
|
||||
// The red wall is for genuine "no way to run" only; the free-trial states get the quiet nudge below.
|
||||
const showWarningBanner = !isOnline || (modelsLoaded && freeTrialArmSettled && !hasModelConnected && !freeTrialActive && !freeTrialSpent);
|
||||
const settingsSettled = useAppSelector((s) => s.settings.settled);
|
||||
const backendUnreachable = settingsSettled && !settingsKnown;
|
||||
const showWarningBanner = !isOnline || backendUnreachable || (settingsKnown && modelsLoaded && freeTrialArmSettled && !hasModelConnected && !freeTrialActive && !freeTrialSpent);
|
||||
const [ftNudgeDismissed, setFtNudgeDismissed] = useState<boolean>(() => {
|
||||
try { return localStorage.getItem('os_ft_nudge_dismissed') === '1'; } catch { return false; }
|
||||
});
|
||||
// Spent nudge hides the moment they connect a real model; the post-wow nudge only shows on the trial lane (so it already implies no own model) and is dismissible.
|
||||
const showFreeTrialNudge = isOnline && ((freeTrialSpent && !hasModelConnected) || (freeTrialUsed && !ftNudgeDismissed));
|
||||
const showFreeTrialNudge = isOnline && settingsKnown && ((freeTrialSpent && !hasModelConnected) || (freeTrialUsed && !ftNudgeDismissed));
|
||||
|
||||
const bannerDismissedForVersion = availableVersion != null && dismissedVersion === availableVersion;
|
||||
const isUpdateActionable = updateStatus === 'available' || updateStatus === 'downloaded' || updateStatus === 'downloading';
|
||||
@@ -478,6 +482,8 @@ const AppShell: React.FC = () => {
|
||||
<Typography sx={{ fontSize: '0.875rem', color: '#ef4444', flex: 1, fontWeight: 500, letterSpacing: '0.01em' }}>
|
||||
{!isOnline
|
||||
? 'No internet connection; agents cannot reach AI models or external services'
|
||||
: backendUnreachable
|
||||
? 'Cannot reach the OpenSwarm backend; your settings and agents are unavailable until it comes back'
|
||||
: (
|
||||
<>
|
||||
No AI model connected.{' '}
|
||||
|
||||
@@ -66,6 +66,7 @@ const DashboardEmptyState: React.FC<{
|
||||
const model = useAppSelector((s) => s.settings.data.default_model);
|
||||
const mode = useAppSelector((s) => s.settings.data.default_mode);
|
||||
const canRun = useAppSelector((s) => hasFreeTrialActive(s) || hasModelConnected(s));
|
||||
const settingsKnown = useAppSelector((s) => s.settings.loaded);
|
||||
const personalized = useAppSelector((s) => s.settings.data.personalized_starters ?? []);
|
||||
const personalizedMenu = useAppSelector((s) => s.settings.data.personalized_menu ?? null);
|
||||
const userName = useAppSelector((s) => s.settings.data.user_name ?? null);
|
||||
@@ -248,7 +249,8 @@ const DashboardEmptyState: React.FC<{
|
||||
</>
|
||||
) : (
|
||||
<Typography sx={{ color: c.text.ghost, fontSize: c.font.size.base, textAlign: 'center' }}>
|
||||
Connect a model in Settings to get started.
|
||||
{/* With the backend down we don't know what the user has connected, so don't tell them they have nothing. */}
|
||||
{settingsKnown ? 'Connect a model in Settings to get started.' : 'Waiting for the OpenSwarm backend...'}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
@@ -139,7 +139,12 @@ export interface BrowseResult {
|
||||
interface SettingsState {
|
||||
data: AppSettings;
|
||||
loading: boolean;
|
||||
/** We have the user's real settings. Anything that judges the user (no model connected, onboarding
|
||||
* not done, free runs spent) must read THIS, never `settled`: a failed fetch is not an answer. */
|
||||
loaded: boolean;
|
||||
/** The first fetch finished, either way. Only the paint gate wants this, so a dead backend shows a
|
||||
* window with an honest error instead of a blank one. */
|
||||
settled: boolean;
|
||||
modalOpen: boolean;
|
||||
/** When non-null, Settings opens to this tab instead of 'general'. */
|
||||
initialTab: string | null;
|
||||
@@ -182,6 +187,7 @@ const initialState: SettingsState = {
|
||||
data: DEFAULT_SETTINGS,
|
||||
loading: false,
|
||||
loaded: false,
|
||||
settled: false,
|
||||
modalOpen: false,
|
||||
initialTab: null,
|
||||
draft: null,
|
||||
@@ -334,6 +340,7 @@ const settingsSlice = createSlice({
|
||||
.addCase(fetchSettings.fulfilled, (state, action) => {
|
||||
state.loading = false;
|
||||
state.loaded = true;
|
||||
state.settled = true;
|
||||
// Drop a stale response: on boot three fetches race (initial, sub-sync, free-trial mint); if the pre-mint one resolves last it would wipe the armed trial. Newest wins.
|
||||
if (state.latestWriteId && action.meta.requestId !== state.latestWriteId) return;
|
||||
// Fill any field an older backend shape omitted so no consumer reads undefined; the payload still wins for everything it does send.
|
||||
@@ -347,7 +354,8 @@ const settingsSlice = createSlice({
|
||||
})
|
||||
.addCase(fetchSettings.rejected, (state) => {
|
||||
state.loading = false;
|
||||
state.loaded = true;
|
||||
state.settled = true;
|
||||
// `loaded` deliberately stays put. A failed fetch is not an answer, and claiming one made every gate read DEFAULT_SETTINGS as fact: with the backend down the app greeted a configured user as a brand-new one and offered to sell them a subscription. A refresh that fails keeps the last good copy; a first fetch that fails stays unknown.
|
||||
})
|
||||
.addCase(updateSettingsPatch.fulfilled, (state, action) => {
|
||||
// A user save is authoritative; claim newest so an in-flight GET can't overwrite it, and consume the draft so reopening shows the saved state.
|
||||
|
||||
Reference in New Issue
Block a user