Compare commits

...

3 Commits

Author SHA1 Message Date
Dylan Hurd
4d98928c00 feat(tui2) /personality 2026-01-20 14:48:09 -08:00
Dylan Hurd
24ab98ed85 copy 2026-01-20 14:40:56 -08:00
Dylan Hurd
df6a95ce9c feat(tui) /permissions 2026-01-20 13:48:15 -08:00
23 changed files with 210 additions and 49 deletions

View File

@@ -38,7 +38,7 @@ pub fn builtin_approval_presets() -> Vec<ApprovalPreset> {
ApprovalPreset {
id: "full-access",
label: "Agent (full access)",
description: "Codex can edit files outside this workspace and run commands with network access. Exercise caution when using.",
description: "Codex can edit files outside this workspace and access the internet. Exercise caution when using.",
approval: AskForApproval::Never,
sandbox: SandboxPolicy::DangerFullAccess,
},

View File

@@ -997,8 +997,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,
@@ -1383,6 +1387,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;
}

View File

@@ -125,6 +125,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.
@@ -212,6 +213,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),

View File

@@ -302,6 +302,7 @@ mod tests {
cmds,
vec![
"model",
"permissions",
"experimental",
"resume",
"compact",

View File

@@ -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 "

View File

@@ -2254,6 +2254,9 @@ impl ChatWidget {
SlashCommand::Approvals => {
self.open_approvals_popup();
}
SlashCommand::Permissions => {
self.open_permissions_popup();
}
SlashCommand::ElevateSandbox => {
#[cfg(target_os = "windows")]
{
@@ -3519,6 +3522,54 @@ 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<F, D>(
&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<SelectionItem> = Vec::new();
@@ -3535,14 +3586,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 = Some(preset.description.to_string());
let name = label_for_preset(&preset, windows_degraded_sandbox_enabled);
let description = Some(description_for_preset(&preset));
let disabled_reason = match self.config.approval_policy.can_set(&preset.approval) {
Ok(()) => None,
Err(err) => Some(err.to_string()),
@@ -3558,6 +3608,7 @@ impl ChatWidget {
vec![Box::new(move |tx| {
tx.send(AppEvent::OpenFullAccessConfirmation {
preset: preset_clone.clone(),
return_to_permissions,
});
})]
} else if preset.id == "auto" {
@@ -3627,7 +3678,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,
@@ -3727,7 +3778,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<Box<dyn Renderable>> = Vec::new();
@@ -3755,8 +3810,12 @@ impl ChatWidget {
tx.send(AppEvent::PersistFullAccessWarningAcknowledged);
}));
let deny_actions: Vec<SelectionAction> = vec![Box::new(|tx| {
tx.send(AppEvent::OpenApprovalsPopup);
let deny_actions: Vec<SelectionAction> = vec![Box::new(move |tx| {
if return_to_permissions {
tx.send(AppEvent::OpenPermissionsPopup);
} else {
tx.send(AppEvent::OpenApprovalsPopup);
}
})];
let items = vec![

View File

@@ -2,12 +2,11 @@
source: tui/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

View File

@@ -2689,7 +2689,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);
@@ -3010,7 +3010,7 @@ async fn approvals_popup_navigation_skips_disabled() {
.expect("render approvals popup after disabled selection");
let screen = terminal.backend().vt100().screen().contents();
assert!(
screen.contains("Select Approval Mode"),
screen.contains("Update Model Permissions"),
"popup should remain open after selecting a disabled entry"
);
assert!(

View File

@@ -947,6 +947,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(),

View File

@@ -14,6 +14,7 @@ pub enum SlashCommand {
// more frequently used commands should be listed first.
Model,
Approvals,
Permissions,
#[strum(serialize = "setup-elevated-sandbox")]
ElevateSandbox,
Experimental,
@@ -60,6 +61,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::Experimental => "toggle beta features",
SlashCommand::Mcp => "list configured MCP tools",
@@ -86,6 +88,7 @@ impl SlashCommand {
// | SlashCommand::Undo
| SlashCommand::Model
| SlashCommand::Approvals
| SlashCommand::Permissions
| SlashCommand::ElevateSandbox
| SlashCommand::Experimental
| SlashCommand::Review

View File

@@ -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;
}

View File

@@ -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),

View File

@@ -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]

View File

@@ -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,

View File

@@ -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 "

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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<F, D>(
&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<SelectionItem> = 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<Box<dyn Renderable>> = Vec::new();
@@ -3438,8 +3490,12 @@ impl ChatWidget {
tx.send(AppEvent::PersistFullAccessWarningAcknowledged);
}));
let deny_actions: Vec<SelectionAction> = vec![Box::new(|tx| {
tx.send(AppEvent::OpenApprovalsPopup);
let deny_actions: Vec<SelectionAction> = vec![Box::new(move |tx| {
if return_to_permissions {
tx.send(AppEvent::OpenPermissionsPopup);
} else {
tx.send(AppEvent::OpenApprovalsPopup);
}
})];
let items = vec![

View File

@@ -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

View File

@@ -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);

View File

@@ -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(),

View File

@@ -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,