diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 4eefc375..67f0199c 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -930,7 +930,7 @@ dependencies = [ "once_cell", "path-clean", "pretty_assertions", - "rand 0.8.5", + "rand 0.9.2", "ratatui", "ratatui-image", "regex-lite", diff --git a/codex-rs/tui/Cargo.toml b/codex-rs/tui/Cargo.toml index 75aa6f8e..78a6a778 100644 --- a/codex-rs/tui/Cargo.toml +++ b/codex-rs/tui/Cargo.toml @@ -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" diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 3d12ad54..08889b66 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -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; diff --git a/codex-rs/tui/src/bottom_pane/approval_modal_view.rs b/codex-rs/tui/src/bottom_pane/approval_modal_view.rs index 57bb0ad7..0cfc24d7 100644 --- a/codex-rs/tui/src/bottom_pane/approval_modal_view.rs +++ b/codex-rs/tui/src/bottom_pane/approval_modal_view.rs @@ -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()); diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index 601f4d48..c801d6fd 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -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, 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 = [ diff --git a/codex-rs/tui/src/bottom_pane/mod.rs b/codex-rs/tui/src/bottom_pane/mod.rs index 85ed59de..c05e9e94 100644 --- a/codex-rs/tui/src/bottom_pane/mod.rs +++ b/codex-rs/tui/src/bottom_pane/mod.rs @@ -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); diff --git a/codex-rs/tui/src/bottom_pane/textarea.rs b/codex-rs/tui/src/bottom_pane/textarea.rs index d29698d7..33cdbbbc 100644 --- a/codex-rs/tui/src/bottom_pane/textarea.rs +++ b/codex-rs/tui/src/bottom_pane/textarea.rs @@ -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 = 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 = 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 = 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 { diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index b7bd380b..84aee144 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -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, 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) { 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, diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index eb7142ac..192ab3c6 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -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, diff --git a/codex-rs/tui/src/history_cell.rs b/codex-rs/tui/src/history_cell.rs index bc90ad34..faf01c5e 100644 --- a/codex-rs/tui/src/history_cell.rs +++ b/codex-rs/tui/src/history_cell.rs @@ -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> = 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> = vec![vec!["๐Ÿ– ".red().bold(), message.into()].into(), "".into()]; PlainHistoryCell { lines } diff --git a/codex-rs/tui/src/slash_command.rs b/codex-rs/tui/src/slash_command.rs index 0de1f6fa..71d60237 100644 --- a/codex-rs/tui/src/slash_command.rs +++ b/codex-rs/tui/src/slash_command.rs @@ -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",