Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion codex-rs/common/docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Common is a utility dependency for TUI, exec, and CLI:
- **Config display**: `create_config_summary_entries()` for status displays
- **Model selection**: `model_presets` for available models
- **OSS support**: `oss` module for Ollama/LM Studio integration
- **Approval mode display**: `approval_mode_label()` for TUI status line

### Core Implementation

Expand Down Expand Up @@ -58,7 +59,12 @@ pub struct CliConfigOverrides {

**Approval Presets:**

`approval_presets` provides named combinations like "full-auto" that set both approval policy and sandbox mode together.
`approval_presets` provides named combinations like "full-auto" that set both approval policy and sandbox mode together. The module includes:

- `builtin_approval_presets()`: Returns the list of preset combinations (Read Only, Agent, Full Access)
- `approval_mode_label()`: Maps current approval policy and sandbox policy back to a display label for status line display

The `approval_mode_label()` function matches current config against builtin presets using fuzzy sandbox matching (ignores `writable_roots` differences for `WorkspaceWrite` policies). Returns `None` if no preset matches.

**OSS Provider Utilities:**

Expand Down
85 changes: 85 additions & 0 deletions codex-rs/common/src/approval_presets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,88 @@ pub fn builtin_approval_presets() -> Vec<ApprovalPreset> {
},
]
}

/// Returns the display label for the current approval mode based on matching
/// the given approval policy and sandbox policy against the builtin presets.
///
/// Returns a simplified label for display in the status line:
/// - "Read Only" for read-only mode
/// - "Agent" for agent mode with workspace write access
/// - "Full Access" for full access mode (simplified from "Agent (full access)")
///
/// Returns `None` if no preset matches the current configuration.
pub fn approval_mode_label(approval: AskForApproval, sandbox: &SandboxPolicy) -> Option<String> {
builtin_approval_presets()
.into_iter()
.find(|preset| preset.approval == approval && sandbox_matches(&preset.sandbox, sandbox))
.map(|preset| {
// Simplify "Agent (full access)" to "Full Access"
if preset.id == "full-access" {
"Full Access".to_string()
} else {
preset.label.to_string()
}
})
}

/// Check if sandbox policies match, ignoring differences in writable_roots
/// for WorkspaceWrite policies.
fn sandbox_matches(preset_sandbox: &SandboxPolicy, current_sandbox: &SandboxPolicy) -> bool {
matches!(
(preset_sandbox, current_sandbox),
(SandboxPolicy::ReadOnly, SandboxPolicy::ReadOnly)
| (
SandboxPolicy::DangerFullAccess,
SandboxPolicy::DangerFullAccess
)
| (
SandboxPolicy::WorkspaceWrite { .. },
SandboxPolicy::WorkspaceWrite { .. }
)
)
}

#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;

#[test]
fn approval_mode_label_returns_read_only_for_read_only_preset() {
let label = approval_mode_label(AskForApproval::OnRequest, &SandboxPolicy::ReadOnly);
assert_eq!(label, Some("Read Only".to_string()));
}

#[test]
fn approval_mode_label_returns_agent_for_workspace_write_preset() {
let sandbox = SandboxPolicy::new_workspace_write_policy();
let label = approval_mode_label(AskForApproval::OnRequest, &sandbox);
assert_eq!(label, Some("Agent".to_string()));
}

#[test]
fn approval_mode_label_returns_full_access_for_danger_full_access_preset() {
let label = approval_mode_label(AskForApproval::Never, &SandboxPolicy::DangerFullAccess);
assert_eq!(label, Some("Full Access".to_string()));
}

#[test]
fn approval_mode_label_matches_workspace_write_with_extra_roots() {
// When user has extra writable roots, it should still match "Agent"
let sandbox = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![PathBuf::from("/tmp/extra")],
network_access: false,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
};
let label = approval_mode_label(AskForApproval::OnRequest, &sandbox);
assert_eq!(label, Some("Agent".to_string()));
}

