Skip to content

Commit 62d5258

Browse files
committed
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)
1 parent 810ebe0 commit 62d5258

File tree

7 files changed

+104
-7
lines changed

7 files changed

+104
-7
lines changed

codex-rs/core/src/config/mod.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ use crate::protocol::AskForApproval;
3232
use crate::protocol::SandboxPolicy;
3333
use codex_app_server_protocol::Tools;
3434
use codex_app_server_protocol::UserSavedConfig;
35+
use codex_protocol::config_types::AltScreenMode;
3536
use codex_protocol::config_types::ForcedLoginMethod;
3637
use codex_protocol::config_types::ReasoningSummary;
3738
use codex_protocol::config_types::SandboxMode;
@@ -236,6 +237,14 @@ pub struct Config {
236237
/// consistently to both mouse wheels and trackpads.
237238
pub tui_scroll_invert: bool,
238239

240+
/// Controls whether the TUI uses the terminal's alternate screen buffer.
241+
///
242+
/// This is the same `tui.alternate_screen` value from `config.toml` (see [`Tui`]).
243+
/// - `auto` (default): Disable alternate screen in Zellij, enable elsewhere.
244+
/// - `always`: Always use alternate screen (original behavior).
245+
/// - `never`: Never use alternate screen (inline mode, preserves scrollback).
246+
pub tui_alternate_screen: AltScreenMode,
247+
239248
/// The directory that should be treated as the current working directory
240249
/// for the session. All relative paths inside the business-logic layer are
241250
/// resolved against this path.
@@ -1418,6 +1427,11 @@ impl Config {
14181427
.as_ref()
14191428
.and_then(|t| t.scroll_wheel_like_max_duration_ms),
14201429
tui_scroll_invert: cfg.tui.as_ref().map(|t| t.scroll_invert).unwrap_or(false),
1430+
tui_alternate_screen: cfg
1431+
.tui
1432+
.as_ref()
1433+
.map(|t| t.alternate_screen)
1434+
.unwrap_or_default(),
14211435
otel: {
14221436
let t: OtelConfigToml = cfg.otel.unwrap_or_default();
14231437
let log_user_prompt = t.log_user_prompt.unwrap_or(false);
@@ -3211,6 +3225,7 @@ model_verbosity = "high"
32113225
tui_scroll_wheel_tick_detect_max_ms: None,
32123226
tui_scroll_wheel_like_max_duration_ms: None,
32133227
tui_scroll_invert: false,
3228+
tui_alternate_screen: AltScreenMode::Auto,
32143229
otel: OtelConfig::default(),
32153230
},
32163231
o3_profile_config
@@ -3294,6 +3309,7 @@ model_verbosity = "high"
32943309
tui_scroll_wheel_tick_detect_max_ms: None,
32953310
tui_scroll_wheel_like_max_duration_ms: None,
32963311
tui_scroll_invert: false,
3312+
tui_alternate_screen: AltScreenMode::Auto,
32973313
otel: OtelConfig::default(),
32983314
};
32993315

@@ -3392,6 +3408,7 @@ model_verbosity = "high"
33923408
tui_scroll_wheel_tick_detect_max_ms: None,
33933409
tui_scroll_wheel_like_max_duration_ms: None,
33943410
tui_scroll_invert: false,
3411+
tui_alternate_screen: AltScreenMode::Auto,
33953412
otel: OtelConfig::default(),
33963413
};
33973414

@@ -3476,6 +3493,7 @@ model_verbosity = "high"
34763493
tui_scroll_wheel_tick_detect_max_ms: None,
34773494
tui_scroll_wheel_like_max_duration_ms: None,
34783495
tui_scroll_invert: false,
3496+
tui_alternate_screen: AltScreenMode::Auto,
34793497
otel: OtelConfig::default(),
34803498
};
34813499

codex-rs/core/src/config/types.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// Note this file should generally be restricted to simple struct/enum
44
// definitions that do not contain business logic.
55

