From c0e88f3c04219aaa032aaf4b20a8c3284cc81449 Mon Sep 17 00:00:00 2001 From: Felipe Coury Date: Wed, 28 Oct 2026 00:56:14 +0000 Subject: [PATCH] Disable terminal status sequences on older VTE backends (#57612) ## Why VTE versions before 0.54 lack the parser that safely ignores unknown OSC sequences. Terminal status reporting needs to account for the VTE backend even when the terminal identifies itself as GNOME Terminal or another application. ## What changed - Track VTE backend versions independently of terminal application identity. - Disable `OSC 7501` and `OSC 21337` status reporting when the detected VTE version is below 0.54. Preserve existing behavior for newer or unknown versions. - Prefer `VTE_VERSION` over the terminal application version, treating empty or malformed values as unknown. ## Testing Add regression tests for GNOME identity with VTE metadata, version precedence, and status protocol selection around the VTE 0.54 boundary, including unknown versions. GitOrigin-RevId: 1aa57901d1b1457f9971c07cb51ec4554915ae3b --- codex-rs/cli/src/doctor.rs | 1 + codex-rs/terminal-detection/src/lib.rs | 31 ++++++ .../terminal-detection/src/terminal_tests.rs | 100 ++++++++++++++---- .../tui/src/iterm_session_status_tests.rs | 48 +++++++++ codex-rs/tui/src/local_settings_tests.rs | 1 + .../src/markdown_render/web_links_tests.rs | 1 + codex-rs/tui/src/notifications/mod.rs | 1 + codex-rs/tui/src/pets/image_protocol.rs | 1 + codex-rs/tui/src/resize_reflow_cap.rs | 2 + codex-rs/tui/src/terminal_program_status.rs | 15 ++- codex-rs/tui/src/tui/scrollback_tests.rs | 1 + codex-rs/tui/src/tui/size_monitor_tests.rs | 1 + 12 files changed, 180 insertions(+), 23 deletions(-) diff --git a/codex-rs/cli/src/doctor.rs b/codex-rs/cli/src/doctor.rs index d57ba01484..18149ce607 100644 --- a/codex-rs/cli/src/doctor.rs +++ b/codex-rs/cli/src/doctor.rs @@ -4348,6 +4348,7 @@ mod tests { fn terminal_inputs() -> TerminalCheckInputs { TerminalCheckInputs { info: TerminalInfo { + backend: None, name: TerminalName::Unknown, term_program: None, version: None, diff --git a/codex-rs/terminal-detection/src/lib.rs b/codex-rs/terminal-detection/src/lib.rs index 823a180ab2..26b51fa5b5 100644 --- a/codex-rs/terminal-detection/src/lib.rs +++ b/codex-rs/terminal-detection/src/lib.rs @@ -19,6 +19,18 @@ pub struct TerminalInfo { pub term: Option, /// Multiplexer metadata when a terminal multiplexer is active. pub multiplexer: Option, + /// Backend metadata observed independently of the terminal application identity. + pub backend: Option, +} + +/// Known terminal rendering backends derived from environment metadata. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum TerminalBackend { + /// VTE, used by GNOME Terminal and other terminal applications. + Vte { + /// Encoded as major * 10_000 + minor * 100 + micro; unknown if malformed. + version: Option, + }, } /// Known terminal name categories derived from environment variables. @@ -86,6 +98,7 @@ impl TerminalInfo { version, term, multiplexer, + backend: None, } } @@ -252,6 +265,24 @@ pub fn terminal_info() -> TerminalInfo { /// `TERM_SESSION_ID` is not terminal-specific and cannot identify Apple Terminal. /// - Finally, `TERM` is used as the capability fallback. fn detect_terminal_info_from_env(env: &dyn Environment) -> TerminalInfo { + let mut terminal = detect_terminal_identity_from_env(env); + // Identity probes can return before VTE is considered. Keep its backend + // metadata even when GNOME or another TERM_PROGRAM identifies the application. + let vte_version = env.var("VTE_VERSION"); + if vte_version.is_some() || terminal.name == TerminalName::Vte { + terminal.backend = Some(TerminalBackend::Vte { + // Choose the raw value before parsing so malformed values retain + // precedence over the terminal-version fallback. + version: vte_version + .as_deref() + .or(terminal.version.as_deref()) + .and_then(|version| version.parse().ok()), + }); + } + terminal +} + +fn detect_terminal_identity_from_env(env: &dyn Environment) -> TerminalInfo { let multiplexer = detect_multiplexer(env); if let Some(term_program) = env.var_non_empty("TERM_PROGRAM") diff --git a/codex-rs/terminal-detection/src/terminal_tests.rs b/codex-rs/terminal-detection/src/terminal_tests.rs index 49d5d1583e..d6b8e12635 100644 --- a/codex-rs/terminal-detection/src/terminal_tests.rs +++ b/codex-rs/terminal-detection/src/terminal_tests.rs @@ -33,6 +33,7 @@ fn terminal_info( multiplexer: Option, ) -> TerminalInfo { TerminalInfo { + backend: None, name, term_program: term_program.map(ToString::to_string), version: version.map(ToString::to_string), @@ -299,6 +300,7 @@ fn detects_zellij_multiplexer() { assert_eq!( terminal, TerminalInfo { + backend: None, name: TerminalName::Unknown, term_program: None, version: None, @@ -665,13 +667,18 @@ fn detects_vte() { let terminal = detect_terminal_info_from_env(&env); assert_eq!( terminal, - terminal_info( - TerminalName::Vte, - /*term_program*/ None, - Some("7000"), - /*term*/ None, - /*multiplexer*/ None - ), + TerminalInfo { + backend: Some(TerminalBackend::Vte { + version: Some(7000) + }), + ..terminal_info( + TerminalName::Vte, + /*term_program*/ None, + Some("7000"), + /*term*/ None, + /*multiplexer*/ None + ) + }, "vte_version_info" ); assert_eq!( @@ -686,13 +693,18 @@ fn detects_vte() { let terminal = detect_terminal_info_from_env(&env); assert_eq!( terminal, - terminal_info( - TerminalName::Vte, - Some("VTE"), - Some("7000"), - /*term*/ None, - /*multiplexer*/ None - ), + TerminalInfo { + backend: Some(TerminalBackend::Vte { + version: Some(7000) + }), + ..terminal_info( + TerminalName::Vte, + Some("VTE"), + Some("7000"), + /*term*/ None, + /*multiplexer*/ None + ) + }, "vte_term_program_info" ); assert_eq!( @@ -705,13 +717,16 @@ fn detects_vte() { let terminal = detect_terminal_info_from_env(&env); assert_eq!( terminal, - terminal_info( - TerminalName::Vte, - /*term_program*/ None, - /*version*/ None, - /*term*/ None, - /*multiplexer*/ None - ), + TerminalInfo { + backend: Some(TerminalBackend::Vte { version: None }), + ..terminal_info( + TerminalName::Vte, + /*term_program*/ None, + /*version*/ None, + /*term*/ None, + /*multiplexer*/ None + ) + }, "vte_empty_info" ); assert_eq!(terminal.user_agent_token(), "VTE", "vte_empty_user_agent"); @@ -862,3 +877,46 @@ fn tmux_preserves_safe_underlying_terminal_identifiers() { ); } } + +#[test] +fn captures_vte_backend_when_gnome_identity_takes_precedence() { + let env = FakeEnvironment::new() + .with_var("GNOME_TERMINAL_SCREEN", "1") + .with_var("VTE_VERSION", "5204") + .with_var("TMUX", "session"); + assert_eq!( + detect_terminal_info_from_env(&env), + TerminalInfo { + name: TerminalName::GnomeTerminal, + term_program: None, + version: None, + term: None, + multiplexer: Some(Multiplexer::Tmux { version: None }), + backend: Some(TerminalBackend::Vte { + version: Some(5204) + }), + } + ); +} + +#[test] +fn raw_vte_version_takes_precedence_even_when_malformed() { + for (raw_version, version) in [("5204", Some(5204)), ("invalid", None), ("", None)] { + let env = FakeEnvironment::new() + .with_var("TERM_PROGRAM", "VTE") + .with_var("TERM_PROGRAM_VERSION", "7600") + .with_var("VTE_VERSION", raw_version); + assert_eq!( + detect_terminal_info_from_env(&env), + TerminalInfo { + name: TerminalName::Vte, + term_program: Some("VTE".to_string()), + version: Some("7600".to_string()), + term: None, + multiplexer: None, + backend: Some(TerminalBackend::Vte { version }), + }, + "VTE_VERSION={raw_version:?}" + ); + } +} diff --git a/codex-rs/tui/src/iterm_session_status_tests.rs b/codex-rs/tui/src/iterm_session_status_tests.rs index 7612916829..f3684e297e 100644 --- a/codex-rs/tui/src/iterm_session_status_tests.rs +++ b/codex-rs/tui/src/iterm_session_status_tests.rs @@ -1,6 +1,7 @@ use std::io; use codex_terminal_detection::Multiplexer; +use codex_terminal_detection::TerminalBackend; use codex_terminal_detection::TerminalInfo; use codex_terminal_detection::TerminalName; use crossterm::Command; @@ -146,6 +147,7 @@ fn cache_changes_only_after_a_successful_write() { #[test] fn selects_protocols_for_terminal_environment() { let direct_iterm = TerminalInfo { + backend: None, name: TerminalName::Iterm2, term_program: Some("iTerm.app".to_string()), version: Some("3.7.0".to_string()), @@ -206,4 +208,50 @@ fn selects_protocols_for_terminal_environment() { ), generic_protocol ); + assert_eq!( + terminal_program_status_support( + &TerminalInfo { + backend: Some(TerminalBackend::Vte { + version: Some(5204) + }), + ..direct_iterm + }, + /*stdout_is_terminal*/ true, + /*screen_active*/ false, + ), + no_protocols + ); +} + +#[test] +fn selects_protocols_for_vte_backends() { + for name in [TerminalName::GnomeTerminal, TerminalName::Vte] { + for (version, osc_7501) in [ + (Some(5204), false), + (Some(5300), false), + (Some(5399), false), + (Some(5400), true), + (Some(7600), true), + (None, true), + ] { + let terminal = TerminalInfo { + name, + term_program: None, + version: None, + term: None, + multiplexer: None, + backend: Some(TerminalBackend::Vte { version }), + }; + assert_eq!( + terminal_program_status_support( + &terminal, /*stdout_is_terminal*/ true, /*screen_active*/ false, + ), + TerminalProgramStatusSupport { + osc_7501, + osc_21337: false, + }, + "terminal {name:?}, VTE backend version={version:?}" + ); + } + } } diff --git a/codex-rs/tui/src/local_settings_tests.rs b/codex-rs/tui/src/local_settings_tests.rs index 8515a72b2b..c67c874ee0 100644 --- a/codex-rs/tui/src/local_settings_tests.rs +++ b/codex-rs/tui/src/local_settings_tests.rs @@ -280,6 +280,7 @@ async fn copy_on_select_respects_terminal_defaults_and_config_overrides() -> any ), ] { let terminal = TerminalInfo { + backend: None, name, multiplexer, term_program: None, diff --git a/codex-rs/tui/src/markdown_render/web_links_tests.rs b/codex-rs/tui/src/markdown_render/web_links_tests.rs index cf8a08f929..3d5ea55cf6 100644 --- a/codex-rs/tui/src/markdown_render/web_links_tests.rs +++ b/codex-rs/tui/src/markdown_render/web_links_tests.rs @@ -67,6 +67,7 @@ fn bare_urls_keep_link_style_when_wrapped_in_tables() { fn terminal(name: TerminalName) -> TerminalInfo { TerminalInfo { + backend: None, name, term_program: None, version: None, diff --git a/codex-rs/tui/src/notifications/mod.rs b/codex-rs/tui/src/notifications/mod.rs index ee6694b357..5e2053c838 100644 --- a/codex-rs/tui/src/notifications/mod.rs +++ b/codex-rs/tui/src/notifications/mod.rs @@ -72,6 +72,7 @@ mod tests { fn test_terminal(name: TerminalName) -> TerminalInfo { TerminalInfo { + backend: None, name, term_program: None, version: None, diff --git a/codex-rs/tui/src/pets/image_protocol.rs b/codex-rs/tui/src/pets/image_protocol.rs index 947cedc180..9cbcb96a5e 100644 --- a/codex-rs/tui/src/pets/image_protocol.rs +++ b/codex-rs/tui/src/pets/image_protocol.rs @@ -637,6 +637,7 @@ mod tests { term: Option<&str>, ) -> TerminalInfo { TerminalInfo { + backend: None, name, term_program: term_program.map(str::to_string), version: version.map(str::to_string), diff --git a/codex-rs/tui/src/resize_reflow_cap.rs b/codex-rs/tui/src/resize_reflow_cap.rs index 4dd9ffb199..e690a77eb4 100644 --- a/codex-rs/tui/src/resize_reflow_cap.rs +++ b/codex-rs/tui/src/resize_reflow_cap.rs @@ -82,6 +82,7 @@ mod tests { fn test_terminal(name: TerminalName) -> TerminalInfo { TerminalInfo { + backend: None, name, term_program: None, version: None, @@ -165,6 +166,7 @@ mod tests { #[test] fn unknown_terminal_uses_fallback_even_under_multiplexer() { let terminal = TerminalInfo { + backend: None, name: TerminalName::Unknown, term_program: None, version: None, diff --git a/codex-rs/tui/src/terminal_program_status.rs b/codex-rs/tui/src/terminal_program_status.rs index 427b47d81d..f5b6788610 100644 --- a/codex-rs/tui/src/terminal_program_status.rs +++ b/codex-rs/tui/src/terminal_program_status.rs @@ -12,6 +12,7 @@ use std::io::stdout; use std::sync::Mutex; use std::sync::OnceLock; +use codex_terminal_detection::TerminalBackend; use codex_terminal_detection::TerminalInfo; use codex_terminal_detection::TerminalName; use codex_terminal_detection::terminal_info; @@ -21,6 +22,9 @@ use ratatui::crossterm::execute; static SESSION_STATUS_STATE: SessionStatusState = SessionStatusState::new(); static TERMINAL_PROGRAM_STATUS_SUPPORT: OnceLock = OnceLock::new(); +// VTE 0.54 introduced a parser that safely ignores unknown OSC sequences. +const MIN_VTE_VERSION_WITH_SAFE_OSC: u32 = 5400; + struct SessionStatusState { last_emitted: Mutex, } @@ -217,12 +221,19 @@ fn terminal_program_status_support( stdout_is_terminal: bool, screen_active: bool, ) -> TerminalProgramStatusSupport { - let osc_21337 = stdout_is_terminal + let osc_7501 = stdout_is_terminal + && match terminal.backend { + Some(TerminalBackend::Vte { version }) => { + version.is_none_or(|version| version >= MIN_VTE_VERSION_WITH_SAFE_OSC) + } + None => true, + }; + let osc_21337 = osc_7501 && terminal.name == TerminalName::Iterm2 && terminal.multiplexer.is_none() && !screen_active; TerminalProgramStatusSupport { - osc_7501: stdout_is_terminal, + osc_7501, osc_21337, } } diff --git a/codex-rs/tui/src/tui/scrollback_tests.rs b/codex-rs/tui/src/tui/scrollback_tests.rs index 33fdeb7052..27312d89a5 100644 --- a/codex-rs/tui/src/tui/scrollback_tests.rs +++ b/codex-rs/tui/src/tui/scrollback_tests.rs @@ -20,6 +20,7 @@ use ratatui::text::Line; #[test] fn windows_terminal_uses_full_screen_unless_zellij_is_active() { let mut terminal = TerminalInfo { + backend: None, name: TerminalName::WindowsTerminal, term_program: None, version: None, diff --git a/codex-rs/tui/src/tui/size_monitor_tests.rs b/codex-rs/tui/src/tui/size_monitor_tests.rs index 1986104da4..957b055008 100644 --- a/codex-rs/tui/src/tui/size_monitor_tests.rs +++ b/codex-rs/tui/src/tui/size_monitor_tests.rs @@ -245,6 +245,7 @@ async fn drop_does_not_wait_for_a_blocked_query_or_start_another() { fn monitor_requires_tmux_detection() { let (draw_tx, _) = broadcast::channel(/*capacity*/ 1); let mut info = TerminalInfo { + backend: None, name: TerminalName::Ghostty, term_program: None, version: None,