From a6ffd1c88796b786f2d027decf14ec937f93d549 Mon Sep 17 00:00:00 2001 From: Charles Cunningham Date: Fri, 13 Mar 2026 16:45:56 -0700 Subject: [PATCH] tui: gate slash help by runtime availability Co-authored-by: Codex --- codex-rs/tui/src/bottom_pane/help_view.rs | 25 ++++---- codex-rs/tui/src/bottom_pane/mod.rs | 1 + codex-rs/tui/src/chatwidget.rs | 59 ++++++++++--------- ..._chatwidget__tests__slash_help_output.snap | 23 +------- ...dget__tests__slash_help_search_output.snap | 23 +------- codex-rs/tui/src/chatwidget/tests.rs | 12 ++-- 6 files changed, 54 insertions(+), 89 deletions(-) diff --git a/codex-rs/tui/src/bottom_pane/help_view.rs b/codex-rs/tui/src/bottom_pane/help_view.rs index 886768f2c7..48b88c5c4c 100644 --- a/codex-rs/tui/src/bottom_pane/help_view.rs +++ b/codex-rs/tui/src/bottom_pane/help_view.rs @@ -14,12 +14,13 @@ use ratatui::widgets::Paragraph; use ratatui::widgets::Widget; use unicode_width::UnicodeWidthStr; +use crate::bottom_pane::BuiltinCommandFlags; use crate::bottom_pane::CancellationEvent; use crate::bottom_pane::bottom_pane_view::BottomPaneView; use crate::bottom_pane::popup_consts::MAX_POPUP_ROWS; use crate::bottom_pane::selection_popup_common::render_menu_surface; +use crate::bottom_pane::visible_builtins_for_input; use crate::key_hint; -use crate::slash_command::visible_built_in_slash_commands; use crate::wrapping::RtOptions; use crate::wrapping::word_wrap_lines; @@ -49,15 +50,17 @@ struct HelpSearch { pub(crate) struct SlashHelpView { complete: bool, + rows: Vec, scroll_top: Cell, follow_selected_match: Cell, search: HelpSearch, } impl SlashHelpView { - pub(crate) fn new() -> Self { + pub(crate) fn new(flags: BuiltinCommandFlags) -> Self { Self { complete: false, + rows: Self::build_document(flags), scroll_top: Cell::new(0), follow_selected_match: Cell::new(false), search: HelpSearch::default(), @@ -71,7 +74,7 @@ impl SlashHelpView { .into() } - fn build_document() -> Vec { + fn build_document(flags: BuiltinCommandFlags) -> Vec { let mut rows = vec![ HelpRow { plain_text: "Slash Commands".to_string(), @@ -103,7 +106,7 @@ impl SlashHelpView { }, ]; - for cmd in visible_built_in_slash_commands() { + for cmd in visible_builtins_for_input(flags) { rows.push(HelpRow { plain_text: format!("/{}", cmd.command()), line: Line::from(format!("/{}", cmd.command()).cyan().bold()), @@ -273,8 +276,7 @@ impl SlashHelpView { return; } - let matches = - Self::matching_logical_rows(&Self::build_document(), &self.search.active_query); + let matches = Self::matching_logical_rows(&self.rows, &self.search.active_query); if matches.is_empty() { return; } @@ -425,8 +427,7 @@ impl crate::render::renderable::Renderable for SlashHelpView { let [_footer_gap_area, footer_line_area] = Layout::vertical([Constraint::Length(1), Constraint::Length(1)]).areas(footer_area); - let rows = Self::build_document(); - let (lines, row_starts, row_ends) = Self::wrap_rows(&rows, body_area.width); + let (lines, row_starts, row_ends) = Self::wrap_rows(&self.rows, body_area.width); let header_lines = lines.iter().take(2).cloned().collect::>(); let mut body_lines = lines.iter().skip(2).cloned().collect::>(); let visible_rows = Self::visible_body_rows(body_area.height); @@ -436,7 +437,7 @@ impl crate::render::renderable::Renderable for SlashHelpView { if self.search.input.is_none() && !self.search.active_query.is_empty() && let Some(selected_row_idx) = - Self::matching_logical_rows(&rows, &self.search.active_query) + Self::matching_logical_rows(&self.rows, &self.search.active_query) .get(self.search.selected_match) .copied() { @@ -464,7 +465,8 @@ impl crate::render::renderable::Renderable for SlashHelpView { let footer_line = self.footer_line(); Paragraph::new(footer_line.clone()).render(footer_line_area, buf); - let indicator = self.search_indicator(&rows, body_lines.len(), visible_rows, scroll_top); + let indicator = + self.search_indicator(&self.rows, body_lines.len(), visible_rows, scroll_top); let indicator_width = UnicodeWidthStr::width(indicator.as_str()) as u16; let footer_width = footer_line.width() as u16; if footer_width + indicator_width + 2 <= footer_line_area.width { @@ -481,8 +483,7 @@ impl crate::render::renderable::Renderable for SlashHelpView { } fn desired_height(&self, width: u16) -> u16 { - let rows = Self::build_document(); - let (wrapped_rows, _, _) = Self::wrap_rows(&rows, width.saturating_sub(4)); + let (wrapped_rows, _, _) = Self::wrap_rows(&self.rows, width.saturating_sub(4)); let content_rows = wrapped_rows.len() as u16; content_rows.max(HELP_VIEW_MIN_BODY_ROWS + 4) } diff --git a/codex-rs/tui/src/bottom_pane/mod.rs b/codex-rs/tui/src/bottom_pane/mod.rs index 73d5577a5b..f1a8566718 100644 --- a/codex-rs/tui/src/bottom_pane/mod.rs +++ b/codex-rs/tui/src/bottom_pane/mod.rs @@ -154,6 +154,7 @@ pub(crate) use list_selection_view::SelectionItem; pub(crate) use prompt_args::parse_slash_name; pub(crate) use slash_commands::BuiltinCommandFlags; pub(crate) use slash_commands::find_builtin_command; +pub(crate) use slash_commands::visible_builtins_for_input; /// Pane displayed in the lower half of the chat UI. /// diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 697a58f29e..d087e3308c 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -4358,7 +4358,9 @@ impl ChatWidget { match cmd { SlashCommand::Help => { self.bottom_pane - .show_view(Box::new(crate::bottom_pane::SlashHelpView::new())); + .show_view(Box::new(crate::bottom_pane::SlashHelpView::new( + self.builtin_command_flags(), + ))); QueueReplayControl::Continue } SlashCommand::Feedback => { @@ -4793,7 +4795,9 @@ impl ChatWidget { match cmd { SlashCommand::Help => { self.bottom_pane - .show_view(Box::new(crate::bottom_pane::SlashHelpView::new())); + .show_view(Box::new(crate::bottom_pane::SlashHelpView::new( + self.builtin_command_flags(), + ))); QueueReplayControl::Continue } SlashCommand::Approvals | SlashCommand::Permissions => { @@ -5829,34 +5833,35 @@ impl ChatWidget { text: &'a str, ) -> Option<(SlashCommand, &'a str, usize)> { let (name, rest, rest_offset) = parse_slash_name(text)?; - let cmd = find_builtin_command( - name, - BuiltinCommandFlags { - collaboration_modes_enabled: self.collaboration_modes_enabled(), - connectors_enabled: self.connectors_enabled(), - fast_command_enabled: self.fast_mode_enabled(), - personality_command_enabled: self.config.features.enabled(Feature::Personality), - realtime_conversation_enabled: self.realtime_conversation_enabled(), - audio_device_selection_enabled: self.realtime_audio_device_selection_enabled(), - allow_elevate_sandbox: { - #[cfg(target_os = "windows")] - { - codex_core::windows_sandbox::ELEVATED_SANDBOX_NUX_ENABLED - && matches!( - WindowsSandboxLevel::from_config(&self.config), - WindowsSandboxLevel::RestrictedToken - ) - } - #[cfg(not(target_os = "windows"))] - { - false - } - }, - }, - )?; + let cmd = find_builtin_command(name, self.builtin_command_flags())?; Some((cmd, rest, rest_offset)) } + fn builtin_command_flags(&self) -> BuiltinCommandFlags { + BuiltinCommandFlags { + collaboration_modes_enabled: self.collaboration_modes_enabled(), + connectors_enabled: self.connectors_enabled(), + fast_command_enabled: self.fast_mode_enabled(), + personality_command_enabled: self.config.features.enabled(Feature::Personality), + realtime_conversation_enabled: self.realtime_conversation_enabled(), + audio_device_selection_enabled: self.realtime_audio_device_selection_enabled(), + allow_elevate_sandbox: { + #[cfg(target_os = "windows")] + { + codex_core::windows_sandbox::ELEVATED_SANDBOX_NUX_ENABLED + && matches!( + WindowsSandboxLevel::from_config(&self.config), + WindowsSandboxLevel::RestrictedToken + ) + } + #[cfg(not(target_os = "windows"))] + { + false + } + }, + } + } + fn is_known_slash_draft(&self, draft: &UserMessage) -> bool { let Some((name, _, _)) = parse_slash_name(&draft.text) else { return false; diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__slash_help_output.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__slash_help_output.snap index b74fde4dec..8060910fb3 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__slash_help_output.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__slash_help_output.snap @@ -41,11 +41,6 @@ expression: popup [--confirm-world-writable] [--remember-world-writable] [--enable-windows- sandbox=elevated|legacy] - /setup-default-sandbox - set up elevated agent sandbox - Usage: - /setup-default-sandbox - /experimental toggle experimental features Usage: @@ -161,11 +156,6 @@ expression: popup Usage: /mcp - /apps - manage apps - Usage: - /apps - /logout log out of Codex Usage: @@ -213,17 +203,6 @@ expression: popup /personality /personality - /realtime - toggle realtime voice mode (experimental) - Usage: - /realtime - - /settings - configure realtime microphone/speaker - Usage: - /settings - /settings [default|] - /test-approval test approval request Usage: @@ -241,4 +220,4 @@ expression: popup /debug-m-drop - ↑/↓ scroll | [ctrl + p / ctrl + n] page | / search | esc close 1-233/240 + ↑/↓ scroll | [ctrl + p / ctrl + n] page | / search | esc close 1-212/219 diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__slash_help_search_output.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__slash_help_search_output.snap index 738dbce75c..6ba1dcd2f5 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__slash_help_search_output.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__slash_help_search_output.snap @@ -41,11 +41,6 @@ expression: searching [--confirm-world-writable] [--remember-world-writable] [--enable-windows- sandbox=elevated|legacy] - /setup-default-sandbox - set up elevated agent sandbox - Usage: - /setup-default-sandbox - /experimental toggle experimental features Usage: @@ -161,11 +156,6 @@ expression: searching Usage: /mcp - /apps - manage apps - Usage: - /apps - /logout log out of Codex Usage: @@ -213,17 +203,6 @@ expression: searching /personality /personality - /realtime - toggle realtime voice mode (experimental) - Usage: - /realtime - - /settings - configure realtime microphone/speaker - Usage: - /settings - /settings [default|] - /test-approval test approval request Usage: @@ -241,4 +220,4 @@ expression: searching /debug-m-drop - Search: /maintainers | enter apply | esc cancel 1 match | 1-233/240 + Search: /maintainers | enter apply | esc cancel 1 match | 1-212/219 diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index 4de19d5f9b..68dfd4c516 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -6132,30 +6132,30 @@ async fn slash_help_search_navigates_matches_with_n_and_p() { let _ = render_bottom_popup(&chat, 100); chat.handle_key_event(KeyEvent::from(KeyCode::Char('/'))); - for ch in "toggle".chars() { + for ch in "show".chars() { chat.handle_key_event(KeyEvent::from(KeyCode::Char(ch))); } chat.handle_key_event(KeyEvent::from(KeyCode::Enter)); let first = render_bottom_popup(&chat, 100); assert!(first.contains("n/p match")); - assert!(first.contains("/fast")); + assert!(first.contains("/help")); chat.handle_key_event(KeyEvent::from(KeyCode::Char('n'))); let second = render_bottom_popup(&chat, 100); - assert!(second.contains("/experimental")); + assert!(second.contains("/diff")); chat.handle_key_event(KeyEvent::from(KeyCode::Char('n'))); let third = render_bottom_popup(&chat, 100); - assert!(third.contains("/realtime")); + assert!(third.contains("/status")); chat.handle_key_event(KeyEvent::from(KeyCode::Char('p'))); let previous = render_bottom_popup(&chat, 100); - assert!(previous.contains("/experimental")); + assert!(previous.contains("/diff")); chat.handle_key_event(KeyEvent::new(KeyCode::Char('N'), KeyModifiers::SHIFT)); let shifted_previous = render_bottom_popup(&chat, 100); - assert!(shifted_previous.contains("/fast")); + assert!(shifted_previous.contains("/help")); } #[tokio::test]