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
2026-05-08 08:03:11 -07:00
committed by GitHub
Unverified
parent f86d95a242
commit e6312d44f0
7 changed files with 77 additions and 2 deletions
@@ -12,6 +12,8 @@
//! - Model information (name, reasoning level)
//! - Directory paths (current dir, project root)
//! - Git information (branch name)
//! - Permissions profile
//! - Approval mode
//! - Context usage (remaining %, used %, window size)
//! - Usage limits (5-hour, weekly)
//! - Session info (thread title, ID, tokens used)
@@ -81,6 +83,13 @@ pub(crate) enum StatusLineItem {
#[strum(to_string = "run-state", serialize = "status")]
Status,
/// Active permission profile or sandbox summary.
Permissions,
/// Active command approval mode.
#[strum(to_string = "approval-mode", serialize = "approval")]
ApprovalMode,
/// Percentage of context window remaining.
ContextRemaining,
@@ -143,6 +152,8 @@ impl StatusLineItem {
"Committed branch changes against the default branch (omitted when unavailable)"
}
StatusLineItem::Status => "Compact session run-state text (Ready, Working, Thinking)",
StatusLineItem::Permissions => "Active permission profile or sandbox mode",
StatusLineItem::ApprovalMode => "Active command approval mode",
StatusLineItem::ContextRemaining => {
"Percentage of context window remaining (omitted when unknown)"
}
@@ -184,6 +195,8 @@ impl StatusLineItem {
StatusLineItem::PullRequestNumber => StatusSurfacePreviewItem::PullRequestNumber,
StatusLineItem::BranchChanges => StatusSurfacePreviewItem::BranchChanges,
StatusLineItem::Status => StatusSurfacePreviewItem::Status,
StatusLineItem::Permissions => StatusSurfacePreviewItem::Permissions,
StatusLineItem::ApprovalMode => StatusSurfacePreviewItem::ApprovalMode,
StatusLineItem::ContextRemaining => StatusSurfacePreviewItem::ContextRemaining,
StatusLineItem::ContextUsed => StatusSurfacePreviewItem::ContextUsed,
StatusLineItem::FiveHourLimit => StatusSurfacePreviewItem::FiveHourLimit,
@@ -45,6 +45,8 @@ impl StatusLineAccent {
StatusLineItem::FiveHourLimit | StatusLineItem::WeeklyLimit => Self::Limit,
StatusLineItem::CodexVersion | StatusLineItem::SessionId => Self::Metadata,
StatusLineItem::FastMode | StatusLineItem::RawOutput => Self::Mode,
StatusLineItem::Permissions => Self::Mode,
StatusLineItem::ApprovalMode => Self::Mode,
StatusLineItem::ThreadTitle => Self::Thread,
StatusLineItem::TaskProgress => Self::Progress,
}
@@ -16,6 +16,8 @@ pub(crate) enum StatusSurfacePreviewItem {
GitBranch,
PullRequestNumber,
BranchChanges,
Permissions,
ApprovalMode,
ContextRemaining,
ContextUsed,
FiveHourLimit,
@@ -45,6 +47,8 @@ impl StatusSurfacePreviewItem {
StatusSurfacePreviewItem::GitBranch => "feat/awesome-feature",
StatusSurfacePreviewItem::PullRequestNumber => "PR #123",
StatusSurfacePreviewItem::BranchChanges => "+12 -3",
StatusSurfacePreviewItem::Permissions => "Workspace",
StatusSurfacePreviewItem::ApprovalMode => "on-request",
StatusSurfacePreviewItem::ContextRemaining => "Context 0% left",
StatusSurfacePreviewItem::ContextUsed => "Context 0% used",
StatusSurfacePreviewItem::FiveHourLimit => "5h 0%",
@@ -74,6 +78,8 @@ impl StatusSurfacePreviewItem {
Self::GitBranch,
Self::PullRequestNumber,
Self::BranchChanges,
Self::Permissions,
Self::ApprovalMode,
Self::ContextRemaining,
Self::ContextUsed,
Self::FiveHourLimit,
+6 -1
View File
@@ -9141,6 +9141,8 @@ impl ChatWidget {
.set(policy.to_core())
{
tracing::warn!(%err, "failed to set approval_policy on chat config");
} else {
self.refresh_status_surfaces();
}
}
@@ -9150,7 +9152,9 @@ impl ChatWidget {
&mut self,
profile: PermissionProfile,
) -> ConstraintResult<()> {
self.config.permissions.set_permission_profile(profile)
self.config.permissions.set_permission_profile(profile)?;
self.refresh_status_surfaces();
Ok(())
}
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
@@ -9231,6 +9235,7 @@ impl ChatWidget {
pub(crate) fn set_approvals_reviewer(&mut self, policy: ApprovalsReviewer) {
self.config.approvals_reviewer = policy;
self.refresh_status_surfaces();
}
pub(crate) fn set_full_access_warning_acknowledged(&mut self, acknowledged: bool) {
@@ -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,