mirror of
https://github.com/openai/codex.git
synced 2026-10-20 17:38:45 +00:00
## Why The TUI treats both an active turn and MCP startup as a running task. That currently blocks `/resume` and several settings commands even though they do not compete with turn execution, which is especially frustrating when MCP startup is slow. Model, permissions, personality, and service-tier selections already update thread settings independently of the running turn. Other clients can send those updates mid-turn, while the current turn continues with its captured settings. Allowing the same updates from local slash commands makes the TUI consistent with that existing behavior. ## What changed - Allow `/resume` while a task is running. - Allow `/model`, `/permissions`, `/personality`, and service-tier commands such as `/fast` while a task is running. - Keep the existing behavior where the active turn uses its captured settings and updates apply to subsequent turns. - Exercise the commands under the busy state in the existing TUI tests and retain coverage for commands that should remain blocked. ## Behavior note Turn settings such as model selection and reasoning effort are captured when a turn starts. Changing them during an active turn affects the next turn, not the turn already in progress. The status bar updates immediately, so it may temporarily display the newly selected setting before that setting is actually in effect. ## Verification - Focused `codex-tui` tests for resume dispatch, settings popups, `/fast`, and disabled-command behavior. ## Related issues Closes #19015. Addresses the next-turn-safe model/reasoning switching portion of #14356; dedicated shortcuts and the proposed depth meter remain out of scope.
236 lines
8.8 KiB
Rust
236 lines
8.8 KiB
Rust
//! User input submission, queue draining, and draft restore flow for `ChatWidget`.
|
|
//!
|
|
//! The queue data itself lives in `input_queue`; this module owns the app-level
|
|
//! effects around taking composer input, submitting user turns, draining queued
|
|
//! follow-ups, and restoring draft state across interrupts or thread switches.
|
|
|
|
use super::*;
|
|
|
|
impl ChatWidget {
|
|
pub(super) fn handle_composer_input_result(
|
|
&mut self,
|
|
input_result: InputResult,
|
|
had_modal_or_popup: bool,
|
|
) {
|
|
match input_result {
|
|
InputResult::Submitted {
|
|
text,
|
|
text_elements,
|
|
} => {
|
|
let user_message = self.user_message_from_submission(text, text_elements);
|
|
if user_message.text.is_empty()
|
|
&& user_message.local_images.is_empty()
|
|
&& user_message.remote_image_urls.is_empty()
|
|
{
|
|
return;
|
|
}
|
|
let should_submit_now = self.is_session_configured()
|
|
&& !self.is_plan_streaming_in_tui()
|
|
&& !self.input_queue.suppress_queue_autosend;
|
|
if should_submit_now {
|
|
if self.only_user_shell_commands_running()
|
|
&& !user_message.text.starts_with('!')
|
|
{
|
|
self.queue_user_message(user_message);
|
|
return;
|
|
}
|
|
// Submitted is emitted when user submits.
|
|
// Reset any reasoning header only when we are actually submitting a turn.
|
|
self.reasoning_buffer.clear();
|
|
self.full_reasoning_buffer.clear();
|
|
self.set_status_header(String::from("Working"));
|
|
self.submit_user_message(user_message);
|
|
} else {
|
|
self.queue_user_message(user_message);
|
|
}
|
|
}
|
|
InputResult::Queued {
|
|
text,
|
|
text_elements,
|
|
action,
|
|
pending_pastes,
|
|
} => {
|
|
let user_message = self.user_message_from_submission(text, text_elements);
|
|
self.queue_user_message_with_options(user_message, action, pending_pastes);
|
|
}
|
|
InputResult::Command(cmd) => {
|
|
self.handle_slash_command_dispatch(cmd);
|
|
}
|
|
InputResult::ServiceTierCommand(command) => {
|
|
self.handle_service_tier_command_dispatch(command);
|
|
}
|
|
InputResult::CommandWithArgs(cmd, args, text_elements) => {
|
|
self.handle_slash_command_with_args_dispatch(cmd, args, text_elements);
|
|
}
|
|
InputResult::None => {}
|
|
}
|
|
if had_modal_or_popup && self.bottom_pane.no_modal_or_popup_active() {
|
|
self.maybe_send_next_queued_input();
|
|
}
|
|
self.refresh_plan_mode_nudge();
|
|
}
|
|
|
|
pub(super) fn defer_input_until_settings_applied(&mut self) {
|
|
if !self.bottom_pane.no_modal_or_popup_active() {
|
|
self.input_queue.suppress_queue_autosend = true;
|
|
}
|
|
}
|
|
|
|
pub(super) fn on_modal_or_popup_closed(&mut self) {
|
|
if self.input_queue.suppress_queue_autosend {
|
|
self.app_event_tx.send(AppEvent::SettingsSelectionClosed);
|
|
} else {
|
|
self.maybe_send_next_queued_input();
|
|
}
|
|
}
|
|
|
|
pub(super) fn queue_user_message(&mut self, user_message: UserMessage) {
|
|
self.queue_user_message_with_options(user_message, QueuedInputAction::Plain, Vec::new());
|
|
}
|
|
|
|
pub(crate) fn set_queue_submissions_until_session_configured(&mut self, queue: bool) {
|
|
self.bottom_pane
|
|
.set_queue_submissions(queue && !self.is_session_configured());
|
|
}
|
|
|
|
pub(super) fn queue_user_message_with_options(
|
|
&mut self,
|
|
user_message: UserMessage,
|
|
action: QueuedInputAction,
|
|
pending_pastes: Vec<(String, String)>,
|
|
) {
|
|
if !self.is_session_configured()
|
|
|| self.is_user_turn_pending_or_running()
|
|
|| self.input_queue.suppress_queue_autosend
|
|
{
|
|
self.input_queue
|
|
.queued_user_messages
|
|
.push_back(QueuedUserMessage {
|
|
user_message,
|
|
action,
|
|
pending_pastes,
|
|
});
|
|
self.input_queue
|
|
.queued_user_message_history_records
|
|
.push_back(UserMessageHistoryRecord::UserMessageText);
|
|
self.refresh_pending_input_preview();
|
|
} else {
|
|
self.submit_user_message(user_message);
|
|
}
|
|
}
|
|
|
|
/// If idle and there are queued inputs, submit exactly one to start the next turn.
|
|
pub(crate) fn maybe_send_next_queued_input(&mut self) -> bool {
|
|
if self.input_queue.suppress_queue_autosend {
|
|
return false;
|
|
}
|
|
if self.is_user_turn_pending_or_running() {
|
|
return false;
|
|
}
|
|
let mut submitted_follow_up = false;
|
|
while !self.is_user_turn_pending_or_running() {
|
|
let Some((queued_message, history_record)) = self.pop_next_queued_user_message() else {
|
|
break;
|
|
};
|
|
match queued_message.action {
|
|
QueuedInputAction::Plain => {
|
|
submitted_follow_up = self.submit_user_message_with_history_record(
|
|
queued_message.into_user_message(),
|
|
history_record,
|
|
);
|
|
break;
|
|
}
|
|
QueuedInputAction::ParseSlash => {
|
|
let drain = self.submit_queued_slash_prompt(queued_message);
|
|
if drain == QueueDrain::Stop {
|
|
submitted_follow_up = self.is_user_turn_pending_or_running();
|
|
break;
|
|
}
|
|
}
|
|
QueuedInputAction::RunShell => {
|
|
let drain = self.submit_queued_shell_prompt(queued_message.into_user_message());
|
|
if drain == QueueDrain::Stop {
|
|
submitted_follow_up = self.is_user_turn_pending_or_running();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Update the list to reflect the remaining queued messages (if any).
|
|
self.refresh_pending_input_preview();
|
|
submitted_follow_up
|
|
}
|
|
|
|
pub(super) fn is_user_turn_pending_or_running(&self) -> bool {
|
|
self.input_queue.user_turn_pending_start || self.bottom_pane.is_task_running()
|
|
}
|
|
|
|
pub(super) fn only_user_shell_commands_running(&self) -> bool {
|
|
self.turn_lifecycle.agent_turn_running
|
|
&& !self.running_commands.is_empty()
|
|
&& self
|
|
.running_commands
|
|
.values()
|
|
.all(|command| command.source == ExecCommandSource::UserShell)
|
|
}
|
|
|
|
/// Rebuild and update the bottom-pane pending-input preview.
|
|
pub(super) fn refresh_pending_input_preview(&mut self) {
|
|
let preview = self.input_queue.preview();
|
|
self.bottom_pane.set_pending_input_preview(
|
|
preview.queued_messages,
|
|
preview.pending_steers,
|
|
preview.rejected_steers,
|
|
);
|
|
}
|
|
|
|
pub(crate) fn submit_user_message_with_mode(
|
|
&mut self,
|
|
text: String,
|
|
mut collaboration_mode: CollaborationModeMask,
|
|
) {
|
|
if collaboration_mode.mode == Some(ModeKind::Plan)
|
|
&& let Some(effort) = self.config.plan_mode_reasoning_effort.clone()
|
|
{
|
|
collaboration_mode.reasoning_effort = Some(Some(effort));
|
|
}
|
|
if self.turn_lifecycle.agent_turn_running
|
|
&& self.active_collaboration_mask.as_ref() != Some(&collaboration_mode)
|
|
{
|
|
self.add_error_message(
|
|
"Cannot switch collaboration mode while a turn is running.".to_string(),
|
|
);
|
|
return;
|
|
}
|
|
self.set_collaboration_mask_from_user_action(collaboration_mode);
|
|
let should_queue = self.is_plan_streaming_in_tui();
|
|
let user_message = UserMessage {
|
|
text,
|
|
local_images: Vec::new(),
|
|
remote_image_urls: Vec::new(),
|
|
text_elements: Vec::new(),
|
|
mention_bindings: Vec::new(),
|
|
};
|
|
if should_queue {
|
|
self.queue_user_message(user_message);
|
|
} else {
|
|
self.submit_user_message(user_message);
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
pub(crate) fn queued_user_message_texts(&self) -> Vec<String> {
|
|
self.input_queue
|
|
.rejected_steers_queue
|
|
.iter()
|
|
.map(|message| message.text.clone())
|
|
.chain(
|
|
self.input_queue
|
|
.queued_user_messages
|
|
.iter()
|
|
.map(|message| message.text.clone()),
|
|
)
|
|
.collect()
|
|
}
|
|
}
|