diff --git a/frontend/src/app/components/Layout/AppShell.tsx b/frontend/src/app/components/Layout/AppShell.tsx
index d22ae9f6..07c8e5d6 100644
--- a/frontend/src/app/components/Layout/AppShell.tsx
+++ b/frontend/src/app/components/Layout/AppShell.tsx
@@ -23,7 +23,7 @@ import { LayoutGrid } from 'lucide-react';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import { Settings as LucideSettings } from 'lucide-react';
import { Palette } from 'lucide-react';
-import { ArrowLeft, ArrowRight, Plus } from 'lucide-react';
+import { ArrowLeft, ArrowRight, Plus, Clock } from 'lucide-react';
import { AnimatedPanelLeft } from './animatedIcons';
import RestartAltIcon from '@mui/icons-material/RestartAlt';
import SystemUpdateAltIcon from '@mui/icons-material/SystemUpdateAlt';
@@ -177,6 +177,28 @@ const AppShell: React.FC = () => {
if (h >= 1) return `~${h}h`;
return `~${Math.max(1, Math.round(secs / 60))}m`;
}, [freeTrialResetsAt]);
+
+ // Paid (openswarm-pro) usage meter: same calm "you're near/at the cap, here's when it's back"
+ // pattern as the free-trial nudge, but the bar IS the message. Only fires in pro mode on real
+ // server-owned usage (requests_in_window/plan_limit), and only once near the cap, so it never
+ // clutters the normal flow. window_ends_at is unix MS (the trial's resets_at is seconds).
+ const proUsage = useAppSelector((s) => {
+ const d = s.settings.data as any;
+ if (!d || d.connection_mode !== 'openswarm-pro') return null;
+ const u = d.openswarm_usage_cached;
+ return u && u.plan_limit > 0 ? u : null;
+ }, shallowEqual);
+ const proPct = proUsage ? Math.min(1, proUsage.requests_in_window / proUsage.plan_limit) : 0;
+ const proMaxed = !!proUsage && proPct >= 1;
+ const showUsageNudge = isOnline && !!proUsage && proPct >= 0.8;
+ const usageResetLabel = React.useMemo(() => {
+ const endsAt = proUsage?.window_ends_at ?? 0;
+ if (!endsAt) return null;
+ const secs = (endsAt - Date.now()) / 1000;
+ if (secs <= 90) return null;
+ const h = Math.floor(secs / 3600);
+ return h >= 1 ? `~${h}h` : `~${Math.max(1, Math.round(secs / 60))}m`;
+ }, [proUsage]);
// 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);
@@ -610,6 +632,30 @@ const AppShell: React.FC = () => {
+
+
+ {/* the bar is the message: how full your Pro window is. calm accent, never red. */}
+
+
+
+ {usageResetLabel && (
+
+
+ {usageResetLabel}
+
+ )}
+ {proMaxed && (
+ dispatch(openSettingsModal('models'))}
+ sx={{ color: c.accent.primary, cursor: 'pointer', fontSize: '0.8rem', '&:hover': { textDecoration: 'underline' } }}
+ >
+ Upgrade
+
+ )}
+
+
+
{showUpdateBanner && (