replace /prompts with a rotating placeholder (#2314)

This commit is contained in:
Jeremy Rose
2025-08-15 22:37:10 -04:00
committed by GitHub
parent d3078b9adc
commit 7a80d3c96c
11 changed files with 84 additions and 72 deletions

2
codex-rs/Cargo.lock generated
View File

@@ -930,7 +930,7 @@ dependencies = [
"once_cell",
"path-clean",
"pretty_assertions",
"rand 0.8.5",
"rand 0.9.2",
"ratatui",
"ratatui-image",
"regex-lite",

View File

@@ -75,6 +75,7 @@ tui-markdown = "0.3.3"
unicode-segmentation = "1.12.0"
unicode-width = "0.1"
uuid = "1"
rand = "0.9"
[target.'cfg(unix)'.dependencies]
libc = "0.2"
@@ -84,5 +85,5 @@ libc = "0.2"
chrono = { version = "0.4", features = ["serde"] }
insta = "1.43.1"
pretty_assertions = "1"
rand = "0.8"
rand = "0.9"
vt100 = "0.16.2"

View File

@@ -416,11 +416,6 @@ impl App<'_> {
widget.add_status_output();
}
}
SlashCommand::Prompts => {
if let AppState::Chat { widget } = &mut self.app_state {
widget.add_prompts_output();
}
}
#[cfg(debug_assertions)]
SlashCommand::TestApproval => {
use codex_core::protocol::EventMsg;

View File

@@ -98,6 +98,7 @@ mod tests {
app_event_tx: AppEventSender::new(tx_raw2),
has_input_focus: true,
enhanced_keys_supported: false,
placeholder_text: "Ask Codex to do anything".to_string(),
});
assert_eq!(CancellationEvent::Handled, view.on_ctrl_c(&mut pane));
assert!(view.queue.is_empty());

View File

