mirror of
https://github.com/openai/codex.git
synced 2026-11-02 19:43:24 +00:00
tui: canonicalize pending nudge matching
This commit is contained in:
@@ -3443,29 +3443,18 @@ impl ChatWidget {
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let has_user_content = !user_message.text.is_empty()
|
||||
|| !user_message.local_images.is_empty()
|
||||
|| !user_message.remote_image_urls.is_empty();
|
||||
let has_images = !user_message.local_images.is_empty()
|
||||
|| !user_message.remote_image_urls.is_empty();
|
||||
let should_preview_as_pending_nudge = self.is_session_configured()
|
||||
&& (self.is_plan_streaming_in_tui() || self.stream_controller.is_some())
|
||||
&& (!user_message.text.is_empty()
|
||||
|| !user_message.local_images.is_empty()
|
||||
|| !user_message.remote_image_urls.is_empty())
|
||||
&& self.bottom_pane.is_task_running()
|
||||
&& !self.is_review_mode
|
||||
&& has_user_content
|
||||
&& user_message.text.strip_prefix('!').is_none()
|
||||
&& ((user_message.local_images.is_empty()
|
||||
&& user_message.remote_image_urls.is_empty())
|
||||
|| self.current_model_supports_images());
|
||||
&& (!has_images || self.current_model_supports_images());
|
||||
if should_preview_as_pending_nudge {
|
||||
self.pending_nudges.push_back(
|
||||
Self::rendered_user_message_event_from_parts(
|
||||
user_message.text.clone(),
|
||||
user_message.text_elements.clone(),
|
||||
user_message
|
||||
.local_images
|
||||
.iter()
|
||||
.map(|image| image.path.clone())
|
||||
.collect(),
|
||||
user_message.remote_image_urls.clone(),
|
||||
),
|
||||
);
|
||||
self.refresh_pending_input_preview();
|
||||
self.submit_user_message_without_local_history(user_message);
|
||||
} else if self.is_session_configured() {
|
||||
// Submitted is only emitted when steer is enabled.
|
||||
@@ -4256,6 +4245,8 @@ impl ChatWidget {
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let pending_nudge =
|
||||
(!render_in_history).then(|| Self::rendered_user_message_event_from_inputs(&items));
|
||||
let personality = self
|
||||
.config
|
||||
.personality
|
||||
@@ -4295,6 +4286,11 @@ impl ChatWidget {
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(pending_nudge) = pending_nudge {
|
||||
self.pending_nudges.push_back(pending_nudge);
|
||||
self.refresh_pending_input_preview();
|
||||
}
|
||||
|
||||
// Show replayable user content in conversation history.
|
||||
if render_in_history && !text.is_empty() {
|
||||
let local_image_paths = local_images
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use super::*;
|
||||
use codex_core::parse_turn_item;
|
||||
use codex_protocol::models::ResponseInputItem;
|
||||
use codex_protocol::protocol::ConversationStartParams;
|
||||
use codex_protocol::protocol::RealtimeAudioFrame;
|
||||
use codex_protocol::protocol::RealtimeConversationClosedEvent;
|
||||
@@ -87,6 +89,19 @@ impl ChatWidget {
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn rendered_user_message_event_from_inputs(
|
||||
items: &[UserInput],
|
||||
) -> RenderedUserMessageEvent {
|
||||
let response_item = ResponseInputItem::from(items.to_vec()).into();
|
||||
let Some(TurnItem::UserMessage(user_message)) = parse_turn_item(&response_item) else {
|
||||
unreachable!("user inputs must round-trip into a user-message turn item");
|
||||
};
|
||||
let EventMsg::UserMessage(event) = user_message.as_legacy_event() else {
|
||||
unreachable!("UserMessageItem::as_legacy_event must return UserMessage");
|
||||
};
|
||||
Self::rendered_user_message_event_from_event(&event)
|
||||
}
|
||||
|
||||
pub(super) fn should_render_realtime_user_message_event(
|
||||
&self,
|
||||
event: &UserMessageEvent,
|
||||
|
||||
@@ -1832,14 +1832,20 @@ fn drain_insert_history(
|
||||
}
|
||||
|
||||
fn raw_user_message_event(text: &str) -> Event {
|
||||
let response_item: codex_protocol::models::ResponseItem =
|
||||
codex_protocol::models::ResponseInputItem::from(vec![UserInput::Text {
|
||||
raw_user_message_event_for_inputs(
|
||||
format!("raw-user-{text}"),
|
||||
vec![UserInput::Text {
|
||||
text: text.to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}])
|
||||
.into();
|
||||
}],
|
||||
)
|
||||
}
|
||||
|
||||
fn raw_user_message_event_for_inputs(id: String, items: Vec<UserInput>) -> Event {
|
||||
let response_item: codex_protocol::models::ResponseItem =
|
||||
codex_protocol::models::ResponseInputItem::from(items).into();
|
||||
Event {
|
||||
id: format!("raw-user-{text}"),
|
||||
id,
|
||||
msg: EventMsg::RawResponseItem(RawResponseItemEvent {
|
||||
item: response_item,
|
||||
}),
|
||||
@@ -3507,6 +3513,36 @@ async fn steer_enter_uses_pending_nudges_while_plan_stream_is_active() {
|
||||
assert!(lines_to_single_string(&inserted[0]).contains("queued submission"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn steer_enter_uses_pending_nudges_while_turn_is_running_without_streaming() {
|
||||
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(None).await;
|
||||
chat.thread_id = Some(ThreadId::new());
|
||||
chat.on_task_started();
|
||||
|
||||
chat.bottom_pane
|
||||
.set_composer_text("queued while running".to_string(), Vec::new(), Vec::new());
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
||||
|
||||
assert!(chat.queued_user_messages.is_empty());
|
||||
assert_eq!(chat.pending_nudges.len(), 1);
|
||||
assert_eq!(
|
||||
chat.pending_nudges.front().unwrap().message(),
|
||||
"queued while running"
|
||||
);
|
||||
match next_submit_op(&mut op_rx) {
|
||||
Op::UserTurn { .. } => {}
|
||||
other => panic!("expected Op::UserTurn, got {other:?}"),
|
||||
}
|
||||
assert!(drain_insert_history(&mut rx).is_empty());
|
||||
|
||||
chat.handle_codex_event(raw_user_message_event("queued while running"));
|
||||
|
||||
assert!(chat.pending_nudges.is_empty());
|
||||
let inserted = drain_insert_history(&mut rx);
|
||||
assert_eq!(inserted.len(), 1);
|
||||
assert!(lines_to_single_string(&inserted[0]).contains("queued while running"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn steer_enter_uses_pending_nudges_while_final_answer_stream_is_active() {
|
||||
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(None).await;
|
||||
@@ -3543,6 +3579,34 @@ async fn steer_enter_uses_pending_nudges_while_final_answer_stream_is_active() {
|
||||
assert!(lines_to_single_string(&inserted[0]).contains("queued while streaming"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn raw_response_item_with_canonicalized_user_payload_clears_pending_nudge() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None).await;
|
||||
let items = vec![
|
||||
UserInput::Text {
|
||||
text: "hello".to_string(),
|
||||
text_elements: vec![TextElement::new((1..4).into(), Some("ell".to_string()))],
|
||||
},
|
||||
UserInput::Text {
|
||||
text: " world".to_string(),
|
||||
text_elements: vec![TextElement::new((1..6).into(), Some("world".to_string()))],
|
||||
},
|
||||
];
|
||||
chat.pending_nudges
|
||||
.push_back(ChatWidget::rendered_user_message_event_from_inputs(&items));
|
||||
chat.refresh_pending_input_preview();
|
||||
|
||||
chat.handle_codex_event(raw_user_message_event_for_inputs(
|
||||
"raw-user-canonicalized".to_string(),
|
||||
items,
|
||||
));
|
||||
|
||||
assert!(chat.pending_nudges.is_empty());
|
||||
let inserted = drain_insert_history(&mut rx);
|
||||
assert_eq!(inserted.len(), 1);
|
||||
assert!(lines_to_single_string(&inserted[0]).contains("hello world"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn steer_enter_during_final_stream_preserves_follow_up_prompts_in_order() {
|
||||
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(None).await;
|
||||
|
||||
Reference in New Issue
Block a user