Codex Handbook
tui/src/chatwidget/input_flow.rs 217 lines
//! 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();                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 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                .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()    }}