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
38 changes: 38 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 @@ -154,6 +154,20 @@ pub(crate) struct SelectionItem {
pub disabled_reason: Option<String>,
}

impl SelectionItem {
/// Whether accepting this row drills one level deeper into the menu tree.
///
/// This is the single source of truth for tree navigation: `move_right` only fires on rows
/// that answer `true`, and footer hints only advertise a drill-in key when a menu contains
/// such a row. Toggle rows (no actions) and rows that merely close the view do not qualify.
pub(crate) fn opens_child_view(&self) -> bool {
!self.is_disabled
&& self.disabled_reason.is_none()
&& !self.actions.is_empty()
&& self.dismiss_parent_on_child_accept
}
}

/// Construction-time configuration for [`ListSelectionView`].
///
/// This config is consumed once by [`ListSelectionView::new`]. After
Expand All @@ -176,6 +190,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 @@ -231,6 +246,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 @@ -267,6 +283,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 @@ -339,6 +356,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 @@ -995,6 +1013,26 @@ 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(SelectionItem::opens_child_view);
if selected_opens_child {
self.accept();
}
}
KeyEvent {
code: KeyCode::Backspace,
..
Expand Down
84 changes: 84 additions & 0 deletions codex-rs/tui/src/bottom_pane/popup_consts.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! Shared popup-related constants for bottom pane widgets.

use ratatui::text::Line;
use ratatui::text::Span;

use crate::key_hint;
use crate::key_hint::KeyBinding;
Expand Down Expand Up @@ -32,6 +33,89 @@ pub(crate) fn standard_popup_hint_line_for_keymap(list_keymap: &ListKeymap) -> L
)
}

/// Footer hint for a menu that participates in tree navigation (see
/// `SelectionViewParams::tree_navigation_enabled`).
///
/// Key labels are resolved from the live list keymap so rebound keys stay accurate; only the
/// verbs describing what each key does at this level of the tree are supplied by the caller.
pub(crate) struct TreeNavigationHint {
/// What pressing the accept keys does on this menu's rows.
pub accept: TreeNavigationAccept,
/// Whether the menu has toggle rows driven by space.
pub include_space_toggle: bool,
/// Verb for the go-back keys, e.g. "closes" at the root or "goes back" one level down.
pub cancel_label: &'static str,
}

