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 { FREEBUFF_PREMIUM_SESSION_LIMIT } from '@codebuff/common/constants/freebuff-models'
@@ -247,30 +254,31 @@ export const WaitingRoomScreen: React.FC<WaitingRoomScreenProps> = ({
247254
248255 const [ exitHover , setExitHover ] = useState ( false )
249256
250- // Elapsed-in-queue timer. Starts from `queuedAt` so it keeps ticking even if
251- // the user wanders away and comes back.
252- const queuedAtMs = useMemo ( ( ) => {
253- if ( session ?. status === 'queued' ) return Date . parse ( session . queuedAt )
254- return null
255- } , [ session ] )
256- const now = useNow ( 1000 , queuedAtMs !== null )
257- const elapsedMs = queuedAtMs ? now - queuedAtMs : 0
258-
259257 const isQueued = session ?. status === 'queued'
260258 // 'none' = user hasn't joined any queue yet. We're in the pre-chat landing
261259 // state: show the picker with live N-in-line hints and a prompt. Picking a
262260 // model triggers joinFreebuffQueue, which POSTs and transitions us to
263261 // 'queued' (waiting room) or straight to 'active' (chat) if no wait.
264262 const isLanding = session ?. status === 'none'
263+ // Elapsed-in-queue timer. Starts from `queuedAt` so it keeps ticking even if
264+ // the user wanders away and comes back. On the landing picker we tick once a
265+ // minute so the premium reset countdown stays fresh.
266+ const queuedAtMs = useMemo ( ( ) => {
267+ if ( session ?. status === 'queued' ) return Date . parse ( session . queuedAt )
268+ return null
269+ } , [ session ] )
270+ const now = useNow ( isQueued ? 1000 : 60_000 , isQueued || isLanding )
271+ const elapsedMs = queuedAtMs ? now - queuedAtMs : 0
265272
266273 // Premium quota counter for the title line. All premium models share one
267274 // pool; the server replicates the same snapshot under each premium model
268275 // id, so any entry has the right count. Renders amber when exhausted so
269276 // the limit reads as "you've hit it" rather than just another count.
270277 const rateLimitsByModel = getRateLimitsByModel ( session )
271- const sharedPremiumUsed = rateLimitsByModel
272- ? ( Object . values ( rateLimitsByModel ) [ 0 ] ?. recentCount ?? 0 )
273- : 0
278+ const premiumRateLimit = rateLimitsByModel
279+ ? Object . values ( rateLimitsByModel ) [ 0 ]
280+ : undefined
281+ const sharedPremiumUsed = premiumRateLimit ?. recentCount ?? 0
274282 const isPremiumExhausted =
275283 sharedPremiumUsed >= FREEBUFF_PREMIUM_SESSION_LIMIT
276284 const premiumUsedColor = isPremiumExhausted ? theme . secondary : theme . muted
@@ -280,6 +288,26 @@ export const WaitingRoomScreen: React.FC<WaitingRoomScreenProps> = ({
280288 const formattedSharedPremiumUsed = formatSessionUnits (
281289 sharedPremiumUsed ,
282290 ) . padStart ( sessionUnitWidth )
291+ const premiumResetAt = getFreebuffPremiumResetAt ( {
292+ rateLimitsByModel,
293+ nowMs : now ,
294+ } )
295+ const premiumResetAtMs = premiumResetAt . getTime ( )
296+ const premiumResetCountdown = formatFreebuffPremiumResetCountdown (
297+ premiumResetAt ,
298+ now ,
299+ )
300+
301+ useEffect ( ( ) => {
302+ if ( ! isLanding || ! premiumRateLimit ) return
303+
304+ const delayMs = Math . max ( 0 , premiumResetAtMs - Date . now ( ) + 1_000 )
305+ const timer = setTimeout ( ( ) => {
306+ refreshFreebuffLandingMetadata ( ) . catch ( ( ) => { } )
307+ } , delayMs )
308+
309+ return ( ) => clearTimeout ( timer )
310+ } , [ isLanding , premiumRateLimit , premiumResetAtMs ] )
283311
284312 return (
285313 < box
@@ -366,10 +394,17 @@ export const WaitingRoomScreen: React.FC<WaitingRoomScreenProps> = ({
366394 < span fg = { theme . foreground } attributes = { TextAttributes . BOLD } >
367395 Pick a model to start
368396 </ span >
397+ </ text >
398+ < text
399+ style = { { fg : theme . muted , marginBottom : 1 , wrapMode : 'word' } }
400+ >
369401 < span fg = { premiumUsedColor } >
370- { ' · ' }
371402 { formattedSharedPremiumUsed } of{ ' ' }
372- { FREEBUFF_PREMIUM_SESSION_LIMIT } premium sessions used today
403+ { FREEBUFF_PREMIUM_SESSION_LIMIT } premium sessions used
404+ </ span >
405+ < span fg = { theme . muted } >
406+ { ' · ' }
407+ resets in { premiumResetCountdown }
373408 </ span >
374409 </ text >
375410 < FreebuffModelSelector />
0 commit comments