fix(tui): tab submits when no task running in steer mode (#10035)
When steer mode is enabled, Tab used to only queue while a task was running and otherwise did nothing. Treat Tab as an immediate submit when no task is running so input isn't dropped when the inflight turn ends mid-typing. Adds a regression test and updates docs/tooltips.
This commit is contained in:
parent
c9271cdff2
commit
a3e4bd3bc0
3 changed files with 85 additions and 3 deletions
|
|
@ -28,6 +28,10 @@
|
|||
//!
|
||||
//! # 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.
|
||||
//! `Tab` does not submit when entering a `!` shell command.
|
||||
//!
|
||||
//! On submit/queue paths, the composer:
|
||||
//!
|
||||
//! - Expands pending paste placeholders so element ranges align with the final text.
|
||||
|
|
@ -444,7 +448,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. `Tab` does not submit when the input is a `!` shell command.
|
||||
/// 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) {
|
||||
|
|
@ -2383,7 +2388,17 @@ impl ChatComposer {
|
|||
modifiers: KeyModifiers::NONE,
|
||||
kind: KeyEventKind::Press,
|
||||
..
|
||||
} if self.is_task_running => self.handle_submission(true),
|
||||
} if self.steer_enabled && !self.is_bang_shell_command() => {
|
||||
self.handle_submission(self.is_task_running)
|
||||
}
|
||||
KeyEvent {
|
||||
code: KeyCode::Tab,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
kind: KeyEventKind::Press,
|
||||
..
|
||||
} if self.is_task_running && !self.is_bang_shell_command() => {
|
||||
self.handle_submission(true)
|
||||
}
|
||||
KeyEvent {
|
||||
code: KeyCode::Enter,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
|
|
@ -2396,6 +2411,10 @@ impl ChatComposer {
|
|||
}
|
||||
}
|
||||
|
||||
fn is_bang_shell_command(&self) -> bool {
|
||||
self.textarea.text().trim_start().starts_with('!')
|
||||
}
|
||||
|
||||
/// Applies any due `PasteBurst` flush at time `now`.
|
||||
///
|
||||
/// Converts [`PasteBurst::flush_if_due`] results into concrete textarea mutations.
|
||||
|
|
@ -5427,6 +5446,65 @@ mod tests {
|
|||
assert!(elements.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::<AppEvent>();
|
||||
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 tab_does_not_submit_for_bang_shell_command_in_steer_mode() {
|
||||
use crossterm::event::KeyCode;
|
||||
use crossterm::event::KeyEvent;
|
||||
use crossterm::event::KeyModifiers;
|
||||
|
||||
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,
|
||||
);
|
||||
composer.set_steer_enabled(true);
|
||||
composer.set_task_running(false);
|
||||
|
||||
type_chars_humanlike(&mut composer, &['!', 'l', 's']);
|
||||
|
||||
let (result, _needs_redraw) =
|
||||
composer.handle_key_event(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
|
||||
|
||||
assert!(matches!(result, InputResult::None));
|
||||
assert!(
|
||||
composer.textarea.text().starts_with("!ls"),
|
||||
"expected Tab not to submit or clear a `!` command"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slash_mention_dispatches_command_and_inserts_at() {
|
||||
use crossterm::event::KeyCode;
|
||||
|
|
|
|||
|
|
@ -18,6 +18,6 @@ Visit the Codex community forum: https://community.openai.com/c/codex/37
|
|||
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 (except `!`).
|
||||
Paste an image with Ctrl+V to attach it to your next message.
|
||||
You can resume a previous conversation by running `codex resume`
|
||||
|
|
|
|||
|
|
@ -96,6 +96,10 @@ 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. `Tab` does not submit when
|
||||
the input starts with `!` (shell command).
|
||||
|
||||
### Normal submit/queue path
|
||||
|
||||
`handle_submission` calls `prepare_submission_text` for both submit and queue. That method:
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue