mirror of
https://github.com/openai/codex.git
synced 2026-05-02 10:26:45 +00:00
tui: add slash command help page
Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
@@ -6381,6 +6381,63 @@ mod tests {
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slash_popup_help_first_for_root_ui() {
|
||||
use ratatui::Terminal;
|
||||
use ratatui::backend::TestBackend;
|
||||
|
||||
let (tx, _rx) = unbounded_channel::<AppEvent>();
|
||||
let sender = AppEventSender::new(tx);
|
||||
|
||||
let mut composer = ChatComposer::new(
|
||||
true,
|
||||
sender,
|
||||
false,
|
||||
"Ask Codex to do anything".to_string(),
|
||||
false,
|
||||
);
|
||||
|
||||
type_chars_humanlike(&mut composer, &['/']);
|
||||
|
||||
let mut terminal = match Terminal::new(TestBackend::new(60, 8)) {
|
||||
Ok(t) => t,
|
||||
Err(e) => panic!("Failed to create terminal: {e}"),
|
||||
};
|
||||
terminal
|
||||
.draw(|f| composer.render(f.area(), f.buffer_mut()))
|
||||
.unwrap_or_else(|e| panic!("Failed to draw composer: {e}"));
|
||||
|
||||
insta::assert_snapshot!("slash_popup_root", terminal.backend());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slash_popup_help_first_for_root_logic() {
|
||||
use super::super::command_popup::CommandItem;
|
||||
let (tx, _rx) = unbounded_channel::<AppEvent>();
|
||||
let sender = AppEventSender::new(tx);
|
||||
let mut composer = ChatComposer::new(
|
||||
true,
|
||||
sender,
|
||||
false,
|
||||
"Ask Codex to do anything".to_string(),
|
||||
false,
|
||||
);
|
||||
type_chars_humanlike(&mut composer, &['/']);
|
||||
|
||||
match &composer.active_popup {
|
||||
ActivePopup::Command(popup) => match popup.selected_item() {
|
||||
Some(CommandItem::Builtin(cmd)) => {
|
||||
assert_eq!(cmd.command(), "help")
|
||||
}
|
||||
Some(CommandItem::UserPrompt(_)) => {
|
||||
panic!("unexpected prompt selected for '/'")
|
||||
}
|
||||
None => panic!("no selected command for '/'"),
|
||||
},
|
||||
_ => panic!("slash popup not active after typing '/'"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slash_popup_model_first_for_mo_ui() {
|
||||
use ratatui::Terminal;
|
||||
|
||||
@@ -338,6 +338,20 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn help_is_first_suggestion_for_root_popup() {
|
||||
let mut popup = CommandPopup::new(Vec::new(), CommandPopupFlags::default());
|
||||
popup.on_composer_text_change("/".to_string());
|
||||
let matches = popup.filtered_items();
|
||||
match matches.first() {
|
||||
Some(CommandItem::Builtin(cmd)) => assert_eq!(cmd.command(), "help"),
|
||||
Some(CommandItem::UserPrompt(_)) => {
|
||||
panic!("unexpected prompt ranked before '/help' for '/'")
|
||||
}
|
||||
None => panic!("expected at least one match for '/'"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filtered_commands_keep_presentation_order_for_prefix() {
|
||||
let mut popup = CommandPopup::new(Vec::new(), CommandPopupFlags::default());
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
source: tui/src/bottom_pane/chat_composer.rs
|
||||
expression: terminal.backend()
|
||||
---
|
||||
" "
|
||||
"› / "
|
||||
" "
|
||||
" /help show slash command help "
|
||||
" /model choose what model and reasoning effort to "
|
||||
" use "
|
||||
" /permissions choose what Codex is allowed to do "
|
||||
" /experimental toggle experimental features "
|
||||
@@ -271,6 +271,7 @@ use crate::render::renderable::Renderable;
|
||||
use crate::render::renderable::RenderableExt;
|
||||
use crate::render::renderable::RenderableItem;
|
||||
use crate::slash_command::SlashCommand;
|
||||
use crate::slash_command::built_in_slash_commands;
|
||||
use crate::status::RateLimitSnapshotDisplay;
|
||||
use crate::status_indicator_widget::STATUS_DETAILS_DEFAULT_MAX_LINES;
|
||||
use crate::status_indicator_widget::StatusDetailsCapitalization;
|
||||
@@ -4346,6 +4347,10 @@ impl ChatWidget {
|
||||
return QueueReplayControl::Stop;
|
||||
}
|
||||
match cmd {
|
||||
SlashCommand::Help => {
|
||||
self.add_slash_help_output();
|
||||
QueueReplayControl::Continue
|
||||
}
|
||||
SlashCommand::Feedback => {
|
||||
if !self.config.feedback_enabled {
|
||||
let params = crate::bottom_pane::feedback_disabled_params();
|
||||
@@ -4773,6 +4778,10 @@ impl ChatWidget {
|
||||
args_message: UserMessage,
|
||||
) -> QueueReplayControl {
|
||||
match cmd {
|
||||
SlashCommand::Help => {
|
||||
self.add_slash_help_output();
|
||||
QueueReplayControl::Continue
|
||||
}
|
||||
SlashCommand::Approvals | SlashCommand::Permissions => {
|
||||
let args = match SlashCommandInvocation::parse_args(
|
||||
&args_message.text,
|
||||
@@ -6939,6 +6948,39 @@ impl ChatWidget {
|
||||
));
|
||||
}
|
||||
|
||||
pub(crate) fn add_slash_help_output(&mut self) {
|
||||
let mut lines = vec![
|
||||
Line::from("Slash Commands".bold()),
|
||||
Line::from(""),
|
||||
Line::from(
|
||||
"Type / to open the command popup. For commands with both a picker and an arg form, bare /command opens the picker and /command ... runs directly."
|
||||
.dim(),
|
||||
),
|
||||
Line::from("Args use shell-style quoting; quote values with spaces.".dim()),
|
||||
Line::from(""),
|
||||
];
|
||||
|
||||
const DESCRIPTION_COLUMN: usize = 56;
|
||||
|
||||
for (_, cmd) in built_in_slash_commands() {
|
||||
let forms = cmd.help_forms();
|
||||
let primary = if forms[0].is_empty() {
|
||||
format!("/{}", cmd.command())
|
||||
} else {
|
||||
format!("/{} {}", cmd.command(), forms[0])
|
||||
};
|
||||
let padded = format!(" {primary:<DESCRIPTION_COLUMN$}");
|
||||
lines.push(Line::from(vec![padded.cyan(), cmd.description().dim()]));
|
||||
|
||||
for form in &forms[1..] {
|
||||
let syntax = format!(" /{} {}", cmd.command(), form);
|
||||
lines.push(Line::from(syntax.dim()));
|
||||
}
|
||||
}
|
||||
|
||||
self.add_to_history(crate::history_cell::PlainHistoryCell::new(lines));
|
||||
}
|
||||
|
||||
pub(crate) fn add_debug_config_output(&mut self) {
|
||||
self.add_to_history(crate::debug_config::new_debug_config_output(
|
||||
&self.config,
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests.rs
|
||||
expression: rendered
|
||||
---
|
||||
Slash Commands
|
||||
|
||||
Type / to open the command popup. For commands with both a picker and an arg form, bare /command opens the picker and /command ... runs directly.
|
||||
Args use shell-style quoting; quote values with spaces.
|
||||
|
||||
/help show slash command help
|
||||
/model choose what model and reasoning effort to use
|
||||
/model <model> [default|none|minimal|low|medium|high|xhigh] [plan-only|all-modes]
|
||||
/fast toggle Fast mode to enable fastest inference at 2X plan usage
|
||||
/fast <on|off|status>
|
||||
/approvals choose what Codex is allowed to do
|
||||
/approvals <read-only|auto|full-access> [--confirm-full-access] [--remember-full-access] [--confirm-world-writable] [--remember-world-writable] [--enable-windows-sandbox=elevated|legacy]
|
||||
/permissions choose what Codex is allowed to do
|
||||
/permissions <read-only|auto|full-access> [--confirm-full-access] [--remember-full-access] [--confirm-world-writable] [--remember-world-writable] [--enable-windows-sandbox=elevated|legacy]
|
||||
/setup-default-sandbox set up elevated agent sandbox
|
||||
/experimental toggle experimental features
|
||||
/experimental <feature-key>=on|off ...
|
||||
/skills use skills to improve how Codex performs specific tasks
|
||||
/skills <list|manage>
|
||||
/review review my current changes and find issues
|
||||
/review uncommitted
|
||||
/review branch <name>
|
||||
/review commit <sha> [title]
|
||||
/review <instructions>
|
||||
/rename rename the current thread
|
||||
/rename <title...>
|
||||
/new start a new chat during a conversation
|
||||
/resume resume a saved chat
|
||||
/resume <thread-id>
|
||||
/fork fork the current chat
|
||||
/init create an AGENTS.md file with instructions for Codex
|
||||
/compact summarize conversation to prevent hitting the context limit
|
||||
/plan switch to Plan mode
|
||||
/plan <prompt...>
|
||||
/collab change collaboration mode (experimental)
|
||||
/collab <default|plan>
|
||||
/agent switch the active agent thread
|
||||
/agent <thread-id>
|
||||
/diff show git diff (including untracked files)
|
||||
/copy copy the latest Codex output to your clipboard
|
||||
/mention mention a file
|
||||
/status show current session configuration and token usage
|
||||
/debug-config show config layers and requirement sources for debugging
|
||||
/statusline configure which items appear in the status line
|
||||
/statusline <item-id>...
|
||||
/statusline none
|
||||
/theme choose a syntax highlighting theme
|
||||
/theme <theme-name>
|
||||
/mcp list configured MCP tools
|
||||
/apps manage apps
|
||||
/logout log out of Codex
|
||||
/quit exit Codex
|
||||
/exit exit Codex
|
||||
/feedback send logs to maintainers
|
||||
/feedback <bug|bad-result|good-result|safety-check|other>
|
||||
/rollout print the rollout file path
|
||||
/ps list background terminals
|
||||
/clean stop all background terminals
|
||||
/clear clear the terminal and start a new chat
|
||||
/personality choose a communication style for Codex
|
||||
/personality <none|friendly|pragmatic>
|
||||
/realtime toggle realtime voice mode (experimental)
|
||||
/settings configure realtime microphone/speaker
|
||||
/settings <microphone|speaker> [default|<device-name>]
|
||||
/test-approval test approval request
|
||||
/multi-agents switch the active agent thread
|
||||
/multi-agents <thread-id>
|
||||
/debug-m-drop DO NOT USE
|
||||
/debug-m-update DO NOT USE
|
||||
@@ -6000,6 +6000,21 @@ async fn slash_copy_reports_when_no_copyable_output_exists() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn slash_help_renders_reference_page() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None).await;
|
||||
|
||||
chat.dispatch_command(SlashCommand::Help);
|
||||
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
assert_eq!(cells.len(), 1, "expected one help cell");
|
||||
let rendered = lines_to_single_string(&cells[0]);
|
||||
assert_snapshot!("slash_help_output", rendered);
|
||||
assert!(rendered.contains("/help"));
|
||||
assert!(rendered.contains("/model <model>"));
|
||||
assert!(rendered.contains("/review <instructions>"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn slash_copy_state_is_preserved_during_running_task() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None).await;
|
||||
|
||||
@@ -12,6 +12,7 @@ use strum_macros::IntoStaticStr;
|
||||
pub enum SlashCommand {
|
||||
// DO NOT ALPHA-SORT! Enum order is presentation order in the popup, so
|
||||
// more frequently used commands should be listed first.
|
||||
Help,
|
||||
Model,
|
||||
Fast,
|
||||
Approvals,
|
||||
@@ -68,6 +69,7 @@ impl SlashCommand {
|
||||
/// User-visible description shown in the popup.
|
||||
pub fn description(self) -> &'static str {
|
||||
match self {
|
||||
SlashCommand::Help => "show slash command help",
|
||||
SlashCommand::Feedback => "send logs to maintainers",
|
||||
SlashCommand::New => "start a new chat during a conversation",
|
||||
SlashCommand::Init => "create an AGENTS.md file with instructions for Codex",
|
||||
@@ -120,9 +122,69 @@ impl SlashCommand {
|
||||
self.into()
|
||||
}
|
||||
|
||||
/// Human-facing forms accepted by the TUI.
|
||||
///
|
||||
/// An empty string represents the bare `/command` form.
|
||||
pub fn help_forms(self) -> &'static [&'static str] {
|
||||
match self {
|
||||
SlashCommand::Help => &[""],
|
||||
SlashCommand::Model => &[
|
||||
"",
|
||||
"<model> [default|none|minimal|low|medium|high|xhigh] [plan-only|all-modes]",
|
||||
],
|
||||
SlashCommand::Fast => &["", "<on|off|status>"],
|
||||
SlashCommand::Approvals | SlashCommand::Permissions => &[
|
||||
"",
|
||||
"<read-only|auto|full-access> [--confirm-full-access] [--remember-full-access] [--confirm-world-writable] [--remember-world-writable] [--enable-windows-sandbox=elevated|legacy]",
|
||||
],
|
||||
SlashCommand::ElevateSandbox => &[""],
|
||||
SlashCommand::SandboxReadRoot => &["<absolute-directory-path>"],
|
||||
SlashCommand::Experimental => &["", "<feature-key>=on|off ..."],
|
||||
SlashCommand::Skills => &["", "<list|manage>"],
|
||||
SlashCommand::Review => &[
|
||||
"",
|
||||
"uncommitted",
|
||||
"branch <name>",
|
||||
"commit <sha> [title]",
|
||||
"<instructions>",
|
||||
],
|
||||
SlashCommand::Rename => &["", "<title...>"],
|
||||
SlashCommand::New => &[""],
|
||||
SlashCommand::Resume => &["", "<thread-id>"],
|
||||
SlashCommand::Fork => &[""],
|
||||
SlashCommand::Init => &[""],
|
||||
SlashCommand::Compact => &[""],
|
||||
SlashCommand::Plan => &["", "<prompt...>"],
|
||||
SlashCommand::Collab => &["", "<default|plan>"],
|
||||
SlashCommand::Agent | SlashCommand::MultiAgents => &["", "<thread-id>"],
|
||||
SlashCommand::Diff => &[""],
|
||||
SlashCommand::Copy => &[""],
|
||||
SlashCommand::Mention => &[""],
|
||||
SlashCommand::Status => &[""],
|
||||
SlashCommand::DebugConfig => &[""],
|
||||
SlashCommand::Statusline => &["", "<item-id>...", "none"],
|
||||
SlashCommand::Theme => &["", "<theme-name>"],
|
||||
SlashCommand::Mcp => &[""],
|
||||
SlashCommand::Apps => &[""],
|
||||
SlashCommand::Logout => &[""],
|
||||
SlashCommand::Quit | SlashCommand::Exit => &[""],
|
||||
SlashCommand::Feedback => &["", "<bug|bad-result|good-result|safety-check|other>"],
|
||||
SlashCommand::Rollout => &[""],
|
||||
SlashCommand::Ps => &[""],
|
||||
SlashCommand::Clean => &[""],
|
||||
SlashCommand::Clear => &[""],
|
||||
SlashCommand::Personality => &["", "<none|friendly|pragmatic>"],
|
||||
SlashCommand::Realtime => &[""],
|
||||
SlashCommand::Settings => &["", "<microphone|speaker> [default|<device-name>]"],
|
||||
SlashCommand::TestApproval => &[""],
|
||||
SlashCommand::MemoryDrop | SlashCommand::MemoryUpdate => &[""],
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether bare dispatch opens interactive UI that should be resolved before queueing.
|
||||
pub fn requires_interaction(self) -> bool {
|
||||
match self {
|
||||
SlashCommand::Help => false,
|
||||
SlashCommand::Feedback
|
||||
| SlashCommand::Resume
|
||||
| SlashCommand::Review
|
||||
@@ -171,6 +233,7 @@ impl SlashCommand {
|
||||
/// Whether this command can be run while a task is in progress.
|
||||
pub fn available_during_task(self) -> bool {
|
||||
match self {
|
||||
SlashCommand::Help => true,
|
||||
SlashCommand::New
|
||||
| SlashCommand::Resume
|
||||
| SlashCommand::Fork
|
||||
|
||||
Reference in New Issue
Block a user