From 4d98928c00a655beaee847453ad739bc2653ee54 Mon Sep 17 00:00:00 2001 From: Dylan Hurd Date: Tue, 20 Jan 2026 14:48:09 -0800 Subject: [PATCH] feat(tui2) /personality --- codex-rs/tui2/src/app.rs | 11 ++- codex-rs/tui2/src/app_event.rs | 4 + .../tui2/src/bottom_pane/command_popup.rs | 12 ++- .../src/bottom_pane/list_selection_view.rs | 6 +- ..._chat_composer__tests__slash_popup_mo.snap | 4 +- ...sts__list_selection_footer_note_wraps.snap | 3 +- ..._list_selection_spacing_with_subtitle.snap | 2 +- ...st_selection_spacing_without_subtitle.snap | 2 +- codex-rs/tui2/src/chatwidget.rs | 78 ++++++++++++++++--- ...get__tests__approvals_selection_popup.snap | 7 +- codex-rs/tui2/src/chatwidget/tests.rs | 2 +- codex-rs/tui2/src/history_cell.rs | 5 ++ codex-rs/tui2/src/slash_command.rs | 3 + 13 files changed, 111 insertions(+), 28 deletions(-) diff --git a/codex-rs/tui2/src/app.rs b/codex-rs/tui2/src/app.rs index 920d571451..e2a46cc65d 100644 --- a/codex-rs/tui2/src/app.rs +++ b/codex-rs/tui2/src/app.rs @@ -1725,8 +1725,12 @@ impl App { AppEvent::OpenAllModelsPopup { models } => { self.chat_widget.open_all_models_popup(models); } - AppEvent::OpenFullAccessConfirmation { preset } => { - self.chat_widget.open_full_access_confirmation(preset); + AppEvent::OpenFullAccessConfirmation { + preset, + return_to_permissions, + } => { + self.chat_widget + .open_full_access_confirmation(preset, return_to_permissions); } AppEvent::OpenWorldWritableWarningConfirmation { preset, @@ -2070,6 +2074,9 @@ impl App { AppEvent::OpenApprovalsPopup => { self.chat_widget.open_approvals_popup(); } + AppEvent::OpenPermissionsPopup => { + self.chat_widget.open_permissions_popup(); + } AppEvent::OpenReviewBranchPicker(cwd) => { self.chat_widget.show_review_branch_picker(&cwd).await; } diff --git a/codex-rs/tui2/src/app_event.rs b/codex-rs/tui2/src/app_event.rs index ff3f0e3875..b3a7dbd483 100644 --- a/codex-rs/tui2/src/app_event.rs +++ b/codex-rs/tui2/src/app_event.rs @@ -119,6 +119,7 @@ pub(crate) enum AppEvent { /// Open the confirmation prompt before enabling full access mode. OpenFullAccessConfirmation { preset: ApprovalPreset, + return_to_permissions: bool, }, /// Open the Windows world-writable directories warning. @@ -201,6 +202,9 @@ pub(crate) enum AppEvent { /// Re-open the approval presets popup. OpenApprovalsPopup, + /// Re-open the permissions presets popup. + OpenPermissionsPopup, + /// Open the branch picker option from the review popup. OpenReviewBranchPicker(PathBuf), diff --git a/codex-rs/tui2/src/bottom_pane/command_popup.rs b/codex-rs/tui2/src/bottom_pane/command_popup.rs index 000ce2e4ea..d146f08c70 100644 --- a/codex-rs/tui2/src/bottom_pane/command_popup.rs +++ b/codex-rs/tui2/src/bottom_pane/command_popup.rs @@ -297,7 +297,17 @@ mod tests { CommandItem::UserPrompt(_) => None, }) .collect(); - assert_eq!(cmds, vec!["model", "resume", "compact", "mention", "mcp"]); + assert_eq!( + cmds, + vec![ + "model", + "permissions", + "resume", + "compact", + "mention", + "mcp" + ] + ); } #[test] diff --git a/codex-rs/tui2/src/bottom_pane/list_selection_view.rs b/codex-rs/tui2/src/bottom_pane/list_selection_view.rs index 27c7dc4233..182a6d485f 100644 --- a/codex-rs/tui2/src/bottom_pane/list_selection_view.rs +++ b/codex-rs/tui2/src/bottom_pane/list_selection_view.rs @@ -559,7 +559,7 @@ mod tests { ]; ListSelectionView::new( SelectionViewParams { - title: Some("Select Approval Mode".to_string()), + title: Some("Update Model Permissions".to_string()), subtitle: subtitle.map(str::to_string), footer_hint: Some(standard_popup_hint_line()), items, @@ -629,7 +629,7 @@ mod tests { ]); let view = ListSelectionView::new( SelectionViewParams { - title: Some("Select Approval Mode".to_string()), + title: Some("Update Model Permissions".to_string()), footer_note: Some(footer_note), footer_hint: Some(standard_popup_hint_line()), items, @@ -656,7 +656,7 @@ mod tests { }]; let mut view = ListSelectionView::new( SelectionViewParams { - title: Some("Select Approval Mode".to_string()), + title: Some("Update Model Permissions".to_string()), footer_hint: Some(standard_popup_hint_line()), items, is_searchable: true, diff --git a/codex-rs/tui2/src/bottom_pane/snapshots/codex_tui2__bottom_pane__chat_composer__tests__slash_popup_mo.snap b/codex-rs/tui2/src/bottom_pane/snapshots/codex_tui2__bottom_pane__chat_composer__tests__slash_popup_mo.snap index dc66d149e4..5eac0b56ab 100644 --- a/codex-rs/tui2/src/bottom_pane/snapshots/codex_tui2__bottom_pane__chat_composer__tests__slash_popup_mo.snap +++ b/codex-rs/tui2/src/bottom_pane/snapshots/codex_tui2__bottom_pane__chat_composer__tests__slash_popup_mo.snap @@ -5,5 +5,5 @@ expression: terminal.backend() " " "› /mo " " " -" /model choose what model and reasoning effort to use " -" /mention mention a file " +" /model choose what model and reasoning effort to " +" use " diff --git a/codex-rs/tui2/src/bottom_pane/snapshots/codex_tui2__bottom_pane__list_selection_view__tests__list_selection_footer_note_wraps.snap b/codex-rs/tui2/src/bottom_pane/snapshots/codex_tui2__bottom_pane__list_selection_view__tests__list_selection_footer_note_wraps.snap index 18d0eb2483..5416271a2a 100644 --- a/codex-rs/tui2/src/bottom_pane/snapshots/codex_tui2__bottom_pane__list_selection_view__tests__list_selection_footer_note_wraps.snap +++ b/codex-rs/tui2/src/bottom_pane/snapshots/codex_tui2__bottom_pane__list_selection_view__tests__list_selection_footer_note_wraps.snap @@ -1,10 +1,9 @@ --- source: tui2/src/bottom_pane/list_selection_view.rs -assertion_line: 640 expression: "render_lines_with_width(&view, 40)" --- - Select Approval Mode + Update Model Permissions › 1. Read Only (current) Codex can read files diff --git a/codex-rs/tui2/src/bottom_pane/snapshots/codex_tui2__bottom_pane__list_selection_view__tests__list_selection_spacing_with_subtitle.snap b/codex-rs/tui2/src/bottom_pane/snapshots/codex_tui2__bottom_pane__list_selection_view__tests__list_selection_spacing_with_subtitle.snap index 2cc2578c56..f5828b178f 100644 --- a/codex-rs/tui2/src/bottom_pane/snapshots/codex_tui2__bottom_pane__list_selection_view__tests__list_selection_spacing_with_subtitle.snap +++ b/codex-rs/tui2/src/bottom_pane/snapshots/codex_tui2__bottom_pane__list_selection_view__tests__list_selection_spacing_with_subtitle.snap @@ -3,7 +3,7 @@ source: tui2/src/bottom_pane/list_selection_view.rs expression: render_lines(&view) --- - Select Approval Mode + Update Model Permissions Switch between Codex approval presets › 1. Read Only (current) Codex can read files diff --git a/codex-rs/tui2/src/bottom_pane/snapshots/codex_tui2__bottom_pane__list_selection_view__tests__list_selection_spacing_without_subtitle.snap b/codex-rs/tui2/src/bottom_pane/snapshots/codex_tui2__bottom_pane__list_selection_view__tests__list_selection_spacing_without_subtitle.snap index 88a5d14932..c1b7b93e64 100644 --- a/codex-rs/tui2/src/bottom_pane/snapshots/codex_tui2__bottom_pane__list_selection_view__tests__list_selection_spacing_without_subtitle.snap +++ b/codex-rs/tui2/src/bottom_pane/snapshots/codex_tui2__bottom_pane__list_selection_view__tests__list_selection_spacing_without_subtitle.snap @@ -3,7 +3,7 @@ source: tui2/src/bottom_pane/list_selection_view.rs expression: render_lines(&view) --- - Select Approval Mode + Update Model Permissions › 1. Read Only (current) Codex can read files 2. Full Access Codex can edit files diff --git a/codex-rs/tui2/src/chatwidget.rs b/codex-rs/tui2/src/chatwidget.rs index c4cdf76695..eb2c866206 100644 --- a/codex-rs/tui2/src/chatwidget.rs +++ b/codex-rs/tui2/src/chatwidget.rs @@ -2020,6 +2020,9 @@ impl ChatWidget { SlashCommand::Approvals => { self.open_approvals_popup(); } + SlashCommand::Permissions => { + self.open_permissions_popup(); + } SlashCommand::ElevateSandbox => { #[cfg(target_os = "windows")] { @@ -3225,6 +3228,52 @@ impl ChatWidget { /// Open a popup to choose the approvals mode (ask for approval policy + sandbox policy). pub(crate) fn open_approvals_popup(&mut self) { + self.open_approval_mode_popup( + "Update Model Permissions", + true, + false, + |preset, is_windows_degraded| { + if preset.id == "auto" && is_windows_degraded { + "Agent (non-elevated sandbox)".to_string() + } else { + preset.label.to_string() + } + }, + |preset| preset.description.to_string(), + ); + } + + /// Open a popup to choose the permissions mode (approval policy + sandbox policy). + pub(crate) fn open_permissions_popup(&mut self) { + let include_read_only = cfg!(target_os = "windows"); + self.open_approval_mode_popup( + "Update Model Permissions", + include_read_only, + true, + |preset, _| match preset.id { + "read-only" => "Read Only".to_string(), + "auto" => "Default".to_string(), + "full-access" => "Full Access".to_string(), + _ => preset.label.to_string(), + }, + |preset| match preset.id { + "auto" => format!("{} Identical to Agent approvals.", preset.description), + _ => preset.description.to_string(), + }, + ); + } + + fn open_approval_mode_popup( + &mut self, + title: &str, + include_read_only: bool, + return_to_permissions: bool, + label_for_preset: F, + description_for_preset: D, + ) where + F: Fn(&ApprovalPreset, bool) -> String, + D: Fn(&ApprovalPreset) -> String, + { let current_approval = self.config.approval_policy.value(); let current_sandbox = self.config.sandbox_policy.get(); let mut items: Vec = Vec::new(); @@ -3241,15 +3290,13 @@ impl ChatWidget { && presets.iter().any(|preset| preset.id == "auto"); for preset in presets.into_iter() { + if !include_read_only && preset.id == "read-only" { + continue; + } let is_current = Self::preset_matches_current(current_approval, current_sandbox, &preset); - let name = if preset.id == "auto" && windows_degraded_sandbox_enabled { - "Agent (non-elevated sandbox)".to_string() - } else { - preset.label.to_string() - }; - let description_text = preset.description; - let description = Some(description_text.to_string()); + let name = label_for_preset(&preset, windows_degraded_sandbox_enabled); + let description = Some(description_for_preset(&preset)); let requires_confirmation = preset.id == "full-access" && !self .config @@ -3261,6 +3308,7 @@ impl ChatWidget { vec![Box::new(move |tx| { tx.send(AppEvent::OpenFullAccessConfirmation { preset: preset_clone.clone(), + return_to_permissions, }); })] } else if preset.id == "auto" { @@ -3329,7 +3377,7 @@ impl ChatWidget { }); self.bottom_pane.show_selection_view(SelectionViewParams { - title: Some("Select Approval Mode".to_string()), + title: Some(title.to_string()), footer_note, footer_hint: Some(standard_popup_hint_line()), items, @@ -3410,7 +3458,11 @@ impl ChatWidget { None } - pub(crate) fn open_full_access_confirmation(&mut self, preset: ApprovalPreset) { + pub(crate) fn open_full_access_confirmation( + &mut self, + preset: ApprovalPreset, + return_to_permissions: bool, + ) { let approval = preset.approval; let sandbox = preset.sandbox; let mut header_children: Vec> = Vec::new(); @@ -3438,8 +3490,12 @@ impl ChatWidget { tx.send(AppEvent::PersistFullAccessWarningAcknowledged); })); - let deny_actions: Vec = vec![Box::new(|tx| { - tx.send(AppEvent::OpenApprovalsPopup); + let deny_actions: Vec = vec![Box::new(move |tx| { + if return_to_permissions { + tx.send(AppEvent::OpenPermissionsPopup); + } else { + tx.send(AppEvent::OpenApprovalsPopup); + } })]; let items = vec![ diff --git a/codex-rs/tui2/src/chatwidget/snapshots/codex_tui2__chatwidget__tests__approvals_selection_popup.snap b/codex-rs/tui2/src/chatwidget/snapshots/codex_tui2__chatwidget__tests__approvals_selection_popup.snap index 46ec74d117..9ba6bb598f 100644 --- a/codex-rs/tui2/src/chatwidget/snapshots/codex_tui2__chatwidget__tests__approvals_selection_popup.snap +++ b/codex-rs/tui2/src/chatwidget/snapshots/codex_tui2__chatwidget__tests__approvals_selection_popup.snap @@ -2,12 +2,11 @@ source: tui2/src/chatwidget/tests.rs expression: popup --- - Select Approval Mode + Update Model Permissions › 1. Read Only (current) Requires approval to edit files and run commands. 2. Agent Read and edit files, and run commands. - 3. Agent (full access) Codex can edit files outside this workspace and run - commands with network access. Exercise caution when - using. + 3. Agent (full access) Codex can edit files outside this workspace and + access the internet. Exercise caution when using. Press enter to confirm or esc to go back diff --git a/codex-rs/tui2/src/chatwidget/tests.rs b/codex-rs/tui2/src/chatwidget/tests.rs index f1cf4c32e0..06c5fec0a2 100644 --- a/codex-rs/tui2/src/chatwidget/tests.rs +++ b/codex-rs/tui2/src/chatwidget/tests.rs @@ -2404,7 +2404,7 @@ async fn full_access_confirmation_popup_snapshot() { .into_iter() .find(|preset| preset.id == "full-access") .expect("full access preset"); - chat.open_full_access_confirmation(preset); + chat.open_full_access_confirmation(preset, false); let popup = render_bottom_popup(&chat, 80); assert_snapshot!("full_access_confirmation_popup", popup); diff --git a/codex-rs/tui2/src/history_cell.rs b/codex-rs/tui2/src/history_cell.rs index a313f9b2a7..f2cb1f7434 100644 --- a/codex-rs/tui2/src/history_cell.rs +++ b/codex-rs/tui2/src/history_cell.rs @@ -1009,6 +1009,11 @@ pub(crate) fn new_session_info( "/approvals".into(), " - choose what Codex can do without approval".dim(), ]), + Line::from(vec![ + " ".into(), + "/permissions".into(), + " - choose what Codex is allowed to do".dim(), + ]), Line::from(vec![ " ".into(), "/model".into(), diff --git a/codex-rs/tui2/src/slash_command.rs b/codex-rs/tui2/src/slash_command.rs index 2e1e032983..749958212d 100644 --- a/codex-rs/tui2/src/slash_command.rs +++ b/codex-rs/tui2/src/slash_command.rs @@ -14,6 +14,7 @@ pub enum SlashCommand { // more frequently used commands should be listed first. Model, Approvals, + Permissions, #[strum(serialize = "setup-elevated-sandbox")] ElevateSandbox, Skills, @@ -57,6 +58,7 @@ impl SlashCommand { SlashCommand::Model => "choose what model and reasoning effort to use", SlashCommand::Collab => "change collaboration mode (experimental)", SlashCommand::Approvals => "choose what Codex can do without approval", + SlashCommand::Permissions => "choose what Codex is allowed to do", SlashCommand::ElevateSandbox => "set up elevated agent sandbox", SlashCommand::Mcp => "list configured MCP tools", SlashCommand::Logout => "log out of Codex", @@ -82,6 +84,7 @@ impl SlashCommand { // | SlashCommand::Undo | SlashCommand::Model | SlashCommand::Approvals + | SlashCommand::Permissions | SlashCommand::ElevateSandbox | SlashCommand::Review | SlashCommand::Logout => false,