mirror of
https://github.com/openai/codex.git
synced 2026-10-20 00:18:49 +00:00
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
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+5
@@ -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
|
||||
+5
@@ -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
|
||||
+1
-1
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-2
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user