Render TUI prompts before submitting user turns (#33373)

## Why

Starting a user turn can wait on remote work. Show the submitted prompt immediately instead of leaving the input invisible during that wait.

## What changed

- Queue optimistic prompt history before app-event submissions and render a frame before starting the turn.
- Preserve the existing submit-failure behavior for direct submissions.

## Testing

- Verify that an initial prompt is added to history before its user-turn event is submitted during session replay.

GitOrigin-RevId: 57f30c6e0067127e6b082a206f7d154fcfef8d7d
This commit is contained in:
jif
2026-07-15 16:24:09 +00:00
committed by copyberry
parent 2e4f55608b
commit 38b064c31b
3 changed files with 48 additions and 27 deletions
+9
View File
@@ -411,6 +411,15 @@ impl App {
return Ok(AppRunControl::Exit(ExitReason::Fatal(message)));
}
AppEvent::CodexOp(op) => {
if matches!(&op, AppCommand::UserTurn { .. }) {
self.handle_draw_pre_render(tui)?;
if self.transcript_reflow.has_pending_reflow() {
self.transcript_reflow.schedule_immediate();
self.maybe_run_resize_reflow(tui)?;
}
self.chat_widget.pre_draw_tick();
self.render_chat_widget_frame(tui)?;
}
self.chat_widget.prepare_local_op_submission(&op);
self.submit_active_thread_op(app_server, op).await?;
}
+6
View File
@@ -367,12 +367,14 @@ async fn enqueue_primary_thread_session_replays_turns_before_initial_prompt_subm
.await?;
let mut saw_replayed_answer = false;
let mut saw_optimistic_prompt = false;
let mut submitted_items = None;
while let Ok(event) = app_event_rx.try_recv() {
match event {
AppEvent::InsertHistoryCell(cell) => {
let transcript = lines_to_single_string(&cell.transcript_lines(/*width*/ 80));
saw_replayed_answer |= transcript.contains("earlier prompt");
saw_optimistic_prompt |= transcript.contains(&initial_prompt);
}
AppEvent::SubmitThreadOp {
thread_id: op_thread_id,
@@ -382,6 +384,10 @@ async fn enqueue_primary_thread_session_replays_turns_before_initial_prompt_subm
submitted_items = Some(items);
}
AppEvent::CodexOp(Op::UserTurn { items, .. }) => {
assert!(
saw_optimistic_prompt,
"expected optimistic prompt before turn submission"
);
submitted_items = Some(items);
}
_ => {}
+33 -27
View File
@@ -350,6 +350,25 @@ impl ChatWidget {
collaboration_mode,
personality,
);
let submitted_message = UserMessage {
text,
local_images,
remote_image_urls,
text_elements,
mention_bindings,
};
// App-event submissions are handled serially, and turn/start can wait on remote work.
// Queue the optimistic prompt first so the user's input is visible while that happens.
// Direct submissions do not share that queue, so keep their existing failure behavior.
let render_before_submit =
render_in_history && matches!(&self.codex_op_target, CodexOpTarget::AppEvent);
if render_before_submit {
self.on_user_message_display(user_message_display_for_history(
submitted_message.clone(),
&history_record,
));
}
if !self.submit_op(op.clone()) {
return (false, None);
@@ -360,7 +379,8 @@ impl ChatWidget {
// Persist the submitted text to cross-session message history. Mentions are encoded into
// placeholder syntax so recall can reconstruct the mention bindings in a future session.
let encoded_mentions = mention_bindings
let encoded_mentions = submitted_message
.mention_bindings
.iter()
.map(|binding| LinkedMention {
sigil: binding.sigil,
@@ -369,8 +389,11 @@ impl ChatWidget {
})
.collect::<Vec<_>>();
let history_text = match &history_record {
UserMessageHistoryRecord::UserMessageText if !text.is_empty() => {
Some(encode_history_mentions(&text, &encoded_mentions))
UserMessageHistoryRecord::UserMessageText if !submitted_message.text.is_empty() => {
Some(encode_history_mentions(
&submitted_message.text,
&encoded_mentions,
))
}
UserMessageHistoryRecord::Override(history) if !history.text.is_empty() => {
Some(encode_history_mentions(&history.text, &encoded_mentions))
@@ -390,30 +413,13 @@ impl ChatWidget {
}
if render_in_history {
self.safety_buffering_prompt = Some(UserMessage {
text: text.clone(),
local_images: local_images.clone(),
remote_image_urls: remote_image_urls.clone(),
text_elements: text_elements.clone(),
mention_bindings: mention_bindings.clone(),
});
}
// Show replayable user content in conversation history.
let display_user_message = render_in_history.then(|| {
user_message_display_for_history(
UserMessage {
text,
local_images,
remote_image_urls,
text_elements,
mention_bindings,
},
&history_record,
)
});
if let Some(display) = display_user_message {
self.on_user_message_display(display);
self.safety_buffering_prompt = Some(submitted_message.clone());
if !render_before_submit {
self.on_user_message_display(user_message_display_for_history(
submitted_message,
&history_record,
));
}
}
self.transcript.needs_final_message_separator = false;