mirror of
https://github.com/openai/codex.git
synced 2026-10-19 17:28:46 +00:00
## Why The startup composer accepted edits but ignored submission. Its first frame also preceded configured screen settings, and startup failures could leave typed text unavailable. ## What changed - Honor configured submit bindings during startup, retaining one confirmation until the session and protected input screens are ready. Repeated submit keys do not duplicate the message; edits, `Esc`, disconnects, or destination changes cancel confirmation while preserving the draft. - Apply local screen, status-line, keymap, and paste settings before the first editable frame. Anchor the composer at the bottom in owned-screen mode and retain the loading frame until resumed history is ready to render. - Keep an in-memory recovery copy through submission acknowledgment. On startup failure, restore the terminal before printing unsent text, expanding paste placeholders and escaping terminal control characters. ## Testing Add regression coverage for deferred single submission, configured key chords, cancellation, protected input, paste handling, draft recovery, initial screen settings, and loading-frame handoff, plus a startup layout snapshot. GitOrigin-RevId: 1eb4c63027f7d59af5d42d8545cd5cb5c3594306
169 lines
6.4 KiB
Rust
169 lines
6.4 KiB
Rust
//! Queued user input and pending-steer state for `ChatWidget`.
|
|
//!
|
|
//! This module keeps the mutable input queues together so `ChatWidget` can
|
|
//! apply UI/protocol effects around a focused reducer-style state bag.
|
|
|
|
use std::collections::VecDeque;
|
|
|
|
use super::PendingSteer;
|
|
use super::QueuedUserMessage;
|
|
use super::UserMessage;
|
|
use super::UserMessageHistoryRecord;
|
|
use super::UserMessageSource;
|
|
use super::user_message_preview_text;
|
|
|
|
#[derive(Debug, Default, PartialEq, Eq)]
|
|
pub(super) struct PendingInputPreview {
|
|
pub(super) queued_messages: Vec<String>,
|
|
pub(super) pending_steers: Vec<String>,
|
|
pub(super) rejected_steers: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug, Default)]
|
|
pub(super) struct InputQueueState {
|
|
/// The visible draft confirmed during startup, awaiting the protected-input handoff.
|
|
pub(super) startup_submission: Option<crate::bottom_pane::ComposerDraftSnapshot>,
|
|
/// User inputs queued while a turn is in progress.
|
|
pub(super) queued_user_messages: VecDeque<QueuedUserMessage>,
|
|
/// History records for queued user messages. Slash commands such as `/goal`
|
|
/// can render history that differs from the text submitted to core, so this
|
|
/// stays in lockstep with `queued_user_messages`, with missing entries
|
|
/// treated as user-message text.
|
|
pub(super) queued_user_message_history_records: VecDeque<UserMessageHistoryRecord>,
|
|
/// A user turn has been submitted to core, but `TurnStarted` has not arrived yet.
|
|
pub(super) user_turn_pending_start: bool,
|
|
/// User messages that tried to steer a non-regular turn and must be retried first.
|
|
pub(super) rejected_steers_queue: VecDeque<UserMessage>,
|
|
/// The origin of each rejected steer, kept in lockstep with its message.
|
|
pub(super) rejected_steer_sources: VecDeque<UserMessageSource>,
|
|
/// History records for rejected steers. Slash commands such as `/goal` can
|
|
/// render history that differs from the text submitted to core, so this stays
|
|
/// in lockstep with `rejected_steers_queue`, with missing entries treated as
|
|
/// user-message text.
|
|
pub(super) rejected_steer_history_records: VecDeque<UserMessageHistoryRecord>,
|
|
/// Steers already submitted to core but not yet committed into history.
|
|
pub(super) pending_steers: VecDeque<PendingSteer>,
|
|
/// When set, the next interrupt should resubmit all pending steers as one
|
|
/// fresh user turn instead of restoring them into the composer.
|
|
pub(super) submit_pending_steers_after_interrupt: bool,
|
|
pub(super) suppress_queue_autosend: bool,
|
|
/// Hold submissions while a usage failure or backend-directed model fallback is resolved.
|
|
pub(super) rate_limit_recovery_pending: bool,
|
|
pub(super) recovered_queue: bool,
|
|
}
|
|
|
|
impl InputQueueState {
|
|
pub(super) fn has_queued_follow_up_messages(&self) -> bool {
|
|
!self.rejected_steers_queue.is_empty() || !self.queued_user_messages.is_empty()
|
|
}
|
|
|
|
pub(super) fn clear(&mut self) {
|
|
self.startup_submission = None;
|
|
self.recovered_queue = false;
|
|
self.queued_user_messages.clear();
|
|
self.queued_user_message_history_records.clear();
|
|
self.user_turn_pending_start = false;
|
|
self.rejected_steers_queue.clear();
|
|
self.rejected_steer_sources.clear();
|
|
self.rejected_steer_history_records.clear();
|
|
self.pending_steers.clear();
|
|
self.submit_pending_steers_after_interrupt = false;
|
|
self.rate_limit_recovery_pending = false;
|
|
}
|
|
|
|
pub(super) fn preview(&self) -> PendingInputPreview {
|
|
let queued_messages = self
|
|
.queued_user_messages
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(idx, message)| {
|
|
user_message_preview_text(
|
|
message,
|
|
self.queued_user_message_history_records.get(idx),
|
|
)
|
|
})
|
|
.collect();
|
|
let pending_steers = self
|
|
.pending_steers
|
|
.iter()
|
|
.map(|steer| {
|
|
user_message_preview_text(&steer.user_message, Some(&steer.history_record))
|
|
})
|
|
.collect();
|
|
let rejected_steers = self
|
|
.rejected_steers_queue
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(idx, message)| {
|
|
user_message_preview_text(message, self.rejected_steer_history_records.get(idx))
|
|
})
|
|
.collect();
|
|
|
|
PendingInputPreview {
|
|
queued_messages,
|
|
pending_steers,
|
|
rejected_steers,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use pretty_assertions::assert_eq;
|
|
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn preview_keeps_queue_categories_separate() {
|
|
let mut state = InputQueueState::default();
|
|
state
|
|
.queued_user_messages
|
|
.push_back(UserMessage::from("queued").into());
|
|
state
|
|
.rejected_steers_queue
|
|
.push_back(UserMessage::from("rejected"));
|
|
state.pending_steers.push_back(PendingSteer {
|
|
client_id: "test-submission".to_string(),
|
|
user_message: UserMessage::from("pending"),
|
|
history_record: UserMessageHistoryRecord::UserMessageText,
|
|
source: UserMessageSource::Prompt,
|
|
compare_key: crate::chatwidget::user_messages::PendingSteerCompareKey {
|
|
message: "pending".to_string(),
|
|
image_count: 0,
|
|
},
|
|
});
|
|
|
|
assert_eq!(
|
|
state.preview(),
|
|
PendingInputPreview {
|
|
queued_messages: vec!["queued".to_string()],
|
|
pending_steers: vec!["pending".to_string()],
|
|
rejected_steers: vec!["rejected".to_string()],
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn clear_resets_all_input_queues() {
|
|
let mut state = InputQueueState::default();
|
|
state
|
|
.queued_user_messages
|
|
.push_back(UserMessage::from("queued").into());
|
|
state
|
|
.rejected_steers_queue
|
|
.push_back(UserMessage::from("rejected"));
|
|
state.user_turn_pending_start = true;
|
|
state.submit_pending_steers_after_interrupt = true;
|
|
|
|
state.clear();
|
|
|
|
assert!(state.queued_user_messages.is_empty());
|
|
assert!(state.queued_user_message_history_records.is_empty());
|
|
assert!(!state.user_turn_pending_start);
|
|
assert!(state.rejected_steers_queue.is_empty());
|
|
assert!(state.rejected_steer_history_records.is_empty());
|
|
assert!(state.pending_steers.is_empty());
|
|
assert!(!state.submit_pending_steers_after_interrupt);
|
|
}
|
|
}
|