From e28fa3e7a4481e1b196dd43c57bff96809f22b60 Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Sat, 19 Sep 2026 19:00:19 +0000 Subject: [PATCH] Unify TUI completion popup styling and preserve result identity (#46694) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Why Narrow completion popups can hide distinguishing filenames, parent paths, or category tags. Unified mentions also need a stable result viewport across searches and filters. ## What changed - Use shared selection styling and `›` markers across command, file, skill, and unified mention popups. - Give unified mentions a fixed-height picker layout with a header, filter tabs, query display, scroll indicators, and keyboard hints. - Bound mention name columns to leave room for parent paths and measure alignment in terminal cells. - Truncate file paths while retaining filenames and recalculating visible fuzzy highlights, preserving the full path for insertion. - Keep category tags visible when skill and app descriptions are hidden. ## Testing Add snapshots and assertions for narrow and clipped layouts, scrolling, empty filters, full-row selection styling, Unicode column alignment, long paths, preserved file selections, and category tags. GitOrigin-RevId: 54c34cc2472b43bac22011206efed3041eb77308 --- ...aits_for_the_pending_command_to_leave.snap | 2 +- codex-rs/tui/src/bottom_pane/command_popup.rs | 26 +-- .../tui/src/bottom_pane/file_search_popup.rs | 173 ++++++++++++--- .../src/bottom_pane/mentions_v2/candidate.rs | 2 +- .../tui/src/bottom_pane/mentions_v2/footer.rs | 88 ++------ .../tui/src/bottom_pane/mentions_v2/popup.rs | 7 +- .../tui/src/bottom_pane/mentions_v2/render.rs | 205 +++++++++++++----- .../bottom_pane/mentions_v2/render_tests.rs | 203 +++++++++++++++++ ...ts__bounded_filesystem_columns_narrow.snap | 7 + ...ests__bounded_filesystem_columns_wide.snap | 7 + ...tions_v2__render__tests__empty_filter.snap | 21 ++ ...ns_v2__render__tests__narrow_scrolled.snap | 21 ++ ...__mentions_v2__render__tests__one_row.snap | 5 + ...ne__mentions_v2__render__tests__short.snap | 12 + ...ane__mentions_v2__render__tests__wide.snap | 21 ++ .../src/bottom_pane/selection_popup_common.rs | 41 +--- .../src/bottom_pane/selection_row_layout.rs | 49 ++++- .../tui/src/bottom_pane/selection_tabs.rs | 14 +- codex-rs/tui/src/bottom_pane/skill_popup.rs | 90 +++++--- ..._tests__default_unified_mention_popup.snap | 13 +- ...e_shell_parameter_with_matching_skill.snap | 2 +- ...up_ignores_shell_positional_parameter.snap | 2 +- ...up_ignores_unbindable_qualified_skill.snap | 2 +- ...r__tests__mention_popup_type_prefixes.snap | 4 +- ...composer__tests__plugin_mention_popup.snap | 4 +- ...ill_popup_accepts_digit_leading_skill.snap | 4 +- ...ack_from_bound_skill_with_path_suffix.snap | 4 +- ...s_normal_target_after_ambiguous_probe.snap | 4 +- ...unbound_mention_left_of_bound_mention.snap | 4 +- ...ntion_right_of_adjacent_bound_mention.snap | 4 +- ..._chat_composer__tests__slash_popup_ar.snap | 2 +- ..._chat_composer__tests__slash_popup_bt.snap | 2 +- ..._chat_composer__tests__slash_popup_mo.snap | 2 +- ...chat_composer__tests__slash_popup_pet.snap | 2 +- ...chat_composer__tests__slash_popup_res.snap | 2 +- ..._chat_composer__tests__slash_popup_si.snap | 2 +- ...t_composer__tests__task_mention_popup.snap | 13 +- ...falls_back_from_bound_plugin_on_right.snap | 13 +- ...er__tests__vim_replace_slash_recovery.snap | 2 +- ...mmand_popup__tests__command_popup_app.snap | 9 +- ...p__tests__command_popup_default_items.snap | 1 + ...mmand_popup_filter_reset_after_scroll.snap | 9 +- ...and_popup__tests__command_popup_voice.snap | 8 +- ...p__tests__command_popup_wrap_boundary.snap | 28 +-- ...hing_filenames_and_original_selection.snap | 57 +++++ ...pup__tests__skill_popup_category_tags.snap | 114 ++++++++++ ...ests__skill_popup_lowercase_expansion.snap | 34 +-- ...ll_popup__tests__skill_popup_scrolled.snap | 26 ++- ...apps_mentions_only_callable_installed.snap | 6 +- ...idget__tests__worktrees_command_local.snap | 5 +- 50 files changed, 1037 insertions(+), 341 deletions(-) create mode 100644 codex-rs/tui/src/bottom_pane/mentions_v2/render_tests.rs create mode 100644 codex-rs/tui/src/bottom_pane/mentions_v2/snapshots/codex_tui__bottom_pane__mentions_v2__render__tests__bounded_filesystem_columns_narrow.snap create mode 100644 codex-rs/tui/src/bottom_pane/mentions_v2/snapshots/codex_tui__bottom_pane__mentions_v2__render__tests__bounded_filesystem_columns_wide.snap create mode 100644 codex-rs/tui/src/bottom_pane/mentions_v2/snapshots/codex_tui__bottom_pane__mentions_v2__render__tests__empty_filter.snap create mode 100644 codex-rs/tui/src/bottom_pane/mentions_v2/snapshots/codex_tui__bottom_pane__mentions_v2__render__tests__narrow_scrolled.snap create mode 100644 codex-rs/tui/src/bottom_pane/mentions_v2/snapshots/codex_tui__bottom_pane__mentions_v2__render__tests__one_row.snap create mode 100644 codex-rs/tui/src/bottom_pane/mentions_v2/snapshots/codex_tui__bottom_pane__mentions_v2__render__tests__short.snap create mode 100644 codex-rs/tui/src/bottom_pane/mentions_v2/snapshots/codex_tui__bottom_pane__mentions_v2__render__tests__wide.snap create mode 100644 codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__file_search_popup__tests__long_paths_keep_distinguishing_filenames_and_original_selection.snap create mode 100644 codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__skill_popup__tests__skill_popup_category_tags.snap diff --git a/codex-rs/tui/src/bottom_pane/chat_composer/snapshots/codex_tui__bottom_pane__chat_composer__sparkle__tests__a_fresh_astra_response_waits_for_the_pending_command_to_leave.snap b/codex-rs/tui/src/bottom_pane/chat_composer/snapshots/codex_tui__bottom_pane__chat_composer__sparkle__tests__a_fresh_astra_response_waits_for_the_pending_command_to_leave.snap index 60125b16ac..3f714c9f94 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer/snapshots/codex_tui__bottom_pane__chat_composer__sparkle__tests__a_fresh_astra_response_waits_for_the_pending_command_to_leave.snap +++ b/codex-rs/tui/src/bottom_pane/chat_composer/snapshots/codex_tui__bottom_pane__chat_composer__sparkle__tests__a_fresh_astra_response_waits_for_the_pending_command_to_leave.snap @@ -6,7 +6,7 @@ command during startup: › /status - /status show current session configuration and token usage +› /status show current session configuration and token usage /statusline configure which items appear in the status line after command: diff --git a/codex-rs/tui/src/bottom_pane/command_popup.rs b/codex-rs/tui/src/bottom_pane/command_popup.rs index 618a6e99c1..51760df392 100644 --- a/codex-rs/tui/src/bottom_pane/command_popup.rs +++ b/codex-rs/tui/src/bottom_pane/command_popup.rs @@ -2,6 +2,7 @@ use ratatui::buffer::Buffer; use ratatui::layout::Rect; use ratatui::widgets::WidgetRef; +use super::picker_style::selection_style; use super::popup_consts::MAX_POPUP_ROWS; use super::scroll_state::ScrollState; use super::selection_popup_common::ColumnWidthConfig; @@ -13,15 +14,12 @@ use super::slash_commands::BuiltinCommandFlags; use super::slash_commands::ServiceTierCommand; use super::slash_commands::SlashCommandItem; use super::slash_commands::commands_for_input; -use crate::render::Insets; -use crate::render::RectExt; use crate::slash_command::SlashCommand; // Hide alias commands in the default popup list so each unique action appears once. // `quit` is an alias of `exit`, and `btw` is an alias of `side`, so we skip // those aliases here. const ALIAS_COMMANDS: &[SlashCommand] = &[SlashCommand::Quit, SlashCommand::Btw]; -const COMMAND_LEFT_INSET: u16 = 2; const COMMAND_COLUMN_WIDTH: ColumnWidthConfig = ColumnWidthConfig::new( ColumnWidthMode::AutoAllRows, /*name_column_width*/ None, @@ -138,7 +136,7 @@ impl CommandPopup { &rows, &self.state, MAX_POPUP_ROWS, - width.saturating_sub(COMMAND_LEFT_INSET), + width, COMMAND_COLUMN_WIDTH, ) } @@ -206,17 +204,22 @@ impl CommandPopup { ) -> Vec { matches .into_iter() - .map(|(item, indices)| { + .enumerate() + .map(|(index, (item, indices))| { let name = format!("/{}", item.command()); let description = item.description().to_string(); GenericDisplayRow { - selection_style: None, + category_tag: None, name, - name_prefix_spans: Vec::new(), + name_prefix_spans: vec![if self.state.selected_idx == Some(index) { + "› ".into() + } else { + " ".into() + }], + selection_style: Some(selection_style()), match_indices: indices.map(|v| v.into_iter().map(|i| i + 1).collect()), display_shortcut: None, description: Some(description), - category_tag: None, wrap_indent: None, is_disabled: false, disabled_reason: None, @@ -269,12 +272,7 @@ impl WidgetRef for CommandPopup { fn render_ref(&self, area: Rect, buf: &mut Buffer) { let rows = self.rows_from_matches(self.filtered()); render_rows_with_col_width_mode( - area.inset(Insets::tlbr( - /*top*/ 0, - COMMAND_LEFT_INSET, - /*bottom*/ 0, - /*right*/ 0, - )), + area, buf, &rows, &self.state, diff --git a/codex-rs/tui/src/bottom_pane/file_search_popup.rs b/codex-rs/tui/src/bottom_pane/file_search_popup.rs index 0982826470..e311f4809c 100644 --- a/codex-rs/tui/src/bottom_pane/file_search_popup.rs +++ b/codex-rs/tui/src/bottom_pane/file_search_popup.rs @@ -1,17 +1,22 @@ +//! Display file completions while preserving the full path used for insertion. +//! Narrow rows retain the filename; fuzzy highlights refer to the displayed text. + use std::path::PathBuf; use codex_file_search::FileMatch; +use codex_utils_fuzzy_match::fuzzy_match; use ratatui::buffer::Buffer; use ratatui::layout::Rect; +use ratatui::text::Line; use ratatui::widgets::WidgetRef; -use crate::render::Insets; -use crate::render::RectExt; - +use super::picker_rows::render_rows_single_line; use super::popup_consts::MAX_POPUP_ROWS; use super::scroll_state::ScrollState; use super::selection_popup_common::GenericDisplayRow; -use super::selection_popup_common::render_rows; +use crate::line_truncation::truncate_line_with_ellipsis_if_overflow; +use crate::text_formatting::center_truncate_path; +use crate::width::display_width; /// Visual state for the file-search popup. pub(crate) struct FileSearchPopup { @@ -105,7 +110,7 @@ impl FileSearchPopup { // up to MAX_RESULTS regardless of the waiting flag so the list // remains stable while a newer search is in-flight. - self.matches.len().clamp(1, MAX_POPUP_ROWS) as u16 + self.matches.len().clamp(/*min*/ 1, MAX_POPUP_ROWS) as u16 + 2 } } @@ -117,34 +122,69 @@ impl WidgetRef for &FileSearchPopup { } else { self.matches .iter() - .map(|m| GenericDisplayRow { - selection_style: None, - name: m.path.to_string_lossy().to_string(), - name_prefix_spans: Vec::new(), - match_indices: m - .indices - .as_ref() - .map(|v| v.iter().map(|&i| i as usize).collect()), - display_shortcut: None, - description: None, - category_tag: None, - wrap_indent: None, - is_disabled: false, - disabled_reason: None, + .enumerate() + .map(|(idx, m)| { + let path = m.path.to_string_lossy(); + let width = usize::from(area.width.saturating_sub(/*rhs*/ 2)); + let name = if display_width(&path) <= width { + path.to_string() + } else { + let filename = m + .path + .file_name() + .unwrap_or(m.path.as_os_str()) + .to_string_lossy(); + let filename_width = display_width(&filename); + if let Some(parent) = m.path.parent() + && filename_width + 2 <= width + { + let parent = truncate_line_with_ellipsis_if_overflow( + Line::from(parent.to_string_lossy().into_owned()), + width - filename_width - 1, + ); + format!("{parent}{}{filename}", std::path::MAIN_SEPARATOR) + } else { + center_truncate_path(&filename, width) + } + }; + let match_indices = if name == path { + m.indices + .as_ref() + .map(|indices| indices.iter().map(|&index| index as usize).collect()) + } else { + fuzzy_match(&name, &self.display_query).map(|(indices, _)| indices) + }; + GenericDisplayRow { + category_tag: None, + selection_style: Some(super::picker_style::selection_style()), + name, + name_prefix_spans: vec![ + if self.state.selected_idx == Some(idx) { + "› " + } else { + " " + } + .into(), + ], + match_indices, + display_shortcut: None, + description: None, + wrap_indent: None, + is_disabled: false, + disabled_reason: None, + } }) .collect() }; let empty_message = if self.waiting { - "loading..." + " loading..." } else { - "no matches" + " no matches" }; - render_rows( - area.inset(Insets::tlbr( - /*top*/ 0, /*left*/ 2, /*bottom*/ 0, /*right*/ 0, - )), + render_rows_single_line( + area, buf, &rows_all, &self.state, @@ -170,6 +210,87 @@ mod tests { } } + #[test] + fn long_paths_keep_distinguishing_filenames_and_original_selection() { + let mut snapshots = Vec::new(); + for components in [ + vec!["src", "shared", "long_directory"], + vec!["long_directory_name_that_fills_row"], + vec!["長いディレクトリ名", "e\u{301}tudes"], + ] { + let parent: PathBuf = components.iter().collect(); + let paths: Vec = ["parser_alpha.rs", "parser_beta.rs"] + .into_iter() + .map(|name| parent.join(name)) + .collect(); + let mut popup = FileSearchPopup::new(); + popup.set_query("parser"); + popup.set_matches( + "parser", + paths + .iter() + .enumerate() + .map(|(index, path)| { + let mut matched = file_match(index); + matched.path = path.clone(); + matched.indices = + fuzzy_match(&path.to_string_lossy(), "parser").map(|(indices, _)| { + indices.into_iter().map(|index| index as u32).collect() + }); + matched + }) + .collect(), + ); + for (selected, path) in paths.iter().enumerate() { + for width in [28, 40, 80] { + let area = Rect::new( + /*x*/ 0, + /*y*/ 0, + width, + popup.calculate_required_height(), + ); + let mut buf = Buffer::empty(area); + (&popup).render_ref(area, &mut buf); + let text = buf + .content + .chunks(usize::from(width)) + .map(|row| { + row.iter() + .map(ratatui::buffer::Cell::symbol) + .collect::() + .trim_end() + .replace(std::path::MAIN_SEPARATOR, "/") + }) + .collect::>() + .join("\n"); + assert!( + text.contains("parser_alpha.rs") && text.contains("parser_beta.rs"), + "{text}" + ); + assert_eq!(popup.selected_match(), Some(path)); + let unselected_y = if selected == 0 { 2 } else { 1 }; + let parser_x = (0..width.saturating_sub(/*rhs*/ 5)) + .find(|&x| { + (x..x + 6) + .map(|column| buf[(column, unselected_y)].symbol()) + .eq(["p", "a", "r", "s", "e", "r"]) + }) + .expect("visible filename match"); + assert!( + buf[(parser_x, unselected_y)] + .modifier + .contains(ratatui::style::Modifier::BOLD) + ); + if selected == 0 { + snapshots.push(format!("{components:?}, width {width}\n{text}")); + } + } + popup.move_down(); + } + } + insta::assert_snapshot!(snapshots.join("\n\n")); + } + #[test] fn set_matches_keeps_only_the_first_page_of_results() { let mut popup = FileSearchPopup::new(); @@ -180,6 +301,6 @@ mod tests { popup.matches, (0..MAX_POPUP_ROWS).map(file_match).collect::>() ); - assert_eq!(popup.calculate_required_height(), MAX_POPUP_ROWS as u16); + assert_eq!(popup.calculate_required_height(), MAX_POPUP_ROWS as u16 + 2); } } diff --git a/codex-rs/tui/src/bottom_pane/mentions_v2/candidate.rs b/codex-rs/tui/src/bottom_pane/mentions_v2/candidate.rs index bc869a4c46..420eeee0c5 100644 --- a/codex-rs/tui/src/bottom_pane/mentions_v2/candidate.rs +++ b/codex-rs/tui/src/bottom_pane/mentions_v2/candidate.rs @@ -31,7 +31,7 @@ impl MentionType { pub(super) fn span(self, base_style: Style) -> Span<'static> { let style = match self { - Self::Plugin => base_style.magenta(), + Self::Plugin => base_style.fg(crate::style::accent_color()), Self::Skill => base_style.dim(), Self::Task => base_style.cyan(), Self::File => base_style.cyan(), diff --git a/codex-rs/tui/src/bottom_pane/mentions_v2/footer.rs b/codex-rs/tui/src/bottom_pane/mentions_v2/footer.rs index 6200083cfb..3a2f9e60e1 100644 --- a/codex-rs/tui/src/bottom_pane/mentions_v2/footer.rs +++ b/codex-rs/tui/src/bottom_pane/mentions_v2/footer.rs @@ -1,3 +1,5 @@ +//! Keyboard help for the live mention popup; filter tabs live above the results. + use crossterm::event::KeyCode; use ratatui::buffer::Buffer; use ratatui::layout::Rect; @@ -8,74 +10,22 @@ use ratatui::widgets::Widget; use crate::key_hint; use crate::line_truncation::truncate_line_with_ellipsis_if_overflow; -use super::search_mode::SearchMode; - -pub(super) fn render_footer(area: Rect, buf: &mut Buffer, search_mode: SearchMode) { - let right_line = search_mode_indicator_line(search_mode); - let right_width = right_line.width() as u16; - let gap = u16::from(right_width > 0); - let left_width = area.width.saturating_sub(right_width).saturating_sub(gap); - let left_line = - truncate_line_with_ellipsis_if_overflow(footer_hint_line(), left_width as usize); - left_line.render( - Rect { - x: area.x, - y: area.y, - width: left_width, - height: 1, - }, - buf, - ); - if right_width > 0 && right_width <= area.width { - right_line.render( - Rect { - x: area.x + area.width - right_width, - y: area.y, - width: right_width, - height: 1, - }, - buf, - ); +pub(super) fn render_footer(area: Rect, buf: &mut Buffer) { + let mut spans = Vec::new(); + for (keys, label) in [ + (vec![KeyCode::Enter, KeyCode::Tab], " insert · "), + (vec![KeyCode::Esc], " close · "), + (vec![KeyCode::Up, KeyCode::Down], " select · "), + (vec![KeyCode::Left, KeyCode::Right], " filter"), + ] { + let keys = keys + .into_iter() + .map(|key| key_hint::plain(key).display_label()) + .collect::>() + .join("/"); + spans.extend(key_hint::key_label_spans(&keys)); + spans.push(label.dim()); } -} - -fn footer_hint_line() -> Line<'static> { - Line::from(vec![ - key_hint::plain(KeyCode::Enter).into(), - " insert · ".dim(), - key_hint::plain(KeyCode::Esc).into(), - " close · ".dim(), - key_hint::plain(KeyCode::Left).into(), - "/".dim(), - key_hint::plain(KeyCode::Right).into(), - " switch search modes".dim(), - ]) -} - -fn search_mode_indicator_line(active_search_mode: SearchMode) -> Line<'static> { - let modes = [ - SearchMode::Results, - SearchMode::FilesystemOnly, - SearchMode::Tools, - ]; - let mut spans = Vec::with_capacity(modes.len() * 2 - 1); - - for (index, search_mode) in modes.into_iter().enumerate() { - if index > 0 { - spans.push(" ".dim()); - } - - if search_mode == active_search_mode { - let label = format!("[{}]", search_mode.label()); - let span = match search_mode { - SearchMode::Results | SearchMode::FilesystemOnly => label.cyan().bold(), - SearchMode::Tools => label.magenta().bold(), - }; - spans.push(span); - } else { - spans.push(format!(" {} ", search_mode.label()).dim()); - } - } - - Line::from(spans) + let line = Line::from(spans); + truncate_line_with_ellipsis_if_overflow(line, usize::from(area.width)).render(area, buf); } diff --git a/codex-rs/tui/src/bottom_pane/mentions_v2/popup.rs b/codex-rs/tui/src/bottom_pane/mentions_v2/popup.rs index 7ebc8ebf07..ca242dae7e 100644 --- a/codex-rs/tui/src/bottom_pane/mentions_v2/popup.rs +++ b/codex-rs/tui/src/bottom_pane/mentions_v2/popup.rs @@ -1,3 +1,5 @@ +//! Own live mention candidates and asynchronous file search while reserving a stable picker viewport. + use codex_file_search::FileMatch; use ratatui::buffer::Buffer; use ratatui::layout::Rect; @@ -7,6 +9,7 @@ use super::candidate::Candidate; use super::candidate::SearchResult; use super::candidate::Selection; use super::filter::filtered_candidates; +use super::render::POPUP_HEIGHT; use super::render::render_popup; use super::search_mode::SearchMode; use crate::bottom_pane::popup_consts::MAX_POPUP_ROWS; @@ -92,8 +95,7 @@ impl Popup { } pub(crate) fn calculate_required_height(&self, _width: u16) -> u16 { - let visible = self.rows.len().clamp(1, MAX_POPUP_ROWS); - (visible as u16).saturating_add(2) + POPUP_HEIGHT } /// Rebuilds cached rows and keeps selection valid after search inputs change. @@ -120,6 +122,7 @@ impl WidgetRef for Popup { &self.state, self.file_search.empty_message(), self.search_mode, + &self.query, ); } } diff --git a/codex-rs/tui/src/bottom_pane/mentions_v2/render.rs b/codex-rs/tui/src/bottom_pane/mentions_v2/render.rs index 4d80a3590e..4cf35452eb 100644 --- a/codex-rs/tui/src/bottom_pane/mentions_v2/render.rs +++ b/codex-rs/tui/src/bottom_pane/mentions_v2/render.rs @@ -1,3 +1,9 @@ +//! Render unified mentions with the shared picker frame and the live result catalog. +//! +//! Reserve the same result viewport across filters and searches. Preserve the +//! filesystem columns, fuzzy matches and right-aligned type metadata. Bound the +//! primary column so long names cannot crowd out distinguishing parent paths. + use ratatui::buffer::Buffer; use ratatui::layout::Rect; use ratatui::style::Color; @@ -7,17 +13,26 @@ use ratatui::style::Stylize; use ratatui::text::Line; use ratatui::text::Span; use ratatui::widgets::Widget; +use unicode_width::UnicodeWidthStr; use crate::line_truncation::truncate_line_with_ellipsis_if_overflow; -use crate::style::accent_style; use super::candidate::MentionType; use super::candidate::SearchResult; use super::candidate::Selection; use super::footer::render_footer; use super::search_mode::SearchMode; +use crate::bottom_pane::picker_style::render_scroll_indicators; +use crate::bottom_pane::picker_style::selection_style; use crate::bottom_pane::popup_consts::MAX_POPUP_ROWS; use crate::bottom_pane::scroll_state::ScrollState; +use crate::bottom_pane::selection_picker_layout::PickerLayoutSizes; +use crate::bottom_pane::selection_picker_layout::picker_areas; +use crate::bottom_pane::selection_popup_common::RenderedRows; +use crate::bottom_pane::selection_tabs::render_filled_tab_bar; + +/// Two header lines, header/tab gaps, tabs, query, two overflow rows and help. +pub(super) const POPUP_HEIGHT: u16 = MAX_POPUP_ROWS as u16 + 9; pub(super) fn render_popup( area: Rect, @@ -26,36 +41,95 @@ pub(super) fn render_popup( state: &ScrollState, empty_message: &str, search_mode: SearchMode, + query: &str, ) { - let (list_area, hint_area) = if area.height > 2 { - let hint_area = Rect { - x: area.x, - y: area.y + area.height - 1, - width: area.width, - height: 1, - }; - let list_area = Rect { - x: area.x, - y: area.y, - width: area.width, - height: area.height - 2, - }; - (list_area, Some(hint_area)) - } else { - (area, None) - }; - - render_rows(list_area, buf, rows, state, empty_message); - - if let Some(hint_area) = hint_area { - let hint_area = Rect { - x: hint_area.x + 2, - y: hint_area.y, - width: hint_area.width.saturating_sub(2), - height: hint_area.height, - }; - render_footer(hint_area, buf, search_mode); + ratatui::widgets::Clear.render(area, buf); + if area.is_empty() { + return; } + // At very short heights, retain the selected result before adding controls. + if area.height < 4 { + render_rows(area, buf, rows, state, empty_message); + return; + } + + let content = Rect { + height: area.height - 1, + ..area + }; + let [header, _, tabs, _, search, above, list, below, _, _] = picker_areas( + content, + PickerLayoutSizes { + header: 2, + header_gap: 1, + tabs: 1, + search: 1, + rows: MAX_POPUP_ROWS as u16, + side: 0, + }, + ); + let inset = |rect: Rect| Rect { + x: rect.x.saturating_add(/*rhs*/ 2).min(rect.right()), + width: rect.width.saturating_sub(/*rhs*/ 4), + ..rect + }; + for (offset, line) in [ + Line::from("Mentions".bold()), + Line::from("Files, directories, plugins, skills and tasks".dim()), + ] + .into_iter() + .take(usize::from(header.height)) + .enumerate() + { + line.render( + inset(Rect { + y: header.y + offset as u16, + height: 1, + ..header + }), + buf, + ); + } + let modes = [ + SearchMode::Results, + SearchMode::FilesystemOnly, + SearchMode::Tools, + ]; + let active = modes + .iter() + .position(|mode| *mode == search_mode) + .unwrap_or_default(); + render_filled_tab_bar(&modes.map(SearchMode::label), active, inset(tabs), buf); + let query = if query.is_empty() { + "Type to search mentions" + } else { + query + }; + truncate_line_with_ellipsis_if_overflow( + Line::from(query.to_owned().dim()), + usize::from(inset(search).width), + ) + .render(inset(search), buf); + let rendered = render_rows(list, buf, rows, state, empty_message); + if above.height > 0 && below.height > 0 { + render_scroll_indicators( + Rect { + y: above.y, + height: below.bottom() - above.y, + ..area + }, + buf, + rendered, + ); + } + render_footer( + inset(Rect { + y: area.bottom() - 1, + height: 1, + ..area + }), + buf, + ); } fn render_rows( @@ -64,19 +138,22 @@ fn render_rows( rows: &[SearchResult], state: &ScrollState, empty_message: &str, -) { - if area.height == 0 { - return; +) -> RenderedRows { + if area.is_empty() { + return RenderedRows::default(); } if rows.is_empty() { Line::from(vec![" ".into(), empty_message.italic()]).render(area, buf); - return; + return RenderedRows { + lines: 1, + ..Default::default() + }; } - let visible_items = MAX_POPUP_ROWS - .min(rows.len()) - .min(area.height.max(1) as usize); - let mut start_idx = state.scroll_top.min(rows.len().saturating_sub(1)); + let visible_items = MAX_POPUP_ROWS.min(rows.len()).min(usize::from(area.height)); + let mut start_idx = state + .scroll_top + .min(rows.len().saturating_sub(visible_items)); if let Some(sel) = state.selected_idx { if sel < start_idx { start_idx = sel; @@ -91,18 +168,16 @@ fn render_rows( let mut cur_y = area.y; let primary_column_width = rows .iter() - .skip(start_idx) - .take(visible_items) .map(primary_text_width) .max() - .unwrap_or(0); + .unwrap_or(/*default*/ 0); for (idx, row) in rows.iter().enumerate().skip(start_idx).take(visible_items) { if cur_y >= area.y + area.height { break; } let selected = Some(idx) == state.selected_idx; - let line = build_line(row, selected, area.width as usize, primary_column_width); + let line = build_line(row, selected, usize::from(area.width), primary_column_width); line.render( Rect { x: area.x, @@ -112,7 +187,13 @@ fn render_rows( }, buf, ); - cur_y = cur_y.saturating_add(1); + cur_y = cur_y.saturating_add(/*rhs*/ 1); + } + RenderedRows { + lines: visible_items as u16, + items: visible_items, + has_above: start_idx > 0, + has_below: start_idx + visible_items < rows.len(), } } @@ -126,10 +207,13 @@ fn build_line( let dim_style = Style::default().dim(); let tag = row.mention_type.span(base_style); let tag_width = tag.width(); - let gutter = if selected { "> " } else { " " }; - let gutter_width = gutter.len(); + let gutter = if selected { "› " } else { " " }; + let gutter_width = UnicodeWidthStr::width(gutter); let content_width = width.saturating_sub(gutter_width.saturating_add(tag_width).saturating_add(2)); + // Keep room for secondary text even when an off-screen result has a long name. + let primary_column_width = + primary_column_width.min(content_width.saturating_sub(/*rhs*/ 2) / 2); let content = truncate_line_with_ellipsis_if_overflow( content_line(row, base_style, dim_style, primary_column_width), content_width, @@ -147,11 +231,16 @@ fn build_line( } spans.push(tag); if selected { - let style = accent_style(); + let style = selection_style(); spans.iter_mut().for_each(|span| span.style = style); } - Line::from(spans) + let line = Line::from(spans); + if selected { + line.style(selection_style()) + } else { + line + } } fn content_line( @@ -160,17 +249,19 @@ fn content_line( dim_style: Style, primary_column_width: usize, ) -> Line<'static> { - let mut spans = Vec::new(); - spans.extend(primary_spans(row, base_style)); + let primary = Line::from(primary_spans(row, base_style)); if let Some(secondary) = secondary_line(row, base_style, dim_style) { + let primary = truncate_line_with_ellipsis_if_overflow(primary, primary_column_width); let padding = primary_column_width - .saturating_sub(primary_text_width(row)) - .saturating_add(2); + .saturating_sub(primary.width()) + .saturating_add(/*rhs*/ 2); + let mut spans = primary.spans; spans.push(" ".repeat(padding).set_style(dim_style)); spans.extend(secondary.spans); + Line::from(spans) + } else { + primary } - - Line::from(spans) } fn primary_spans(row: &SearchResult, base_style: Style) -> Vec> { @@ -185,7 +276,7 @@ fn primary_spans(row: &SearchResult, base_style: Style) -> Vec> { let mut spans = Vec::with_capacity(row.display_name.len()); let name_style = match row.mention_type { - MentionType::Plugin => base_style.magenta(), + MentionType::Plugin => base_style.fg(crate::style::accent_color()), MentionType::Skill => base_style.dim(), MentionType::Task => base_style.cyan(), MentionType::File | MentionType::Directory => base_style, @@ -267,8 +358,8 @@ fn path_spans(row: &SearchResult, base_style: Style) -> Vec> { fn primary_text_width(row: &SearchResult) -> usize { file_name(row) - .map(|file_name| file_name.chars().count()) - .unwrap_or_else(|| row.display_name.chars().count()) + .map(UnicodeWidthStr::width) + .unwrap_or_else(|| UnicodeWidthStr::width(row.display_name.as_str())) } fn file_name(row: &SearchResult) -> Option<&str> { @@ -299,3 +390,7 @@ fn file_name_start(row: &SearchResult) -> usize { Selection::File(_) | Selection::Tool { .. } => usize::MAX, } } + +#[cfg(test)] +#[path = "render_tests.rs"] +mod tests; diff --git a/codex-rs/tui/src/bottom_pane/mentions_v2/render_tests.rs b/codex-rs/tui/src/bottom_pane/mentions_v2/render_tests.rs new file mode 100644 index 0000000000..3b60d48b25 --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/mentions_v2/render_tests.rs @@ -0,0 +1,203 @@ +//! Render the live mention frame at full, clipped and narrow dimensions. + +use super::*; +use pretty_assertions::assert_eq; + +fn rows() -> Vec { + let mut rows = (0..10) + .map(|index| SearchResult { + display_name: format!("Plugin {index:02}"), + description: Some("Tools and workflows".to_string()), + mention_type: MentionType::Plugin, + selection: Selection::Tool { + insert_text: format!("plugin-{index}"), + path: None, + }, + match_indices: Some(vec![0]), + score: 0, + }) + .collect::>(); + for name in ["src/界.rs", "src/plain.rs"] { + rows.push(SearchResult { + display_name: name.to_string(), + description: None, + mention_type: MentionType::File, + selection: Selection::File(name.into()), + match_indices: None, + score: 0, + }); + } + rows +} + +fn text(buffer: &Buffer) -> String { + (buffer.area.y..buffer.area.bottom()) + .map(|y| { + let mut line = String::new(); + let mut x = buffer.area.x; + while x < buffer.area.right() { + let symbol = buffer[(x, y)].symbol(); + line.push_str(symbol); + x += UnicodeWidthStr::width(symbol).max(/*other*/ 1) as u16; + } + line.trim_end().to_owned() + }) + .collect::>() + .join("\n") +} + +#[test] +fn live_picker_frame_snapshots() { + let rows = rows(); + for (name, width, height, selected) in [ + ("wide", 80, POPUP_HEIGHT, 0), + ("narrow_scrolled", 40, POPUP_HEIGHT, rows.len() - 1), + ("short", 40, 7, rows.len() - 1), + ("one_row", 40, 1, rows.len() - 1), + ] { + let mut buffer = Buffer::empty(Rect::new(/*x*/ 0, /*y*/ 0, width, height)); + render_popup( + buffer.area, + &mut buffer, + &rows, + &ScrollState { + selected_idx: Some(selected), + scroll_top: 0, + }, + "no matches", + SearchMode::Results, + "", + ); + insta::assert_snapshot!(name, text(&buffer)); + } +} + +#[test] +fn selection_covers_the_row_and_empty_filter_clears_previous_results() { + let mut buffer = Buffer::empty(Rect::new( + /*x*/ 0, + /*y*/ 0, + /*width*/ 80, + POPUP_HEIGHT, + )); + render_popup( + buffer.area, + &mut buffer, + &rows(), + &ScrollState { + selected_idx: Some(0), + scroll_top: 0, + }, + "no matches", + SearchMode::Results, + "", + ); + let selected_y = (0..buffer.area.height) + .find(|y| buffer[(0, *y)].symbol() == "›") + .expect("selected result"); + let selected_styles = (0..buffer.area.width) + .map(|x| { + let cell = &buffer[(x, selected_y)]; + (cell.fg, cell.bg, cell.modifier) + }) + .collect::>(); + let style = selection_style(); + let expected = ( + style.fg.unwrap_or(Color::Reset), + style.bg.unwrap_or(Color::Reset), + style.add_modifier, + ); + assert_eq!( + selected_styles, + vec![expected; usize::from(buffer.area.width)] + ); + + render_popup( + buffer.area, + &mut buffer, + &[], + &ScrollState::new(), + "loading...", + SearchMode::FilesystemOnly, + "new-query", + ); + insta::assert_snapshot!("empty_filter", text(&buffer)); + assert!(buffer.content.iter().all(|cell| { + cell.bg == Color::Reset + || cell.bg + == crate::bottom_pane::picker_style::active_tab_style() + .bg + .unwrap_or(Color::Reset) + })); +} + +#[test] +fn filesystem_columns_use_terminal_cell_widths() { + let rows = rows() + .into_iter() + .filter(|row| row.mention_type == MentionType::File) + .collect::>(); + let mut buffer = Buffer::empty(Rect::new( + /*x*/ 0, /*y*/ 0, /*width*/ 60, /*height*/ 2, + )); + render_rows( + buffer.area, + &mut buffer, + &rows, + &ScrollState::new(), + "no matches", + ); + let columns = (0..buffer.area.height) + .map(|y| { + (0..buffer.area.width - 3) + .find(|x| { + (0..4) + .map(|offset| buffer[(*x + offset, y)].symbol()) + .collect::() + == "src/" + }) + .expect("parent path") + }) + .collect::>(); + assert_eq!(columns, vec![12, 12]); +} + +#[test] +fn long_mentions_leave_room_for_distinguishing_parent_paths() { + let mut rows = rows(); + rows[0].display_name = "Long off-screen plugin name ".repeat(/*n*/ 16); + for name in [ + "src/界.rs", + "tests/界.rs", + "deep/a_very_long_filename_that_exceeds_the_column.rs", + ] { + rows.push(SearchResult { + display_name: name.to_string(), + description: None, + mention_type: MentionType::File, + selection: Selection::File(name.into()), + match_indices: None, + score: 0, + }); + } + let first_file = rows.len() - 3; + for (name, width) in [ + ("bounded_filesystem_columns_narrow", 40), + ("bounded_filesystem_columns_wide", 80), + ] { + let mut buffer = Buffer::empty(Rect::new( + /*x*/ 0, /*y*/ 0, width, /*height*/ 3, + )); + render_rows( + buffer.area, + &mut buffer, + &rows, + &ScrollState { + selected_idx: Some(first_file), + scroll_top: first_file, + }, + "no matches", + ); + insta::assert_snapshot!(name, text(&buffer)); + } +} diff --git a/codex-rs/tui/src/bottom_pane/mentions_v2/snapshots/codex_tui__bottom_pane__mentions_v2__render__tests__bounded_filesystem_columns_narrow.snap b/codex-rs/tui/src/bottom_pane/mentions_v2/snapshots/codex_tui__bottom_pane__mentions_v2__render__tests__bounded_filesystem_columns_narrow.snap new file mode 100644 index 0000000000..03394e26c1 --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/mentions_v2/snapshots/codex_tui__bottom_pane__mentions_v2__render__tests__bounded_filesystem_columns_narrow.snap @@ -0,0 +1,7 @@ +--- +source: tui/src/bottom_pane/mentions_v2/render_tests.rs +expression: text(&buffer) +--- +› 界.rs src/ File + 界.rs tests/ File + a_very_long_f… deep/ File diff --git a/codex-rs/tui/src/bottom_pane/mentions_v2/snapshots/codex_tui__bottom_pane__mentions_v2__render__tests__bounded_filesystem_columns_wide.snap b/codex-rs/tui/src/bottom_pane/mentions_v2/snapshots/codex_tui__bottom_pane__mentions_v2__render__tests__bounded_filesystem_columns_wide.snap new file mode 100644 index 0000000000..3f2cd5308e --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/mentions_v2/snapshots/codex_tui__bottom_pane__mentions_v2__render__tests__bounded_filesystem_columns_wide.snap @@ -0,0 +1,7 @@ +--- +source: tui/src/bottom_pane/mentions_v2/render_tests.rs +expression: text(&buffer) +--- +› 界.rs src/ File + 界.rs tests/ File + a_very_long_filename_that_exceeds… deep/ File diff --git a/codex-rs/tui/src/bottom_pane/mentions_v2/snapshots/codex_tui__bottom_pane__mentions_v2__render__tests__empty_filter.snap b/codex-rs/tui/src/bottom_pane/mentions_v2/snapshots/codex_tui__bottom_pane__mentions_v2__render__tests__empty_filter.snap new file mode 100644 index 0000000000..326e5718a1 --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/mentions_v2/snapshots/codex_tui__bottom_pane__mentions_v2__render__tests__empty_filter.snap @@ -0,0 +1,21 @@ +--- +source: tui/src/bottom_pane/mentions_v2/render_tests.rs +expression: text(&buffer) +--- + Mentions + Files, directories, plugins, skills and tasks + + All Results Filesystem Only Plugins + + new-query + + loading... + + + + + + + + + enter/tab insert · esc close · ↑/↓ select · ←/→ filter diff --git a/codex-rs/tui/src/bottom_pane/mentions_v2/snapshots/codex_tui__bottom_pane__mentions_v2__render__tests__narrow_scrolled.snap b/codex-rs/tui/src/bottom_pane/mentions_v2/snapshots/codex_tui__bottom_pane__mentions_v2__render__tests__narrow_scrolled.snap new file mode 100644 index 0000000000..27ea2abf85 --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/mentions_v2/snapshots/codex_tui__bottom_pane__mentions_v2__render__tests__narrow_scrolled.snap @@ -0,0 +1,21 @@ +--- +source: tui/src/bottom_pane/mentions_v2/render_tests.rs +expression: text(&buffer) +--- + Mentions + Files, directories, plugins, skills + + All Results Filesystem Only › + + Type to search mentions +↑ + Plugin 04 Tools and workflows Plugin + Plugin 05 Tools and workflows Plugin + Plugin 06 Tools and workflows Plugin + Plugin 07 Tools and workflows Plugin + Plugin 08 Tools and workflows Plugin + Plugin 09 Tools and workflows Plugin + 界.rs src/ File +› plain.rs src/ File + + enter/tab insert · esc close · ↑/↓ … diff --git a/codex-rs/tui/src/bottom_pane/mentions_v2/snapshots/codex_tui__bottom_pane__mentions_v2__render__tests__one_row.snap b/codex-rs/tui/src/bottom_pane/mentions_v2/snapshots/codex_tui__bottom_pane__mentions_v2__render__tests__one_row.snap new file mode 100644 index 0000000000..7be29dcfff --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/mentions_v2/snapshots/codex_tui__bottom_pane__mentions_v2__render__tests__one_row.snap @@ -0,0 +1,5 @@ +--- +source: tui/src/bottom_pane/mentions_v2/render_tests.rs +expression: text(&buffer) +--- +› plain.rs src/ File diff --git a/codex-rs/tui/src/bottom_pane/mentions_v2/snapshots/codex_tui__bottom_pane__mentions_v2__render__tests__short.snap b/codex-rs/tui/src/bottom_pane/mentions_v2/snapshots/codex_tui__bottom_pane__mentions_v2__render__tests__short.snap new file mode 100644 index 0000000000..c4242b5edb --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/mentions_v2/snapshots/codex_tui__bottom_pane__mentions_v2__render__tests__short.snap @@ -0,0 +1,12 @@ +--- +source: tui/src/bottom_pane/mentions_v2/render_tests.rs +assertion_line: 71 +expression: text(&buffer) +--- + Mentions + Files, directories, plugins, skills + All Results Filesystem Only › + Type to search mentions + 界.rs src/ File +› plain.rs src/ File + enter/tab insert · esc close · ↑/↓ … diff --git a/codex-rs/tui/src/bottom_pane/mentions_v2/snapshots/codex_tui__bottom_pane__mentions_v2__render__tests__wide.snap b/codex-rs/tui/src/bottom_pane/mentions_v2/snapshots/codex_tui__bottom_pane__mentions_v2__render__tests__wide.snap new file mode 100644 index 0000000000..c9475bd7c6 --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/mentions_v2/snapshots/codex_tui__bottom_pane__mentions_v2__render__tests__wide.snap @@ -0,0 +1,21 @@ +--- +source: tui/src/bottom_pane/mentions_v2/render_tests.rs +expression: text(&buffer) +--- + Mentions + Files, directories, plugins, skills and tasks + + All Results Filesystem Only Plugins + + Type to search mentions + +› Plugin 00 Tools and workflows Plugin + Plugin 01 Tools and workflows Plugin + Plugin 02 Tools and workflows Plugin + Plugin 03 Tools and workflows Plugin + Plugin 04 Tools and workflows Plugin + Plugin 05 Tools and workflows Plugin + Plugin 06 Tools and workflows Plugin + Plugin 07 Tools and workflows Plugin +↓ + enter/tab insert · esc close · ↑/↓ select · ←/→ filter 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 e99d16ddfa..c012f9dd49 100644 --- a/codex-rs/tui/src/bottom_pane/selection_popup_common.rs +++ b/codex-rs/tui/src/bottom_pane/selection_popup_common.rs @@ -38,6 +38,7 @@ pub(crate) struct GenericDisplayRow { pub display_shortcut: Option, pub match_indices: Option>, // indices to bold (char positions) pub description: Option, // optional grey text after the name + /// Required result identity, retained even when the description column hides. pub category_tag: Option, pub disabled_reason: Option, // optional disabled message pub is_disabled: bool, @@ -277,7 +278,7 @@ fn wrap_standard_row( use crate::wrapping::RtOptions; use crate::wrapping::word_wrap_line; - let full_line = build_full_line(row, desc_col, description_layout); + let full_line = build_full_line(row, desc_col, width, description_layout); let continuation_indent = wrap_indent(row, desc_col, width); let options = RtOptions::new(width.max(1) as usize) .initial_indent(Line::from("")) @@ -616,31 +617,6 @@ pub(crate) fn render_rows_with_col_width_mode( ) } -/// Render rows as a single line each (no wrapping), truncating overflow with an ellipsis. -/// -/// This path always uses viewport-local width alignment and is best for dense -/// list UIs where multi-line descriptions would add too much vertical churn. -/// Returns the number of terminal lines actually rendered. -pub(crate) fn render_rows_single_line( - area: Rect, - buf: &mut Buffer, - rows_all: &[GenericDisplayRow], - state: &ScrollState, - max_results: usize, - empty_message: &str, -) -> u16 { - render_rows_single_line_with_col_width_mode( - area, - buf, - rows_all, - state, - max_results, - empty_message, - ColumnWidthConfig::default(), - ) - .lines -} - /// Render a list of rows as a single line each (no wrapping), truncating overflow with an /// ellipsis while honoring the configured column width behavior. /// Returns the terminal lines and selectable items actually rendered. @@ -695,7 +671,8 @@ pub(crate) fn render_rows_single_line_with_col_width_mode( break; } - let mut full_line = build_full_line(row, desc_col, column_width.description_layout); + let mut full_line = + build_full_line(row, desc_col, area.width, column_width.description_layout); apply_row_state_style( std::slice::from_mut(&mut full_line), Some(i) == state.selected_idx && !row.is_disabled, @@ -957,8 +934,13 @@ mod tests { match_indices: match_index.map(|index| vec![index]), ..Default::default() }; - let text = - build_full_line(&row, desc_col, SelectionDescriptionLayout::Columns).to_string(); + let text = build_full_line( + &row, + desc_col, + /*width*/ 80, + SelectionDescriptionLayout::Columns, + ) + .to_string(); assert!(text.starts_with(expected), "unexpected row: {text:?}"); } @@ -987,6 +969,7 @@ mod tests { let line = build_full_line( row, /*desc_col*/ 4, + area.width, SelectionDescriptionLayout::Columns, ); let name = line.spans.first().expect("fuzzy-matched name span"); diff --git a/codex-rs/tui/src/bottom_pane/selection_row_layout.rs b/codex-rs/tui/src/bottom_pane/selection_row_layout.rs index 5dac95bb1d..e69083cfe5 100644 --- a/codex-rs/tui/src/bottom_pane/selection_row_layout.rs +++ b/codex-rs/tui/src/bottom_pane/selection_row_layout.rs @@ -118,27 +118,64 @@ fn append_shortcut(row: &GenericDisplayRow, spans: &mut Vec>) { pub(super) fn build_full_line( row: &GenericDisplayRow, desc_col: usize, + width: u16, description_layout: SelectionDescriptionLayout, ) -> Line<'static> { let description = (desc_col > 0) .then(|| combined_description(row, description_layout)) .flatten(); let name_prefix_width = line_width(&Line::from(row.name_prefix_spans.clone())); - let name_limit = description - .as_ref() - .map(|_| desc_col.saturating_sub(2).saturating_sub(name_prefix_width)) + // Category tags identify the result even when secondary descriptions hide. + // Reserve their cells before truncating the name on narrow rows. + let metadata_col = row + .category_tag + .as_deref() + .map(|tag| { + let desired_col = if desc_col > 0 { + desc_col + } else { + name_prefix_width + display_width(&row.name) + 2 + }; + desired_col.min(usize::from(width).saturating_sub(display_width(tag))) + }) + .or_else(|| description.as_ref().map(|_| desc_col)); + let name_limit = metadata_col + .map(|column| { + column + .saturating_sub(/*rhs*/ 2) + .saturating_sub(name_prefix_width) + }) .unwrap_or(usize::MAX); - let name_spans = build_name_spans(row, name_limit); + let name_limit = if row.category_tag.is_some() && display_width(&row.name) > name_limit { + name_limit.saturating_sub(/*rhs*/ 1) + } else { + name_limit + }; + let name_spans = if row.category_tag.is_some() + && metadata_col.is_some_and(|column| column <= name_prefix_width) + { + Vec::new() + } else { + build_name_spans(row, name_limit) + }; let name_width = name_prefix_width + line_width(&Line::from(name_spans.clone())); let mut spans = row.name_prefix_spans.clone(); spans.extend(name_spans); append_shortcut(row, &mut spans); - if let Some(description) = description { - let gap = desc_col.saturating_sub(name_width); + if let Some(metadata_col) = metadata_col { + let gap = metadata_col.saturating_sub(name_width); if gap > 0 { spans.push(" ".repeat(gap).into()); } + } + if let Some(tag) = &row.category_tag { + spans.push(tag.clone().dim()); + if description.is_some() { + spans.push(" ".dim()); + } + } + if let Some(description) = description { spans.push(description.dim()); } Line::from(spans) diff --git a/codex-rs/tui/src/bottom_pane/selection_tabs.rs b/codex-rs/tui/src/bottom_pane/selection_tabs.rs index 3ffd224abc..b7dc8b6f37 100644 --- a/codex-rs/tui/src/bottom_pane/selection_tabs.rs +++ b/codex-rs/tui/src/bottom_pane/selection_tabs.rs @@ -82,10 +82,15 @@ pub(super) fn render_tab_bar( } } -/// Render a fixed-height tab strip while keeping the active tab visible. -fn render_filled_tab_bar(labels: &[&str], active_idx: usize, area: Rect, buf: &mut Buffer) { +/// Render a fixed-height tab strip and return the visible tab hit targets. +pub(crate) fn render_filled_tab_bar( + labels: &[&str], + active_idx: usize, + area: Rect, + buf: &mut Buffer, +) -> Vec<(usize, Rect)> { if labels.is_empty() || area.is_empty() { - return; + return Vec::new(); } let active_idx = active_idx.min(labels.len() - 1); let widths = labels @@ -117,6 +122,7 @@ fn render_filled_tab_bar(labels: &[&str], active_idx: usize, area: Rect, buf: &m x += 2; } let right = area.right().saturating_sub(u16::from(show_right) * 2); + let mut regions = Vec::new(); for idx in start..end { let width = widths[idx].min(usize::from(right.saturating_sub(x))) as u16; let line = Line::from(format!(" {} ", labels[idx])); @@ -128,6 +134,7 @@ fn render_filled_tab_bar(labels: &[&str], active_idx: usize, area: Rect, buf: &m }; let tab = Rect::new(x, area.y, width, /*height*/ 1); line.render(tab, buf); + regions.push((idx, tab)); x = x.saturating_add(width).saturating_add(/*rhs*/ 1); } if show_right { @@ -141,6 +148,7 @@ fn render_filled_tab_bar(labels: &[&str], active_idx: usize, area: Rect, buf: &m buf, ); } + regions } fn tab_bar_lines(tabs: &[SelectionTab], active_idx: usize, width: u16) -> Vec> { diff --git a/codex-rs/tui/src/bottom_pane/skill_popup.rs b/codex-rs/tui/src/bottom_pane/skill_popup.rs index f7ca4639c2..0d12d10b5e 100644 --- a/codex-rs/tui/src/bottom_pane/skill_popup.rs +++ b/codex-rs/tui/src/bottom_pane/skill_popup.rs @@ -3,17 +3,16 @@ use ratatui::buffer::Buffer; use ratatui::layout::Constraint; use ratatui::layout::Layout; use ratatui::layout::Rect; +use ratatui::style::Stylize; use ratatui::text::Line; use ratatui::widgets::Widget; use ratatui::widgets::WidgetRef; +use super::picker_rows::render_rows_single_line; use super::popup_consts::MAX_POPUP_ROWS; use super::scroll_state::ScrollState; use super::selection_popup_common::GenericDisplayRow; -use super::selection_popup_common::render_rows_single_line; use crate::key_hint; -use crate::render::Insets; -use crate::render::RectExt; use crate::text_formatting::truncate_text; use codex_utils_fuzzy_match::fuzzy_match; @@ -88,7 +87,7 @@ impl SkillPopup { pub(crate) fn calculate_required_height(&self, _width: u16) -> u16 { let rows = self.rows_from_matches(self.filtered()); let visible = rows.len().clamp(1, MAX_POPUP_ROWS); - (visible as u16).saturating_add(2) + (visible as u16).saturating_add(/*rhs*/ 3) } pub(crate) fn move_up(&mut self) { @@ -126,30 +125,29 @@ impl SkillPopup { ) -> Vec { matches .into_iter() - .map(|(idx, indices, _score)| { + .enumerate() + .map(|(visible_idx, (idx, indices, _score))| { let mention = &self.mentions[idx]; let name = truncate_text(&mention.display_name, MENTION_NAME_TRUNCATE_LEN); - let description = match ( - mention.category_tag.as_deref(), - mention.description.as_deref(), - ) { - (Some(tag), Some(description)) if !description.is_empty() => { - Some(format!("{tag} {description}")) - } - (Some(tag), _) => Some(tag.to_string()), - (None, Some(description)) if !description.is_empty() => { - Some(description.to_string()) - } - _ => None, - }; GenericDisplayRow { - selection_style: None, + category_tag: mention.category_tag.clone(), + selection_style: Some(super::picker_style::selection_style()), name, - name_prefix_spans: Vec::new(), + name_prefix_spans: vec![ + if self.state.selected_idx == Some(visible_idx) { + "› " + } else { + " " + } + .into(), + ], match_indices: indices, display_shortcut: None, - description, - category_tag: None, + description: mention + .description + .as_ref() + .filter(|description| !description.is_empty()) + .cloned(), is_disabled: false, disabled_reason: None, wrap_indent: None, @@ -201,22 +199,17 @@ impl SkillPopup { impl WidgetRef for SkillPopup { fn render_ref(&self, area: Rect, buf: &mut Buffer) { - let (list_area, hint_area) = if area.height > 2 { - let [list_area, _spacer_area, hint_area] = Layout::vertical([ - Constraint::Length(area.height - 2), - Constraint::Length(1), - Constraint::Length(1), - ]) - .areas(area); + let (list_area, hint_area) = if area.height > 1 { + let [list_area, hint_area] = + Layout::vertical([Constraint::Length(area.height - 1), Constraint::Length(1)]) + .areas(area); (list_area, Some(hint_area)) } else { (area, None) }; let rows = self.rows_from_matches(self.filtered()); render_rows_single_line( - list_area.inset(Insets::tlbr( - /*top*/ 0, /*left*/ 2, /*bottom*/ 0, /*right*/ 0, - )), + list_area, buf, &rows, &self.state, @@ -237,11 +230,10 @@ impl WidgetRef for SkillPopup { fn skill_popup_hint_line() -> Line<'static> { Line::from(vec![ - "Press ".into(), key_hint::plain(KeyCode::Enter).into(), - " to insert or ".into(), + " insert · ".dim(), key_hint::plain(KeyCode::Esc).into(), - " to close".into(), + " close".dim(), ]) } @@ -310,7 +302,7 @@ mod tests { ); assert_eq!( popup.calculate_required_height(72), - (MAX_POPUP_ROWS as u16) + 2 + (MAX_POPUP_ROWS as u16) + 3 ); } @@ -332,6 +324,32 @@ mod tests { insta::assert_snapshot!("skill_popup_scrolled", render_popup(&popup, /*width*/ 72)); } + #[test] + fn category_tags_distinguish_mentions_when_descriptions_hide() { + let mut snapshots = Vec::new(); + for name in ["Shared", "Shared mention with a very long name"] { + let mut popup = SkillPopup::new( + [("[App]", 0), ("[Skill]", 1)] + .into_iter() + .map(|(tag, rank)| MentionItem { + description: Some("Secondary details".to_string()), + ..ranked_mention_item(name, &[], tag, rank) + }) + .collect(), + ); + popup.set_query("Shared"); + + for width in [32, 72] { + let rendered = render_popup(&popup, width); + assert!(rendered.contains("[App]"), "{rendered}"); + assert!(rendered.contains("[Skill]"), "{rendered}"); + assert_eq!(rendered.contains("Secondary details"), width == 72); + snapshots.push(format!("name={name:?}, width={width}\n{rendered}")); + } + } + insta::assert_snapshot!("skill_popup_category_tags", snapshots.join("\n\n")); + } + #[test] fn lowercase_expansion_preserves_match_ranking_and_highlighting() { let mut popup = SkillPopup::new(vec![ diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__default_unified_mention_popup.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__default_unified_mention_popup.snap index 015b3ec340..fba1b5e6a7 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__default_unified_mention_popup.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__default_unified_mention_popup.snap @@ -1,13 +1,14 @@ --- source: tui/src/bottom_pane/chat_composer.rs +assertion_line: 5348 expression: terminal.backend() --- " " "› @sa " " " -" " -"> Sample Plugin Plugin with skills and an MCP server Plugin" -" other (sample) Example skill used in tests. Skill " -" notes (sample) Example skill used in tests. Skill " -" " -" enter insert · esc close · ←/→ switch search modes [All Results] Filesystem Only Plugins " +" Mentions " +" Files, directories, plugins, skills and tasks " +" All Results Filesystem Only Plugins " +" sa " +"› Sample Plugin Plugin with skills and an MCP server Plugin" +" enter/tab insert · esc close · ↑/↓ select · ←/→ filter " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__file_popup_ignores_bare_shell_parameter_with_matching_skill.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__file_popup_ignores_bare_shell_parameter_with_matching_skill.snap index 679a00bc18..15e4b2654c 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__file_popup_ignores_bare_shell_parameter_with_matching_skill.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__file_popup_ignores_bare_shell_parameter_with_matching_skill.snap @@ -9,5 +9,5 @@ expression: terminal.backend() " " " " " " -" " " loading... " +" " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__file_popup_ignores_shell_positional_parameter.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__file_popup_ignores_shell_positional_parameter.snap index 7e77a86f00..5264bab854 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__file_popup_ignores_shell_positional_parameter.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__file_popup_ignores_shell_positional_parameter.snap @@ -9,5 +9,5 @@ expression: terminal.backend() " " " " " " -" " " loading... " +" " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__file_popup_ignores_unbindable_qualified_skill.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__file_popup_ignores_unbindable_qualified_skill.snap index a2a0a5fee0..c4336faea5 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__file_popup_ignores_unbindable_qualified_skill.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__file_popup_ignores_unbindable_qualified_skill.snap @@ -9,5 +9,5 @@ expression: terminal.backend() " " " " " " -" " " loading... " +" " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__mention_popup_type_prefixes.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__mention_popup_type_prefixes.snap index 8bfb4abf3c..cddd31e0b6 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__mention_popup_type_prefixes.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__mention_popup_type_prefixes.snap @@ -7,7 +7,7 @@ expression: terminal.backend() " " " " " " -" Google Calendar [Skill] Find availability and plan event changes " +"› Google Calendar [Skill] Find availability and plan event changes " " Google Calendar [App] Look up events and availability " " " -" Press enter to insert or esc to close " +" enter insert · esc close " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__plugin_mention_popup.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__plugin_mention_popup.snap index e46bc96cfa..4cca10a117 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__plugin_mention_popup.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__plugin_mention_popup.snap @@ -8,6 +8,6 @@ expression: terminal.backend() " " " " " " -" Sample Plugin [Plugin] Plugin that includes the Figma MCP server and Skills for common workflows " +"› Sample Plugin [Plugin] Plugin that includes the Figma MCP server and Skills for common workflows " " " -" Press enter to insert or esc to close " +" enter insert · esc close " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__skill_popup_accepts_digit_leading_skill.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__skill_popup_accepts_digit_leading_skill.snap index e53ca10d1f..b0d8d1b0e3 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__skill_popup_accepts_digit_leading_skill.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__skill_popup_accepts_digit_leading_skill.snap @@ -8,6 +8,6 @@ expression: terminal.backend() " " " " " " -" 1password [Skill] Example skill used in tests. " +"› 1password [Skill] Example skill used in tests. " " " -" Press enter to insert or esc to close " +" enter insert · esc close " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__skill_popup_falls_back_from_bound_skill_with_path_suffix.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__skill_popup_falls_back_from_bound_skill_with_path_suffix.snap index 63cdf4bccc..d0b91448a1 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__skill_popup_falls_back_from_bound_skill_with_path_suffix.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__skill_popup_falls_back_from_bound_skill_with_path_suffix.snap @@ -8,6 +8,6 @@ expression: terminal.backend() " " " " " " -" left [Skill] Example skill used in tests. " +"› left [Skill] Example skill used in tests. " " " -" Press enter to insert or esc to close " +" enter insert · esc close " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__skill_popup_preserves_normal_target_after_ambiguous_probe.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__skill_popup_preserves_normal_target_after_ambiguous_probe.snap index c641293b3a..cd5dbc868e 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__skill_popup_preserves_normal_target_after_ambiguous_probe.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__skill_popup_preserves_normal_target_after_ambiguous_probe.snap @@ -8,6 +8,6 @@ expression: terminal.backend() " " " " " " -" deploy (plugin) [Skill] Example skill used in tests. " +"› deploy (plugin) [Skill] Example skill used in tests. " " " -" Press enter to insert or esc to close " +" enter insert · esc close " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__skill_popup_targets_unbound_mention_left_of_bound_mention.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__skill_popup_targets_unbound_mention_left_of_bound_mention.snap index d2fe3b11e0..e803ced3fb 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__skill_popup_targets_unbound_mention_left_of_bound_mention.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__skill_popup_targets_unbound_mention_left_of_bound_mention.snap @@ -8,6 +8,6 @@ expression: terminal.backend() " " " " " " -" unbound-skill [Skill] Example skill used in tests. " +"› unbound-skill [Skill] Example skill used in tests. " " " -" Press enter to insert or esc to close " +" enter insert · esc close " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__skill_popup_targets_unbound_mention_right_of_adjacent_bound_mention.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__skill_popup_targets_unbound_mention_right_of_adjacent_bound_mention.snap index a3423c8bba..c14a9ab0ed 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__skill_popup_targets_unbound_mention_right_of_adjacent_bound_mention.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__skill_popup_targets_unbound_mention_right_of_adjacent_bound_mention.snap @@ -8,6 +8,6 @@ expression: terminal.backend() " " " " " " -" unbound-skill [Skill] Example skill used in tests. " +"› unbound-skill [Skill] Example skill used in tests. " " " -" Press enter to insert or esc to close " +" enter insert · esc close " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__slash_popup_ar.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__slash_popup_ar.snap index 9714636b39..733b55acee 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__slash_popup_ar.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__slash_popup_ar.snap @@ -6,4 +6,4 @@ expression: terminal.backend() "› /ar " " " " " -" /archive archive this session " +"› /archive archive this session " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__slash_popup_bt.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__slash_popup_bt.snap index 5aee8ff4d8..effe79d682 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__slash_popup_bt.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__slash_popup_bt.snap @@ -6,4 +6,4 @@ expression: terminal.backend() "› /bt " " " " " -" /btw start a side conversation in an ephemeral fork " +"› /btw start a side conversation in an ephemeral fork " 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 2d5b29038a..350984ff85 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 @@ -6,4 +6,4 @@ expression: terminal.backend() "› /mo " " " " " -" /model choose what model and reasoning effort to use " +"› /model choose what model and reasoning effort to use " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__slash_popup_pet.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__slash_popup_pet.snap index 3ac116bb46..a9e9ffa629 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__slash_popup_pet.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__slash_popup_pet.snap @@ -6,4 +6,4 @@ expression: terminal.backend() "› /pet " " " " " -" /pets choose or hide the terminal pet " +"› /pets choose or hide the terminal pet " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__slash_popup_res.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__slash_popup_res.snap index ff5628a2f3..5cfb86704f 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__slash_popup_res.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__slash_popup_res.snap @@ -7,4 +7,4 @@ expression: terminal.backend() " " " " " " -" /resume resume a saved chat " +"› /resume resume a saved chat " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__slash_popup_si.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__slash_popup_si.snap index 50a89ae1ab..3ee0b8b231 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__slash_popup_si.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__slash_popup_si.snap @@ -6,4 +6,4 @@ expression: terminal.backend() "› /si " " " " " -" /side start a side conversation in an ephemeral fork " +"› /side start a side conversation in an ephemeral fork " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__task_mention_popup.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__task_mention_popup.snap index de2d3b6ecf..a0ed196038 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__task_mention_popup.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__task_mention_popup.snap @@ -1,13 +1,14 @@ --- source: tui/src/bottom_pane/chat_composer.rs +assertion_line: 5348 expression: terminal.backend() --- " " "› @migr " " " -" " -" " -"> Review database migration /workspace/project Task " -" migration /another/project Task " -" " -" enter insert · esc close · ←/→ switch search modes [All Results] Filesystem Only Plugins " +" Mentions " +" Files, directories, plugins, skills and tasks " +" All Results Filesystem Only Plugins " +" migr " +"› Review database migration /workspace/project Task " +" enter/tab insert · esc close · ↑/↓ select · ←/→ filter " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__unified_mention_popup_falls_back_from_bound_plugin_on_right.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__unified_mention_popup_falls_back_from_bound_plugin_on_right.snap index 35bff9abb2..a7a63e57b4 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__unified_mention_popup_falls_back_from_bound_plugin_on_right.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__unified_mention_popup_falls_back_from_bound_plugin_on_right.snap @@ -1,13 +1,14 @@ --- source: tui/src/bottom_pane/chat_composer.rs +assertion_line: 5348 expression: terminal.backend() --- " " "› @left @bound " " " -" " -" " -" " -"> left Plugin used to test bound mention fallback. Plugin" -" " -" enter insert · esc close · ←/→ switch search modes [All Results] Filesystem Only Plugins " +" Mentions " +" Files, directories, plugins, skills and tasks " +" All Results Filesystem Only Plugins " +" left " +"› left Plugin used to test bound mention fallback. Plugin" +" enter/tab insert · esc close · ↑/↓ select · ←/→ filter " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__vim_replace_slash_recovery.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__vim_replace_slash_recovery.snap index 3620a75299..6b45b58d83 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__vim_replace_slash_recovery.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__vim_replace_slash_recovery.snap @@ -10,4 +10,4 @@ expression: terminal.backend() " " " " " " -" /diff show git diff (including untracked files) " +"› /diff show git diff (including untracked files) " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__command_popup__tests__command_popup_app.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__command_popup__tests__command_popup_app.snap index dde9247957..979e71cd2e 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__command_popup__tests__command_popup_app.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__command_popup__tests__command_popup_app.snap @@ -5,13 +5,14 @@ expression: "format!(\"{buf:?}\")" Buffer { area: Rect { x: 0, y: 0, width: 72, height: 2 }, content: [ - " /app continue this session in the Desktop app ", + "› /app continue this session in the Desktop app ", " /approve approve one retry of a recent auto-review denial ", ], styles: [ - x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, - x: 2, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD, - x: 52, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD | REVERSED, + x: 12, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: REVERSED, + x: 52, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD | REVERSED, + x: 0, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, x: 3, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD, x: 6, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, x: 12, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__command_popup__tests__command_popup_default_items.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__command_popup__tests__command_popup_default_items.snap index 8e631793d1..3685bb98db 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__command_popup__tests__command_popup_default_items.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__command_popup__tests__command_popup_default_items.snap @@ -1,5 +1,6 @@ --- source: tui/src/bottom_pane/command_popup.rs +assertion_line: 523 expression: commands --- /model - choose what model and reasoning effort to use diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__command_popup__tests__command_popup_filter_reset_after_scroll.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__command_popup__tests__command_popup_filter_reset_after_scroll.snap index 0313c5b3cc..2dbd73c380 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__command_popup__tests__command_popup_filter_reset_after_scroll.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__command_popup__tests__command_popup_filter_reset_after_scroll.snap @@ -5,14 +5,15 @@ expression: "format!(\"{buf:?}\")" Buffer { area: Rect { x: 0, y: 0, width: 72, height: 3 }, content: [ - " /status show current session configuration and token usage ", + "› /status show current session configuration and token usage ", " /statusline configure which items appear in the status line ", " /stop stop all background terminals ", ], styles: [ - x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, - x: 2, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD, - x: 65, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD | REVERSED, + x: 15, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: REVERSED, + x: 65, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD | REVERSED, + x: 0, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, x: 3, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD, x: 5, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, x: 15, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__command_popup__tests__command_popup_voice.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__command_popup__tests__command_popup_voice.snap index 612778c2f4..4d5f478d19 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__command_popup__tests__command_popup_voice.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__command_popup__tests__command_popup_voice.snap @@ -5,11 +5,11 @@ expression: "format!(\"{buf:?}\")" Buffer { area: Rect { x: 0, y: 0, width: 72, height: 1 }, content: [ - " /voice start or stop voice; use /voice settings to choose a voice ", + "› /voice start or stop voice; use /voice settings to choose a voice ", ], styles: [ - x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, - x: 2, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD, - x: 68, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD | REVERSED, + x: 10, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: REVERSED, + x: 68, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD | REVERSED, ] } diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__command_popup__tests__command_popup_wrap_boundary.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__command_popup__tests__command_popup_wrap_boundary.snap index 6e83f7d70b..d348d469e4 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__command_popup__tests__command_popup_wrap_boundary.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__command_popup__tests__command_popup_wrap_boundary.snap @@ -6,16 +6,17 @@ width 32 Buffer { area: Rect { x: 0, y: 0, width: 32, height: 3 }, content: [ - " /tier-one Use faster ", + "› /tier-one Use faster ", " inference ", " /tier-two Keep default speed ", ], styles: [ - x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, - x: 2, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD, - x: 23, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, - x: 2, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD, - x: 22, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD | REVERSED, + x: 13, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: REVERSED, + x: 23, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD | REVERSED, + x: 13, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: REVERSED, + x: 22, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD | REVERSED, + x: 0, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, x: 3, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD, x: 7, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, x: 13, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, @@ -27,12 +28,12 @@ width 33 Buffer { area: Rect { x: 0, y: 0, width: 33, height: 2 }, content: [ - " /tier-one Use faster inference", + "› /tier-one Use faster inference", " /tier-two Keep default speed ", ], styles: [ - x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, - x: 2, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD, + x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD | REVERSED, + x: 13, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: REVERSED, x: 0, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, x: 3, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD, x: 7, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, @@ -45,13 +46,14 @@ width 34 Buffer { area: Rect { x: 0, y: 0, width: 34, height: 2 }, content: [ - " /tier-one Use faster inference ", + "› /tier-one Use faster inference ", " /tier-two Keep default speed ", ], styles: [ - x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, - x: 2, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD, - x: 33, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD | REVERSED, + x: 13, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: REVERSED, + x: 33, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD | REVERSED, + x: 0, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, x: 3, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD, x: 7, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, x: 13, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__file_search_popup__tests__long_paths_keep_distinguishing_filenames_and_original_selection.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__file_search_popup__tests__long_paths_keep_distinguishing_filenames_and_original_selection.snap new file mode 100644 index 0000000000..b593925c49 --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__file_search_popup__tests__long_paths_keep_distinguishing_filenames_and_original_selection.snap @@ -0,0 +1,57 @@ +--- +source: tui/src/bottom_pane/file_search_popup.rs +assertion_line: 291 +expression: "snapshots.join(\"\\n\\n\")" +--- +["src", "shared", "long_directory"], width 28 + +› src/share…/parser_alpha.rs + src/shared…/parser_beta.rs + + +["src", "shared", "long_directory"], width 40 + +› src/shared/long_direc…/parser_alpha.rs + src/shared/long_direct…/parser_beta.rs + + +["src", "shared", "long_directory"], width 80 + +› src/shared/long_directory/parser_alpha.rs + src/shared/long_directory/parser_beta.rs + + +["long_directory_name_that_fills_row"], width 28 + +› long_dire…/parser_alpha.rs + long_direc…/parser_beta.rs + + +["long_directory_name_that_fills_row"], width 40 + +› long_directory_name_t…/parser_alpha.rs + long_directory_name_th…/parser_beta.rs + + +["long_directory_name_that_fills_row"], width 80 + +› long_directory_name_that_fills_row/parser_alpha.rs + long_directory_name_that_fills_row/parser_beta.rs + + +["長いディレクトリ名", "e\u{301}tudes"], width 28 + +› 長 い デ ィ …/parser_alpha.rs + 長 い デ ィ レ …/parser_beta.rs + + +["長いディレクトリ名", "e\u{301}tudes"], width 40 + +› 長 い デ ィ レ ク ト リ 名 /ét…/parser_alpha.rs + 長 い デ ィ レ ク ト リ 名 /étu…/parser_beta.rs + + +["長いディレクトリ名", "e\u{301}tudes"], width 80 + +› 長 い デ ィ レ ク ト リ 名 /études/parser_alpha.rs + 長 い デ ィ レ ク ト リ 名 /études/parser_beta.rs diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__skill_popup__tests__skill_popup_category_tags.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__skill_popup__tests__skill_popup_category_tags.snap new file mode 100644 index 0000000000..cf8ab654e1 --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__skill_popup__tests__skill_popup_category_tags.snap @@ -0,0 +1,114 @@ +--- +source: tui/src/bottom_pane/skill_popup.rs +expression: "snapshots.join(\"\\n\\n\")" +--- +name="Shared", width=32 +Buffer { + area: Rect { x: 0, y: 0, width: 32, height: 5 }, + content: [ + " ", + "› Shared [App] ", + " Shared [Skill] ", + " ", + " enter insert · esc close ", + ], + styles: [ + x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 0, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD | REVERSED, + x: 10, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: REVERSED, + x: 15, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD | REVERSED, + x: 0, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 2, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD, + x: 8, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 10, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, + x: 17, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 2, y: 4, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD, + x: 7, y: 4, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, + x: 17, y: 4, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD, + x: 20, y: 4, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, + x: 26, y: 4, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + ] +} + +name="Shared", width=72 +Buffer { + area: Rect { x: 0, y: 0, width: 72, height: 5 }, + content: [ + " ", + "› Shared [App] Secondary details ", + " Shared [Skill] Secondary details ", + " ", + " enter insert · esc close ", + ], + styles: [ + x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 0, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD | REVERSED, + x: 10, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: REVERSED, + x: 33, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD | REVERSED, + x: 0, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 2, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD, + x: 8, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 10, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, + x: 35, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 2, y: 4, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD, + x: 7, y: 4, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, + x: 17, y: 4, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD, + x: 20, y: 4, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, + x: 26, y: 4, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + ] +} + +name="Shared mention with a very long name", width=32 +Buffer { + area: Rect { x: 0, y: 0, width: 32, height: 5 }, + content: [ + " ", + "› Shared mention with a … [App]", + " Shared mention with … [Skill]", + " ", + " enter insert · esc close ", + ], + styles: [ + x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 0, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD | REVERSED, + x: 27, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: REVERSED, + x: 0, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 2, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD, + x: 8, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 25, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, + x: 0, y: 3, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 2, y: 4, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD, + x: 7, y: 4, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, + x: 17, y: 4, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD, + x: 20, y: 4, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, + x: 26, y: 4, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + ] +} + +name="Shared mention with a very long name", width=72 +Buffer { + area: Rect { x: 0, y: 0, width: 72, height: 5 }, + content: [ + " ", + "› Shared mention with a ver... [App] Secondary details ", + " Shared mention with a ver... [Skill] Secondary details ", + " ", + " enter insert · esc close ", + ], + styles: [ + x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 0, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD | REVERSED, + x: 32, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: REVERSED, + x: 55, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD | REVERSED, + x: 0, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 2, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD, + x: 8, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 32, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, + x: 57, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 2, y: 4, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD, + x: 7, y: 4, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, + x: 17, y: 4, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD, + x: 20, y: 4, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, + x: 26, y: 4, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + ] +} diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__skill_popup__tests__skill_popup_lowercase_expansion.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__skill_popup__tests__skill_popup_lowercase_expansion.snap index c82063275d..773d42aac0 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__skill_popup__tests__skill_popup_lowercase_expansion.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__skill_popup__tests__skill_popup_lowercase_expansion.snap @@ -3,34 +3,38 @@ source: tui/src/bottom_pane/skill_popup.rs expression: "render_popup(&popup, 48)" --- Buffer { - area: Rect { x: 0, y: 0, width: 48, height: 6 }, + area: Rect { x: 0, y: 0, width: 48, height: 7 }, content: [ - " ai̇x [Skill] ", + " ", + "› ai̇x [Skill] ", " aİx [Skill] ", " i̇x [Skill] ", " İx [Skill] ", " ", - " Press enter to insert or esc to close ", + " enter insert · esc close ", ], styles: [ x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, - x: 2, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD, - x: 14, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, - x: 3, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD, - x: 5, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, - x: 7, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, - x: 14, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, - x: 2, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD, - x: 4, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 0, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD | REVERSED, + x: 7, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: REVERSED, + x: 14, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD | REVERSED, + x: 0, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 3, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD, + x: 5, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, x: 7, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, x: 14, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, x: 2, y: 3, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD, x: 4, y: 3, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, x: 7, y: 3, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, x: 14, y: 3, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, - x: 8, y: 5, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD, - x: 13, y: 5, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, - x: 27, y: 5, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD, - x: 30, y: 5, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 2, y: 4, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD, + x: 4, y: 4, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 7, y: 4, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, + x: 14, y: 4, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 2, y: 6, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD, + x: 7, y: 6, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, + x: 17, y: 6, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD, + x: 20, y: 6, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, + x: 26, y: 6, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, ] } diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__skill_popup__tests__skill_popup_scrolled.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__skill_popup__tests__skill_popup_scrolled.snap index 0d885191d2..d293dbdc3f 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__skill_popup__tests__skill_popup_scrolled.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__skill_popup__tests__skill_popup_scrolled.snap @@ -3,8 +3,9 @@ source: tui/src/bottom_pane/skill_popup.rs expression: "render_popup(&popup, 72)" --- Buffer { - area: Rect { x: 0, y: 0, width: 72, height: 10 }, + area: Rect { x: 0, y: 0, width: 72, height: 11 }, content: [ + "↑ ", " Mention 01 [Skill] Description 01 ", " Mention 02 [Skill] Description 02 ", " Mention 03 [Skill] Description 03 ", @@ -12,14 +13,12 @@ Buffer { " Mention 05 [Skill] Description 05 ", " Mention 06 [Skill] Description 06 ", " Mention 07 [Skill] Description 07 ", - " Mention 08 [Skill] Description 08 ", - " ", - " Press enter to insert or esc to close ", + "› Mention 08 [Skill] Description 08 ", + "↓ ", + " enter insert · esc close ", ], styles: [ x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, - x: 14, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, - x: 36, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, x: 14, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, x: 36, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, x: 14, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, @@ -32,11 +31,16 @@ Buffer { x: 36, y: 5, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, x: 14, y: 6, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, x: 36, y: 6, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, - x: 2, y: 7, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD, + x: 14, y: 7, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, x: 36, y: 7, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, - x: 8, y: 9, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD, - x: 13, y: 9, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, - x: 27, y: 9, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD, - x: 30, y: 9, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 0, y: 8, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD | REVERSED, + x: 14, y: 8, fg: Reset, bg: Reset, underline: Reset, modifier: REVERSED, + x: 36, y: 8, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD | REVERSED, + x: 0, y: 9, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 2, y: 10, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD, + x: 7, y: 10, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, + x: 17, y: 10, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD, + x: 20, y: 10, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, + x: 26, y: 10, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, ] } diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__apps_mentions_only_callable_installed.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__apps_mentions_only_callable_installed.snap index a985b87d73..c00c803e34 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__apps_mentions_only_callable_installed.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__apps_mentions_only_callable_installed.snap @@ -1,13 +1,13 @@ --- source: tui/src/chatwidget/tests/popups_and_settings.rs -assertion_line: 2317 expression: snapshot --- › $ - Google Drive [App] Connected files - Press enter to insert or esc to close +› Google Drive [App] Connected files + + enter insert · esc close --- plugin authentication --- Plugins diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__worktrees_command_local.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__worktrees_command_local.snap index 50f75113a6..40cf9e25f0 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__worktrees_command_local.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__worktrees_command_local.snap @@ -1,8 +1,7 @@ --- -source: tui/src/chatwidget/tests/slash_commands.rs -assertion_line: 3091 +source: tui/src/chatwidget/tests/worktree_picker_tests.rs expression: popup --- › /work - /worktree start or continue a conversation in a new worktree +› /worktree start or continue a conversation in a new worktree