diff --git a/codex-cli/bin/codex.js b/codex-cli/bin/codex.js index 6ec8069bd25..6bc1ad5c627 100644 --- a/codex-cli/bin/codex.js +++ b/codex-cli/bin/codex.js @@ -95,7 +95,6 @@ function detectPackageManager() { return "bun"; } - if ( __dirname.includes(".bun/install/global") || __dirname.includes(".bun\\install\\global") diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 6099e1c2969..6f10a428e74 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -32,6 +32,7 @@ use crate::protocol::AskForApproval; use crate::protocol::SandboxPolicy; use codex_app_server_protocol::Tools; use codex_app_server_protocol::UserSavedConfig; +use codex_protocol::config_types::AltScreenMode; use codex_protocol::config_types::ForcedLoginMethod; use codex_protocol::config_types::ReasoningSummary; use codex_protocol::config_types::SandboxMode; @@ -236,6 +237,14 @@ pub struct Config { /// consistently to both mouse wheels and trackpads. pub tui_scroll_invert: bool, + /// Controls whether the TUI uses the terminal's alternate screen buffer. + /// + /// This is the same `tui.alternate_screen` value from `config.toml` (see [`Tui`]). + /// - `auto` (default): Disable alternate screen in Zellij, enable elsewhere. + /// - `always`: Always use alternate screen (original behavior). + /// - `never`: Never use alternate screen (inline mode, preserves scrollback). + pub tui_alternate_screen: AltScreenMode, + /// The directory that should be treated as the current working directory /// for the session. All relative paths inside the business-logic layer are /// resolved against this path. @@ -1431,6 +1440,11 @@ impl Config { .as_ref() .and_then(|t| t.scroll_wheel_like_max_duration_ms), tui_scroll_invert: cfg.tui.as_ref().map(|t| t.scroll_invert).unwrap_or(false), + tui_alternate_screen: cfg + .tui + .as_ref() + .map(|t| t.alternate_screen) + .unwrap_or_default(), otel: { let t: OtelConfigToml = cfg.otel.unwrap_or_default(); let log_user_prompt = t.log_user_prompt.unwrap_or(false); @@ -1618,6 +1632,7 @@ persistence = "none" scroll_wheel_tick_detect_max_ms: None, scroll_wheel_like_max_duration_ms: None, scroll_invert: false, + alternate_screen: AltScreenMode::Auto, } ); } @@ -3233,6 +3248,7 @@ model_verbosity = "high" tui_scroll_wheel_tick_detect_max_ms: None, tui_scroll_wheel_like_max_duration_ms: None, tui_scroll_invert: false, + tui_alternate_screen: AltScreenMode::Auto, otel: OtelConfig::default(), }, o3_profile_config @@ -3317,6 +3333,7 @@ model_verbosity = "high" tui_scroll_wheel_tick_detect_max_ms: None, tui_scroll_wheel_like_max_duration_ms: None, tui_scroll_invert: false, + tui_alternate_screen: AltScreenMode::Auto, otel: OtelConfig::default(), }; @@ -3416,6 +3433,7 @@ model_verbosity = "high" tui_scroll_wheel_tick_detect_max_ms: None, tui_scroll_wheel_like_max_duration_ms: None, tui_scroll_invert: false, + tui_alternate_screen: AltScreenMode::Auto, otel: OtelConfig::default(), }; @@ -3501,6 +3519,7 @@ model_verbosity = "high" tui_scroll_wheel_tick_detect_max_ms: None, tui_scroll_wheel_like_max_duration_ms: None, tui_scroll_invert: false, + tui_alternate_screen: AltScreenMode::Auto, otel: OtelConfig::default(), }; diff --git a/codex-rs/core/src/config/types.rs b/codex-rs/core/src/config/types.rs index 6c421d4608f..e26f249ff31 100644 --- a/codex-rs/core/src/config/types.rs +++ b/codex-rs/core/src/config/types.rs @@ -3,6 +3,7 @@ // Note this file should generally be restricted to simple struct/enum // definitions that do not contain business logic. +pub use codex_protocol::config_types::AltScreenMode; use codex_utils_absolute_path::AbsolutePathBuf; use std::collections::BTreeMap; use std::collections::HashMap; @@ -514,6 +515,17 @@ pub struct Tui { /// wheel and trackpad input. #[serde(default)] pub scroll_invert: bool, + + /// Controls whether the TUI uses the terminal's alternate screen buffer. + /// + /// - `auto` (default): Disable alternate screen in Zellij, enable elsewhere. + /// - `always`: Always use alternate screen (original behavior). + /// - `never`: Never use alternate screen (inline mode only, preserves scrollback). + /// + /// Using alternate screen provides a cleaner fullscreen experience but prevents + /// scrollback in terminal multiplexers like Zellij that follow the xterm spec. + #[serde(default)] + pub alternate_screen: AltScreenMode, } const fn default_true() -> bool { diff --git a/codex-rs/protocol/src/config_types.rs b/codex-rs/protocol/src/config_types.rs index a98ec4e2b2f..b6e4a1e34ab 100644 --- a/codex-rs/protocol/src/config_types.rs +++ b/codex-rs/protocol/src/config_types.rs @@ -80,3 +80,38 @@ pub enum TrustLevel { Trusted, Untrusted, } + +/// Controls whether the TUI uses the terminal's alternate screen buffer. +/// +/// **Background:** The alternate screen buffer provides a cleaner fullscreen experience +/// without polluting the terminal's scrollback history. However, it conflicts with terminal +/// multiplexers like Zellij that strictly follow the xterm specification, which defines +/// that alternate screen buffers should not have scrollback. +/// +/// **Zellij's behavior:** Zellij intentionally disables scrollback in alternate screen mode +/// (see https://github.com/zellij-org/zellij/pull/1032) to comply with the xterm spec. This +/// is by design and not configurable in Zellij—there is no option to enable scrollback in +/// alternate screen mode. +/// +/// **Solution:** This setting provides a pragmatic workaround: +/// - `auto` (default): Automatically detect the terminal multiplexer. If running in Zellij, +/// disable alternate screen to preserve scrollback. Enable it everywhere else. +/// - `always`: Always use alternate screen mode (original behavior before this fix). +/// - `never`: Never use alternate screen mode. Runs in inline mode, preserving scrollback +/// in all multiplexers. +/// +/// The CLI flag `--no-alt-screen` can override this setting at runtime. +#[derive( + Debug, Serialize, Deserialize, Default, Clone, Copy, PartialEq, Eq, Display, JsonSchema, TS, +)] +#[serde(rename_all = "lowercase")] +#[strum(serialize_all = "lowercase")] +pub enum AltScreenMode { + /// Auto-detect: disable alternate screen in Zellij, enable elsewhere. + #[default] + Auto, + /// Always use alternate screen (original behavior). + Always, + /// Never use alternate screen (inline mode only). + Never, +} diff --git a/codex-rs/tui/src/cli.rs b/codex-rs/tui/src/cli.rs index 2b19b4c0649..8f011ff5937 100644 --- a/codex-rs/tui/src/cli.rs +++ b/codex-rs/tui/src/cli.rs @@ -85,6 +85,14 @@ pub struct Cli { #[arg(long = "add-dir", value_name = "DIR", value_hint = ValueHint::DirPath)] pub add_dir: Vec, + /// Disable alternate screen mode + /// + /// Runs the TUI in inline mode, preserving terminal scrollback history. This is useful + /// in terminal multiplexers like Zellij that follow the xterm spec strictly and disable + /// scrollback in alternate screen buffers. + #[arg(long = "no-alt-screen", default_value_t = false)] + pub no_alt_screen: bool, + #[clap(skip)] pub config_overrides: CliConfigOverrides, } diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 6f4faaad659..2ed761270f9 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -22,6 +22,8 @@ use codex_core::config::resolve_oss_provider; use codex_core::find_thread_path_by_id_str; use codex_core::get_platform_sandbox; use codex_core::protocol::AskForApproval; +use codex_core::terminal::Multiplexer; +use codex_protocol::config_types::AltScreenMode; use codex_protocol::config_types::SandboxMode; use codex_utils_absolute_path::AbsolutePathBuf; use std::fs::OpenOptions; @@ -493,7 +495,15 @@ async fn run_ratatui_app( resume_picker::ResumeSelection::StartFresh }; - let Cli { prompt, images, .. } = cli; + let Cli { + prompt, + images, + no_alt_screen, + .. + } = cli; + + let use_alt_screen = determine_alt_screen_mode(no_alt_screen, config.tui_alternate_screen); + tui.set_alt_screen_enabled(use_alt_screen); let app_result = App::run( &mut tui, @@ -527,6 +537,37 @@ fn restore() { } } +/// Determine whether to use the terminal's alternate screen buffer. +/// +/// The alternate screen buffer provides a cleaner fullscreen experience without polluting +/// the terminal's scrollback history. However, it conflicts with terminal multiplexers like +/// Zellij that strictly follow the xterm spec, which disallows scrollback in alternate screen +/// buffers. Zellij intentionally disables scrollback in alternate screen mode (see +/// https://github.com/zellij-org/zellij/pull/1032) and offers no configuration option to +/// change this behavior. +/// +/// This function implements a pragmatic workaround: +/// - If `--no-alt-screen` is explicitly passed, always disable alternate screen +/// - Otherwise, respect the `tui.alternate_screen` config setting: +/// - `always`: Use alternate screen everywhere (original behavior) +/// - `never`: Inline mode only, preserves scrollback +/// - `auto` (default): Auto-detect the terminal multiplexer and disable alternate screen +/// only in Zellij, enabling it everywhere else +fn determine_alt_screen_mode(no_alt_screen: bool, tui_alternate_screen: AltScreenMode) -> bool { + if no_alt_screen { + false + } else { + match tui_alternate_screen { + AltScreenMode::Always => true, + AltScreenMode::Never => false, + AltScreenMode::Auto => { + let terminal_info = codex_core::terminal::terminal_info(); + !matches!(terminal_info.multiplexer, Some(Multiplexer::Zellij { .. })) + } + } + } +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum LoginStatus { AuthMode(AuthMode), diff --git a/codex-rs/tui/src/tui.rs b/codex-rs/tui/src/tui.rs index ff9ce94d24f..b5fee2e4258 100644 --- a/codex-rs/tui/src/tui.rs +++ b/codex-rs/tui/src/tui.rs @@ -247,6 +247,8 @@ pub struct Tui { terminal_focused: Arc, enhanced_keys_supported: bool, notification_backend: Option, + // When false, enter_alt_screen() becomes a no-op (for Zellij scrollback support) + alt_screen_enabled: bool, } impl Tui { @@ -274,9 +276,15 @@ impl Tui { terminal_focused: Arc::new(AtomicBool::new(true)), enhanced_keys_supported, notification_backend: Some(detect_backend()), + alt_screen_enabled: true, } } + /// Set whether alternate screen is enabled. When false, enter_alt_screen() becomes a no-op. + pub fn set_alt_screen_enabled(&mut self, enabled: bool) { + self.alt_screen_enabled = enabled; + } + pub fn frame_requester(&self) -> FrameRequester { self.frame_requester.clone() } @@ -407,6 +415,9 @@ impl Tui { /// Enter alternate screen and expand the viewport to full terminal size, saving the current /// inline viewport for restoration when leaving. pub fn enter_alt_screen(&mut self) -> Result<()> { + if !self.alt_screen_enabled { + return Ok(()); + } let _ = execute!(self.terminal.backend_mut(), EnterAlternateScreen); // Enable "alternate scroll" so terminals may translate wheel to arrows let _ = execute!(self.terminal.backend_mut(), EnableAlternateScroll); @@ -426,6 +437,9 @@ impl Tui { /// Leave alternate screen and restore the previously saved inline viewport, if any. pub fn leave_alt_screen(&mut self) -> Result<()> { + if !self.alt_screen_enabled { + return Ok(()); + } // Disable alternate scroll when leaving alt-screen let _ = execute!(self.terminal.backend_mut(), DisableAlternateScroll); let _ = execute!(self.terminal.backend_mut(), LeaveAlternateScreen); diff --git a/codex-rs/tui2/src/cli.rs b/codex-rs/tui2/src/cli.rs index b0daa447701..8c057dac78c 100644 --- a/codex-rs/tui2/src/cli.rs +++ b/codex-rs/tui2/src/cli.rs @@ -85,6 +85,11 @@ pub struct Cli { #[arg(long = "add-dir", value_name = "DIR", value_hint = ValueHint::DirPath)] pub add_dir: Vec, + /// Disable alternate screen mode for better scrollback in terminal multiplexers like Zellij. + /// This runs the TUI in inline mode, preserving terminal scrollback history. + #[arg(long = "no-alt-screen", default_value_t = false)] + pub no_alt_screen: bool, + #[clap(skip)] pub config_overrides: CliConfigOverrides, } @@ -109,6 +114,7 @@ impl From for Cli { cwd: cli.cwd, web_search: cli.web_search, add_dir: cli.add_dir, + no_alt_screen: cli.no_alt_screen, config_overrides: cli.config_overrides, } } diff --git a/codex-rs/tui2/src/lib.rs b/codex-rs/tui2/src/lib.rs index 1c161bf6278..1e41d223b0d 100644 --- a/codex-rs/tui2/src/lib.rs +++ b/codex-rs/tui2/src/lib.rs @@ -22,6 +22,8 @@ use codex_core::config::resolve_oss_provider; use codex_core::find_thread_path_by_id_str; use codex_core::get_platform_sandbox; use codex_core::protocol::AskForApproval; +use codex_core::terminal::Multiplexer; +use codex_protocol::config_types::AltScreenMode; use codex_protocol::config_types::SandboxMode; use codex_utils_absolute_path::AbsolutePathBuf; use std::fs::OpenOptions; @@ -515,12 +517,39 @@ async fn run_ratatui_app( resume_picker::ResumeSelection::StartFresh }; - let Cli { prompt, images, .. } = cli; + let Cli { + prompt, + images, + no_alt_screen, + .. + } = cli; // Run the main chat + transcript UI on the terminal's alternate screen so // the entire viewport can be used without polluting normal scrollback. This // mirrors the behavior of the legacy TUI but keeps inline mode available // for smaller prompts like onboarding and model migration. + // + // However, alternate screen prevents scrollback in terminal multiplexers like + // Zellij that strictly follow the xterm spec (which disallows scrollback in + // alternate screen buffers). This auto-detects the terminal and disables + // alternate screen in Zellij while keeping it enabled elsewhere. + let use_alt_screen = if no_alt_screen { + // CLI flag explicitly disables alternate screen + false + } else { + match config.tui_alternate_screen { + AltScreenMode::Always => true, + AltScreenMode::Never => false, + AltScreenMode::Auto => { + // Auto-detect: disable in Zellij, enable elsewhere + let terminal_info = codex_core::terminal::terminal_info(); + !matches!(terminal_info.multiplexer, Some(Multiplexer::Zellij { .. })) + } + } + }; + + // Set flag on Tui so all enter_alt_screen() calls respect the setting + tui.set_alt_screen_enabled(use_alt_screen); let _ = tui.enter_alt_screen(); let app_result = App::run( diff --git a/codex-rs/tui2/src/tui.rs b/codex-rs/tui2/src/tui.rs index 7a0ef65a25d..1d6f129fa11 100644 --- a/codex-rs/tui2/src/tui.rs +++ b/codex-rs/tui2/src/tui.rs @@ -143,6 +143,8 @@ pub struct Tui { terminal_focused: Arc, enhanced_keys_supported: bool, notification_backend: Option, + // When false, enter_alt_screen() becomes a no-op (for Zellij scrollback support) + alt_screen_enabled: bool, } impl Tui { @@ -170,9 +172,15 @@ impl Tui { terminal_focused: Arc::new(AtomicBool::new(true)), enhanced_keys_supported, notification_backend: Some(detect_backend()), + alt_screen_enabled: true, } } + /// Set whether alternate screen is enabled. When false, enter_alt_screen() becomes a no-op. + pub fn set_alt_screen_enabled(&mut self, enabled: bool) { + self.alt_screen_enabled = enabled; + } + pub fn frame_requester(&self) -> FrameRequester { self.frame_requester.clone() } @@ -309,6 +317,9 @@ impl Tui { /// Enter alternate screen and expand the viewport to full terminal size, saving the current /// inline viewport for restoration when leaving. pub fn enter_alt_screen(&mut self) -> Result<()> { + if !self.alt_screen_enabled { + return Ok(()); + } if !self.alt_screen_nesting.enter() { self.alt_screen_active.store(true, Ordering::Relaxed); return Ok(()); @@ -330,6 +341,9 @@ impl Tui { /// Leave alternate screen and restore the previously saved inline viewport, if any. pub fn leave_alt_screen(&mut self) -> Result<()> { + if !self.alt_screen_enabled { + return Ok(()); + } if !self.alt_screen_nesting.leave() { self.alt_screen_active .store(self.alt_screen_nesting.is_active(), Ordering::Relaxed); diff --git a/docs/tui-alternate-screen.md b/docs/tui-alternate-screen.md new file mode 100644 index 00000000000..2fe141a2f23 --- /dev/null +++ b/docs/tui-alternate-screen.md @@ -0,0 +1,130 @@ +# TUI Alternate Screen and Terminal Multiplexers + +## Overview + +This document explains the design decision behind Codex's alternate screen handling, particularly in terminal multiplexers like Zellij. This addresses a fundamental conflict between fullscreen TUI behavior and terminal scrollback history preservation. + +## The Problem + +### Fullscreen TUI Benefits + +Codex's TUI uses the terminal's **alternate screen buffer** to provide a clean fullscreen experience. This approach: + +- Uses the entire viewport without polluting the terminal's scrollback history +- Provides a dedicated environment for the chat interface +- Mirrors the behavior of other terminal applications (vim, tmux, etc.) + +### The Zellij Conflict + +Terminal multiplexers like **Zellij** strictly follow the xterm specification, which defines that alternate screen buffers should **not** have scrollback. This is intentional design, not a bug: + +- **Zellij PR:** https://github.com/zellij-org/zellij/pull/1032 +- **Rationale:** The xterm spec explicitly states that alternate screen mode disallows scrollback +- **Configurability:** This is not configurable in Zellij—there is no option to enable scrollback in alternate screen mode + +When using Codex's TUI in Zellij, users cannot scroll back through the conversation history because: + +1. The TUI runs in alternate screen mode (fullscreen) +2. Zellij disables scrollback in alternate screen buffers (per xterm spec) +3. The entire conversation becomes inaccessible via normal terminal scrolling + +## The Solution + +Codex implements a **pragmatic workaround** with three modes, controlled by `tui.alternate_screen` in `config.toml`: + +### 1. `auto` (default) + +- **Behavior:** Automatically detect the terminal multiplexer +- **In Zellij:** Disable alternate screen mode (inline mode, preserves scrollback) +- **Elsewhere:** Enable alternate screen mode (fullscreen experience) +- **Rationale:** Provides the best UX in each environment + +### 2. `always` + +- **Behavior:** Always use alternate screen mode (original behavior) +- **Use case:** Users who prefer fullscreen and don't use Zellij, or who have found a workaround + +### 3. `never` + +- **Behavior:** Never use alternate screen mode (inline mode) +- **Use case:** Users who always want scrollback history preserved +- **Trade-off:** Pollutes the terminal scrollback with TUI output + +## Runtime Override + +The `--no-alt-screen` CLI flag can override the config setting at runtime: + +```bash +codex --no-alt-screen +``` + +This runs the TUI in inline mode regardless of the configuration, useful for: + +- One-off sessions where scrollback is critical +- Debugging terminal-related issues +- Testing alternate screen behavior + +## Implementation Details + +### Auto-Detection + +The `auto` mode detects Zellij by checking the `ZELLIJ` environment variable: + +```rust +let terminal_info = codex_core::terminal::terminal_info(); +!matches!(terminal_info.multiplexer, Some(Multiplexer::Zellij { .. })) +``` + +This detection happens in the helper function `determine_alt_screen_mode()` in `codex-rs/tui/src/lib.rs`. + +### Configuration Schema + +The `AltScreenMode` enum is defined in `codex-rs/protocol/src/config_types.rs` and serializes to lowercase TOML: + +```toml +[tui] +# Options: auto, always, never +alternate_screen = "auto" +``` + +### Why Not Just Disable Alternate Screen in Zellij Permanently? + +We use `auto` detection instead of always disabling in Zellij because: + +1. Many Zellij users don't care about scrollback and prefer the fullscreen experience +2. Some users may use tmux inside Zellij, creating a chain of multiplexers +3. Provides user choice without requiring manual configuration + +## Related Issues and References + +- **Original Issue:** [GitHub #2558](https://github.com/openai/codex/issues/2558) - "No scrollback in Zellij" +- **Implementation PR:** [GitHub #8555](https://github.com/openai/codex/pull/8555) +- **Zellij PR:** https://github.com/zellij-org/zellij/pull/1032 (why scrollback is disabled) +- **xterm Spec:** Alternate screen buffers should not have scrollback + +## Future Considerations + +### Alternative Approaches Considered + +1. **Implement custom scrollback in TUI:** Would require significant architectural changes to buffer and render all historical output +2. **Request Zellij to add a config option:** Not viable—Zellij maintainers explicitly chose this behavior to follow the spec +3. **Disable alternate screen unconditionally:** Would degrade UX for non-Zellij users + +### Transcript Pager + +Codex's transcript pager (opened with Ctrl+T) provides an alternative way to review conversation history, even in fullscreen mode. However, this is not as seamless as natural scrollback. + +## For Developers + +When modifying TUI code, remember: + +- The `determine_alt_screen_mode()` function encapsulates all the logic +- Configuration is in `config.tui_alternate_screen` +- CLI flag is in `cli.no_alt_screen` +- The behavior is applied via `tui.set_alt_screen_enabled()` + +If you encounter issues with terminal state after running Codex, you can restore your terminal with: + +```bash +reset +```