Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion codex-rs/tui/src/app/agents_overview.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
69 changes: 64 additions & 5 deletions codex-rs/tui/src/app/agents_overview_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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(),
Expand Down
13 changes: 4 additions & 9 deletions codex-rs/tui/src/app/agents_overview_view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -223,7 +221,6 @@ impl AgentsOverviewView {
project_groups,
selected,
state,
exit_on_cancel,
app_event_tx,
keymap: keymap.list,
agents_keymap: keymap.agents,
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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 => {
Expand Down
6 changes: 6 additions & 0 deletions codex-rs/tui/src/app/tests/navigation_reconnect_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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?;
Expand Down
Loading