mirror of
https://github.com/openai/codex.git
synced 2026-11-03 19:53:25 +00:00
tui: add slash help search and status
Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
co-authored by
Codex
parent
8fbc9d32fd
commit
77e1e8029a
@@ -1,3 +1,5 @@
|
||||
use std::cell::Cell;
|
||||
|
||||
use crossterm::event::KeyCode;
|
||||
use crossterm::event::KeyEvent;
|
||||
use crossterm::event::KeyModifiers;
|
||||
@@ -5,10 +7,12 @@ use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Constraint;
|
||||
use ratatui::layout::Layout;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::style::Style;
|
||||
use ratatui::style::Stylize;
|
||||
use ratatui::text::Line;
|
||||
use ratatui::widgets::Paragraph;
|
||||
use ratatui::widgets::Widget;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::bottom_pane::CancellationEvent;
|
||||
use crate::bottom_pane::bottom_pane_view::BottomPaneView;
|
||||
@@ -21,35 +25,45 @@ use crate::wrapping::word_wrap_lines;
|
||||
|
||||
const HELP_VIEW_MIN_BODY_ROWS: u16 = 6;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum HelpRowWrap {
|
||||
None,
|
||||
Note,
|
||||
Description,
|
||||
Usage,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct HelpRow {
|
||||
plain_text: String,
|
||||
line: Line<'static>,
|
||||
wrap: HelpRowWrap,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct HelpSearch {
|
||||
active_query: String,
|
||||
input: Option<String>,
|
||||
selected_match: usize,
|
||||
}
|
||||
|
||||
pub(crate) struct SlashHelpView {
|
||||
complete: bool,
|
||||
scroll_top: usize,
|
||||
scroll_top: Cell<usize>,
|
||||
follow_selected_match: Cell<bool>,
|
||||
search: HelpSearch,
|
||||
}
|
||||
|
||||
impl SlashHelpView {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
complete: false,
|
||||
scroll_top: 0,
|
||||
scroll_top: Cell::new(0),
|
||||
follow_selected_match: Cell::new(false),
|
||||
search: HelpSearch::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn footer_hint() -> Line<'static> {
|
||||
Line::from(vec![
|
||||
"Scroll with ".into(),
|
||||
key_hint::plain(KeyCode::Up).into(),
|
||||
"/".into(),
|
||||
key_hint::plain(KeyCode::Down).into(),
|
||||
" or ".into(),
|
||||
key_hint::plain(KeyCode::PageUp).into(),
|
||||
"/".into(),
|
||||
key_hint::plain(KeyCode::PageDown).into(),
|
||||
", close with ".into(),
|
||||
key_hint::plain(KeyCode::Esc).into(),
|
||||
".".into(),
|
||||
])
|
||||
}
|
||||
|
||||
fn visible_body_rows(area_height: u16) -> usize {
|
||||
area_height
|
||||
.saturating_sub(3)
|
||||
@@ -57,69 +71,258 @@ impl SlashHelpView {
|
||||
.into()
|
||||
}
|
||||
|
||||
fn build_lines(width: u16) -> Vec<Line<'static>> {
|
||||
let width = width.max(24);
|
||||
let note_opts = RtOptions::new(width as usize)
|
||||
.initial_indent(Line::from(""))
|
||||
.subsequent_indent(Line::from(""));
|
||||
let usage_opts = RtOptions::new(width as usize)
|
||||
.initial_indent(Line::from(" "))
|
||||
.subsequent_indent(Line::from(" "));
|
||||
|
||||
let mut lines = vec![Line::from("Slash Commands".bold()), Line::from("")];
|
||||
lines.extend(word_wrap_lines(
|
||||
[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(),
|
||||
)],
|
||||
note_opts.clone(),
|
||||
));
|
||||
lines.extend(word_wrap_lines(
|
||||
[Line::from(
|
||||
"Args use shell-style quoting; quote values with spaces.".dim(),
|
||||
)],
|
||||
note_opts,
|
||||
));
|
||||
lines.push(Line::from(""));
|
||||
fn build_document() -> Vec<HelpRow> {
|
||||
let mut rows = vec![
|
||||
HelpRow {
|
||||
plain_text: "Slash Commands".to_string(),
|
||||
line: Line::from("Slash Commands".bold()),
|
||||
wrap: HelpRowWrap::None,
|
||||
},
|
||||
HelpRow {
|
||||
plain_text: String::new(),
|
||||
line: Line::from(""),
|
||||
wrap: HelpRowWrap::None,
|
||||
},
|
||||
HelpRow {
|
||||
plain_text: "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.".to_string(),
|
||||
line: 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(),
|
||||
),
|
||||
wrap: HelpRowWrap::Note,
|
||||
},
|
||||
HelpRow {
|
||||
plain_text: "Args use shell-style quoting; quote values with spaces.".to_string(),
|
||||
line: Line::from("Args use shell-style quoting; quote values with spaces.".dim()),
|
||||
wrap: HelpRowWrap::Note,
|
||||
},
|
||||
HelpRow {
|
||||
plain_text: String::new(),
|
||||
line: Line::from(""),
|
||||
wrap: HelpRowWrap::None,
|
||||
},
|
||||
];
|
||||
|
||||
for (_, cmd) in built_in_slash_commands() {
|
||||
lines.push(Line::from(format!("/{}", cmd.command()).cyan().bold()));
|
||||
lines.extend(word_wrap_lines(
|
||||
[Line::from(format!(" {}", cmd.description()).dim())],
|
||||
RtOptions::new(width as usize)
|
||||
.initial_indent(Line::from(""))
|
||||
.subsequent_indent(Line::from(" ")),
|
||||
));
|
||||
lines.push(Line::from(" Usage:".dim()));
|
||||
rows.push(HelpRow {
|
||||
plain_text: format!("/{}", cmd.command()),
|
||||
line: Line::from(format!("/{}", cmd.command()).cyan().bold()),
|
||||
wrap: HelpRowWrap::None,
|
||||
});
|
||||
rows.push(HelpRow {
|
||||
plain_text: format!(" {}", cmd.description()),
|
||||
line: Line::from(format!(" {}", cmd.description()).dim()),
|
||||
wrap: HelpRowWrap::Description,
|
||||
});
|
||||
rows.push(HelpRow {
|
||||
plain_text: " Usage:".to_string(),
|
||||
line: Line::from(" Usage:".dim()),
|
||||
wrap: HelpRowWrap::None,
|
||||
});
|
||||
for form in cmd.help_forms() {
|
||||
let usage = if form.is_empty() {
|
||||
let plain_text = if form.is_empty() {
|
||||
format!("/{}", cmd.command())
|
||||
} else {
|
||||
format!("/{} {}", cmd.command(), form)
|
||||
};
|
||||
lines.extend(word_wrap_lines(
|
||||
[Line::from(usage.cyan())],
|
||||
usage_opts.clone(),
|
||||
));
|
||||
rows.push(HelpRow {
|
||||
plain_text: plain_text.clone(),
|
||||
line: Line::from(plain_text.cyan()),
|
||||
wrap: HelpRowWrap::Usage,
|
||||
});
|
||||
}
|
||||
lines.push(Line::from(""));
|
||||
rows.push(HelpRow {
|
||||
plain_text: String::new(),
|
||||
line: Line::from(""),
|
||||
wrap: HelpRowWrap::None,
|
||||
});
|
||||
}
|
||||
|
||||
while lines.last().is_some_and(|line| line.spans.is_empty()) {
|
||||
lines.pop();
|
||||
while rows.last().is_some_and(|row| row.plain_text.is_empty()) {
|
||||
rows.pop();
|
||||
}
|
||||
|
||||
lines
|
||||
rows
|
||||
}
|
||||
|
||||
fn scroll_by(&mut self, delta: isize) {
|
||||
self.scroll_top = self.scroll_top.saturating_add_signed(delta);
|
||||
self.scroll_top
|
||||
.set(self.scroll_top.get().saturating_add_signed(delta));
|
||||
self.follow_selected_match.set(false);
|
||||
}
|
||||
|
||||
fn matching_logical_rows(rows: &[HelpRow], query: &str) -> Vec<usize> {
|
||||
let query = query.to_ascii_lowercase();
|
||||
rows.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(idx, row)| {
|
||||
row.plain_text
|
||||
.to_ascii_lowercase()
|
||||
.contains(query.as_str())
|
||||
.then_some(idx)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn current_query(&self) -> Option<&str> {
|
||||
if let Some(input) = self.search.input.as_deref() {
|
||||
return (!input.is_empty()).then_some(input);
|
||||
}
|
||||
(!self.search.active_query.is_empty()).then_some(self.search.active_query.as_str())
|
||||
}
|
||||
|
||||
fn search_indicator(
|
||||
&self,
|
||||
rows: &[HelpRow],
|
||||
total_rows: usize,
|
||||
visible_rows: usize,
|
||||
scroll_top: usize,
|
||||
) -> String {
|
||||
let start_row = if total_rows == 0 { 0 } else { scroll_top + 1 };
|
||||
let end_row = (scroll_top + visible_rows).min(total_rows);
|
||||
let viewport = format!("{start_row}-{end_row}/{total_rows}");
|
||||
let Some(query) = self.current_query() else {
|
||||
return viewport;
|
||||
};
|
||||
let match_count = Self::matching_logical_rows(rows, query).len();
|
||||
if self.search.input.is_some() {
|
||||
return format!(
|
||||
"{} match{} | {viewport}",
|
||||
match_count,
|
||||
if match_count == 1 { "" } else { "es" }
|
||||
);
|
||||
}
|
||||
if match_count == 0 {
|
||||
return format!("0/0 | {viewport}");
|
||||
}
|
||||
let current_match = self.search.selected_match.min(match_count - 1) + 1;
|
||||
format!("{current_match}/{match_count} | {viewport}")
|
||||
}
|
||||
|
||||
fn footer_line(&self) -> Line<'static> {
|
||||
if let Some(input) = self.search.input.as_deref() {
|
||||
return Line::from(vec![
|
||||
"Search: ".dim(),
|
||||
format!("/{input}").cyan(),
|
||||
" ".into(),
|
||||
key_hint::plain(KeyCode::Enter).into(),
|
||||
" apply ".dim(),
|
||||
key_hint::plain(KeyCode::Esc).into(),
|
||||
" cancel".dim(),
|
||||
]);
|
||||
}
|
||||
|
||||
Line::from(vec![
|
||||
key_hint::plain(KeyCode::Up).into(),
|
||||
"/".into(),
|
||||
key_hint::plain(KeyCode::Down).into(),
|
||||
" scroll ".dim(),
|
||||
key_hint::plain(KeyCode::PageUp).into(),
|
||||
"/".into(),
|
||||
key_hint::plain(KeyCode::PageDown).into(),
|
||||
" page ".dim(),
|
||||
"/ search ".dim(),
|
||||
"n/p match ".dim(),
|
||||
"q/".dim(),
|
||||
key_hint::plain(KeyCode::Esc).into(),
|
||||
" close".dim(),
|
||||
])
|
||||
}
|
||||
|
||||
fn wrap_rows(rows: &[HelpRow], width: u16) -> (Vec<Line<'static>>, Vec<usize>, Vec<usize>) {
|
||||
let width = width.max(24);
|
||||
let note_opts = RtOptions::new(width as usize)
|
||||
.initial_indent(Line::from(""))
|
||||
.subsequent_indent(Line::from(""));
|
||||
let description_opts = RtOptions::new(width as usize)
|
||||
.initial_indent(Line::from(""))
|
||||
.subsequent_indent(Line::from(" "));
|
||||
let usage_opts = RtOptions::new(width as usize)
|
||||
.initial_indent(Line::from(" "))
|
||||
.subsequent_indent(Line::from(" "));
|
||||
|
||||
let mut wrapped_rows = Vec::new();
|
||||
let mut row_starts = Vec::with_capacity(rows.len());
|
||||
let mut row_ends = Vec::with_capacity(rows.len());
|
||||
|
||||
for row in rows {
|
||||
row_starts.push(wrapped_rows.len());
|
||||
let wrapped = match row.wrap {
|
||||
HelpRowWrap::None => vec![row.line.clone()],
|
||||
HelpRowWrap::Note => word_wrap_lines([row.line.clone()], note_opts.clone()),
|
||||
HelpRowWrap::Description => {
|
||||
word_wrap_lines([row.line.clone()], description_opts.clone())
|
||||
}
|
||||
HelpRowWrap::Usage => word_wrap_lines([row.line.clone()], usage_opts.clone()),
|
||||
};
|
||||
wrapped_rows.extend(wrapped);
|
||||
row_ends.push(wrapped_rows.len());
|
||||
}
|
||||
|
||||
(wrapped_rows, row_starts, row_ends)
|
||||
}
|
||||
|
||||
fn move_to_match(&mut self, delta: isize) {
|
||||
if self.search.input.is_some() || self.search.active_query.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let matches =
|
||||
Self::matching_logical_rows(&Self::build_document(), &self.search.active_query);
|
||||
if matches.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let next = (self.search.selected_match as isize + delta).rem_euclid(matches.len() as isize);
|
||||
self.search.selected_match = next as usize;
|
||||
self.follow_selected_match.set(true);
|
||||
}
|
||||
}
|
||||
|
||||
impl BottomPaneView for SlashHelpView {
|
||||
fn handle_key_event(&mut self, key_event: KeyEvent) {
|
||||
if let Some(input) = self.search.input.as_mut() {
|
||||
match key_event {
|
||||
KeyEvent {
|
||||
code: KeyCode::Esc, ..
|
||||
} => {
|
||||
self.search.input = None;
|
||||
}
|
||||
KeyEvent {
|
||||
code: KeyCode::Enter,
|
||||
..
|
||||
} => {
|
||||
self.search.active_query = self.search.input.take().unwrap_or_default();
|
||||
self.search.selected_match = 0;
|
||||
self.follow_selected_match
|
||||
.set(!self.search.active_query.is_empty());
|
||||
}
|
||||
KeyEvent {
|
||||
code: KeyCode::Backspace,
|
||||
..
|
||||
} => {
|
||||
input.pop();
|
||||
}
|
||||
KeyEvent {
|
||||
code: KeyCode::Char(c),
|
||||
modifiers,
|
||||
..
|
||||
} if !modifiers.intersects(KeyModifiers::CONTROL | KeyModifiers::ALT) => {
|
||||
input.push(c);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
match key_event {
|
||||
KeyEvent {
|
||||
code: KeyCode::Char('/'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
} => {
|
||||
self.search.input = Some(self.search.active_query.clone());
|
||||
}
|
||||
KeyEvent {
|
||||
code: KeyCode::Up, ..
|
||||
}
|
||||
@@ -156,15 +359,22 @@ impl BottomPaneView for SlashHelpView {
|
||||
..
|
||||
} => self.scroll_by(MAX_POPUP_ROWS as isize),
|
||||
KeyEvent {
|
||||
code: KeyCode::Esc, ..
|
||||
}
|
||||
| KeyEvent {
|
||||
code: KeyCode::Char('q'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
} => {
|
||||
self.on_ctrl_c();
|
||||
}
|
||||
KeyEvent {
|
||||
code: KeyCode::Char('n'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
} => self.move_to_match(1),
|
||||
KeyEvent {
|
||||
code: KeyCode::Char('p'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
} => self.move_to_match(-1),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -177,6 +387,10 @@ impl BottomPaneView for SlashHelpView {
|
||||
self.complete = true;
|
||||
CancellationEvent::Handled
|
||||
}
|
||||
|
||||
fn prefer_esc_to_handle_key_event(&self) -> bool {
|
||||
self.search.input.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::render::renderable::Renderable for SlashHelpView {
|
||||
@@ -193,20 +407,65 @@ impl crate::render::renderable::Renderable for SlashHelpView {
|
||||
])
|
||||
.areas(content_area);
|
||||
|
||||
let lines = Self::build_lines(body_area.width);
|
||||
let rows = Self::build_document();
|
||||
let (lines, row_starts, row_ends) = Self::wrap_rows(&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);
|
||||
let max_scroll = lines.len().saturating_sub(visible_rows);
|
||||
let scroll_top = self.scroll_top.min(max_scroll) as u16;
|
||||
let max_scroll = body_lines.len().saturating_sub(visible_rows);
|
||||
let mut scroll_top = self.scroll_top.get().min(max_scroll);
|
||||
|
||||
Paragraph::new(lines.iter().take(2).cloned().collect::<Vec<_>>()).render(header_area, buf);
|
||||
Paragraph::new(lines.iter().skip(2).cloned().collect::<Vec<_>>())
|
||||
.scroll((scroll_top, 0))
|
||||
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)
|
||||
.get(self.search.selected_match)
|
||||
.copied()
|
||||
{
|
||||
let start = row_starts[selected_row_idx].saturating_sub(2);
|
||||
let end = row_ends[selected_row_idx].saturating_sub(2);
|
||||
if self.follow_selected_match.get() || start < scroll_top {
|
||||
scroll_top = start;
|
||||
} else if end > scroll_top + visible_rows {
|
||||
scroll_top = end.saturating_sub(visible_rows);
|
||||
}
|
||||
scroll_top = scroll_top.min(max_scroll);
|
||||
self.scroll_top.set(scroll_top);
|
||||
self.follow_selected_match.set(false);
|
||||
for line in body_lines.iter_mut().take(end).skip(start) {
|
||||
*line = line.clone().patch_style(Style::new().reversed());
|
||||
}
|
||||
}
|
||||
|
||||
self.scroll_top.set(scroll_top);
|
||||
|
||||
Paragraph::new(header_lines).render(header_area, buf);
|
||||
Paragraph::new(body_lines.clone())
|
||||
.scroll((scroll_top as u16, 0))
|
||||
.render(body_area, buf);
|
||||
Paragraph::new(Self::footer_hint()).render(footer_area, buf);
|
||||
|
||||
let footer_line = self.footer_line();
|
||||
Paragraph::new(footer_line.clone()).render(footer_area, buf);
|
||||
let indicator = self.search_indicator(&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_area.width {
|
||||
Paragraph::new(indicator.dim()).render(
|
||||
Rect::new(
|
||||
footer_area.x + footer_area.width - indicator_width,
|
||||
footer_area.y,
|
||||
indicator_width,
|
||||
footer_area.height,
|
||||
),
|
||||
buf,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn desired_height(&self, width: u16) -> u16 {
|
||||
let content_rows = Self::build_lines(width.saturating_sub(4)).len() as u16;
|
||||
let rows = Self::build_document();
|
||||
let (wrapped_rows, _, _) = Self::wrap_rows(&rows, width.saturating_sub(4));
|
||||
let content_rows = wrapped_rows.len() as u16;
|
||||
content_rows.max(HELP_VIEW_MIN_BODY_ROWS + 3)
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -240,4 +240,4 @@ expression: popup
|
||||
/debug-m-drop
|
||||
|
||||
/debug-m-update
|
||||
Scroll with ↑/↓ or pgup/pgdn, close with esc.
|
||||
↑/↓ scroll pgup/pgdn page / search n/p match q/esc close 1-233/239
|
||||
|
||||
+243
@@ -0,0 +1,243 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests.rs
|
||||
expression: searching
|
||||
---
|
||||
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
|
||||
Usage:
|
||||
/help
|
||||
|
||||
/model
|
||||
choose what model and reasoning effort to use
|
||||
Usage:
|
||||
/model
|
||||
/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
|
||||
Usage:
|
||||
/fast
|
||||
/fast <on|off|status>
|
||||
|
||||
/approvals
|
||||
choose what Codex is allowed to do
|
||||
Usage:
|
||||
/approvals
|
||||
/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
|
||||
Usage:
|
||||
/permissions
|
||||
/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
|
||||
Usage:
|
||||
/setup-default-sandbox
|
||||
|
||||
/experimental
|
||||
toggle experimental features
|
||||
Usage:
|
||||
/experimental
|
||||
/experimental <feature-key>=on|off ...
|
||||
|
||||
/skills
|
||||
use skills to improve how Codex performs specific tasks
|
||||
Usage:
|
||||
/skills
|
||||
/skills <list|manage>
|
||||
|
||||
/review
|
||||
review my current changes and find issues
|
||||
Usage:
|
||||
/review
|
||||
/review uncommitted
|
||||
/review branch <name>
|
||||
/review commit <sha> [title]
|
||||
/review <instructions>
|
||||
|
||||
/rename
|
||||
rename the current thread
|
||||
Usage:
|
||||
/rename
|
||||
/rename <title...>
|
||||
|
||||
/new
|
||||
start a new chat during a conversation
|
||||
Usage:
|
||||
/new
|
||||
|
||||
/resume
|
||||
resume a saved chat
|
||||
Usage:
|
||||
/resume
|
||||
/resume <thread-id>
|
||||
|
||||
/fork
|
||||
fork the current chat
|
||||
Usage:
|
||||
/fork
|
||||
|
||||
/init
|
||||
create an AGENTS.md file with instructions for Codex
|
||||
Usage:
|
||||
/init
|
||||
|
||||
/compact
|
||||
summarize conversation to prevent hitting the context limit
|
||||
Usage:
|
||||
/compact
|
||||
|
||||
/plan
|
||||
switch to Plan mode
|
||||
Usage:
|
||||
/plan
|
||||
/plan <prompt...>
|
||||
|
||||
/collab
|
||||
change collaboration mode (experimental)
|
||||
Usage:
|
||||
/collab
|
||||
/collab <default|plan>
|
||||
|
||||
/agent
|
||||
switch the active agent thread
|
||||
Usage:
|
||||
/agent
|
||||
/agent <thread-id>
|
||||
|
||||
/diff
|
||||
show git diff (including untracked files)
|
||||
Usage:
|
||||
/diff
|
||||
|
||||
/copy
|
||||
copy the latest Codex output to your clipboard
|
||||
Usage:
|
||||
/copy
|
||||
|
||||
/mention
|
||||
mention a file
|
||||
Usage:
|
||||
/mention
|
||||
|
||||
/status
|
||||
show current session configuration and token usage
|
||||
Usage:
|
||||
/status
|
||||
|
||||
/debug-config
|
||||
show config layers and requirement sources for debugging
|
||||
Usage:
|
||||
/debug-config
|
||||
|
||||
/statusline
|
||||
configure which items appear in the status line
|
||||
Usage:
|
||||
/statusline
|
||||
/statusline <item-id>...
|
||||
/statusline none
|
||||
|
||||
/theme
|
||||
choose a syntax highlighting theme
|
||||
Usage:
|
||||
/theme
|
||||
/theme <theme-name>
|
||||
|
||||
/mcp
|
||||
list configured MCP tools
|
||||
Usage:
|
||||
/mcp
|
||||
|
||||
/apps
|
||||
manage apps
|
||||
Usage:
|
||||
/apps
|
||||
|
||||
/logout
|
||||
log out of Codex
|
||||
Usage:
|
||||
/logout
|
||||
|
||||
/quit
|
||||
exit Codex
|
||||
Usage:
|
||||
/quit
|
||||
|
||||
/exit
|
||||
exit Codex
|
||||
Usage:
|
||||
/exit
|
||||
|
||||
/feedback
|
||||
send logs to maintainers
|
||||
Usage:
|
||||
/feedback
|
||||
/feedback <bug|bad-result|good-result|safety-check|other>
|
||||
|
||||
/rollout
|
||||
print the rollout file path
|
||||
Usage:
|
||||
/rollout
|
||||
|
||||
/ps
|
||||
list background terminals
|
||||
Usage:
|
||||
/ps
|
||||
|
||||
/clean
|
||||
stop all background terminals
|
||||
Usage:
|
||||
/clean
|
||||
|
||||
/clear
|
||||
clear the terminal and start a new chat
|
||||
Usage:
|
||||
/clear
|
||||
|
||||
/personality
|
||||
choose a communication style for Codex
|
||||
Usage:
|
||||
/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:
|
||||
/test-approval
|
||||
|
||||
/multi-agents
|
||||
switch the active agent thread
|
||||
Usage:
|
||||
/multi-agents
|
||||
/multi-agents <thread-id>
|
||||
|
||||
/debug-m-drop
|
||||
DO NOT USE
|
||||
Usage:
|
||||
/debug-m-drop
|
||||
|
||||
/debug-m-update
|
||||
Search: /maintainers enter apply esc cancel 1 match | 1-233/239
|
||||
@@ -6023,6 +6023,59 @@ async fn slash_help_opens_reference_popup() {
|
||||
assert!(scrolled.contains("/review <instructions>"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn slash_help_search_jumps_to_lower_match() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None).await;
|
||||
|
||||
chat.dispatch_command(SlashCommand::Help);
|
||||
let _ = render_bottom_popup(&chat, 100);
|
||||
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Char('/')));
|
||||
for ch in "maintainers".chars() {
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Char(ch)));
|
||||
}
|
||||
let searching = render_bottom_popup(&chat, 100);
|
||||
assert_snapshot!("slash_help_search_output", searching);
|
||||
assert!(searching.contains("Search: /maintainers"));
|
||||
assert!(searching.contains("1 match"));
|
||||
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Enter));
|
||||
|
||||
let popup = render_bottom_popup(&chat, 100);
|
||||
assert!(popup.contains("/feedback"));
|
||||
assert!(popup.contains("1/1 |"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn slash_help_search_navigates_matches_with_n_and_p() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None).await;
|
||||
|
||||
chat.dispatch_command(SlashCommand::Help);
|
||||
let _ = render_bottom_popup(&chat, 100);
|
||||
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Char('/')));
|
||||
for ch in "toggle".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("1/3 |"));
|
||||
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Char('n')));
|
||||
let second = render_bottom_popup(&chat, 100);
|
||||
assert!(second.contains("2/3 |"));
|
||||
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Char('n')));
|
||||
let third = render_bottom_popup(&chat, 100);
|
||||
assert!(third.contains("3/3 |"));
|
||||
assert!(third.contains("/realtime"));
|
||||
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Char('p')));
|
||||
let previous = render_bottom_popup(&chat, 100);
|
||||
assert!(previous.contains("2/3 |"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn slash_help_esc_dismisses_popup() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None).await;
|
||||
|
||||
Reference in New Issue
Block a user