From 7adf65f3088191ae2b36d8533aeb0408ddcee475 Mon Sep 17 00:00:00 2001 From: Helmut Januschka Date: Fri, 26 Dec 2025 15:33:41 +0100 Subject: [PATCH 1/4] fix: add tui.alternate_screen config and --no-alt-screen CLI flag Fixes #2558 - Zellij users cannot scroll back due to alternate screen mode. This adds: - `AltScreenMode` enum with values: auto, always, never - `tui.alternate_screen` config option (default: auto) - `--no-alt-screen` CLI flag to force inline mode - Auto-detection: When running in Zellij (detected via ZELLIJ env vars), alternate screen is automatically disabled to preserve scrollback The implementation: - Detects Zellij using existing terminal_info() infrastructure - In "auto" mode, skips enter_alt_screen() when in Zellij - In "never" mode or with --no-alt-screen flag, always uses inline mode - In "always" mode, uses alternate screen (original behavior) --- codex-rs/core/src/config/mod.rs | 18 +++++++++++++ codex-rs/core/src/config/types.rs | 12 +++++++++ codex-rs/protocol/src/config_types.rs | 18 +++++++++++++ codex-rs/tui/src/cli.rs | 5 ++++ codex-rs/tui2/src/cli.rs | 6 +++++ codex-rs/tui2/src/lib.rs | 39 ++++++++++++++++++++++----- 6 files changed, 91 insertions(+), 7 deletions(-) diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 6099e1c2969..1d0d089d3fb 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); @@ -3233,6 +3247,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 +3332,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 +3432,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 +3518,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..273b3240bb6 100644 --- a/codex-rs/protocol/src/config_types.rs +++ b/codex-rs/protocol/src/config_types.rs @@ -80,3 +80,21 @@ pub enum TrustLevel { Trusted, Untrusted, } + +/// Controls whether the TUI uses the terminal's alternate screen buffer. +/// +/// Using alternate screen provides a cleaner fullscreen experience but prevents +/// scrollback in terminal multiplexers like Zellij that follow the xterm spec +/// (which says alternate screen should not have scrollback). +#[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..0a77842af46 100644 --- a/codex-rs/tui/src/cli.rs +++ b/codex-rs/tui/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, } 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..29feddfaad2 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,13 +517,34 @@ async fn run_ratatui_app( resume_picker::ResumeSelection::StartFresh }; - let Cli { prompt, images, .. } = cli; + let Cli { + prompt, + images, + no_alt_screen, + .. + } = cli; + + // Determine whether to use alternate screen based on CLI flag and config. + // Alternate screen provides a cleaner fullscreen experience but prevents + // scrollback in terminal multiplexers like Zellij that follow xterm spec. + 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 { .. })) + } + } + }; - // 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. - let _ = tui.enter_alt_screen(); + if use_alt_screen { + let _ = tui.enter_alt_screen(); + } let app_result = App::run( &mut tui, @@ -536,7 +559,9 @@ async fn run_ratatui_app( ) .await; - let _ = tui.leave_alt_screen(); + if use_alt_screen { + let _ = tui.leave_alt_screen(); + } restore(); if let Ok(exit_info) = &app_result { let mut stdout = std::io::stdout(); From 63630ab9cf52e8f791e63384a471ed8a95a49eeb Mon Sep 17 00:00:00 2001 From: Helmut Januschka Date: Fri, 26 Dec 2025 16:05:32 +0100 Subject: [PATCH 2/4] fix: make enter_alt_screen() respect alt_screen_enabled flag Addresses review comments: - Resume picker now respects --no-alt-screen flag - Legacy TUI now properly wires up the flag The fix adds an alt_screen_enabled flag to the Tui struct that makes enter_alt_screen()/leave_alt_screen() no-ops when false. This ensures all call sites (resume picker, overlays, etc.) respect the setting. --- codex-rs/tui/src/lib.rs | 24 +++++++++++++++++++++++- codex-rs/tui/src/tui.rs | 14 ++++++++++++++ codex-rs/tui2/src/lib.rs | 10 ++++------ codex-rs/tui2/src/tui.rs | 14 ++++++++++++++ 4 files changed, 55 insertions(+), 7 deletions(-) diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 6f4faaad659..cab5ae08820 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,27 @@ async fn run_ratatui_app( resume_picker::ResumeSelection::StartFresh }; - let Cli { prompt, images, .. } = cli; + let Cli { + prompt, + images, + no_alt_screen, + .. + } = cli; + + // Determine whether to use alternate screen based on CLI flag and config. + let use_alt_screen = if no_alt_screen { + false + } else { + match config.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 { .. })) + } + } + }; + tui.set_alt_screen_enabled(use_alt_screen); let app_result = App::run( &mut tui, 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/lib.rs b/codex-rs/tui2/src/lib.rs index 29feddfaad2..ffe004595e2 100644 --- a/codex-rs/tui2/src/lib.rs +++ b/codex-rs/tui2/src/lib.rs @@ -542,9 +542,9 @@ async fn run_ratatui_app( } }; - if use_alt_screen { - let _ = tui.enter_alt_screen(); - } + // 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( &mut tui, @@ -559,9 +559,7 @@ async fn run_ratatui_app( ) .await; - if use_alt_screen { - let _ = tui.leave_alt_screen(); - } + let _ = tui.leave_alt_screen(); restore(); if let Ok(exit_info) = &app_result { let mut stdout = std::io::stdout(); 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); From b079102c6966ebd8b568a5e91730c98da37b9a60 Mon Sep 17 00:00:00 2001 From: Helmut Januschka Date: Wed, 7 Jan 2026 22:50:10 +0100 Subject: [PATCH 3/4] test: include alternate_screen in tui default --- codex-rs/core/src/config/mod.rs | 1 + codex-rs/protocol/src/config_types.rs | 25 +++++- codex-rs/tui/src/cli.rs | 7 +- codex-rs/tui/src/lib.rs | 45 +++++++--- codex-rs/tui2/src/lib.rs | 12 ++- docs/tui-alternate-screen.md | 121 ++++++++++++++++++++++++++ 6 files changed, 189 insertions(+), 22 deletions(-) create mode 100644 docs/tui-alternate-screen.md diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 1d0d089d3fb..6f10a428e74 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -1632,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, } ); } diff --git a/codex-rs/protocol/src/config_types.rs b/codex-rs/protocol/src/config_types.rs index 273b3240bb6..b6e4a1e34ab 100644 --- a/codex-rs/protocol/src/config_types.rs +++ b/codex-rs/protocol/src/config_types.rs @@ -83,10 +83,27 @@ pub enum TrustLevel { /// Controls whether the TUI uses the terminal's alternate screen buffer. /// -/// Using alternate screen provides a cleaner fullscreen experience but prevents -/// scrollback in terminal multiplexers like Zellij that follow the xterm spec -/// (which says alternate screen should not have scrollback). -#[derive(Debug, Serialize, Deserialize, Default, Clone, Copy, PartialEq, Eq, Display, JsonSchema, TS)] +/// **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 { diff --git a/codex-rs/tui/src/cli.rs b/codex-rs/tui/src/cli.rs index 0a77842af46..8f011ff5937 100644 --- a/codex-rs/tui/src/cli.rs +++ b/codex-rs/tui/src/cli.rs @@ -85,8 +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. + /// 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, diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index cab5ae08820..2ed761270f9 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -502,19 +502,7 @@ async fn run_ratatui_app( .. } = cli; - // Determine whether to use alternate screen based on CLI flag and config. - let use_alt_screen = if no_alt_screen { - false - } else { - match config.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 { .. })) - } - } - }; + 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( @@ -549,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/tui2/src/lib.rs b/codex-rs/tui2/src/lib.rs index ffe004595e2..1e41d223b0d 100644 --- a/codex-rs/tui2/src/lib.rs +++ b/codex-rs/tui2/src/lib.rs @@ -524,9 +524,15 @@ async fn run_ratatui_app( .. } = cli; - // Determine whether to use alternate screen based on CLI flag and config. - // Alternate screen provides a cleaner fullscreen experience but prevents - // scrollback in terminal multiplexers like Zellij that follow xterm spec. + // 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 diff --git a/docs/tui-alternate-screen.md b/docs/tui-alternate-screen.md new file mode 100644 index 00000000000..3089ef9edda --- /dev/null +++ b/docs/tui-alternate-screen.md @@ -0,0 +1,121 @@ +# 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 +``` From c989ed9962481988bd5a6525140710872b467817 Mon Sep 17 00:00:00 2001 From: Josh McKinney Date: Fri, 9 Jan 2026 10:19:37 -0800 Subject: [PATCH 4/4] run prettier --- codex-cli/bin/codex.js | 1 - docs/tui-alternate-screen.md | 9 +++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) 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/docs/tui-alternate-screen.md b/docs/tui-alternate-screen.md index 3089ef9edda..2fe141a2f23 100644 --- a/docs/tui-alternate-screen.md +++ b/docs/tui-alternate-screen.md @@ -9,6 +9,7 @@ This document explains the design decision behind Codex's alternate screen handl ### 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.) @@ -22,6 +23,7 @@ Terminal multiplexers like **Zellij** strictly follow the xterm specification, w - **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 @@ -31,16 +33,19 @@ When using Codex's TUI in Zellij, users cannot scroll back through the conversat 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 @@ -54,6 +59,7 @@ 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 @@ -84,6 +90,7 @@ 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 @@ -110,12 +117,14 @@ Codex's transcript pager (opened with Ctrl+T) provides an alternative way to rev ## 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 ```