feat(tui) Permissions update history item (#11550)

## Summary
We should document in the tui when you switch permissions!

## Testing
- [x] Added unit tests
- [x] Tested locally
This commit is contained in:
Dylan Hurd 2026-02-13 23:44:27 -08:00 committed by GitHub
parent 3164670101
commit ebceb71db6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 237 additions and 7 deletions

View file

@ -5423,15 +5423,23 @@ impl ChatWidget {
});
})]
} else {
Self::approval_preset_actions(preset.approval, preset.sandbox.clone())
Self::approval_preset_actions(
preset.approval,
preset.sandbox.clone(),
name.clone(),
)
}
}
#[cfg(not(target_os = "windows"))]
{
Self::approval_preset_actions(preset.approval, preset.sandbox.clone())
Self::approval_preset_actions(
preset.approval,
preset.sandbox.clone(),
name.clone(),
)
}
} else {
Self::approval_preset_actions(preset.approval, preset.sandbox.clone())
Self::approval_preset_actions(preset.approval, preset.sandbox.clone(), name.clone())
};
items.push(SelectionItem {
name,
@ -5485,6 +5493,7 @@ impl ChatWidget {
fn approval_preset_actions(
approval: AskForApproval,
sandbox: SandboxPolicy,
label: String,
) -> Vec<SelectionAction> {
vec![Box::new(move |tx| {
let sandbox_clone = sandbox.clone();
@ -5501,6 +5510,9 @@ impl ChatWidget {
}));
tx.send(AppEvent::UpdateAskForApprovalPolicy(approval));
tx.send(AppEvent::UpdateSandboxPolicy(sandbox_clone));
tx.send(AppEvent::InsertHistoryCell(Box::new(
history_cell::new_info_event(format!("Permissions updated to {label}"), None),
)));
})]
}
@ -5547,6 +5559,7 @@ impl ChatWidget {
preset: ApprovalPreset,
return_to_permissions: bool,
) {
let selected_name = preset.label.to_string();
let approval = preset.approval;
let sandbox = preset.sandbox;
let mut header_children: Vec<Box<dyn Renderable>> = Vec::new();
@ -5563,12 +5576,14 @@ impl ChatWidget {
));
let header = ColumnRenderable::with(header_children);
let mut accept_actions = Self::approval_preset_actions(approval, sandbox.clone());
let mut accept_actions =
Self::approval_preset_actions(approval, sandbox.clone(), selected_name.clone());
accept_actions.push(Box::new(|tx| {
tx.send(AppEvent::UpdateFullAccessWarningAcknowledged(true));
}));
let mut accept_and_remember_actions = Self::approval_preset_actions(approval, sandbox);
let mut accept_and_remember_actions =
Self::approval_preset_actions(approval, sandbox, selected_name);
accept_and_remember_actions.push(Box::new(|tx| {
tx.send(AppEvent::UpdateFullAccessWarningAcknowledged(true));
tx.send(AppEvent::PersistFullAccessWarningAcknowledged);
@ -5678,7 +5693,11 @@ impl ChatWidget {
}));
}
if let (Some(approval), Some(sandbox)) = (approval, sandbox.clone()) {
accept_actions.extend(Self::approval_preset_actions(approval, sandbox));
accept_actions.extend(Self::approval_preset_actions(
approval,
sandbox,
mode_label.to_string(),
));
}
let mut accept_and_remember_actions: Vec<SelectionAction> = Vec::new();
@ -5687,7 +5706,11 @@ impl ChatWidget {
tx.send(AppEvent::PersistWorldWritableWarningAcknowledged);
}));
if let (Some(approval), Some(sandbox)) = (approval, sandbox) {
accept_and_remember_actions.extend(Self::approval_preset_actions(approval, sandbox));
accept_and_remember_actions.extend(Self::approval_preset_actions(
approval,
sandbox,
mode_label.to_string(),
));
}
let items = vec![

View file

@ -0,0 +1,5 @@
---
source: tui/src/chatwidget/tests.rs
expression: "lines_to_single_string(&cells[0])"
---
• Permissions updated to Full Access

View file

@ -0,0 +1,5 @@
---
source: tui/src/chatwidget/tests.rs
expression: "lines_to_single_string(&cells[0])"
---
• Permissions updated to Default

View file

@ -0,0 +1,5 @@
---
source: tui/src/chatwidget/tests.rs
expression: "lines_to_single_string(&cells[0])"
---
• Permissions updated to Default (non-admin sandbox)

View file

@ -20,6 +20,8 @@ use codex_core::config::Config;
use codex_core::config::ConfigBuilder;
use codex_core::config::Constrained;
use codex_core::config::ConstraintError;
#[cfg(target_os = "windows")]
use codex_core::config::types::WindowsSandboxModeToml;
use codex_core::config_loader::RequirementSource;
use codex_core::features::Feature;
use codex_core::models_manager::manager::ModelsManager;
@ -5430,6 +5432,196 @@ async fn approvals_popup_navigation_skips_disabled() {
);
}
#[tokio::test]
async fn permissions_selection_emits_history_cell_when_selection_changes() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None).await;
#[cfg(target_os = "windows")]
{
chat.config.notices.hide_world_writable_warning = Some(true);
chat.set_windows_sandbox_mode(Some(WindowsSandboxModeToml::Unelevated));
}
chat.config.notices.hide_full_access_warning = Some(true);
chat.open_permissions_popup();
chat.handle_key_event(KeyEvent::from(KeyCode::Down));
chat.handle_key_event(KeyEvent::from(KeyCode::Enter));
let cells = drain_insert_history(&mut rx);
assert_eq!(
cells.len(),
1,
"expected one permissions selection history cell"
);
let rendered = lines_to_single_string(&cells[0]);
assert!(
rendered.contains("Permissions updated to"),
"expected permissions selection history message, got: {rendered}"
);
}
#[tokio::test]
async fn permissions_selection_history_snapshot_after_mode_switch() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None).await;
#[cfg(target_os = "windows")]
{
chat.config.notices.hide_world_writable_warning = Some(true);
chat.set_windows_sandbox_mode(Some(WindowsSandboxModeToml::Unelevated));
}
chat.config.notices.hide_full_access_warning = Some(true);
chat.open_permissions_popup();
chat.handle_key_event(KeyEvent::from(KeyCode::Down));
#[cfg(target_os = "windows")]
chat.handle_key_event(KeyEvent::from(KeyCode::Down));
chat.handle_key_event(KeyEvent::from(KeyCode::Enter));
let cells = drain_insert_history(&mut rx);
assert_eq!(cells.len(), 1, "expected one mode-switch history cell");
assert_snapshot!(
"permissions_selection_history_after_mode_switch",
lines_to_single_string(&cells[0])
);
}
#[tokio::test]
async fn permissions_selection_history_snapshot_full_access_to_default() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None).await;
#[cfg(target_os = "windows")]
{
chat.config.notices.hide_world_writable_warning = Some(true);
chat.set_windows_sandbox_mode(Some(WindowsSandboxModeToml::Unelevated));
}
chat.config.notices.hide_full_access_warning = Some(true);
chat.config
.permissions
.approval_policy
.set(AskForApproval::Never)
.expect("set approval policy");
chat.config
.permissions
.sandbox_policy
.set(SandboxPolicy::DangerFullAccess)
.expect("set sandbox policy");
chat.open_permissions_popup();
chat.handle_key_event(KeyEvent::from(KeyCode::Up));
chat.handle_key_event(KeyEvent::from(KeyCode::Enter));
let cells = drain_insert_history(&mut rx);
assert_eq!(cells.len(), 1, "expected one mode-switch history cell");
let rendered = lines_to_single_string(&cells[0]);
#[cfg(target_os = "windows")]
insta::with_settings!({ snapshot_suffix => "windows" }, {
assert_snapshot!("permissions_selection_history_full_access_to_default", rendered);
});
#[cfg(not(target_os = "windows"))]
assert_snapshot!(
"permissions_selection_history_full_access_to_default",
rendered
);
}
#[tokio::test]
async fn permissions_selection_emits_history_cell_when_current_is_selected() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None).await;
#[cfg(target_os = "windows")]
{
chat.config.notices.hide_world_writable_warning = Some(true);
chat.set_windows_sandbox_mode(Some(WindowsSandboxModeToml::Unelevated));
}
chat.config
.permissions
.approval_policy
.set(AskForApproval::OnRequest)
.expect("set approval policy");
chat.config
.permissions
.sandbox_policy
.set(SandboxPolicy::new_workspace_write_policy())
.expect("set sandbox policy");
chat.open_permissions_popup();
chat.handle_key_event(KeyEvent::from(KeyCode::Enter));
let cells = drain_insert_history(&mut rx);
assert_eq!(
cells.len(),
1,
"expected history cell even when selecting current permissions"
);
let rendered = lines_to_single_string(&cells[0]);
assert!(
rendered.contains("Permissions updated to"),
"expected permissions update history message, got: {rendered}"
);
}
#[tokio::test]
async fn permissions_full_access_history_cell_emitted_only_after_confirmation() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None).await;
#[cfg(target_os = "windows")]
{
chat.config.notices.hide_world_writable_warning = Some(true);
chat.set_windows_sandbox_mode(Some(WindowsSandboxModeToml::Unelevated));
}
chat.config.notices.hide_full_access_warning = None;
chat.open_permissions_popup();
chat.handle_key_event(KeyEvent::from(KeyCode::Down));
#[cfg(target_os = "windows")]
chat.handle_key_event(KeyEvent::from(KeyCode::Down));
chat.handle_key_event(KeyEvent::from(KeyCode::Enter));
let mut open_confirmation_event = None;
let mut cells_before_confirmation = Vec::new();
while let Ok(event) = rx.try_recv() {
match event {
AppEvent::InsertHistoryCell(cell) => {
cells_before_confirmation.push(cell.display_lines(80));
}
AppEvent::OpenFullAccessConfirmation {
preset,
return_to_permissions,
} => {
open_confirmation_event = Some((preset, return_to_permissions));
}
_ => {}
}
}
if cfg!(not(target_os = "windows")) {
assert!(
cells_before_confirmation.is_empty(),
"did not expect history cell before confirming full access"
);
}
let (preset, return_to_permissions) =
open_confirmation_event.expect("expected full access confirmation event");
chat.open_full_access_confirmation(preset, return_to_permissions);
let popup = render_bottom_popup(&chat, 80);
assert!(
popup.contains("Enable full access?"),
"expected full access confirmation popup, got: {popup}"
);
chat.handle_key_event(KeyEvent::from(KeyCode::Enter));
let cells_after_confirmation = drain_insert_history(&mut rx);
let total_history_cells = cells_before_confirmation.len() + cells_after_confirmation.len();
assert_eq!(
total_history_cells, 1,
"expected one full access history cell total"
);
let rendered = if !cells_before_confirmation.is_empty() {
lines_to_single_string(&cells_before_confirmation[0])
} else {
lines_to_single_string(&cells_after_confirmation[0])
};
assert!(
rendered.contains("Permissions updated to Full Access"),
"expected full access update history message, got: {rendered}"
);
}
//
// Snapshot test: command approval modal
//