6+
pub use codex_protocol::config_types::AltScreenMode;
67
use codex_utils_absolute_path::AbsolutePathBuf;
78
use std::collections::BTreeMap;
89
use std::collections::HashMap;
@@ -505,6 +506,17 @@ pub struct Tui {
505506
/// wheel and trackpad input.
506507
#[serde(default)]
507508
pub scroll_invert: bool,
509+
510+
/// Controls whether the TUI uses the terminal's alternate screen buffer.
511+
///
512+
/// - `auto` (default): Disable alternate screen in Zellij, enable elsewhere.
513+
/// - `always`: Always use alternate screen (original behavior).
514+
/// - `never`: Never use alternate screen (inline mode only, preserves scrollback).
515+
///
516+
/// Using alternate screen provides a cleaner fullscreen experience but prevents
517+
/// scrollback in terminal multiplexers like Zellij that follow the xterm spec.
518+
#[serde(default)]
519+
pub alternate_screen: AltScreenMode,
508520
}
509521

510522
const fn default_true() -> bool {

codex-rs/protocol/src/config_types.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,21 @@ pub enum TrustLevel {
8080
Trusted,
8181
Untrusted,
8282
}
83+
84+
/// Controls whether the TUI uses the terminal's alternate screen buffer.
85+
///
86+
/// Using alternate screen provides a cleaner fullscreen experience but prevents
87+
/// scrollback in terminal multiplexers like Zellij that follow the xterm spec
88+
/// (which says alternate screen should not have scrollback).
89+
#[derive(Debug, Serialize, Deserialize, Default, Clone, Copy, PartialEq, Eq, Display, JsonSchema, TS)]
90+
#[serde(rename_all = "lowercase")]
91+
#[strum(serialize_all = "lowercase")]
92+
pub enum AltScreenMode {
93+
/// Auto-detect: disable alternate screen in Zellij, enable elsewhere.
94+
#[default]
95+
Auto,
96+
/// Always use alternate screen (original behavior).
97+
Always,
98+
/// Never use alternate screen (inline mode only).
99+
Never,
100+
}

codex-rs/tui/src/cli.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,11 @@ pub struct Cli {
8585
#[arg(long = "add-dir", value_name = "DIR", value_hint = ValueHint::DirPath)]
8686
pub add_dir: Vec<PathBuf>,
8787

88+
/// Disable alternate screen mode for better scrollback in terminal multiplexers like Zellij.
89+
/// This runs the TUI in inline mode, preserving terminal scrollback history.
90+
#[arg(long = "no-alt-screen", default_value_t = false)]
91+
pub no_alt_screen: bool,
92+
8893
#[clap(skip)]
8994
pub config_overrides: CliConfigOverrides,
9095
}

codex-rs/tui2/src/cli.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,11 @@ pub struct Cli {
8585
#[arg(long = "add-dir", value_name = "DIR", value_hint = ValueHint::DirPath)]
8686
pub add_dir: Vec<PathBuf>,
8787

88+
/// Disable alternate screen mode for better scrollback in terminal multiplexers like Zellij.
89+
/// This runs the TUI in inline mode, preserving terminal scrollback history.
90+
#[arg(long = "no-alt-screen", default_value_t = false)]
91+
pub no_alt_screen: bool,
92+
8893
#[clap(skip)]
8994
pub config_overrides: CliConfigOverrides,
9095
}
@@ -109,6 +114,7 @@ impl From<codex_tui::Cli> for Cli {
109114
cwd: cli.cwd,
110115
web_search: cli.web_search,
111116
add_dir: cli.add_dir,
117+
no_alt_screen: cli.no_alt_screen,
112118
config_overrides: cli.config_overrides,
113119
}
114120
}

codex-rs/tui2/src/lib.rs

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ use codex_core::config::resolve_oss_provider;
2222
use codex_core::find_conversation_path_by_id_str;
2323
use codex_core::get_platform_sandbox;
2424
use codex_core::protocol::AskForApproval;
25+
use codex_core::terminal::Multiplexer;
26+
use codex_protocol::config_types::AltScreenMode;
2527
use codex_protocol::config_types::SandboxMode;
2628
use codex_utils_absolute_path::AbsolutePathBuf;
2729
use std::fs::OpenOptions;
@@ -514,13 +516,34 @@ async fn run_ratatui_app(
514516
resume_picker::ResumeSelection::StartFresh
515517
};
516518

