tui: gate slash help by runtime availability

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Charles Cunningham
2026-03-15 23:36:06 -07:00
co-authored by Codex
parent 1a54e52fd9
commit a6ffd1c887
6 changed files with 54 additions and 89 deletions
+13 -12
View File
@@ -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<HelpRow>,
scroll_top: Cell<usize>,
follow_selected_match: Cell<bool>,
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<HelpRow> {
fn build_document(flags: BuiltinCommandFlags) -> Vec<HelpRow> {
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::<Vec<_>>();
let mut body_lines = lines.iter().skip(2).cloned().collect::<Vec<_>>();
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)
}
+1
View File
@@ -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.
///
+32 -27
View File
@@ -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;
@@ -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 <none|friendly|pragmatic>
/realtime
toggle realtime voice mode (experimental)
Usage:
/realtime
/settings
configure realtime microphone/speaker
Usage:
/settings
/settings <microphone|speaker> [default|<device-name>]
/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
@@ -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 <none|friendly|pragmatic>
/realtime
toggle realtime voice mode (experimental)
Usage:
/realtime
/settings
configure realtime microphone/speaker
Usage:
/settings
/settings <microphone|speaker> [default|<device-name>]
/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
+6 -6
View File
@@ -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]