diff --git a/codex-rs/tui/src/app/agents_overview.rs b/codex-rs/tui/src/app/agents_overview.rs index b0ef6e2d19..cb1ebc9252 100644 --- a/codex-rs/tui/src/app/agents_overview.rs +++ b/codex-rs/tui/src/app/agents_overview.rs @@ -284,7 +284,6 @@ impl App { AgentsOverviewView::new( rows, selected_thread_id, - self.primary_thread_id.is_none(), self.config.features.enabled(Feature::Worktrees) && !crate::uses_remote_workspace_or_environment( &self.app_server_target, diff --git a/codex-rs/tui/src/app/agents_overview_tests.rs b/codex-rs/tui/src/app/agents_overview_tests.rs index 182b26af6c..e2bff9b91d 100644 --- a/codex-rs/tui/src/app/agents_overview_tests.rs +++ b/codex-rs/tui/src/app/agents_overview_tests.rs @@ -59,6 +59,7 @@ async fn older_server_notice_falls_back_in_short_overview() { .collect::(); insta::assert_snapshot!(header.trim_end(), @" Old srv"); } +use crate::app::agents_overview_view::AgentsOverviewFocus; use crate::app::test_support::make_test_app; use crate::app_event::AgentsOverviewThreadRefresh; use crate::bottom_pane::BottomPaneView; @@ -106,6 +107,66 @@ fn overview_draft(app: &App) -> (String, usize) { (composer.current_text_with_pending(), composer.cursor()) } +#[tokio::test] +async fn overview_escape_returns_from_list_to_composer() { + for (vim, offline) in [(false, false), (true, false), (false, true), (true, true)] { + let mut app = make_test_app().await; + app.config.disable_paste_burst = true; + if vim { + app.chat_widget.toggle_vim_mode_and_notify(); + } + let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel(); + app.app_event_tx = AppEventSender::new(tx); + let view = app.agents_overview_view( + vec![overview_thread( + ThreadId::new(), + /*parent_thread_id*/ None, + "Existing task", + ThreadStatus::Idle, + )], + /*selected_thread_id*/ None, + ); + app.chat_widget.show_bottom_pane_view(Box::new(view)); + app.chat_widget.handle_paste("task draft".into()); + app.chat_widget.handle_key_event(KeyCode::Home.into()); + let draft = overview_draft(&app); + if offline { + app.agents_overview + .view_state + .lock() + .unwrap() + .connection_notice = Some("Reconnecting"); + } + if vim { + app.chat_widget.handle_key_event(KeyCode::Esc.into()); + } + app.chat_widget.handle_key_event(KeyCode::Esc.into()); + assert!(matches!( + app.agents_overview.view_state.lock().unwrap().focus, + AgentsOverviewFocus::List + )); + app.chat_widget.handle_key_event(KeyCode::Down.into()); + app.chat_widget.handle_key_event(KeyCode::Esc.into()); + + assert!(matches!( + app.agents_overview.view_state.lock().unwrap().focus, + AgentsOverviewFocus::Composer + )); + assert_eq!( + app.chat_widget + .selected_index_for_present_view(AGENTS_OVERVIEW_VIEW_ID), + Some(0) + ); + assert_eq!(overview_draft(&app), draft); + assert!(rx.try_recv().is_err()); + if !vim && !offline { + insta::assert_snapshot!(render_bottom_popup(&app.chat_widget, /*width*/ 80).lines().last().unwrap(), @" enter create task ctrl+j newline esc tasks"); + } + app.chat_widget.handle_key_event(KeyCode::Char('!').into()); + assert_eq!(overview_draft(&app).0, "!task draft"); + } +} + #[tokio::test] async fn overview_right_opens_current_or_highlighted_task() { let mut app = make_test_app().await; @@ -658,7 +719,7 @@ async fn shared_overview_seeds_once_and_retains_locally_resumed_history() -> Res app.chat_widget .handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE)); app.chat_widget - .handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE)); + .handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); finish_overview_refresh(&mut app, &app_server, &mut event_rx).await; let retained: HashSet<_> = app.agents_overview.threads.keys().copied().collect(); assert_eq!(retained, expected); @@ -1164,7 +1225,6 @@ async fn shared_overview_shows_only_root_sessions() { let mut action_view = AgentsOverviewView::new( view.rows.clone(), Some(first_root), - /*exit_on_cancel*/ false, /*worktrees_enabled*/ false, crate::app_event_sender::AppEventSender::new(event_tx), app.keymap.clone(), @@ -1282,7 +1342,7 @@ async fn shared_overview_shows_only_root_sessions() { app.chat_widget .handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE)); app.chat_widget - .handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE)); + .handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); let view = app.agents_overview_view(threads, /*selected_thread_id*/ None); app.chat_widget.show_bottom_pane_view(Box::new(view)); let rendered = render_bottom_popup(&app.chat_widget, /*width*/ 96); @@ -1304,7 +1364,7 @@ async fn shared_overview_shows_only_root_sessions() { .expect("render full-screen dashboard"); assert_eq!(tui.terminal.viewport_area.height, screen_size.height); app.chat_widget - .handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE)); + .handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); app.render_chat_widget_frame(&mut tui, screen_size) .expect("restore conversation after closing dashboard"); assert!(tui.terminal.viewport_area.height < screen_size.height); @@ -1406,7 +1466,6 @@ async fn filtered_dashboard_actions_use_configured_shortcuts() { ) .rows, Some(first), - /*exit_on_cancel*/ false, /*worktrees_enabled*/ false, crate::app_event_sender::AppEventSender::new(event_tx), app.keymap.clone(), diff --git a/codex-rs/tui/src/app/agents_overview_view.rs b/codex-rs/tui/src/app/agents_overview_view.rs index d89b8263ee..5db8cad719 100644 --- a/codex-rs/tui/src/app/agents_overview_view.rs +++ b/codex-rs/tui/src/app/agents_overview_view.rs @@ -180,7 +180,6 @@ pub(super) struct AgentsOverviewView { project_groups: Vec, selected: usize, state: Arc>, - exit_on_cancel: bool, app_event_tx: AppEventSender, keymap: ListKeymap, agents_keymap: AgentsKeymap, @@ -192,7 +191,6 @@ impl AgentsOverviewView { pub(super) fn new( rows: Vec, selected_thread_id: Option, - exit_on_cancel: bool, worktrees_enabled: bool, app_event_tx: AppEventSender, keymap: RuntimeKeymap, @@ -223,7 +221,6 @@ impl AgentsOverviewView { project_groups, selected, state, - exit_on_cancel, app_event_tx, keymap: keymap.list, agents_keymap: keymap.agents, @@ -650,7 +647,9 @@ impl BottomPaneView for AgentsOverviewView { return; } - if self.state().connection_notice.is_some() && !self.agents_keymap.new_task.is_pressed(key) + if self.state().connection_notice.is_some() + && !self.agents_keymap.new_task.is_pressed(key) + && self.keymap.action_for(key) != Some(ListAction::Cancel) { match self.keymap.action_for(key) { Some(ListAction::MoveUp) => self.move_selection(/*forward*/ false), @@ -727,11 +726,7 @@ impl BottomPaneView for AgentsOverviewView { state.input.clear(); state.renaming = false; } else { - if self.exit_on_cancel { - self.app_event_tx - .send(AppEvent::Exit(crate::app::ExitMode::Immediate)); - } - state.completion = Some(ViewCompletion::Cancelled); + state.focus_composer(); } } ListAction::PageUp | ListAction::PageDown => { diff --git a/codex-rs/tui/src/app/tests/navigation_reconnect_tests.rs b/codex-rs/tui/src/app/tests/navigation_reconnect_tests.rs index 01338a16a5..2d8ef54a58 100644 --- a/codex-rs/tui/src/app/tests/navigation_reconnect_tests.rs +++ b/codex-rs/tui/src/app/tests/navigation_reconnect_tests.rs @@ -415,6 +415,12 @@ async fn reconnect_daemon_command_center_after_socket_replacement_without_a_conv ) .await?; + assert!(app.chat_widget.has_active_view()); + // Esc returns to the overview composer. Dismiss the retained view explicitly + // to inspect the unavailable conversation and its cached draft below. + app.agents_overview.view_state.lock().unwrap().completion = + Some(crate::bottom_pane::ViewCompletion::Accepted); + app.chat_widget.handle_key_event(KeyCode::Null.into()); assert!(!app.chat_widget.has_active_view()); assert_eq!(app.current_displayed_thread_id(), Some(id)); let history = drain_history(&mut app, &mut tui, &mut session, &mut events).await?;