517-
let Cli { prompt, images, .. } = cli;
519+
let Cli {
520+
prompt,
521+
images,
522+
no_alt_screen,
523+
..
524+
} = cli;
525+
526+
// Determine whether to use alternate screen based on CLI flag and config.
527+
// Alternate screen provides a cleaner fullscreen experience but prevents
528+
// scrollback in terminal multiplexers like Zellij that follow xterm spec.
529+
let use_alt_screen = if no_alt_screen {
530+
// CLI flag explicitly disables alternate screen
531+
false
532+
} else {
533+
match config.tui_alternate_screen {
534+
AltScreenMode::Always => true,
535+
AltScreenMode::Never => false,
536+
AltScreenMode::Auto => {
537+
// Auto-detect: disable in Zellij, enable elsewhere
538+
let terminal_info = codex_core::terminal::terminal_info();
539+
!matches!(terminal_info.multiplexer, Some(Multiplexer::Zellij { .. }))
540+
}
541+
}
542+
};
518543

519-
// Run the main chat + transcript UI on the terminal's alternate screen so
520-
// the entire viewport can be used without polluting normal scrollback. This
521-
// mirrors the behavior of the legacy TUI but keeps inline mode available
522-
// for smaller prompts like onboarding and model migration.
523-
let _ = tui.enter_alt_screen();
544+
if use_alt_screen {
545+
let _ = tui.enter_alt_screen();
546+
}
524547

525548
let app_result = App::run(
526549
&mut tui,
@@ -535,7 +558,9 @@ async fn run_ratatui_app(
535558
)
536559
.await;
537560

538-
let _ = tui.leave_alt_screen();
561+
if use_alt_screen {
562+
let _ = tui.leave_alt_screen();
563+
}
539564
restore();
540565
if let Ok(exit_info) = &app_result {
541566
let mut stdout = std::io::stdout();

docs/config.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -936,6 +936,18 @@ scroll_wheel_like_max_duration_ms = 200
936936

937937
# Invert scroll direction for mouse wheel/trackpad. TUI2 only.
938938
scroll_invert = false
939+
940+
# Alternate screen mode. TUI2 only.
941+
# Controls whether the TUI uses the terminal's alternate screen buffer.
942+
# Valid values: "auto" (default), "always", "never"
943+
#
944+
# - "auto": Disable alternate screen in Zellij (which follows xterm spec and
945+
# doesn't support scrollback in alternate screen), enable elsewhere.
946+
# - "always": Always use alternate screen (original behavior).
947+
# - "never": Never use alternate screen (inline mode, preserves scrollback).
948+
#
949+
# You can also use the --no-alt-screen CLI flag to force "never" mode.
950+
alternate_screen = "auto"
939951
```
940952

941953
> [!NOTE]
@@ -1053,6 +1065,7 @@ Valid values:
10531065
| `tui.scroll_wheel_tick_detect_max_ms` | number | Auto-mode threshold (ms) for promoting a stream to wheel-like behavior (default: 12). |
10541066
| `tui.scroll_wheel_like_max_duration_ms` | number | Auto-mode fallback duration (ms) used for 1-event-per-tick terminals (default: 200). |
10551067
| `tui.scroll_invert` | boolean | Invert mouse scroll direction in TUI2 (default: false). |
1068+
| `tui.alternate_screen` | `auto` \| `always` \| `never` | Alternate screen mode in TUI2 (default: `auto`). Use `never` or `--no-alt-screen` for Zellij scrollback. |
10561069
| `hide_agent_reasoning` | boolean | Hide model reasoning events. |
10571070
| `check_for_update_on_startup` | boolean | Check for Codex updates on startup (default: true). Set to `false` only if updates are centrally managed. |
10581071
| `show_raw_agent_reasoning` | boolean | Show raw reasoning (when available). |

0 commit comments

Comments
 (0)