diff --git a/codex-rs/tui/src/bottom_pane/command_popup.rs b/codex-rs/tui/src/bottom_pane/command_popup.rs index 8de266f8..a492a2e1 100644 --- a/codex-rs/tui/src/bottom_pane/command_popup.rs +++ b/codex-rs/tui/src/bottom_pane/command_popup.rs @@ -95,32 +95,10 @@ impl CommandPopup { /// Determine the preferred height of the popup for a given width. /// Accounts for wrapped descriptions so that long tooltips don't overflow. pub(crate) fn calculate_required_height(&self, width: u16) -> u16 { - use super::selection_popup_common::GenericDisplayRow; use super::selection_popup_common::measure_rows_height; - let matches = self.filtered(); - let rows_all: Vec = if matches.is_empty() { - Vec::new() - } else { - matches - .into_iter() - .map(|(item, indices, _)| match item { - CommandItem::Builtin(cmd) => GenericDisplayRow { - name: format!("/{}", cmd.command()), - match_indices: indices.map(|v| v.into_iter().map(|i| i + 1).collect()), - is_current: false, - description: Some(cmd.description().to_string()), - }, - CommandItem::UserPrompt(i) => GenericDisplayRow { - name: format!("/{}", self.prompts[i].name), - match_indices: indices.map(|v| v.into_iter().map(|i| i + 1).collect()), - is_current: false, - description: Some("send saved prompt".to_string()), - }, - }) - .collect() - }; + let rows = self.rows_from_matches(self.filtered()); - measure_rows_height(&rows_all, &self.state, MAX_POPUP_ROWS, width) + measure_rows_height(&rows, &self.state, MAX_POPUP_ROWS, width) } /// Compute fuzzy-filtered matches over built-in commands and user prompts, @@ -172,6 +150,32 @@ impl CommandPopup { self.filtered().into_iter().map(|(c, _, _)| c).collect() } + fn rows_from_matches( + &self, + matches: Vec<(CommandItem, Option>, i32)>, + ) -> Vec { + matches + .into_iter() + .map(|(item, indices, _)| { + let (name, description) = match item { + CommandItem::Builtin(cmd) => { + (format!("/{}", cmd.command()), cmd.description().to_string()) + } + CommandItem::UserPrompt(i) => ( + format!("/{}", self.prompts[i].name), + "send saved prompt".to_string(), + ), + }; + GenericDisplayRow { + name, + match_indices: indices.map(|v| v.into_iter().map(|i| i + 1).collect()), + is_current: false, + description: Some(description), + } + }) + .collect() + } + /// Move the selection cursor one step up. pub(crate) fn move_up(&mut self) { let len = self.filtered_items().len(); @@ -198,32 +202,11 @@ impl CommandPopup { impl WidgetRef for CommandPopup { fn render_ref(&self, area: Rect, buf: &mut Buffer) { - let matches = self.filtered(); - let rows_all: Vec = if matches.is_empty() { - Vec::new() - } else { - matches - .into_iter() - .map(|(item, indices, _)| match item { - CommandItem::Builtin(cmd) => GenericDisplayRow { - name: format!("/{}", cmd.command()), - match_indices: indices.map(|v| v.into_iter().map(|i| i + 1).collect()), - is_current: false, - description: Some(cmd.description().to_string()), - }, - CommandItem::UserPrompt(i) => GenericDisplayRow { - name: format!("/{}", self.prompts[i].name), - match_indices: indices.map(|v| v.into_iter().map(|i| i + 1).collect()), - is_current: false, - description: Some("send saved prompt".to_string()), - }, - }) - .collect() - }; + let rows = self.rows_from_matches(self.filtered()); render_rows( area, buf, - &rows_all, + &rows, &self.state, MAX_POPUP_ROWS, false, diff --git a/codex-rs/tui/src/bottom_pane/selection_popup_common.rs b/codex-rs/tui/src/bottom_pane/selection_popup_common.rs index 61a26f93..684924a4 100644 --- a/codex-rs/tui/src/bottom_pane/selection_popup_common.rs +++ b/codex-rs/tui/src/bottom_pane/selection_popup_common.rs @@ -15,6 +15,7 @@ use ratatui::widgets::Paragraph; use ratatui::widgets::Widget; use super::scroll_state::ScrollState; +use crate::ui_consts::LIVE_PREFIX_COLS; /// A generic representation of a display row for selection popups. pub(crate) struct GenericDisplayRow { @@ -99,14 +100,34 @@ pub(crate) fn render_rows( .border_style(Style::default().add_modifier(Modifier::DIM)); block.render(area, buf); - // Content renders to the right of the border. + // Content renders to the right of the border with the same live prefix + // padding used by the composer so the popup aligns with the input text. + let prefix_cols = LIVE_PREFIX_COLS; let content_area = Rect { - x: area.x.saturating_add(1), + x: area.x.saturating_add(prefix_cols), y: area.y, - width: area.width.saturating_sub(1), + width: area.width.saturating_sub(prefix_cols), height: area.height, }; + // Clear the padding column(s) so stale characters never peek between the + // border and the popup contents. + let padding_cols = prefix_cols.saturating_sub(1); + if padding_cols > 0 { + let pad_start = area.x.saturating_add(1); + let pad_end = pad_start + .saturating_add(padding_cols) + .min(area.x.saturating_add(area.width)); + let pad_bottom = area.y.saturating_add(area.height); + for x in pad_start..pad_end { + for y in area.y..pad_bottom { + if let Some(cell) = buf.cell_mut((x, y)) { + cell.set_symbol(" "); + } + } + } + } + if rows_all.is_empty() { if content_area.height > 0 { let para = Paragraph::new(Line::from(empty_message.dim().italic())); diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__slash_popup_mo.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__slash_popup_mo.snap index f908fb61..9c667c0d 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__slash_popup_mo.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__slash_popup_mo.snap @@ -4,5 +4,5 @@ expression: terminal.backend() --- "▌ /mo " "▌ " -"▌/model choose what model and reasoning effort to use " -"▌/mention mention a file " +"▌ /model choose what model and reasoning effort to use " +"▌ /mention mention a file " 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 65606ed7..ced53b7c 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 @@ -5,7 +5,7 @@ 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 +▌ > 1. Read Only (current) Codex can read files +▌ 2. Full Access Codex can edit files 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_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 b42a5f8c..b9858a43 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 @@ -4,7 +4,7 @@ expression: render_lines(&view) --- ▌ Select Approval Mode ▌ -▌> 1. Read Only (current) Codex can read files -▌ 2. Full Access Codex can edit files +▌ > 1. Read Only (current) Codex can read files +▌ 2. Full Access Codex can edit files Press Enter to confirm or Esc to go back