Skip to content

Commit 845bed1

Browse files
committed
Session countdown
1 parent 8ee55ab commit 845bed1

File tree

3 files changed

+70
-1
lines changed

3 files changed

+70
-1
lines changed

cli/src/chat.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { ReviewScreen } from './components/review-screen'
2121
import { MessageWithAgents } from './components/message-with-agents'
2222
import { areCreditsRestored } from './components/out-of-credits-banner'
2323
import { PendingBashMessage } from './components/pending-bash-message'
24+
import { useHasActiveFreebuffSession } from './components/freebuff-session-countdown'
2425
import { StatusBar } from './components/status-bar'
2526
import { TopBanner } from './components/top-banner'
2627
import { getSlashCommandsWithSkills } from './data/slash-commands'
@@ -1337,9 +1338,13 @@ export const Chat = ({
13371338
return ` ${segments.join(' ')} `
13381339
}, [queuePreviewTitle, pausedQueueText])
13391340

1341+
const hasActiveFreebuffSession = useHasActiveFreebuffSession()
13401342
const shouldShowStatusLine =
13411343
!feedbackMode &&
1342-
(hasStatusIndicatorContent || shouldShowQueuePreview || !isAtBottom)
1344+
(hasStatusIndicatorContent ||
1345+
shouldShowQueuePreview ||
1346+
!isAtBottom ||
1347+
hasActiveFreebuffSession)
13431348

13441349
// Track mouse movement for ad activity (throttled)
13451350
const lastMouseActivityRef = useRef<number>(0)
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import React, { useEffect, useState } from 'react'
2+
3+
import { useTheme } from '../hooks/use-theme'
4+
import { useFreebuffSessionStore } from '../state/freebuff-session-store'
5+
import { IS_FREEBUFF } from '../utils/constants'
6+
7+
const LOW_THRESHOLD_MS = 5 * 60_000
8+
const CRITICAL_THRESHOLD_MS = 60_000
9+
10+
const formatRemaining = (ms: number): string => {
11+
if (ms <= 0) return 'expiring…'
12+
const totalSeconds = Math.ceil(ms / 1000)
13+
if (totalSeconds < 60) return `${totalSeconds}s left`
14+
const minutes = Math.floor(totalSeconds / 60)
15+
if (minutes < 60) return `${minutes}m left`
16+
const hours = Math.floor(minutes / 60)
17+
const rem = minutes % 60
18+
return rem === 0 ? `${hours}h left` : `${hours}h ${rem}m left`
19+
}
20+
21+
/**
22+
* Small countdown shown while a freebuff session is active. Renders the
23+
* time remaining until the server-issued `expiresAt` so users aren't
24+
* surprised when their seat is released. Returns null in non-freebuff
25+
* builds or when no active session exists — safe to always mount.
26+
*/
27+
export const FreebuffSessionCountdown: React.FC = () => {
28+
const theme = useTheme()
29+
const session = useFreebuffSessionStore((s) => s.session)
30+
const expiresAtMs =
31+
session?.status === 'active' ? Date.parse(session.expiresAt) : null
32+
33+
const [now, setNow] = useState(() => Date.now())
34+
useEffect(() => {
35+
if (!expiresAtMs) return
36+
const id = setInterval(() => setNow(Date.now()), 1000)
37+
return () => clearInterval(id)
38+
}, [expiresAtMs])
39+
40+
if (!IS_FREEBUFF || !expiresAtMs) return null
41+
42+
const remainingMs = expiresAtMs - now
43+
const color =
44+
remainingMs < CRITICAL_THRESHOLD_MS
45+
? theme.error
46+
: remainingMs < LOW_THRESHOLD_MS
47+
? theme.warning
48+
: theme.muted
49+
50+
return <span fg={color}>{formatRemaining(remainingMs)}</span>
51+
}
52+
53+
/** True when the freebuff session countdown will render non-null content.
54+
* Used by the chat surface to keep the status bar visible while a
55+
* session is active, even when there's no streaming/queue activity. */
56+
export const useHasActiveFreebuffSession = (): boolean => {
57+
return useFreebuffSessionStore(
58+
(s) => IS_FREEBUFF && s.session?.status === 'active',
59+
)
60+
}

cli/src/components/status-bar.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React, { useEffect, useState } from 'react'
22

3+
import { FreebuffSessionCountdown } from './freebuff-session-countdown'
34
import { ScrollToBottomButton } from './scroll-to-bottom-button'
45
import { ShimmerText } from './shimmer-text'
56
import { StopButton } from './stop-button'
@@ -169,6 +170,9 @@ export const StatusBar = ({
169170
}}
170171
>
171172
<text style={{ wrapMode: 'none' }}>{elapsedTimeContent}</text>
173+
<text style={{ wrapMode: 'none' }}>
174+
<FreebuffSessionCountdown />
175+
</text>
172176
{onStop && (statusIndicatorState.kind === 'waiting' || statusIndicatorState.kind === 'streaming') && (
173177
<StopButton onClick={onStop} />
174178
)}

0 commit comments

Comments
 (0)