From 3ef3cecd2002c7841519df37417426d66fd5cc3d Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Wed, 9 Sep 2026 23:58:34 +0000 Subject: [PATCH] Open tasks with Right from the agents overview (#44344) ## What changed Allow `Right` to open the selected task from the task list or an empty, focused composer. Preserve editor keybindings and guard against pending input, active popups, and offline state. Keep metadata editing unaffected and show the shortcut in navigation hints. ## Testing Add regression tests for opening the current or highlighted task, preserving cursor movement and renaming, and preventing navigation while offline. Update snapshots to cover the new hints. GitOrigin-RevId: 9fc01b62ad0829b964509ae6286a041fc4936b9d --- codex-rs/tui/src/app/agents_overview_input.rs | 23 ++++- .../tui/src/app/agents_overview_render.rs | 6 +- codex-rs/tui/src/app/agents_overview_tests.rs | 90 +++++++++++++++++++ codex-rs/tui/src/app/agents_overview_view.rs | 1 + ...sts__overview_empty_prompt_right_hint.snap | 5 ++ ...view__tests__overview_right_open_hint.snap | 5 ++ ...daemon_command_center_vanished_rename.snap | 2 +- codex-rs/tui/src/bottom_pane/chat_composer.rs | 1 + .../chat_composer/agents_navigation.rs | 16 ++++ ...iew__tests__agents_overview_attention.snap | 2 +- ...__tests__agents_overview_empty_narrow.snap | 4 +- ...__tests__agents_overview_last_message.snap | 2 +- ...ts__agents_overview_worktree_grouping.snap | 3 +- 13 files changed, 151 insertions(+), 9 deletions(-) create mode 100644 codex-rs/tui/src/app/snapshots/codex_tui__app__agents_overview__tests__overview_empty_prompt_right_hint.snap create mode 100644 codex-rs/tui/src/app/snapshots/codex_tui__app__agents_overview__tests__overview_right_open_hint.snap diff --git a/codex-rs/tui/src/app/agents_overview_input.rs b/codex-rs/tui/src/app/agents_overview_input.rs index c2a194a4dd..0ecbcded17 100644 --- a/codex-rs/tui/src/app/agents_overview_input.rs +++ b/codex-rs/tui/src/app/agents_overview_input.rs @@ -1,5 +1,5 @@ //! Focus and input routing for the agent dashboard. Refreshes retain the shared -//! composer; dashboard actions never consume keys while its editor owns focus. +//! composer; Right opens the selected task from an empty editor with no pending input. use super::*; use crate::bottom_pane::InputResult; @@ -13,6 +13,17 @@ impl AgentsOverviewView { let mut state = self.state(); let offline = state.connection_notice.is_some(); let status_grouping = state.status_grouping; + if !offline + && crate::key_hint::plain(KeyCode::Right).is_press(key) + && state + .composer + .as_ref() + .is_some_and(ChatComposer::can_leave_empty_prompt_with_right) + { + drop(state); + self.activate(); + return; + } if key.code == KeyCode::Esc && !state.composer_owns_escape() { state.focus = AgentsOverviewFocus::List; return; @@ -102,6 +113,16 @@ impl AgentsOverviewView { { *key = "esc esc".to_string(); } + if composing + && state.connection_notice.is_none() + && state + .composer + .as_ref() + .is_some_and(ChatComposer::can_leave_empty_prompt_with_right) + && self.rows.get(self.selected).is_some() + { + hints.push(("→".to_string(), "open task".to_string())); + } if area.width < 60 && hints.len() > 2 { hints.remove(/*index*/ 1); } diff --git a/codex-rs/tui/src/app/agents_overview_render.rs b/codex-rs/tui/src/app/agents_overview_render.rs index 1798d81daa..a3c2928d93 100644 --- a/codex-rs/tui/src/app/agents_overview_render.rs +++ b/codex-rs/tui/src/app/agents_overview_render.rs @@ -63,7 +63,11 @@ impl AgentsOverviewView { "resume", true, ); - add_hint(list_hint(ListAction::Accept), "open", true); + let open_hint = (!self.state().editing_metadata()) + .then(|| list_hint(ListAction::MoveRight)) + .flatten() + .or_else(|| list_hint(ListAction::Accept)); + add_hint(open_hint, "open", true); add_hint( self.agents_keymap .primary_hint("new_task", &self.agents_keymap.new_task), diff --git a/codex-rs/tui/src/app/agents_overview_tests.rs b/codex-rs/tui/src/app/agents_overview_tests.rs index 452bee5912..182b26af6c 100644 --- a/codex-rs/tui/src/app/agents_overview_tests.rs +++ b/codex-rs/tui/src/app/agents_overview_tests.rs @@ -106,6 +106,96 @@ fn overview_draft(app: &App) -> (String, usize) { (composer.current_text_with_pending(), composer.cursor()) } +#[tokio::test] +async fn overview_right_opens_current_or_highlighted_task() { + let mut app = make_test_app().await; + let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel(); + app.app_event_tx = AppEventSender::new(tx); + let current = ThreadId::new(); + let other = ThreadId::new(); + app.primary_thread_id = Some(current); + let threads = vec![ + overview_thread( + current, + /*parent_thread_id*/ None, + "Current", + ThreadStatus::Idle, + ), + overview_thread( + other, + /*parent_thread_id*/ None, + "Other", + ThreadStatus::Idle, + ), + ]; + + let view = app.agents_overview_view(threads.clone(), /*selected_thread_id*/ None); + app.chat_widget.show_bottom_pane_view(Box::new(view)); + let rendered = render_bottom_popup(&app.chat_widget, /*width*/ 96); + insta::assert_snapshot!( + "overview_empty_prompt_right_hint", + rendered.lines().last().unwrap() + ); + app.chat_widget.handle_key_event(KeyCode::Right.into()); + assert!( + matches!(rx.try_recv(), Ok(AppEvent::SelectAgentsOverviewThread { thread_id }) if thread_id == current) + ); + assert!(app.chat_widget.no_modal_or_popup_active()); + + let mut view = app.agents_overview_view(threads, /*selected_thread_id*/ None); + view.handle_key_event(KeyCode::Esc.into()); + view.handle_key_event(KeyCode::Down.into()); + app.chat_widget.show_bottom_pane_view(Box::new(view)); + let rendered = render_bottom_popup(&app.chat_widget, /*width*/ 96); + insta::assert_snapshot!( + "overview_right_open_hint", + rendered.lines().find(|line| line.contains("open")).unwrap() + ); + app.chat_widget.handle_key_event(KeyCode::Right.into()); + assert!( + matches!(rx.try_recv(), Ok(AppEvent::SelectAgentsOverviewThread { thread_id }) if thread_id == other) + ); + assert!(app.chat_widget.no_modal_or_popup_active()); +} + +#[tokio::test] +async fn overview_right_preserves_editors_and_offline_state() { + let mut app = make_test_app().await; + app.config.disable_paste_burst = true; + let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel(); + app.app_event_tx = AppEventSender::new(tx); + let thread_id = ThreadId::new(); + let mut view = app.agents_overview_view( + vec![overview_thread( + thread_id, + /*parent_thread_id*/ None, + "Task", + ThreadStatus::Idle, + )], + Some(thread_id), + ); + view.handle_paste("ab".into()); + view.handle_key_event(KeyCode::Home.into()); + view.handle_key_event(KeyCode::Right.into()); + assert_eq!(overview_draft(&app), ("ab".into(), 1)); + view.handle_key_event(KeyCode::Esc.into()); + view.handle_key_event(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL)); + view.handle_key_event(KeyCode::Right.into()); + assert!(app.agents_overview.view_state.lock().unwrap().renaming); + view.handle_key_event(KeyCode::Esc.into()); + app.agents_overview + .view_state + .lock() + .unwrap() + .connection_notice = Some("Offline"); + view.handle_key_event(KeyCode::Right.into()); + view.handle_key_event(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::CONTROL)); + view.on_ctrl_c(); + view.handle_key_event(KeyCode::Right.into()); + assert!(!view.is_complete()); + assert!(rx.try_recv().is_err()); +} + #[tokio::test] async fn overview_composer_preserves_editing_and_routes_focus() { let mut app = make_test_app().await; diff --git a/codex-rs/tui/src/app/agents_overview_view.rs b/codex-rs/tui/src/app/agents_overview_view.rs index 607981fe6e..d89b8263ee 100644 --- a/codex-rs/tui/src/app/agents_overview_view.rs +++ b/codex-rs/tui/src/app/agents_overview_view.rs @@ -739,6 +739,7 @@ impl BottomPaneView for AgentsOverviewView { self.move_selection(action == ListAction::PageDown); } } + ListAction::MoveRight if !self.state().editing_metadata() => self.activate(), ListAction::MoveLeft | ListAction::MoveRight => {} } } else if key.code == KeyCode::Backspace { diff --git a/codex-rs/tui/src/app/snapshots/codex_tui__app__agents_overview__tests__overview_empty_prompt_right_hint.snap b/codex-rs/tui/src/app/snapshots/codex_tui__app__agents_overview__tests__overview_empty_prompt_right_hint.snap new file mode 100644 index 0000000000..12858275b5 --- /dev/null +++ b/codex-rs/tui/src/app/snapshots/codex_tui__app__agents_overview__tests__overview_empty_prompt_right_hint.snap @@ -0,0 +1,5 @@ +--- +source: tui/src/app/agents_overview_tests.rs +expression: rendered.lines().last().unwrap() +--- + enter create task ctrl+j newline esc tasks → open task diff --git a/codex-rs/tui/src/app/snapshots/codex_tui__app__agents_overview__tests__overview_right_open_hint.snap b/codex-rs/tui/src/app/snapshots/codex_tui__app__agents_overview__tests__overview_right_open_hint.snap new file mode 100644 index 0000000000..4ddb6123e8 --- /dev/null +++ b/codex-rs/tui/src/app/snapshots/codex_tui__app__agents_overview__tests__overview_right_open_hint.snap @@ -0,0 +1,5 @@ +--- +source: tui/src/app/agents_overview_tests.rs +expression: "rendered.lines().find(|line| line.contains(\"open\")).unwrap()" +--- + ↑↓ navigate ctrl+o resume → open ctrl+n new task ctrl+f search ctrl+s group ctrl+r rename diff --git a/codex-rs/tui/src/app/tests/snapshots/codex_tui__app__tests__navigation_reconnect__daemon_command_center_vanished_rename.snap b/codex-rs/tui/src/app/tests/snapshots/codex_tui__app__tests__navigation_reconnect__daemon_command_center_vanished_rename.snap index 511e2dfa0c..112848c292 100644 --- a/codex-rs/tui/src/app/tests/snapshots/codex_tui__app__tests__navigation_reconnect__daemon_command_center_vanished_rename.snap +++ b/codex-rs/tui/src/app/tests/snapshots/codex_tui__app__tests__navigation_reconnect__daemon_command_center_vanished_rename.snap @@ -25,4 +25,4 @@ expression: "render_bottom_popup(&app.chat_widget, 100)" › Describe a new task - enter create task ctrl+j newline esc tasks + enter create task ctrl+j newline esc tasks → open task diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index 05dbee3e91..b5639c05e2 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -18,6 +18,7 @@ //! # Key Event Routing //! //! Plain Left opens agents when the local-daemon composer is empty and available for input. +//! The agents dashboard uses the matching empty-editor guards for Right to open a task. //! Explicit editor remaps take precedence. //! Most key handling goes through [`ChatComposer::handle_key_event`], which dispatches to a //! popup-specific handler if a popup is visible and otherwise to diff --git a/codex-rs/tui/src/bottom_pane/chat_composer/agents_navigation.rs b/codex-rs/tui/src/bottom_pane/chat_composer/agents_navigation.rs index f2fdbf0dd2..bace61118c 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer/agents_navigation.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer/agents_navigation.rs @@ -2,10 +2,26 @@ //! //! Only local-daemon sessions enable this shortcut. Draft contents, transient input surfaces, //! and explicit editor remaps take precedence over navigation. +//! The dashboard can also leave an empty editor with Right under the same input guards. use super::*; impl ChatComposer { + pub(crate) fn can_leave_empty_prompt_with_right(&self) -> bool { + let move_right = if self.draft.textarea.is_vim_normal_mode() { + &self.vim_normal_keymap.move_right + } else { + &self.editor_keymap.move_right + }; + move_right.is_pressed(KeyCode::Right.into()) + && self.has_focus + && self.draft.input_enabled + && self.is_empty() + && !self.is_in_paste_burst() + && !self.popup_active() + && !self.draft.textarea.is_vim_operator_pending() + } + pub(crate) fn set_agents_navigation_enabled(&mut self, enabled: bool) { self.agents_navigation_enabled = enabled; } diff --git a/codex-rs/tui/src/snapshots/codex_tui__app__agents_overview__tests__agents_overview_attention.snap b/codex-rs/tui/src/snapshots/codex_tui__app__agents_overview__tests__agents_overview_attention.snap index 2a46cc98eb..d5d44aa746 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__app__agents_overview__tests__agents_overview_attention.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__app__agents_overview__tests__agents_overview_attention.snap @@ -25,4 +25,4 @@ expression: "render_bottom_popup(&app.chat_widget,\n96).replace(&format!(\"{proj › Describe a new task - enter create task ctrl+j newline esc tasks + enter create task ctrl+j newline esc tasks → open task diff --git a/codex-rs/tui/src/snapshots/codex_tui__app__agents_overview__tests__agents_overview_empty_narrow.snap b/codex-rs/tui/src/snapshots/codex_tui__app__agents_overview__tests__agents_overview_empty_narrow.snap index f790e818b3..5eff11dbb0 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__app__agents_overview__tests__agents_overview_empty_narrow.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__app__agents_overview__tests__agents_overview_empty_narrow.snap @@ -23,6 +23,6 @@ expression: "render_bottom_popup(&app.chat_widget, 48)" › Describe a new task - ↑↓ navigate ctrl+o resume enter open ctrl+n - new task ctrl+f search ctrl+s group ctrl+r + ↑↓ navigate ctrl+o resume → open ctrl+n new + task ctrl+f search ctrl+s group ctrl+r rename ctrl+x stop esc back diff --git a/codex-rs/tui/src/snapshots/codex_tui__app__agents_overview__tests__agents_overview_last_message.snap b/codex-rs/tui/src/snapshots/codex_tui__app__agents_overview__tests__agents_overview_last_message.snap index 8954549f5a..4efc045fb0 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__app__agents_overview__tests__agents_overview_last_message.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__app__agents_overview__tests__agents_overview_last_message.snap @@ -25,4 +25,4 @@ expression: "render_bottom_popup(&app.chat_widget,\n96).replace(&format!(\"{proj › Describe a new task - enter create task ctrl+j newline esc tasks + enter create task ctrl+j newline esc tasks → open task diff --git a/codex-rs/tui/src/snapshots/codex_tui__app__agents_overview__tests__agents_overview_worktree_grouping.snap b/codex-rs/tui/src/snapshots/codex_tui__app__agents_overview__tests__agents_overview_worktree_grouping.snap index 6bc3144270..da7d114dad 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__app__agents_overview__tests__agents_overview_worktree_grouping.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__app__agents_overview__tests__agents_overview_worktree_grouping.snap @@ -1,6 +1,5 @@ --- source: tui/src/app/agents_overview_tests.rs -assertion_line: 943 expression: grouping --- Agent command center @@ -26,4 +25,4 @@ expression: grouping › Describe a new task - enter create task ctrl+j newline esc tasks + enter create task ctrl+j newline esc tasks → open task