/// What the accept keys do on the rows of a tree-navigation menu.
///
/// Only advertise what the rows actually support: `move_right` drills in exclusively for rows
/// that open a child view, so a menu whose rows are toggles (or that has no rows with actions)
/// must not claim otherwise.
pub(crate) enum TreeNavigationAccept {
/// No row opens a child and accepting does nothing meaningful; no clause is rendered.
None,
/// Rows resolve in place (leaf picker): only `accept` is advertised, with this verb.
InPlace(&'static str),
/// Rows open a sub-menu: both `move_right` and `accept` are advertised, with this verb.
DrillIn(&'static str),
}

pub(crate) fn tree_navigation_hint_line(
list_keymap: &ListKeymap,
hint: TreeNavigationHint,
) -> Line<'static> {
let mut spans: Vec<Span<'static>> = Vec::new();

let (include_move_right, accept_label) = match hint.accept {
TreeNavigationAccept::None => (false, None),
TreeNavigationAccept::InPlace(label) => (false, Some(label)),
TreeNavigationAccept::DrillIn(label) => (true, Some(label)),
};
if let Some(accept_label) = accept_label {
let accept_keys: Vec<KeyBinding> = include_move_right
.then(|| primary_binding(&list_keymap.move_right))
.flatten()
.into_iter()
.chain(primary_binding(&list_keymap.accept))
.collect();
push_key_clause(&mut spans, &accept_keys, accept_label);
}

if hint.include_space_toggle {
push_key_clause(
&mut spans,
&[key_hint::plain(KeyCode::Char(' '))],
"toggles",
);
}

let cancel_keys: Vec<KeyBinding> = primary_binding(&list_keymap.move_left)
.into_iter()
.chain(primary_binding(&list_keymap.cancel))
.collect();
push_key_clause(&mut spans, &cancel_keys, hint.cancel_label);

Line::from(spans)
}

/// Append `<key>[ or <key>] <label>` to `spans`, separating clauses with `; `.
fn push_key_clause(spans: &mut Vec<Span<'static>>, keys: &[KeyBinding], label: &str) {
if keys.is_empty() {
return;
}
if !spans.is_empty() {
spans.push("; ".into());
}
for (idx, key) in keys.iter().enumerate() {
if idx > 0 {
spans.push(" or ".into());
}
spans.push((*key).into());
}
spans.push(format!(" {label}").into());
}

pub(crate) fn accept_cancel_hint_line(
accept: Option<KeyBinding>,
accept_label: &'static str,
Expand Down
3 changes: 3 additions & 0 deletions codex-rs/tui/src/chatwidget.rs
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,10 @@ use crate::bottom_pane::SelectionShortcutAction;
use crate::bottom_pane::SelectionToggle;
use crate::bottom_pane::SelectionViewParams;
use crate::bottom_pane::custom_prompt_view::CustomPromptView;
use crate::bottom_pane::popup_consts::TreeNavigationAccept;
use crate::bottom_pane::popup_consts::TreeNavigationHint;
use crate::bottom_pane::popup_consts::standard_popup_hint_line;
use crate::bottom_pane::popup_consts::tree_navigation_hint_line;
use crate::clipboard_paste::paste_image_to_temp_png;
use crate::collaboration_modes;
use crate::diff_render::display_path_for;
Expand Down
61 changes: 53 additions & 8 deletions codex-rs/tui/src/chatwidget/settings_popups.rs
Original file line number Diff line number Diff line change
Expand Up @@ -143,8 +143,16 @@ 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(tree_navigation_hint_line(
&self.bottom_pane.list_keymap(),
TreeNavigationHint {
accept: TreeNavigationAccept::DrillIn("opens"),
include_space_toggle: false,
cancel_label: "closes",
},
)),
items,
tree_navigation_enabled: true,
..Default::default()
});
}
Expand Down Expand Up @@ -181,7 +189,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 @@ -241,8 +250,16 @@ impl ChatWidget {

self.bottom_pane.show_selection_view(SelectionViewParams {
header: Box::new(header),
footer_hint: Some(standard_popup_hint_line()),
footer_hint: Some(tree_navigation_hint_line(
&self.bottom_pane.list_keymap(),
TreeNavigationHint {
accept: TreeNavigationAccept::InPlace("selects"),
include_space_toggle: false,
cancel_label: "goes back",
},
)),
items,
tree_navigation_enabled: true,
..Default::default()
});
}
Expand All @@ -259,7 +276,8 @@ impl ChatWidget {
actions: vec![Box::new(|tx| {
tx.send(AppEvent::OpenGoalPlanNodeObjectiveMenu);
})],
dismiss_on_select: true,
dismiss_on_select: false,
dismiss_parent_on_child_accept: true,
..Default::default()
}
}
Expand Down Expand Up @@ -311,8 +329,16 @@ impl ChatWidget {

self.bottom_pane.show_selection_view(SelectionViewParams {
header: Box::new(header),
footer_hint: Some(standard_popup_hint_line()),
footer_hint: Some(tree_navigation_hint_line(
&self.bottom_pane.list_keymap(),
TreeNavigationHint {
accept: TreeNavigationAccept::InPlace("selects"),
include_space_toggle: false,
cancel_label: "goes back",
},
)),
items,
tree_navigation_enabled: true,
..Default::default()
});
}
Expand All @@ -331,15 +357,32 @@ impl ChatWidget {
}
items.push(back_to_config_menu_item());

// Most section rows are toggles and the "Back to sections" row only closes this level, so
// the drill-in hint is only truthful when the section actually contains a row that opens a
// child view (today: the usage-limit reset entry under Account & automation).
let section_accept_hint = if items.iter().any(SelectionItem::opens_child_view) {
TreeNavigationAccept::DrillIn("opens")
} else {
TreeNavigationAccept::None
};

let mut header = ColumnRenderable::new();
header.push(Line::from(format!("Config: {}", section.label()).bold()));
header.push(Line::from(section.description().dim()));

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(tree_navigation_hint_line(
&self.bottom_pane.list_keymap(),
TreeNavigationHint {
accept: section_accept_hint,
include_space_toggle: true,
cancel_label: "goes back",
},
)),
items,
is_searchable: true,
tree_navigation_enabled: true,
search_placeholder: Some(format!("Search {} settings", section.label())),
..Default::default()
});
Expand Down Expand Up @@ -649,7 +692,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 @@ -681,7 +725,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
→ or enter opens; space toggles; ← 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; ← 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
space toggles; ← 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
space toggles; ← or esc goes back
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@ expression: popup
the built-in default).
6. Goal objective limit Max characters echoed per goal objective (now 4000).

Press enter to open; esc to close
→ or enter opens; ← or esc closes
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@ expression: self_healing_popup
error.
Back to sections Return to the config section menu.

Press space to toggle; esc to close
space toggles; ← or esc goes back
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
source: tui/src/chatwidget/tests/usage_limit_reset_tests.rs
source: tui/src/chatwidget/tests/usage_limit_reset_tests/manual.rs
expression: "normalize_snapshot_paths(render_bottom_popup(&chat, 90))"
---
Usage limit resets
Expand All @@ -8,4 +8,4 @@ expression: "normalize_snapshot_paths(render_bottom_popup(&chat, 90))"
1. Banked reset Earned reset credit. Expires in 3d 4h.
› 2. Cancel (default) (n)

Press enter to confirm or esc to go back
enter selects; ← or esc goes back
Loading
Loading