Skip to content

Commit a62e98d

Browse files
etraut-openaicopyberry
authored andcommitted
Return focus to the agents overview composer on Escape (#44360)
## Why Pressing `Esc` in the agents overview task list dismissed the overview or exited Codex when no primary thread existed, instead of returning to the draft. ## What changed Make list cancellation return focus to the composer, including while reconnecting, preserving the draft and cursor position. ## Testing Add regression coverage for returning from the list to the composer with Vim mode enabled or disabled, both online and reconnecting. Verify draft and cursor preservation, resumed text entry, and the absence of app events. GitOrigin-RevId: 8920677c42b0c9ff5023979e198f069abcfcf54b
1 parent d996b4f commit a62e98d

4 files changed

Lines changed: 74 additions & 15 deletions

File tree

‎codex-rs/tui/src/app/agents_overview.rs‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,6 @@ impl App {
284284
AgentsOverviewView::new(
285285
rows,
286286
selected_thread_id,
287-
self.primary_thread_id.is_none(),
288287
self.config.features.enabled(Feature::Worktrees)
289288
&& !crate::uses_remote_workspace_or_environment(
290289
&self.app_server_target,

‎codex-rs/tui/src/app/agents_overview_tests.rs‎

Lines changed: 64 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ async fn older_server_notice_falls_back_in_short_overview() {
5959
.collect::<String>();
6060
insta::assert_snapshot!(header.trim_end(), @" Old srv");
6161
}
62+
use crate::app::agents_overview_view::AgentsOverviewFocus;
6263
use crate::app::test_support::make_test_app;
6364
use crate::app_event::AgentsOverviewThreadRefresh;
6465
use crate::bottom_pane::BottomPaneView;
@@ -106,6 +107,66 @@ fn overview_draft(app: &App) -> (String, usize) {
106107
(composer.current_text_with_pending(), composer.cursor())
107108
}
108109

110+
#[tokio::test]
111+
async fn overview_escape_returns_from_list_to_composer() {
112+
for (vim, offline) in [(false, false), (true, false), (false, true), (true, true)] {
113+
let mut app = make_test_app().await;
114+
app.config.disable_paste_burst = true;
115+
if vim {
116+
app.chat_widget.toggle_vim_mode_and_notify();
117+
}
118+
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
119+
app.app_event_tx = AppEventSender::new(tx);
120+
let view = app.agents_overview_view(
121+
vec![overview_thread(
122+
ThreadId::new(),
123+
/*parent_thread_id*/ None,
124+
"Existing task",
125+
ThreadStatus::Idle,
126+
)],
127+
/*selected_thread_id*/ None,
128+
);
129+
app.chat_widget.show_bottom_pane_view(Box::new(view));
130+
app.chat_widget.handle_paste("task draft".into());
131+
app.chat_widget.handle_key_event(KeyCode::Home.into());
132+
let draft = overview_draft(&app);
133+
if offline {
134+
app.agents_overview
135+
.view_state
136+
.lock()
137+
.unwrap()
138+
.connection_notice = Some("Reconnecting");
139+
}
140+
if vim {
141+
app.chat_widget.handle_key_event(KeyCode::Esc.into());
142+
}
143+
app.chat_widget.handle_key_event(KeyCode::Esc.into());
144+
assert!(matches!(
145+
app.agents_overview.view_state.lock().unwrap().focus,
146+
AgentsOverviewFocus::List
147+
));
148+
app.chat_widget.handle_key_event(KeyCode::Down.into());
149+
app.chat_widget.handle_key_event(KeyCode::Esc.into());
150+
151+
assert!(matches!(
152+
app.agents_overview.view_state.lock().unwrap().focus,
153+
AgentsOverviewFocus::Composer
154+
));
155+
assert_eq!(
156+
app.chat_widget
157+
.selected_index_for_present_view(AGENTS_OVERVIEW_VIEW_ID),
158+
Some(0)
159+
);
160+
assert_eq!(overview_draft(&app), draft);
161+
assert!(rx.try_recv().is_err());
162+
if !vim && !offline {
163+
insta::assert_snapshot!(render_bottom_popup(&app.chat_widget, /*width*/ 80).lines().last().unwrap(), @" enter create task ctrl+j newline esc tasks");
164+
}
165+
app.chat_widget.handle_key_event(KeyCode::Char('!').into());
166+
assert_eq!(overview_draft(&app).0, "!task draft");
167+
}
168+
}
169+
109170
#[tokio::test]
110171
async fn overview_right_opens_current_or_highlighted_task() {
111172
let mut app = make_test_app().await;
@@ -658,7 +719,7 @@ async fn shared_overview_seeds_once_and_retains_locally_resumed_history() -> Res
658719
app.chat_widget
659720
.handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
660721
app.chat_widget
661-
.handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
722+
.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
662723
finish_overview_refresh(&mut app, &app_server, &mut event_rx).await;
663724
let retained: HashSet<_> = app.agents_overview.threads.keys().copied().collect();
664725
assert_eq!(retained, expected);
@@ -1164,7 +1225,6 @@ async fn shared_overview_shows_only_root_sessions() {
11641225
let mut action_view = AgentsOverviewView::new(
11651226
view.rows.clone(),
11661227
Some(first_root),
1167-
/*exit_on_cancel*/ false,
11681228
/*worktrees_enabled*/ false,
11691229
crate::app_event_sender::AppEventSender::new(event_tx),
11701230
app.keymap.clone(),
@@ -1282,7 +1342,7 @@ async fn shared_overview_shows_only_root_sessions() {
12821342
app.chat_widget
12831343
.handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
12841344
app.chat_widget
1285-
.handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
1345+
.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
12861346
let view = app.agents_overview_view(threads, /*selected_thread_id*/ None);
12871347
app.chat_widget.show_bottom_pane_view(Box::new(view));
12881348
let rendered = render_bottom_popup(&app.chat_widget, /*width*/ 96);
@@ -1304,7 +1364,7 @@ async fn shared_overview_shows_only_root_sessions() {
13041364
.expect("render full-screen dashboard");
13051365
assert_eq!(tui.terminal.viewport_area.height, screen_size.height);
13061366
app.chat_widget
1307-
.handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
1367+
.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
13081368
app.render_chat_widget_frame(&mut tui, screen_size)
13091369
.expect("restore conversation after closing dashboard");
13101370
assert!(tui.terminal.viewport_area.height < screen_size.height);
@@ -1406,7 +1466,6 @@ async fn filtered_dashboard_actions_use_configured_shortcuts() {
14061466
)
14071467
.rows,
14081468
Some(first),
1409-
/*exit_on_cancel*/ false,
14101469
/*worktrees_enabled*/ false,
14111470
crate::app_event_sender::AppEventSender::new(event_tx),
14121471
app.keymap.clone(),

‎codex-rs/tui/src/app/agents_overview_view.rs‎

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,6 @@ pub(super) struct AgentsOverviewView {
180180
project_groups: Vec<AgentsOverviewProjectGroup>,
181181
selected: usize,
182182
state: Arc<Mutex<AgentsOverviewViewState>>,
183-
exit_on_cancel: bool,
184183
app_event_tx: AppEventSender,
185184
keymap: ListKeymap,
186185
agents_keymap: AgentsKeymap,
@@ -192,7 +191,6 @@ impl AgentsOverviewView {
192191
pub(super) fn new(
193192
rows: Vec<AgentsOverviewRow>,
194193
selected_thread_id: Option<ThreadId>,
195-
exit_on_cancel: bool,
196194
worktrees_enabled: bool,
197195
app_event_tx: AppEventSender,
198196
keymap: RuntimeKeymap,
@@ -223,7 +221,6 @@ impl AgentsOverviewView {
223221
project_groups,
224222
selected,
225223
state,
226-
exit_on_cancel,
227224
app_event_tx,
228225
keymap: keymap.list,
229226
agents_keymap: keymap.agents,
@@ -650,7 +647,9 @@ impl BottomPaneView for AgentsOverviewView {
650647
return;
651648
}
652649

653-
if self.state().connection_notice.is_some() && !self.agents_keymap.new_task.is_pressed(key)
650+
if self.state().connection_notice.is_some()
651+
&& !self.agents_keymap.new_task.is_pressed(key)
652+
&& self.keymap.action_for(key) != Some(ListAction::Cancel)
654653
{
655654
match self.keymap.action_for(key) {
656655
Some(ListAction::MoveUp) => self.move_selection(/*forward*/ false),
@@ -727,11 +726,7 @@ impl BottomPaneView for AgentsOverviewView {
727726
state.input.clear();
728727
state.renaming = false;
729728
} else {
730-
if self.exit_on_cancel {
731-
self.app_event_tx
732-
.send(AppEvent::Exit(crate::app::ExitMode::Immediate));
733-
}
734-
state.completion = Some(ViewCompletion::Cancelled);
729+
state.focus_composer();
735730
}
736731
}
737732
ListAction::PageUp | ListAction::PageDown => {

‎codex-rs/tui/src/app/tests/navigation_reconnect_tests.rs‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,12 @@ async fn reconnect_daemon_command_center_after_socket_replacement_without_a_conv
415415
)
416416
.await?;
417417

418+
assert!(app.chat_widget.has_active_view());
419+
// Esc returns to the overview composer. Dismiss the retained view explicitly
420+
// to inspect the unavailable conversation and its cached draft below.
421+
app.agents_overview.view_state.lock().unwrap().completion =
422+
Some(crate::bottom_pane::ViewCompletion::Accepted);
423+
app.chat_widget.handle_key_event(KeyCode::Null.into());
418424
assert!(!app.chat_widget.has_active_view());
419425
assert_eq!(app.current_displayed_thread_id(), Some(id));
420426
let history = drain_history(&mut app, &mut tui, &mut session, &mut events).await?;

0 commit comments

Comments
 (0)