diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index e6a3266d6a..8f82267690 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -17,6 +17,9 @@ //! //! # Submission and Prompt Expansion //! +//! When steer is enabled, `Enter` submits immediately. `Tab` requests queuing while a task is +//! running; if no task is running, `Tab` submits just like Enter so input is never dropped. +//! //! On submit/queue paths, the composer: //! //! - Expands pending paste placeholders so element ranges align with the final text. @@ -373,7 +376,8 @@ impl ChatComposer { /// Enables or disables "Steer" behavior for submission keys. /// /// When steer is enabled, `Enter` produces [`InputResult::Submitted`] (send immediately) and - /// `Tab` produces [`InputResult::Queued`] (eligible to queue if a task is running). + /// `Tab` produces [`InputResult::Queued`] when a task is running; otherwise it submits + /// immediately. /// When steer is disabled, `Enter` produces [`InputResult::Queued`], preserving the default /// "queue while a task is running" behavior. pub fn set_steer_enabled(&mut self, enabled: bool) { @@ -2005,6 +2009,12 @@ impl ChatComposer { } self.handle_input_basic(key_event) } + KeyEvent { + code: KeyCode::Tab, + modifiers: KeyModifiers::NONE, + kind: KeyEventKind::Press, + .. + } if self.steer_enabled => self.handle_submission(self.is_task_running), KeyEvent { code: KeyCode::Tab, modifiers: KeyModifiers::NONE, @@ -4343,6 +4353,35 @@ mod tests { assert!(composer.textarea.is_empty()); } + #[test] + fn tab_submits_when_no_task_running_in_steer_mode() { + use crossterm::event::KeyCode; + use crossterm::event::KeyEvent; + use crossterm::event::KeyModifiers; + + let (tx, _rx) = unbounded_channel::(); + let sender = AppEventSender::new(tx); + let mut composer = ChatComposer::new( + true, + sender, + false, + "Ask Codex to do anything".to_string(), + false, + ); + composer.set_steer_enabled(true); + + type_chars_humanlike(&mut composer, &['h', 'i']); + + let (result, _needs_redraw) = + composer.handle_key_event(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE)); + + assert!(matches!( + result, + InputResult::Submitted { ref text, .. } if text == "hi" + )); + assert!(composer.textarea.is_empty()); + } + #[test] fn slash_mention_dispatches_command_and_inserts_at() { use crossterm::event::KeyCode; diff --git a/codex-rs/tui/tooltips.txt b/codex-rs/tui/tooltips.txt index 31819c6628..35c915d8df 100644 --- a/codex-rs/tui/tooltips.txt +++ b/codex-rs/tui/tooltips.txt @@ -12,6 +12,6 @@ Use /mcp to list configured MCP tools. You can run any shell command from Codex using `!` (e.g. `!ls`) Type / to open the command popup; Tab autocompletes slash commands. When the composer is empty, press Esc to step back and edit your last message; Enter confirms. -Press Tab to queue a message instead of sending it immediately; Enter always sends immediately. +Press Tab to queue a message when a task is running; otherwise it sends immediately. Paste an image with Ctrl+V to attach it to your next message. You can resume a previous conversation by running `codex resume` diff --git a/docs/tui-chat-composer.md b/docs/tui-chat-composer.md index baab4ce0ed..b258d32461 100644 --- a/docs/tui-chat-composer.md +++ b/docs/tui-chat-composer.md @@ -78,6 +78,9 @@ popup so gating stays in sync. There are multiple submission paths, but they share the same core rules: +When steer mode is enabled, `Tab` requests queuing if a task is already running; otherwise it +submits immediately. `Enter` always submits immediately in this mode. + ### Normal submit/queue path `handle_submission` calls `prepare_submission_text` for both submit and queue. That method: