@@ -3,7 +3,10 @@ import { useKeyboard } from '@opentui/react'
33import React , { useCallback , useState } from 'react'
44
55import { Button } from './button'
6- import { returnToFreebuffLanding } from '../hooks/use-freebuff-session'
6+ import {
7+ refreshFreebuffSession ,
8+ returnToFreebuffLanding ,
9+ } from '../hooks/use-freebuff-session'
710import { useTheme } from '../hooks/use-theme'
811import { 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 */
2427export 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 )
0 commit comments