11import { TextAttributes } from '@opentui/core'
22import { useKeyboard , useRenderer } from '@opentui/react'
3- import React , { useCallback , useMemo , useState } from 'react'
3+ import React , { useCallback , useEffect , useMemo , useState } from 'react'
44
55import { Button } from './button'
66import { ChoiceAdBanner , CHOICE_AD_BANNER_HEIGHT } from './choice-ad-banner'
77import { FreebuffModelSelector } from './freebuff-model-selector'
88import { ShimmerText } from './shimmer-text'
9- import { takeOverFreebuffSession } from '../hooks/use-freebuff-session'
9+ import {
10+ refreshFreebuffLandingMetadata ,
11+ takeOverFreebuffSession ,
12+ } from '../hooks/use-freebuff-session'
1013import { useFreebuffCtrlCExit } from '../hooks/use-freebuff-ctrl-c-exit'
1114import { useGravityAd } from '../hooks/use-gravity-ad'
1215import { useLogo } from '../hooks/use-logo'
@@ -15,6 +18,10 @@ import { useSheenAnimation } from '../hooks/use-sheen-animation'
1518import { useTerminalDimensions } from '../hooks/use-terminal-dimensions'
1619import { useTheme } from '../hooks/use-theme'
1720import { exitFreebuffCleanly } from '../utils/freebuff-exit'
21+ import {
22+ formatFreebuffPremiumResetCountdown ,
23+ getFreebuffPremiumResetAt ,
24+ } from '../utils/freebuff-premium-reset'
1825import { formatSessionUnits } from '../utils/format-session-units'
1926import { getLogoAccentColor , getLogoBlockColor } from '../utils/theme-system'
2027import {
@@ -250,15 +257,6 @@ export const WaitingRoomScreen: React.FC<WaitingRoomScreenProps> = ({
250257
251258 const [ exitHover , setExitHover ] = useState ( false )
252259
253- // Elapsed-in-queue timer. Starts from `queuedAt` so it keeps ticking even if
254- // the user wanders away and comes back.
255- const queuedAtMs = useMemo ( ( ) => {
256- if ( session ?. status === 'queued' ) return Date . parse ( session . queuedAt )
257- return null
258- } , [ session ] )
259- const now = useNow ( 1000 , queuedAtMs !== null )
260- const elapsedMs = queuedAtMs ? now - queuedAtMs : 0
261-
262260 const isQueued = session ?. status === 'queued'
263261 const accessTier =
264262 session && 'accessTier' in session ? session . accessTier : 'full'
@@ -267,15 +265,25 @@ export const WaitingRoomScreen: React.FC<WaitingRoomScreenProps> = ({
267265 // model triggers joinFreebuffQueue, which POSTs and transitions us to
268266 // 'queued' (waiting room) or straight to 'active' (chat) if no wait.
269267 const isLanding = session ?. status === 'none'
268+ // Elapsed-in-queue timer. Starts from `queuedAt` so it keeps ticking even if
269+ // the user wanders away and comes back. On the landing picker we tick once a
270+ // minute so the premium reset countdown stays fresh.
271+ const queuedAtMs = useMemo ( ( ) => {
272+ if ( session ?. status === 'queued' ) return Date . parse ( session . queuedAt )
273+ return null
274+ } , [ session ] )
275+ const now = useNow ( isQueued ? 1000 : 60_000 , isQueued || isLanding )
276+ const elapsedMs = queuedAtMs ? now - queuedAtMs : 0
270277
271278 // Premium quota counter for the title line. All premium models share one
272279 // pool; the server replicates the same snapshot under each premium model
273280 // id, so any entry has the right count. Renders amber when exhausted so
274281 // the limit reads as "you've hit it" rather than just another count.
275282 const rateLimitsByModel = getRateLimitsByModel ( session )
276- const sharedPremiumUsed = rateLimitsByModel
277- ? ( Object . values ( rateLimitsByModel ) [ 0 ] ?. recentCount ?? 0 )
278- : 0
283+ const premiumRateLimit = rateLimitsByModel
284+ ? Object . values ( rateLimitsByModel ) [ 0 ]
285+ : undefined
286+ const sharedPremiumUsed = premiumRateLimit ?. recentCount ?? 0
279287 const isPremiumExhausted =
280288 sharedPremiumUsed >=
281289 ( accessTier === 'limited'
@@ -293,6 +301,26 @@ export const WaitingRoomScreen: React.FC<WaitingRoomScreenProps> = ({
293301 const sessionUnitWidth = String ( sessionLimit ) . length + 2
294302 const formattedSharedPremiumUsed =
295303 formatSessionUnits ( sharedPremiumUsed ) . padStart ( sessionUnitWidth )
304+ const premiumResetAt = getFreebuffPremiumResetAt ( {
305+ rateLimitsByModel,
306+ nowMs : now ,
307+ } )
308+ const premiumResetAtMs = premiumResetAt . getTime ( )
309+ const premiumResetCountdown = formatFreebuffPremiumResetCountdown (
310+ premiumResetAt ,
311+ now ,
312+ )
313+
314+ useEffect ( ( ) => {
315+ if ( ! isLanding || ! premiumRateLimit ) return
316+
317+ const delayMs = Math . max ( 0 , premiumResetAtMs - Date . now ( ) + 1_000 )
318+ const timer = setTimeout ( ( ) => {
319+ refreshFreebuffLandingMetadata ( ) . catch ( ( ) => { } )
320+ } , delayMs )
321+
322+ return ( ) => clearTimeout ( timer )
323+ } , [ isLanding , premiumRateLimit , premiumResetAtMs ] )
296324
297325 return (
298326 < box
@@ -379,10 +407,17 @@ export const WaitingRoomScreen: React.FC<WaitingRoomScreenProps> = ({
379407 < span fg = { theme . foreground } attributes = { TextAttributes . BOLD } >
380408 Pick a model to start
381409 </ span >
410+ </ text >
411+ < text
412+ style = { { fg : theme . muted , marginBottom : 1 , wrapMode : 'word' } }
413+ >
382414 < span fg = { premiumUsedColor } >
383- { ' · ' }
384415 { formattedSharedPremiumUsed } of { sessionLimit } { sessionLabel } { ' ' }
385- used today
416+ used
417+ </ span >
418+ < span fg = { theme . muted } >
419+ { ' · ' }
420+ resets in { premiumResetCountdown }
386421 </ span >
387422 </ text >
388423 < FreebuffModelSelector />
0 commit comments