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