Skip to content

Commit f99d28f

Browse files
jahoomaclaude
andcommitted
Simplify freebuff waiting-room implementation
Collapse client-side draining/ended into a single ended state, move freebuff session state into zustand (replacing the module-level handle singleton), host the Fireworks probe inside admitFromQueue, and share the wire types between server and CLI. Drops ~150 lines net. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 5ca04d3 commit f99d28f

17 files changed

Lines changed: 359 additions & 405 deletions

cli/src/app.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -376,9 +376,9 @@ const AuthedSurface = ({
376376
// Falling through to <Chat> on 'none' would leave the user unable to send
377377
// any free-mode request until the next poll cycle.
378378
//
379-
// 'draining' and 'ended' deliberately fall through to <Chat>: the agent
380-
// may still be finishing work under the server-side grace period, and the
381-
// chat surface itself swaps the input box for the session-ended banner.
379+
// 'ended' deliberately falls through to <Chat>: the agent may still be
380+
// finishing work under the server-side grace period, and the chat surface
381+
// itself swaps the input box for the session-ended banner.
382382
if (
383383
IS_FREEBUFF &&
384384
(session === null ||

cli/src/chat.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1344,9 +1344,7 @@ export const Chat = ({
13441344
const hasActiveFreebuffSession =
13451345
IS_FREEBUFF && freebuffSession?.status === 'active'
13461346
const isFreebuffSessionOver =
1347-
IS_FREEBUFF &&
1348-
(freebuffSession?.status === 'draining' ||
1349-
freebuffSession?.status === 'ended')
1347+
IS_FREEBUFF && freebuffSession?.status === 'ended'
13501348
const shouldShowStatusLine =
13511349
!feedbackMode &&
13521350
(hasStatusIndicatorContent ||

cli/src/components/freebuff-superseded-screen.tsx

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,11 @@ import React, { useCallback } from 'react'
55
import { useLogo } from '../hooks/use-logo'
66
import { useTerminalDimensions } from '../hooks/use-terminal-dimensions'
77
import { useTheme } from '../hooks/use-theme'
8-
import { flushAnalytics } from '../utils/analytics'
9-
import { withTimeout } from '../utils/terminal-color-detection'
8+
import { exitFreebuffCleanly } from '../utils/freebuff-exit'
109
import { getLogoAccentColor, getLogoBlockColor } from '../utils/theme-system'
1110

1211
import type { KeyEvent } from '@opentui/core'
1312

14-
/** Cap on analytics flush so a slow network doesn't block process exit. */
15-
const EXIT_CLEANUP_TIMEOUT_MS = 1000
16-
1713
/**
1814
* Terminal state shown after a 409 session_superseded response. Another CLI on
1915
* the same account rotated our instance id and we've stopped polling — the
@@ -31,17 +27,12 @@ export const FreebuffSupersededScreen: React.FC = () => {
3127
})
3228

3329
// Ctrl+C exits. Stdin is in raw mode, so SIGINT never fires — the key comes
34-
// through as a normal OpenTUI key event. No DELETE needed here: the other
35-
// CLI already rotated our instance id, so our seat (if any) belongs to them.
30+
// through as a normal OpenTUI key event.
3631
useKeyboard(
3732
useCallback((key: KeyEvent) => {
3833
if (key.ctrl && key.name === 'c') {
3934
key.preventDefault?.()
40-
withTimeout(flushAnalytics(), EXIT_CLEANUP_TIMEOUT_MS, undefined).finally(
41-
() => {
42-
process.exit(0)
43-
},
44-
)
35+
exitFreebuffCleanly()
4536
}
4637
}, []),
4738
)

cli/src/components/session-ended-banner.tsx

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,8 @@ import { useKeyboard } from '@opentui/react'
33
import React, { useCallback, useState } from 'react'
44

55
import { Button } from './button'
6-
import { refreshFreebuffSession } from '../hooks/use-freebuff-session'
6+
import { rejoinFreebuffSession } from '../hooks/use-freebuff-session'
77
import { useTheme } from '../hooks/use-theme'
8-
import { useChatStore } from '../state/chat-store'
98
import { BORDER_CHARS } from '../utils/ui-constants'
109

1110
import type { KeyEvent } from '@opentui/core'
@@ -18,10 +17,9 @@ interface SessionEndedBannerProps {
1817
}
1918

2019
/**
21-
* Replaces the chat input when the freebuff session has ended (client state
22-
* `draining` or `ended`). Captures Enter to re-queue the user; Esc keeps
23-
* falling through to the global stream-interrupt handler so in-flight work
24-
* can be cancelled.
20+
* Replaces the chat input when the freebuff session has ended. Captures
21+
* Enter to re-queue the user; Esc keeps falling through to the global
22+
* stream-interrupt handler so in-flight work can be cancelled.
2523
*/
2624
export const SessionEndedBanner: React.FC<SessionEndedBannerProps> = ({
2725
isStreaming,
@@ -40,13 +38,7 @@ export const SessionEndedBanner: React.FC<SessionEndedBannerProps> = ({
4038
// Once the POST lands, the hook flips status to 'queued' and app.tsx
4139
// swaps us into <WaitingRoomScreen>, unmounting this banner. No need to
4240
// clear `rejoining` on success — the component will be gone.
43-
refreshFreebuffSession()
44-
.then(() => {
45-
// Wipe the prior conversation so the next admitted session starts
46-
// with empty history instead of continuing the one that just ended.
47-
useChatStore.getState().reset()
48-
})
49-
.catch(() => setRejoining(false))
41+
rejoinFreebuffSession().catch(() => setRejoining(false))
5042
}, [canRejoin])
5143

5244
useKeyboard(

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

Lines changed: 31 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,17 @@ import { AdBanner } from './ad-banner'
66
import { Button } from './button'
77
import { ChoiceAdBanner } from './choice-ad-banner'
88
import { ShimmerText } from './shimmer-text'
9-
import { endFreebuffSessionBestEffort } from '../hooks/use-freebuff-session'
109
import { useGravityAd } from '../hooks/use-gravity-ad'
1110
import { useLogo } from '../hooks/use-logo'
1211
import { useSheenAnimation } from '../hooks/use-sheen-animation'
1312
import { useTerminalDimensions } from '../hooks/use-terminal-dimensions'
1413
import { useTheme } from '../hooks/use-theme'
15-
import { flushAnalytics } from '../utils/analytics'
16-
import { withTimeout } from '../utils/terminal-color-detection'
14+
import { exitFreebuffCleanly } from '../utils/freebuff-exit'
1715
import { getLogoAccentColor, getLogoBlockColor } from '../utils/theme-system'
1816

1917
import type { FreebuffSessionResponse } from '../types/freebuff-session'
2018
import type { KeyEvent } from '@opentui/core'
2119

22-
/** Cap on exit cleanup (DELETE /session + flushAnalytics) so a slow network
23-
* doesn't block process exit. */
24-
const EXIT_CLEANUP_TIMEOUT_MS = 1000
25-
2620
interface WaitingRoomScreenProps {
2721
session: FreebuffSessionResponse | null
2822
error: string | null
@@ -82,30 +76,15 @@ export const WaitingRoomScreen: React.FC<WaitingRoomScreenProps> = ({
8276
forceStart: true,
8377
})
8478

85-
// Release the seat + flush analytics before exit. Used by both Ctrl+C and
86-
// the top-right X button so they always do the same cleanup.
87-
const handleExit = useCallback(() => {
88-
const cleanup = Promise.allSettled([
89-
flushAnalytics(),
90-
endFreebuffSessionBestEffort(),
91-
])
92-
withTimeout(cleanup, EXIT_CLEANUP_TIMEOUT_MS, undefined).finally(() => {
93-
process.exit(0)
94-
})
95-
}, [])
96-
9779
// Ctrl+C exits. Stdin is in raw mode, so SIGINT never fires — the key comes
98-
// through as a normal OpenTUI key event.
80+
// through as a normal OpenTUI key event. Shared with the top-right X button.
9981
useKeyboard(
100-
useCallback(
101-
(key: KeyEvent) => {
102-
if (key.ctrl && key.name === 'c') {
103-
key.preventDefault?.()
104-
handleExit()
105-
}
106-
},
107-
[handleExit],
108-
),
82+
useCallback((key: KeyEvent) => {
83+
if (key.ctrl && key.name === 'c') {
84+
key.preventDefault?.()
85+
exitFreebuffCleanly()
86+
}
87+
}, []),
10988
)
11089

11190
const [exitHover, setExitHover] = useState(false)
@@ -148,7 +127,7 @@ export const WaitingRoomScreen: React.FC<WaitingRoomScreenProps> = ({
148127
}}
149128
>
150129
<Button
151-
onClick={handleExit}
130+
onClick={exitFreebuffCleanly}
152131
onMouseOver={() => setExitHover(true)}
153132
onMouseOut={() => setExitHover(false)}
154133
style={{ paddingLeft: 1, paddingRight: 1 }}
@@ -201,7 +180,9 @@ export const WaitingRoomScreen: React.FC<WaitingRoomScreenProps> = ({
201180
{isQueued && session && (
202181
<>
203182
<text style={{ fg: theme.foreground, marginBottom: 1 }}>
204-
You're in the waiting room
183+
{session.position === 1
184+
? "You're next in line"
185+
: "You're in the waiting room"}
205186
</text>
206187

207188
<box
@@ -211,34 +192,25 @@ export const WaitingRoomScreen: React.FC<WaitingRoomScreenProps> = ({
211192
gap: 0,
212193
}}
213194
>
214-
{session.position === 1 ? (
215-
<>
216-
<text style={{ fg: theme.primary, alignSelf: 'flex-start' }}>
217-
<ShimmerText text="Next in line" />
218-
</text>
219-
<text style={{ fg: theme.muted, alignSelf: 'flex-start' }}>
220-
{session.queueDepth === 1
221-
? 'just you in line right now'
222-
: `${session.queueDepth} people in line`}
223-
</text>
224-
</>
225-
) : (
226-
<text style={{ fg: theme.foreground, alignSelf: 'flex-start' }}>
227-
<span fg={theme.muted}>Position </span>
228-
<span fg={theme.primary} attributes={TextAttributes.BOLD}>
229-
{session.position}
230-
</span>
231-
<span fg={theme.muted}> / {session.queueDepth}</span>
232-
</text>
233-
)}
234-
{session.position !== 1 && (
235-
<text style={{ fg: theme.foreground, alignSelf: 'flex-start' }}>
236-
<span fg={theme.muted}>Wait </span>
237-
<span fg={theme.primary}>
238-
<ShimmerText text={formatWait(session.estimatedWaitMs)} />
239-
</span>
240-
</text>
241-
)}
195+
<text style={{ fg: theme.foreground, alignSelf: 'flex-start' }}>
196+
<span fg={theme.muted}>Position </span>
197+
<span fg={theme.primary} attributes={TextAttributes.BOLD}>
198+
{session.position}
199+
</span>
200+
<span fg={theme.muted}> / {session.queueDepth}</span>
201+
</text>
202+
<text style={{ fg: theme.foreground, alignSelf: 'flex-start' }}>
203+
<span fg={theme.muted}>Wait </span>
204+
<span fg={theme.primary}>
205+
<ShimmerText
206+
text={
207+
session.position === 1
208+
? 'any moment now'
209+
: formatWait(session.estimatedWaitMs)
210+
}
211+
/>
212+
</span>
213+
</text>
242214
<text style={{ fg: theme.muted, alignSelf: 'flex-start' }}>
243215
<span>Elapsed </span>
244216
{formatElapsed(elapsedMs)}

cli/src/hooks/helpers/send-message.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -510,11 +510,10 @@ function handleFreebuffGateError(
510510
switch (kind) {
511511
case 'session_expired':
512512
case 'waiting_room_required':
513-
// Our seat is gone mid-chat. Flip to the client-only `ended` state
514-
// instead of auto re-queuing: the Chat surface stays mounted so any
515-
// in-flight agent work can finish under the server-side grace period,
516-
// and the session-ended banner prompts the user to press Enter when
517-
// they're ready to rejoin the waiting room.
513+
// Our seat is gone mid-chat. Flip to `ended` instead of auto re-queuing:
514+
// the Chat surface stays mounted so any in-flight agent work can finish
515+
// under the server-side grace period, and the session-ended banner
516+
// prompts the user to press Enter when they're ready to rejoin.
518517
markFreebuffSessionEnded()
519518
return
520519
case 'waiting_room_queued':

0 commit comments

Comments
 (0)