1+ import { getRateLimitsByModel } from '@codebuff/common/types/freebuff-session'
12import { TextAttributes } from '@opentui/core'
23import { useKeyboard } from '@opentui/react'
34import React , { useCallback , useState } from 'react'
89 returnToFreebuffLanding ,
910} from '../hooks/use-freebuff-session'
1011import { useTheme } from '../hooks/use-theme'
12+ import { useFreebuffSessionStore } from '../state/freebuff-session-store'
13+ import { formatSessionUnits } from '../utils/format-session-units'
1114import { BORDER_CHARS } from '../utils/ui-constants'
1215
1316import type { KeyEvent } from '@opentui/core'
@@ -32,6 +35,19 @@ export const SessionEndedBanner: React.FC<SessionEndedBannerProps> = ({
3235 'waiting-room' | 'same-chat' | null
3336 > ( null )
3437
38+ // All premium models share one daily pool; the server replicates the same
39+ // snapshot under each premium model id, so the first entry has the right
40+ // count.
41+ const premiumQuota = useFreebuffSessionStore (
42+ ( s ) => Object . values ( getRateLimitsByModel ( s . session ) ?? { } ) [ 0 ] ?? null ,
43+ )
44+ const isQuotaExhausted = premiumQuota
45+ ? premiumQuota . recentCount >= premiumQuota . limit
46+ : false
47+ const bannerTitle = premiumQuota
48+ ? `Session ended · ${ formatSessionUnits ( premiumQuota . recentCount ) } of ${ premiumQuota . limit } premium sessions used today`
49+ : 'Session ended'
50+
3551 // While a request is still streaming, restart is disabled: it would
3652 // unmount <Chat> and abort the in-flight agent run. The promise is "we
3753 // let the agent finish" — honoring that means Enter does nothing until
@@ -78,12 +94,15 @@ export const SessionEndedBanner: React.FC<SessionEndedBannerProps> = ({
7894
7995 return (
8096 < box
81- title = "Session ended"
97+ title = { bannerTitle }
8298 titleAlignment = "center"
8399 style = { {
84100 width : '100%' ,
85101 borderStyle : 'single' ,
86- borderColor : theme . muted ,
102+ // Amber border doubles as the "you've hit the cap" signal now that
103+ // the quota count lives in the title (which can't carry per-char
104+ // color); muted otherwise.
105+ borderColor : isQuotaExhausted ? theme . secondary : theme . muted ,
87106 customBorderChars : BORDER_CHARS ,
88107 paddingLeft : 1 ,
89108 paddingRight : 1 ,
@@ -93,9 +112,6 @@ export const SessionEndedBanner: React.FC<SessionEndedBannerProps> = ({
93112 gap : 0 ,
94113 } }
95114 >
96- < text style = { { fg : theme . foreground , wrapMode : 'word' } } >
97- Your freebuff session has ended.
98- </ text >
99115 { isStreaming ? (
100116 < text style = { { fg : theme . muted , wrapMode : 'word' } } >
101117 Agent is wrapping up. Rejoin the wait room after it's finished.
@@ -115,7 +131,7 @@ export const SessionEndedBanner: React.FC<SessionEndedBannerProps> = ({
115131 fg :
116132 pendingAction === 'same-chat'
117133 ? theme . muted
118- : theme . primary ,
134+ : theme . foreground ,
119135 } }
120136 attributes = { TextAttributes . BOLD }
121137 >
@@ -144,11 +160,14 @@ export const SessionEndedBanner: React.FC<SessionEndedBannerProps> = ({
144160 ? theme . muted
145161 : theme . foreground ,
146162 } }
147- attributes = { TextAttributes . BOLD }
148163 >
149- { pendingAction === 'waiting-room'
150- ? 'Opening model selection…'
151- : 'Change model (ESC)' }
164+ { pendingAction === 'waiting-room' ? (
165+ 'Opening model selection…'
166+ ) : (
167+ < >
168+ Change model< span fg = { theme . muted } > { ' Esc' } </ span >
169+ </ >
170+ ) }
152171 </ text >
153172 </ Button >
154173 </ box >
0 commit comments