#[test]
fn approval_mode_label_returns_none_for_unmatched_config() {
// A config that doesn't match any preset (e.g., Never approval with ReadOnly sandbox)
let label = approval_mode_label(AskForApproval::Never, &SandboxPolicy::ReadOnly);
assert_eq!(label, None);
}
}
16 changes: 15 additions & 1 deletion codex-rs/tui-pty-e2e/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -264,11 +264,25 @@ impl TuiSession {
let config_path = codex_home.join("config.toml");
let config_content = config.config_toml.clone().unwrap_or_else(|| {
// Generate default config with model, trusted project path,
// and mock_provider that doesn't require OpenAI auth
// and mock_provider that doesn't require OpenAI auth.
//
// IMPORTANT: Canonicalize the cwd path for the projects section.
// On macOS, /tmp is a symlink to /private/tmp, so paths like
// /var/folders/... can become /private/var/folders/... after
// canonicalization. The config loader canonicalizes CODEX_HOME
// and resolved_cwd, so we must use the same canonicalized path
// here to ensure the project trust level is properly matched.
let cwd_path = config
.cwd
.as_ref()
.and_then(|p| std::fs::canonicalize(p).ok())
.map(|p| p.to_string_lossy().into_owned())
.or_else(|| {
config
.cwd
.as_ref()
.map(|p| p.to_string_lossy().into_owned())
})
.unwrap_or_else(|| codex_home.to_string_lossy().into_owned());
let acp_section = if config.allow_http_fallback {
"\n[acp]\nallow_http_fallback = true\n"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,4 @@ expression: normalize_for_input_snapshot(contents)

› [DEFAULT_PROMPT]

⎇ master · ? for shortcuts
⎇ master · Approval Mode: Agent · ? for shortcuts
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,4 @@ expression: normalize_for_input_snapshot(contents)

› [DEFAULT_PROMPT]

⎇ master · ? for shortcuts
⎇ master · Approval Mode: Agent · ? for shortcuts
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,4 @@ expression: normalize_for_input_snapshot(contents)

› [DEFAULT_PROMPT]

⎇ master · ? for shortcuts
⎇ master · Approval Mode: Agent · ? for shortcuts
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,4 @@ expression: normalize_for_input_snapshot(contents)

› [DEFAULT_PROMPT]

⎇ master · ? for shortcuts
⎇ master · Approval Mode: Agent · ? for shortcuts
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,4 @@ expression: normalize_for_input_snapshot(session.screen_contents())

› [DEFAULT_PROMPT]

⎇ master · ? for shortcuts
⎇ master · Approval Mode: Agent · ? for shortcuts
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@ expression: normalize_for_input_snapshot(session.screen_contents())

› first message

⎇ master
⎇ master · Approval Mode: Agent
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ expression: normalize_for_input_snapshot(session.screen_contents())

› [DEFAULT_PROMPT]

⎇ master · ? for shortcuts
⎇ master · Approval Mode: Agent · ? for shortcuts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
---
source: tui-pty-e2e/tests/input_handling.rs
assertion_line: 54
expression: normalize_for_input_snapshot(session.screen_contents())
---
› Hel

Approval Mode: Agent
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ expression: normalize_for_input_snapshot(contents)
---
› [DEFAULT_PROMPT]

⎇ master · Skillsets v19.1.1 · ? for shortcuts
⎇ master · Approval Mode: Agent · Skillsets v19.1.1 · ? for shortcuts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ expression: normalize_for_input_snapshot(session.screen_contents())

› [DEFAULT_PROMPT]

⎇ master · ? for shortcuts
⎇ master · Approval Mode: Agent · ? for shortcuts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
---
source: tui-pty-e2e/tests/prompt_flow.rs
assertion_line: 82
expression: normalize_for_input_snapshot(session.screen_contents())
---
› Line 1
Line 2
Line 3

Approval Mode: Agent
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ expression: normalize_for_input_snapshot(session.screen_contents())

› [DEFAULT_PROMPT]

? for shortcuts
Approval Mode: Agent · ? for shortcuts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ expression: normalize_for_input_snapshot(session.screen_contents())

› [DEFAULT_PROMPT]

⎇ master · ? for shortcuts
⎇ master · Approval Mode: Agent · ? for shortcuts
19 changes: 19 additions & 0 deletions codex-rs/tui/docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,25 @@ The footer uses visual differentiation:

For E2E testing, `NORI_SYNC_SYSTEM_INFO=1` env var enables synchronous collection in debug builds. This is set automatically by `@/codex-rs/tui-pty-e2e` to ensure footer data appears immediately in tests.

*Approval Mode Display:*

The footer displays the current approval mode (e.g., "Approval Mode: Agent") to provide users visibility into their permission settings without opening the `/approvals` popup.

Data flow:
1. `ChatWidget` computes the label using `approval_mode_label()` from `@/codex-rs/common/src/approval_presets.rs`
2. The function matches current `approval_policy` and `sandbox_policy` against builtin presets
3. Label is passed through `BottomPane` → `ChatComposer` → `FooterProps` → `build_footer_line()`
4. Footer renders it in magenta styling, positioned after git branch and before skillset

The label updates dynamically when the user changes approval settings via `/approvals`:
- `ChatWidget::set_approval_policy()` and `set_sandbox_policy()` both call `update_approval_mode_label()`
- Initial setup occurs in `on_session_configured()` when the session starts

Display values:
- "Read Only" - Read-only sandbox, approval on every action
- "Agent" - Workspace write access, approval on every action
- "Full Access" - Full disk access, no approval required

**Configuration Flow:**

TUI respects config overrides from:
Expand Down
8 changes: 8 additions & 0 deletions codex-rs/tui/src/bottom_pane/chat_composer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@ pub(crate) struct ChatComposer {
footer_hint_override: Option<Vec<(String, String)>>,
context_window_percent: Option<i64>,
system_info: Option<crate::system_info::SystemInfo>,
/// The approval mode label to display in the footer (e.g., "Read Only", "Agent", "Full Access").
approval_mode_label: Option<String>,
}

/// Popup state – at most one can be visible at any time.
Expand Down Expand Up @@ -158,6 +160,7 @@ impl ChatComposer {
footer_hint_override: None,
context_window_percent: None,
system_info: None,
approval_mode_label: None,
};
// Apply configuration via the setter to keep side-effects centralized.
this.set_disable_paste_burst(disable_paste_burst);
Expand Down Expand Up @@ -1403,6 +1406,7 @@ impl ChatComposer {
is_task_running: self.is_task_running,
_context_window_percent: self.context_window_percent,
git_branch,
approval_mode_label: self.approval_mode_label.clone(),
nori_profile,
nori_version,
git_lines_added,
Expand Down Expand Up @@ -1552,6 +1556,10 @@ impl ChatComposer {
self.system_info = Some(info);
}

pub(crate) fn set_approval_mode_label(&mut self, label: Option<String>) {
self.approval_mode_label = label;
}

pub(crate) fn set_esc_backtrack_hint(&mut self, show: bool) {
self.esc_backtrack_hint = show;
if show {
Expand Down
Loading
Loading