Show permissions and approval mode in the TUI status line (#21677)

Fixes #21665.

## Why

The TUI status line is the right place for compact, glanceable session
state. The original request was motivated by the need to see the active
permission posture without opening `/permissions` or `/status`,
especially when switching between safer and more permissive modes during
a session.

This PR intentionally separates `permissions` from `approval-mode`
instead of combining them into one status-line item. They answer related
but different questions: `permissions` describes the active
sandbox/profile shape, while `approval-mode` describes how command
approvals are handled. Keeping them separate makes each item
independently configurable and avoids long combined labels in an already
space-constrained status line.

The tradeoff is that users who want the full permission posture in the
status line need to opt into both items. In exchange, users can show
only the sandbox/profile label, only the approval behavior, or both, and
named user-defined profiles remain concise. Non-standard permission
shapes are rendered as `Custom permissions` rather than trying to
squeeze detailed profile contents into the status line; `/status`
remains the fuller explanatory surface.

## What changed

- Added a configurable `permissions` status-line item.
- Added a separate `approval-mode` status-line item, with `approval` as
an alias.
- Render standard permission states compactly as `Read Only`,
`Workspace`, or `Full Access`.
- Preserve user-defined permission profile names directly in the status
line.
- Render unnamed non-standard permission shapes as `Custom permissions`.
- Refresh status surfaces when `/permissions` updates the permission
profile, approval policy, or approval reviewer.
- Updated status-line preview snapshot coverage for the new items.

## Verification

- `cargo test -p codex-tui
status_permissions_non_default_workspace_write_uses_workspace_label`
- `cargo test -p codex-tui
permissions_selection_emits_history_cell_when_selection_changes`
- `cargo insta pending-snapshots --manifest-path tui/Cargo.toml`
This commit is contained in:
Eric Traut authored and GitHub committed 2026-05-08 08:03:11 -07:00
1 parent f86d95a242
commit e6312d44f0
7 files changed
+77 -2

No files matched your search

@@ -2,5 +2,5 @@
source: tui/src/chatwidget/tests/status_surface_previews.rs
expression: snapshot
---
status line: my-project · feat/awesome-feature · thread title
status line: my-project · feat/awesome-feature · thread title · Read Only · on-request
terminal title: thread title | feat/awesome-feature | Tasks 0/0
@@ -6,7 +6,12 @@
use super::*;
use crate::bottom_pane::status_line_from_segments;
use crate::branch_summary;
use crate::legacy_core::config::Config;
use crate::status::format_tokens_compact;
use codex_app_server_protocol::AskForApproval;
use codex_protocol::config_types::ApprovalsReviewer;
use codex_protocol::models::PermissionProfile;
use codex_utils_sandbox_summary::summarize_permission_profile;
/// Items shown in the terminal title when the user has not configured a
/// custom selection. Intentionally minimal: activity indicator + project name.
@@ -590,6 +595,8 @@ impl ChatWidget {
}
}),
StatusLineItem::Status => Some(self.run_state_status_text()),
StatusLineItem::Permissions => Some(permissions_display(&self.config)),
StatusLineItem::ApprovalMode => Some(approval_mode_display(&self.config)),
StatusLineItem::UsedTokens => {
let usage = self.status_line_total_usage();
let total = usage.blended_total();
@@ -678,6 +685,8 @@ impl ChatWidget {
StatusSurfacePreviewItem::GitBranch => StatusLineItem::GitBranch,
StatusSurfacePreviewItem::PullRequestNumber => StatusLineItem::PullRequestNumber,
StatusSurfacePreviewItem::BranchChanges => StatusLineItem::BranchChanges,
StatusSurfacePreviewItem::Permissions => StatusLineItem::Permissions,
StatusSurfacePreviewItem::ApprovalMode => StatusLineItem::ApprovalMode,
StatusSurfacePreviewItem::ContextRemaining => StatusLineItem::ContextRemaining,
StatusSurfacePreviewItem::ContextUsed => StatusLineItem::ContextUsed,
StatusSurfacePreviewItem::FiveHourLimit => StatusLineItem::FiveHourLimit,
@@ -890,6 +899,44 @@ impl ChatWidget {
}
}
fn permissions_display(config: &Config) -> String {
let active_permission_profile = config.permissions.active_permission_profile();
if let Some(active_permission_profile) = active_permission_profile.as_ref()
&& !active_permission_profile.id.starts_with(':')
{
return active_permission_profile.id.clone();
}
let permission_profile = config.permissions.permission_profile();
let summary = summarize_permission_profile(&permission_profile, config.cwd.as_path());
if let Some(details) = summary.strip_prefix("read-only")
&& !details.contains("(network access enabled)")
{
return "Read Only".to_string();
}
if let Some(details) = summary.strip_prefix("workspace-write")
&& !details.contains("(network access enabled)")
{
return "Workspace".to_string();
}
if permission_profile == PermissionProfile::Disabled {
return "Full Access".to_string();
}
"Custom permissions".to_string()
}
fn approval_mode_display(config: &Config) -> String {
let approval_policy = AskForApproval::from(config.permissions.approval_policy.value());
if approval_policy == AskForApproval::OnRequest
&& config.approvals_reviewer == ApprovalsReviewer::AutoReview
{
"auto-review".to_string()
} else {
config.permissions.approval_policy.value().to_string()
}
}
fn parse_items_with_invalids<T>(ids: impl IntoIterator<Item = String>) -> (Vec<T>, Vec<String>)
where
T: std::str::FromStr,
@@ -112,6 +112,8 @@ async fn status_surface_preview_lines_hardcoded_only_snapshot() {
StatusLineItem::ProjectRoot,
StatusLineItem::GitBranch,
StatusLineItem::ThreadTitle,
StatusLineItem::Permissions,
StatusLineItem::ApprovalMode,
],
&[
TerminalTitleItem::Thread,