diff --git a/codex-rs/common/docs.md b/codex-rs/common/docs.md index bd2c5c61c..c18c71bfa 100644 --- a/codex-rs/common/docs.md +++ b/codex-rs/common/docs.md @@ -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 @@ -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:** diff --git a/codex-rs/common/src/approval_presets.rs b/codex-rs/common/src/approval_presets.rs index 1b673d1d9..9b1279df2 100644 --- a/codex-rs/common/src/approval_presets.rs +++ b/codex-rs/common/src/approval_presets.rs @@ -44,3 +44,88 @@ pub fn builtin_approval_presets() -> Vec { }, ] } + +/// 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 { + 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); + } +} diff --git a/codex-rs/tui-pty-e2e/src/lib.rs b/codex-rs/tui-pty-e2e/src/lib.rs index 8e7f93ba6..122d90244 100644 --- a/codex-rs/tui-pty-e2e/src/lib.rs +++ b/codex-rs/tui-pty-e2e/src/lib.rs @@ -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" diff --git a/codex-rs/tui-pty-e2e/tests/snapshots/acp_tool_calls__acp_explored_after_assistant_message.snap b/codex-rs/tui-pty-e2e/tests/snapshots/acp_tool_calls__acp_explored_after_assistant_message.snap index b247e413c..1c7775f93 100644 --- a/codex-rs/tui-pty-e2e/tests/snapshots/acp_tool_calls__acp_explored_after_assistant_message.snap +++ b/codex-rs/tui-pty-e2e/tests/snapshots/acp_tool_calls__acp_explored_after_assistant_message.snap @@ -24,4 +24,4 @@ expression: normalize_for_input_snapshot(contents) › [DEFAULT_PROMPT] - ⎇ master · ? for shortcuts + ⎇ master · Approval Mode: Agent · ? for shortcuts diff --git a/codex-rs/tui-pty-e2e/tests/snapshots/acp_tool_calls__acp_multi_call_exploring.snap b/codex-rs/tui-pty-e2e/tests/snapshots/acp_tool_calls__acp_multi_call_exploring.snap index e10389b53..c46bc7e22 100644 --- a/codex-rs/tui-pty-e2e/tests/snapshots/acp_tool_calls__acp_multi_call_exploring.snap +++ b/codex-rs/tui-pty-e2e/tests/snapshots/acp_tool_calls__acp_multi_call_exploring.snap @@ -21,4 +21,4 @@ expression: normalize_for_input_snapshot(contents) › [DEFAULT_PROMPT] - ⎇ master · ? for shortcuts + ⎇ master · Approval Mode: Agent · ? for shortcuts diff --git a/codex-rs/tui-pty-e2e/tests/snapshots/acp_tool_calls__acp_tool_call_echo.snap b/codex-rs/tui-pty-e2e/tests/snapshots/acp_tool_calls__acp_tool_call_echo.snap index cb20ff38e..81cbd3795 100644 --- a/codex-rs/tui-pty-e2e/tests/snapshots/acp_tool_calls__acp_tool_call_echo.snap +++ b/codex-rs/tui-pty-e2e/tests/snapshots/acp_tool_calls__acp_tool_call_echo.snap @@ -17,4 +17,4 @@ expression: normalize_for_input_snapshot(contents) › [DEFAULT_PROMPT] - ⎇ master · ? for shortcuts + ⎇ master · Approval Mode: Agent · ? for shortcuts diff --git a/codex-rs/tui-pty-e2e/tests/snapshots/acp_tool_calls__acp_tool_call_no_duplicates.snap b/codex-rs/tui-pty-e2e/tests/snapshots/acp_tool_calls__acp_tool_call_no_duplicates.snap index 2c95eb82e..6ac75dbd9 100644 --- a/codex-rs/tui-pty-e2e/tests/snapshots/acp_tool_calls__acp_tool_call_no_duplicates.snap +++ b/codex-rs/tui-pty-e2e/tests/snapshots/acp_tool_calls__acp_tool_call_no_duplicates.snap @@ -17,4 +17,4 @@ expression: normalize_for_input_snapshot(contents) › [DEFAULT_PROMPT] - ⎇ master · ? for shortcuts + ⎇ master · Approval Mode: Agent · ? for shortcuts diff --git a/codex-rs/tui-pty-e2e/tests/snapshots/acp_tool_calls__acp_tool_call_read.snap b/codex-rs/tui-pty-e2e/tests/snapshots/acp_tool_calls__acp_tool_call_read.snap index 5c91d5789..186432499 100644 --- a/codex-rs/tui-pty-e2e/tests/snapshots/acp_tool_calls__acp_tool_call_read.snap +++ b/codex-rs/tui-pty-e2e/tests/snapshots/acp_tool_calls__acp_tool_call_read.snap @@ -17,4 +17,4 @@ expression: normalize_for_input_snapshot(session.screen_contents()) › [DEFAULT_PROMPT] - ⎇ master · ? for shortcuts + ⎇ master · Approval Mode: Agent · ? for shortcuts diff --git a/codex-rs/tui-pty-e2e/tests/snapshots/input_handling__history_navigation_multiple_messages.snap b/codex-rs/tui-pty-e2e/tests/snapshots/input_handling__history_navigation_multiple_messages.snap index 0476f2bd9..8a61791d2 100644 --- a/codex-rs/tui-pty-e2e/tests/snapshots/input_handling__history_navigation_multiple_messages.snap +++ b/codex-rs/tui-pty-e2e/tests/snapshots/input_handling__history_navigation_multiple_messages.snap @@ -16,4 +16,4 @@ expression: normalize_for_input_snapshot(session.screen_contents()) › first message - ⎇ master + ⎇ master · Approval Mode: Agent diff --git a/codex-rs/tui-pty-e2e/tests/snapshots/input_handling__history_navigation_up_down.snap b/codex-rs/tui-pty-e2e/tests/snapshots/input_handling__history_navigation_up_down.snap index c3944a841..eb16d2968 100644 --- a/codex-rs/tui-pty-e2e/tests/snapshots/input_handling__history_navigation_up_down.snap +++ b/codex-rs/tui-pty-e2e/tests/snapshots/input_handling__history_navigation_up_down.snap @@ -10,4 +10,4 @@ expression: normalize_for_input_snapshot(session.screen_contents()) › [DEFAULT_PROMPT] - ⎇ master · ? for shortcuts + ⎇ master · Approval Mode: Agent · ? for shortcuts diff --git a/codex-rs/tui-pty-e2e/tests/snapshots/input_handling__typing_and_backspace.snap b/codex-rs/tui-pty-e2e/tests/snapshots/input_handling__typing_and_backspace.snap index e9764a3af..ad4b1942a 100644 --- a/codex-rs/tui-pty-e2e/tests/snapshots/input_handling__typing_and_backspace.snap +++ b/codex-rs/tui-pty-e2e/tests/snapshots/input_handling__typing_and_backspace.snap @@ -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 diff --git a/codex-rs/tui-pty-e2e/tests/snapshots/nori_footer__full_footer.snap b/codex-rs/tui-pty-e2e/tests/snapshots/nori_footer__full_footer.snap index f15588d26..f61991fe4 100644 --- a/codex-rs/tui-pty-e2e/tests/snapshots/nori_footer__full_footer.snap +++ b/codex-rs/tui-pty-e2e/tests/snapshots/nori_footer__full_footer.snap @@ -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 diff --git a/codex-rs/tui-pty-e2e/tests/snapshots/prompt_flow__custom_response.snap b/codex-rs/tui-pty-e2e/tests/snapshots/prompt_flow__custom_response.snap index d6b9b90f6..cc0bc5a26 100644 --- a/codex-rs/tui-pty-e2e/tests/snapshots/prompt_flow__custom_response.snap +++ b/codex-rs/tui-pty-e2e/tests/snapshots/prompt_flow__custom_response.snap @@ -10,4 +10,4 @@ expression: normalize_for_input_snapshot(session.screen_contents()) › [DEFAULT_PROMPT] - ⎇ master · ? for shortcuts + ⎇ master · Approval Mode: Agent · ? for shortcuts diff --git a/codex-rs/tui-pty-e2e/tests/snapshots/prompt_flow__multiline_input.snap b/codex-rs/tui-pty-e2e/tests/snapshots/prompt_flow__multiline_input.snap index 75fafafc0..a08c6c881 100644 --- a/codex-rs/tui-pty-e2e/tests/snapshots/prompt_flow__multiline_input.snap +++ b/codex-rs/tui-pty-e2e/tests/snapshots/prompt_flow__multiline_input.snap @@ -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 diff --git a/codex-rs/tui-pty-e2e/tests/snapshots/prompt_flow__prompt_submitted.snap b/codex-rs/tui-pty-e2e/tests/snapshots/prompt_flow__prompt_submitted.snap index e8c782138..f72e17de0 100644 --- a/codex-rs/tui-pty-e2e/tests/snapshots/prompt_flow__prompt_submitted.snap +++ b/codex-rs/tui-pty-e2e/tests/snapshots/prompt_flow__prompt_submitted.snap @@ -10,4 +10,4 @@ expression: normalize_for_input_snapshot(session.screen_contents()) › [DEFAULT_PROMPT] - ? for shortcuts + Approval Mode: Agent · ? for shortcuts diff --git a/codex-rs/tui-pty-e2e/tests/snapshots/streaming__submit_input.snap b/codex-rs/tui-pty-e2e/tests/snapshots/streaming__submit_input.snap index 38de2ebeb..276f475db 100644 --- a/codex-rs/tui-pty-e2e/tests/snapshots/streaming__submit_input.snap +++ b/codex-rs/tui-pty-e2e/tests/snapshots/streaming__submit_input.snap @@ -10,4 +10,4 @@ expression: normalize_for_input_snapshot(session.screen_contents()) › [DEFAULT_PROMPT] - ⎇ master · ? for shortcuts + ⎇ master · Approval Mode: Agent · ? for shortcuts diff --git a/codex-rs/tui/docs.md b/codex-rs/tui/docs.md index e8cc8bcdf..5a62b6d42 100644 --- a/codex-rs/tui/docs.md +++ b/codex-rs/tui/docs.md @@ -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: diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index 09555bf8f..28ba6d2d6 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -114,6 +114,8 @@ pub(crate) struct ChatComposer { footer_hint_override: Option>, context_window_percent: Option, system_info: Option, + /// The approval mode label to display in the footer (e.g., "Read Only", "Agent", "Full Access"). + approval_mode_label: Option, } /// Popup state – at most one can be visible at any time. @@ -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); @@ -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, @@ -1552,6 +1556,10 @@ impl ChatComposer { self.system_info = Some(info); } + pub(crate) fn set_approval_mode_label(&mut self, label: Option) { + self.approval_mode_label = label; + } + pub(crate) fn set_esc_backtrack_hint(&mut self, show: bool) { self.esc_backtrack_hint = show; if show { diff --git a/codex-rs/tui/src/bottom_pane/footer.rs b/codex-rs/tui/src/bottom_pane/footer.rs index 5717dba7a..8dfca0f9f 100644 --- a/codex-rs/tui/src/bottom_pane/footer.rs +++ b/codex-rs/tui/src/bottom_pane/footer.rs @@ -19,6 +19,8 @@ pub(crate) struct FooterProps { pub(crate) is_task_running: bool, pub(crate) _context_window_percent: Option, pub(crate) git_branch: Option, + /// The approval mode label to display (e.g., "Read Only", "Agent", "Full Access"). + pub(crate) approval_mode_label: Option, pub(crate) nori_profile: Option, pub(crate) nori_version: Option, pub(crate) git_lines_added: Option, @@ -254,6 +256,13 @@ fn build_footer_line(props: &FooterProps) -> Line<'static> { spans.push(Span::from(" · ").dim()); } + // Add approval mode if available: "Approval Mode: Agent" (magenta) + if let Some(label) = &props.approval_mode_label { + spans.push(Span::from("Approval Mode: ").magenta()); + spans.push(Span::from(label.clone()).magenta()); + spans.push(Span::from(" · ").dim()); + } + // Add nori profile if available: "Skillset: name" (cyan) if let Some(profile) = &props.nori_profile { spans.push(Span::from("Skillset: ").cyan()); @@ -459,6 +468,7 @@ mod tests { is_task_running: false, _context_window_percent: None, git_branch: None, + approval_mode_label: None, nori_profile: None, nori_version: None, git_lines_added: None, @@ -476,6 +486,7 @@ mod tests { is_task_running: false, _context_window_percent: None, git_branch: None, + approval_mode_label: None, nori_profile: None, nori_version: None, git_lines_added: None, @@ -493,6 +504,7 @@ mod tests { is_task_running: false, _context_window_percent: None, git_branch: None, + approval_mode_label: None, nori_profile: None, nori_version: None, git_lines_added: None, @@ -510,6 +522,7 @@ mod tests { is_task_running: true, _context_window_percent: None, git_branch: None, + approval_mode_label: None, nori_profile: None, nori_version: None, git_lines_added: None, @@ -527,6 +540,7 @@ mod tests { is_task_running: false, _context_window_percent: None, git_branch: None, + approval_mode_label: None, nori_profile: None, nori_version: None, git_lines_added: None, @@ -544,6 +558,7 @@ mod tests { is_task_running: false, _context_window_percent: None, git_branch: None, + approval_mode_label: None, nori_profile: None, nori_version: None, git_lines_added: None, @@ -561,6 +576,7 @@ mod tests { is_task_running: true, _context_window_percent: Some(72), git_branch: None, + approval_mode_label: None, nori_profile: None, nori_version: None, git_lines_added: None, @@ -581,6 +597,7 @@ mod tests { is_task_running: false, _context_window_percent: Some(72), git_branch: Some("feature/test".to_string()), + approval_mode_label: None, nori_profile: Some("clifford".to_string()), nori_version: Some("19.1.1".to_string()), git_lines_added: Some(10), @@ -601,6 +618,7 @@ mod tests { is_task_running: false, _context_window_percent: Some(100), git_branch: Some("main".to_string()), + approval_mode_label: None, nori_profile: None, nori_version: None, git_lines_added: Some(5), @@ -621,6 +639,7 @@ mod tests { is_task_running: false, _context_window_percent: Some(85), git_branch: None, + approval_mode_label: None, nori_profile: None, nori_version: None, git_lines_added: None, @@ -642,6 +661,7 @@ mod tests { is_task_running: false, _context_window_percent: Some(72), git_branch: Some("feature/worktree-branch".to_string()), + approval_mode_label: None, nori_profile: Some("clifford".to_string()), nori_version: Some("19.1.1".to_string()), git_lines_added: Some(5), @@ -650,4 +670,70 @@ mod tests { }, ); } + + #[test] + fn footer_with_approval_mode() { + // Test that approval mode label is displayed in the footer + snapshot_footer( + "footer_with_approval_mode_agent", + FooterProps { + mode: FooterMode::ShortcutSummary, + esc_backtrack_hint: false, + use_shift_enter_hint: false, + is_task_running: false, + _context_window_percent: Some(72), + git_branch: Some("feature/test".to_string()), + approval_mode_label: Some("Agent".to_string()), + nori_profile: Some("clifford".to_string()), + nori_version: Some("19.1.1".to_string()), + git_lines_added: Some(10), + git_lines_removed: Some(3), + is_worktree: false, + }, + ); + } + + #[test] + fn footer_with_approval_mode_read_only() { + // Test Read Only mode display + snapshot_footer( + "footer_with_approval_mode_read_only", + FooterProps { + mode: FooterMode::ShortcutSummary, + esc_backtrack_hint: false, + use_shift_enter_hint: false, + is_task_running: false, + _context_window_percent: None, + git_branch: Some("main".to_string()), + approval_mode_label: Some("Read Only".to_string()), + nori_profile: None, + nori_version: None, + git_lines_added: None, + git_lines_removed: None, + is_worktree: false, + }, + ); + } + + #[test] + fn footer_with_approval_mode_full_access() { + // Test Full Access mode display + snapshot_footer( + "footer_with_approval_mode_full_access", + FooterProps { + mode: FooterMode::ShortcutSummary, + esc_backtrack_hint: false, + use_shift_enter_hint: false, + is_task_running: false, + _context_window_percent: None, + git_branch: Some("main".to_string()), + approval_mode_label: Some("Full Access".to_string()), + nori_profile: None, + nori_version: None, + git_lines_added: None, + git_lines_removed: None, + is_worktree: false, + }, + ); + } } diff --git a/codex-rs/tui/src/bottom_pane/mod.rs b/codex-rs/tui/src/bottom_pane/mod.rs index e157db352..b27464b2b 100644 --- a/codex-rs/tui/src/bottom_pane/mod.rs +++ b/codex-rs/tui/src/bottom_pane/mod.rs @@ -407,6 +407,12 @@ impl BottomPane { self.request_redraw(); } + /// Update the approval mode label displayed in the footer. + pub(crate) fn set_approval_mode_label(&mut self, label: Option) { + self.composer.set_approval_mode_label(label); + self.request_redraw(); + } + pub(crate) fn composer_is_empty(&self) -> bool { self.composer.is_empty() } diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_with_approval_mode_agent.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_with_approval_mode_agent.snap new file mode 100644 index 000000000..1e5f309d3 --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_with_approval_mode_agent.snap @@ -0,0 +1,6 @@ +--- +source: tui/src/bottom_pane/footer.rs +assertion_line: 457 +expression: terminal.backend() +--- +" ⎇ feature/test · Approval Mode: Agent · Skillset: clifford · Skillsets v19.1.1" diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_with_approval_mode_full_access.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_with_approval_mode_full_access.snap new file mode 100644 index 000000000..06df688ce --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_with_approval_mode_full_access.snap @@ -0,0 +1,6 @@ +--- +source: tui/src/bottom_pane/footer.rs +assertion_line: 457 +expression: terminal.backend() +--- +" ⎇ main · Approval Mode: Full Access · ? for shortcuts " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_with_approval_mode_read_only.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_with_approval_mode_read_only.snap new file mode 100644 index 000000000..7ce668965 --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_with_approval_mode_read_only.snap @@ -0,0 +1,6 @@ +--- +source: tui/src/bottom_pane/footer.rs +assertion_line: 457 +expression: terminal.backend() +--- +" ⎇ main · Approval Mode: Read Only · ? for shortcuts " diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 2a21228d2..660ef1bcf 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -137,6 +137,7 @@ use std::path::Path; use chrono::Local; use codex_common::approval_presets::ApprovalPreset; +use codex_common::approval_presets::approval_mode_label; use codex_common::approval_presets::builtin_approval_presets; use codex_common::model_presets::ModelPreset; use codex_common::model_presets::builtin_model_presets; @@ -473,6 +474,9 @@ impl ChatWidget { // Clear the "Connecting to [Agent]" status indicator shown during agent startup self.bottom_pane.hide_status_indicator(); + // Update footer with current approval mode + self.update_approval_mode_label(); + self.bottom_pane .set_history_metadata(event.history_log_id, event.history_entry_count); self.conversation_id = Some(event.session_id); @@ -3290,6 +3294,7 @@ impl ChatWidget { /// Set the approval policy in the widget's config copy. pub(crate) fn set_approval_policy(&mut self, policy: AskForApproval) { self.config.approval_policy = policy; + self.update_approval_mode_label(); } /// Set the sandbox policy in the widget's config copy. @@ -3304,6 +3309,14 @@ impl ChatWidget { if should_clear_downgrade { self.config.forced_auto_mode_downgraded_on_windows = false; } + + self.update_approval_mode_label(); + } + + /// Update the approval mode label displayed in the footer based on current config. + fn update_approval_mode_label(&mut self) { + let label = approval_mode_label(self.config.approval_policy, &self.config.sandbox_policy); + self.bottom_pane.set_approval_mode_label(label); } pub(crate) fn set_full_access_warning_acknowledged(&mut self, acknowledged: bool) {