mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
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:
committed by
GitHub
Unverified
parent
f86d95a242
commit
e6312d44f0
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user