Skip to content

Commit b2d482d

Browse files
committed
Merge remote-tracking branch 'origin/main' into jahooma/freebuff-limited-deepseek
# Conflicts: # cli/src/components/waiting-room-screen.tsx # web/src/app/api/v1/chat/completions/_post.ts
2 parents 3a783c8 + 99b3d0a commit b2d482d

23 files changed

Lines changed: 1554 additions & 109 deletions

File tree

cli/src/components/waiting-room-screen.tsx

Lines changed: 51 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
import { TextAttributes } from '@opentui/core'
22
import { useKeyboard, useRenderer } from '@opentui/react'
3-
import React, { useCallback, useMemo, useState } from 'react'
3+
import React, { useCallback, useEffect, useMemo, useState } from 'react'
44

55
import { Button } from './button'
66
import { ChoiceAdBanner, CHOICE_AD_BANNER_HEIGHT } from './choice-ad-banner'
77
import { FreebuffModelSelector } from './freebuff-model-selector'
88
import { ShimmerText } from './shimmer-text'
9-
import { takeOverFreebuffSession } from '../hooks/use-freebuff-session'
9+
import {
10+
refreshFreebuffLandingMetadata,
11+
takeOverFreebuffSession,
12+
} from '../hooks/use-freebuff-session'
1013
import { useFreebuffCtrlCExit } from '../hooks/use-freebuff-ctrl-c-exit'
1114
import { useGravityAd } from '../hooks/use-gravity-ad'
1215
import { useLogo } from '../hooks/use-logo'
@@ -15,6 +18,10 @@ import { useSheenAnimation } from '../hooks/use-sheen-animation'
1518
import { useTerminalDimensions } from '../hooks/use-terminal-dimensions'
1619
import { useTheme } from '../hooks/use-theme'
1720
import { exitFreebuffCleanly } from '../utils/freebuff-exit'
21+
import {
22+
formatFreebuffPremiumResetCountdown,
23+
getFreebuffPremiumResetAt,
24+
} from '../utils/freebuff-premium-reset'
1825
import { formatSessionUnits } from '../utils/format-session-units'
1926
import { getLogoAccentColor, getLogoBlockColor } from '../utils/theme-system'
2027
import {
@@ -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 />

cli/src/hooks/use-freebuff-session.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,13 @@ export function returnToFreebuffLanding(
285285
})
286286
}
287287

