Skip to content

Commit 6c9bac8

Browse files
charley-oaicodex
andcommitted
tui: wait for async slash replay updates
Co-authored-by: Codex <noreply@openai.com>
1 parent ef28639 commit 6c9bac8

File tree

2 files changed

+194
-21
lines changed

2 files changed

+194
-21
lines changed

codex-rs/tui/src/app.rs

Lines changed: 182 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -730,6 +730,7 @@ pub(crate) struct App {
730730
primary_thread_id: Option<ThreadId>,
731731
primary_session_configured: Option<SessionConfiguredEvent>,
732732
pending_primary_events: VecDeque<Event>,
733+
pending_async_queue_resume_barriers: usize,
733734
}
734735

735736
#[derive(Default)]
@@ -757,8 +758,22 @@ fn normalize_harness_overrides_for_cwd(
757758
}
758759

759760
impl App {
761+
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
762+
fn begin_async_queue_resume_barrier(&mut self) {
763+
self.pending_async_queue_resume_barriers += 1;
764+
}
765+
766+
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
767+
fn finish_async_queue_resume_barrier(&mut self) {
768+
if self.pending_async_queue_resume_barriers == 0 {
769+
tracing::warn!("finished async queue-resume barrier with no pending barrier");
770+
return;
771+
}
772+
self.pending_async_queue_resume_barriers -= 1;
773+
}
774+
760775
fn maybe_resume_queued_inputs_after_app_events(&mut self, app_events_drained: bool) {
761-
if !app_events_drained {
776+
if !app_events_drained || self.pending_async_queue_resume_barriers != 0 {
762777
return;
763778
}
764779

@@ -2301,6 +2316,7 @@ impl App {
23012316
primary_thread_id: None,
23022317
primary_session_configured: None,
23032318
pending_primary_events: VecDeque::new(),
2319+
pending_async_queue_resume_barriers: 0,
23042320
};
23052321

23062322
// On startup, if Agent mode (workspace-write) or ReadOnly is active, warn about world-writable dirs on Windows.
@@ -2319,6 +2335,7 @@ impl App {
23192335
.hide_world_writable_warning
23202336
.unwrap_or(false);
23212337
if should_check {
2338+
app.begin_async_queue_resume_barrier();
23222339
let cwd = app.config.cwd.clone();
23232340
let env_map: std::collections::HashMap<String, String> = std::env::vars().collect();
23242341
let tx = app.app_event_tx.clone();
@@ -2429,10 +2446,10 @@ impl App {
24292446
) {
24302447
waiting_for_initial_session_configured = false;
24312448
}
2432-
// Some replayed slash commands pause queue draining until their app-side updates
2433-
// or popup flows settle. Only resume once the app-event queue is fully drained so
2434-
// multi-event commands (for example approvals updates) cannot interleave later
2435-
// queued input.
2449+
// Some replayed slash commands pause queue draining until their app-side updates,
2450+
// popup flows, or async follow-up work settle. Only resume once the app-event
2451+
// queue is fully drained and no background slash-command completions are still
2452+
// pending, so later queued input cannot interleave with those updates.
24362453
app.maybe_resume_queued_inputs_after_app_events(app_event_rx.is_empty());
24372454
match control {
24382455
AppRunControl::Continue => {}
@@ -2890,6 +2907,7 @@ impl App {
28902907

28912908
self.chat_widget.show_windows_sandbox_setup_status();
28922909
self.windows_sandbox.setup_started_at = Some(Instant::now());
2910+
self.begin_async_queue_resume_barrier();
28932911
let session_telemetry = self.session_telemetry.clone();
28942912
tokio::task::spawn_blocking(move || {
28952913
let result = codex_core::windows_sandbox::run_elevated_setup(
@@ -2906,10 +2924,10 @@ impl App {
29062924
1,
29072925
&[],
29082926
);
2909-
AppEvent::EnableWindowsSandboxForAgentMode {
2927+
AppEvent::WindowsSandboxElevatedSetupCompleted {
29102928
preset: preset.clone(),
2911-
mode: WindowsSandboxEnableMode::Elevated,
29122929
approvals_reviewer,
2930+
setup_succeeded: true,
29132931
}
29142932
}
29152933
Err(err) => {
@@ -2941,9 +2959,10 @@ impl App {
29412959
error = %err,
29422960
"failed to run elevated Windows sandbox setup"
29432961
);
2944-
AppEvent::OpenWindowsSandboxFallbackPrompt {
2962+
AppEvent::WindowsSandboxElevatedSetupCompleted {
29452963
preset,
29462964
approvals_reviewer,
2965+
setup_succeeded: false,
29472966
}
29482967
}
29492968
};
@@ -2955,6 +2974,33 @@ impl App {
29552974
let _ = (preset, approvals_reviewer);
29562975
}
29572976
}
2977+
AppEvent::WindowsSandboxElevatedSetupCompleted {
2978+
preset,
2979+
approvals_reviewer,
2980+
setup_succeeded,
2981+
} => {
2982+
#[cfg(target_os = "windows")]
2983+
{
2984+
self.finish_async_queue_resume_barrier();
2985+
let event = if setup_succeeded {
2986+
AppEvent::EnableWindowsSandboxForAgentMode {
2987+
preset,
2988+
mode: WindowsSandboxEnableMode::Elevated,
2989+
approvals_reviewer,
2990+
}
2991+
} else {
2992+
AppEvent::OpenWindowsSandboxFallbackPrompt {
2993+
preset,
2994+
approvals_reviewer,
2995+
}
2996+
};
2997+
self.app_event_tx.send(event);
2998+
}
2999+
#[cfg(not(target_os = "windows"))]
3000+
{
3001+
let _ = (preset, approvals_reviewer, setup_succeeded);
3002+
}
3003+
}
29583004
AppEvent::BeginWindowsSandboxLegacySetup {
29593005
preset,
29603006
approvals_reviewer,
@@ -2969,6 +3015,7 @@ impl App {
29693015
let codex_home = self.config.codex_home.clone();
29703016
let tx = self.app_event_tx.clone();
29713017

3018+
self.begin_async_queue_resume_barrier();
29723019
tokio::task::spawn_blocking(move || {
29733020
let preset_for_error = preset.clone();
29743021
let result = codex_core::windows_sandbox::run_legacy_setup_preflight(
@@ -3010,19 +3057,22 @@ impl App {
30103057
error,
30113058
} => {
30123059
#[cfg(target_os = "windows")]
3013-
match error {
3014-
None => {
3015-
self.app_event_tx
3016-
.send(AppEvent::EnableWindowsSandboxForAgentMode {
3017-
preset,
3018-
mode: WindowsSandboxEnableMode::Legacy,
3019-
approvals_reviewer,
3020-
});
3021-
}
3022-
Some(err) => {
3023-
self.chat_widget.add_error_message(format!(
3024-
"Failed to enable the Windows sandbox feature: {err}"
3025-
));
3060+
{
3061+
self.finish_async_queue_resume_barrier();
3062+
match error {
3063+
None => {
3064+
self.app_event_tx
3065+
.send(AppEvent::EnableWindowsSandboxForAgentMode {
3066+
preset,
3067+
mode: WindowsSandboxEnableMode::Legacy,
3068+
approvals_reviewer,
3069+
});
3070+
}
3071+
Some(err) => {
3072+
self.chat_widget.add_error_message(format!(
3073+
"Failed to enable the Windows sandbox feature: {err}"
3074+
));
3075+
}
30263076
}
30273077
}
30283078
#[cfg(not(target_os = "windows"))]
@@ -3419,6 +3469,7 @@ impl App {
34193469
&& policy_is_workspace_write_or_ro
34203470
&& !self.chat_widget.world_writable_warning_hidden();
34213471
if should_check {
3472+
self.begin_async_queue_resume_barrier();
34223473
let cwd = self.config.cwd.clone();
34233474
let env_map: std::collections::HashMap<String, String> =
34243475
std::env::vars().collect();
@@ -3591,6 +3642,10 @@ impl App {
35913642
AppEvent::OpenApprovalsPopup => {
35923643
self.chat_widget.open_approvals_popup();
35933644
}
3645+
AppEvent::WorldWritableScanCompleted => {
3646+
#[cfg(target_os = "windows")]
3647+
self.finish_async_queue_resume_barrier();
3648+
}
35943649
AppEvent::OpenAgentPicker => {
35953650
self.open_agent_picker().await;
35963651
}
@@ -4285,6 +4340,7 @@ impl App {
42854340
failed_scan: true,
42864341
});
42874342
}
4343+
tx.send(AppEvent::WorldWritableScanCompleted);
42884344
});
42894345
}
42904346
}
@@ -6555,6 +6611,7 @@ guardian_approval = true
65556611
primary_thread_id: None,
65566612
primary_session_configured: None,
65576613
pending_primary_events: VecDeque::new(),
6614+
pending_async_queue_resume_barriers: 0,
65586615
}
65596616
}
65606617

@@ -6615,6 +6672,7 @@ guardian_approval = true
66156672
primary_thread_id: None,
66166673
primary_session_configured: None,
66176674
pending_primary_events: VecDeque::new(),
6675+
pending_async_queue_resume_barriers: 0,
66186676
},
66196677
rx,
66206678
op_rx,
@@ -7728,6 +7786,109 @@ guardian_approval = true
77287786
}
77297787
}
77307788

7789+
#[tokio::test]
7790+
async fn queued_followup_waits_for_pending_async_resume_barrier() {
7791+
let (mut app, mut app_event_rx, mut op_rx) = make_test_app_with_channels().await;
7792+
app.chat_widget.set_model("gpt-5.2-codex");
7793+
app.chat_widget
7794+
.set_feature_enabled(Feature::Personality, true);
7795+
app.chat_widget.handle_codex_event(Event {
7796+
id: "configured".into(),
7797+
msg: EventMsg::SessionConfigured(SessionConfiguredEvent {
7798+
session_id: ThreadId::new(),
7799+
forked_from_id: None,
7800+
thread_name: None,
7801+
model: "gpt-5.2-codex".to_string(),
7802+
model_provider_id: "test-provider".to_string(),
7803+
service_tier: None,
7804+
approval_policy: AskForApproval::Never,
7805+
approvals_reviewer: ApprovalsReviewer::User,
7806+
sandbox_policy: SandboxPolicy::new_read_only_policy(),
7807+
cwd: PathBuf::from("/home/user/project"),
7808+
reasoning_effort: Some(ReasoningEffortConfig::default()),
7809+
history_log_id: 0,
7810+
history_entry_count: 0,
7811+
initial_messages: None,
7812+
network_proxy: None,
7813+
rollout_path: None,
7814+
}),
7815+
});
7816+
app.chat_widget.handle_codex_event(Event {
7817+
id: "turn-started".into(),
7818+
msg: EventMsg::TurnStarted(TurnStartedEvent {
7819+
turn_id: "turn-1".to_string(),
7820+
model_context_window: None,
7821+
collaboration_mode_kind: Default::default(),
7822+
}),
7823+
});
7824+
while app_event_rx.try_recv().is_ok() {}
7825+
while op_rx.try_recv().is_ok() {}
7826+
7827+
app.chat_widget
7828+
.handle_serialized_slash_command(UserMessage::from("/personality pragmatic"));
7829+
app.chat_widget
7830+
.set_composer_text("followup".to_string(), Vec::new(), Vec::new());
7831+
app.chat_widget
7832+
.handle_key_event(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
7833+
7834+
app.chat_widget.handle_codex_event(Event {
7835+
id: "turn-complete".into(),
7836+
msg: EventMsg::TurnComplete(TurnCompleteEvent {
7837+
turn_id: "turn-1".to_string(),
7838+
last_agent_message: None,
7839+
}),
7840+
});
7841+
7842+
loop {
7843+
match app_event_rx.try_recv() {
7844+
Ok(AppEvent::CodexOp(Op::OverrideTurnContext {
7845+
personality: Some(Personality::Pragmatic),
7846+
..
7847+
})) => continue,
7848+
Ok(AppEvent::UpdatePersonality(Personality::Pragmatic)) => {
7849+
app.on_update_personality(Personality::Pragmatic);
7850+
break;
7851+
}
7852+
Ok(AppEvent::PersistPersonalitySelection {
7853+
personality: Personality::Pragmatic,
7854+
}) => continue,
7855+
Ok(_) => continue,
7856+
Err(TryRecvError::Empty) => panic!("expected personality update events"),
7857+
Err(TryRecvError::Disconnected) => panic!("expected personality update events"),
7858+
}
7859+
}
7860+
7861+
app.pending_async_queue_resume_barriers = 1;
7862+
app.maybe_resume_queued_inputs_after_app_events(true);
7863+
7864+
assert_eq!(
7865+
app.chat_widget.queued_user_message_texts(),
7866+
vec!["followup".to_string()]
7867+
);
7868+
assert!(
7869+
op_rx.try_recv().is_err(),
7870+
"queued follow-up should stay queued while async replay barriers are pending"
7871+
);
7872+
7873+
app.pending_async_queue_resume_barriers = 0;
7874+
app.maybe_resume_queued_inputs_after_app_events(true);
7875+
7876+
match next_user_turn_op(&mut op_rx) {
7877+
Op::UserTurn {
7878+
items,
7879+
personality: Some(Personality::Pragmatic),
7880+
..
7881+
} => assert_eq!(
7882+
items,
7883+
vec![UserInput::Text {
7884+
text: "followup".to_string(),
7885+
text_elements: Vec::new(),
7886+
}]
7887+
),
7888+
other => panic!("expected Op::UserTurn with pragmatic personality, got {other:?}"),
7889+
}
7890+
}
7891+
77317892
#[tokio::test]
77327893
async fn shutdown_first_exit_returns_immediate_exit_when_shutdown_submit_fails() {
77337894
let mut app = make_test_app().await;

codex-rs/tui/src/app_event.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,14 @@ pub(crate) enum AppEvent {
298298
approvals_reviewer: ApprovalsReviewer,
299299
},
300300

301+
/// Result of the elevated Windows sandbox setup flow.
302+
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
303+
WindowsSandboxElevatedSetupCompleted {
304+
preset: ApprovalPreset,
305+
approvals_reviewer: ApprovalsReviewer,
306+
setup_succeeded: bool,
307+
},
308+
301309
/// Begin the non-elevated Windows sandbox setup flow.
302310
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
303311
BeginWindowsSandboxLegacySetup {
@@ -326,6 +334,10 @@ pub(crate) enum AppEvent {
326334
error: Option<String>,
327335
},
328336

337+
/// Result of the asynchronous Windows world-writable scan.
338+
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
339+
WorldWritableScanCompleted,
340+
329341
/// Enable the Windows sandbox feature and switch to Agent mode.
330342
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
331343
EnableWindowsSandboxForAgentMode {

0 commit comments

Comments
 (0)