@@ -1925,19 +1925,9 @@ impl App {
19251925 AppEvent :: CodexEvent ( event) => {
19261926 self . enqueue_primary_event ( event) . await ?;
19271927 }
1928- AppEvent :: Exit ( mode) => match mode {
1929- ExitMode :: ShutdownFirst => {
1930- // Mark the thread we are explicitly shutting down for exit so
1931- // its shutdown completion does not trigger agent failover.
1932- self . pending_shutdown_exit_thread_id =
1933- self . active_thread_id . or ( self . chat_widget . thread_id ( ) ) ;
1934- self . chat_widget . submit_op ( Op :: Shutdown ) ;
1935- }
1936- ExitMode :: Immediate => {
1937- self . pending_shutdown_exit_thread_id = None ;
1938- return Ok ( AppRunControl :: Exit ( ExitReason :: UserRequested ) ) ;
1939- }
1940- } ,
1928+ AppEvent :: Exit ( mode) => {
1929+ return Ok ( self . handle_exit_mode ( mode) ) ;
1930+ }
19411931 AppEvent :: FatalExitRequest ( message) => {
19421932 return Ok ( AppRunControl :: Exit ( ExitReason :: Fatal ( message) ) ) ;
19431933 }
@@ -2914,6 +2904,27 @@ impl App {
29142904 Ok ( AppRunControl :: Continue )
29152905 }
29162906
2907+ fn handle_exit_mode ( & mut self , mode : ExitMode ) -> AppRunControl {
2908+ match mode {
2909+ ExitMode :: ShutdownFirst => {
2910+ // Mark the thread we are explicitly shutting down for exit so
2911+ // its shutdown completion does not trigger agent failover.
2912+ self . pending_shutdown_exit_thread_id =
2913+ self . active_thread_id . or ( self . chat_widget . thread_id ( ) ) ;
2914+ if self . chat_widget . submit_op ( Op :: Shutdown ) {
2915+ AppRunControl :: Continue
2916+ } else {
2917+ self . pending_shutdown_exit_thread_id = None ;
2918+ AppRunControl :: Exit ( ExitReason :: UserRequested )
2919+ }
2920+ }
2921+ ExitMode :: Immediate => {
2922+ self . pending_shutdown_exit_thread_id = None ;
2923+ AppRunControl :: Exit ( ExitReason :: UserRequested )
2924+ }
2925+ }
2926+ }
2927+
29172928 fn handle_codex_event_now ( & mut self , event : Event ) {
29182929 let needs_refresh = matches ! (
29192930 event. msg,
@@ -4703,6 +4714,34 @@ mod tests {
47034714 }
47044715 }
47054716
4717+ #[ tokio:: test]
4718+ async fn shutdown_first_exit_returns_immediate_exit_when_shutdown_submit_fails ( ) {
4719+ let mut app = make_test_app ( ) . await ;
4720+ let thread_id = ThreadId :: new ( ) ;
4721+ app. active_thread_id = Some ( thread_id) ;
4722+
4723+ let control = app. handle_exit_mode ( ExitMode :: ShutdownFirst ) ;
4724+
4725+ assert_eq ! ( app. pending_shutdown_exit_thread_id, None ) ;
4726+ assert ! ( matches!(
4727+ control,
4728+ AppRunControl :: Exit ( ExitReason :: UserRequested )
4729+ ) ) ;
4730+ }
4731+
4732+ #[ tokio:: test]
4733+ async fn shutdown_first_exit_waits_for_shutdown_when_submit_succeeds ( ) {
4734+ let ( mut app, _app_event_rx, mut op_rx) = make_test_app_with_channels ( ) . await ;
4735+ let thread_id = ThreadId :: new ( ) ;
4736+ app. active_thread_id = Some ( thread_id) ;
4737+
4738+ let control = app. handle_exit_mode ( ExitMode :: ShutdownFirst ) ;
4739+
4740+ assert_eq ! ( app. pending_shutdown_exit_thread_id, Some ( thread_id) ) ;
4741+ assert ! ( matches!( control, AppRunControl :: Continue ) ) ;
4742+ assert_eq ! ( op_rx. try_recv( ) , Ok ( Op :: Shutdown ) ) ;
4743+ }
4744+
47064745 #[ tokio:: test]
47074746 async fn clear_only_ui_reset_preserves_chat_session_state ( ) {
47084747 let mut app = make_test_app ( ) . await ;
0 commit comments