mirror of
https://github.com/openai/codex.git
synced 2026-10-21 07:42:01 +00:00
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
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -59,6 +59,7 @@ async fn older_server_notice_falls_back_in_short_overview() {
|
||||
.collect::<String>();
|
||||
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(),
|
||||
|
||||
@@ -180,7 +180,6 @@ pub(super) struct AgentsOverviewView {
|
||||
project_groups: Vec<AgentsOverviewProjectGroup>,
|
||||
selected: usize,
|
||||
state: Arc<Mutex<AgentsOverviewViewState>>,
|
||||
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<AgentsOverviewRow>,
|
||||
selected_thread_id: Option<ThreadId>,
|
||||
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 => {
|
||||
|
||||
@@ -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?;
|
||||
|
||||
Reference in New Issue
Block a user