diff --git a/codex-rs/tui/src/app/event_dispatch.rs b/codex-rs/tui/src/app/event_dispatch.rs index dfe7ceb4f9..14c8c0b359 100644 --- a/codex-rs/tui/src/app/event_dispatch.rs +++ b/codex-rs/tui/src/app/event_dispatch.rs @@ -1139,6 +1139,27 @@ impl App { let _ = (preset, mode); } } + AppEvent::AcknowledgeWindowsSandboxSetup => { + #[cfg(target_os = "windows")] + { + match ConfigEditsBuilder::new(&self.config.codex_home) + .set_windows_wsl_setup_acknowledged(true) + .apply() + .await + { + Ok(()) => { + self.config.windows_wsl_setup_acknowledged = true; + self.chat_widget.set_windows_wsl_setup_acknowledged(true); + } + Err(err) => { + tracing::warn!( + error = %err, + "failed to persist Windows sandbox onboarding acknowledgement" + ); + } + } + } + } AppEvent::PersistModelSelection { model, effort } => { let profile = self.active_profile.as_deref(); match ConfigEditsBuilder::new(&self.config.codex_home) diff --git a/codex-rs/tui/src/app_event.rs b/codex-rs/tui/src/app_event.rs index bc688e32ce..c8e819f344 100644 --- a/codex-rs/tui/src/app_event.rs +++ b/codex-rs/tui/src/app_event.rs @@ -664,6 +664,10 @@ pub(crate) enum AppEvent { mode: WindowsSandboxEnableMode, }, + /// Record that the Windows sandbox onboarding has been acknowledged. + #[cfg_attr(not(target_os = "windows"), allow(dead_code))] + AcknowledgeWindowsSandboxSetup, + /// Update the Windows sandbox feature mode without changing approval presets. #[cfg_attr(not(target_os = "windows"), allow(dead_code))] diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index d06c674566..f30c572385 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -8826,6 +8826,7 @@ impl ChatWidget { let accept_otel = self.session_telemetry.clone(); let legacy_otel = self.session_telemetry.clone(); let legacy_preset = preset.clone(); + let skip_otel = self.session_telemetry.clone(); let quit_otel = self.session_telemetry.clone(); let items = vec![ SelectionItem { @@ -8860,6 +8861,20 @@ impl ChatWidget { dismiss_on_select: true, ..Default::default() }, + SelectionItem { + name: "Continue without sandbox (WSL2 / don't warn again)".to_string(), + description: None, + actions: vec![Box::new(move |tx| { + skip_otel.counter( + "codex.windows_sandbox.elevated_prompt_skip", + /*inc*/ 1, + &[], + ); + tx.send(AppEvent::AcknowledgeWindowsSandboxSetup); + })], + dismiss_on_select: true, + ..Default::default() + }, SelectionItem { name: "Quit".to_string(), description: None, @@ -8982,6 +8997,7 @@ impl ChatWidget { #[cfg(target_os = "windows")] pub(crate) fn maybe_prompt_windows_sandbox_enable(&mut self, show_now: bool) { if show_now + && !self.config.windows_wsl_setup_acknowledged && WindowsSandboxLevel::from_config(&self.config) == WindowsSandboxLevel::Disabled && let Some(preset) = builtin_approval_presets() .into_iter() @@ -9063,6 +9079,11 @@ impl ChatWidget { ); } + #[cfg_attr(not(target_os = "windows"), allow(dead_code))] + pub(crate) fn set_windows_wsl_setup_acknowledged(&mut self, acknowledged: bool) { + self.config.windows_wsl_setup_acknowledged = acknowledged; + } + #[cfg_attr(not(target_os = "windows"), allow(dead_code))] pub(crate) fn set_feature_enabled(&mut self, feature: Feature, enabled: bool) -> bool { if let Err(err) = self.config.features.set_enabled(feature, enabled) { diff --git a/codex-rs/tui/src/chatwidget/tests/permissions.rs b/codex-rs/tui/src/chatwidget/tests/permissions.rs index 09595a11ba..426d376ffd 100644 --- a/codex-rs/tui/src/chatwidget/tests/permissions.rs +++ b/codex-rs/tui/src/chatwidget/tests/permissions.rs @@ -220,6 +220,10 @@ async fn startup_prompts_for_windows_sandbox_when_agent_requested() { popup.contains("Use non-admin sandbox"), "expected startup prompt to offer non-admin fallback: {popup}" ); + assert!( + popup.contains("Continue without sandbox"), + "expected startup prompt to offer WSL/skip fallback: {popup}" + ); assert!( popup.contains("Quit"), "expected startup prompt to offer quit action: {popup}" @@ -241,6 +245,22 @@ async fn startup_does_not_prompt_for_windows_sandbox_when_not_requested() { ); } +#[cfg(target_os = "windows")] +#[tokio::test] +async fn startup_does_not_prompt_for_windows_sandbox_after_acknowledgement() { + let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; + + chat.set_feature_enabled(Feature::WindowsSandbox, /*enabled*/ false); + chat.set_feature_enabled(Feature::WindowsSandboxElevated, /*enabled*/ false); + chat.set_windows_wsl_setup_acknowledged(true); + chat.maybe_prompt_windows_sandbox_enable(/*show_now*/ true); + + assert!( + chat.bottom_pane.no_modal_or_popup_active(), + "expected no startup sandbox NUX popup after acknowledgement" + ); +} + #[tokio::test] async fn approvals_popup_shows_disabled_presets() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 466b7b9e6e..79bc70eb33 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -1415,6 +1415,7 @@ async fn run_ratatui_app( let should_show_trust_screen = should_show_trust_screen(&config); let should_prompt_windows_sandbox_nux_at_startup = cfg!(target_os = "windows") && trust_decision_was_made + && !config.windows_wsl_setup_acknowledged && WindowsSandboxLevel::from_config(&config) == WindowsSandboxLevel::Disabled; let Cli {