@@ -3,9 +3,15 @@ import { useKeyboard } from '@opentui/react'
33import React , { useCallback , useEffect , useMemo , useState } from 'react'
44
55import { Button } from './button'
6- import { FREEBUFF_MODELS } from '@codebuff/common/constants/freebuff-models'
6+ import {
7+ DEFAULT_FREEBUFF_MODEL_ID ,
8+ FREEBUFF_DEPLOYMENT_HOURS_LABEL ,
9+ FREEBUFF_MODELS ,
10+ isFreebuffModelAvailable ,
11+ } from '@codebuff/common/constants/freebuff-models'
712
813import { joinFreebuffQueue } from '../hooks/use-freebuff-session'
14+ import { useNow } from '../hooks/use-now'
915import { useFreebuffModelStore } from '../state/freebuff-model-store'
1016import { useFreebuffSessionStore } from '../state/freebuff-session-store'
1117import { useTerminalDimensions } from '../hooks/use-terminal-dimensions'
@@ -33,7 +39,9 @@ export const FreebuffModelSelector: React.FC = () => {
3339 const theme = useTheme ( )
3440 const { terminalWidth } = useTerminalDimensions ( )
3541 const selectedModel = useFreebuffModelStore ( ( s ) => s . selectedModel )
42+ const setSelectedModel = useFreebuffModelStore ( ( s ) => s . setSelectedModel )
3643 const session = useFreebuffSessionStore ( ( s ) => s . session )
44+ const now = useNow ( 60_000 )
3745 const [ pending , setPending ] = useState < string | null > ( null )
3846 const [ hoveredId , setHoveredId ] = useState < string | null > ( null )
3947 // Keyboard cursor — separate from the actually-selected model so that
@@ -45,6 +53,15 @@ export const FreebuffModelSelector: React.FC = () => {
4553 setFocusedId ( selectedModel )
4654 } , [ selectedModel ] )
4755
56+ useEffect ( ( ) => {
57+ if (
58+ ( session ?. status === 'none' || ! session ) &&
59+ ! isFreebuffModelAvailable ( selectedModel , new Date ( now ) )
60+ ) {
61+ setSelectedModel ( DEFAULT_FREEBUFF_MODEL_ID )
62+ }
63+ } , [ now , selectedModel , session , setSelectedModel ] )
64+
4865 // Landing ('none'): depths come from the server snapshot, no "self" to
4966 // subtract. In-queue ('queued'): for the user's queue, "ahead" is
5067 // `position - 1` (themselves don't count); for every other queue, switching
@@ -85,7 +102,8 @@ export const FreebuffModelSelector: React.FC = () => {
85102 )
86103
87104 // Decide row vs column layout based on whether both buttons actually fit
88- // side-by-side. Each button's inner text is "● {displayName} · {tagline} {hint}",
105+ // side-by-side. Each button's inner text is
106+ // "● {displayName} · {tagline} · {hours} {hint}",
89107 // plus 2 cols of border and 2 cols of padding. Buttons are separated by a
90108 // gap of 2. If the total exceeds the terminal width, stack vertically.
91109 const stackVertically = useMemo ( ( ) => {
@@ -97,6 +115,9 @@ export const FreebuffModelSelector: React.FC = () => {
97115 model . displayName . length +
98116 3 /* " · " */ +
99117 model . tagline . length +
118+ ( model . availability === 'deployment_hours'
119+ ? 3 + FREEBUFF_DEPLOYMENT_HOURS_LABEL . length
120+ : 0 ) +
100121 2 /* " " */ +
101122 hintWidth
102123 return sum + inner + BUTTON_CHROME + ( idx > 0 ? GAP : 0 )
@@ -115,10 +136,11 @@ export const FreebuffModelSelector: React.FC = () => {
115136 ( modelId : string ) => {
116137 if ( pending ) return
117138 if ( modelId === committedModelId ) return
139+ if ( ! isFreebuffModelAvailable ( modelId , new Date ( now ) ) ) return
118140 setPending ( modelId )
119141 joinFreebuffQueue ( modelId ) . finally ( ( ) => setPending ( null ) )
120142 } ,
121- [ pending , committedModelId ] ,
143+ [ pending , committedModelId , now ] ,
122144 )
123145
124146 // Tab / Shift+Tab and arrow keys move the focus highlight only; Enter or
@@ -136,7 +158,10 @@ export const FreebuffModelSelector: React.FC = () => {
136158 const isCommit = name === 'return' || name === 'enter' || name === 'space'
137159 if ( ! isForward && ! isBackward && ! isCommit ) return
138160 if ( isCommit ) {
139- if ( focusedId !== committedModelId ) {
161+ if (
162+ focusedId !== committedModelId &&
163+ isFreebuffModelAvailable ( focusedId , new Date ( now ) )
164+ ) {
140165 key . preventDefault ?.( )
141166 pick ( focusedId )
142167 }
@@ -154,7 +179,7 @@ export const FreebuffModelSelector: React.FC = () => {
154179 setFocusedId ( target . id )
155180 }
156181 } ,
157- [ pending , pick , focusedId , committedModelId ] ,
182+ [ pending , pick , focusedId , committedModelId , now ] ,
158183 ) ,
159184 )
160185
@@ -181,15 +206,22 @@ export const FreebuffModelSelector: React.FC = () => {
181206 const isSelected = model . id === selectedModel
182207 const isHovered = hoveredId === model . id
183208 const isFocused = focusedId === model . id && ! isSelected
209+ const isAvailable = isFreebuffModelAvailable ( model . id , new Date ( now ) )
184210 const indicator = isSelected ? '●' : '○'
185211 const indicatorColor = isSelected ? theme . primary : theme . muted
186- const labelColor = isSelected ? theme . foreground : theme . muted
212+ const labelColor = isSelected && isAvailable ? theme . foreground : theme . muted
187213 // Clickable whenever picking would actually do something — i.e.
188214 // anything except re-picking the queue we're already in.
189- const interactable = ! pending && model . id !== committedModelId
215+ const interactable = ! pending && isAvailable && model . id !== committedModelId
190216 const ahead = aheadByModel ?. [ model . id ]
191217 const hint =
192- ahead === undefined ? '' : ahead === 0 ? 'No wait' : `${ ahead } ahead`
218+ ! isAvailable
219+ ? 'Closed'
220+ : ahead === undefined
221+ ? ''
222+ : ahead === 0
223+ ? 'No wait'
224+ : `${ ahead } ahead`
193225
194226 const borderColor = isSelected
195227 ? theme . primary
@@ -202,7 +234,7 @@ export const FreebuffModelSelector: React.FC = () => {
202234 key = { model . id }
203235 onClick = { ( ) => {
204236 setFocusedId ( model . id )
205- pick ( model . id )
237+ if ( isAvailable ) pick ( model . id )
206238 } }
207239 onMouseOver = { ( ) => interactable && setHoveredId ( model . id ) }
208240 onMouseOut = { ( ) => setHoveredId ( ( curr ) => ( curr === model . id ? null : curr ) ) }
@@ -223,6 +255,9 @@ export const FreebuffModelSelector: React.FC = () => {
223255 { model . displayName }
224256 </ span >
225257 < span fg = { theme . muted } > · { model . tagline } </ span >
258+ { model . availability === 'deployment_hours' && (
259+ < span fg = { theme . muted } > · { FREEBUFF_DEPLOYMENT_HOURS_LABEL } </ span >
260+ ) }
226261 < span fg = { theme . muted } > { hint . padEnd ( hintWidth ) } </ span >
227262 </ text >
228263 </ Button >
0 commit comments