From 724af9494a13fadbec393e69f59948dded04e312 Mon Sep 17 00:00:00 2001 From: Hunter Bown Date: Fri, 8 May 2026 02:30:57 -0500 Subject: [PATCH] fix(tui): keep interactive sessions in alternate screen (#1158) --- crates/cli/src/lib.rs | 9 ++++----- crates/tui/src/main.rs | 32 +++++++++----------------------- crates/tui/src/tui/ui.rs | 6 ++++-- crates/tui/src/tui/ui/tests.rs | 8 +++++++- docs/CONFIGURATION.md | 2 +- docs/MODES.md | 1 - 6 files changed, 25 insertions(+), 33 deletions(-) diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs index 6d1a1640..31f186ec 100644 --- a/crates/cli/src/lib.rs +++ b/crates/cli/src/lib.rs @@ -84,7 +84,7 @@ struct Cli { api_key: Option, #[arg(long)] base_url: Option, - #[arg(long = "no-alt-screen")] + #[arg(long = "no-alt-screen", hide = true)] no_alt_screen: bool, #[arg(long = "mouse-capture", conflicts_with = "no_mouse_capture")] mouse_capture: bool, @@ -1356,9 +1356,9 @@ fn build_tui_command( if let Some(profile) = cli.profile.as_ref() { cmd.arg("--profile").arg(profile); } - if cli.no_alt_screen { - cmd.arg("--no-alt-screen"); - } + // Accepted for older scripts, but no longer forwarded: the interactive TUI + // always owns the alternate screen to avoid host scrollback hijacking. + let _ = cli.no_alt_screen; if cli.mouse_capture { cmd.arg("--mouse-capture"); } @@ -2569,7 +2569,6 @@ mod tests { "--api-key", "--approval-policy", "--sandbox-mode", - "--no-alt-screen", "--mouse-capture", "--no-mouse-capture", "--skip-onboarding", diff --git a/crates/tui/src/main.rs b/crates/tui/src/main.rs index d6b3f7aa..2ab4f2de 100644 --- a/crates/tui/src/main.rs +++ b/crates/tui/src/main.rs @@ -145,8 +145,9 @@ struct Cli { #[arg(short = 'c', long = "continue")] continue_session: bool, - /// Disable the alternate screen buffer (inline mode) - #[arg(long = "no-alt-screen")] + /// Deprecated compatibility flag; the interactive TUI always owns the + /// alternate screen so terminal scrollback cannot hijack the viewport. + #[arg(long = "no-alt-screen", hide = true)] no_alt_screen: bool, /// Enable TUI mouse capture for internal scrolling and transcript selection @@ -3623,23 +3624,8 @@ fn parse_sandbox_policy( } } -fn should_use_alt_screen(cli: &Cli, config: &Config) -> bool { - if cli.no_alt_screen { - return false; - } - - let mode = config - .tui - .as_ref() - .and_then(|tui| tui.alternate_screen.as_deref()) - .unwrap_or("auto") - .to_ascii_lowercase(); - - match mode.as_str() { - "always" => true, - "never" => false, - _ => true, - } +fn should_use_alt_screen(_cli: &Cli, _config: &Config) -> bool { + true } fn should_use_mouse_capture(cli: &Cli, config: &Config, use_alt_screen: bool) -> bool { @@ -4590,15 +4576,15 @@ mod terminal_mode_tests { } #[test] - fn no_alt_screen_flag_disables_alternate_screen() { + fn no_alt_screen_flag_is_accepted_but_keeps_alternate_screen() { let cli = parse_cli(&["deepseek", "--no-alt-screen"]); let config = Config::default(); - assert!(!should_use_alt_screen(&cli, &config)); + assert!(should_use_alt_screen(&cli, &config)); } #[test] - fn config_can_disable_alternate_screen() { + fn config_never_is_accepted_but_keeps_alternate_screen() { let cli = parse_cli(&["deepseek"]); let config = Config { tui: Some(crate::config::TuiConfig { @@ -4612,7 +4598,7 @@ mod terminal_mode_tests { ..Config::default() }; - assert!(!should_use_alt_screen(&cli, &config)); + assert!(should_use_alt_screen(&cli, &config)); } #[test] diff --git a/crates/tui/src/tui/ui.rs b/crates/tui/src/tui/ui.rs index ae7dece8..104e5a74 100644 --- a/crates/tui/src/tui/ui.rs +++ b/crates/tui/src/tui/ui.rs @@ -135,7 +135,7 @@ const SIDEBAR_VISIBLE_MIN_WIDTH: u16 = 100; const DEFAULT_TERMINAL_PROBE_TIMEOUT_MS: u64 = 500; type AppTerminal = Terminal>; -const TERMINAL_ORIGIN_RESET: &[u8] = b"\x1b[r\x1b[?6l\x1b[H\x1b[2J"; +const TERMINAL_ORIGIN_RESET: &[u8] = b"\x1b[r\x1b[?6l\x1b[H\x1b[2J\x1b[3J"; /// Run the interactive TUI event loop. /// @@ -6291,7 +6291,9 @@ fn reset_terminal_viewport(terminal: &mut AppTerminal) -> Result<()> { // Reset scroll margins and origin mode before clearing. Some interactive // child processes leave DECSTBM/DECOM behind; if ratatui's diff renderer // then writes "row 0", terminals can place it relative to the leaked - // scroll region and the whole viewport appears shifted down. + // scroll region and the whole viewport appears shifted down. CSI 3J also + // erases saved scrollback so a focus/resize recapture cannot leave the + // host terminal's scrollbar above the live TUI. terminal.backend_mut().write_all(TERMINAL_ORIGIN_RESET)?; terminal.backend_mut().flush()?; terminal.clear()?; diff --git a/crates/tui/src/tui/ui/tests.rs b/crates/tui/src/tui/ui/tests.rs index 71f630af..ff163cc2 100644 --- a/crates/tui/src/tui/ui/tests.rs +++ b/crates/tui/src/tui/ui/tests.rs @@ -46,9 +46,15 @@ fn terminal_origin_reset_resets_scroll_region_origin_and_clears() { "must reset scroll margins and origin mode before repaint" ); assert!( - TERMINAL_ORIGIN_RESET.ends_with(b"\x1b[H\x1b[2J"), + TERMINAL_ORIGIN_RESET.ends_with(b"\x1b[H\x1b[2J\x1b[3J"), "must home the cursor and clear the viewport" ); + assert!( + TERMINAL_ORIGIN_RESET + .windows(b"\x1b[3J".len()) + .any(|sequence| sequence == b"\x1b[3J"), + "must erase saved scrollback when reclaiming the viewport" + ); } #[test] diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 9a0b5bb4..2323f5c8 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -452,7 +452,7 @@ If you are upgrading from older releases: - `[notifications].include_summary` (bool, optional): defaults to `false`. When `true`, the notification body includes the elapsed duration and the turn's cost in the configured display currency. -- `tui.alternate_screen` (string, optional): `auto`, `always`, or `never`. `auto` and `always` use the TUI-owned alternate screen so transcript scrolling stays inside the app; `--no-alt-screen` forces inline mode. Set `never` or run with `--no-alt-screen` only when you intentionally want real terminal scrollback. +- `tui.alternate_screen` (string, optional): `auto`, `always`, or `never`. This is retained for config compatibility, but interactive sessions now always use the TUI-owned alternate screen so host terminal scrollback cannot hijack the viewport. - `tui.mouse_capture` (bool, optional, default `true` on non-Windows terminals when the alternate screen is active; `false` on Windows and inside JetBrains JediTerm — PyCharm/IDEA/CLion/etc. — where mouse-event escapes leak into the input stream as garbled text, see #878 / #898): enable internal mouse scrolling, transcript selection, and right-click context actions. TUI-owned drag selection copies only user/assistant transcript text. Set this to `false` or run with `--no-mouse-capture` for raw terminal selection; set it to `true` or run with `--mouse-capture` to opt in anywhere it's defaulted off. - `tui.terminal_probe_timeout_ms` (int, optional, default `500`): startup terminal-mode probe timeout in milliseconds. Values are clamped to `100..=5000`; timeout emits a warning and aborts startup instead of hanging indefinitely. - `tui.osc8_links` (bool, optional, default `true`): emit OSC 8 escape sequences around URLs in transcript output so terminals that support them (iTerm2, Terminal.app 13+, Ghostty, Kitty, WezTerm, Alacritty, recent gnome-terminal/konsole) render them as Cmd+click hyperlinks. Terminals without OSC 8 support render the plain URL and ignore the escape. Set `false` for terminals that misrender the sequence; selection/clipboard output always strips the escapes. diff --git a/docs/MODES.md b/docs/MODES.md index d4755641..7ef9b6c8 100644 --- a/docs/MODES.md +++ b/docs/MODES.md @@ -83,7 +83,6 @@ Run `deepseek --help` for the canonical list. Common flags: - `-r, --resume `: resume a saved session - `-c, --continue`: resume the most recent session in this workspace - `--max-subagents `: clamp to `1..=20` -- `--no-alt-screen`: run inline without the alternate screen buffer - `--mouse-capture` / `--no-mouse-capture`: opt in or out of internal mouse scrolling, transcript selection, and right-click context actions. Mouse capture is enabled by default on non-Windows terminals so drag selection copies only user/assistant transcript text; hold Shift while dragging or use `--no-mouse-capture` for raw terminal selection. It defaults off on Windows (CMD/terminal mouse-escape spam in the prompt) and inside JetBrains JediTerm — PyCharm/IDEA/CLion/etc. — where the terminal advertises mouse support but forwards SGR mouse events as raw text (#878, #898). Use `--mouse-capture` to opt in anywhere it's defaulted off. - `--profile `: select config profile - `--config `: config file path