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
20 changes: 20 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,23 @@ 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,
}
5 changes: 5 additions & 0 deletions codex-rs/tui/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,
}
24 changes: 23 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,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,
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
33 changes: 28 additions & 5 deletions 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,33 @@ 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.
// 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