diff --git a/codex-rs/tui/src/app/agents_overview.rs b/codex-rs/tui/src/app/agents_overview.rs index 23646a33cd..ed198c926d 100644 --- a/codex-rs/tui/src/app/agents_overview.rs +++ b/codex-rs/tui/src/app/agents_overview.rs @@ -74,7 +74,6 @@ impl App { if matches!(self.app_server_target, AppServerTarget::Embedded) { let workload_identity_selected = codex_login::is_workload_identity_selected(); self.chat_widget.show_selection_view(SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, title: Some("Shared agents unavailable".to_string()), subtitle: Some( if workload_identity_selected { diff --git a/codex-rs/tui/src/app/agents_overview_actions.rs b/codex-rs/tui/src/app/agents_overview_actions.rs index 14ff91a989..dd9cbd77b3 100644 --- a/codex-rs/tui/src/app/agents_overview_actions.rs +++ b/codex-rs/tui/src/app/agents_overview_actions.rs @@ -137,7 +137,6 @@ impl App { ), }; self.chat_widget.show_selection_view(SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, header: Box::new(LifecycleHeader(vec![ title.bold().into(), description.dim().into(), @@ -308,7 +307,6 @@ impl App { ); } self.chat_widget.show_selection_view(SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, header: Box::new(LifecycleHeader(header)), items: vec![SelectionItem { name: "Back to agents".to_string(), diff --git a/codex-rs/tui/src/app/agents_overview_errors.rs b/codex-rs/tui/src/app/agents_overview_errors.rs index 554460c287..5dc356b8d1 100644 --- a/codex-rs/tui/src/app/agents_overview_errors.rs +++ b/codex-rs/tui/src/app/agents_overview_errors.rs @@ -33,7 +33,6 @@ impl App { ..Default::default() }]; self.chat_widget.show_selection_view(SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, header: Box::new(AgentsOverviewErrorHeader(vec![ Line::from("Unable to complete action".bold()), Line::from(message.clone().dim()), diff --git a/codex-rs/tui/src/app/daemon_menu.rs b/codex-rs/tui/src/app/daemon_menu.rs index 7c92ae5cbb..08062f4d3c 100644 --- a/codex-rs/tui/src/app/daemon_menu.rs +++ b/codex-rs/tui/src/app/daemon_menu.rs @@ -79,7 +79,6 @@ impl App { }) .collect(); self.chat_widget.show_selection_view(SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, header: Box::new(DaemonMenuHeader(header)), items, ..SelectionViewParams::picker() @@ -106,7 +105,6 @@ impl App { let mut header = vec![Line::from("Update daemon and exit Codex?".bold())]; header.extend(explanation.lines().map(|line| Line::from(line.to_owned()))); self.chat_widget.show_selection_view(SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, header: Box::new(DaemonMenuHeader(header)), items: vec![ SelectionItem { diff --git a/codex-rs/tui/src/app/input.rs b/codex-rs/tui/src/app/input.rs index d46a805f9a..31e358643e 100644 --- a/codex-rs/tui/src/app/input.rs +++ b/codex-rs/tui/src/app/input.rs @@ -385,7 +385,6 @@ impl App { let allow_background = running_side_thread_id.is_none() && !self.chat_widget.has_queued_follow_up_messages(); self.chat_widget.show_selection_view(SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, title: Some("Task is still running".to_string()), subtitle: Some("Choose what happens to the current task.".to_string()), footer_hint: Some(standard_popup_hint_line()), diff --git a/codex-rs/tui/src/app/session_lifecycle.rs b/codex-rs/tui/src/app/session_lifecycle.rs index 41353b4c14..403f9b79a7 100644 --- a/codex-rs/tui/src/app/session_lifecycle.rs +++ b/codex-rs/tui/src/app/session_lifecycle.rs @@ -213,7 +213,6 @@ impl App { .collect(); SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, view_id: Some(AGENT_PICKER_VIEW_ID), title: Some("Subagents".to_string()), subtitle: Some(AgentNavigationState::picker_subtitle()), diff --git a/codex-rs/tui/src/app/session_picker.rs b/codex-rs/tui/src/app/session_picker.rs index 499587b290..018a3f5ad1 100644 --- a/codex-rs/tui/src/app/session_picker.rs +++ b/codex-rs/tui/src/app/session_picker.rs @@ -144,7 +144,6 @@ impl App { .is_some() { self.chat_widget.show_selection_view(SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, title: Some("Unable to resume session".to_string()), subtitle: Some(message.clone()), items: vec![SelectionItem { diff --git a/codex-rs/tui/src/app/tests/disconnect_tests.rs b/codex-rs/tui/src/app/tests/disconnect_tests.rs index 269455babb..b1ed8d1e6b 100644 --- a/codex-rs/tui/src/app/tests/disconnect_tests.rs +++ b/codex-rs/tui/src/app/tests/disconnect_tests.rs @@ -53,7 +53,6 @@ async fn lost_mutation_reply_preserves_work_without_resubmitting() -> Result<()> let overview = app.agents_overview_view(Vec::new(), /*selected_thread_id*/ None); app.chat_widget.show_bottom_pane_view(Box::new(overview)); app.chat_widget.show_selection_view(SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, items: vec![SelectionItem { name: "Stale server action".into(), actions: vec![Box::new(|tx| tx.send(AppEvent::CodexOp(Op::Interrupt)))], diff --git a/codex-rs/tui/src/app/tests/key_chords.rs b/codex-rs/tui/src/app/tests/key_chords.rs index 75d1038f4c..04436675a5 100644 --- a/codex-rs/tui/src/app/tests/key_chords.rs +++ b/codex-rs/tui/src/app/tests/key_chords.rs @@ -496,7 +496,6 @@ async fn physical_dispatch_band_events_are_dropped() -> Result<()> { async fn physical_chords_route_list_and_mixed_request_input_modals() -> Result<()> { let (mut app, mut tui, mut app_server) = chord_app().await?; app.chat_widget.show_selection_view(SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, view_id: Some("list"), items: ["First", "Second"] .into_iter() diff --git a/codex-rs/tui/src/app/thread_goal_actions.rs b/codex-rs/tui/src/app/thread_goal_actions.rs index cb6c96e3f9..1dd2c9f3e7 100644 --- a/codex-rs/tui/src/app/thread_goal_actions.rs +++ b/codex-rs/tui/src/app/thread_goal_actions.rs @@ -317,7 +317,6 @@ impl App { }, ]; self.chat_widget.show_selection_view(SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, title: Some("Replace goal?".to_string()), subtitle: Some(format!( "New objective: {}", diff --git a/codex-rs/tui/src/bottom_pane/app_link_view.rs b/codex-rs/tui/src/bottom_pane/app_link_view.rs index 44fae4d28d..a215ff36c0 100644 --- a/codex-rs/tui/src/bottom_pane/app_link_view.rs +++ b/codex-rs/tui/src/bottom_pane/app_link_view.rs @@ -654,7 +654,6 @@ impl AppLinkView { ' ' }; GenericDisplayRow { - category_tag: None, selection_style: Some(crate::bottom_pane::selection_style()), name: format!("{prefix} {}. {label}", index + 1), ..Default::default() diff --git a/codex-rs/tui/src/bottom_pane/approval_overlay.rs b/codex-rs/tui/src/bottom_pane/approval_overlay.rs index 35036d8efe..0c676aae69 100644 --- a/codex-rs/tui/src/bottom_pane/approval_overlay.rs +++ b/codex-rs/tui/src/bottom_pane/approval_overlay.rs @@ -314,7 +314,6 @@ impl ApprovalOverlay { .collect(); let params = SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, footer_hint: Some(approval_footer_hint(request, approval_keymap, list_keymap)), items, header, diff --git a/codex-rs/tui/src/bottom_pane/approval_overlay/clipping_tests.rs b/codex-rs/tui/src/bottom_pane/approval_overlay/clipping_tests.rs index 47ea8f6713..5413d50db6 100644 --- a/codex-rs/tui/src/bottom_pane/approval_overlay/clipping_tests.rs +++ b/codex-rs/tui/src/bottom_pane/approval_overlay/clipping_tests.rs @@ -40,7 +40,7 @@ fn clipped_exec_approval_opens_the_complete_command() { keymap.approval, keymap.list, ); - for (width, height) in [(80, 14), (40, 10)] { + for (width, height) in [(80, 14), (40, 10), (40, 8), (40, 7)] { let area = Rect::new(/*x*/ 0, /*y*/ 0, width, height); let mut buffer = Buffer::empty(area); view.render(area, &mut buffer); diff --git a/codex-rs/tui/src/bottom_pane/approval_overlay/snapshots/codex_tui__bottom_pane__approval_overlay__clipping_tests__clipped_exec_approval_opens_the_complete_command.snap b/codex-rs/tui/src/bottom_pane/approval_overlay/snapshots/codex_tui__bottom_pane__approval_overlay__clipping_tests__clipped_exec_approval_opens_the_complete_command.snap index 0d91e7714d..4747b441fb 100644 --- a/codex-rs/tui/src/bottom_pane/approval_overlay/snapshots/codex_tui__bottom_pane__approval_overlay__clipping_tests__clipped_exec_approval_opens_the_complete_command.snap +++ b/codex-rs/tui/src/bottom_pane/approval_overlay/snapshots/codex_tui__bottom_pane__approval_overlay__clipping_tests__clipped_exec_approval_opens_the_complete_command.snap @@ -31,6 +31,25 @@ bound=true 40x10 Press enter to confirm or esc to cancel +bound=true 40x8 + + Would you like to run the following + [… 43 lines] ctrl+g view all +› 1. Yes, proceed (y) + 2. No, and tell Codex what to do + differently (esc) + Press enter to confirm or esc to + cancel + +bound=true 40x7 + + [… 43 lines] ctrl+g view all +› 1. Yes, proceed (y) + 2. No, and tell Codex what to do + differently (esc) + Press enter to confirm or esc to + cancel + bound=false 80x14 Would you like to run the following command? @@ -58,3 +77,22 @@ bound=false 40x10 differently (esc) Press enter to confirm or esc to cancel + +bound=false 40x8 + + Would you like to run the following + [… 43 lines] +› 1. Yes, proceed (y) + 2. No, and tell Codex what to do + differently (esc) + Press enter to confirm or esc to + cancel + +bound=false 40x7 + + [… 43 lines] +› 1. Yes, proceed (y) + 2. No, and tell Codex what to do + differently (esc) + Press enter to confirm or esc to + cancel diff --git a/codex-rs/tui/src/bottom_pane/async_questions/mod.rs b/codex-rs/tui/src/bottom_pane/async_questions/mod.rs index c3143ab9f5..063c556a3c 100644 --- a/codex-rs/tui/src/bottom_pane/async_questions/mod.rs +++ b/codex-rs/tui/src/bottom_pane/async_questions/mod.rs @@ -184,7 +184,6 @@ impl AsyncQuestions { let number = index + 1; let prefix = format!("{prefix} {number}. "); GenericDisplayRow { - category_tag: None, // Other stays an inline editor with foreground-only focus. selection_style: (index < self.options().len()) .then(super::picker_style::selection_style), diff --git a/codex-rs/tui/src/bottom_pane/experimental_features_view.rs b/codex-rs/tui/src/bottom_pane/experimental_features_view.rs index a51f04b937..63a5bd85e1 100644 --- a/codex-rs/tui/src/bottom_pane/experimental_features_view.rs +++ b/codex-rs/tui/src/bottom_pane/experimental_features_view.rs @@ -151,7 +151,6 @@ impl ExperimentalFeaturesView { let read_only = if item.writable { "" } else { " (read-only)" }; let name = format!("{prefix} [{marker}] {}{read_only}", item.name); rows.push(GenericDisplayRow { - category_tag: None, selection_style: Some(super::picker_style::selection_style()), wrap_indent: Some(6), name, diff --git a/codex-rs/tui/src/bottom_pane/feedback_view.rs b/codex-rs/tui/src/bottom_pane/feedback_view.rs index a200a5a6e0..3a4965a6a4 100644 --- a/codex-rs/tui/src/bottom_pane/feedback_view.rs +++ b/codex-rs/tui/src/bottom_pane/feedback_view.rs @@ -145,7 +145,6 @@ pub(crate) fn feedback_selection_params( app_event_tx: AppEventSender, ) -> super::SelectionViewParams { super::SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, title: Some("How was this?".to_string()), items: vec![ make_feedback_item( @@ -186,7 +185,6 @@ pub(crate) fn feedback_selection_params( /// Build the selection popup params shown when feedback is disabled. pub(crate) fn feedback_disabled_params() -> super::SelectionViewParams { super::SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, title: Some("Sending feedback is disabled".to_string()), subtitle: Some("This action is disabled by configuration.".to_string()), footer_hint: Some(standard_popup_hint_line()), @@ -310,7 +308,6 @@ pub(crate) fn feedback_upload_consent_params( } super::SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, footer_hint: Some(standard_popup_hint_line()), items: vec![ super::SelectionItem { diff --git a/codex-rs/tui/src/bottom_pane/list_selection_view.rs b/codex-rs/tui/src/bottom_pane/list_selection_view.rs index 84e73fe493..575a7ea332 100644 --- a/codex-rs/tui/src/bottom_pane/list_selection_view.rs +++ b/codex-rs/tui/src/bottom_pane/list_selection_view.rs @@ -1,5 +1,6 @@ -//! Render selectable lists while existing callers retain the legacy presentation. -//! Picker appearance opts into stable controls, full-width selection, and separate overflow rows. +//! Render selectable lists with shared picker controls and full-width focus rows. +//! Selection and scrolling use filtered row indices; actions map back to source items. +//! Controls and overflow indicators keep their own rows outside the result viewport. use crossterm::event::KeyCode; use crossterm::event::KeyEvent; @@ -19,7 +20,6 @@ use ratatui::widgets::Wrap; use super::selection_picker_layout::PickerLayoutSizes; use super::selection_picker_layout::picker_areas; -use super::selection_popup_common::render_menu_surface; use super::selection_popup_common::wrap_styled_line; use crate::app_event_sender::AppEventSender; use crate::clipboard_paste::normalize_pasted_search_query; @@ -49,7 +49,6 @@ use super::selection_popup_common::render_rows_single_line_with_col_width_mode; use super::selection_popup_common::render_rows_with_col_width_mode; pub(crate) use super::selection_row_layout::SelectionDescriptionLayout; use super::selection_tabs::SelectionTab; -use super::selection_tabs::TabAppearance; use super::selection_tabs::render_tab_bar; use super::selection_tabs::tab_bar_height; use unicode_width::UnicodeWidthStr; @@ -118,15 +117,6 @@ pub(crate) enum SelectionRowDisplay { SingleLine, } -/// Visual treatment is opt-in while existing workflows retain their current layout. -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] -pub(crate) enum SelectionAppearance { - #[default] - Legacy, - /// Full-width focus and separate overflow rows on a terminal or panel surface. - Picker, -} - /// One selectable item in the generic selection list. pub(crate) type SelectionAction = Box; @@ -197,12 +187,9 @@ pub(crate) struct SelectionItem { /// `col_width_mode` controls column width mode in selection lists: /// `AutoVisible` (default) measures only rows visible in the viewport /// `AutoAllRows` measures all rows to ensure stable column widths as the user scrolls -/// `Fixed` used a fixed 30/70 split between columns /// `row_display` controls whether rows can wrap or stay single-line with ellipsis truncation -/// `description_layout` optionally moves descriptions below labels when their -/// column would become too narrow. +/// `description_layout` optionally hides descriptions when their column would become too narrow. pub(crate) struct SelectionViewParams { - pub appearance: SelectionAppearance, pub picker_surface: PickerSurface, /// Upper row budget; compact completion menus retain the default of eight. pub max_visible_rows: usize, @@ -225,6 +212,7 @@ pub(crate) struct SelectionViewParams { /// Rendered left-column width to use for auto-sized rows. pub name_column_width: Option, pub header: Box, + /// Optional shortcut to the complete header, shown when its text is clipped. pub header_view_all_hint: Option, /// Blank rows after the header; defaults to one. Inline banners use zero. pub header_gap: u16, @@ -263,7 +251,6 @@ pub(crate) struct SelectionViewParams { impl Default for SelectionViewParams { fn default() -> Self { Self { - appearance: SelectionAppearance::default(), picker_surface: PickerSurface::default(), max_visible_rows: MAX_POPUP_ROWS, reserve_result_rows: false, @@ -304,7 +291,6 @@ impl Default for SelectionViewParams { /// visible rows and source items and for preserving selection while filters /// change. pub(crate) struct ListSelectionView { - appearance: SelectionAppearance, picker_surface: PickerSurface, max_visible_rows: usize, reserve_result_rows: bool, @@ -423,20 +409,13 @@ impl ListSelectionView { if params.title.is_some() || params.subtitle.is_some() { let title = params.title.map(|title| Line::from(title.bold())); let subtitle = params.subtitle.map(|subtitle| Line::from(subtitle.dim())); - header = match params.appearance { - SelectionAppearance::Legacy => Box::new(ColumnRenderable::with([ - header, - Box::new(title), - Box::new(subtitle), - ])), - SelectionAppearance::Picker => Box::new(ColumnRenderable::with([ - header, - Box::new( - Paragraph::new(title.into_iter().chain(subtitle).collect::>()) - .wrap(Wrap { trim: false }), - ), - ])), - }; + header = Box::new(ColumnRenderable::with([ + header, + Box::new( + Paragraph::new(title.into_iter().chain(subtitle).collect::>()) + .wrap(Wrap { trim: false }), + ), + ])); } let active_tab_idx = params.initial_tab_id.as_ref().and_then(|initial_tab_id| { params @@ -451,7 +430,6 @@ impl ListSelectionView { }; let has_initial_selected_idx = params.initial_selected_idx.is_some(); let mut s = Self { - appearance: params.appearance, picker_surface: params.picker_surface, max_visible_rows: params.max_visible_rows.max(/*other*/ 1), reserve_result_rows: params.reserve_result_rows, @@ -528,17 +506,14 @@ impl ListSelectionView { fn active_header(&self, width: u16) -> &dyn Renderable { self.active_tab_idx .and_then(|idx| self.tabs.get(idx)) - .filter(|tab| { - self.appearance == SelectionAppearance::Legacy - || tab.header.desired_height(width) > 0 - }) + .filter(|tab| tab.header.desired_height(width) > 0) .map(|tab| tab.header.as_ref()) .unwrap_or(self.header.as_ref()) } fn header_height(&self, width: u16) -> u16 { let active_height = self.active_header(width).desired_height(width); - if self.appearance == SelectionAppearance::Picker && self.reserve_result_rows { + if self.reserve_result_rows { self.tabs .iter() .map(|tab| tab.header.desired_height(width)) @@ -575,7 +550,7 @@ impl ListSelectionView { fn visible_rows(&self, len: usize) -> usize { let rendered = self.rendered_item_count.get(); - let limit = if self.appearance == SelectionAppearance::Picker && rendered > 0 { + let limit = if rendered > 0 { self.max_visible_rows.min(rendered) } else { self.max_visible_rows @@ -675,8 +650,7 @@ impl ListSelectionView { .to_string() .len(); let mut enabled_row_number = 0; - let row_selection_style = - (self.appearance == SelectionAppearance::Picker).then(selection_style); + let row_selection_style = Some(selection_style()); self.filtered_indices .iter() .enumerate() @@ -726,13 +700,13 @@ impl ListSelectionView { let wrap_indent = (description.is_none() && item.disabled_reason.is_none()) .then_some(wrap_prefix_width); GenericDisplayRow { + category_tag: None, selection_style: row_selection_style, name: name_with_marker, name_prefix_spans, display_shortcut: item.display_shortcut, match_indices: None, description, - category_tag: None, wrap_indent, is_disabled, disabled_reason: item.disabled_reason.clone(), @@ -758,9 +732,7 @@ impl ListSelectionView { }; self.active_tab_idx = Some(next_idx); self.search_query.clear(); - if self.appearance == SelectionAppearance::Picker { - self.rendered_item_count.set(/*val*/ 0); - } + self.rendered_item_count.set(/*val*/ 0); self.state.reset(); self.apply_filter(); if self.state.selected_idx.is_none() { @@ -948,20 +920,10 @@ impl ListSelectionView { self.last_selected_actual_idx.take() } - fn tab_appearance(&self) -> TabAppearance { - match self.appearance { - SelectionAppearance::Legacy => TabAppearance::Legacy, - SelectionAppearance::Picker => TabAppearance::Filled, - } - } - fn footer_hint_lines(&self, width: u16) -> Vec> { let Some(hint) = self.active_footer_hint() else { return Vec::new(); }; - if self.appearance == SelectionAppearance::Legacy { - return vec![hint.clone()]; - } let mut lines = wrap_styled_line(hint, width); if self.reserve_result_rows { let height = self @@ -981,7 +943,7 @@ impl ListSelectionView { } fn result_area_height(&self, measured_height: u16) -> u16 { - if self.appearance == SelectionAppearance::Picker && self.reserve_result_rows { + if self.reserve_result_rows { let reserved_height = self.max_visible_rows.min(usize::from(u16::MAX)) as u16; measured_height.max(reserved_height) } else { @@ -989,13 +951,6 @@ impl ListSelectionView { } } - fn rows_width(&self, total_width: u16) -> u16 { - match self.appearance { - SelectionAppearance::Legacy => total_width.saturating_sub(/*rhs*/ 2), - SelectionAppearance::Picker => total_width, - } - } - fn clear_to_terminal_bg(buf: &mut Buffer, area: Rect) { let buf_area = buf.area(); let min_x = area.x.max(buf_area.x); @@ -1315,7 +1270,7 @@ impl Renderable for ListSelectionView { // Leave the preview's right inset outside the full-width focus row. width.saturating_sub(MENU_SURFACE_HORIZONTAL_INSET / 2 + SIDE_CONTENT_GAP + side_w) } else { - self.rows_width(width) + width }; // Measure at the same width as the painted rows, including any side panel. @@ -1336,27 +1291,13 @@ impl Renderable for ListSelectionView { }; let rows_height = self.result_area_height(rows_height); - let tab_height = tab_bar_height( - &self.tabs, - self.active_tab_idx.unwrap_or(/*default*/ 0), - inner_width, - self.tab_appearance(), - ); + let tab_height = tab_bar_height(&self.tabs, inner_width); let mut height = self.header_height(inner_width); - let header_gap = if self.appearance == SelectionAppearance::Picker && height == 0 { - 0 - } else { - self.header_gap - }; - let tab_gap = u16::from( - tab_height > 0 - && (self.appearance == SelectionAppearance::Legacy || self.is_searchable), - ); + let header_gap = if height == 0 { 0 } else { self.header_gap }; + let tab_gap = u16::from(tab_height > 0 && self.is_searchable); height = height.saturating_add(tab_height + tab_gap); height = height.saturating_add(rows_height + 2 + header_gap); - if self.appearance == SelectionAppearance::Picker - && self.picker_surface == PickerSurface::Panel - { + if self.picker_surface == PickerSurface::Panel { height = height.saturating_add(/*rhs*/ 1); } if self.is_searchable { @@ -1403,62 +1344,50 @@ impl Renderable for ListSelectionView { let hint_height = hint_lines.len() as u16; let inner_width = popup_content_width(area.width); let header_height = self.header_height(inner_width); - let tab_height = tab_bar_height( - &self.tabs, - self.active_tab_idx.unwrap_or(/*default*/ 0), - inner_width, - self.tab_appearance(), - ); + let tab_height = tab_bar_height(&self.tabs, inner_width); let mut footer_rows = note_height + hint_height; - if self.appearance == SelectionAppearance::Picker { - // Wrapped hints yield before the title, controls, and selected result. - let minimum_content = u16::from(self.picker_surface == PickerSurface::Panel) - + header_height.min(/*other*/ 1) - + tab_height - + u16::from(self.is_searchable) - + 1; - footer_rows = footer_rows.min(area.height.saturating_sub(minimum_content)); - } + // Wrapped hints yield before the title, controls, and selected result. + let minimum_content = u16::from(self.picker_surface == PickerSurface::Panel) + + header_height.min(/*other*/ 1) + + tab_height + + u16::from(self.is_searchable) + + 1; + footer_rows = footer_rows.min(area.height.saturating_sub(minimum_content)); let [content_area, footer_area] = Layout::vertical([Constraint::Fill(1), Constraint::Length(footer_rows)]).areas(area); let outer_content_area = content_area; - // Paint the shared menu surface and then layout inside the returned inset. - let content_area = match self.appearance { - SelectionAppearance::Legacy => render_menu_surface(outer_content_area, buf), - SelectionAppearance::Picker => { - Self::clear_to_terminal_bg(buf, outer_content_area); - if self.picker_surface == PickerSurface::Panel { - ratatui::widgets::Block::default() - .style(crate::style::user_message_style()) - .render(outer_content_area, buf); - } - // The list owns its vertical spacer rows so hints remain adjacent - // to results even when search, tabs, or a header are present. - let area = outer_content_area.inset(Insets::vh(/*v*/ 0, /*h*/ 2)); - if self.picker_surface == PickerSurface::Panel { - Rect::new( - area.x, - area.y.saturating_add(/*rhs*/ 1), - area.width, - area.height.saturating_sub(/*rhs*/ 1), - ) - } else { - area - } - } + Self::clear_to_terminal_bg(buf, outer_content_area); + if self.picker_surface == PickerSurface::Panel { + ratatui::widgets::Block::default() + .style(crate::style::user_message_style()) + .render(outer_content_area, buf); + } + // The list owns its vertical spacer rows so hints remain adjacent + // to results even when search, tabs, or a header are present. + let content_area = outer_content_area.inset(Insets::vh(/*v*/ 0, /*h*/ 2)); + let content_area = if self.picker_surface == PickerSurface::Panel { + // At tiny heights, leave room for both the clipped header and selected row. + let top_gap = u16::from(content_area.height > 2); + Rect::new( + content_area.x, + content_area.y.saturating_add(top_gap), + content_area.width, + content_area.height.saturating_sub(top_gap), + ) + } else { + content_area }; let side_w = self.side_layout_width(inner_width); // When side-by-side is active, shrink the list to make room. - let full_rows_width = self.rows_width(outer_content_area.width); let effective_rows_width = if let Some(sw) = side_w { outer_content_area .width .saturating_sub(MENU_SURFACE_HORIZONTAL_INSET / 2 + SIDE_CONTENT_GAP + sw) } else { - full_rows_width + outer_content_area.width }; let header = self.active_header(inner_width); @@ -1486,7 +1415,6 @@ impl Renderable for ListSelectionView { } else { 0 }; - let stacked_gap = if stacked_side_h > 0 { 1 } else { 0 }; let [ header_area, @@ -1499,52 +1427,17 @@ impl Renderable for ListSelectionView { below_area, _, stacked_side_area, - ] = if self.appearance == SelectionAppearance::Picker { - picker_areas( - content_area, - PickerLayoutSizes { - header: header_height, - header_gap: self.header_gap, - tabs: tab_height, - search: u16::from(self.is_searchable), - rows: rows_height, - side: stacked_side_h, - }, - ) - } else { - let [ - header, - header_gap, - tabs, - tab_gap, - search, - rows, - side_gap, - side, - ] = Layout::vertical([ - Constraint::Max(header_height), - Constraint::Max(self.header_gap), - Constraint::Length(tab_height), - Constraint::Length(u16::from(tab_height > 0)), - Constraint::Length(if self.is_searchable { 1 } else { 0 }), - Constraint::Length(rows_height), - Constraint::Length(stacked_gap), - Constraint::Length(stacked_side_h), - ]) - .areas(content_area); - [ - header, - header_gap, - tabs, - tab_gap, - search, - Rect::default(), - rows, - Rect::default(), - side_gap, - side, - ] - }; + ] = picker_areas( + content_area, + PickerLayoutSizes { + header: header_height, + header_gap: self.header_gap, + tabs: tab_height, + search: u16::from(self.is_searchable), + rows: rows_height, + side: stacked_side_h, + }, + ); if !self.is_searchable && header_area.height < header_height { let [header_area, elision_area] = @@ -1568,7 +1461,6 @@ impl Renderable for ListSelectionView { self.active_tab_idx.unwrap_or(/*default*/ 0), tabs_area, buf, - self.tab_appearance(), ); } @@ -1589,11 +1481,7 @@ impl Renderable for ListSelectionView { // -- List rows -- if list_area.height > 0 { let render_area = Rect { - x: if rows.is_empty() && self.appearance == SelectionAppearance::Legacy { - list_area.x - } else { - list_area.x.saturating_sub(2) - }, + x: list_area.x.saturating_sub(/*rhs*/ 2), y: list_area.y, width: effective_rows_width.max(1), height: list_area.height, @@ -1619,10 +1507,7 @@ impl Renderable for ListSelectionView { ), }; self.rendered_item_count.set(rendered_rows.items); - if self.appearance == SelectionAppearance::Picker - && above_area.height > 0 - && below_area.height > 0 - { + if above_area.height > 0 && below_area.height > 0 { render_scroll_indicators( Rect::new( render_area.x, @@ -1720,14 +1605,7 @@ impl Renderable for ListSelectionView { width: hint_area.width.saturating_sub(2), height: hint_area.height, }; - match self.appearance { - SelectionAppearance::Legacy => { - hint_lines[0].clone().dim().render(hint_area, buf) - } - SelectionAppearance::Picker => { - Paragraph::new(hint_lines).dim().render(hint_area, buf); - } - } + Paragraph::new(hint_lines).dim().render(hint_area, buf); } } } @@ -2108,7 +1986,7 @@ mod tests { } #[test] - fn empty_searchable_list_aligns_message_with_search() { + fn empty_searchable_list_keeps_search_and_no_matches_visible() { let (tx_raw, _rx) = unbounded_channel::(); let tx = AppEventSender::new(tx_raw); let view = new_view( diff --git a/codex-rs/tui/src/bottom_pane/mcp_server_elicitation.rs b/codex-rs/tui/src/bottom_pane/mcp_server_elicitation.rs index 07d3a74cec..568097e214 100644 --- a/codex-rs/tui/src/bottom_pane/mcp_server_elicitation.rs +++ b/codex-rs/tui/src/bottom_pane/mcp_server_elicitation.rs @@ -921,7 +921,6 @@ impl McpServerElicitationOverlay { let prefix_label = format!("{prefix} {number}. "); let wrap_indent = UnicodeWidthStr::width(prefix_label.as_str()); GenericDisplayRow { - category_tag: None, selection_style: Some(crate::bottom_pane::selection_style()), name: format!("{prefix_label}{}", option.label), description: option.description.clone(), diff --git a/codex-rs/tui/src/bottom_pane/memories_settings_view.rs b/codex-rs/tui/src/bottom_pane/memories_settings_view.rs index 561e581147..00b5882a59 100644 --- a/codex-rs/tui/src/bottom_pane/memories_settings_view.rs +++ b/codex-rs/tui/src/bottom_pane/memories_settings_view.rs @@ -167,7 +167,6 @@ impl MemoriesSettingsView { .into_iter() .enumerate() .map(|(idx, name)| GenericDisplayRow { - category_tag: None, selection_style: Some(super::picker_style::selection_style()), wrap_indent: Some(2), name: if state.selected_idx == Some(idx) { @@ -210,7 +209,6 @@ impl MemoriesSettingsView { } => (format!("{prefix} {name}"), description), }; GenericDisplayRow { - category_tag: None, selection_style: Some(super::picker_style::selection_style()), wrap_indent: Some(if matches!(item, MemoriesMenuItem::Setting { .. }) { 6 diff --git a/codex-rs/tui/src/bottom_pane/mod.rs b/codex-rs/tui/src/bottom_pane/mod.rs index 19ce128b68..e8a8402008 100644 --- a/codex-rs/tui/src/bottom_pane/mod.rs +++ b/codex-rs/tui/src/bottom_pane/mod.rs @@ -144,7 +144,6 @@ pub(crate) use list_selection_view::ColumnWidthMode; pub(crate) use list_selection_view::ListSelectionView; pub(crate) use list_selection_view::OnSelectionChangedCallback; pub(crate) use list_selection_view::PickerSurface; -pub(crate) use list_selection_view::SelectionAppearance; pub(crate) use list_selection_view::SelectionDescriptionLayout; pub(crate) use list_selection_view::SelectionRowDisplay; pub(crate) use list_selection_view::SelectionToggle; @@ -153,6 +152,7 @@ pub(crate) use list_selection_view::SideContentWidth; pub(crate) use list_selection_view::popup_content_width; pub(crate) use list_selection_view::side_by_side_layout_widths; pub(crate) use memories_settings_view::MemoriesSettingsView; +pub(crate) use picker_style::selection_style; use slash_commands::ServiceTierCommand; mod feedback_note_view; mod feedback_view; @@ -178,11 +178,11 @@ mod pending_input_preview; mod pending_thread_approvals; mod picker_option; mod picker_presets; +mod picker_rows; pub(crate) use picker_option::picker_option_list; pub(crate) use picker_option::picker_option_row; -mod picker_rows; mod picker_style; -pub(crate) use picker_style::selection_style; +pub(crate) use picker_style::active_tab_style; pub(crate) mod popup_consts; mod scroll_state; mod selection_picker_layout; @@ -1343,12 +1343,7 @@ impl BottomPane { if params.footer_hint.is_none() || params.footer_hint.as_ref() == Some(&popup_consts::standard_popup_hint_line()) { - params.footer_hint = Some(match params.appearance { - SelectionAppearance::Picker => { - popup_consts::picker_hint_line_for_keymap(&self.keymap.list) - } - SelectionAppearance::Legacy => self.standard_popup_hint_line(), - }); + params.footer_hint = Some(popup_consts::picker_hint_line_for_keymap(&self.keymap.list)); } } @@ -1408,10 +1403,6 @@ impl BottomPane { true } - pub(crate) fn standard_popup_hint_line(&self) -> Line<'static> { - popup_consts::standard_popup_hint_line_for_keymap(&self.keymap.list) - } - pub(crate) fn replace_view_if_present( &mut self, view_id: &'static str, @@ -3371,7 +3362,6 @@ mod tests { pane.set_task_running(/*running*/ true); pane.show_selection_view(SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, title: Some("Agents".to_string()), items: vec![SelectionItem { name: "Main".to_string(), @@ -3475,7 +3465,6 @@ mod tests { keymap.list.cancel = vec![crate::key_hint::plain(KeyCode::Char('q'))]; pane.set_keymap_bindings(&keymap); pane.show_selection_view(SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, title: Some("Agents".to_string()), items: vec![SelectionItem { name: "Main".to_string(), diff --git a/codex-rs/tui/src/bottom_pane/multi_select_picker.rs b/codex-rs/tui/src/bottom_pane/multi_select_picker.rs index fba6779e79..d6fe7d58fc 100644 --- a/codex-rs/tui/src/bottom_pane/multi_select_picker.rs +++ b/codex-rs/tui/src/bottom_pane/multi_select_picker.rs @@ -319,7 +319,6 @@ impl MultiSelectPicker { let item_name = truncate_text(&item.name, ITEM_NAME_TRUNCATE_LEN); let name = format!("{prefix} [{marker}] {item_name}"); rows.push(GenericDisplayRow { - category_tag: None, name, description: item.description.clone(), selection_style: Some(picker_style::selection_style()), @@ -328,7 +327,6 @@ impl MultiSelectPicker { if item.section_break_after && visible_idx + 1 < self.filtered_indices.len() { rows.push(GenericDisplayRow { - category_tag: None, name: SECTION_BREAK_ROW.to_string(), is_disabled: true, ..Default::default() diff --git a/codex-rs/tui/src/bottom_pane/picker_option.rs b/codex-rs/tui/src/bottom_pane/picker_option.rs index b136e8eabc..620fa1eb01 100644 --- a/codex-rs/tui/src/bottom_pane/picker_option.rs +++ b/codex-rs/tui/src/bottom_pane/picker_option.rs @@ -57,7 +57,6 @@ fn numbered_row(index: usize, label: Line<'static>, is_selected: bool) -> Generi span })); GenericDisplayRow { - category_tag: None, name_prefix_spans: spans, selection_style: Some(selection_style()), wrap_indent: Some(prefix_width), diff --git a/codex-rs/tui/src/bottom_pane/picker_presets.rs b/codex-rs/tui/src/bottom_pane/picker_presets.rs index 8e10cc9ccb..26db4f5588 100644 --- a/codex-rs/tui/src/bottom_pane/picker_presets.rs +++ b/codex-rs/tui/src/bottom_pane/picker_presets.rs @@ -12,7 +12,6 @@ impl SelectionViewParams { /// Titles and subtitles wrap; standard hints follow the active list keymap. pub(crate) fn picker() -> Self { Self { - appearance: super::SelectionAppearance::Picker, picker_surface: PickerSurface::Panel, col_width_mode: ColumnWidthMode::AutoAllRows, description_layout: SelectionDescriptionLayout::HideWhenNarrow { diff --git a/codex-rs/tui/src/bottom_pane/popup_consts.rs b/codex-rs/tui/src/bottom_pane/popup_consts.rs index 876ce1f441..12baeb5a86 100644 --- a/codex-rs/tui/src/bottom_pane/popup_consts.rs +++ b/codex-rs/tui/src/bottom_pane/popup_consts.rs @@ -24,15 +24,6 @@ pub(crate) fn standard_popup_hint_line() -> Line<'static> { ]) } -pub(crate) fn standard_popup_hint_line_for_keymap(list_keymap: &ListKeymap) -> Line<'static> { - accept_cancel_hint_line( - list_keymap.primary_hint(ListAction::Accept), - "to confirm", - list_keymap.primary_hint(ListAction::Cancel), - "to go back", - ) -} - /// Compact footer for shared pickers, using only the configured list actions. pub(crate) fn picker_hint_line_for_keymap(list_keymap: &ListKeymap) -> Line<'static> { let mut spans = Vec::new(); diff --git a/codex-rs/tui/src/bottom_pane/request_user_input/mod.rs b/codex-rs/tui/src/bottom_pane/request_user_input/mod.rs index b835dd69e2..39168deb89 100644 --- a/codex-rs/tui/src/bottom_pane/request_user_input/mod.rs +++ b/codex-rs/tui/src/bottom_pane/request_user_input/mod.rs @@ -424,7 +424,6 @@ impl RequestUserInputOverlay { let prefix_label = format!("{prefix} {number}. "); let wrap_indent = UnicodeWidthStr::width(prefix_label.as_str()); GenericDisplayRow { - category_tag: None, selection_style: Some(crate::bottom_pane::selection_style()), name: format!("{prefix_label}{label}"), description: Some(opt.description.clone()), @@ -442,7 +441,6 @@ impl RequestUserInputOverlay { let prefix_label = format!("{prefix} {number}. "); let wrap_indent = UnicodeWidthStr::width(prefix_label.as_str()); rows.push(GenericDisplayRow { - category_tag: None, selection_style: Some(crate::bottom_pane::selection_style()), name: format!("{prefix_label}{OTHER_OPTION_LABEL}"), description: Some(OTHER_OPTION_DESCRIPTION.to_string()), @@ -983,7 +981,6 @@ impl RequestUserInputOverlay { let prefix = if idx == selected { '›' } else { ' ' }; let number = idx + 1; GenericDisplayRow { - category_tag: None, selection_style: Some(crate::bottom_pane::selection_style()), name: format!("{prefix} {number}. {label}"), description: Some(description.clone()), diff --git a/codex-rs/tui/src/bottom_pane/selection_picker_layout.rs b/codex-rs/tui/src/bottom_pane/selection_picker_layout.rs index 40a1a0baa8..c80fc33c5e 100644 --- a/codex-rs/tui/src/bottom_pane/selection_picker_layout.rs +++ b/codex-rs/tui/src/bottom_pane/selection_picker_layout.rs @@ -19,9 +19,14 @@ pub(super) fn picker_areas(area: Rect, sizes: PickerLayoutSizes) -> [Rect; 10] { let tabs = sizes.tabs.min(area.height); let search = sizes.search.min(area.height.saturating_sub(tabs)); let minimum_rows = sizes.rows.min(if tabs > 0 { 1 } else { 3 }); + // Keep the elision notice when it fits alongside at least one result row. + let minimum_header = u16::from( + sizes.header > 0 && area.height.saturating_sub(tabs + search) > u16::from(sizes.rows > 0), + ); let header = sizes .header - .min(area.height.saturating_sub(tabs + search + minimum_rows)); + .min(area.height.saturating_sub(tabs + search + minimum_rows)) + .max(minimum_header); let mut remaining = area.height.saturating_sub(header + tabs + search); let indicators = u16::from(remaining >= minimum_rows + 2); remaining = remaining.saturating_sub(indicators * 2); diff --git a/codex-rs/tui/src/bottom_pane/selection_picker_tests.rs b/codex-rs/tui/src/bottom_pane/selection_picker_tests.rs index 99caf8e8b5..afb130b72b 100644 --- a/codex-rs/tui/src/bottom_pane/selection_picker_tests.rs +++ b/codex-rs/tui/src/bottom_pane/selection_picker_tests.rs @@ -10,7 +10,6 @@ fn browser() -> ListSelectionView { let (tx, _rx) = unbounded_channel::(); ListSelectionView::new( SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, picker_surface: PickerSurface::Panel, max_visible_rows: 24, title: Some("Keymap".to_owned()), @@ -65,7 +64,6 @@ fn shared_menu_presentation_at_wide_and_narrow_sizes() { let (tx, _rx) = unbounded_channel(); let view = ListSelectionView::new( SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, title: Some("Choose an option".into()), subtitle: Some("A menu with descriptions and disabled choices.".into()), items: vec![ diff --git a/codex-rs/tui/src/bottom_pane/selection_tabs.rs b/codex-rs/tui/src/bottom_pane/selection_tabs.rs index b7dc8b6f37..a13b8a674f 100644 --- a/codex-rs/tui/src/bottom_pane/selection_tabs.rs +++ b/codex-rs/tui/src/bottom_pane/selection_tabs.rs @@ -1,30 +1,19 @@ -//! Tab headers for selection lists, including a single-row filled picker variant. +//! Single-row filled tab headers for selection lists. //! //! Filled tabs always retain the active tab and reserve arrows for hidden neighbors. use ratatui::buffer::Buffer; use ratatui::layout::Rect; -use ratatui::style::Styled; use ratatui::style::Stylize; use ratatui::text::Line; -use ratatui::text::Span; use ratatui::widgets::Widget; use crate::line_truncation::truncate_line_with_ellipsis_if_overflow; use crate::render::renderable::Renderable; -use crate::style::accent_style; use super::SelectionItem; use super::picker_style::active_tab_style; -const TAB_GAP_WIDTH: usize = 2; - -#[derive(Clone, Copy)] -pub(super) enum TabAppearance { - Legacy, - Filled, -} - pub(crate) struct SelectionTab { pub(crate) id: String, pub(crate) label: String, @@ -32,22 +21,8 @@ pub(crate) struct SelectionTab { pub(crate) items: Vec, } -pub(super) fn tab_bar_height( - tabs: &[SelectionTab], - active_idx: usize, - width: u16, - appearance: TabAppearance, -) -> u16 { - if tabs.is_empty() { - return 0; - } - if matches!(appearance, TabAppearance::Filled) { - return u16::from(width > 0); - } - tab_bar_lines(tabs, active_idx, width) - .len() - .try_into() - .unwrap_or(u16::MAX) +pub(super) fn tab_bar_height(tabs: &[SelectionTab], width: u16) -> u16 { + u16::from(!tabs.is_empty() && width > 0) } pub(super) fn render_tab_bar( @@ -55,31 +30,12 @@ pub(super) fn render_tab_bar( active_idx: usize, area: Rect, buf: &mut Buffer, - appearance: TabAppearance, ) { - if matches!(appearance, TabAppearance::Filled) { - let labels = tabs - .iter() - .map(|tab| tab.label.as_str()) - .collect::>(); - render_filled_tab_bar(&labels, active_idx, area, buf); - return; - } - for (offset, line) in tab_bar_lines(tabs, active_idx, area.width) - .into_iter() - .take(area.height as usize) - .enumerate() - { - line.render( - Rect { - x: area.x, - y: area.y.saturating_add(offset as u16), - width: area.width, - height: 1, - }, - buf, - ); - } + let labels = tabs + .iter() + .map(|tab| tab.label.as_str()) + .collect::>(); + render_filled_tab_bar(&labels, active_idx, area, buf); } /// Render a fixed-height tab strip and return the visible tab hit targets. @@ -151,58 +107,6 @@ pub(crate) fn render_filled_tab_bar( regions } -fn tab_bar_lines(tabs: &[SelectionTab], active_idx: usize, width: u16) -> Vec> { - if tabs.is_empty() { - return Vec::new(); - } - - let max_width = width.max(1) as usize; - let mut lines = Vec::new(); - let mut current_spans: Vec> = Vec::new(); - let mut current_width = 0usize; - - for (idx, tab) in tabs.iter().enumerate() { - let unit = tab_unit(tab.label.as_str(), idx == active_idx); - let unit_width = Line::from(unit.clone()).width(); - let gap_width = if current_spans.is_empty() { - 0 - } else { - TAB_GAP_WIDTH - }; - - if !current_spans.is_empty() && current_width + gap_width + unit_width > max_width { - lines.push(Line::from(current_spans)); - current_spans = Vec::new(); - current_width = 0; - } - - if !current_spans.is_empty() { - current_spans.push(" ".into()); - current_width += TAB_GAP_WIDTH; - } - current_width += unit_width; - current_spans.extend(unit); - } - - if !current_spans.is_empty() { - lines.push(Line::from(current_spans)); - } - lines -} - -fn tab_unit(label: &str, active: bool) -> Vec> { - if active { - let style = accent_style(); - vec![ - "[".set_style(style), - label.to_string().set_style(style), - "]".set_style(style), - ] - } else { - vec![label.to_string().dim()] - } -} - #[cfg(test)] #[path = "selection_tabs_tests.rs"] mod tests; diff --git a/codex-rs/tui/src/bottom_pane/skills_toggle_view.rs b/codex-rs/tui/src/bottom_pane/skills_toggle_view.rs index d1532d44a9..a6d6572599 100644 --- a/codex-rs/tui/src/bottom_pane/skills_toggle_view.rs +++ b/codex-rs/tui/src/bottom_pane/skills_toggle_view.rs @@ -152,7 +152,6 @@ impl SkillsToggleView { let item_name = &item.name; let name = format!("{prefix} [{marker}] {item_name}"); GenericDisplayRow { - category_tag: None, selection_style: Some(super::picker_style::selection_style()), name, description: Some(item.description.clone()), diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__list_selection_view__tests__list_selection_col_width_mode_auto_all_rows_scroll.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__list_selection_view__tests__list_selection_col_width_mode_auto_all_rows_scroll.snap index 1ef9490f8a..8d3aff6f37 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__list_selection_view__tests__list_selection_col_width_mode_auto_all_rows_scroll.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__list_selection_view__tests__list_selection_col_width_mode_auto_all_rows_scroll.snap @@ -1,11 +1,12 @@ --- source: tui/src/bottom_pane/list_selection_view.rs +assertion_line: 2903 expression: "render_before_after_scroll_snapshot(ColumnWidthMode::AutoAllRows, 96)" --- before scroll: - Debug + › 1. Item 1 desc 1 2. Item 2 desc 2 3. Item 3 desc 3 @@ -14,12 +15,12 @@ before scroll: 6. Item 6 desc 6 7. Item 7 desc 7 8. Item 8 desc 8 - +↓ after scroll: - Debug +↑ 2. Item 2 desc 2 3. Item 3 desc 3 4. Item 4 desc 4 diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__list_selection_view__tests__list_selection_col_width_mode_auto_visible_scroll.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__list_selection_view__tests__list_selection_col_width_mode_auto_visible_scroll.snap index 30bdc4bce1..ccd8d9ee46 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__list_selection_view__tests__list_selection_col_width_mode_auto_visible_scroll.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__list_selection_view__tests__list_selection_col_width_mode_auto_visible_scroll.snap @@ -1,11 +1,12 @@ --- source: tui/src/bottom_pane/list_selection_view.rs +assertion_line: 2895 expression: "render_before_after_scroll_snapshot(ColumnWidthMode::AutoVisible, 96)" --- before scroll: - Debug + › 1. Item 1 desc 1 2. Item 2 desc 2 3. Item 3 desc 3 @@ -14,12 +15,12 @@ before scroll: 6. Item 6 desc 6 7. Item 7 desc 7 8. Item 8 desc 8 - +↓ after scroll: - Debug +↑ 2. Item 2 desc 2 3. Item 3 desc 3 4. Item 4 desc 4 diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__list_selection_view__tests__list_selection_empty_searchable.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__list_selection_view__tests__list_selection_empty_searchable.snap index a69e588ec5..0ffd647d57 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__list_selection_view__tests__list_selection_empty_searchable.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__list_selection_view__tests__list_selection_empty_searchable.snap @@ -1,11 +1,12 @@ --- source: tui/src/bottom_pane/list_selection_view.rs +assertion_line: 2046 expression: rendered --- - Select a base branch Type to search branches - no matches + +no matches Press enter to confirm or esc to go back diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__list_selection_view__tests__list_selection_footer_note_wraps.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__list_selection_view__tests__list_selection_footer_note_wraps.snap index 8eb41f417f..dbb6abcc70 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__list_selection_view__tests__list_selection_footer_note_wraps.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__list_selection_view__tests__list_selection_footer_note_wraps.snap @@ -1,13 +1,15 @@ --- source: tui/src/bottom_pane/list_selection_view.rs +assertion_line: 1989 expression: "render_lines_with_width(&view, 40)" --- - Select Approval Mode -› 1. Read Only (current) Codex can - read files + +› 1. Read Only (current) Codex can read + files Note: Use /setup-default-sandbox to allow network access. - Press enter to confirm or esc to go ba + Press enter to confirm or esc to go + back diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__list_selection_view__tests__list_selection_halfwidth_sound_marks_narrow.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__list_selection_view__tests__list_selection_halfwidth_sound_marks_narrow.snap index 81badbe243..7a5d266169 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__list_selection_view__tests__list_selection_halfwidth_sound_marks_narrow.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__list_selection_view__tests__list_selection_halfwidth_sound_marks_narrow.snap @@ -1,30 +1,29 @@ --- source: tui/src/bottom_pane/list_selection_view.rs +assertion_line: 2890 expression: rendered --- width 20: - Halfwidth sound + marks -› 1. abガc - dakute - n - descri - ption + +› 1. abガc dakuten + descript + ion 2. aパc - handak - uten - descri - ption + handakut + en + descript + ion width 24: + Halfwidth sound + marks - Halfwidth sound mark › 1. abガc dakuten - descriptio - n + description 2. aパc handakuten - descriptio - n + description diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__list_selection_view__tests__list_selection_model_picker_width_80.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__list_selection_view__tests__list_selection_model_picker_width_80.snap index be81978c89..0c0351d938 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__list_selection_view__tests__list_selection_model_picker_width_80.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__list_selection_view__tests__list_selection_model_picker_width_80.snap @@ -1,13 +1,14 @@ --- source: tui/src/bottom_pane/list_selection_view.rs +assertion_line: 2825 expression: "render_lines_with_width(&view, 80)" --- - Select Model and Effort -› 1. gpt-5.1-codex (current) Optimized for Codex. Balance of reasoning - quality and coding ability. + +› 1. gpt-5.1-codex (current) Optimized for Codex. Balance of reasoning quality + and coding ability. 2. gpt-5.1-codex-mini Optimized for Codex. Cheaper, faster, but less capable. - 3. gpt-4.1-codex Legacy model. Use when you need compatibility - with older automations. + 3. gpt-4.1-codex Legacy model. Use when you need compatibility with + older automations. diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__list_selection_view__tests__list_selection_narrow_width_preserves_rows.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__list_selection_view__tests__list_selection_narrow_width_preserves_rows.snap index 3ce6a3c45f..4a7f9b99df 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__list_selection_view__tests__list_selection_narrow_width_preserves_rows.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__list_selection_view__tests__list_selection_narrow_width_preserves_rows.snap @@ -1,16 +1,11 @@ --- source: tui/src/bottom_pane/list_selection_view.rs +assertion_line: 2852 expression: "render_lines_with_width(&view, 24)" --- - Debug -› 1. Item 1 - xxxxxxxxx - x - 2. Item 2 - xxxxxxxxx - x - 3. Item 3 - xxxxxxxxx - x + +› 1. Item 1 xxxxxxxxxx + 2. Item 2 xxxxxxxxxx + 3. Item 3 xxxxxxxxxx diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__list_selection_view__tests__list_selection_spacing_with_subtitle.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__list_selection_view__tests__list_selection_spacing_with_subtitle.snap index 512f6bbca6..052039e505 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__list_selection_view__tests__list_selection_spacing_with_subtitle.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__list_selection_view__tests__list_selection_spacing_with_subtitle.snap @@ -1,11 +1,12 @@ --- source: tui/src/bottom_pane/list_selection_view.rs +assertion_line: 1887 expression: render_lines(&view) --- - Select Approval Mode Switch between Codex approval presets + › 1. Read Only (current) Codex can read files 2. Full Access Codex can edit files diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__list_selection_view__tests__list_selection_spacing_without_subtitle.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__list_selection_view__tests__list_selection_spacing_without_subtitle.snap index ddd0f90cd8..3e7bbd0d5b 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__list_selection_view__tests__list_selection_spacing_without_subtitle.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__list_selection_view__tests__list_selection_spacing_without_subtitle.snap @@ -1,10 +1,11 @@ --- source: tui/src/bottom_pane/list_selection_view.rs +assertion_line: 1878 expression: render_lines(&view) --- - Select Approval Mode + › 1. Read Only (current) Codex can read files 2. Full Access Codex can edit files diff --git a/codex-rs/tui/src/bottom_pane/tests/actionable_banner_tests.rs b/codex-rs/tui/src/bottom_pane/tests/actionable_banner_tests.rs index 470c5f2303..855220962c 100644 --- a/codex-rs/tui/src/bottom_pane/tests/actionable_banner_tests.rs +++ b/codex-rs/tui/src/bottom_pane/tests/actionable_banner_tests.rs @@ -135,15 +135,21 @@ fn partially_clipped_banner_preserves_hidden_action_shortcuts() { ); pane.set_inline_banner(Some(ActionableBanner { title: "Usage limit reached".into(), - actions: vec![SelectionItem { - name: "View usage".into(), - actions: vec![Box::new(|tx| { - tx.send(AppEvent::OpenUrlInBrowser { - url: "https://example.com/usage".into(), - }); - })], - ..Default::default() - }], + actions: vec![ + SelectionItem { + name: "Keep working".into(), + ..Default::default() + }, + SelectionItem { + name: "View usage".into(), + actions: vec![Box::new(|tx| { + tx.send(AppEvent::OpenUrlInBrowser { + url: "https://example.com/usage".into(), + }); + })], + ..Default::default() + }, + ], ..Default::default() })); let width = 70; @@ -155,9 +161,10 @@ fn partially_clipped_banner_preserves_hidden_action_shortcuts() { ); let rendered = render_snapshot(&pane, footer_only_area); + assert!(rendered.contains("Keep working")); assert!(!rendered.contains("View usage")); - pane.handle_key_event(KeyEvent::new(KeyCode::Char('1'), KeyModifiers::NONE)); - assert_eq!(pane.composer_text(), "1"); + pane.handle_key_event(KeyEvent::new(KeyCode::Char('2'), KeyModifiers::NONE)); + assert_eq!(pane.composer_text(), "2"); assert!(rx.try_recv().is_err()); } diff --git a/codex-rs/tui/src/bottom_pane/tests/picker_hint_tests.rs b/codex-rs/tui/src/bottom_pane/tests/picker_hint_tests.rs index 5e709191cd..f34e6586ab 100644 --- a/codex-rs/tui/src/bottom_pane/tests/picker_hint_tests.rs +++ b/codex-rs/tui/src/bottom_pane/tests/picker_hint_tests.rs @@ -16,7 +16,6 @@ fn popup_hints_follow_configured_bindings() { keymap.list.cancel = vec![crate::key_hint::plain(KeyCode::F(/*n*/ 2))]; pane.set_keymap_bindings(&keymap); pane.show_selection_view(SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, title: Some("Choose an action".into()), items: vec![SelectionItem { name: "Continue".into(), @@ -59,7 +58,6 @@ fn custom_and_noncancelable_popup_hints_keep_their_policy() { ), ] { let mut params = SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, allow_cancel, footer_hint: supplied, ..Default::default() diff --git a/codex-rs/tui/src/bottom_pane/user_verification.rs b/codex-rs/tui/src/bottom_pane/user_verification.rs index 8369d6663b..436613a939 100644 --- a/codex-rs/tui/src/bottom_pane/user_verification.rs +++ b/codex-rs/tui/src/bottom_pane/user_verification.rs @@ -90,7 +90,6 @@ impl UserVerificationView { .collect(); let header = prompt_header(&request); let params = SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, footer_note: Some( accept_cancel_hint_line( list_keymap.primary_hint(ListAction::Accept), diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index c04a606c88..825da453e8 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -1088,7 +1088,6 @@ impl ChatWidget { _ => return, }; self.bottom_pane.show_selection_view(SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, title: Some(format!("Enable {name}?")), subtitle: Some(format!("{label} are disabled in this TUI session.")), footer_note: (feature == Feature::MemoryTool).then(|| { diff --git a/codex-rs/tui/src/chatwidget/connectors.rs b/codex-rs/tui/src/chatwidget/connectors.rs index edc7af43fb..b21b4511ec 100644 --- a/codex-rs/tui/src/chatwidget/connectors.rs +++ b/codex-rs/tui/src/chatwidget/connectors.rs @@ -214,7 +214,6 @@ impl ChatWidget { fn connectors_loading_popup_params(&self) -> SelectionViewParams { SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, view_id: Some(CONNECTORS_SELECTION_VIEW_ID), title: Some("Apps".to_string()), subtitle: Some("Loading installed and available apps...".to_string()), @@ -230,7 +229,6 @@ impl ChatWidget { fn connectors_error_popup_params(&self) -> SelectionViewParams { SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, view_id: Some(CONNECTORS_SELECTION_VIEW_ID), title: Some("Apps".to_string()), subtitle: Some("Failed to load apps.".to_string()), @@ -344,7 +342,6 @@ impl ChatWidget { } SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, view_id: Some(CONNECTORS_SELECTION_VIEW_ID), header: Box::new(header), items, diff --git a/codex-rs/tui/src/chatwidget/copy_picker.rs b/codex-rs/tui/src/chatwidget/copy_picker.rs index 762906a99e..538688e43c 100644 --- a/codex-rs/tui/src/chatwidget/copy_picker.rs +++ b/codex-rs/tui/src/chatwidget/copy_picker.rs @@ -97,7 +97,6 @@ impl ChatWidget { .collect(); self.show_selection_view(SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, title: Some("Copy to clipboard".into()), items, ..SelectionViewParams::picker() diff --git a/codex-rs/tui/src/chatwidget/goal_menu.rs b/codex-rs/tui/src/chatwidget/goal_menu.rs index b84cb06473..151ac79a14 100644 --- a/codex-rs/tui/src/chatwidget/goal_menu.rs +++ b/codex-rs/tui/src/chatwidget/goal_menu.rs @@ -48,7 +48,6 @@ impl ChatWidget { }); })]; self.show_selection_view(SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, title: Some("Resume paused goal?".to_string()), subtitle: Some(format!("Goal: {objective}")), footer_hint: Some(standard_popup_hint_line()), diff --git a/codex-rs/tui/src/chatwidget/luna_reserve_model.rs b/codex-rs/tui/src/chatwidget/luna_reserve_model.rs index 3ebd839e8d..e1c98e36a2 100644 --- a/codex-rs/tui/src/chatwidget/luna_reserve_model.rs +++ b/codex-rs/tui/src/chatwidget/luna_reserve_model.rs @@ -47,7 +47,6 @@ impl ChatWidget { self.show_model_selection_view( vec![LUNA_RESERVE_MODEL.to_string()], SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, view_id: Some(view_id), header, footer_hint: Some(picker_hint_line_for_keymap(&self.bottom_pane.list_keymap())), diff --git a/codex-rs/tui/src/chatwidget/misalignment_policy.rs b/codex-rs/tui/src/chatwidget/misalignment_policy.rs index 7191b085f4..eb7283fa19 100644 --- a/codex-rs/tui/src/chatwidget/misalignment_policy.rs +++ b/codex-rs/tui/src/chatwidget/misalignment_policy.rs @@ -166,7 +166,6 @@ impl ChatWidget { ); } self.bottom_pane.show_selection_view(SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, view_id: Some(PRECAUTION_VIEW), header: Box::new( Paragraph::new(vec![ @@ -204,7 +203,6 @@ impl ChatWidget { let can_continue = review.continuation_message().is_some(); self.bottom_pane.dismiss_view_by_id(PRECAUTION_VIEW); self.bottom_pane.show_selection_view(SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, view_id: Some(PRECAUTION_VIEW), title: Some("Chat paused as a precaution".to_string()), items: vec![ diff --git a/codex-rs/tui/src/chatwidget/model_popups.rs b/codex-rs/tui/src/chatwidget/model_popups.rs index 66cd36942d..7e2df92ae9 100644 --- a/codex-rs/tui/src/chatwidget/model_popups.rs +++ b/codex-rs/tui/src/chatwidget/model_popups.rs @@ -190,7 +190,6 @@ impl ChatWidget { self.show_model_selection_view( model_ids, SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, view_id: Some(MODEL_SELECTION_VIEW_ID), items, header, @@ -283,7 +282,6 @@ impl ChatWidget { self.show_model_selection_view( model_ids, SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, view_id: Some(view_id), items, header, @@ -450,7 +448,6 @@ impl ChatWidget { })]; self.bottom_pane.show_selection_view(SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, title: Some(PLAN_MODE_REASONING_SCOPE_TITLE.to_string()), subtitle: Some(subtitle), items: vec![ @@ -644,7 +641,6 @@ impl ChatWidget { .wrap(Wrap { trim: false }); self.bottom_pane.show_selection_view(SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, header: Box::new(header), items, initial_selected_idx, @@ -712,7 +708,6 @@ impl ChatWidget { ]) .wrap(Wrap { trim: false }); self.bottom_pane.show_selection_view(SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, header: Box::new(header), items, ..SelectionViewParams::picker() diff --git a/codex-rs/tui/src/chatwidget/permission_discovery.rs b/codex-rs/tui/src/chatwidget/permission_discovery.rs index 52ef53445f..09f7f418d5 100644 --- a/codex-rs/tui/src/chatwidget/permission_discovery.rs +++ b/codex-rs/tui/src/chatwidget/permission_discovery.rs @@ -18,7 +18,6 @@ impl ChatWidget { let request_id = uuid::Uuid::new_v4(); self.permission_popup_request_id = Some(request_id); self.bottom_pane.show_selection_view(SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, view_id: Some(VIEW_ID), title: Some("Update Model Permissions".into()), items: vec![SelectionItem { @@ -65,7 +64,6 @@ impl ChatWidget { self.open_permission_profiles_popup(discovery); } Err(message) => self.bottom_pane.show_selection_view(SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, view_id: Some(VIEW_ID), title: Some("Update Model Permissions".to_string()), subtitle: Some(message), diff --git a/codex-rs/tui/src/chatwidget/permission_popups.rs b/codex-rs/tui/src/chatwidget/permission_popups.rs index e2caea2cf7..b4798efe9c 100644 --- a/codex-rs/tui/src/chatwidget/permission_popups.rs +++ b/codex-rs/tui/src/chatwidget/permission_popups.rs @@ -173,7 +173,6 @@ impl ChatWidget { }); self.bottom_pane.show_selection_view(SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, footer_note, items, title: Some("Update Model Permissions".into()), @@ -230,7 +229,6 @@ impl ChatWidget { ); self.bottom_pane.show_selection_view(SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, header: Box::new( Paragraph::new(vec![ Line::from("Auto-review Denials".bold()), @@ -512,7 +510,6 @@ impl ChatWidget { ]; self.bottom_pane.show_selection_view(SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, items, header: Box::new(header), ..SelectionViewParams::picker() diff --git a/codex-rs/tui/src/chatwidget/permissions_menu.rs b/codex-rs/tui/src/chatwidget/permissions_menu.rs index 4c497144b4..da80735a76 100644 --- a/codex-rs/tui/src/chatwidget/permissions_menu.rs +++ b/codex-rs/tui/src/chatwidget/permissions_menu.rs @@ -163,7 +163,6 @@ impl ChatWidget { )); } self.bottom_pane.show_selection_view(SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, view_id: Some(super::permission_discovery::VIEW_ID), items, title: Some("Update Model Permissions".to_string()), diff --git a/codex-rs/tui/src/chatwidget/pets.rs b/codex-rs/tui/src/chatwidget/pets.rs index 92cfc2af3f..0d688fde9a 100644 --- a/codex-rs/tui/src/chatwidget/pets.rs +++ b/codex-rs/tui/src/chatwidget/pets.rs @@ -294,7 +294,6 @@ impl ChatWidget { self.pet_picker_preview_state.clear(); self.pet_picker_preview_pet = None; self.bottom_pane.show_selection_view(SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, view_id: Some(PET_SELECTION_LOADING_VIEW_ID), title: Some("Loading Pet".to_string()), subtitle: Some("Preparing the terminal pet.".to_string()), diff --git a/codex-rs/tui/src/chatwidget/plan_implementation.rs b/codex-rs/tui/src/chatwidget/plan_implementation.rs index 6698416972..4354de95ed 100644 --- a/codex-rs/tui/src/chatwidget/plan_implementation.rs +++ b/codex-rs/tui/src/chatwidget/plan_implementation.rs @@ -75,7 +75,6 @@ pub(super) fn selection_view_params( ); SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, title: Some(PLAN_IMPLEMENTATION_TITLE.to_string()), subtitle: None, footer_hint: Some(standard_popup_hint_line()), diff --git a/codex-rs/tui/src/chatwidget/plugin_catalog.rs b/codex-rs/tui/src/chatwidget/plugin_catalog.rs index 8a233c80ff..91e7844538 100644 --- a/codex-rs/tui/src/chatwidget/plugin_catalog.rs +++ b/codex-rs/tui/src/chatwidget/plugin_catalog.rs @@ -340,7 +340,6 @@ impl Renderable for PluginDisclosureLine { impl ChatWidget { pub(super) fn plugins_loading_popup_params(&self) -> SelectionViewParams { SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, view_id: Some(PLUGINS_SELECTION_VIEW_ID), header: Box::new(DelayedLoadingHeader::new( self.frame_requester.clone(), @@ -360,7 +359,6 @@ impl ChatWidget { pub(super) fn marketplace_add_loading_popup_params(&self) -> SelectionViewParams { SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, view_id: Some(PLUGINS_SELECTION_VIEW_ID), header: Box::new(DelayedLoadingHeader::new( self.frame_requester.clone(), @@ -400,7 +398,6 @@ impl ChatWidget { let plugins_response_for_on_cancel = plugins_response.clone(); SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, view_id: Some(PLUGINS_SELECTION_VIEW_ID), header: Box::new(header), footer_hint: Some(Line::from(vec![ @@ -459,7 +456,6 @@ impl ChatWidget { marketplace_display_name: &str, ) -> SelectionViewParams { SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, view_id: Some(PLUGINS_SELECTION_VIEW_ID), title: Some("Plugins".to_string()), subtitle: Some(format!("Removing {marketplace_display_name}...")), @@ -481,7 +477,6 @@ impl ChatWidget { .map(|name| format!("Upgrading {name} marketplace...")) .unwrap_or_else(|| "Upgrading marketplaces...".to_string()); SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, view_id: Some(PLUGINS_SELECTION_VIEW_ID), header: Box::new(DelayedLoadingHeader::new( self.frame_requester.clone(), @@ -504,7 +499,6 @@ impl ChatWidget { plugin_display_name: &str, ) -> SelectionViewParams { SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, view_id: Some(PLUGINS_SELECTION_VIEW_ID), header: Box::new(DelayedLoadingHeader::new( self.frame_requester.clone(), @@ -527,7 +521,6 @@ impl ChatWidget { plugin_display_name: &str, ) -> SelectionViewParams { SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, view_id: Some(PLUGINS_SELECTION_VIEW_ID), title: Some("Plugins".to_string()), subtitle: Some(format!("Installing {plugin_display_name}...")), @@ -546,7 +539,6 @@ impl ChatWidget { plugin_display_name: &str, ) -> SelectionViewParams { SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, view_id: Some(PLUGINS_SELECTION_VIEW_ID), title: Some("Plugins".to_string()), subtitle: Some(format!("Uninstalling {plugin_display_name}...")), @@ -562,7 +554,6 @@ impl ChatWidget { pub(super) fn plugins_error_popup_params(&self, err: &str) -> SelectionViewParams { SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, view_id: Some(PLUGINS_SELECTION_VIEW_ID), title: Some("Plugins".to_string()), subtitle: Some("Failed to load plugins.".to_string()), @@ -614,7 +605,6 @@ impl ChatWidget { } SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, view_id: Some(PLUGINS_SELECTION_VIEW_ID), title: Some("Plugins".to_string()), subtitle: Some("Failed to add marketplace.".to_string()), @@ -669,7 +659,6 @@ impl ChatWidget { } SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, view_id: Some(PLUGINS_SELECTION_VIEW_ID), title: Some("Plugins".to_string()), subtitle: Some("Failed to remove marketplace.".to_string()), @@ -707,7 +696,6 @@ impl ChatWidget { } SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, view_id: Some(PLUGINS_SELECTION_VIEW_ID), title: Some("Plugins".to_string()), subtitle: Some("Failed to load plugin details.".to_string()), @@ -934,7 +922,6 @@ impl ChatWidget { active_tab_id.and_then(|tab_id| plugin_tab_id_matching_saved_id(&tab_id, &tabs)); SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, view_id: Some(PLUGINS_SELECTION_VIEW_ID), header: Box::new(()), footer_hint: Some(plugins_popup_hint_line( @@ -1151,7 +1138,6 @@ impl ChatWidget { }); SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, view_id: Some(PLUGINS_SELECTION_VIEW_ID), header: Box::new(header), footer_hint: Some(plugin_detail_hint_line()), diff --git a/codex-rs/tui/src/chatwidget/plugins.rs b/codex-rs/tui/src/chatwidget/plugins.rs index dcb4bc1066..5462f9f58e 100644 --- a/codex-rs/tui/src/chatwidget/plugins.rs +++ b/codex-rs/tui/src/chatwidget/plugins.rs @@ -955,7 +955,6 @@ impl ChatWidget { }); Some(SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, view_id: Some(PLUGINS_SELECTION_VIEW_ID), header: Box::new(header), footer_hint: Some(plugin_detail_hint_line()), diff --git a/codex-rs/tui/src/chatwidget/rate_limits.rs b/codex-rs/tui/src/chatwidget/rate_limits.rs index 9542c5cfa2..05eca3ce4e 100644 --- a/codex-rs/tui/src/chatwidget/rate_limits.rs +++ b/codex-rs/tui/src/chatwidget/rate_limits.rs @@ -507,7 +507,6 @@ impl ChatWidget { ]; self.bottom_pane.show_selection_view(SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, view_id: Some(RATE_LIMIT_SWITCH_PROMPT_VIEW_ID), title: Some("Approaching rate limits".to_string()), subtitle: Some(format!("Switch to {switch_model} for lower credit usage?")), diff --git a/codex-rs/tui/src/chatwidget/realtime_settings.rs b/codex-rs/tui/src/chatwidget/realtime_settings.rs index 66e83f706c..3057764af5 100644 --- a/codex-rs/tui/src/chatwidget/realtime_settings.rs +++ b/codex-rs/tui/src/chatwidget/realtime_settings.rs @@ -25,7 +25,6 @@ impl ChatWidget { }) .collect(); self.bottom_pane.show_selection_view(SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, title: Some("Select voice".to_string()), subtitle: Some("Applies to your next voice conversation.".to_string()), footer_hint: Some(standard_popup_hint_line()), diff --git a/codex-rs/tui/src/chatwidget/review_popups.rs b/codex-rs/tui/src/chatwidget/review_popups.rs index 00ea85b854..8adc58ae13 100644 --- a/codex-rs/tui/src/chatwidget/review_popups.rs +++ b/codex-rs/tui/src/chatwidget/review_popups.rs @@ -62,7 +62,6 @@ impl ChatWidget { }); self.bottom_pane.show_selection_view(SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, title: Some("Select a review preset".into()), items, ..SelectionViewParams::picker() @@ -92,7 +91,6 @@ impl ChatWidget { } self.bottom_pane.show_selection_view(SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, picker_surface: PickerSurface::Panel, title: Some("Select a base branch".to_string()), subtitle: Some(format!("Current branch: {current_branch}")), @@ -130,7 +128,6 @@ impl ChatWidget { } self.bottom_pane.show_selection_view(SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, picker_surface: PickerSurface::Panel, title: Some("Select a commit to review".into()), items, diff --git a/codex-rs/tui/src/chatwidget/safety_buffering.rs b/codex-rs/tui/src/chatwidget/safety_buffering.rs index 18488307ef..dacfbc5db0 100644 --- a/codex-rs/tui/src/chatwidget/safety_buffering.rs +++ b/codex-rs/tui/src/chatwidget/safety_buffering.rs @@ -227,7 +227,6 @@ impl ChatWidget { }, ]); self.bottom_pane.show_selection_view(SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, view_id: Some(SAFETY_BUFFERING_PROMPT_VIEW_ID), header: Box::new(SafetyBufferingHeader(header)), footer_note: Some(Line::from(SAFETY_BUFFERING_FOOTER).dim()), @@ -259,7 +258,6 @@ impl ChatWidget { self.bottom_pane .dismiss_view_by_id(SAFETY_BUFFERING_PROMPT_VIEW_ID); self.bottom_pane.show_selection_view(SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, view_id: Some(SAFETY_BUFFERING_PROMPT_VIEW_ID), header: Box::new(SafetyBufferingHeader(vec![ "Stop this attempt and retry?".bold().into(), diff --git a/codex-rs/tui/src/chatwidget/skills.rs b/codex-rs/tui/src/chatwidget/skills.rs index e59bcf07ee..680675bcd9 100644 --- a/codex-rs/tui/src/chatwidget/skills.rs +++ b/codex-rs/tui/src/chatwidget/skills.rs @@ -49,7 +49,6 @@ impl ChatWidget { ]; self.bottom_pane.show_selection_view(SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, title: Some("Skills".to_string()), subtitle: Some("Choose an action".to_string()), footer_hint: Some(standard_popup_hint_line()), diff --git a/codex-rs/tui/src/chatwidget/slash_dispatch.rs b/codex-rs/tui/src/chatwidget/slash_dispatch.rs index 222a5a0f56..a07662d0db 100644 --- a/codex-rs/tui/src/chatwidget/slash_dispatch.rs +++ b/codex-rs/tui/src/chatwidget/slash_dispatch.rs @@ -206,7 +206,6 @@ impl ChatWidget { } SlashCommand::Archive => { self.bottom_pane.show_selection_view(SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, title: Some("Archive this session?".to_string()), subtitle: Some( "Are you sure? This will archive the current session".to_string(), @@ -235,7 +234,6 @@ impl ChatWidget { } SlashCommand::Delete => { self.bottom_pane.show_selection_view(SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, title: Some("Delete this session?".to_string()), subtitle: Some( "Cannot be undone. Subagent threads will also be deleted.".to_string(), diff --git a/codex-rs/tui/src/chatwidget/tests/permissions.rs b/codex-rs/tui/src/chatwidget/tests/permissions.rs index a79f9d89b7..c0d90956a5 100644 --- a/codex-rs/tui/src/chatwidget/tests/permissions.rs +++ b/codex-rs/tui/src/chatwidget/tests/permissions.rs @@ -114,7 +114,6 @@ async fn permission_discovery_discards_stale_results_and_preserves_covering_moda chat.open_permissions_popup(); let request_id = chat.permission_popup_request_id.unwrap(); chat.bottom_pane.show_selection_view(SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, view_id: Some("other-modal"), title: Some("Other modal".to_string()), ..Default::default() diff --git a/codex-rs/tui/src/chatwidget/tests/usage.rs b/codex-rs/tui/src/chatwidget/tests/usage.rs index 703d61b174..6bea412c31 100644 --- a/codex-rs/tui/src/chatwidget/tests/usage.rs +++ b/codex-rs/tui/src/chatwidget/tests/usage.rs @@ -1110,7 +1110,6 @@ fn dismiss_popup(chat: &mut ChatWidget) { fn show_usage_test_overlay(chat: &mut ChatWidget) { chat.bottom_pane.show_selection_view(SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, view_id: Some(TEST_OVERLAY_VIEW_ID), title: Some("Covering overlay".to_string()), items: vec![SelectionItem { diff --git a/codex-rs/tui/src/chatwidget/transcript_export.rs b/codex-rs/tui/src/chatwidget/transcript_export.rs index 452f0cb412..e99e9795bc 100644 --- a/codex-rs/tui/src/chatwidget/transcript_export.rs +++ b/codex-rs/tui/src/chatwidget/transcript_export.rs @@ -25,7 +25,6 @@ impl ChatWidget { pub(super) fn show_transcript_export_popup(&mut self) { self.show_selection_view(SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, header: Box::new( Paragraph::new(vec![ Line::from("Export conversation".bold()), diff --git a/codex-rs/tui/src/chatwidget/usage.rs b/codex-rs/tui/src/chatwidget/usage.rs index 8563a3bdf3..a45aee4a88 100644 --- a/codex-rs/tui/src/chatwidget/usage.rs +++ b/codex-rs/tui/src/chatwidget/usage.rs @@ -47,7 +47,6 @@ impl ChatWidget { }; SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, view_id: Some(USAGE_MENU_VIEW_ID), title: Some("Usage".to_string()), subtitle: Some("View account usage or redeem an earned reset.".to_string()), @@ -109,7 +108,6 @@ impl ChatWidget { let request_id = self.take_next_rate_limit_reset_request_id(); self.pending_rate_limit_reset_request_id = Some(request_id); self.bottom_pane.show_selection_view(SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, view_id: Some(RATE_LIMIT_RESET_VIEW_ID), title: Some("Usage limit resets".to_string()), subtitle: Some("Checking your available resets...".to_string()), @@ -209,7 +207,6 @@ impl ChatWidget { ..Default::default() }); SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, view_id: Some(RATE_LIMIT_RESET_VIEW_ID), title: Some("Usage limit resets".to_string()), subtitle: Some(format!( @@ -250,7 +247,6 @@ impl ChatWidget { |reset_detail| format!("{reset_title} · {reset_detail}"), ); self.bottom_pane.show_selection_view(SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, view_id: Some(RATE_LIMIT_RESET_CONFIRMATION_VIEW_ID), title: Some("Use this reset?".to_string()), subtitle: Some(subtitle), @@ -299,7 +295,6 @@ impl ChatWidget { fn rate_limit_reset_message_params(message: &str) -> SelectionViewParams { SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, view_id: Some(RATE_LIMIT_RESET_VIEW_ID), title: Some("Usage limit resets".to_string()), subtitle: Some(message.to_string()), @@ -314,7 +309,6 @@ impl ChatWidget { fn reset_refresh_params(message: &str) -> SelectionViewParams { SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, view_id: Some(RATE_LIMIT_RESET_VIEW_ID), title: Some("Usage limit resets".to_string()), subtitle: Some(message.to_string()), @@ -348,7 +342,6 @@ impl ChatWidget { self.bottom_pane .dismiss_view_by_id(RATE_LIMIT_RESET_VIEW_ID); self.bottom_pane.show_selection_view(SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, view_id: Some(RATE_LIMIT_RESET_VIEW_ID), title: Some("Usage limit resets".to_string()), subtitle: Some("Resetting your usage...".to_string()), @@ -414,7 +407,6 @@ impl ChatWidget { self.pending_rate_limit_reset_request_id = None; self.pending_rate_limit_reset_idempotency_key = Some(idempotency_key.clone()); self.replace_rate_limit_reset_popup(SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, view_id: Some(RATE_LIMIT_RESET_VIEW_ID), title: Some("Usage limit resets".to_string()), subtitle: Some("Couldn't reset usage. Please try again.".to_string()), @@ -474,7 +466,6 @@ impl ChatWidget { fn rate_limit_reset_success_loading_params() -> SelectionViewParams { SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, view_id: Some(RATE_LIMIT_RESET_VIEW_ID), title: Some("Usage limit resets".to_string()), subtitle: Some("Usage reset. Checking your remaining resets...".to_string()), diff --git a/codex-rs/tui/src/chatwidget/windows_sandbox_prompts.rs b/codex-rs/tui/src/chatwidget/windows_sandbox_prompts.rs index 6a48824bb8..bb3d63988b 100644 --- a/codex-rs/tui/src/chatwidget/windows_sandbox_prompts.rs +++ b/codex-rs/tui/src/chatwidget/windows_sandbox_prompts.rs @@ -114,7 +114,6 @@ impl ChatWidget { }); self.bottom_pane.show_selection_view(SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, title: None, footer_hint: Some(standard_popup_hint_line()), items, @@ -249,7 +248,6 @@ impl ChatWidget { }); self.bottom_pane.show_selection_view(SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, title: None, footer_hint: Some(standard_popup_hint_line()), items, diff --git a/codex-rs/tui/src/chatwidget/worktree_picker.rs b/codex-rs/tui/src/chatwidget/worktree_picker.rs index fa4b456a03..cdff579b36 100644 --- a/codex-rs/tui/src/chatwidget/worktree_picker.rs +++ b/codex-rs/tui/src/chatwidget/worktree_picker.rs @@ -43,7 +43,6 @@ impl ChatWidget { }; let current_name = name.clone(); self.bottom_pane.show_selection_view(SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, title: Some(title.into()), items: vec![ SelectionItem { @@ -95,7 +94,6 @@ impl ChatWidget { } self.bottom_pane.show_selection_view(SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, title: Some("Worktrees".into()), items: vec![ SelectionItem { @@ -149,7 +147,6 @@ impl ChatWidget { self.bottom_pane.dismiss_view_by_id(BROWSER_VIEW_ID); self.worktree_popup_request_id = Some(request.id); self.bottom_pane.show_selection_view(SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, view_id: Some(BROWSER_VIEW_ID), picker_surface: PickerSurface::Panel, title: Some("Managed worktrees".into()), @@ -194,7 +191,6 @@ impl ChatWidget { } }; self.bottom_pane.show_selection_view(SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, title: Some("Managed worktrees".to_string()), subtitle: Some( if entries.is_empty() { @@ -344,7 +340,6 @@ impl ChatWidget { Owner::None | Owner::Unavailable(_) => "Worktree".to_string(), }; self.bottom_pane.show_selection_view(SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, title: Some(title), subtitle: Some(entry.cwd.display().to_string()), items, @@ -357,7 +352,6 @@ impl ChatWidget { return; } self.bottom_pane.show_selection_view(SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, title: Some("Delete this worktree?".to_string()), subtitle: Some(root.display().to_string()), items: vec![ diff --git a/codex-rs/tui/src/keymap_setup.rs b/codex-rs/tui/src/keymap_setup.rs index 4ed00954ed..adc4e946fd 100644 --- a/codex-rs/tui/src/keymap_setup.rs +++ b/codex-rs/tui/src/keymap_setup.rs @@ -297,7 +297,6 @@ pub(crate) fn build_keymap_action_menu_params( }); SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, view_id: Some(KEYMAP_ACTION_MENU_VIEW_ID), header: Box::new(Paragraph::new(header).wrap(Wrap { trim: false })), footer_note: Some(Line::from(vec![ @@ -363,7 +362,6 @@ pub(crate) fn build_keymap_replace_binding_menu_params( .collect(); SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, view_id: Some(KEYMAP_REPLACE_BINDING_MENU_VIEW_ID), header: Box::new(Paragraph::new(header).wrap(Wrap { trim: false })), footer_hint: Some(picker_hint_line_for_keymap(&runtime_keymap.list)), @@ -386,7 +384,6 @@ pub(crate) fn build_keymap_conflict_params( KeymapCaptureMode::SingleKey }; SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, header: Box::new( Paragraph::new(vec![ Line::from("Shortcut Conflict".bold()), @@ -722,11 +719,6 @@ mod tests { use tokio::sync::mpsc::UnboundedReceiver; use tokio::sync::mpsc::unbounded_channel; - // Keep layout fixtures portable while runtime defaults remain platform-specific. - fn snapshot_runtime(runtime: RuntimeKeymap) -> RuntimeKeymap { - runtime - } - fn app_event_sender() -> AppEventSender { let (tx, _rx) = unbounded_channel(); AppEventSender::new(tx) @@ -901,7 +893,7 @@ mod tests { #[test] fn keymap_picker_fast_mode_enabled_snapshot() { - let runtime = snapshot_runtime(RuntimeKeymap::defaults()); + let runtime = RuntimeKeymap::defaults(); let params = build_keymap_picker_params_with_filter( &runtime, &TuiKeymap::default(), @@ -995,7 +987,7 @@ mod tests { #[test] fn picker_content_snapshot() { - let runtime = snapshot_runtime(RuntimeKeymap::defaults()); + let runtime = RuntimeKeymap::defaults(); let params = build_keymap_picker_params(&runtime, &TuiKeymap::default()); let all_tab = selection_tab(¶ms, KEYMAP_ALL_TAB_ID); let snapshot = params @@ -1190,7 +1182,7 @@ mod tests { #[test] fn picker_question_actions_snapshot() { - let runtime = snapshot_runtime(RuntimeKeymap::defaults()); + let runtime = RuntimeKeymap::defaults(); let params = build_keymap_picker_params_for_selected_action( &runtime, &TuiKeymap::default(), @@ -1223,7 +1215,7 @@ mod tests { #[test] fn picker_all_tab_items_remain_searchable() { - let runtime = snapshot_runtime(RuntimeKeymap::defaults()); + let runtime = RuntimeKeymap::defaults(); let params = build_keymap_picker_params(&runtime, &TuiKeymap::default()); let all_tab = selection_tab(¶ms, KEYMAP_ALL_TAB_ID); let snapshot = all_tab @@ -1246,7 +1238,7 @@ mod tests { #[test] fn picker_wide_render_snapshot() { - let runtime = snapshot_runtime(RuntimeKeymap::defaults()); + let runtime = RuntimeKeymap::defaults(); let params = build_keymap_picker_params(&runtime, &TuiKeymap::default()); assert_snapshot!("keymap_picker_wide", render_picker(params, /*width*/ 120)); @@ -1254,7 +1246,7 @@ mod tests { #[test] fn picker_narrow_render_snapshot() { - let runtime = snapshot_runtime(RuntimeKeymap::defaults()); + let runtime = RuntimeKeymap::defaults(); let params = build_keymap_picker_params(&runtime, &TuiKeymap::default()); assert_snapshot!("keymap_picker_narrow", render_picker(params, /*width*/ 78)); @@ -1265,8 +1257,7 @@ mod tests { let keymap = keymap_with_replacement(&TuiKeymap::default(), "composer", "submit", "ctrl-enter") .expect("replace binding"); - let runtime = - snapshot_runtime(RuntimeKeymap::from_config(&keymap).expect("runtime keymap")); + let runtime = RuntimeKeymap::from_config(&keymap).expect("runtime keymap"); let params = build_keymap_picker_params(&runtime, &keymap); assert_snapshot!("keymap_picker_custom", render_picker(params, /*width*/ 120)); @@ -1281,8 +1272,7 @@ mod tests { &["ctrl-t".to_string(), "ctrl-x ctrl-t".to_string()], ) .expect("global chord binding"); - let runtime = - snapshot_runtime(RuntimeKeymap::from_config(&keymap).expect("runtime keymap")); + let runtime = RuntimeKeymap::from_config(&keymap).expect("runtime keymap"); let params = build_keymap_picker_params(&runtime, &keymap); assert_snapshot!( diff --git a/codex-rs/tui/src/keymap_setup/picker.rs b/codex-rs/tui/src/keymap_setup/picker.rs index 623a86157e..6ad29c5ff6 100644 --- a/codex-rs/tui/src/keymap_setup/picker.rs +++ b/codex-rs/tui/src/keymap_setup/picker.rs @@ -297,7 +297,6 @@ fn build_keymap_picker_params_for_action( tabs.push(keymap_debug_tab()); SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, view_id: Some(KEYMAP_PICKER_VIEW_ID), picker_surface: PickerSurface::Panel, max_visible_rows: 24, diff --git a/codex-rs/tui/src/pets/picker.rs b/codex-rs/tui/src/pets/picker.rs index a5c1490bab..72251d7776 100644 --- a/codex-rs/tui/src/pets/picker.rs +++ b/codex-rs/tui/src/pets/picker.rs @@ -117,7 +117,6 @@ pub(crate) fn build_pet_picker_params( .collect(); SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, view_id: Some(PET_PICKER_VIEW_ID), title: Some("Select Pet".to_string()), subtitle: Some("Choose a pet to wake in the terminal.".to_string()), diff --git a/codex-rs/tui/src/resume_picker.rs b/codex-rs/tui/src/resume_picker.rs index b054e8db4c..3e37641be1 100644 --- a/codex-rs/tui/src/resume_picker.rs +++ b/codex-rs/tui/src/resume_picker.rs @@ -21,6 +21,8 @@ use crate::markdown_render::render_streaming_markdown_lines_with_width_and_cwd a use crate::pager_overlay::Overlay; use crate::status::format_directory_display; use crate::style::footer_hint_label_style; +use crate::style::readable_color_on; +use crate::style::secondary_text_style; use crate::terminal_palette::best_color; use crate::terminal_palette::default_bg; use crate::text_formatting::truncate_text; @@ -54,8 +56,6 @@ use crossterm::event::KeyCode; use crossterm::event::KeyEvent; use crossterm::event::KeyEventKind; use crossterm::event::KeyModifiers; -use ratatui::layout::Constraint; -use ratatui::layout::Layout; use ratatui::layout::Rect; use ratatui::layout::Size; use ratatui::style::Color; @@ -75,6 +75,7 @@ use unicode_width::UnicodeWidthStr; use uuid::Uuid; mod archive; +mod layout; mod page_loading; #[cfg(test)] @@ -102,7 +103,6 @@ const SESSION_META_CWD_ICON: &str = "⌁"; const FOOTER_COMPACT_BREAKPOINT: u16 = 120; const FOOTER_HINT_LEFT_PADDING: usize = 1; const FOOTER_HINT_GAP: usize = 3; -const PICKER_CHROME_HEIGHT: u16 = 8; const PICKER_LIST_HORIZONTAL_INSET: u16 = 4; #[derive(Debug, Clone)] @@ -578,7 +578,7 @@ async fn run_session_picker_with_loader( state.request_frame(); if let Ok(size) = alt.tui.terminal.size() { - let list_height = size.height.saturating_sub(PICKER_CHROME_HEIGHT) as usize; + let list_height = usize::from(layout::areas(Rect::from(size)).list.height); state.update_viewport(list_height, list_viewport_width(size.width)); state.ensure_minimum_rows_for_view(list_height); } @@ -620,7 +620,7 @@ async fn run_session_picker_with_loader( TuiEvent::Draw | TuiEvent::Resume | TuiEvent::Resize(_) | TuiEvent::FocusGained => { let list_width = list_viewport_width(screen_size.width); let list_height = - usize::from(screen_size.height.saturating_sub(PICKER_CHROME_HEIGHT)); + usize::from(layout::areas(Rect::from(screen_size)).list.height); state.update_viewport(list_height, list_width); state.ensure_minimum_rows_for_view(list_height); draw_picker(alt.tui, &state, screen_size)?; @@ -2110,54 +2110,7 @@ fn parse_timestamp_str(ts: &str) -> Option> { } fn draw_picker(tui: &mut Tui, state: &PickerState, screen_size: Size) -> std::io::Result<()> { - // Render full-screen overlay - tui.draw(screen_size.height, |frame| { - let area = frame.area(); - let [header, _header_gap, search, _search_gap, list, footer] = Layout::vertical([ - Constraint::Length(1), - Constraint::Length(1), - Constraint::Length(1), - Constraint::Length(1), - Constraint::Min(area.height.saturating_sub(PICKER_CHROME_HEIGHT)), - Constraint::Length(4), - ]) - .areas(area); - - let chrome = |area: Rect| { - Rect::new( - area.x.saturating_add(1), - area.y, - area.width.saturating_sub(2), - area.height, - ) - }; - - // Header - let header_title = if default_bg().is_some_and(is_light) { - state.action.title().bold().fg(best_color((0, 100, 0))) - } else { - state.action.title().bold().cyan() - }; - let header_line: Line = vec![header_title].into(); - frame.render_widget_ref(&header_line, chrome(header)); - - // Search line - let search = chrome(search); - frame.render_widget_ref(&search_line(state, search.width), search); - - let list = Rect::new( - list.x.saturating_add(2), - list.y, - list_viewport_width(list.width), - list.height, - ); - render_list(frame, list, state); - if state.is_transcript_loading() { - render_transcript_loading_overlay(frame, list); - } - - render_picker_footer(frame, footer, state, list.height); - }) + tui.draw(screen_size.height, |frame| layout::render(frame, state)) } fn list_viewport_width(width: u16) -> u16 { @@ -2166,38 +2119,40 @@ fn list_viewport_width(width: u16) -> u16 { fn search_line(state: &PickerState, width: u16) -> Line<'_> { if let Some(error) = state.inline_error.as_deref() { - return Line::from(error.red()); + return Line::from(truncate_text(error, usize::from(width)).red()); } - let search = if state.query.is_empty() { - "Type to search".dim() + if state.query.is_empty() { + "Type to search".set_style(secondary_text_style()).into() } else { - format!("Search: {}", state.query).into() - }; - let search_width = UnicodeWidthStr::width(search.content.as_ref()); - let mut toolbar = toolbar_line(state, /*compact*/ false); - if search_width.saturating_add(toolbar.width()) > usize::from(width.saturating_sub(2)) { - toolbar = toolbar_line(state, /*compact*/ true); + truncate_text(&format!("Search: {}", state.query), usize::from(width)).into() } - let toolbar_width = toolbar.width(); - let spacer_width = width - .saturating_sub((search_width + toolbar_width) as u16) - .max(2) as usize; - let available_search_width = width - .saturating_sub(toolbar_width as u16) - .saturating_sub(spacer_width as u16) as usize; - let search = if search_width > available_search_width { - let truncated = truncate_text(search.content.as_ref(), available_search_width); - if state.query.is_empty() { - truncated.dim() - } else { - truncated.into() - } - } else { - search - }; +} - let mut spans = vec![search, " ".repeat(spacer_width).into()]; - spans.extend(toolbar.spans); +fn toolbar_for_width(state: &PickerState, width: u16) -> Line<'static> { + let toolbar = toolbar_line(state, /*compact*/ false); + if toolbar.width() <= usize::from(width) { + return toolbar; + } + let toolbar = toolbar_line(state, /*compact*/ true); + if toolbar.width() <= usize::from(width) { + return toolbar; + } + // Keep the control affected by the arrow keys visible when the compact strip cannot fit. + let spans = match state.toolbar_focus { + ToolbarControl::Filter => filter_control_spans(state, /*compact*/ true), + ToolbarControl::Sort => sort_control_spans(state, /*compact*/ true), + ToolbarControl::Status => vec![ + "Status:".set_style(secondary_text_style()), + toolbar_value( + match state.status { + SessionStatus::Active => "Active", + SessionStatus::Archived => "Archived", + }, + /*active*/ true, + /*focused*/ true, + ), + ], + }; spans.into() } @@ -2209,7 +2164,7 @@ fn toolbar_line(state: &PickerState, compact: bool) -> Line<'static> { " " }; spans.extend(filter_control_spans(state, compact)); - spans.push(separator.dim()); + spans.push(separator.set_style(secondary_text_style())); if matches!(state.action, SessionPickerAction::Resume) { let status_focused = state.toolbar_focus == ToolbarControl::Status; if compact { @@ -2223,7 +2178,7 @@ fn toolbar_line(state: &PickerState, compact: bool) -> Line<'static> { status_focused, )); } else { - spans.push("Status: ".dim()); + spans.push("Status: ".set_style(secondary_text_style())); spans.push(toolbar_value( "Active", state.status == SessionStatus::Active, @@ -2235,7 +2190,7 @@ fn toolbar_line(state: &PickerState, compact: bool) -> Line<'static> { status_focused, )); } - spans.push(separator.dim()); + spans.push(separator.set_style(secondary_text_style())); } spans.extend(sort_control_spans(state, compact)); spans.into() @@ -2245,7 +2200,7 @@ fn sort_control_spans(state: &PickerState, compact: bool) -> Vec> let sort_focused = state.toolbar_focus == ToolbarControl::Sort; if compact { return vec![ - "Sort:".dim(), + "Sort:".set_style(secondary_text_style()), toolbar_value( sort_key_label(state.sort_key), /*active*/ true, @@ -2254,7 +2209,7 @@ fn sort_control_spans(state: &PickerState, compact: bool) -> Vec> ]; } vec![ - "Sort: ".dim(), + "Sort: ".set_style(secondary_text_style()), toolbar_value( sort_key_label(ThreadSortKey::UpdatedAt), state.sort_key == ThreadSortKey::UpdatedAt, @@ -2272,7 +2227,7 @@ fn filter_control_spans(state: &PickerState, compact: bool) -> Vec let filter_focused = state.toolbar_focus == ToolbarControl::Filter; if compact || state.filter_cwd.is_none() { return vec![ - "Filter:".dim(), + "Filter:".set_style(secondary_text_style()), toolbar_value( filter_mode_label(state.filter_mode), /*active*/ true, @@ -2281,7 +2236,7 @@ fn filter_control_spans(state: &PickerState, compact: bool) -> Vec ]; } vec![ - "Filter: ".dim(), + "Filter: ".set_style(secondary_text_style()), toolbar_value( filter_mode_label(SessionFilterMode::Cwd), state.filter_mode == SessionFilterMode::Cwd, @@ -2296,15 +2251,16 @@ fn filter_control_spans(state: &PickerState, compact: bool) -> Vec } fn toolbar_value(label: &'static str, active: bool, focused: bool) -> Span<'static> { + let value = format!(" {label} "); if active { - let value = format!("[{label}]"); - if focused { - value.magenta() + value.set_style(if focused { + crate::bottom_pane::active_tab_style() } else { - value.into() - } + let style = crate::style::user_message_style().bold().not_dim(); + style.fg(readable_color_on(Color::Reset, style.bg)) + }) } else { - format!(" {label} ").dim() + value.set_style(secondary_text_style()) } } @@ -2369,7 +2325,10 @@ fn render_picker_footer_separator( progress_width, 1, ); - frame.render_widget_ref(&Line::from(progress_label.dim()), percent_area); + frame.render_widget_ref( + &Line::from(progress_label.set_style(secondary_text_style())), + percent_area, + ); } } @@ -2587,21 +2546,19 @@ fn hint_line_for_row(hints: &[PickerFooterHint], width: u16) -> Line<'static> { if let Some(line) = fit_footer_hints(hints, FooterHintLabelMode::Compact, width) { return line; } - if let Some(line) = fit_footer_hints(hints, FooterHintLabelMode::KeyOnly, width) { - return line; - } - let mut retained = (0..hints.len()).collect::>(); retained.sort_by_key(|idx| hints[*idx].priority); - for retain_count in (1..=retained.len()).rev() { - let mut candidate_indices = retained[..retain_count].to_vec(); - candidate_indices.sort_unstable(); - let candidate = candidate_indices - .iter() - .map(|idx| &hints[*idx]) - .collect::>(); - if let Some(line) = fit_footer_hint_refs(&candidate, FooterHintLabelMode::KeyOnly, width) { - return line; + for mode in [FooterHintLabelMode::Compact, FooterHintLabelMode::KeyOnly] { + for retain_count in (1..=retained.len()).rev() { + let mut candidate_indices = retained[..retain_count].to_vec(); + candidate_indices.sort_unstable(); + let candidate = candidate_indices + .iter() + .map(|idx| &hints[*idx]) + .collect::>(); + if let Some(line) = fit_footer_hint_refs(&candidate, mode, width) { + return line; + } } } Line::default() @@ -2646,14 +2603,23 @@ fn render_transcript_loading_overlay(frame: &mut crate::custom_terminal::Frame, fn transcript_loading_overlay_style() -> Style { let Some(bg) = default_bg() else { - return Style::default().bg(Color::DarkGray); + return Style::default() + .fg(Color::Reset) + .bg(Color::Reset) + .not_dim() + .not_reversed(); }; let (overlay, alpha) = if is_light(bg) { ((0, 0, 0), 0.08) } else { ((255, 255, 255), 0.14) }; - Style::default().bg(best_color(blend(overlay, bg, alpha))) + let fill = best_color(blend(overlay, bg, alpha)); + Style::default() + .bg(fill) + .fg(readable_color_on(Color::Reset, Some(fill))) + .not_dim() + .not_reversed() } #[derive(Clone, Copy)] @@ -2791,7 +2757,13 @@ fn render_list(frame: &mut crate::custom_terminal::Frame, area: Rect, state: &Pi } if state.pagination.is_loading() && y < content_area.y.saturating_add(content_area.height) { - let loading_line: Line = vec![" ".into(), "Loading older sessions…".italic().dim()].into(); + let loading_line: Line = vec![ + " ".into(), + "Loading older sessions…" + .set_style(secondary_text_style()) + .italic(), + ] + .into(); let rect = Rect::new(area.x, y, area.width, 1); frame.render_widget_ref(&loading_line, rect); } @@ -2814,7 +2786,12 @@ fn render_list(frame: &mut crate::custom_terminal::Frame, area: Rect, state: &Pi } fn more_line(label: &'static str) -> Line<'static> { - vec![label.dim()].into() + vec![ + label + .fg(crate::style::accent_color_on(/*background*/ None)) + .not_dim(), + ] + .into() } fn render_session_lines( @@ -2849,15 +2826,13 @@ fn render_comfortable_session_lines( let title_line = Line::from(vec![marker, title]); let mut lines = vec![title_line]; let row_style = if is_selected { - Some(dense_selected_style()) + dense_selected_style() } else if is_zebra { - Some(dense_zebra_style()) + dense_zebra_style() } else { - None + Style::default() }; - if let Some(style) = row_style { - lines = apply_session_row_background(lines, style, width); - } + lines = apply_session_row_background(lines, row_style, width); if is_expanded { lines.extend(render_transcript_preview_lines(row, state, width)); return lines; @@ -2887,11 +2862,7 @@ fn render_comfortable_session_lines( show_cwd, width, ); - if let Some(style) = row_style { - lines.extend(apply_session_row_background(footer_lines, style, width)); - } else { - lines.extend(footer_lines); - } + lines.extend(apply_session_row_background(footer_lines, row_style, width)); lines } @@ -2913,8 +2884,21 @@ fn apply_line_background(mut line: Line<'static>, style: Style, width: u16) -> L } line.style = line.style.patch(style); for span in &mut line.spans { - // Keep identity accents while inheriting the selected row's background. + // Preserve each role's hue and weight, then resolve it against the painted row. span.style = style.patch(span.style); + if style + .add_modifier + .contains(ratatui::style::Modifier::REVERSED) + { + span.style.fg = style.fg; + span.style = span.style.reversed(); + } else { + span.style.fg = Some(readable_color_on( + span.style.fg.unwrap_or(Color::Reset), + span.style.bg, + )); + span.style = span.style.not_reversed(); + } } line } @@ -2977,26 +2961,18 @@ fn dense_summary_line(input: DenseSummaryInput<'_>) -> Line<'static> { let spans = vec![ input.marker, - dense_column_text(input.date, columns.date_width).dim(), + dense_column_text(input.date, columns.date_width).set_style(secondary_text_style()), title, ]; - let mut line = Line::from(spans); - if input.is_selected { - let padding = (input.width as usize).saturating_sub(line.width()); - if padding > 0 { - line.spans - .push(" ".repeat(padding).set_style(dense_selected_style())); - } - line = line.style(dense_selected_style()); + let line = Line::from(spans); + let row_style = if input.is_selected { + dense_selected_style() } else if input.is_zebra { - let padding = (input.width as usize).saturating_sub(line.width()); - if padding > 0 { - line.spans - .push(" ".repeat(padding).set_style(dense_zebra_style())); - } - line = line.style(dense_zebra_style()); - } - line + dense_zebra_style() + } else { + Style::default() + }; + apply_line_background(line, row_style, input.width) } struct DenseColumns { @@ -3017,7 +2993,7 @@ fn dense_zebra_style() -> Style { } fn dense_selected_style() -> Style { - selected_session_style().patch(dense_row_background_style(/*selected*/ true)) + selected_session_style() } fn dense_row_background_style(selected: bool) -> Style { @@ -3041,17 +3017,13 @@ fn dense_column_text(text: &str, width: usize) -> String { fn selection_marker(is_selected: bool, is_expanded: bool) -> Span<'static> { match (is_selected, is_expanded) { (true, true) => "⌄ ".set_style(selected_session_style().bold()), - (true, false) => "❯ ".set_style(selected_session_style().bold()), + (true, false) => "› ".set_style(selected_session_style()), (false, _) => " ".into(), } } fn selected_session_style() -> Style { - if default_bg().is_some_and(is_light) { - Style::default().fg(Color::Magenta) - } else { - Style::default().fg(Color::Yellow) - } + crate::style::selection_style() } fn session_title_span( @@ -3060,13 +3032,12 @@ fn session_title_span( use_theme_colors: bool, is_selected: bool, ) -> Span<'static> { - if use_theme_colors && let Some(thread_id) = thread_id { + let title = if use_theme_colors && let Some(thread_id) = thread_id { title.fg(crate::thread_color::thread_color(thread_id)) - } else if is_selected { - title.set_style(selected_session_style()) } else { title.into() - } + }; + if is_selected { title.bold() } else { title } } fn render_footer_lines( @@ -3185,7 +3156,7 @@ fn footer_line(parts: Vec, width: usize, cwd_width: usize) -> Line<' if idx > 0 { let gap_width = SESSION_META_FIELD_GAP_WIDTH.min(remaining_width); if gap_width > 0 { - spans.push(" ".repeat(gap_width).dim()); + spans.push(" ".repeat(gap_width).set_style(secondary_text_style())); remaining_width = remaining_width.saturating_sub(gap_width); } } @@ -3200,7 +3171,7 @@ fn footer_line(parts: Vec, width: usize, cwd_width: usize) -> Line<' if let Some(target_width) = target_width { let padding = target_width.saturating_sub(used_width); if padding > 0 { - spans.push(" ".repeat(padding).dim()); + spans.push(" ".repeat(padding).set_style(secondary_text_style())); remaining_width = remaining_width.saturating_sub(padding); } } @@ -3218,7 +3189,7 @@ fn push_footer_part( let Some(prefix) = part.prefix() else { let text = truncate_text(&text, available_width); let width = UnicodeWidthStr::width(text.as_str()); - spans.push(text.dim()); + spans.push(text.set_style(secondary_text_style())); return width; }; @@ -3226,14 +3197,14 @@ fn push_footer_part( if available_width <= prefix_width { let prefix = truncate_text(prefix, available_width); let width = UnicodeWidthStr::width(prefix.as_str()); - spans.push(prefix.dim()); + spans.push(prefix.set_style(secondary_text_style())); return width; } - spans.push(prefix.dim()); + spans.push(prefix.set_style(secondary_text_style())); let mut used_width = prefix_width; if !text.is_empty() && used_width < available_width { - spans.push(" ".dim()); + spans.push(" ".set_style(secondary_text_style())); used_width += 1; } let text_width = target_width @@ -3243,8 +3214,10 @@ fn push_footer_part( let text = truncate_text(&text, text_width); let rendered_text_width = UnicodeWidthStr::width(text.as_str()); match part { - FooterPart::Branch(None) | FooterPart::Cwd(None) => spans.push(text.dim().italic()), - _ => spans.push(text.dim()), + FooterPart::Branch(None) | FooterPart::Cwd(None) => { + spans.push(text.set_style(secondary_text_style()).italic()) + } + _ => spans.push(text.set_style(secondary_text_style())), } used_width + rendered_text_width } @@ -3260,12 +3233,24 @@ fn render_transcript_preview_lines( }; let preview_lines = match state.transcript_previews.get(&thread_id) { Some(TranscriptPreviewState::Loading) => { - vec![vec![" │ ".dim(), "Loading recent transcript...".italic().dim()].into()] + vec![ + vec![ + " │ ".set_style(secondary_text_style()), + "Loading recent transcript..." + .set_style(secondary_text_style()) + .italic(), + ] + .into(), + ] } Some(TranscriptPreviewState::Failed) => vec![ vec![ - " │ ".dim(), - "Could not load transcript preview".italic().red(), + " │ ".set_style(secondary_text_style()), + "Could not load transcript preview" + .set_style(crate::style::status_style( + crate::style::StatusTone::Failure, + )) + .italic(), ] .into(), ], @@ -3295,11 +3280,10 @@ fn render_expanded_session_details( .as_ref() .map(|path| format_directory_display(path, /*max_width*/ None)) .unwrap_or_else(|| "-".to_string()); - let branch = row - .git_branch - .as_ref() - .map(|branch| format!("{SESSION_META_BRANCH_ICON} {branch}")) - .unwrap_or_else(|| format!("{SESSION_META_BRANCH_ICON} no branch")); + let branch = format!( + "{SESSION_META_BRANCH_ICON} {}", + row.git_branch.as_deref().unwrap_or("no branch") + ); vec![ expanded_detail_line("Session:", &session, width), @@ -3312,8 +3296,12 @@ fn render_expanded_session_details( ), expanded_detail_line("Directory:", &directory, width), expanded_detail_line("Branch:", &branch, width), - vec![" │".dim()].into(), - vec![" │ ".dim(), "Conversation:".dim()].into(), + vec![" │".set_style(secondary_text_style())].into(), + vec![ + " │ ".set_style(secondary_text_style()), + "Conversation:".set_style(secondary_text_style()), + ] + .into(), ] } @@ -3325,8 +3313,10 @@ fn render_conversation_preview_lines( if lines.is_empty() { return vec![ vec![ - " └ ".dim(), - "No transcript preview available".italic().dim(), + " └ ".set_style(secondary_text_style()), + "No transcript preview available" + .set_style(secondary_text_style()) + .italic(), ] .into(), ]; @@ -3410,19 +3400,14 @@ fn connector_style_from_content(style: Style) -> Style { } fn conversation_assistant_style() -> Style { - if default_bg().is_some_and(is_light) { - Style::default().fg(Color::Gray) - } else { - Style::default().fg(Color::DarkGray) - } + secondary_text_style() } fn conversation_user_style() -> Style { - if default_bg().is_some_and(is_light) { - Style::default().fg(Color::DarkGray).italic() - } else { - Style::default().fg(Color::Gray).italic() - } + Style::default() + .fg(readable_color_on(Color::Reset, /*background*/ None)) + .not_dim() + .italic() } fn expanded_detail_line(label: &'static str, value: &str, width: u16) -> Line<'static> { @@ -3433,9 +3418,9 @@ fn expanded_detail_line(label: &'static str, value: &str, width: u16) -> Line<'s .saturating_sub(prefix_width + LABEL_WIDTH + gap_width) .max(1); vec![ - " │ ".dim(), - format!("{label: Line<'static> { if state.search_state.is_active() || (state.pagination.is_loading() && state.pagination.next_cursor.is_some()) { - return vec!["Searching…".italic().dim()].into(); + return vec!["Searching…".set_style(secondary_text_style()).italic()].into(); } if state.pagination.reached_scan_cap { let msg = format!( "Search scanned first {} sessions; more may exist", state.pagination.num_scanned_files ); - return vec![Span::from(msg).italic().dim()].into(); + return vec![Span::from(msg).set_style(secondary_text_style()).italic()].into(); } - return vec!["No results for your search".italic().dim()].into(); + return vec![ + "No results for your search" + .set_style(secondary_text_style()) + .italic(), + ] + .into(); } if state.pagination.is_loading() { if state.all_rows.is_empty() && state.pagination.num_scanned_files == 0 { - return vec!["Loading sessions…".italic().dim()].into(); + return vec![ + "Loading sessions…" + .set_style(secondary_text_style()) + .italic(), + ] + .into(); } - return vec!["Loading older sessions…".italic().dim()].into(); + return vec![ + "Loading older sessions…" + .set_style(secondary_text_style()) + .italic(), + ] + .into(); } - vec!["No sessions yet".italic().dim()].into() + vec!["No sessions yet".set_style(secondary_text_style()).italic()].into() } #[cfg(test)] @@ -4620,7 +4620,7 @@ mod tests { } #[test] - fn hint_line_prioritizes_keybinds_when_very_narrow() { + fn hint_line_preserves_primary_action_labels_when_very_narrow() { let loader = page_only_loader(|_| {}); let mut state = PickerState::new( FrameRequester::test_dummy(), @@ -4641,13 +4641,10 @@ mod tests { .join("\n"); assert!(lines.iter().all(|line| line.width() <= width as usize)); - assert!(rendered.contains("enter")); - assert!(rendered.contains("esc")); - assert!(rendered.contains("ctrl+c")); - assert!(rendered.contains("ctrl+o")); - assert!(rendered.contains("ctrl+t")); - assert!(rendered.contains("ctrl+e")); - assert!(rendered.contains("↑/↓")); + assert_eq!( + rendered, + " enter resume esc new ctrl+c quit\n ctrl+o comfy ctrl+t preview" + ); } #[test] @@ -5417,7 +5414,7 @@ session_picker_view = "dense" } #[test] - fn search_line_renders_sort_and_filter_tabs() { + fn toolbar_renders_sort_and_filter_tabs() { use crate::custom_terminal::Terminal; use crate::test_backend::VT100Backend; @@ -5438,7 +5435,7 @@ session_picker_view = "dense" { let mut frame = terminal.get_frame(); - let line = search_line(&state, frame.area().width); + let line = toolbar_for_width(&state, frame.area().width); frame.render_widget_ref(&line, frame.area()); } terminal.flush().expect("flush"); @@ -5450,7 +5447,7 @@ session_picker_view = "dense" } #[test] - fn search_line_compacts_toolbar_on_narrow_width() { + fn toolbar_compacts_on_narrow_width() { let loader = page_only_loader(|_| {}); let state = PickerState::new( FrameRequester::test_dummy(), @@ -5461,12 +5458,12 @@ session_picker_view = "dense" SessionPickerAction::Resume, ); - let line = search_line(&state, /*width*/ 40).to_string(); + let line = toolbar_for_width(&state, /*width*/ 40).to_string(); - assert!(line.contains("Filter:[Cwd]")); - assert!(line.contains("[Active]")); - assert!(line.contains("Sort:[Updated]")); - assert!(line.find("Filter:[Cwd]") < line.find("Sort:[Updated]")); + assert!(line.contains("Filter: Cwd ")); + assert!(line.contains(" Active ")); + assert!(line.contains("Sort: Updated ")); + assert!(line.find("Filter: Cwd ") < line.find("Sort: Updated ")); } fn dense_snapshot_row() -> Row { @@ -5626,7 +5623,7 @@ session_picker_view = "dense" assert_eq!(line.width(), 80); assert_eq!(line.style.fg, selected_session_style().fg); - assert_eq!(line.spans[0].content, "❯ "); + assert_eq!(line.spans[0].content, "› "); } #[test] @@ -6249,7 +6246,7 @@ session_picker_view = "dense" ); assert_eq!( - search_line(&state, /*width*/ 80) + toolbar_for_width(&state, /*width*/ 80) .to_string() .matches("Cwd") .count(), diff --git a/codex-rs/tui/src/resume_picker/archive_tests.rs b/codex-rs/tui/src/resume_picker/archive_tests.rs index f4d232feb1..5923f50152 100644 --- a/codex-rs/tui/src/resume_picker/archive_tests.rs +++ b/codex-rs/tui/src/resume_picker/archive_tests.rs @@ -213,7 +213,7 @@ fn archived_status_preserves_directory_filter_and_hides_archive_shortcut() { "); insta::assert_snapshot!( super::super::toolbar_line(&state, /*compact*/ true).to_string(), - @"Filter:[Cwd] [Archived] Sort:[Updated]" + @"Filter: Cwd Archived Sort: Updated " ); state.toggle_filter_mode(); diff --git a/codex-rs/tui/src/resume_picker/layout.rs b/codex-rs/tui/src/resume_picker/layout.rs new file mode 100644 index 0000000000..25aed489b7 --- /dev/null +++ b/codex-rs/tui/src/resume_picker/layout.rs @@ -0,0 +1,85 @@ +//! Session-picker chrome shares its geometry with paging and rendering. +//! Decorative gaps collapse before the list loses room for two comfortable rows. + +use ratatui::layout::Constraint; +use ratatui::layout::Layout; +use ratatui::layout::Rect; +use ratatui::style::Stylize; +use ratatui::text::Line; + +use super::PickerState; +use super::list_viewport_width; +use super::render_list; +use super::render_picker_footer; +use super::render_transcript_loading_overlay; +use super::search_line; +use super::toolbar_for_width; + +pub(super) struct PickerAreas { + header: Rect, + toolbar: Rect, + search: Rect, + pub(super) list: Rect, + footer: Rect, +} + +pub(super) fn areas(area: Rect) -> PickerAreas { + const FIXED_CHROME_ROWS: u16 = 7; + const MIN_LIST_ROWS: u16 = 6; + let gaps = area + .height + .saturating_sub(FIXED_CHROME_ROWS + MIN_LIST_ROWS) + .min(/*other*/ 3); + let [header, _, toolbar, _, search, _, list, footer] = Layout::vertical([ + Constraint::Length(1), + Constraint::Length(u16::from(gaps >= 3)), + Constraint::Length(1), + Constraint::Length(u16::from(gaps >= 2)), + Constraint::Length(1), + Constraint::Length(u16::from(gaps >= 1)), + Constraint::Min(area.height.saturating_sub(FIXED_CHROME_ROWS + gaps)), + Constraint::Length(4), + ]) + .areas(area); + PickerAreas { + header, + toolbar, + search, + list, + footer, + } +} + +pub(super) fn render(frame: &mut crate::custom_terminal::Frame, state: &PickerState) { + let PickerAreas { + header, + toolbar, + search, + list, + footer, + } = areas(frame.area()); + let chrome = |area: Rect| { + Rect::new( + area.x.saturating_add(/*rhs*/ 1), + area.y, + area.width.saturating_sub(/*rhs*/ 2), + area.height, + ) + }; + frame.render_widget_ref(&Line::from(state.action.title().bold()), chrome(header)); + let toolbar = chrome(toolbar); + frame.render_widget_ref(&toolbar_for_width(state, toolbar.width), toolbar); + let search = chrome(search); + frame.render_widget_ref(&search_line(state, search.width), search); + let list = Rect::new( + list.x.saturating_add(/*rhs*/ 2), + list.y, + list_viewport_width(list.width), + list.height, + ); + render_list(frame, list, state); + if state.is_transcript_loading() { + render_transcript_loading_overlay(frame, list); + } + render_picker_footer(frame, footer, state, list.height); +} diff --git a/codex-rs/tui/src/resume_picker_color_tests.rs b/codex-rs/tui/src/resume_picker_color_tests.rs index 81e6caa316..7a61988c84 100644 --- a/codex-rs/tui/src/resume_picker_color_tests.rs +++ b/codex-rs/tui/src/resume_picker_color_tests.rs @@ -1,11 +1,22 @@ use super::*; +use crate::terminal_palette::rgb_color; +use crate::terminal_palette::with_test_default_colors; +use crate::terminal_probe::DefaultColors; +use pretty_assertions::assert_eq; +use ratatui::buffer::Buffer; +use ratatui::style::Modifier; #[test] -fn thread_colors_survive_selection_in_both_picker_layouts() { - let id = ThreadId::from_u128(/*value*/ 42); +fn thread_colors_and_selection_contrast_in_both_picker_layouts() { + let original_theme = crate::render::highlight::current_syntax_theme(); + crate::render::highlight::set_syntax_theme( + two_face::theme::extra() + .get(two_face::theme::EmbeddedThemeName::CatppuccinMocha) + .clone(), + ); let row = Row { path: None, - thread_id: Some(id), + thread_id: Some(ThreadId::from_u128(/*value*/ 42)), thread_name: Some("Named task".into()), preview: "Original prompt".into(), created_at: None, @@ -21,22 +32,307 @@ fn thread_colors_survive_selection_in_both_picker_layouts() { /*filter_cwd*/ None, SessionPickerAction::Resume, ); - let mut snapshot = Vec::new(); - for density in [SessionListDensity::Comfortable, SessionListDensity::Dense] { - state.density = density; - for use_colors in [true, false] { - state.use_theme_colors = use_colors; - for selected in [true, false] { - let lines = render_session_lines( - &row, &state, selected, /*is_expanded*/ false, /*is_zebra*/ false, - /*width*/ 32, + let (fg, bg) = ((0, 0, 0), (255, 255, 255)); + with_test_default_colors(DefaultColors { fg, bg }, || { + for density in [SessionListDensity::Comfortable, SessionListDensity::Dense] { + state.density = density; + for use_colors in [true, false] { + state.use_theme_colors = use_colors; + for (selected, zebra) in [(true, false), (false, false), (false, true)] { + let lines = render_session_lines( + &row, &state, selected, /*is_expanded*/ false, zebra, + /*width*/ 40, + ); + let area = Rect::new( + /*x*/ 0, + /*y*/ 0, + /*width*/ 40, + lines.len() as u16, + ); + let mut buffer = Buffer::empty(area); + buffer.set_style(area, Style::default().fg(rgb_color(fg)).bg(rgb_color(bg))); + for (y, line) in lines.into_iter().enumerate() { + line.render( + Rect::new(/*x*/ 0, y as u16, /*width*/ 40, /*height*/ 1), + &mut buffer, + ); + } + let title_x = match density { + SessionListDensity::Comfortable => 2, + SessionListDensity::Dense => 14, + }; + let title_cell = &buffer[(title_x, 0)]; + if use_colors { + assert_eq!( + title_cell.fg, + readable_color_on( + crate::thread_color::thread_color(row.thread_id.unwrap()), + Some(title_cell.bg), + ) + ); + } else { + let expected = if selected { + selected_session_style().fg.unwrap() + } else { + rgb_color(fg) + }; + assert_eq!(title_cell.fg, expected); + } + if selected { + let style = selected_session_style(); + let metadata_y = match density { + SessionListDensity::Comfortable => 1, + SessionListDensity::Dense => 0, + }; + let metadata = &buffer[(2, metadata_y)]; + assert_eq!( + ( + buffer + .content + .iter() + .map(|cell| cell.bg) + .collect::>(), + title_cell.modifier.contains(Modifier::BOLD), + metadata.modifier.contains(Modifier::BOLD), + ), + (vec![style.bg.unwrap(); buffer.content.len()], true, false) + ); + } + } + } + } + }); + crate::render::highlight::set_syntax_theme(original_theme); +} + +#[test] +fn loading_overlay_clears_inherited_selection_colors_and_reversal() { + use crate::custom_terminal::Terminal; + use crate::test_backend::VT100Backend; + + let width = UnicodeWidthStr::width("Loading transcript…") as u16; + let area = Rect::new(/*x*/ 0, /*y*/ 0, width, /*height*/ 1); + let backend = VT100Backend::new(width, /*height*/ 1); + let mut terminal = Terminal::with_options(backend).expect("terminal"); + terminal.set_viewport_area(area); + let mut frame = terminal.get_frame(); + frame.buffer.set_style( + area, + Style::default().red().bg(Color::Yellow).dim().reversed(), + ); + + render_transcript_loading_overlay(&mut frame, area); + + let style = transcript_loading_overlay_style(); + assert_eq!( + frame + .buffer + .content + .iter() + .map(|cell| (cell.fg, cell.bg, cell.modifier)) + .collect::>(), + vec![ + ( + style.fg.unwrap_or(Color::Reset), + style.bg.unwrap_or(Color::Reset), + Modifier::BOLD + ); + usize::from(width) + ] + ); +} + +#[test] +fn compact_picker_keeps_metadata_and_labeled_primary_actions() { + use crate::custom_terminal::Terminal; + use crate::test_backend::VT100Backend; + + let timestamp = parse_timestamp_str("2026-09-17T12:00:00Z"); + let mut state = PickerState::new( + FrameRequester::test_dummy(), + Arc::new(|_| {}), + ProviderFilter::Any, + /*show_all*/ false, + Some(PathBuf::from("/tmp/codex")), + SessionPickerAction::Resume, + ); + state.relative_time_reference = timestamp; + state.filtered_rows = (1..=6) + .map(|id| Row { + path: None, + thread_id: Some(ThreadId::from_u128(id)), + thread_name: Some(format!("Session {id}")), + preview: String::new(), + created_at: timestamp, + updated_at: timestamp, + cwd: Some(PathBuf::from("/tmp/codex")), + git_branch: Some("fcoury/contrast".into()), + }) + .collect(); + let mut snapshots = Vec::new(); + with_test_default_colors( + DefaultColors { + fg: (32, 32, 32), + bg: (255, 255, 255), + }, + || { + for (action, width, height) in [ + (SessionPickerAction::Resume, 118, 30), + (SessionPickerAction::Resume, 70, 12), + (SessionPickerAction::Resume, 38, 12), + (SessionPickerAction::Fork, 38, 12), + ] { + state.action = action; + let backend = VT100Backend::new(width, height); + let mut terminal = Terminal::with_options(backend).expect("terminal"); + let area = Rect::new(/*x*/ 0, /*y*/ 0, width, height); + terminal.set_viewport_area(area); + let list = layout::areas(area).list; + state.update_viewport(usize::from(list.height), list_viewport_width(width)); + { + let mut frame = terminal.get_frame(); + layout::render(&mut frame, &state); + let progress = frame + .buffer + .content + .iter() + .filter(|cell| cell.symbol() == "%") + .collect::>(); + assert_eq!(progress.len(), 1); + assert!(!progress[0].modifier.contains(Modifier::DIM)); + } + terminal.flush().expect("flush"); + let text = terminal.backend().to_string(); + assert!( + text.contains(" fcoury/contrast"), + "{width}x{height}: {text}" ); - snapshot.push(format!( - "{density:?} colors={use_colors} selected={selected}: {:?}", - lines[0] - )); + assert!(text.contains("now"), "{width}x{height}: {text}"); + assert!( + text.contains(&format!("enter {}", action.action_label())), + "{action:?} {width}x{height}: {text}" + ); + assert!( + text.contains(if width >= FOOTER_COMPACT_BREAKPOINT { + "esc start new" + } else { + "esc new" + }), + "{width}x{height}: {text}" + ); + snapshots.push(format!("{action:?} {width}x{height}\n{text}")); + } + }, + ); + insta::assert_snapshot!(snapshots.join("\n")); +} + +#[test] +fn reversed_selection_overrides_thread_color() { + let line = apply_line_background( + Line::from("Title".fg(rgb_color((150, 50, 170)))), + Style::default() + .fg(Color::Reset) + .bg(Color::Reset) + .reversed(), + /*width*/ 12, + ); + assert_eq!( + ( + line.spans[0].style.fg, + line.spans[0] + .style + .add_modifier + .contains(Modifier::REVERSED) + ), + (Some(Color::Reset), true) + ); +} + +#[tokio::test] +async fn narrow_toolbar_keeps_keyboard_focused_control_visible() { + use crate::custom_terminal::Terminal; + use crate::test_backend::VT100Backend; + + let mut snapshots = Vec::new(); + for action in [SessionPickerAction::Resume, SessionPickerAction::Fork] { + for (width, height) in [(38, 12), (24, 9)] { + let mut state = PickerState::new( + FrameRequester::test_dummy(), + Arc::new(|_| {}), + ProviderFilter::Any, + /*show_all*/ false, + Some(PathBuf::from("/tmp/codex")), + action, + ); + if matches!(action, SessionPickerAction::Resume) { + state.status = SessionStatus::Archived; + } + let controls: &[(&str, &str)] = match action { + SessionPickerAction::Resume => &[ + ("Filter: Cwd", "Cwd"), + ("Status: Archived", "Archived"), + ("Sort: Updated", "Updated"), + ], + SessionPickerAction::Fork => { + &[("Filter: Cwd", "Cwd"), ("Sort: Updated", "Updated")] + } + }; + for &(label, value) in controls { + with_test_default_colors( + DefaultColors { + fg: (32, 32, 32), + bg: (130, 130, 130), + }, + || { + let backend = VT100Backend::new(width, height); + let mut terminal = Terminal::with_options(backend).expect("terminal"); + let area = Rect::new(/*x*/ 0, /*y*/ 0, width, height); + terminal.set_viewport_area(area); + let list = layout::areas(area).list; + state.update_viewport(usize::from(list.height), list_viewport_width(width)); + let mut frame = terminal.get_frame(); + layout::render(&mut frame, &state); + let rows = frame + .buffer + .content + .chunks(usize::from(width)) + .map(|row| { + row.iter() + .map(ratatui::buffer::Cell::symbol) + .collect::() + }) + .collect::>(); + let y = rows + .iter() + .position(|row| row.contains(label)) + .expect("keyboard-focused toolbar control stays visible"); + let x = rows[y].find(value).unwrap(); + let focused_style = crate::bottom_pane::active_tab_style(); + let cells = (x..x + value.len()) + .map(|x| &frame.buffer[(x as u16, y as u16)]) + .collect::>(); + assert_eq!( + cells.iter().map(|cell| cell.bg).collect::>(), + vec![focused_style.bg.unwrap(); value.len()] + ); + assert!( + cells + .iter() + .all(|cell| !cell.modifier.contains(Modifier::DIM)) + ); + snapshots.push(format!( + "{action:?} {width}x{height} {label}\n{}", + rows[y].trim_end() + )); + }, + ); + state + .handle_key(KeyEvent::from(KeyCode::Tab)) + .await + .unwrap(); } } } - insta::assert_snapshot!(snapshot.join("\n")); + insta::assert_snapshot!(snapshots.join("\n\n")); } diff --git a/codex-rs/tui/src/snapshots/codex_tui__resume_picker__color_tests__compact_picker_keeps_metadata_and_labeled_primary_actions.snap b/codex-rs/tui/src/snapshots/codex_tui__resume_picker__color_tests__compact_picker_keeps_metadata_and_labeled_primary_actions.snap new file mode 100644 index 0000000000..8e384a2567 --- /dev/null +++ b/codex-rs/tui/src/snapshots/codex_tui__resume_picker__color_tests__compact_picker_keeps_metadata_and_labeled_primary_actions.snap @@ -0,0 +1,70 @@ +--- +source: tui/src/resume_picker_color_tests.rs +expression: "snapshots.join(\"\\n\")" +--- +Resume 118x30 + Resume a previous session + + Filter: Cwd All Status: Active Archived Sort: Updated Created + + Type to search + + › Session 1 + now  fcoury/contrast + + Session 2 + now  fcoury/contrast + + Session 3 + now  fcoury/contrast + + Session 4 + now  fcoury/contrast + + Session 5 + now  fcoury/contrast + + Session 6 + now  fcoury/contrast + + + +─────────────────────────────────────────────────────────────────────────────────────────────────────── 1 / 6 · 100% ─ + enter resume ctrl+a archive esc new ctrl+c quit tab focus ←/→ option + ctrl+o dense ctrl+t preview ctrl+e exp ↑/↓ browse +Resume 70x12 + Resume a previous session + Filter: Cwd Active Sort: Updated + Type to search + › Session 1 + now  fcoury/contrast + + Session 2 + ↓ more +───────────────────────────────────────────────────────── 1 / 6 · 0% ─ + enter resume ctrl+a archive esc new ctrl+c quit tab focus + ctrl+o dense ctrl+t preview ctrl+e exp ↑/↓ browse +Resume 38x12 + Resume a previous session + Filter: Cwd Active Sort: Updated + Type to search + › Session 1 + now  fcoury/contrast + + Session 2 + ↓ more +───────────────────────── 1 / 6 · 0% ─ + enter resume esc new + ctrl+o dense ctrl+t preview +Fork 38x12 + Fork a previous session + Filter: Cwd Sort: Updated + Type to search + › Session 1 + now  fcoury/contrast + + Session 2 + ↓ more +───────────────────────── 1 / 6 · 0% ─ + enter fork esc new ctrl+c quit + ctrl+o dense ctrl+t preview diff --git a/codex-rs/tui/src/snapshots/codex_tui__resume_picker__color_tests__narrow_toolbar_keeps_keyboard_focused_control_visible.snap b/codex-rs/tui/src/snapshots/codex_tui__resume_picker__color_tests__narrow_toolbar_keeps_keyboard_focused_control_visible.snap new file mode 100644 index 0000000000..56d0f524c0 --- /dev/null +++ b/codex-rs/tui/src/snapshots/codex_tui__resume_picker__color_tests__narrow_toolbar_keeps_keyboard_focused_control_visible.snap @@ -0,0 +1,33 @@ +--- +source: tui/src/resume_picker_color_tests.rs +expression: "snapshots.join(\"\\n\\n\")" +--- +Resume 38x12 Filter: Cwd + Filter: Cwd + +Resume 38x12 Status: Archived + Status: Archived + +Resume 38x12 Sort: Updated + Sort: Updated + +Resume 24x9 Filter: Cwd + Filter: Cwd + +Resume 24x9 Status: Archived + Status: Archived + +Resume 24x9 Sort: Updated + Sort: Updated + +Fork 38x12 Filter: Cwd + Filter: Cwd Sort: Updated + +Fork 38x12 Sort: Updated + Filter: Cwd Sort: Updated + +Fork 24x9 Filter: Cwd + Filter: Cwd + +Fork 24x9 Sort: Updated + Sort: Updated diff --git a/codex-rs/tui/src/snapshots/codex_tui__resume_picker__color_tests__thread_colors_survive_selection_in_both_picker_layouts.snap b/codex-rs/tui/src/snapshots/codex_tui__resume_picker__color_tests__thread_colors_survive_selection_in_both_picker_layouts.snap deleted file mode 100644 index 45c71c8c15..0000000000 --- a/codex-rs/tui/src/snapshots/codex_tui__resume_picker__color_tests__thread_colors_survive_selection_in_both_picker_layouts.snap +++ /dev/null @@ -1,12 +0,0 @@ ---- -source: tui/src/resume_picker_color_tests.rs -expression: "snapshot.join(\"\\n\")" ---- -Comfortable colors=true selected=true: Line::from_iter([Span::from("❯ ").yellow().bold(), Span::from("Named task").fg(Color::Rgb(242, 205, 205)), Span::from(" ").yellow()]).yellow() -Comfortable colors=true selected=false: Line::from_iter([Span::from(" "), Span::from("Named task").fg(Color::Rgb(242, 205, 205))]) -Comfortable colors=false selected=true: Line::from_iter([Span::from("❯ ").yellow().bold(), Span::from("Named task").yellow(), Span::from(" ").yellow()]).yellow() -Comfortable colors=false selected=false: Line::from_iter([Span::from(" "), Span::from("Named task")]) -Dense colors=true selected=true: Line::from_iter([Span::from("❯ ").yellow().bold(), Span::from("- ").dim(), Span::from("Named task ").fg(Color::Rgb(242, 205, 205))]).yellow() -Dense colors=true selected=false: Line::from_iter([Span::from(" "), Span::from("- ").dim(), Span::from("Named task ").fg(Color::Rgb(242, 205, 205))]) -Dense colors=false selected=true: Line::from_iter([Span::from("❯ ").yellow().bold(), Span::from("- ").dim(), Span::from("Named task ").yellow()]).yellow() -Dense colors=false selected=false: Line::from_iter([Span::from(" "), Span::from("- ").dim(), Span::from("Named task ")]) diff --git a/codex-rs/tui/src/snapshots/codex_tui__resume_picker__tests__resume_picker_db_fallback_transition.snap b/codex-rs/tui/src/snapshots/codex_tui__resume_picker__tests__resume_picker_db_fallback_transition.snap index 61158efc1a..538c644b64 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__resume_picker__tests__resume_picker_db_fallback_transition.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__resume_picker__tests__resume_picker_db_fallback_transition.snap @@ -5,8 +5,8 @@ expression: "format!(\"---- loading fallback ----\\n{loading_snapshot}\\n---- re ---- loading fallback ---- Loading sessions… ---- repaired results ---- -❯ first store result - 1d ago ⌁ no cwd  no branch +› first store result + 1d ago ⌁ no cwd  no branch repaired store result 1d ago ⌁ no cwd  no branch diff --git a/codex-rs/tui/src/snapshots/codex_tui__resume_picker__tests__resume_picker_dense_all.snap b/codex-rs/tui/src/snapshots/codex_tui__resume_picker__tests__resume_picker_dense_all.snap index 62cb8d199b..6ebc20a8a2 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__resume_picker__tests__resume_picker_dense_all.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__resume_picker__tests__resume_picker_dense_all.snap @@ -2,4 +2,4 @@ source: tui/src/resume_picker.rs expression: "render_dense_row_snapshot(true, None, 120,)" --- -❯ 15m ago Propose session picker redesign with enough title text to exercise truncation +› 15m ago Propose session picker redesign with enough title text to exercise truncation diff --git a/codex-rs/tui/src/snapshots/codex_tui__resume_picker__tests__resume_picker_dense_all_auto_hidden_cwd.snap b/codex-rs/tui/src/snapshots/codex_tui__resume_picker__tests__resume_picker_dense_all_auto_hidden_cwd.snap index 94f74b55f3..89b2ec29cd 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__resume_picker__tests__resume_picker_dense_all_auto_hidden_cwd.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__resume_picker__tests__resume_picker_dense_all_auto_hidden_cwd.snap @@ -2,4 +2,4 @@ source: tui/src/resume_picker.rs expression: "render_dense_row_snapshot(true, None, 100,)" --- -❯ 15m ago Propose session picker redesign with enough title text to exercise truncation +› 15m ago Propose session picker redesign with enough title text to exercise truncation diff --git a/codex-rs/tui/src/snapshots/codex_tui__resume_picker__tests__resume_picker_dense_all_forced_cwd.snap b/codex-rs/tui/src/snapshots/codex_tui__resume_picker__tests__resume_picker_dense_all_forced_cwd.snap index 9b8c39ef50..3b25001697 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__resume_picker__tests__resume_picker_dense_all_forced_cwd.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__resume_picker__tests__resume_picker_dense_all_forced_cwd.snap @@ -2,4 +2,4 @@ source: tui/src/resume_picker.rs expression: "render_dense_row_snapshot(true, None, 48,)" --- -❯ 15m ago Propose session picker redesig... +› 15m ago Propose session picker redesig... diff --git a/codex-rs/tui/src/snapshots/codex_tui__resume_picker__tests__resume_picker_dense_cwd.snap b/codex-rs/tui/src/snapshots/codex_tui__resume_picker__tests__resume_picker_dense_cwd.snap index 64120ef26a..d68f265a30 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__resume_picker__tests__resume_picker_dense_cwd.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__resume_picker__tests__resume_picker_dense_cwd.snap @@ -2,4 +2,4 @@ source: tui/src/resume_picker.rs expression: "render_dense_row_snapshot(false,\nSome(PathBuf::from(\"/Users/felipe.coury/code/codex.fcoury-session-picker/codex-rs\")),\n100,)" --- -❯ 15m ago Propose session picker redesign with enough title text to exercise truncation +› 15m ago Propose session picker redesign with enough title text to exercise truncation diff --git a/codex-rs/tui/src/snapshots/codex_tui__resume_picker__tests__resume_picker_dense_narrow.snap b/codex-rs/tui/src/snapshots/codex_tui__resume_picker__tests__resume_picker_dense_narrow.snap index 9b8c39ef50..3b25001697 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__resume_picker__tests__resume_picker_dense_narrow.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__resume_picker__tests__resume_picker_dense_narrow.snap @@ -2,4 +2,4 @@ source: tui/src/resume_picker.rs expression: "render_dense_row_snapshot(true, None, 48,)" --- -❯ 15m ago Propose session picker redesig... +› 15m ago Propose session picker redesig... diff --git a/codex-rs/tui/src/snapshots/codex_tui__resume_picker__tests__resume_picker_dense_no_blank_lines.snap b/codex-rs/tui/src/snapshots/codex_tui__resume_picker__tests__resume_picker_dense_no_blank_lines.snap index 50e37a46b9..9ab736d3c1 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__resume_picker__tests__resume_picker_dense_no_blank_lines.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__resume_picker__tests__resume_picker_dense_no_blank_lines.snap @@ -3,4 +3,4 @@ source: tui/src/resume_picker.rs expression: terminal.backend().to_string() --- 15m ago First dense row -❯ 15m ago Second dense row +› 15m ago Second dense row diff --git a/codex-rs/tui/src/snapshots/codex_tui__resume_picker__tests__resume_picker_linked_worktree.snap b/codex-rs/tui/src/snapshots/codex_tui__resume_picker__tests__resume_picker_linked_worktree.snap index 12b962d6e9..9232324d38 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__resume_picker__tests__resume_picker_linked_worktree.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__resume_picker__tests__resume_picker_linked_worktree.snap @@ -2,5 +2,5 @@ source: tui/src/resume_picker.rs expression: "render_picker_list(&state, 100, 12)" --- -❯ Linked session +› Linked session 1d ago ⌁ /repo/linked  no branch diff --git a/codex-rs/tui/src/snapshots/codex_tui__resume_picker__tests__resume_picker_more_indicators.snap b/codex-rs/tui/src/snapshots/codex_tui__resume_picker__tests__resume_picker_more_indicators.snap index 76a645aa07..a107938c7a 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__resume_picker__tests__resume_picker_more_indicators.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__resume_picker__tests__resume_picker_more_indicators.snap @@ -3,8 +3,8 @@ source: tui/src/resume_picker.rs expression: terminal.backend().to_string() --- ↑ more -❯ item-2 - 10m ago ⌁ no cwd  no branch +› item-2 + 10m ago ⌁ no cwd  no branch item-3 ↓ more diff --git a/codex-rs/tui/src/snapshots/codex_tui__resume_picker__tests__resume_picker_narrow_session.snap b/codex-rs/tui/src/snapshots/codex_tui__resume_picker__tests__resume_picker_narrow_session.snap index db583b9957..a8ef2f3a7d 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__resume_picker__tests__resume_picker_narrow_session.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__resume_picker__tests__resume_picker_narrow_session.snap @@ -2,6 +2,6 @@ source: tui/src/resume_picker.rs expression: terminal.backend().to_string() --- -❯ Investigate picker expansion - 15m ago ⌁ /tmp/codex +› Investigate picker expansion + 15m ago ⌁ /tmp/codex  fcoury/session-picker diff --git a/codex-rs/tui/src/snapshots/codex_tui__resume_picker__tests__resume_picker_search_line_sort_filter_tabs.snap b/codex-rs/tui/src/snapshots/codex_tui__resume_picker__tests__resume_picker_search_line_sort_filter_tabs.snap index 7d9be2926d..3e250ca77f 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__resume_picker__tests__resume_picker_search_line_sort_filter_tabs.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__resume_picker__tests__resume_picker_search_line_sort_filter_tabs.snap @@ -2,4 +2,4 @@ source: tui/src/resume_picker.rs expression: terminal.backend().to_string() --- -Type to search Filter: [Cwd] All Status: [Active] Archived Sort: [Updated] Created +Filter: Cwd All Status: Active Archived Sort: Updated Created diff --git a/codex-rs/tui/src/snapshots/codex_tui__resume_picker__tests__resume_picker_table.snap b/codex-rs/tui/src/snapshots/codex_tui__resume_picker__tests__resume_picker_table.snap index b882050d58..85103e4c63 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__resume_picker__tests__resume_picker_table.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__resume_picker__tests__resume_picker_table.snap @@ -5,8 +5,8 @@ expression: snapshot Fix resume picker timestamps 42s ago ⌁ no cwd  no branch -❯ Investigate lazy pagination cap - 35m ago ⌁ no cwd  no branch +› Investigate lazy pagination cap + 35m ago ⌁ no cwd  no branch Explain the codebase 2h ago ⌁ no cwd  no branch diff --git a/codex-rs/tui/src/snapshots/codex_tui__resume_picker__tests__resume_picker_transcript_loading_overlay.snap b/codex-rs/tui/src/snapshots/codex_tui__resume_picker__tests__resume_picker_transcript_loading_overlay.snap index fa6b47cbbd..470e992163 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__resume_picker__tests__resume_picker_transcript_loading_overlay.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__resume_picker__tests__resume_picker_transcript_loading_overlay.snap @@ -2,7 +2,7 @@ source: tui/src/resume_picker.rs expression: snapshot --- -❯ Find pending threads and emails +› Find pending threads and emails - ⌁ no cwd  no branch Plan raw scrollback mod Loading transcript… diff --git a/codex-rs/tui/src/startup_hooks_review.rs b/codex-rs/tui/src/startup_hooks_review.rs index a1ab4b3cb8..160253586e 100644 --- a/codex-rs/tui/src/startup_hooks_review.rs +++ b/codex-rs/tui/src/startup_hooks_review.rs @@ -262,7 +262,6 @@ fn selection_view_params( } SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, footer_hint: Some(hints.into()), items: vec![ selection_item("Review hooks", trusting_all), diff --git a/codex-rs/tui/src/theme_picker.rs b/codex-rs/tui/src/theme_picker.rs index 8cbabdaa5f..562997efee 100644 --- a/codex-rs/tui/src/theme_picker.rs +++ b/codex-rs/tui/src/theme_picker.rs @@ -388,7 +388,6 @@ pub(crate) fn build_theme_picker_params( }) as Box, ); SelectionViewParams { - appearance: crate::bottom_pane::SelectionAppearance::Picker, title: Some("Select Syntax Theme".to_string()), subtitle: Some(theme_picker_subtitle( codex_home_owned.as_deref(),