From 66c2476e1383d3836c38fbf0f8c677c3a7f944e2 Mon Sep 17 00:00:00 2001 From: Amol Kapoor Date: Sun, 18 Jan 2026 20:58:41 -0500 Subject: [PATCH 1/3] feat(tui): Display approval mode in status line footer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add the current approval mode to the footer status line beneath the TUI input area. Shows "Approval Mode: Read Only", "Approval Mode: Agent", or "Approval Mode: Full Access" in magenta, positioned after the git branch and before the skillset. The label updates dynamically when the user changes approval settings via the /approvals command. - Add approval_mode_label helper function to approval_presets.rs - Add approval_mode_label field and rendering to footer.rs - Wire through ChatComposer, BottomPane, and ChatWidget - Add snapshot tests for all three approval modes - Update documentation in common/docs.md and tui/docs.md 🤖 Generated with [Nori](https://nori.ai) Co-Authored-By: Nori --- codex-rs/common/docs.md | 8 +- codex-rs/common/src/approval_presets.rs | 85 ++++++++++++++++++ codex-rs/tui/docs.md | 19 ++++ codex-rs/tui/src/bottom_pane/chat_composer.rs | 8 ++ codex-rs/tui/src/bottom_pane/footer.rs | 86 +++++++++++++++++++ codex-rs/tui/src/bottom_pane/mod.rs | 6 ++ ...ests__footer_with_approval_mode_agent.snap | 6 ++ ...footer_with_approval_mode_full_access.snap | 6 ++ ...__footer_with_approval_mode_read_only.snap | 6 ++ codex-rs/tui/src/chatwidget.rs | 13 +++ 10 files changed, 242 insertions(+), 1 deletion(-) create mode 100644 codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_with_approval_mode_agent.snap create mode 100644 codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_with_approval_mode_full_access.snap create mode 100644 codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_with_approval_mode_read_only.snap 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/docs.md b/codex-rs/tui/docs.md index bc90b7dcf..2d0c807d6 100644 --- a/codex-rs/tui/docs.md +++ b/codex-rs/tui/docs.md @@ -583,6 +583,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 e03b594f9..d865d3cbb 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); @@ -3275,6 +3279,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. @@ -3289,6 +3294,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) { From a7ec4ae964b0a033e87afc12696f9c14107003c8 Mon Sep 17 00:00:00 2001 From: Amol Kapoor Date: Sun, 18 Jan 2026 21:23:59 -0500 Subject: [PATCH 2/3] test(e2e): Update snapshots to include approval mode in footer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update E2E test snapshots to reflect the new approval mode label displayed in the footer status line. The tests now verify that "Approval Mode: Agent" appears in the footer when using the default trusted project configuration. 🤖 Generated with [Nori](https://nori.ai) Co-Authored-By: Nori --- .../acp_tool_calls__acp_explored_after_assistant_message.snap | 2 +- .../snapshots/acp_tool_calls__acp_multi_call_exploring.snap | 2 +- .../tests/snapshots/acp_tool_calls__acp_tool_call_echo.snap | 2 +- .../snapshots/acp_tool_calls__acp_tool_call_no_duplicates.snap | 2 +- .../tests/snapshots/acp_tool_calls__acp_tool_call_read.snap | 2 +- .../input_handling__history_navigation_multiple_messages.snap | 2 +- .../snapshots/input_handling__history_navigation_up_down.snap | 2 +- .../tests/snapshots/input_handling__typing_and_backspace.snap | 3 +++ .../tui-pty-e2e/tests/snapshots/nori_footer__full_footer.snap | 2 +- .../tests/snapshots/prompt_flow__custom_response.snap | 2 +- .../tests/snapshots/prompt_flow__multiline_input.snap | 3 +++ .../tests/snapshots/prompt_flow__prompt_submitted.snap | 2 +- .../tui-pty-e2e/tests/snapshots/streaming__submit_input.snap | 2 +- 13 files changed, 17 insertions(+), 11 deletions(-) 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 From ab17270c9c2ac88ec9ab12338c6d80b370254ad5 Mon Sep 17 00:00:00 2001 From: Amol Kapoor Date: Sun, 18 Jan 2026 21:39:55 -0500 Subject: [PATCH 3/3] fix(e2e): canonicalize cwd path in config to fix macOS test failures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On macOS, /tmp is a symlink to /private/tmp. When the E2E tests wrote config.toml with a non-canonicalized temp directory path, the config loader would canonicalize CODEX_HOME and resolved_cwd but fail to match the project trust level because the paths differed. This caused the approval mode to default to ReadOnly instead of using the configured Agent mode, breaking the snapshot test. Fix by canonicalizing the cwd path when generating config.toml in E2E tests, ensuring path matching works correctly on macOS. 🤖 Generated with [Nori](https://nori.ai) Co-Authored-By: Nori --- codex-rs/tui-pty-e2e/src/lib.rs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) 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"