Skip to content
Open
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
29 changes: 29 additions & 0 deletions codex-rs/tui/src/bottom_pane/list_selection_view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ pub(crate) struct SelectionViewParams {
pub tabs: Vec<SelectionTab>,
pub initial_tab_id: Option<String>,
pub is_searchable: bool,
pub tree_navigation_enabled: bool,
pub search_placeholder: Option<String>,
pub col_width_mode: ColumnWidthMode,
pub row_display: SelectionRowDisplay,
Expand Down Expand Up @@ -224,6 +225,7 @@ impl Default for SelectionViewParams {
tabs: Vec::new(),
initial_tab_id: None,
is_searchable: false,
tree_navigation_enabled: false,
search_placeholder: None,
col_width_mode: ColumnWidthMode::AutoVisible,
row_display: SelectionRowDisplay::Wrapped,
Expand Down Expand Up @@ -259,6 +261,7 @@ pub(crate) struct ListSelectionView {
dismiss_after_child_accept: bool,
app_event_tx: AppEventSender,
is_searchable: bool,
tree_navigation_enabled: bool,
search_query: String,
search_placeholder: Option<String>,
col_width_mode: ColumnWidthMode,
Expand Down Expand Up @@ -330,6 +333,7 @@ impl ListSelectionView {
dismiss_after_child_accept: false,
app_event_tx,
is_searchable: params.is_searchable,
tree_navigation_enabled: params.tree_navigation_enabled,
search_query: String::new(),
search_placeholder: if params.is_searchable {
params.search_placeholder
Expand Down Expand Up @@ -977,6 +981,31 @@ impl BottomPaneView for ListSelectionView {
{
self.switch_tab(/*step*/ 1)
}
_ if allow_plain_char_navigation
&& self.tree_navigation_enabled
&& self.keymap.move_left.is_pressed(key_event) =>
{
self.on_ctrl_c();
}
_ if allow_plain_char_navigation
&& self.tree_navigation_enabled
&& self.keymap.move_right.is_pressed(key_event) =>
{
let selected_opens_child = self
.state
.selected_idx
.and_then(|idx| self.filtered_indices.get(idx).copied())
.and_then(|actual_idx| self.active_items().get(actual_idx))
.is_some_and(|item| {
!item.is_disabled
&& item.disabled_reason.is_none()
&& !item.actions.is_empty()
&& item.dismiss_parent_on_child_accept
});
if selected_opens_child {
self.accept();
}
}
KeyEvent {
code: KeyCode::Backspace,
..
Expand Down
20 changes: 14 additions & 6 deletions codex-rs/tui/src/chatwidget/settings_popups.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,9 @@ impl ChatWidget {

self.bottom_pane.show_selection_view(SelectionViewParams {
header: Box::new(header),
footer_hint: Some(Line::from("Press enter to open; esc to close")),
footer_hint: Some(Line::from("Right or Enter opens; Left or Esc closes")),
items,
tree_navigation_enabled: true,
..Default::default()
});
}
Expand Down Expand Up @@ -180,7 +181,8 @@ impl ChatWidget {
is_disabled: multi_agent_v2,
disabled_reason: multi_agent_v2.then(|| "Managed by multi_agent_v2.".to_string()),
actions,
dismiss_on_select: true,
dismiss_on_select: false,
dismiss_parent_on_child_accept: true,
..Default::default()
}
}
Expand Down Expand Up @@ -240,8 +242,9 @@ impl ChatWidget {

self.bottom_pane.show_selection_view(SelectionViewParams {
header: Box::new(header),
footer_hint: Some(standard_popup_hint_line()),
footer_hint: Some(Line::from("Enter selects; Left or Esc goes back")),
items,
tree_navigation_enabled: true,
..Default::default()
});
}
Expand All @@ -266,9 +269,12 @@ impl ChatWidget {

self.bottom_pane.show_selection_view(SelectionViewParams {
header: Box::new(header),
footer_hint: Some(Line::from("Press space to toggle; esc to close")),
footer_hint: Some(Line::from(
"Right or Enter opens; Space toggles; Left or Esc goes back",
)),
items,
is_searchable: true,
tree_navigation_enabled: true,
search_placeholder: Some(format!("Search {} settings", section.label())),
..Default::default()
});
Expand Down Expand Up @@ -545,7 +551,8 @@ fn config_section_item(
section.description()
)),
actions,
dismiss_on_select: true,
dismiss_on_select: false,
dismiss_parent_on_child_accept: true,
search_value: Some(format!(
"{} {} {}",
section.id(),
Expand Down Expand Up @@ -577,7 +584,8 @@ fn rate_limit_reset_config_item() -> SelectionItem {
name: "Use a usage limit reset".to_string(),
description: Some("Refresh and choose an exact banked reset.".to_string()),
actions,
dismiss_on_select: true,
dismiss_on_select: false,
dismiss_parent_on_child_accept: true,
search_value: Some("usage limit reset banked credit manual".to_string()),
..Default::default()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,4 @@ expression: account_popup
[ ] Automated goal plans Advance goal plans when exactly one next goal
is ready.

Press space to toggle; esc to close
Right or Enter opens; Space toggles; Left or Esc goes back
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
source: tui/src/chatwidget/tests/popups_and_settings.rs
expression: popup
---
Agent subagent threads
Cap concurrent subagent threads per agent run; restart to apply.

› 1. 1 (current) Allow up to 1 concurrent subagent threads per agent run.
2. 2 Allow up to 2 concurrent subagent threads per agent run.
3. 3 Allow up to 3 concurrent subagent threads per agent run.
4. 4 Allow up to 4 concurrent subagent threads per agent run.
5. 6 (default) Allow up to 6 concurrent subagent threads per agent run.
6. 8 Allow up to 8 concurrent subagent threads per agent run.
7. 12 Allow up to 12 concurrent subagent threads per agent run.

Enter selects; Left or Esc goes back
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@ expression: ai_context_popup
context.
Back to sections Return to the config section menu.

Press space to toggle; esc to close
Right or Enter opens; Space toggles; Left or Esc goes back
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,4 @@ expression: interface_popup
[*] Feedback Allow feedback collection from the TUI.
[*] Unstable feature warnings Show warnings for enabled under-development features.

Press space to toggle; esc to close
Right or Enter opens; Space toggles; Left or Esc goes back
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@ expression: popup
4. Agent subagent threads Max concurrent subagent threads per agent run (currently
the built-in default).

Press enter to open; esc to close
Right or Enter opens; Left or Esc closes
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ expression: "normalize_snapshot_paths(render_bottom_popup(&chat, 90))"
1. Banked reset Earned reset credit.
› 2. Cancel (default) (n)

Press enter to confirm or esc to go back
Enter selects; Left or Esc goes back
56 changes: 56 additions & 0 deletions codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3438,6 +3438,61 @@ async fn config_popup_snapshot_and_toggle() {
);
}

#[tokio::test]
async fn config_popup_arrow_keys_navigate_menu_tree() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.2")).await;
chat.thread_id = Some(ThreadId::new());
while rx.try_recv().is_ok() {}

chat.open_config_popup();
chat.handle_key_event(KeyEvent::from(KeyCode::Down));
chat.handle_key_event(KeyEvent::from(KeyCode::Right));
let section = match rx.try_recv() {
Ok(AppEvent::OpenConfigSection { section }) => {
assert_eq!(
section,
crate::common_config_options::CommonConfigSection::AiContext
);
section
}
event => panic!("expected Right to open the selected config section, got {event:?}"),
};

// Simulate the app dispatching the event while the root remains on the view stack.
chat.open_config_section_popup(section);
assert!(render_bottom_popup(&chat, /*width*/ 90).contains("Config: AI context"));

chat.handle_key_event(KeyEvent::from(KeyCode::Left));
let root = render_bottom_popup(&chat, /*width*/ 90);
assert!(root.contains("Choose a focused config.toml settings section."));
assert!(root.contains("› 2. AI context"), "{root}");

chat.handle_key_event(KeyEvent::from(KeyCode::Down));
chat.handle_key_event(KeyEvent::from(KeyCode::Down));
chat.handle_key_event(KeyEvent::from(KeyCode::Right));
assert_matches!(rx.try_recv(), Ok(AppEvent::OpenAgentMaxThreadsMenu));

chat.open_agent_max_threads_popup();
assert!(render_bottom_popup(&chat, /*width*/ 90).contains("Cap concurrent subagent threads"));
chat.handle_key_event(KeyEvent::from(KeyCode::Right));
assert_matches!(
rx.try_recv(),
Err(tokio::sync::mpsc::error::TryRecvError::Empty)
);
assert!(render_bottom_popup(&chat, /*width*/ 90).contains("Cap concurrent subagent threads"));

chat.handle_key_event(KeyEvent::from(KeyCode::Left));
let root = render_bottom_popup(&chat, /*width*/ 90);
assert!(root.contains("› 4. Agent subagent threads"), "{root}");

chat.handle_key_event(KeyEvent::from(KeyCode::Left));
assert!(chat.bottom_pane.no_modal_or_popup_active());

chat.open_config_popup();
chat.handle_key_event(KeyEvent::from(KeyCode::Esc));
assert!(chat.bottom_pane.no_modal_or_popup_active());
}

#[tokio::test]
async fn config_agent_max_threads_popup_selects_value() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.2")).await;
Expand All @@ -3448,6 +3503,7 @@ async fn config_agent_max_threads_popup_selects_value() {

chat.open_agent_max_threads_popup();
let popup = render_bottom_popup(&chat, /*width*/ 90);
assert_chatwidget_snapshot!("config_agent_max_threads_popup", popup);
assert!(popup.contains("Agent subagent threads"), "{popup}");
assert!(popup.contains("(default)"), "{popup}");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,78 @@ async fn reset_picker_defaults_to_cancel() {
);
}

#[tokio::test]
async fn config_usage_limit_reset_left_and_escape_return_one_level() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
set_canonical_reset_provider(&mut chat);
chat.on_rate_limit_reset_credits(Some(exact_reset_summary()));
while rx.try_recv().is_ok() {}

chat.open_config_popup();
chat.handle_key_event(KeyEvent::from(KeyCode::Right));
assert_matches!(
rx.try_recv(),
Ok(AppEvent::OpenConfigSection {
section: crate::common_config_options::CommonConfigSection::AccountAutomation,
})
);
chat.open_config_section_popup(
crate::common_config_options::CommonConfigSection::AccountAutomation,
);

chat.handle_key_event(KeyEvent::from(KeyCode::Right));
assert_matches!(rx.try_recv(), Ok(AppEvent::OpenRateLimitResetConfirm));
let generation = chat.rate_limit_reset_generation;
chat.open_rate_limit_reset_confirm();

chat.handle_key_event(KeyEvent::from(KeyCode::Left));
let account_section = render_bottom_popup(&chat, /*width*/ 90);
assert!(
account_section.contains("Config: Account & automation"),
"{account_section}"
);
assert!(!account_section.contains("Usage limit resets"));
assert_matches!(
rx.try_recv(),
Ok(AppEvent::CancelRateLimitResetCreditSelection {
generation: cancelled_generation,
}) if cancelled_generation == generation
);
assert_matches!(
rx.try_recv(),
Err(tokio::sync::mpsc::error::TryRecvError::Empty)
);

chat.handle_key_event(KeyEvent::from(KeyCode::Right));
assert_matches!(rx.try_recv(), Ok(AppEvent::OpenRateLimitResetConfirm));
chat.open_rate_limit_reset_confirm();

chat.handle_key_event(KeyEvent::from(KeyCode::Esc));
let account_section = render_bottom_popup(&chat, /*width*/ 90);
assert!(
account_section.contains("Config: Account & automation"),
"{account_section}"
);
assert!(!account_section.contains("Usage limit resets"));
assert_matches!(
rx.try_recv(),
Ok(AppEvent::CancelRateLimitResetCreditSelection {
generation: cancelled_generation,
}) if cancelled_generation == generation
);
assert_matches!(
rx.try_recv(),
Err(tokio::sync::mpsc::error::TryRecvError::Empty)
);

chat.handle_key_event(KeyEvent::from(KeyCode::Left));
let root = render_bottom_popup(&chat, /*width*/ 90);
assert!(
root.contains("Choose a focused config.toml settings section."),
"{root}"
);
}

#[tokio::test]
async fn idle_snapshots_never_consume_a_reset() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
Expand Down
3 changes: 2 additions & 1 deletion codex-rs/tui/src/chatwidget/usage_limit_reset/manual.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,9 +160,10 @@ impl ChatWidget {
selected,
reset_label(i64::try_from(selected).unwrap_or(i64::MAX))
)),
footer_hint: Some(standard_popup_hint_line()),
footer_hint: Some(Line::from("Enter selects; Left or Esc goes back")),
items,
initial_selected_idx: Some(selected),
tree_navigation_enabled: true,
on_cancel: Some(Box::new(move |tx| {
tx.send(AppEvent::CancelRateLimitResetCreditSelection { generation });
})),
Expand Down
Loading