feat(tui): clarify Windows auto mode requirements (#5568)
## Summary - Coerce Windows `workspace-write` configs back to read-only, surface the forced downgrade in the approvals popup, and funnel users toward WSL or Full Access. - Add WSL installation instructions to the Auto preset on Windows while keeping the preset available for other platforms. - Skip the trust-on-first-run prompt on native Windows so new folders remain read-only without additional confirmation. - Expose a structured sandbox policy resolution from config to flag Windows downgrades and adjust tests (core, exec, TUI) to reflect the new behavior; provide a Windows-only approvals snapshot. ## Testing - cargo fmt - cargo test -p codex-core config::tests::add_dir_override_extends_workspace_writable_roots - cargo test -p codex-exec suite::resume::exec_resume_preserves_cli_configuration_overrides - cargo test -p codex-tui chatwidget::tests::approvals_selection_popup_snapshot - cargo test -p codex-tui approvals_popup_includes_wsl_note_for_auto_mode - cargo test -p codex-tui windows_skips_trust_prompt - just fix -p codex-core - just fix -p codex-tui
This commit is contained in:
@@ -369,6 +369,9 @@ impl App {
|
||||
AppEvent::OpenFeedbackConsent { category } => {
|
||||
self.chat_widget.open_feedback_consent(category);
|
||||
}
|
||||
AppEvent::ShowWindowsAutoModeInstructions => {
|
||||
self.chat_widget.open_windows_auto_mode_instructions();
|
||||
}
|
||||
AppEvent::PersistModelSelection { model, effort } => {
|
||||
let profile = self.active_profile.as_deref();
|
||||
match persist_model_selection(&self.config.codex_home, profile, &model, effort)
|
||||
|
||||
@@ -72,6 +72,9 @@ pub(crate) enum AppEvent {
|
||||
preset: ApprovalPreset,
|
||||
},
|
||||
|
||||
/// Show Windows Subsystem for Linux setup instructions for auto mode.
|
||||
ShowWindowsAutoModeInstructions,
|
||||
|
||||
/// Update the current approval policy in the running app and widget.
|
||||
UpdateAskForApprovalPolicy(AskForApproval),
|
||||
|
||||
|
||||
@@ -88,6 +88,8 @@ use crate::history_cell::AgentMessageCell;
|
||||
use crate::history_cell::HistoryCell;
|
||||
use crate::history_cell::McpToolCallCell;
|
||||
use crate::markdown::append_markdown;
|
||||
#[cfg(target_os = "windows")]
|
||||
use crate::onboarding::WSL_INSTRUCTIONS;
|
||||
use crate::render::renderable::ColumnRenderable;
|
||||
use crate::render::renderable::Renderable;
|
||||
use crate::slash_command::SlashCommand;
|
||||
@@ -1787,11 +1789,38 @@ impl ChatWidget {
|
||||
let current_sandbox = self.config.sandbox_policy.clone();
|
||||
let mut items: Vec<SelectionItem> = Vec::new();
|
||||
let presets: Vec<ApprovalPreset> = builtin_approval_presets();
|
||||
#[cfg(target_os = "windows")]
|
||||
let header_renderable: Box<dyn Renderable> = if self
|
||||
.config
|
||||
.forced_auto_mode_downgraded_on_windows
|
||||
{
|
||||
use ratatui_macros::line;
|
||||
|
||||
let mut header = ColumnRenderable::new();
|
||||
header.push(line![
|
||||
"Codex forced your settings back to Read Only on this Windows machine.".bold()
|
||||
]);
|
||||
header.push(line![
|
||||
"To re-enable Auto mode, run Codex inside Windows Subsystem for Linux (WSL) or enable Full Access manually.".dim()
|
||||
]);
|
||||
Box::new(header)
|
||||
} else {
|
||||
Box::new(())
|
||||
};
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let header_renderable: Box<dyn Renderable> = Box::new(());
|
||||
for preset in presets.into_iter() {
|
||||
let is_current =
|
||||
current_approval == preset.approval && current_sandbox == preset.sandbox;
|
||||
let name = preset.label.to_string();
|
||||
let description = Some(preset.description.to_string());
|
||||
let description_text = preset.description;
|
||||
let description = if cfg!(target_os = "windows") && preset.id == "auto" {
|
||||
Some(format!(
|
||||
"{description_text}\nRequires Windows Subsystem for Linux (WSL). Show installation instructions..."
|
||||
))
|
||||
} else {
|
||||
Some(description_text.to_string())
|
||||
};
|
||||
let requires_confirmation = preset.id == "full-access"
|
||||
&& !self
|
||||
.config
|
||||
@@ -1805,6 +1834,10 @@ impl ChatWidget {
|
||||
preset: preset_clone.clone(),
|
||||
});
|
||||
})]
|
||||
} else if cfg!(target_os = "windows") && preset.id == "auto" {
|
||||
vec![Box::new(|tx| {
|
||||
tx.send(AppEvent::ShowWindowsAutoModeInstructions);
|
||||
})]
|
||||
} else {
|
||||
Self::approval_preset_actions(preset.approval, preset.sandbox.clone())
|
||||
};
|
||||
@@ -1822,6 +1855,7 @@ impl ChatWidget {
|
||||
title: Some("Select Approval Mode".to_string()),
|
||||
footer_hint: Some(standard_popup_hint_line()),
|
||||
items,
|
||||
header: header_renderable,
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
@@ -1909,6 +1943,43 @@ impl ChatWidget {
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub(crate) fn open_windows_auto_mode_instructions(&mut self) {
|
||||
use ratatui_macros::line;
|
||||
|
||||
let mut header = ColumnRenderable::new();
|
||||
header.push(line![
|
||||
"Auto mode requires Windows Subsystem for Linux (WSL2).".bold()
|
||||
]);
|
||||
header.push(line!["Run Codex inside WSL to enable sandboxed commands."]);
|
||||
header.push(line![""]);
|
||||
header.push(Paragraph::new(WSL_INSTRUCTIONS).wrap(Wrap { trim: false }));
|
||||
|
||||
let items = vec![SelectionItem {
|
||||
name: "Back".to_string(),
|
||||
description: Some(
|
||||
"Return to the approval mode list. Auto mode stays disabled outside WSL."
|
||||
.to_string(),
|
||||
),
|
||||
actions: vec![Box::new(|tx| {
|
||||
tx.send(AppEvent::OpenApprovalsPopup);
|
||||
})],
|
||||
dismiss_on_select: true,
|
||||
..Default::default()
|
||||
}];
|
||||
|
||||
self.bottom_pane.show_selection_view(SelectionViewParams {
|
||||
title: None,
|
||||
footer_hint: Some(standard_popup_hint_line()),
|
||||
items,
|
||||
header: Box::new(header),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub(crate) fn open_windows_auto_mode_instructions(&mut self) {}
|
||||
|
||||
/// Set the approval policy in the widget's config copy.
|
||||
pub(crate) fn set_approval_policy(&mut self, policy: AskForApproval) {
|
||||
self.config.approval_policy = policy;
|
||||
|
||||
@@ -6,12 +6,12 @@ expression: popup
|
||||
|
||||
› 1. Read Only (current) Codex can read files and answer questions. Codex
|
||||
requires approval to make edits, run commands, or
|
||||
access network
|
||||
access network.
|
||||
2. Auto Codex can read files, make edits, and run commands
|
||||
in the workspace. Codex requires approval to work
|
||||
outside the workspace or access network
|
||||
outside the workspace or access network.
|
||||
3. Full Access Codex can read files, make edits, and run commands
|
||||
with network access, without approval. Exercise
|
||||
caution
|
||||
caution.
|
||||
|
||||
Press enter to confirm or esc to go back
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests.rs
|
||||
expression: popup
|
||||
---
|
||||
Select Approval Mode
|
||||
|
||||
› 1. Read Only (current) Codex can read files and answer questions. Codex
|
||||
requires approval to make edits, run commands, or
|
||||
access network.
|
||||
2. Auto Codex can read files, make edits, and run commands
|
||||
in the workspace. Codex requires approval to work
|
||||
outside the workspace or access network.
|
||||
Requires Windows Subsystem for Linux (WSL). Show
|
||||
installation instructions...
|
||||
3. Full Access Codex can read files, make edits, and run commands
|
||||
with network access, without approval. Exercise
|
||||
caution.
|
||||
|
||||
Press enter to confirm or esc to go back
|
||||
@@ -1276,9 +1276,36 @@ fn approvals_selection_popup_snapshot() {
|
||||
chat.open_approvals_popup();
|
||||
|
||||
let popup = render_bottom_popup(&chat, 80);
|
||||
#[cfg(target_os = "windows")]
|
||||
insta::with_settings!({ snapshot_suffix => "windows" }, {
|
||||
assert_snapshot!("approvals_selection_popup", popup);
|
||||
});
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
assert_snapshot!("approvals_selection_popup", popup);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn approvals_popup_includes_wsl_note_for_auto_mode() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual();
|
||||
|
||||
if cfg!(target_os = "windows") {
|
||||
chat.config.forced_auto_mode_downgraded_on_windows = true;
|
||||
}
|
||||
chat.open_approvals_popup();
|
||||
|
||||
let popup = render_bottom_popup(&chat, 80);
|
||||
assert_eq!(
|
||||
popup.contains("Requires Windows Subsystem for Linux (WSL)"),
|
||||
cfg!(target_os = "windows"),
|
||||
"expected auto preset description to mention WSL requirement only on Windows, popup: {popup}"
|
||||
);
|
||||
assert_eq!(
|
||||
popup.contains("Codex forced your settings back to Read Only on this Windows machine."),
|
||||
cfg!(target_os = "windows") && chat.config.forced_auto_mode_downgraded_on_windows,
|
||||
"expected downgrade notice only when auto mode is forced off on Windows, popup: {popup}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn full_access_confirmation_popup_snapshot() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual();
|
||||
@@ -1293,6 +1320,20 @@ fn full_access_confirmation_popup_snapshot() {
|
||||
assert_snapshot!("full_access_confirmation_popup", popup);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[test]
|
||||
fn windows_auto_mode_instructions_popup_lists_install_steps() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual();
|
||||
|
||||
chat.open_windows_auto_mode_instructions();
|
||||
|
||||
let popup = render_bottom_popup(&chat, 120);
|
||||
assert!(
|
||||
popup.contains("wsl --install"),
|
||||
"expected WSL instructions popup to include install command, popup: {popup}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn model_reasoning_selection_popup_snapshot() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual();
|
||||
|
||||
@@ -507,14 +507,16 @@ async fn load_config_or_exit(
|
||||
/// or if the current cwd project is already trusted. If not, we need to
|
||||
/// show the trust screen.
|
||||
fn should_show_trust_screen(config: &Config) -> bool {
|
||||
if config.did_user_set_custom_approval_policy_or_sandbox_mode {
|
||||
// if the user has overridden either approval policy or sandbox mode,
|
||||
// skip the trust flow
|
||||
false
|
||||
} else {
|
||||
// otherwise, skip iff the active project is trusted
|
||||
!config.active_project.is_trusted()
|
||||
if cfg!(target_os = "windows") {
|
||||
// Native Windows cannot enforce sandboxed write access without WSL; skip the trust prompt entirely.
|
||||
return false;
|
||||
}
|
||||
if config.did_user_set_custom_approval_policy_or_sandbox_mode {
|
||||
// Respect explicit approval/sandbox overrides made by the user.
|
||||
return false;
|
||||
}
|
||||
// otherwise, skip iff the active project is trusted
|
||||
!config.active_project.is_trusted()
|
||||
}
|
||||
|
||||
fn should_show_onboarding(
|
||||
@@ -543,3 +545,38 @@ fn should_show_login_screen(login_status: LoginStatus, config: &Config) -> bool
|
||||
|
||||
login_status == LoginStatus::NotAuthenticated
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use codex_core::config::ConfigOverrides;
|
||||
use codex_core::config::ConfigToml;
|
||||
use codex_core::config::ProjectConfig;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[test]
|
||||
fn windows_skips_trust_prompt() -> std::io::Result<()> {
|
||||
let temp_dir = TempDir::new()?;
|
||||
let mut config = Config::load_from_base_config_with_overrides(
|
||||
ConfigToml::default(),
|
||||
ConfigOverrides::default(),
|
||||
temp_dir.path().to_path_buf(),
|
||||
)?;
|
||||
config.did_user_set_custom_approval_policy_or_sandbox_mode = false;
|
||||
config.active_project = ProjectConfig { trust_level: None };
|
||||
|
||||
let should_show = should_show_trust_screen(&config);
|
||||
if cfg!(target_os = "windows") {
|
||||
assert!(
|
||||
!should_show,
|
||||
"Windows trust prompt should always be skipped on native Windows"
|
||||
);
|
||||
} else {
|
||||
assert!(
|
||||
should_show,
|
||||
"Non-Windows should still show trust prompt when project is untrusted"
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user