mirror of
https://github.com/openai/codex.git
synced 2026-04-24 22:54:54 +00:00
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:
@@ -95,7 +95,6 @@ function detectPackageManager() {
|
|||||||
return "bun";
|
return "bun";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
__dirname.includes(".bun/install/global") ||
|
__dirname.includes(".bun/install/global") ||
|
||||||
__dirname.includes(".bun\\install\\global")
|
__dirname.includes(".bun\\install\\global")
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ use crate::protocol::AskForApproval;
|
|||||||
use crate::protocol::SandboxPolicy;
|
use crate::protocol::SandboxPolicy;
|
||||||
use codex_app_server_protocol::Tools;
|
use codex_app_server_protocol::Tools;
|
||||||
use codex_app_server_protocol::UserSavedConfig;
|
use codex_app_server_protocol::UserSavedConfig;
|
||||||
|
use codex_protocol::config_types::AltScreenMode;
|
||||||
use codex_protocol::config_types::ForcedLoginMethod;
|
use codex_protocol::config_types::ForcedLoginMethod;
|
||||||
use codex_protocol::config_types::ReasoningSummary;
|
use codex_protocol::config_types::ReasoningSummary;
|
||||||
use codex_protocol::config_types::SandboxMode;
|
use codex_protocol::config_types::SandboxMode;
|
||||||
@@ -236,6 +237,14 @@ pub struct Config {
|
|||||||
/// consistently to both mouse wheels and trackpads.
|
/// consistently to both mouse wheels and trackpads.
|
||||||
pub tui_scroll_invert: bool,
|
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
|
/// The directory that should be treated as the current working directory
|
||||||
/// for the session. All relative paths inside the business-logic layer are
|
/// for the session. All relative paths inside the business-logic layer are
|
||||||
/// resolved against this path.
|
/// resolved against this path.
|
||||||
@@ -1443,6 +1452,11 @@ impl Config {
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|t| t.scroll_wheel_like_max_duration_ms),
|
.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_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: {
|
otel: {
|
||||||
let t: OtelConfigToml = cfg.otel.unwrap_or_default();
|
let t: OtelConfigToml = cfg.otel.unwrap_or_default();
|
||||||
let log_user_prompt = t.log_user_prompt.unwrap_or(false);
|
let log_user_prompt = t.log_user_prompt.unwrap_or(false);
|
||||||
@@ -1641,6 +1655,7 @@ persistence = "none"
|
|||||||
scroll_wheel_tick_detect_max_ms: None,
|
scroll_wheel_tick_detect_max_ms: None,
|
||||||
scroll_wheel_like_max_duration_ms: None,
|
scroll_wheel_like_max_duration_ms: None,
|
||||||
scroll_invert: false,
|
scroll_invert: false,
|
||||||
|
alternate_screen: AltScreenMode::Auto,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -3276,6 +3291,7 @@ model_verbosity = "high"
|
|||||||
tui_scroll_wheel_tick_detect_max_ms: None,
|
tui_scroll_wheel_tick_detect_max_ms: None,
|
||||||
tui_scroll_wheel_like_max_duration_ms: None,
|
tui_scroll_wheel_like_max_duration_ms: None,
|
||||||
tui_scroll_invert: false,
|
tui_scroll_invert: false,
|
||||||
|
tui_alternate_screen: AltScreenMode::Auto,
|
||||||
otel: OtelConfig::default(),
|
otel: OtelConfig::default(),
|
||||||
},
|
},
|
||||||
o3_profile_config
|
o3_profile_config
|
||||||
@@ -3361,6 +3377,7 @@ model_verbosity = "high"
|
|||||||
tui_scroll_wheel_tick_detect_max_ms: None,
|
tui_scroll_wheel_tick_detect_max_ms: None,
|
||||||
tui_scroll_wheel_like_max_duration_ms: None,
|
tui_scroll_wheel_like_max_duration_ms: None,
|
||||||
tui_scroll_invert: false,
|
tui_scroll_invert: false,
|
||||||
|
tui_alternate_screen: AltScreenMode::Auto,
|
||||||
otel: OtelConfig::default(),
|
otel: OtelConfig::default(),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -3461,6 +3478,7 @@ model_verbosity = "high"
|
|||||||
tui_scroll_wheel_tick_detect_max_ms: None,
|
tui_scroll_wheel_tick_detect_max_ms: None,
|
||||||
tui_scroll_wheel_like_max_duration_ms: None,
|
tui_scroll_wheel_like_max_duration_ms: None,
|
||||||
tui_scroll_invert: false,
|
tui_scroll_invert: false,
|
||||||
|
tui_alternate_screen: AltScreenMode::Auto,
|
||||||
otel: OtelConfig::default(),
|
otel: OtelConfig::default(),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -3547,6 +3565,7 @@ model_verbosity = "high"
|
|||||||
tui_scroll_wheel_tick_detect_max_ms: None,
|
tui_scroll_wheel_tick_detect_max_ms: None,
|
||||||
tui_scroll_wheel_like_max_duration_ms: None,
|
tui_scroll_wheel_like_max_duration_ms: None,
|
||||||
tui_scroll_invert: false,
|
tui_scroll_invert: false,
|
||||||
|
tui_alternate_screen: AltScreenMode::Auto,
|
||||||
otel: OtelConfig::default(),
|
otel: OtelConfig::default(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
// Note this file should generally be restricted to simple struct/enum
|
// Note this file should generally be restricted to simple struct/enum
|
||||||
// definitions that do not contain business logic.
|
// definitions that do not contain business logic.
|
||||||
|
|
||||||
|
pub use codex_protocol::config_types::AltScreenMode;
|
||||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
@@ -523,6 +524,17 @@ pub struct Tui {
|
|||||||
/// wheel and trackpad input.
|
/// wheel and trackpad input.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub scroll_invert: bool,
|
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 {
|
const fn default_true() -> bool {
|
||||||
|
|||||||
@@ -80,3 +80,38 @@ pub enum TrustLevel {
|
|||||||
Trusted,
|
Trusted,
|
||||||
Untrusted,
|
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,
|
||||||
|
}
|
||||||
|
|||||||
@@ -85,6 +85,14 @@ pub struct Cli {
|
|||||||
#[arg(long = "add-dir", value_name = "DIR", value_hint = ValueHint::DirPath)]
|
#[arg(long = "add-dir", value_name = "DIR", value_hint = ValueHint::DirPath)]
|
||||||
pub add_dir: Vec<PathBuf>,
|
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)]
|
#[clap(skip)]
|
||||||
pub config_overrides: CliConfigOverrides,
|
pub config_overrides: CliConfigOverrides,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ use codex_core::config::resolve_oss_provider;
|
|||||||
use codex_core::find_thread_path_by_id_str;
|
use codex_core::find_thread_path_by_id_str;
|
||||||
use codex_core::get_platform_sandbox;
|
use codex_core::get_platform_sandbox;
|
||||||
use codex_core::protocol::AskForApproval;
|
use codex_core::protocol::AskForApproval;
|
||||||
|
use codex_core::terminal::Multiplexer;
|
||||||
|
use codex_protocol::config_types::AltScreenMode;
|
||||||
use codex_protocol::config_types::SandboxMode;
|
use codex_protocol::config_types::SandboxMode;
|
||||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||||
use std::fs::OpenOptions;
|
use std::fs::OpenOptions;
|
||||||
@@ -493,7 +495,15 @@ async fn run_ratatui_app(
|
|||||||
resume_picker::ResumeSelection::StartFresh
|
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(
|
let app_result = App::run(
|
||||||
&mut tui,
|
&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)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum LoginStatus {
|
pub enum LoginStatus {
|
||||||
AuthMode(AuthMode),
|
AuthMode(AuthMode),
|
||||||
|
|||||||
@@ -247,6 +247,8 @@ pub struct Tui {
|
|||||||
terminal_focused: Arc<AtomicBool>,
|
terminal_focused: Arc<AtomicBool>,
|
||||||
enhanced_keys_supported: bool,
|
enhanced_keys_supported: bool,
|
||||||
notification_backend: Option<DesktopNotificationBackend>,
|
notification_backend: Option<DesktopNotificationBackend>,
|
||||||
|
// When false, enter_alt_screen() becomes a no-op (for Zellij scrollback support)
|
||||||
|
alt_screen_enabled: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Tui {
|
impl Tui {
|
||||||
@@ -274,9 +276,15 @@ impl Tui {
|
|||||||
terminal_focused: Arc::new(AtomicBool::new(true)),
|
terminal_focused: Arc::new(AtomicBool::new(true)),
|
||||||
enhanced_keys_supported,
|
enhanced_keys_supported,
|
||||||
notification_backend: Some(detect_backend()),
|
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 {
|
pub fn frame_requester(&self) -> FrameRequester {
|
||||||
self.frame_requester.clone()
|
self.frame_requester.clone()
|
||||||
}
|
}
|
||||||
@@ -407,6 +415,9 @@ impl Tui {
|
|||||||
/// Enter alternate screen and expand the viewport to full terminal size, saving the current
|
/// Enter alternate screen and expand the viewport to full terminal size, saving the current
|
||||||
/// inline viewport for restoration when leaving.
|
/// inline viewport for restoration when leaving.
|
||||||
pub fn enter_alt_screen(&mut self) -> Result<()> {
|
pub fn enter_alt_screen(&mut self) -> Result<()> {
|
||||||
|
if !self.alt_screen_enabled {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
let _ = execute!(self.terminal.backend_mut(), EnterAlternateScreen);
|
let _ = execute!(self.terminal.backend_mut(), EnterAlternateScreen);
|
||||||
// Enable "alternate scroll" so terminals may translate wheel to arrows
|
// Enable "alternate scroll" so terminals may translate wheel to arrows
|
||||||
let _ = execute!(self.terminal.backend_mut(), EnableAlternateScroll);
|
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.
|
/// Leave alternate screen and restore the previously saved inline viewport, if any.
|
||||||
pub fn leave_alt_screen(&mut self) -> Result<()> {
|
pub fn leave_alt_screen(&mut self) -> Result<()> {
|
||||||
|
if !self.alt_screen_enabled {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
// Disable alternate scroll when leaving alt-screen
|
// Disable alternate scroll when leaving alt-screen
|
||||||
let _ = execute!(self.terminal.backend_mut(), DisableAlternateScroll);
|
let _ = execute!(self.terminal.backend_mut(), DisableAlternateScroll);
|
||||||
let _ = execute!(self.terminal.backend_mut(), LeaveAlternateScreen);
|
let _ = execute!(self.terminal.backend_mut(), LeaveAlternateScreen);
|
||||||
|
|||||||
@@ -85,6 +85,11 @@ pub struct Cli {
|
|||||||
#[arg(long = "add-dir", value_name = "DIR", value_hint = ValueHint::DirPath)]
|
#[arg(long = "add-dir", value_name = "DIR", value_hint = ValueHint::DirPath)]
|
||||||
pub add_dir: Vec<PathBuf>,
|
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)]
|
#[clap(skip)]
|
||||||
pub config_overrides: CliConfigOverrides,
|
pub config_overrides: CliConfigOverrides,
|
||||||
}
|
}
|
||||||
@@ -109,6 +114,7 @@ impl From<codex_tui::Cli> for Cli {
|
|||||||
cwd: cli.cwd,
|
cwd: cli.cwd,
|
||||||
web_search: cli.web_search,
|
web_search: cli.web_search,
|
||||||
add_dir: cli.add_dir,
|
add_dir: cli.add_dir,
|
||||||
|
no_alt_screen: cli.no_alt_screen,
|
||||||
config_overrides: cli.config_overrides,
|
config_overrides: cli.config_overrides,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ use codex_core::config::resolve_oss_provider;
|
|||||||
use codex_core::find_thread_path_by_id_str;
|
use codex_core::find_thread_path_by_id_str;
|
||||||
use codex_core::get_platform_sandbox;
|
use codex_core::get_platform_sandbox;
|
||||||
use codex_core::protocol::AskForApproval;
|
use codex_core::protocol::AskForApproval;
|
||||||
|
use codex_core::terminal::Multiplexer;
|
||||||
|
use codex_protocol::config_types::AltScreenMode;
|
||||||
use codex_protocol::config_types::SandboxMode;
|
use codex_protocol::config_types::SandboxMode;
|
||||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||||
use std::fs::OpenOptions;
|
use std::fs::OpenOptions;
|
||||||
@@ -515,12 +517,39 @@ async fn run_ratatui_app(
|
|||||||
resume_picker::ResumeSelection::StartFresh
|
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
|
// Run the main chat + transcript UI on the terminal's alternate screen so
|
||||||
// the entire viewport can be used without polluting normal scrollback. This
|
// the entire viewport can be used without polluting normal scrollback. This
|
||||||
// mirrors the behavior of the legacy TUI but keeps inline mode available
|
// mirrors the behavior of the legacy TUI but keeps inline mode available
|
||||||
// for smaller prompts like onboarding and model migration.
|
// 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 _ = tui.enter_alt_screen();
|
||||||
|
|
||||||
let app_result = App::run(
|
let app_result = App::run(
|
||||||
|
|||||||
@@ -143,6 +143,8 @@ pub struct Tui {
|
|||||||
terminal_focused: Arc<AtomicBool>,
|
terminal_focused: Arc<AtomicBool>,
|
||||||
enhanced_keys_supported: bool,
|
enhanced_keys_supported: bool,
|
||||||
notification_backend: Option<DesktopNotificationBackend>,
|
notification_backend: Option<DesktopNotificationBackend>,
|
||||||
|
// When false, enter_alt_screen() becomes a no-op (for Zellij scrollback support)
|
||||||
|
alt_screen_enabled: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Tui {
|
impl Tui {
|
||||||
@@ -170,9 +172,15 @@ impl Tui {
|
|||||||
terminal_focused: Arc::new(AtomicBool::new(true)),
|
terminal_focused: Arc::new(AtomicBool::new(true)),
|
||||||
enhanced_keys_supported,
|
enhanced_keys_supported,
|
||||||
notification_backend: Some(detect_backend()),
|
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 {
|
pub fn frame_requester(&self) -> FrameRequester {
|
||||||
self.frame_requester.clone()
|
self.frame_requester.clone()
|
||||||
}
|
}
|
||||||
@@ -309,6 +317,9 @@ impl Tui {
|
|||||||
/// Enter alternate screen and expand the viewport to full terminal size, saving the current
|
/// Enter alternate screen and expand the viewport to full terminal size, saving the current
|
||||||
/// inline viewport for restoration when leaving.
|
/// inline viewport for restoration when leaving.
|
||||||
pub fn enter_alt_screen(&mut self) -> Result<()> {
|
pub fn enter_alt_screen(&mut self) -> Result<()> {
|
||||||
|
if !self.alt_screen_enabled {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
if !self.alt_screen_nesting.enter() {
|
if !self.alt_screen_nesting.enter() {
|
||||||
self.alt_screen_active.store(true, Ordering::Relaxed);
|
self.alt_screen_active.store(true, Ordering::Relaxed);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
@@ -330,6 +341,9 @@ impl Tui {
|
|||||||
|
|
||||||
/// Leave alternate screen and restore the previously saved inline viewport, if any.
|
/// Leave alternate screen and restore the previously saved inline viewport, if any.
|
||||||
pub fn leave_alt_screen(&mut self) -> Result<()> {
|
pub fn leave_alt_screen(&mut self) -> Result<()> {
|
||||||
|
if !self.alt_screen_enabled {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
if !self.alt_screen_nesting.leave() {
|
if !self.alt_screen_nesting.leave() {
|
||||||
self.alt_screen_active
|
self.alt_screen_active
|
||||||
.store(self.alt_screen_nesting.is_active(), Ordering::Relaxed);
|
.store(self.alt_screen_nesting.is_active(), Ordering::Relaxed);
|
||||||
|
|||||||
130
docs/tui-alternate-screen.md
Normal file
130
docs/tui-alternate-screen.md
Normal file
@@ -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
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user