Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions codex-rs/core/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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,
}
);
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(),
};

Expand Down Expand Up @@ -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(),
};

Expand Down Expand Up @@ -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(),
};

Expand Down
12 changes: 12 additions & 0 deletions codex-rs/core/src/config/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down
35 changes: 35 additions & 0 deletions codex-rs/protocol/src/config_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
8 changes: 8 additions & 0 deletions codex-rs/tui/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,14 @@ pub struct Cli {
#[arg(long = "add-dir", value_name = "DIR", value_hint = ValueHint::DirPath)]
pub add_dir: Vec<PathBuf>,

/// 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,
}
43 changes: 42 additions & 1 deletion codex-rs/tui/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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),
Expand Down
14 changes: 14 additions & 0 deletions codex-rs/tui/src/tui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,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 {
Expand Down Expand Up @@ -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()
}
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down
6 changes: 6 additions & 0 deletions codex-rs/tui2/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Expand All @@ -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,
}
}
Expand Down
31 changes: 30 additions & 1 deletion codex-rs/tui2/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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(
Expand Down
14 changes: 14 additions & 0 deletions codex-rs/tui2/src/tui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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()
}
Expand Down Expand Up @@ -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(());
Expand All @@ -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);
Expand Down
Loading