288+
/** Refresh picker-only metadata (quota and queue depths) while staying on the
289+
* model selection screen. Used when a midnight-Pacific premium quota reset
290+
* passes while the landing screen is open. */
291+
export function refreshFreebuffLandingMetadata(): Promise<void> {
292+
return restartFreebuffSession('landing')
293+
}
294+
288295
/**
289296
* Join (or re-queue for) `model`. Dual-purpose:
290297
* - First join: called from the pre-chat landing picker. The session starts
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { describe, expect, test } from 'bun:test'
2+
3+
import {
4+
formatFreebuffPremiumResetCountdown,
5+
getFreebuffPremiumResetAt,
6+
} from '../freebuff-premium-reset'
7+
8+
describe('freebuff premium reset helpers', () => {
9+
test('uses server resetAt when it is in the future', () => {
10+
const nowMs = Date.parse('2026-05-11T20:00:00.000Z')
11+
const resetAt = getFreebuffPremiumResetAt({
12+
nowMs,
13+
rateLimitsByModel: {
14+
'test/model': {
15+
model: 'test/model',
16+
limit: 5,
17+
period: 'pacific_day',
18+
resetTimeZone: 'America/Los_Angeles',
19+
resetAt: '2026-05-12T07:00:00.000Z',
20+
windowHours: 24,
21+
recentCount: 2,
22+
},
23+
},
24+
})
25+
26+
expect(resetAt.toISOString()).toBe('2026-05-12T07:00:00.000Z')
27+
})
28+
29+
test('falls back to next midnight Pacific when resetAt is absent', () => {
30+
const resetAt = getFreebuffPremiumResetAt({
31+
nowMs: Date.parse('2026-05-11T20:00:00.000Z'),
32+
})
33+
34+
expect(resetAt.toISOString()).toBe('2026-05-12T07:00:00.000Z')
35+
})
36+
37+
test('keeps expired server resetAt instead of rolling stale quota forward', () => {
38+
const nowMs = Date.parse('2026-05-12T07:05:00.000Z')
39+
const resetAt = getFreebuffPremiumResetAt({
40+
nowMs,
41+
rateLimitsByModel: {
42+
'test/model': {
43+
model: 'test/model',
44+
limit: 5,
45+
period: 'pacific_day',
46+
resetTimeZone: 'America/Los_Angeles',
47+
resetAt: '2026-05-12T07:00:00.000Z',
48+
windowHours: 24,
49+
recentCount: 5,
50+
},
51+
},
52+
})
53+
54+
expect(resetAt.toISOString()).toBe('2026-05-12T07:00:00.000Z')
55+
expect(formatFreebuffPremiumResetCountdown(resetAt, nowMs)).toBe('now')
56+
})
57+
58+
test('handles Pacific daylight saving time boundaries', () => {
59+
const resetAt = getFreebuffPremiumResetAt({
60+
nowMs: Date.parse('2026-01-15T20:00:00.000Z'),
61+
})
62+
63+
expect(resetAt.toISOString()).toBe('2026-01-16T08:00:00.000Z')
64+
})
65+
66+
test('formats hours and minutes left', () => {
67+
const nowMs = Date.parse('2026-05-11T20:00:00.000Z')
68+
const resetAt = new Date('2026-05-12T07:30:00.000Z')
69+
70+
expect(formatFreebuffPremiumResetCountdown(resetAt, nowMs)).toBe('11h 30m')
71+
})
72+
73+
test('formats sub-hour reset countdowns', () => {
74+
const nowMs = Date.parse('2026-05-12T06:30:00.000Z')
75+
const resetAt = new Date('2026-05-12T07:00:00.000Z')
76+
77+
expect(formatFreebuffPremiumResetCountdown(resetAt, nowMs)).toBe('30m')
78+
})
79+
})

cli/src/utils/analytics.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
IS_PROD as defaultIsProd,
1010
DEBUG_ANALYTICS,
1111
} from '@codebuff/common/env'
12+
import { shouldTrackAnalyticsEvent } from '@codebuff/common/util/analytics-sampling'
1213

1314
import type { AnalyticsEvent } from '@codebuff/common/constants/analytics-events'
1415

@@ -211,6 +212,10 @@ export function trackEvent(
211212
return
212213
}
213214

215+
if (!shouldTrackAnalyticsEvent({ event, distinctId, properties })) {
216+
return
217+
}
218+
214219
try {
215220
client.capture({
216221
distinctId,
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { FREEBUFF_PREMIUM_SESSION_RESET_TIMEZONE } from '@codebuff/common/constants/freebuff-models'
2+
import { getZonedDayBounds } from '@codebuff/common/util/zoned-time'
3+
4+
import type { FreebuffSessionRateLimitByModel } from '@codebuff/common/types/freebuff-session'
5+
6+
export function getFreebuffPremiumResetAt(params: {
7+
rateLimitsByModel?: FreebuffSessionRateLimitByModel
8+
nowMs: number
9+
}): Date {
10+
const { rateLimitsByModel, nowMs } = params
11+
const serverResetAt = rateLimitsByModel
12+
? Object.values(rateLimitsByModel)[0]?.resetAt
13+
: undefined
14+
const parsedServerResetAt = serverResetAt ? new Date(serverResetAt) : null
15+
16+
if (
17+
parsedServerResetAt &&
18+
Number.isFinite(parsedServerResetAt.getTime())
19+
) {
20+
return parsedServerResetAt
21+
}
22+
23+
return getZonedDayBounds(
24+
new Date(nowMs),
25+
FREEBUFF_PREMIUM_SESSION_RESET_TIMEZONE,
26+
).resetsAt
27+
}
28+
29+
export function formatFreebuffPremiumResetCountdown(
30+
resetAt: Date,
31+
nowMs: number,
32+
): string {
33+
const diffMs = resetAt.getTime() - nowMs
34+
if (!Number.isFinite(diffMs) || diffMs <= 0) return 'now'
35+
36+
const totalMinutes = Math.max(1, Math.floor(diffMs / 60_000))
37+
const hours = Math.floor(totalMinutes / 60)
38+
const minutes = totalMinutes % 60
39+
40+
if (hours === 0) return `${minutes}m`
41+
return minutes === 0 ? `${hours}h` : `${hours}h ${minutes}m`
42+
}

cli/src/utils/logger.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ import { AnalyticsEvent } from '@codebuff/common/constants/analytics-events'
77
import { env, IS_DEV, IS_TEST, IS_CI } from '@codebuff/common/env'
88
import { createAnalyticsDispatcher } from '@codebuff/common/util/analytics-dispatcher'
99
import { getAnalyticsEventId } from '@codebuff/common/util/analytics-log'
10+
import {
11+
isFullTelemetryEnabled,
12+
summarizeAnalyticsValue,
13+
} from '@codebuff/common/util/analytics-sampling'
1014
import { pino } from 'pino'
1115

1216
import {
@@ -169,10 +173,23 @@ function sendAnalyticsAndLog(
169173
// Skip if the log already has an eventId (to avoid duplicate tracking)
170174
const hasEventId = includeData && getAnalyticsEventId(normalizedData) !== null
171175
if (!IS_DEV && !IS_TEST && !IS_CI && !hasEventId) {
176+
const fullTelemetry = isFullTelemetryEnabled({
177+
distinctId: loggerContext.userId,
178+
properties: loggerContext,
179+
})
180+
const includeRawData =
181+
fullTelemetry || level === 'error' || level === 'fatal'
182+
const dataProperties =
183+
includeData && includeRawData
184+
? { data: normalizedData }
185+
: includeData
186+
? { dataSummary: summarizeAnalyticsValue(normalizedData) }
187+
: {}
188+
172189
trackEvent(AnalyticsEvent.CLI_LOG, {
173190
level,
174191
msg: stringFormat(normalizedMsg ?? '', ...args),
175-
...(includeData ? { data: normalizedData } : {}),
192+
...dataProperties,
176193
...loggerContext,
177194
})
178195
}

0 commit comments

Comments
 (0)