1+ import { TextAttributes } from '@opentui/core'
12import React , { useEffect , useState } from 'react'
23
3- import { FreebuffSessionCountdown } from './freebuff-session-countdown'
44import { ScrollToBottomButton } from './scroll-to-bottom-button'
55import { ShimmerText } from './shimmer-text'
66import { StopButton } from './stop-button'
7+ import { useFreebuffSessionProgress } from '../hooks/use-freebuff-session-progress'
78import { useTheme } from '../hooks/use-theme'
89import { formatElapsedTime } from '../utils/format-elapsed-time'
910
@@ -13,6 +14,17 @@ import type { StatusIndicatorState } from '../utils/status-indicator-state'
1314
1415const SHIMMER_INTERVAL_MS = 160
1516
17+ /** Show the "X:XX left" urgency readout under this many ms remaining. */
18+ const COUNTDOWN_VISIBLE_MS = 5 * 60_000
19+
20+ const formatCountdown = ( ms : number ) : string => {
21+ if ( ms <= 0 ) return 'expiring…'
22+ const totalSeconds = Math . ceil ( ms / 1000 )
23+ const m = Math . floor ( totalSeconds / 60 )
24+ const s = totalSeconds % 60
25+ return `${ m } :${ s . toString ( ) . padStart ( 2 , '0' ) } `
26+ }
27+
1628interface StatusBarProps {
1729 timerStartTime : number | null
1830 isAtBottom : boolean
@@ -132,8 +144,13 @@ export const StatusBar = ({
132144 const statusIndicatorContent = renderStatusIndicator ( )
133145 const elapsedTimeContent = renderElapsedTime ( )
134146
135- // Only show gray background when there's status indicator or timer
136- const hasContent = statusIndicatorContent || elapsedTimeContent
147+ const sessionProgress = useFreebuffSessionProgress ( freebuffSession )
148+
149+ // Show gray background when there's status indicator, timer, or when the
150+ // freebuff session fill is visible (otherwise the fill would float over
151+ // transparent space).
152+ const hasContent =
153+ statusIndicatorContent || elapsedTimeContent || sessionProgress !== null
137154
138155 return (
139156 < box
@@ -147,6 +164,20 @@ export const StatusBar = ({
147164 backgroundColor : hasContent ? theme . surface : 'transparent' ,
148165 } }
149166 >
167+ { sessionProgress !== null && (
168+ < box
169+ style = { {
170+ position : 'absolute' ,
171+ left : 0 ,
172+ top : 0 ,
173+ bottom : 0 ,
174+ // Fill anchors left and shrinks as time passes — the draining
175+ // bar is the countdown; no separate numeric readout needed.
176+ width : `${ sessionProgress . fraction * 100 } %` ,
177+ backgroundColor : theme . surfaceHover ,
178+ } }
179+ />
180+ ) }
150181 < box
151182 style = { {
152183 flexGrow : 1 ,
@@ -176,9 +207,14 @@ export const StatusBar = ({
176207 { onStop && ( statusIndicatorState . kind === 'waiting' || statusIndicatorState . kind === 'streaming' ) && (
177208 < StopButton onClick = { onStop } />
178209 ) }
179- < text style = { { wrapMode : 'none' } } >
180- < FreebuffSessionCountdown session = { freebuffSession } />
181- </ text >
210+ { sessionProgress !== null &&
211+ sessionProgress . remainingMs < COUNTDOWN_VISIBLE_MS && (
212+ < text style = { { wrapMode : 'none' } } >
213+ < span fg = { theme . warning } attributes = { TextAttributes . BOLD } >
214+ { formatCountdown ( sessionProgress . remainingMs ) }
215+ </ span >
216+ </ text >
217+ ) }
182218 </ box >
183219 </ box >
184220 )
0 commit comments