mirror of
https://github.com/openai/codex.git
synced 2026-05-04 19:36:45 +00:00
tui: canonicalize interactive slash drafts
Route interactive slash-command pickers through canonical serialized drafts so live dispatch and queued replay share one parser/executor path. Validation: cargo test -p codex-tui; just fix -p codex-tui; just fmt Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
@@ -2034,6 +2034,19 @@ fn drain_insert_history(
|
||||
out
|
||||
}
|
||||
|
||||
fn run_next_serialized_slash_draft(
|
||||
chat: &mut ChatWidget,
|
||||
rx: &mut tokio::sync::mpsc::UnboundedReceiver<AppEvent>,
|
||||
) {
|
||||
while let Ok(event) = rx.try_recv() {
|
||||
if let AppEvent::HandleSlashCommandDraft(draft) = event {
|
||||
chat.handle_serialized_slash_command(draft);
|
||||
return;
|
||||
}
|
||||
}
|
||||
panic!("expected serialized slash draft event");
|
||||
}
|
||||
|
||||
fn lines_to_single_string(lines: &[ratatui::text::Line<'static>]) -> String {
|
||||
let mut s = String::new();
|
||||
for line in lines {
|
||||
@@ -5538,11 +5551,7 @@ async fn collab_slash_command_opens_picker_and_updates_mode() {
|
||||
);
|
||||
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Enter));
|
||||
let selected_mask = match rx.try_recv() {
|
||||
Ok(AppEvent::UpdateCollaborationMode(mask)) => mask,
|
||||
other => panic!("expected UpdateCollaborationMode event, got {other:?}"),
|
||||
};
|
||||
chat.set_collaboration_mask(selected_mask);
|
||||
run_next_serialized_slash_draft(&mut chat, &mut rx);
|
||||
|
||||
chat.bottom_pane
|
||||
.set_composer_text("hello".to_string(), Vec::new(), Vec::new());
|
||||
@@ -7550,13 +7559,13 @@ async fn experimental_features_popup_snapshot() {
|
||||
|
||||
let features = vec![
|
||||
ExperimentalFeatureItem {
|
||||
feature: Feature::GhostCommit,
|
||||
key: Feature::GhostCommit.key().to_string(),
|
||||
name: "Ghost snapshots".to_string(),
|
||||
description: "Capture undo snapshots each turn.".to_string(),
|
||||
enabled: false,
|
||||
},
|
||||
ExperimentalFeatureItem {
|
||||
feature: Feature::ShellTool,
|
||||
key: Feature::ShellTool.key().to_string(),
|
||||
name: "Shell tool".to_string(),
|
||||
description: "Allow the model to run shell commands.".to_string(),
|
||||
enabled: true,
|
||||
@@ -7576,7 +7585,7 @@ async fn experimental_features_toggle_saves_on_exit() {
|
||||
let expected_feature = Feature::GhostCommit;
|
||||
let view = ExperimentalFeaturesView::new(
|
||||
vec![ExperimentalFeatureItem {
|
||||
feature: expected_feature,
|
||||
key: expected_feature.key().to_string(),
|
||||
name: "Ghost snapshots".to_string(),
|
||||
description: "Capture undo snapshots each turn.".to_string(),
|
||||
enabled: false,
|
||||
@@ -7593,6 +7602,7 @@ async fn experimental_features_toggle_saves_on_exit() {
|
||||
);
|
||||
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
||||
run_next_serialized_slash_draft(&mut chat, &mut rx);
|
||||
|
||||
let mut updates = None;
|
||||
while let Ok(event) = rx.try_recv() {
|
||||
@@ -7747,7 +7757,7 @@ async fn realtime_microphone_picker_popup_snapshot() {
|
||||
|
||||
#[cfg(all(not(target_os = "linux"), feature = "voice-input"))]
|
||||
#[tokio::test]
|
||||
async fn realtime_audio_picker_emits_persist_event() {
|
||||
async fn realtime_audio_picker_emits_serialized_slash_draft() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.2-codex")).await;
|
||||
chat.open_realtime_audio_device_selection_with_names(
|
||||
RealtimeAudioDeviceKind::Speaker,
|
||||
@@ -7760,10 +7770,8 @@ async fn realtime_audio_picker_emits_persist_event() {
|
||||
|
||||
assert_matches!(
|
||||
rx.try_recv(),
|
||||
Ok(AppEvent::PersistRealtimeAudioDeviceSelection {
|
||||
kind: RealtimeAudioDeviceKind::Speaker,
|
||||
name: Some(name),
|
||||
}) if name == "Headphones"
|
||||
Ok(AppEvent::HandleSlashCommandDraft(UserMessage { text, .. }))
|
||||
if text == "/settings speaker Headphones"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8092,6 +8100,43 @@ async fn feedback_selection_popup_snapshot() {
|
||||
assert_snapshot!("feedback_selection_popup", popup);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn feedback_selection_popup_emits_serialized_slash_draft() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None).await;
|
||||
|
||||
chat.dispatch_command(SlashCommand::Feedback);
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
||||
|
||||
assert_matches!(
|
||||
rx.try_recv(),
|
||||
Ok(AppEvent::HandleSlashCommandDraft(UserMessage { text, .. })) if text == "/feedback bug"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn skills_menu_emits_serialized_slash_drafts() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None).await;
|
||||
|
||||
chat.open_skills_menu();
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
||||
|
||||
assert_matches!(
|
||||
rx.try_recv(),
|
||||
Ok(AppEvent::HandleSlashCommandDraft(UserMessage { text, .. }))
|
||||
if text == "/skills list"
|
||||
);
|
||||
|
||||
chat.open_skills_menu();
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE));
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
||||
|
||||
assert_matches!(
|
||||
rx.try_recv(),
|
||||
Ok(AppEvent::HandleSlashCommandDraft(UserMessage { text, .. }))
|
||||
if text == "/skills manage"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn feedback_upload_consent_popup_snapshot() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None).await;
|
||||
@@ -8699,6 +8744,7 @@ async fn approvals_popup_navigation_skips_disabled() {
|
||||
|
||||
// Press Enter; selection should land on an enabled preset and dispatch updates.
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Enter));
|
||||
run_next_serialized_slash_draft(&mut chat, &mut rx);
|
||||
let mut app_events = Vec::new();
|
||||
while let Ok(ev) = rx.try_recv() {
|
||||
app_events.push(ev);
|
||||
@@ -8740,6 +8786,7 @@ async fn permissions_selection_emits_history_cell_when_selection_changes() {
|
||||
chat.open_permissions_popup();
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Down));
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Enter));
|
||||
run_next_serialized_slash_draft(&mut chat, &mut rx);
|
||||
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
assert_eq!(
|
||||
@@ -8769,6 +8816,7 @@ async fn permissions_selection_history_snapshot_after_mode_switch() {
|
||||
#[cfg(target_os = "windows")]
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Down));
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Enter));
|
||||
run_next_serialized_slash_draft(&mut chat, &mut rx);
|
||||
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
assert_eq!(cells.len(), 1, "expected one mode-switch history cell");
|
||||
@@ -8805,6 +8853,7 @@ async fn permissions_selection_history_snapshot_full_access_to_default() {
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Up));
|
||||
}
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Enter));
|
||||
run_next_serialized_slash_draft(&mut chat, &mut rx);
|
||||
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
assert_eq!(cells.len(), 1, "expected one mode-switch history cell");
|
||||
@@ -8843,6 +8892,7 @@ async fn permissions_selection_emits_history_cell_when_current_is_selected() {
|
||||
|
||||
chat.open_permissions_popup();
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Enter));
|
||||
run_next_serialized_slash_draft(&mut chat, &mut rx);
|
||||
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
assert_eq!(
|
||||
@@ -9163,6 +9213,7 @@ async fn permissions_full_access_history_cell_emitted_only_after_confirmation()
|
||||
);
|
||||
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Enter));
|
||||
run_next_serialized_slash_draft(&mut chat, &mut rx);
|
||||
let cells_after_confirmation = drain_insert_history(&mut rx);
|
||||
let total_history_cells = cells_before_confirmation.len() + cells_after_confirmation.len();
|
||||
assert_eq!(
|
||||
|
||||
Reference in New Issue
Block a user