fix: add tui.alternate_screen config and --no-alt-screen CLI flag for Zellij scrollback (#8555)

Fixes #2558

Codex uses alternate screen mode (CSI 1049) which, per xterm spec,
doesn't support scrollback. Zellij follows this strictly, so users can't
scroll back through output.

**Changes:**
- Add `tui.alternate_screen` config: `auto` (default), `always`, `never`
- Add `--no-alt-screen` CLI flag
- Auto-detect Zellij and skip alt screen (uses existing `ZELLIJ` env var
detection)

**Usage:**
```bash
# CLI flag
codex --no-alt-screen

# Or in config.toml
[tui]
alternate_screen = "never"
```

With default `auto` mode, Zellij users get working scrollback without
any config changes.

---------

Co-authored-by: Josh McKinney <joshka@openai.com>
This commit is contained in:
Helmut Januschka
2026-01-09 19:38:26 +01:00
committed by GitHub
Unverified
parent 1aed01e99f
commit 7daaabc795
11 changed files with 310 additions and 3 deletions
+6
View File
@@ -85,6 +85,11 @@ pub struct Cli {
#[arg(long = "add-dir", value_name = "DIR", value_hint = ValueHint::DirPath)]
pub add_dir: Vec<PathBuf>,
/// 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<codex_tui::Cli> 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,
}
}
+30 -1
View File
@@ -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(
+14
View File
@@ -143,6 +143,8 @@ pub struct Tui {
terminal_focused: Arc<AtomicBool>,
enhanced_keys_supported: bool,
notification_backend: Option<DesktopNotificationBackend>,
// 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);