Skip to content

Commit c7e3a77

Browse files
authored
Add Freebuff session restart option (#603)
1 parent e3a4e71 commit c7e3a77

3 files changed

Lines changed: 91 additions & 24 deletions

File tree

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

Lines changed: 83 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ import { useKeyboard } from '@opentui/react'
33
import React, { useCallback, useState } from 'react'
44

55
import { Button } from './button'
6-
import { returnToFreebuffLanding } from '../hooks/use-freebuff-session'
6+
import {
7+
refreshFreebuffSession,
8+
returnToFreebuffLanding,
9+
} from '../hooks/use-freebuff-session'
710
import { useTheme } from '../hooks/use-theme'
811
import { BORDER_CHARS } from '../utils/ui-constants'
912

@@ -18,43 +21,58 @@ interface SessionEndedBannerProps {
1821

1922
/**
2023
* 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.
24+
* Enter to start a new same-chat session. Esc returns to model selection
25+
* once no in-flight work needs the global stream-interrupt handler.
2326
*/
2427
export const SessionEndedBanner: React.FC<SessionEndedBannerProps> = ({
2528
isStreaming,
2629
}) => {
2730
const theme = useTheme()
28-
const [rejoining, setRejoining] = useState(false)
31+
const [pendingAction, setPendingAction] = useState<
32+
'waiting-room' | 'same-chat' | null
33+
>(null)
2934

30-
// While a request is still streaming, rejoin is disabled: it would
35+
// While a request is still streaming, restart is disabled: it would
3136
// unmount <Chat> and abort the in-flight agent run. The promise is "we
3237
// let the agent finish" — honoring that means Enter does nothing until
3338
// the stream ends or the user hits Esc.
34-
const canRejoin = !isStreaming && !rejoining
35-
const rejoin = useCallback(() => {
36-
if (!canRejoin) return
37-
setRejoining(true)
39+
const canRestart = !isStreaming && pendingAction === null
40+
const pickNewModel = useCallback(() => {
41+
if (!canRestart) return
42+
setPendingAction('waiting-room')
3843
// Drop back to the landing picker (status: 'none') so the user picks a
3944
// model and hits Enter again to commit, instead of being silently
4045
// re-queued. app.tsx swaps us into <WaitingRoomScreen> on the
41-
// transition, unmounting this banner — no need to clear `rejoining` on
46+
// transition, unmounting this banner — no need to clear the pending state on
4247
// success.
4348
returnToFreebuffLanding({ resetChat: true }).catch(() =>
44-
setRejoining(false),
49+
setPendingAction(null),
4550
)
46-
}, [canRejoin])
51+
}, [canRestart])
52+
53+
const startSameChatSession = useCallback(() => {
54+
if (!canRestart) return
55+
setPendingAction('same-chat')
56+
// Re-POST with the currently selected model and keep the chat/run state
57+
// intact so the next prompt continues the same conversation.
58+
refreshFreebuffSession().catch(() => setPendingAction(null))
59+
}, [canRestart])
4760

4861
useKeyboard(
4962
useCallback(
5063
(key: KeyEvent) => {
51-
if (!canRejoin) return
64+
if (!canRestart) return
5265
if (key.name === 'return' || key.name === 'enter') {
5366
key.preventDefault?.()
54-
rejoin()
67+
startSameChatSession()
68+
return
69+
}
70+
if (key.name === 'escape') {
71+
key.preventDefault?.()
72+
pickNewModel()
5573
}
5674
},
57-
[rejoin, canRejoin],
75+
[startSameChatSession, pickNewModel, canRestart],
5876
),
5977
)
6078

@@ -83,14 +101,57 @@ export const SessionEndedBanner: React.FC<SessionEndedBannerProps> = ({
83101
Agent is wrapping up. Rejoin the wait room after it's finished.
84102
</text>
85103
) : (
86-
<Button onClick={rejoin}>
87-
<text
88-
style={{ fg: rejoining ? theme.muted : theme.primary }}
89-
attributes={TextAttributes.BOLD}
104+
<box
105+
style={{
106+
width: '100%',
107+
flexDirection: 'row',
108+
alignItems: 'center',
109+
gap: 2,
110+
}}
111+
>
112+
<Button onClick={startSameChatSession}>
113+
<text
114+
style={{
115+
fg:
116+
pendingAction === 'same-chat'
117+
? theme.muted
118+
: theme.primary,
119+
}}
120+
attributes={TextAttributes.BOLD}
121+
>
122+
{pendingAction === 'same-chat'
123+
? 'Starting…'
124+
: 'Press Enter to continue in a new session'}
125+
</text>
126+
</Button>
127+
<box style={{ flexGrow: 1 }} />
128+
<Button
129+
onClick={pickNewModel}
130+
style={{
131+
borderStyle: 'single',
132+
borderColor:
133+
pendingAction === 'waiting-room' ? theme.muted : theme.border,
134+
customBorderChars: BORDER_CHARS,
135+
paddingLeft: 1,
136+
paddingRight: 1,
137+
}}
138+
border={['top', 'bottom', 'left', 'right']}
90139
>
91-
{rejoining ? 'Rejoining…' : 'Press Enter to rejoin waiting room'}
92-
</text>
93-
</Button>
140+
<text
141+
style={{
142+
fg:
143+
pendingAction === 'waiting-room'
144+
? theme.muted
145+
: theme.foreground,
146+
}}
147+
attributes={TextAttributes.BOLD}
148+
>
149+
{pendingAction === 'waiting-room'
150+
? 'Opening model selection…'
151+
: 'Change model (ESC)'}
152+
</text>
153+
</Button>
154+
</box>
94155
)}
95156
</box>
96157
)

cli/src/hooks/use-freebuff-session.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -467,7 +467,10 @@ export function useFreebuffSession(): UseFreebuffSessionResult {
467467
useFreebuffModelStore
468468
.getState()
469469
.setSelectedModel(FALLBACK_FREEBUFF_MODEL_ID)
470-
nextMethod = 'GET'
470+
// The unavailable response came from a POST attempt. Re-POST with
471+
// the fallback model; a GET would only redisplay the old ended row
472+
// and leave the restart banner stuck in its pending state.
473+
nextMethod = 'POST'
471474
schedule(0)
472475
return
473476
}

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,9 @@ export const useSendMessage = ({
138138
setRunState,
139139
setIsRetrying,
140140
} = useChatStore.getState()
141-
const previousRunStateRef = useRef<RunState | null>(null)
141+
const previousRunStateRef = useRef<RunState | null>(
142+
useChatStore.getState().runState,
143+
)
142144
// Memoize stream controller to maintain referential stability across renders
143145
const streamRefsRef = useRef<ReturnType<
144146
typeof createStreamController
@@ -198,6 +200,7 @@ export const useSendMessage = ({
198200

199201
function clearMessages() {
200202
previousRunStateRef.current = null
203+
setRunState(null)
201204
}
202205

203206
const prepareUserMessage = useCallback(

0 commit comments

Comments
 (0)