Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 21 additions & 9 deletions cli/src/components/waiting-room-screen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -221,13 +221,13 @@ export const WaitingRoomScreen: React.FC<WaitingRoomScreenProps> = ({
<span fg={theme.muted}> / {session.queueDepth}</span>
</text>
<text style={{ fg: theme.muted, alignSelf: 'flex-start' }}>
<span>Wait </span>
<span>Wait </span>
{session.position === 1
? 'any moment now'
: formatWait(session.estimatedWaitMs)}
</text>
<text style={{ fg: theme.muted, alignSelf: 'flex-start' }}>
<span>Elapsed </span>
<span>Elapsed </span>
{formatElapsed(elapsedMs)}
</text>
{/* Per-model session quota (e.g. GLM 5.1 caps at 5/20h). Only
Expand All @@ -237,7 +237,8 @@ export const WaitingRoomScreen: React.FC<WaitingRoomScreenProps> = ({
<text style={{ fg: theme.muted, alignSelf: 'flex-start' }}>
<span>Sessions </span>
<span fg={theme.foreground}>
{session.rateLimit.recentCount} / {session.rateLimit.limit}
{session.rateLimit.recentCount} /{' '}
{session.rateLimit.limit}
</span>
<span> used in last {session.rateLimit.windowHours}h</span>
</text>
Expand All @@ -262,10 +263,20 @@ export const WaitingRoomScreen: React.FC<WaitingRoomScreenProps> = ({
⚠ Free mode isn't available in your region
</text>
<text style={{ fg: theme.muted, wrapMode: 'word' }}>
We detected your location as{' '}
<span fg={theme.foreground}>{session.countryCode}</span>,
which is outside the countries where freebuff is currently
offered. Press Ctrl+C to exit.
{session.countryCode === 'UNKNOWN' ? (
<>
We couldn't verify an eligible location for this request.
VPN, Tor, proxy, or unknown-location traffic can't use
freebuff. Press Ctrl+C to exit.
</>
) : (
<>
We detected your location as{' '}
<span fg={theme.foreground}>{session.countryCode}</span>,
which is outside the countries where freebuff is currently
offered. Press Ctrl+C to exit.
</>
)}
</text>
</>
)}
Expand All @@ -279,8 +290,9 @@ export const WaitingRoomScreen: React.FC<WaitingRoomScreenProps> = ({
⚠ Account unavailable
</text>
<text style={{ fg: theme.muted, wrapMode: 'word' }}>
This account has been suspended and can't use freebuff. If you think this is a
mistake, contact support@codebuff.com. Press Ctrl+C to exit.
This account has been suspended and can't use freebuff. If you
think this is a mistake, contact support@codebuff.com. Press
Ctrl+C to exit.
</text>
</>
)}
Expand Down
33 changes: 18 additions & 15 deletions cli/src/hooks/use-freebuff-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ const playAdmissionSound = () => {
}

const sessionEndpoint = (): string => {
const base = (env.NEXT_PUBLIC_CODEBUFF_APP_URL || 'https://codebuff.com').replace(/\/$/, '')
const base = (
env.NEXT_PUBLIC_CODEBUFF_APP_URL || 'https://codebuff.com'
).replace(/\/$/, '')
return `${base}/api/v1/freebuff/session`
}

Expand Down Expand Up @@ -73,10 +75,13 @@ async function callSession(
// generic error and back off on the 10s error-retry cadence instead of
// tight-polling an unrecognized 200 body.
if (resp.status === 403) {
const body = (await resp.json().catch(() => null)) as
| FreebuffSessionResponse
| null
if (body && (body.status === 'country_blocked' || body.status === 'banned')) {
const body = (await resp
.json()
.catch(() => null)) as FreebuffSessionResponse | null
if (
body &&
(body.status === 'country_blocked' || body.status === 'banned')
) {
return body
}
}
Expand All @@ -85,9 +90,9 @@ async function callSession(
// Surface model-switch conflicts and temporary model availability closures
// as non-throw states.
if (resp.status === 409 && method === 'POST') {
const body = (await resp.json().catch(() => null)) as
| FreebuffSessionResponse
| null
const body = (await resp
.json()
.catch(() => null)) as FreebuffSessionResponse | null
if (
body &&
(body.status === 'model_locked' || body.status === 'model_unavailable')
Expand All @@ -101,9 +106,9 @@ async function callSession(
// status (rather than 200) keeps older CLIs in their error path so they
// back off instead of tight-polling an unrecognized 200 body.
if (resp.status === 429 && method === 'POST') {
const body = (await resp.json().catch(() => null)) as
| FreebuffSessionResponse
| null
const body = (await resp
.json()
.catch(() => null)) as FreebuffSessionResponse | null
if (body && body.status === 'rate_limited') {
return body
}
Expand Down Expand Up @@ -190,9 +195,7 @@ export function getFreebuffInstanceId(): string | undefined {
* holding (queued, active, or in the post-expiry grace window with a live
* instance id). DELETE only matters in those states; otherwise we'd fire a
* spurious request the server has nothing to act on. */
function shouldReleaseSlot(
current: FreebuffSessionResponse | null,
): boolean {
function shouldReleaseSlot(current: FreebuffSessionResponse | null): boolean {
if (!current) return false
return (
current.status === 'queued' ||
Expand Down Expand Up @@ -312,7 +315,7 @@ export function markFreebuffSessionSuperseded(): void {

/** Flip into the terminal `country_blocked` state from outside the poll loop.
* Used when the chat-completions gate rejects on country even though the
* session-level country check had failed open (null detection → admitted).
* session-level country check did not catch the request first.
* Transitioning the session state here unmounts the Chat surface in favor of
* the waiting-room's country_blocked message, so the user can't keep typing
* and sending doomed requests. */
Expand Down
4 changes: 2 additions & 2 deletions cli/src/utils/error-handling.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ export const isFreeModeUnavailableError = (error: unknown): boolean => {
/**
* Extract the detected countryCode off a free_mode_unavailable error, if the
* server included one. Used to populate the country_blocked screen after the
* chat-completions gate rejects a user whose session-level country check had
* previously failed open (null country detection → admitted → now blocked).
* chat-completions gate rejects a user whose session-level country check did
* not catch the request first.
*/
export const getCountryCodeFromFreeModeError = (
error: unknown,
Expand Down
5 changes: 3 additions & 2 deletions common/src/types/freebuff-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,11 +98,12 @@ export type FreebuffSessionServerResponse =
status: 'superseded'
}
| {
/** Request originated from a country outside the free-mode allowlist.
/** Request originated outside the free-mode allowlist, or from an
* unknown/anonymized location that cannot be trusted for free mode.
* Returned before queue admission so users don't wait through the
* room only to be rejected on their first chat request. Terminal —
* CLI stops polling and shows a "not available in your country"
* screen. `countryCode` is the resolved country for display. */
* screen. `countryCode` is the resolved country, or UNKNOWN. */
status: 'country_blocked'
countryCode: string
}
Expand Down
Loading
Loading