From a2de8fedcc3abe3cdde09b43515db820fb6b95b5 Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Sun, 20 Sep 2026 16:44:23 +0000 Subject: [PATCH] Move fullscreen transcript control to TUI configuration (#46849) ## What changed Use `tui.fullscreen_transcript` to opt into the fullscreen transcript, including scrolling, selection, and search. It defaults to `false`; `--no-alt-screen` and `tui.alternate_screen = "never"` still take precedence. The new preference controls both the first frame and application startup. Boolean CLI overrides for it are allowed during daemon startup. The old `features.transcript_v2` flag is deprecated and ignored, with a notice directing users to the new setting; it does not migrate into or override the preference. ## Testing Add coverage for default terminal scrollback, fullscreen persistence through startup, alternate-screen restrictions, daemon override validation, and independence from the deprecated flag. Update existing history and activity tests to use the new preference. GitOrigin-RevId: 4117bf1ee548fa8d101a594f9506673d87c74c15 --- codex-rs/cli/tests/features.rs | 2 +- codex-rs/config/src/types.rs | 5 ++++ codex-rs/core/config.schema.json | 5 ++++ codex-rs/core/src/config/config_tests.rs | 2 ++ codex-rs/core/src/config/mod.rs | 7 +++++ codex-rs/features/src/lib.rs | 12 ++++++-- codex-rs/features/src/tests.rs | 9 ------ codex-rs/thread-manager-sample/src/main.rs | 1 + codex-rs/tui/src/app/startup.rs | 2 +- .../src/app/tests/history_hydration_tests.rs | 4 +-- .../src/app_server_session/history_tests.rs | 11 ++----- .../tests/dynamic_activity_tests.rs | 10 ++----- codex-rs/tui/src/daemon_startup.rs | 5 +++- codex-rs/tui/src/daemon_startup_tests.rs | 6 ++++ codex-rs/tui/src/lib.rs | 2 +- codex-rs/tui/src/local_settings.rs | 4 +-- codex-rs/tui/src/local_settings_tests.rs | 27 +++++++++++++++-- codex-rs/tui/src/startup_presentation.rs | 29 +++---------------- .../tui/src/startup_presentation_tests.rs | 9 +++--- codex-rs/tui/tests/suite/focus_palette.rs | 29 +++++++++++++++---- 20 files changed, 106 insertions(+), 75 deletions(-) diff --git a/codex-rs/cli/tests/features.rs b/codex-rs/cli/tests/features.rs index 6a0c5603fc..4beebb0d3f 100644 --- a/codex-rs/cli/tests/features.rs +++ b/codex-rs/cli/tests/features.rs @@ -133,7 +133,7 @@ fn strict_config_is_not_supported_for_cloud_command() -> Result<()> { async fn features_enable_writes_feature_flag_to_config() -> Result<()> { let codex_home = TempDir::new()?; - for feature in ["unified_exec", "transcript_v2"] { + for feature in ["unified_exec", "shell_tool"] { let mut cmd = codex_command(codex_home.path())?; cmd.args(["features", "enable", feature]) .assert() diff --git a/codex-rs/config/src/types.rs b/codex-rs/config/src/types.rs index fc77dc0495..a03851e4d5 100644 --- a/codex-rs/config/src/types.rs +++ b/codex-rs/config/src/types.rs @@ -793,6 +793,11 @@ pub struct Tui { #[serde(default)] pub raw_output_mode: bool, + /// Own the fullscreen transcript, including scrolling, selection, and search. + /// Defaults to `false`; alternate-screen restrictions take precedence. + #[serde(default)] + pub fullscreen_transcript: bool, + /// Controls whether the TUI uses the terminal's alternate screen buffer. /// /// - `auto` (default): Use alternate screen. diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index 90b8c944e9..f3138968a7 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -4226,6 +4226,11 @@ }, "description": "Individual visual effects. Each also requires animations to be enabled." }, + "fullscreen_transcript": { + "default": false, + "description": "Own the fullscreen transcript, including scrolling, selection, and search. Defaults to `false`; alternate-screen restrictions take precedence.", + "type": "boolean" + }, "keymap": { "allOf": [ { diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index b4277cd2cc..4af6772d10 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -1261,6 +1261,7 @@ fn config_toml_deserializes_model_availability_nux() { vim_mode_default: false, question_esc_back: true, raw_output_mode: false, + fullscreen_transcript: false, alternate_screen: AltScreenMode::default(), status_line: None, status_line_use_colors: true, @@ -4388,6 +4389,7 @@ fn tui_config_missing_notifications_field_defaults_to_enabled() { vim_mode_default: false, question_esc_back: true, raw_output_mode: false, + fullscreen_transcript: false, alternate_screen: AltScreenMode::Auto, status_line: None, status_line_use_colors: true, diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 2bc67b91b5..7b6ec2d393 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -770,6 +770,9 @@ pub struct Config { /// Start the TUI in raw scrollback mode for copy-friendly transcript output. pub tui_raw_output_mode: bool, + /// Own the fullscreen transcript when the alternate screen is enabled. + pub tui_fullscreen_transcript: bool, + /// Start the TUI in the specified collaboration mode (plan/default). /// Controls whether the TUI uses the terminal's alternate screen buffer. @@ -4406,6 +4409,10 @@ impl Config { .as_ref() .map(|t| t.raw_output_mode) .unwrap_or(false), + tui_fullscreen_transcript: cfg + .tui + .as_ref() + .is_some_and(|tui| tui.fullscreen_transcript), tui_alternate_screen: cfg .tui .as_ref() diff --git a/codex-rs/features/src/lib.rs b/codex-rs/features/src/lib.rs index 83337a6c89..ad635f05e9 100644 --- a/codex-rs/features/src/lib.rs +++ b/codex-rs/features/src/lib.rs @@ -95,7 +95,7 @@ pub enum Feature { AnalyticsPlanHistory, /// Discover model catalogs for OpenAI API-key authentication. ApiKeyModelDiscovery, - /// Enable the interactive transcript composer and turn-selection UI. + /// Deprecated no-op; use `tui.fullscreen_transcript` instead. TranscriptV2, // Stable. /// Enable the default shell tool. @@ -592,6 +592,10 @@ impl Features { Feature::WebSearchCached, ); } + "transcript_v2" => { + self.record_legacy_usage_force("features.transcript_v2", Feature::TranscriptV2); + continue; + } "tui_app_server" => { continue; } @@ -696,6 +700,10 @@ impl Features { fn legacy_usage_notice(alias: &str, feature: Feature) -> (String, Option) { let canonical = feature.key(); match feature { + Feature::TranscriptV2 => ( + "`[features].transcript_v2` is deprecated and ignored.".to_string(), + Some("Use `[tui].fullscreen_transcript` in config.toml instead.".to_string()), + ), Feature::WebSearchRequest | Feature::WebSearchCached => { let label = match alias { "web_search" => "[features].web_search", @@ -937,7 +945,7 @@ pub const FEATURES: &[FeatureSpec] = &[ FeatureSpec { id: Feature::TranscriptV2, key: "transcript_v2", - stage: Stage::UnderDevelopment, + stage: Stage::Deprecated, default_enabled: false, }, // Stable features. diff --git a/codex-rs/features/src/tests.rs b/codex-rs/features/src/tests.rs index 52437315dd..da6cb3c822 100644 --- a/codex-rs/features/src/tests.rs +++ b/codex-rs/features/src/tests.rs @@ -14,15 +14,6 @@ use std::collections::BTreeMap; use toml::Table; use toml::Value as TomlValue; -#[test] -fn transcript_v2_resolves_explicit_config_overrides() { - let mut features = Features::with_defaults(); - for enabled in [false, true, false] { - features.apply_map(&BTreeMap::from([("transcript_v2".to_string(), enabled)])); - assert_eq!(features.enabled(Feature::TranscriptV2), enabled); - } -} - #[test] fn sleep_tool_config_rejects_unknown_mode() { assert!(toml::from_str::("[sleep_tool]\nmode = 'off'").is_err()); diff --git a/codex-rs/thread-manager-sample/src/main.rs b/codex-rs/thread-manager-sample/src/main.rs index 55d29288ca..f6ac386fb7 100644 --- a/codex-rs/thread-manager-sample/src/main.rs +++ b/codex-rs/thread-manager-sample/src/main.rs @@ -232,6 +232,7 @@ fn new_config(model: Option, arg0_paths: Arg0DispatchPaths) -> anyhow::R tui_show_server_version_notice: true, tui_auto_recap: true, model_availability_nux: ModelAvailabilityNuxConfig::default(), + tui_fullscreen_transcript: false, tui_alternate_screen: AltScreenMode::Auto, tui_status_line: None, tui_status_line_use_colors: true, diff --git a/codex-rs/tui/src/app/startup.rs b/codex-rs/tui/src/app/startup.rs index efb4e8235a..867561454a 100644 --- a/codex-rs/tui/src/app/startup.rs +++ b/codex-rs/tui/src/app/startup.rs @@ -194,7 +194,7 @@ impl App { } // Adopt actual launch ownership before constructing session-local preferences. - tui.prepare_owned_screen(config.features.enabled(Feature::TranscriptV2))?; + tui.prepare_owned_screen(config.tui_fullscreen_transcript)?; let mut local_settings = crate::local_settings::LocalSettings::for_tui(&config, tui); let startup_started_at = Instant::now(); let (app_event_tx, mut app_event_rx) = unbounded_channel(); diff --git a/codex-rs/tui/src/app/tests/history_hydration_tests.rs b/codex-rs/tui/src/app/tests/history_hydration_tests.rs index 8e844a1258..9f28aecc6f 100644 --- a/codex-rs/tui/src/app/tests/history_hydration_tests.rs +++ b/codex-rs/tui/src/app/tests/history_hydration_tests.rs @@ -247,9 +247,7 @@ async fn history_hydration_archived_retry_uses_first_attempt_runtime_settings() ] { for mode in [TranscriptMode::Terminal, TranscriptMode::Owned] { let (mut app, codex_home, mut target) = history_fixture(&[500]).await?; - app.config - .features - .set_enabled(Feature::TranscriptV2, !mode.is_owned())?; + app.config.tui_fullscreen_transcript = !mode.is_owned(); app.config.tui_alternate_screen = codex_config::types::AltScreenMode::Always; app.config.terminal_resize_reflow.max_rows = TerminalResizeReflowMaxRows::Limit(1); app.local_settings = crate::local_settings::LocalSettings::from(&app.config); diff --git a/codex-rs/tui/src/app_server_session/history_tests.rs b/codex-rs/tui/src/app_server_session/history_tests.rs index 4818985e64..0f12c95a81 100644 --- a/codex-rs/tui/src/app_server_session/history_tests.rs +++ b/codex-rs/tui/src/app_server_session/history_tests.rs @@ -5,7 +5,6 @@ use crate::legacy_core::config::ConfigBuilder; use crate::legacy_core::config::TerminalResizeReflowMaxRows; use crate::local_settings::LocalSettings; use codex_config::types::AltScreenMode; -use codex_features::Feature; use pretty_assertions::assert_eq; use std::collections::HashSet; @@ -42,10 +41,7 @@ async fn owned_initial_history_stops_after_viewport_or_scan_budget() { .build() .await .expect("config"); - config - .features - .enable(Feature::TranscriptV2) - .expect("enable owned transcript"); + config.tui_fullscreen_transcript = true; config.tui_alternate_screen = AltScreenMode::Always; config.terminal_resize_reflow.max_rows = TerminalResizeReflowMaxRows::Disabled; let local_settings = LocalSettings::from(&config); @@ -78,10 +74,7 @@ async fn owned_initial_history_stops_after_viewport_or_scan_budget() { Some(100), ); - config - .features - .disable(Feature::TranscriptV2) - .expect("disable owned transcript"); + config.tui_fullscreen_transcript = false; // The mode selected at launch wins even if thread config changes or is unavailable. for config in [Some(&config), None] { let budget = HistoryLoadBudget::new( diff --git a/codex-rs/tui/src/chatwidget/tests/dynamic_activity_tests.rs b/codex-rs/tui/src/chatwidget/tests/dynamic_activity_tests.rs index d528070c66..cbb2e27d64 100644 --- a/codex-rs/tui/src/chatwidget/tests/dynamic_activity_tests.rs +++ b/codex-rs/tui/src/chatwidget/tests/dynamic_activity_tests.rs @@ -37,10 +37,7 @@ async fn terminal_dynamic_activity_retains_calls_across_both_fallbacks() { [(false, AltScreenMode::Always), (true, AltScreenMode::Never)] { let (mut chat, mut rx, _ops) = make_chatwidget_manual(/*model_override*/ None).await; - chat.config - .features - .set_enabled(Feature::TranscriptV2, owned_enabled) - .expect("configure transcript feature"); + chat.config.tui_fullscreen_transcript = owned_enabled; chat.config.tui_alternate_screen = alternate_screen; chat.local_settings = LocalSettings::from(&chat.config); assert_eq!( @@ -89,10 +86,7 @@ async fn owned_dynamic_activity_updates_the_retained_row_after_config_refresh() let (mut chat, mut rx, _ops) = make_chatwidget_manual(/*model_override*/ None).await; chat.local_settings.transcript_mode = TranscriptMode::Owned; // A thread/config refresh cannot change ownership after the terminal starts. - chat.config - .features - .disable(Feature::TranscriptV2) - .expect("disable transcript feature in refreshed config"); + chat.config.tui_fullscreen_transcript = false; drain_insert_history(&mut rx); chat.on_dynamic_tool_item(dynamic_item("call-1", DynamicToolCallStatus::InProgress)); let retained = std::iter::from_fn(|| rx.try_recv().ok()) diff --git a/codex-rs/tui/src/daemon_startup.rs b/codex-rs/tui/src/daemon_startup.rs index c4164e808b..0e76635c3b 100644 --- a/codex-rs/tui/src/daemon_startup.rs +++ b/codex-rs/tui/src/daemon_startup.rs @@ -52,7 +52,10 @@ pub(super) fn config_exclusion( if !cli_kv_overrides .iter() .all(|(key, value)| match key.as_str() { - "suppress_unstable_features_warning" => value.is_bool(), + "suppress_unstable_features_warning" | "tui.fullscreen_transcript" => value.is_bool(), + "tui" => value.as_table().is_some_and(|tui| { + tui.len() == 1 && tui.get("fullscreen_transcript").is_some_and(toml::Value::is_bool) + }), "features" => value.as_table().is_some_and(|features| { !features.is_empty() && features diff --git a/codex-rs/tui/src/daemon_startup_tests.rs b/codex-rs/tui/src/daemon_startup_tests.rs index 28a0bf9fb7..15c62b7d08 100644 --- a/codex-rs/tui/src/daemon_startup_tests.rs +++ b/codex-rs/tui/src/daemon_startup_tests.rs @@ -27,6 +27,12 @@ fn audited_overrides_allow_daemon_without_allowing_arbitrary_config() { ("features={worktrees=true,shell_tool=false}", false), ("features.shell_tool=false", false), ("features.worktrees.enabled=true", false), + ("tui.fullscreen_transcript=true", true), + ("tui.fullscreen_transcript=false", true), + ("tui.fullscreen_transcript=\"true\"", false), + ("tui={fullscreen_transcript=true}", true), + ("tui={fullscreen_transcript='true'}", false), + ("tui={fullscreen_transcript=true,animations=false}", false), ("features={}", false), ("model='test'", false), ] { diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index e602ca2018..4e2490e5fb 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -1843,7 +1843,7 @@ async fn run_ratatui_app( // Cloud configuration and session selection can change screen policy after first paint. let use_alt_screen = determine_alt_screen_mode(cli.no_alt_screen, config.tui_alternate_screen); let mode = crate::transcript_mode::TranscriptMode::resolve( - config.features.enabled(Feature::TranscriptV2), + config.tui_fullscreen_transcript, use_alt_screen, ); if use_alt_screen != tui.is_alt_screen_enabled() || mode.is_owned() != tui.is_owned_screen() { diff --git a/codex-rs/tui/src/local_settings.rs b/codex-rs/tui/src/local_settings.rs index 7723e8d521..f60e5f5d8f 100644 --- a/codex-rs/tui/src/local_settings.rs +++ b/codex-rs/tui/src/local_settings.rs @@ -13,7 +13,6 @@ use crate::transcript_mode::TranscriptMode; use codex_config::types::History; use codex_config::types::Notice; use codex_config::types::Tui; -use codex_features::Feature; use codex_utils_absolute_path::AbsolutePathBuf; #[derive(Clone, Debug, PartialEq)] @@ -56,7 +55,7 @@ impl LocalSettings { }; Self { transcript_mode: TranscriptMode::resolve( - config.features.enabled(Feature::TranscriptV2), + config.tui_fullscreen_transcript, config.tui_alternate_screen != codex_config::types::AltScreenMode::Never, ), tui: Tui { @@ -71,6 +70,7 @@ impl LocalSettings { vim_mode_default: config.tui_vim_mode_default, question_esc_back: config.tui_question_esc_back, raw_output_mode: config.tui_raw_output_mode, + fullscreen_transcript: config.tui_fullscreen_transcript, alternate_screen: config.tui_alternate_screen, status_line: config.tui_status_line.clone(), status_line_use_colors: config.tui_status_line_use_colors, diff --git a/codex-rs/tui/src/local_settings_tests.rs b/codex-rs/tui/src/local_settings_tests.rs index 12aae2c516..cef866fbd4 100644 --- a/codex-rs/tui/src/local_settings_tests.rs +++ b/codex-rs/tui/src/local_settings_tests.rs @@ -16,7 +16,7 @@ async fn launch_screen_mode_survives_configuration_reload() -> anyhow::Result<() .loader_overrides(LoaderOverrides::without_managed_config_for_tests()) .build() .await?; - config.features.enable(Feature::TranscriptV2)?; + config.tui_fullscreen_transcript = true; config.tui_alternate_screen = AltScreenMode::Auto; for (alternate_screen, owned, expected_mode, expected_alt) in [ @@ -34,7 +34,7 @@ async fn launch_screen_mode_survives_configuration_reload() -> anyhow::Result<() ); let mut reloaded_config = config.clone(); - reloaded_config.features.disable(Feature::TranscriptV2)?; + reloaded_config.tui_fullscreen_transcript = false; reloaded_config.tui_alternate_screen = AltScreenMode::Never; reloaded_config.tui_theme = Some("nord".into()); let mut expected = LocalSettings::from(&reloaded_config); @@ -98,6 +98,7 @@ whimsy = false # Retired: must not override effects or prevent strict loading. show_tooltips = false show_server_version_notice = false auto_recap = false +fullscreen_transcript = true vim_mode_default = true terminal_resize_reflow_max_rows = 0 session_picker_view = "comfortable" @@ -119,7 +120,14 @@ fast_default_opt_out = true ignore_project_config: true, ..LoaderOverrides::without_managed_config_for_tests() }) - .cli_overrides(vec![("tui.disable_paste_burst".into(), true.into())]) + .cli_overrides(vec![ + ("tui.disable_paste_burst".into(), true.into()), + // The deprecated flag must not override or migrate into the TUI preference. + ( + "features.transcript_v2".into(), + config_text.is_empty().into(), + ), + ]) .build() .await?; assert_eq!(config.startup_warnings, Vec::::new()); @@ -133,11 +141,24 @@ fast_default_opt_out = true expected.show_tooltips = false; expected.show_server_version_notice = false; expected.auto_recap = false; + expected.fullscreen_transcript = true; expected.vim_mode_default = true; expected.terminal_resize_reflow_max_rows = Some(0); expected.session_picker_view = Some(SessionPickerViewMode::Comfortable); } + assert_eq!( + local.transcript_mode.is_owned(), + expected.fullscreen_transcript + ); assert_eq!(local.tui, expected); + assert_eq!( + config + .features + .legacy_feature_usages() + .map(|usage| usage.alias.as_str()) + .collect::>(), + vec!["features.transcript_v2"], + ); assert_eq!( local.terminal_resize_reflow(), config.terminal_resize_reflow diff --git a/codex-rs/tui/src/startup_presentation.rs b/codex-rs/tui/src/startup_presentation.rs index 5ce74e4634..f332ef09c1 100644 --- a/codex-rs/tui/src/startup_presentation.rs +++ b/codex-rs/tui/src/startup_presentation.rs @@ -12,16 +12,11 @@ use codex_config::CloudConfigBundleLoader; use codex_config::ConfigLoadOptions; use codex_config::LoaderOverrides; use codex_config::TomlValue; -use codex_features::Feature; -use codex_features::FeatureConfigSource; -use codex_features::FeatureOverrides; -use codex_features::Features; use codex_utils_absolute_path::AbsolutePathBuf; use crate::Cli; use crate::keymap::RuntimeKeymap; use crate::legacy_core::config::ConfigTomlLoadResult; -use crate::legacy_core::config::ManagedFeatures; use crate::legacy_core::config::load_config_toml_with_layer_stack; use crate::startup_draft::StartupScreen; @@ -56,25 +51,6 @@ pub(super) async fn load( ) .await?; let config_toml = &bootstrap_config.config_toml; - let configured_features = Features::from_sources( - FeatureConfigSource { - features: config_toml.features.as_ref(), - experimental_use_unified_exec_tool: config_toml.experimental_use_unified_exec_tool, - }, - FeatureConfigSource::default(), - FeatureOverrides::default(), - ); - // Full configuration construction will publish its normal managed-feature warnings once. - let mut startup_warnings = Vec::new(); - let features = ManagedFeatures::from_configured_with_warnings( - configured_features, - bootstrap_config - .config_layer_stack - .requirements() - .feature_requirements - .clone(), - &mut startup_warnings, - )?; let alternate_screen = config_toml .tui .as_ref() @@ -82,7 +58,10 @@ pub(super) async fn load( .unwrap_or_default(); let use_alt_screen = crate::determine_alt_screen_mode(cli.no_alt_screen, alternate_screen); let transcript_mode = crate::transcript_mode::TranscriptMode::resolve( - features.enabled(Feature::TranscriptV2), + config_toml + .tui + .as_ref() + .is_some_and(|tui| tui.fullscreen_transcript), use_alt_screen, ); let status_line_enabled = config_toml diff --git a/codex-rs/tui/src/startup_presentation_tests.rs b/codex-rs/tui/src/startup_presentation_tests.rs index 5cf50dcaba..b1cc9157f2 100644 --- a/codex-rs/tui/src/startup_presentation_tests.rs +++ b/codex-rs/tui/src/startup_presentation_tests.rs @@ -50,23 +50,24 @@ async fn selected_profile_controls_submit_before_the_first_frame() -> anyhow::Re async fn first_frame_respects_screen_and_status_line_overrides() -> anyhow::Result<()> { let home = tempfile::tempdir()?; for (config, args, expected) in [ + ("", vec!["codex"], (true, false, true)), ( - "[features]\ntranscript_v2 = true\n", + "[tui]\nfullscreen_transcript = true\n", vec!["codex"], (true, true, true), ), ( - "[features]\ntranscript_v2 = true\n", + "[tui]\nfullscreen_transcript = true\n", vec!["codex", "--no-alt-screen"], (false, false, true), ), ( - "[features]\ntranscript_v2 = false\n", + "[tui]\nfullscreen_transcript = false\n", vec!["codex"], (true, false, true), ), ( - "[tui]\nalternate_screen = \"never\"\nstatus_line = []\n", + "[tui]\nfullscreen_transcript = true\nalternate_screen = \"never\"\nstatus_line = []\n", vec!["codex"], (false, false, false), ), diff --git a/codex-rs/tui/tests/suite/focus_palette.rs b/codex-rs/tui/tests/suite/focus_palette.rs index 816b8191a6..d13af5ad7b 100644 --- a/codex-rs/tui/tests/suite/focus_palette.rs +++ b/codex-rs/tui/tests/suite/focus_palette.rs @@ -161,12 +161,7 @@ fn owned_screen_entry_paints_before_sync_ends_and_exit_clears_inline_draft() -> let mut terminal = PtyCodex::start( &repo_root, codex_home, - &[ - "-c", - "tui.alternate_screen=\"always\"", - "-c", - "features.transcript_v2=true", - ], + &["-c", "tui.fullscreen_transcript=true"], )?; terminal.wait_for_startup()?; let deadline = Instant::now() + STARTUP_TIMEOUT; @@ -180,6 +175,10 @@ fn owned_screen_entry_paints_before_sync_ends_and_exit_clears_inline_draft() -> "owned screen did not open" ); terminal.wait_for_screen("GPT-5.6-Terra")?; + ensure!( + terminal.parser.screen().alternate_screen(), + "fullscreen did not survive application startup" + ); let enter_alt = b"\x1b[?1049h"; let begin_sync = b"\x1b[?2026h"; let end_sync = b"\x1b[?2026l"; @@ -252,6 +251,24 @@ fn owned_screen_entry_paints_before_sync_ends_and_exit_clears_inline_draft() -> Ok(()) } +#[test] +fn fullscreen_transcript_defaults_to_terminal_scrollback() -> Result<()> { + let repo_root = codex_utils_cargo_bin::repo_root()?; + let codex_home = tempfile::tempdir()?; + write_test_config(codex_home.path(), &repo_root)?; + let mut terminal = PtyCodex::start(&repo_root, codex_home, &[])?; + terminal.wait_for_startup()?; + terminal.wait_for_screen("GPT-5.6-Terra")?; + ensure!( + !terminal + .output + .windows(b"\x1b[?1049h".len()) + .any(|bytes| bytes == b"\x1b[?1049h"), + "default launch entered the alternate screen" + ); + Ok(()) +} + pub(super) struct PtyCodex { master: File, child: Child,