Add running-task exit choices to local daemon sessions (#38447)

## What changed

When `Ctrl-C` is pressed with an empty composer while a task is running in a local daemon session, show a menu that lets the user:

- cancel the task and stay in Codex;
- exit Codex while leaving the task running; or
- stop the task and exit Codex.

Do not offer background execution for side-thread tasks or when follow-up messages are queued. Preserve existing `Ctrl-C` behavior for non-daemon sessions, drafts, paste bursts, MCP startup, and returning from an active side thread.

## Testing

Add TUI tests for each exit action, menu dismissal, side threads, queued follow-ups, composer input, MCP startup, and non-daemon sessions.

GitOrigin-RevId: 539c33d3a4ff288a015bddbe9465d2bbdbe018d0
This commit is contained in:
Eric Traut authored and copyberry committed 2026-08-13 22:38:46 +00:00
1 parent 6344a655a5
commit ca83f7908c
9 files changed
+593 -11

No files matched your search

+55
View File
@@ -531,6 +531,61 @@ impl App {
}
return Ok(self.handle_exit_mode(app_server, mode).await);
}
AppEvent::RunningTaskExit { action, thread_id } => match action {
RunningTaskExitAction::RunInBackground => {
return Ok(self.handle_exit_mode(app_server, ExitMode::Immediate).await);
}
RunningTaskExitAction::CancelTask => {
if self.chat_widget.thread_id() == Some(thread_id)
&& self.chat_widget.is_agent_turn_running()
&& self.chat_widget.submit_op(AppCommand::interrupt())
{
self.chat_widget.pause_active_goal_for_interrupt();
} else if self.side_threads.contains_key(&thread_id)
&& let Err(error) = self
.try_submit_active_thread_op_via_app_server(
app_server,
thread_id,
&AppCommand::interrupt(),
)
.await
{
self.chat_widget
.add_error_message(format!("Failed to interrupt task: {error}"));
}
}
RunningTaskExitAction::Exit => {
if self.chat_widget.thread_id() == Some(thread_id)
&& self.chat_widget.is_active_goal_turn_running()
&& let Err(error) = app_server
.thread_goal_set(
thread_id,
/*objective*/ None,
Some(codex_app_server_protocol::ThreadGoalStatus::Paused),
/*token_budget*/ None,
)
.await
{
self.chat_widget
.add_error_message(format!("Failed to pause task goal: {error}"));
return Ok(AppRunControl::Continue);
}
let turn_id = self
.active_turn_id_for_thread(thread_id)
.await
.unwrap_or_default();
match app_server.turn_interrupt(thread_id, turn_id).await {
Ok(()) => {
self.app_event_tx
.send(AppEvent::Exit(ExitMode::ShutdownFirst));
}
Err(error) => {
self.chat_widget
.add_error_message(format!("Failed to interrupt task: {error}"));
}
}
}
},
AppEvent::Logout => match app_server.logout_account().await {
Ok(()) => {
self.show_shutdown_feedback(tui)?;