@@ -31,7 +31,6 @@ use crate::bottom_pane::textarea::TextAreaState;
use codex_file_search::FileMatch;
use std::cell::RefCell;
const BASE_PLACEHOLDER_TEXT: &str = "Ask Codex to do anything";
/// If the pasted content exceeds this number of characters, replace it with a
/// placeholder in the UI.
const LARGE_PASTE_CHAR_THRESHOLD: usize = 1000;
@@ -61,6 +60,7 @@ pub(crate) struct ChatComposer {
pending_pastes: Vec<(String, String)>,
token_usage_info: Option<TokenUsageInfo>,
has_focus: bool,
placeholder_text: String,
}
/// Popup state at most one can be visible at any time.
@@ -75,6 +75,7 @@ impl ChatComposer {
has_input_focus: bool,
app_event_tx: AppEventSender,
enhanced_keys_supported: bool,
placeholder_text: String,
) -> Self {
let use_shift_enter_hint = enhanced_keys_supported;
@@ -91,6 +92,7 @@ impl ChatComposer {
pending_pastes: Vec::new(),
token_usage_info: None,
has_focus: has_input_focus,
placeholder_text,
}
}
@@ -712,7 +714,7 @@ impl WidgetRef for &ChatComposer {
let mut state = self.textarea_state.borrow_mut();
StatefulWidgetRef::render_ref(&(&self.textarea), textarea_rect, buf, &mut state);
if self.textarea.text().is_empty() {
Line::from(BASE_PLACEHOLDER_TEXT)
Line::from(self.placeholder_text.as_str())
.style(Style::default().dim())
.render_ref(textarea_rect.inner(Margin::new(1, 0)), buf);
}
@@ -885,7 +887,8 @@ mod tests {
let (tx, _rx) = std::sync::mpsc::channel();
let sender = AppEventSender::new(tx);
let mut composer = ChatComposer::new(true, sender, false);
let mut composer =
ChatComposer::new(true, sender, false, "Ask Codex to do anything".to_string());
let needs_redraw = composer.handle_paste("hello".to_string());
assert!(needs_redraw);
@@ -908,7 +911,8 @@ mod tests {
let (tx, _rx) = std::sync::mpsc::channel();
let sender = AppEventSender::new(tx);
let mut composer = ChatComposer::new(true, sender, false);
let mut composer =
ChatComposer::new(true, sender, false, "Ask Codex to do anything".to_string());
let large = "x".repeat(LARGE_PASTE_CHAR_THRESHOLD + 10);
let needs_redraw = composer.handle_paste(large.clone());
@@ -937,7 +941,8 @@ mod tests {
let large = "y".repeat(LARGE_PASTE_CHAR_THRESHOLD + 1);
let (tx, _rx) = std::sync::mpsc::channel();
let sender = AppEventSender::new(tx);
let mut composer = ChatComposer::new(true, sender, false);
let mut composer =
ChatComposer::new(true, sender, false, "Ask Codex to do anything".to_string());
composer.handle_paste(large);
assert_eq!(composer.pending_pastes.len(), 1);
@@ -973,7 +978,12 @@ mod tests {
for (name, input) in test_cases {
// Create a fresh composer for each test case
let mut composer = ChatComposer::new(true, sender.clone(), false);
let mut composer = ChatComposer::new(
true,
sender.clone(),
false,
"Ask Codex to do anything".to_string(),
);
if let Some(text) = input {
composer.handle_paste(text);
@@ -1011,7 +1021,8 @@ mod tests {
let (tx, rx) = std::sync::mpsc::channel();
let sender = AppEventSender::new(tx);
let mut composer = ChatComposer::new(true, sender, false);
let mut composer =
ChatComposer::new(true, sender, false, "Ask Codex to do anything".to_string());
// Type the slash command.
for ch in [
@@ -1054,7 +1065,8 @@ mod tests {
let (tx, rx) = std::sync::mpsc::channel();
let sender = AppEventSender::new(tx);
let mut composer = ChatComposer::new(true, sender, false);
let mut composer =
ChatComposer::new(true, sender, false, "Ask Codex to do anything".to_string());
for ch in ['/', 'm', 'e', 'n', 't', 'i', 'o', 'n'] {
let _ = composer.handle_key_event(KeyEvent::new(KeyCode::Char(ch), KeyModifiers::NONE));
@@ -1093,7 +1105,8 @@ mod tests {
let (tx, _rx) = std::sync::mpsc::channel();
let sender = AppEventSender::new(tx);
let mut composer = ChatComposer::new(true, sender, false);
let mut composer =
ChatComposer::new(true, sender, false, "Ask Codex to do anything".to_string());
// Define test cases: (paste content, is_large)
let test_cases = [
@@ -1166,7 +1179,8 @@ mod tests {
let (tx, _rx) = std::sync::mpsc::channel();
let sender = AppEventSender::new(tx);
let mut composer = ChatComposer::new(true, sender, false);
let mut composer =
ChatComposer::new(true, sender, false, "Ask Codex to do anything".to_string());
// Define test cases: (content, is_large)
let test_cases = [
@@ -1232,7 +1246,8 @@ mod tests {
let (tx, _rx) = std::sync::mpsc::channel();
let sender = AppEventSender::new(tx);
let mut composer = ChatComposer::new(true, sender, false);
let mut composer =
ChatComposer::new(true, sender, false, "Ask Codex to do anything".to_string());
// Define test cases: (cursor_position_from_end, expected_pending_count)
let test_cases = [

View File

@@ -58,6 +58,7 @@ pub(crate) struct BottomPaneParams {
pub(crate) app_event_tx: AppEventSender,
pub(crate) has_input_focus: bool,
pub(crate) enhanced_keys_supported: bool,
pub(crate) placeholder_text: String,
}
impl BottomPane<'_> {
@@ -69,6 +70,7 @@ impl BottomPane<'_> {
params.has_input_focus,
params.app_event_tx.clone(),
enhanced_keys_supported,
params.placeholder_text,
),
active_view: None,
app_event_tx: params.app_event_tx,
@@ -352,6 +354,7 @@ mod tests {
app_event_tx: tx,
has_input_focus: true,
enhanced_keys_supported: false,
placeholder_text: "Ask Codex to do anything".to_string(),
});
pane.push_approval_request(exec_request());
assert_eq!(CancellationEvent::Handled, pane.on_ctrl_c());
@@ -369,6 +372,7 @@ mod tests {
app_event_tx: tx,
has_input_focus: true,
enhanced_keys_supported: false,
placeholder_text: "Ask Codex to do anything".to_string(),
});
// Create an approval modal (active view).
@@ -397,6 +401,7 @@ mod tests {
app_event_tx: tx.clone(),
has_input_focus: true,
enhanced_keys_supported: false,
placeholder_text: "Ask Codex to do anything".to_string(),
});
// Start a running task so the status indicator replaces the composer.
@@ -446,6 +451,7 @@ mod tests {
app_event_tx: tx,
has_input_focus: true,
enhanced_keys_supported: false,
placeholder_text: "Ask Codex to do anything".to_string(),
});
// Begin a task: show initial status.
@@ -477,6 +483,7 @@ mod tests {
app_event_tx: tx,
has_input_focus: true,
enhanced_keys_supported: false,
placeholder_text: "Ask Codex to do anything".to_string(),
});
// Activate spinner (status view replaces composer) with no live ring.
@@ -528,6 +535,7 @@ mod tests {
app_event_tx: tx,
has_input_focus: true,
enhanced_keys_supported: false,
placeholder_text: "Ask Codex to do anything".to_string(),
});
pane.set_task_running(true);

View File

@@ -932,33 +932,33 @@ mod tests {
use rand::prelude::*;
fn rand_grapheme(rng: &mut rand::rngs::StdRng) -> String {
let r: u8 = rng.gen_range(0..100);
let r: u8 = rng.random_range(0..100);
match r {
0..=4 => "\n".to_string(),
5..=12 => " ".to_string(),
13..=35 => (rng.gen_range(b'a'..=b'z') as char).to_string(),
36..=45 => (rng.gen_range(b'A'..=b'Z') as char).to_string(),
46..=52 => (rng.gen_range(b'0'..=b'9') as char).to_string(),
13..=35 => (rng.random_range(b'a'..=b'z') as char).to_string(),
36..=45 => (rng.random_range(b'A'..=b'Z') as char).to_string(),
46..=52 => (rng.random_range(b'0'..=b'9') as char).to_string(),
53..=65 => {
// Some emoji (wide graphemes)
let choices = ["👍", "😊", "🐍", "🚀", "🧪", "🌟"];
choices[rng.gen_range(0..choices.len())].to_string()
choices[rng.random_range(0..choices.len())].to_string()
}
66..=75 => {
// CJK wide characters
let choices = ["", "", "", "", "", "", "", "", ""];
choices[rng.gen_range(0..choices.len())].to_string()
choices[rng.random_range(0..choices.len())].to_string()
}
76..=85 => {
// Combining mark sequences
let base = ["e", "a", "o", "n", "u"][rng.gen_range(0..5)];
let base = ["e", "a", "o", "n", "u"][rng.random_range(0..5)];
let marks = ["\u{0301}", "\u{0308}", "\u{0302}", "\u{0303}"];
format!("{}{}", base, marks[rng.gen_range(0..marks.len())])
format!("{base}{}", marks[rng.random_range(0..marks.len())])
}
86..=92 => {
// Some non-latin single codepoints (Greek, Cyrillic, Hebrew)
let choices = ["Ω", "β", "Ж", "ю", "ש", "م", ""];
choices[rng.gen_range(0..choices.len())].to_string()
choices[rng.random_range(0..choices.len())].to_string()
}
_ => {
// ZWJ sequences (single graphemes but multi-codepoint)
@@ -967,7 +967,7 @@ mod tests {
"👨\u{200D}💻", // man technologist
"🏳️\u{200D}🌈", // rainbow flag
];
choices[rng.gen_range(0..choices.len())].to_string()
choices[rng.random_range(0..choices.len())].to_string()
}
}
}
@@ -1447,7 +1447,7 @@ mod tests {
let mut elem_texts: Vec<String> = Vec::new();
let mut next_elem_id: usize = 0;
// Start with a random base string
let base_len = rng.gen_range(0..30);
let base_len = rng.random_range(0..30);
let mut base = String::new();
for _ in 0..base_len {
base.push_str(&rand_grapheme(&mut rng));
@@ -1457,26 +1457,26 @@ mod tests {
let mut boundaries: Vec<usize> = vec![0];
boundaries.extend(ta.text().char_indices().map(|(i, _)| i).skip(1));
boundaries.push(ta.text().len());
let init = boundaries[rng.gen_range(0..boundaries.len())];
let init = boundaries[rng.random_range(0..boundaries.len())];
ta.set_cursor(init);
let mut width: u16 = rng.gen_range(1..=12);
let mut height: u16 = rng.gen_range(1..=4);
let mut width: u16 = rng.random_range(1..=12);
let mut height: u16 = rng.random_range(1..=4);
for _step in 0..200 {
// Mostly stable width/height, occasionally change
if rng.gen_bool(0.1) {
width = rng.gen_range(1..=12);
if rng.random_bool(0.1) {
width = rng.random_range(1..=12);
}
if rng.gen_bool(0.1) {
height = rng.gen_range(1..=4);
if rng.random_bool(0.1) {
height = rng.random_range(1..=4);
}
// Pick an operation
match rng.gen_range(0..18) {
match rng.random_range(0..18) {
0 => {
// insert small random string at cursor
let len = rng.gen_range(0..6);
let len = rng.random_range(0..6);
let mut s = String::new();
for _ in 0..len {
s.push_str(&rand_grapheme(&mut rng));
@@ -1488,14 +1488,14 @@ mod tests {
let mut b: Vec<usize> = vec![0];
b.extend(ta.text().char_indices().map(|(i, _)| i).skip(1));
b.push(ta.text().len());
let i1 = rng.gen_range(0..b.len());
let i2 = rng.gen_range(0..b.len());
let i1 = rng.random_range(0..b.len());
let i2 = rng.random_range(0..b.len());
let (start, end) = if b[i1] <= b[i2] {
(b[i1], b[i2])
} else {
(b[i2], b[i1])
};
let insert_len = rng.gen_range(0..=4);
let insert_len = rng.random_range(0..=4);
let mut s = String::new();
for _ in 0..insert_len {
s.push_str(&rand_grapheme(&mut rng));
@@ -1520,8 +1520,8 @@ mod tests {
);
}
}
2 => ta.delete_backward(rng.gen_range(0..=3)),
3 => ta.delete_forward(rng.gen_range(0..=3)),
2 => ta.delete_backward(rng.random_range(0..=3)),
3 => ta.delete_forward(rng.random_range(0..=3)),
4 => ta.delete_backward_word(),
5 => ta.kill_to_beginning_of_line(),
6 => ta.kill_to_end_of_line(),
@@ -1534,7 +1534,7 @@ mod tests {
13 => {
// Insert an element with a unique sentinel payload
let payload =
format!("[[EL#{}:{}]]", next_elem_id, rng.gen_range(1000..9999));
format!("[[EL#{}:{}]]", next_elem_id, rng.random_range(1000..9999));
next_elem_id += 1;
ta.insert_element(&payload);
elem_texts.push(payload);
@@ -1545,7 +1545,7 @@ mod tests {
if let Some(start) = ta.text().find(&payload) {
let end = start + payload.len();
if end - start > 2 {
let pos = rng.gen_range(start + 1..end - 1);
let pos = rng.random_range(start + 1..end - 1);
let ins = rand_grapheme(&mut rng);
ta.insert_str_at(pos, &ins);
}
@@ -1558,8 +1558,8 @@ mod tests {
if let Some(start) = ta.text().find(&payload) {
let end = start + payload.len();
// Create an intersecting range [start-δ, end-δ2)
let mut s = start.saturating_sub(rng.gen_range(0..=2));
let mut e = (end + rng.gen_range(0..=2)).min(ta.text().len());
let mut s = start.saturating_sub(rng.random_range(0..=2));
let mut e = (end + rng.random_range(0..=2)).min(ta.text().len());
// Align to char boundaries to satisfy String::replace_range contract
let txt = ta.text();
while s > 0 && !txt.is_char_boundary(s) {
@@ -1571,7 +1571,7 @@ mod tests {
if s < e {
// Small replacement text
let mut srep = String::new();
for _ in 0..rng.gen_range(0..=2) {
for _ in 0..rng.random_range(0..=2) {
srep.push_str(&rand_grapheme(&mut rng));
}
ta.replace_range(s..e, &srep);
@@ -1585,7 +1585,7 @@ mod tests {
if let Some(start) = ta.text().find(&payload) {
let end = start + payload.len();
if end - start > 2 {
let pos = rng.gen_range(start + 1..end - 1);
let pos = rng.random_range(start + 1..end - 1);
ta.set_cursor(pos);
}
}
@@ -1593,7 +1593,7 @@ mod tests {
}
_ => {
// Jump to word boundaries
if rng.gen_bool(0.5) {
if rng.random_bool(0.5) {
let p = ta.beginning_of_previous_word();
ta.set_cursor(p);
} else {

View File

@@ -28,6 +28,7 @@ use codex_core::protocol::TurnDiffEvent;
use codex_protocol::parse_command::ParsedCommand;
use crossterm::event::KeyEvent;
use crossterm::event::KeyEventKind;
use rand::Rng;
use ratatui::buffer::Buffer;
use ratatui::layout::Constraint;
use ratatui::layout::Layout;
@@ -494,6 +495,8 @@ impl ChatWidget<'_> {
initial_images: Vec<PathBuf>,
enhanced_keys_supported: bool,
) -> Self {
let mut rng = rand::rng();
let placeholder = EXAMPLE_PROMPTS[rng.random_range(0..EXAMPLE_PROMPTS.len())].to_string();
let codex_op_tx = spawn_agent(config.clone(), app_event_tx.clone(), conversation_manager);
Self {
@@ -503,6 +506,7 @@ impl ChatWidget<'_> {
app_event_tx,
has_input_focus: true,
enhanced_keys_supported,
placeholder_text: placeholder,
}),
active_exec_cell: None,
config: config.clone(),
@@ -680,10 +684,6 @@ impl ChatWidget<'_> {
));
}
pub(crate) fn add_prompts_output(&mut self) {
self.add_to_history(&history_cell::new_prompts_output());
}
/// Forward file-search results to the bottom pane.
pub(crate) fn apply_file_search_result(&mut self, query: String, matches: Vec<FileMatch>) {
self.bottom_pane.on_file_search_result(query, matches);
@@ -764,6 +764,15 @@ impl WidgetRef for &ChatWidget<'_> {
}
}
const EXAMPLE_PROMPTS: [&str; 6] = [
"Explain this codebase",
"Summarize recent commits",
"Implement {feature}",
"Find and fix a bug in @filename",
"Write tests for @filename",
"Improve documentation in @filename",
];
fn add_token_usage(current_usage: &TokenUsage, new_usage: &TokenUsage) -> TokenUsage {
let cached_input_tokens = match (
current_usage.cached_input_tokens,

View File

@@ -124,6 +124,7 @@ fn make_chatwidget_manual() -> (
app_event_tx: app_event_tx.clone(),
has_input_focus: true,
enhanced_keys_supported: false,
placeholder_text: "Ask Codex to do anything".to_string(),
});
let widget = ChatWidget {
app_event_tx,

View File

@@ -170,7 +170,6 @@ pub(crate) fn new_session_info(
Line::from(format!(" /init - {}", SlashCommand::Init.description()).dim()),
Line::from(format!(" /status - {}", SlashCommand::Status.description()).dim()),
Line::from(format!(" /diff - {}", SlashCommand::Diff.description()).dim()),
Line::from(format!(" /prompts - {}", SlashCommand::Prompts.description()).dim()),
Line::from("".dim()),
];
PlainHistoryCell { lines }
@@ -635,21 +634,6 @@ pub(crate) fn new_status_output(
PlainHistoryCell { lines }
}
pub(crate) fn new_prompts_output() -> PlainHistoryCell {
let lines: Vec<Line<'static>> = vec![
Line::from("/prompts".magenta()),
Line::from(""),
Line::from(" 1. Explain this codebase"),
Line::from(" 2. Summarize recent commits"),
Line::from(" 3. Implement {feature}"),
Line::from(" 4. Find and fix a bug in @filename"),
Line::from(" 5. Write tests for @filename"),
Line::from(" 6. Improve documentation in @filename"),
Line::from(""),
];
PlainHistoryCell { lines }
}
pub(crate) fn new_error_event(message: String) -> PlainHistoryCell {
let lines: Vec<Line<'static>> = vec![vec!["🖐 ".red().bold(), message.into()].into(), "".into()];
PlainHistoryCell { lines }

View File

@@ -18,7 +18,6 @@ pub enum SlashCommand {
Diff,
Mention,
Status,
Prompts,
Logout,
Quit,
#[cfg(debug_assertions)]
@@ -36,7 +35,6 @@ impl SlashCommand {
SlashCommand::Diff => "show git diff (including untracked files)",
SlashCommand::Mention => "mention a file",
SlashCommand::Status => "show current session configuration and token usage",
SlashCommand::Prompts => "show example prompts",
SlashCommand::Logout => "log out of Codex",
#[cfg(debug_assertions)]
SlashCommand::TestApproval => "test approval request",