mirror of
https://github.com/openai/codex.git
synced 2026-11-13 14:46:14 +00:00
Preserve queued TUI input semantics (#39604)
## Why Paste placeholders and deferred slash-command parsing can hide the meaning of an input until it is expanded or a session is configured. Draining such input as an ordinary message can accidentally treat pasted text beginning with `!` as a shell command or lose `/plan` behavior. ## What changed - Track paste-expanded inputs that begin with `!` as literal model input through history, queue editing, retries, and startup queues. - Defer `/plan` parsing when session or model state is unavailable, then submit it in plan mode with shell escapes disabled. - Drain queued input only after session configuration, and keep later queued prompts pending when an initial prompt cannot be submitted. - Suppress queue auto-send before applying a replayed thread session so input from the outgoing thread is not submitted during restoration. ## Testing Add coverage for direct, recalled, edited, interrupted, rejected, startup-queued, and plan-mode paste flows, including placeholder collisions and rejected initial images. GitOrigin-RevId: d482220d8df21754f166fea5b54e9945f622a978
This commit is contained in:
1 parent
8e7f646974
commit
3434c2545b
10 files changed
+519
-36
No files matched your search
@@ -77,6 +77,7 @@
|
||||
//!
|
||||
//! - Expands pending paste placeholders so element ranges align with the final text.
|
||||
//! - Trims whitespace and rebases text elements accordingly.
|
||||
//! - Treats a leading `!` revealed only by paste expansion as literal model input, not shell input.
|
||||
//! - Prunes local attached images so only placeholders that survive expansion are sent.
|
||||
//! - Preserves remote image URLs as separate attachments even when text is empty.
|
||||
//!
|
||||
@@ -424,6 +425,8 @@ fn parent_owned_command_is_allowed(command: SlashCommand, args: &str) -> bool {
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum QueuedInputAction {
|
||||
Plain,
|
||||
/// Preserve model-input provenance when paste expansion reveals a leading shell sigil.
|
||||
Literal,
|
||||
ParseSlash,
|
||||
RunShell,
|
||||
}
|
||||
@@ -3007,13 +3010,29 @@ impl ChatComposer {
|
||||
}
|
||||
self.draft.recent_submission_mention_bindings = original_mention_bindings.clone();
|
||||
if record_history && (!text.is_empty() || !self.attachments.is_empty()) {
|
||||
let preserve_literal_paste = self.slash_commands_enabled()
|
||||
&& text.starts_with('!')
|
||||
&& !original_input.trim_start().starts_with('!');
|
||||
let (history_text, history_text_elements) = if preserve_literal_paste {
|
||||
let history_text = original_input.trim().to_string();
|
||||
let history_text_elements = Self::trim_text_elements(
|
||||
&original_input,
|
||||
&history_text,
|
||||
original_text_elements,
|
||||
);
|
||||
(history_text, history_text_elements)
|
||||
} else {
|
||||
(text.clone(), text_elements.clone())
|
||||
};
|
||||
self.history.record_local_submission(HistoryEntry {
|
||||
text: text.clone(),
|
||||
text_elements: text_elements.clone(),
|
||||
text: history_text,
|
||||
text_elements: history_text_elements,
|
||||
local_image_paths: self.attachments.local_image_paths(),
|
||||
remote_image_urls: self.attachments.remote_image_urls(),
|
||||
mention_bindings: original_mention_bindings,
|
||||
pending_pastes: if pending_paste_handling == PendingPasteHandling::Preserve {
|
||||
pending_pastes: if pending_paste_handling == PendingPasteHandling::Preserve
|
||||
|| preserve_literal_paste
|
||||
{
|
||||
original_pending_pastes.clone()
|
||||
} else {
|
||||
Vec::new()
|
||||
@@ -3091,6 +3110,10 @@ impl ChatComposer {
|
||||
if let Some(pasted) = self.draft.paste_burst.flush_before_modified_input() {
|
||||
self.handle_paste(pasted);
|
||||
}
|
||||
let visible_shell_command = self.is_bang_shell_command();
|
||||
let original_input = self.current_text();
|
||||
let original_text_elements = self.current_text_elements();
|
||||
let original_pending_pastes = self.draft.pending_pastes.clone();
|
||||
let raw_text = self.draft.textarea.text();
|
||||
let defer_slash_validation = self.slash_input().should_parse_on_dequeue(raw_text);
|
||||
let preserve_pending_pastes = defer_slash_validation
|
||||
@@ -3115,7 +3138,20 @@ impl ChatComposer {
|
||||
PendingPasteHandling::Expand
|
||||
},
|
||||
) {
|
||||
let action = slash_input::queued_input_action(&text, defer_slash_validation);
|
||||
let action = if text.starts_with('!') && !visible_shell_command {
|
||||
QueuedInputAction::Literal
|
||||
} else {
|
||||
slash_input::queued_input_action(&text, defer_slash_validation)
|
||||
};
|
||||
let (text, text_elements, pending_pastes) = if action == QueuedInputAction::Literal
|
||||
{
|
||||
let text = original_input.trim().to_string();
|
||||
let text_elements =
|
||||
Self::trim_text_elements(&original_input, &text, original_text_elements);
|
||||
(text, text_elements, original_pending_pastes)
|
||||
} else {
|
||||
(text, text_elements, pending_pastes)
|
||||
};
|
||||
return (
|
||||
InputResult::Queued {
|
||||
text,
|
||||
@@ -3152,13 +3188,19 @@ impl ChatComposer {
|
||||
if let Some((text, text_elements)) =
|
||||
self.prepare_submission_text(/*record_history*/ true)
|
||||
{
|
||||
if should_queue {
|
||||
if self.slash_commands_enabled()
|
||||
&& text.starts_with('!')
|
||||
&& !original_input.trim_start().starts_with('!')
|
||||
{
|
||||
let text = original_input.trim().to_string();
|
||||
let text_elements =
|
||||
Self::trim_text_elements(&original_input, &text, original_text_elements);
|
||||
(
|
||||
InputResult::Queued {
|
||||
text,
|
||||
text_elements,
|
||||
action: QueuedInputAction::Plain,
|
||||
pending_pastes: Vec::new(),
|
||||
action: QueuedInputAction::Literal,
|
||||
pending_pastes: original_pending_pastes,
|
||||
},
|
||||
true,
|
||||
)
|
||||
|
||||
Reference in new issue
Block a user