From 4157b107f7843f5c5c144299f0cb7c3002d330de Mon Sep 17 00:00:00 2001 From: James Grugett Date: Tue, 12 May 2026 21:39:11 -0700 Subject: [PATCH] Label Freebuff limited mode --- cli/src/components/waiting-room-screen.tsx | 51 ++++++++++++++++++- cli/src/hooks/use-freebuff-session.ts | 20 ++++++++ common/src/types/freebuff-session.ts | 24 ++++++--- .../session/__tests__/session.test.ts | 31 +++++++++++ .../app/api/v1/freebuff/session/_handlers.ts | 10 ++++ .../__tests__/session-view.test.ts | 22 ++++++++ web/src/server/free-session/session-view.ts | 12 +++++ 7 files changed, 160 insertions(+), 10 deletions(-) diff --git a/cli/src/components/waiting-room-screen.tsx b/cli/src/components/waiting-room-screen.tsx index f970c3868b..1ef49d35e1 100644 --- a/cli/src/components/waiting-room-screen.tsx +++ b/cli/src/components/waiting-room-screen.tsx @@ -80,6 +80,8 @@ const PRIVACY_SIGNAL_LABELS: Partial> = res_proxy: 'residential proxy', tor: 'Tor', vpn: 'VPN', + hosting: 'hosting network', + service: 'privacy service', } const formatPrivacySignalList = ( @@ -101,6 +103,38 @@ const formatPrivacySignalList = ( return `${labels.slice(0, -1).join(', ')}, or ${labels[labels.length - 1]}` } +const getLimitedModeReason = ( + session: FreebuffSessionResponse | null, +): string | null => { + if (!session || !('countryBlockReason' in session)) { + return 'reduced free model access' + } + + const countryCode = + 'countryCode' in session && + session.countryCode && + session.countryCode !== 'UNKNOWN' + ? session.countryCode + : null + + switch (session.countryBlockReason) { + case 'anonymous_network': + return `${formatPrivacySignalList( + session.ipPrivacySignals ?? undefined, + )} detected` + case 'country_not_allowed': + return `outside available countries${countryCode ? ` (${countryCode})` : ''}` + case 'anonymized_or_unknown_country': + case 'missing_client_ip': + case 'unresolved_client_ip': + return 'location could not be verified' + case 'ip_privacy_lookup_failed': + return 'network check could not finish' + default: + return 'reduced free model access' + } +} + const TakeoverPrompt: React.FC = () => { const theme = useTheme() const [pending, setPending] = useState(false) @@ -261,6 +295,8 @@ export const WaitingRoomScreen: React.FC = ({ const isQueued = session?.status === 'queued' const accessTier = session && 'accessTier' in session ? session.accessTier : 'full' + const limitedModeReason = + accessTier === 'limited' ? getLimitedModeReason(session) : null // 'none' = user hasn't joined any queue yet. We're in the pre-chat landing // state: show the picker with live N-in-line hints and a prompt. Picking a // model triggers joinFreebuffQueue, which POSTs and transitions us to @@ -337,17 +373,28 @@ export const WaitingRoomScreen: React.FC = ({ > {/* Top-right exit affordance so mouse users have a clear way out even when they don't know Ctrl+C works. width: '100%' is required for - justifyContent: 'flex-end' to actually push the X to the right. */} + justifyContent to actually push the X to the right. */} + + {limitedModeReason && ( + + + Limited mode + + · {limitedModeReason} + + )} +