From 49a9d789997ab40d1d75b117644f796988d32e18 Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Tue, 8 Sep 2026 04:19:37 +0000 Subject: [PATCH] Make older app-server notices configurable in the TUI (#43698) ## What changed Add `tui.show_server_version_notice`, enabled by default. Set it to `false` to suppress informational notices about older stable app servers at startup, on reconnect, and in the agents overview. Compatibility errors and version status remain unaffected. Refresh the overview notice when local settings change, clearing pending notices when the setting is disabled. ## Testing Add coverage for disabling and re-enabling overview notices, clearing pending notice state, and preserving remote version status. Extend reconnect and local-settings tests to cover the new setting. GitOrigin-RevId: d50dcf4472ee221ce31cdd7ab2cd766f901a8039 --- 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 | 8 ++ codex-rs/thread-manager-sample/src/main.rs | 1 + codex-rs/tui/src/app/agents_overview.rs | 1 + codex-rs/tui/src/app/config_persistence.rs | 1 + codex-rs/tui/src/app/reconnect.rs | 5 +- codex-rs/tui/src/app/server_version_notice.rs | 35 +++++++++ codex-rs/tui/src/app/session_lifecycle.rs | 2 + codex-rs/tui/src/app/startup.rs | 15 ++-- .../src/app/tests/active_reconnect_tests.rs | 45 +++++++----- codex-rs/tui/src/app/tests/startup.rs | 73 +++++++++++++++++++ codex-rs/tui/src/app/working_directory.rs | 1 + codex-rs/tui/src/history_cell/tests.rs | 10 +++ codex-rs/tui/src/local_settings.rs | 1 + codex-rs/tui/src/local_settings_tests.rs | 2 + codex-rs/tui/src/status/remote_connection.rs | 20 ++++- 18 files changed, 204 insertions(+), 28 deletions(-) diff --git a/codex-rs/config/src/types.rs b/codex-rs/config/src/types.rs index 821824f52e..dbb76178b2 100644 --- a/codex-rs/config/src/types.rs +++ b/codex-rs/config/src/types.rs @@ -741,6 +741,11 @@ pub struct Tui { #[serde(default = "default_true")] pub show_tooltips: bool, + /// Show an informational notice when the connected app server is an older stable release. + /// Defaults to `true`; this does not control compatibility errors or version status. + #[serde(default = "default_true")] + pub show_server_version_notice: bool, + /// Generate automatic conversation recaps when the terminal is unfocused. /// Defaults to `true`. Disabling this leaves `/recap` available on demand. #[serde(default = "default_true")] diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index 2d5d268a67..0e16b958be 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -4119,6 +4119,11 @@ "default": null, "description": "Preferred layout for resume/fork session picker results." }, + "show_server_version_notice": { + "default": true, + "description": "Show an informational notice when the connected app server is an older stable release. Defaults to `true`; this does not control compatibility errors or version status.", + "type": "boolean" + }, "show_tooltips": { "default": true, "description": "Show startup tooltips in the TUI welcome screen. Defaults to `true`.", diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index a093aaf28f..5cf603df74 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -1258,6 +1258,7 @@ fn config_toml_deserializes_model_availability_nux() { animations: true, whimsy: true, show_tooltips: true, + show_server_version_notice: true, auto_recap: true, disable_paste_burst: None, vim_mode_default: false, @@ -4273,6 +4274,7 @@ fn tui_config_missing_notifications_field_defaults_to_enabled() { animations: true, whimsy: true, show_tooltips: true, + show_server_version_notice: true, auto_recap: true, disable_paste_burst: None, vim_mode_default: false, diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index ebda9778b1..d223baa950 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -748,6 +748,9 @@ pub struct Config { /// Show startup tooltips in the TUI welcome screen. pub show_tooltips: bool, + /// Show a TUI notice when the connected app server is an older stable release. + pub tui_show_server_version_notice: bool, + /// Generate automatic TUI recaps. Manual `/recap` remains available when disabled. pub tui_auto_recap: bool, @@ -4354,6 +4357,11 @@ impl Config { animations: cfg.tui.as_ref().map(|t| t.animations).unwrap_or(true), tui_whimsy: cfg.tui.as_ref().map(|t| t.whimsy).unwrap_or(true), show_tooltips: cfg.tui.as_ref().map(|t| t.show_tooltips).unwrap_or(true), + tui_show_server_version_notice: cfg + .tui + .as_ref() + .map(|t| t.show_server_version_notice) + .unwrap_or(true), tui_auto_recap: cfg.tui.as_ref().map(|t| t.auto_recap).unwrap_or(/*default*/ true), model_availability_nux: cfg .tui diff --git a/codex-rs/thread-manager-sample/src/main.rs b/codex-rs/thread-manager-sample/src/main.rs index e99c62c933..47ab3e0926 100644 --- a/codex-rs/thread-manager-sample/src/main.rs +++ b/codex-rs/thread-manager-sample/src/main.rs @@ -227,6 +227,7 @@ fn new_config(model: Option, arg0_paths: Arg0DispatchPaths) -> anyhow::R animations: true, tui_whimsy: true, show_tooltips: true, + tui_show_server_version_notice: true, tui_auto_recap: true, model_availability_nux: ModelAvailabilityNuxConfig::default(), tui_alternate_screen: AltScreenMode::Auto, diff --git a/codex-rs/tui/src/app/agents_overview.rs b/codex-rs/tui/src/app/agents_overview.rs index 79668e61eb..5df1c79a69 100644 --- a/codex-rs/tui/src/app/agents_overview.rs +++ b/codex-rs/tui/src/app/agents_overview.rs @@ -494,6 +494,7 @@ impl App { == RuntimePermissionProfileTurnOverride::LegacySandbox }); self.local_settings = local_settings; + self.refresh_server_version_overview_notice(CODEX_CLI_VERSION); self.config = resume_config; tui.set_notification_settings( self.local_settings.tui.notification_settings.method, diff --git a/codex-rs/tui/src/app/config_persistence.rs b/codex-rs/tui/src/app/config_persistence.rs index 878fb739d9..9cf49ea310 100644 --- a/codex-rs/tui/src/app/config_persistence.rs +++ b/codex-rs/tui/src/app/config_persistence.rs @@ -404,6 +404,7 @@ impl App { .await?; self.apply_runtime_policy_overrides(&mut config, RuntimePolicyOverrideScope::All); self.local_settings = crate::local_settings::LocalSettings::from(&config); + self.refresh_server_version_overview_notice(CODEX_CLI_VERSION); // Other preferences have runtime caches and are adopted when the widget is replaced. self.chat_widget .local_settings diff --git a/codex-rs/tui/src/app/reconnect.rs b/codex-rs/tui/src/app/reconnect.rs index 7bbb432e33..2951be249e 100644 --- a/codex-rs/tui/src/app/reconnect.rs +++ b/codex-rs/tui/src/app/reconnect.rs @@ -440,11 +440,14 @@ impl App { client_version, app_server.server_version(), ); - if self.reconnect.seen_version_notice != connected_notice_key { + if !self.local_settings.tui.show_server_version_notice + || self.reconnect.seen_version_notice != connected_notice_key + { self.reconnect.seen_version_notice = None; self.update_server_version_overview_notice(client_version, /*older_server*/ None); } if let Some((notice, key)) = crate::status::remote_connection::pending_server_version_notice( + &self.local_settings.tui, &self.app_server_target, app_server.server_codex_home(), client_version, diff --git a/codex-rs/tui/src/app/server_version_notice.rs b/codex-rs/tui/src/app/server_version_notice.rs index 6b234a9b7b..b912cc9d61 100644 --- a/codex-rs/tui/src/app/server_version_notice.rs +++ b/codex-rs/tui/src/app/server_version_notice.rs @@ -3,6 +3,41 @@ use super::*; impl App { + pub(super) fn refresh_server_version_overview_notice(&mut self, client_version: &str) { + if !self.local_settings.tui.show_server_version_notice { + self.pending_server_version_notice = None; + self.reconnect.seen_version_notice = None; + } + let server_version = self + .chat_widget + .remote_connection + .as_ref() + .and_then(|connection| connection.version.strip_prefix('v')) + .map(str::to_owned); + let _ = self.initialize_server_version_notice(client_version, server_version.as_deref()); + } + + pub(super) fn initialize_server_version_notice( + &mut self, + client_version: &str, + server_version: Option<&str>, + ) -> Option { + let notice = if matches!(self.app_server_target, AppServerTarget::Embedded) { + None + } else { + crate::status::remote_connection::server_version_notice_for_tui( + &self.local_settings.tui, + client_version, + server_version, + ) + }; + self.update_server_version_overview_notice( + client_version, + notice.as_ref().and(server_version), + ); + notice + } + pub(super) fn update_server_version_overview_notice( &mut self, client_version: &str, diff --git a/codex-rs/tui/src/app/session_lifecycle.rs b/codex-rs/tui/src/app/session_lifecycle.rs index b60a178422..6357ce8851 100644 --- a/codex-rs/tui/src/app/session_lifecycle.rs +++ b/codex-rs/tui/src/app/session_lifecycle.rs @@ -944,6 +944,7 @@ impl App { } } self.local_settings = crate::local_settings::LocalSettings::from(&config); + self.refresh_server_version_overview_notice(CODEX_CLI_VERSION); self.config = config; let name_error = if let Some(name) = new_thread_name { @@ -1249,6 +1250,7 @@ impl App { self.shutdown_current_thread(app_server).await; } self.local_settings = local_settings; + self.refresh_server_version_overview_notice(CODEX_CLI_VERSION); self.config = resume_config; tui.set_notification_settings( self.local_settings.tui.notification_settings.method, diff --git a/codex-rs/tui/src/app/startup.rs b/codex-rs/tui/src/app/startup.rs index 5abb2ba7b4..1e73cdba1d 100644 --- a/codex-rs/tui/src/app/startup.rs +++ b/codex-rs/tui/src/app/startup.rs @@ -262,6 +262,7 @@ impl App { let initial_server_version_notice = if !matches!(app_server_target, AppServerTarget::Embedded) { crate::status::remote_connection::pending_server_version_notice( + &local_settings.tui, &app_server_target, app_server.server_codex_home(), CODEX_CLI_VERSION, @@ -770,12 +771,14 @@ See the Codex keymap documentation for supported actions and examples." if !tui.is_terminal_focused() { app.recap.note_focus_lost(Instant::now()); } - app.update_server_version_overview_notice( - CODEX_CLI_VERSION, - initial_server_version_notice - .as_ref() - .and(app_server.server_version()), - ); + let _ = + app.initialize_server_version_notice(CODEX_CLI_VERSION, app_server.server_version()); + if initial_server_version_notice.is_none() { + app.update_server_version_overview_notice( + CODEX_CLI_VERSION, + /*older_server*/ None, + ); + } if start_in_agents_overview { app.open_agents_overview(&app_server); } else if !matches!(app.app_server_target, AppServerTarget::Embedded) { diff --git a/codex-rs/tui/src/app/tests/active_reconnect_tests.rs b/codex-rs/tui/src/app/tests/active_reconnect_tests.rs index d52534a68c..ba6e507178 100644 --- a/codex-rs/tui/src/app/tests/active_reconnect_tests.rs +++ b/codex-rs/tui/src/app/tests/active_reconnect_tests.rs @@ -11,17 +11,19 @@ use super::disconnect::serve_reconnect_requests; #[tokio::test] async fn reconnect_restores_history_permissions_and_keeps_old_input_paused() -> Result<()> { - for (recovered_queue, edit_offline, resume_error_code, deferred_notice) in [ - (true, false, -32603, false), - (false, false, -32603, false), - (true, true, -32603, false), - (true, false, -32600, false), - (false, false, -32600, false), - (true, true, -32600, false), - (true, false, -32603, true), + for (recovered_queue, edit_offline, resume_error_code, deferred_notice, notice_enabled) in [ + (true, false, -32603, false, false), + (true, false, -32603, false, true), + (false, false, -32603, false, false), + (true, true, -32603, false, false), + (true, false, -32600, false, false), + (false, false, -32600, false, false), + (true, true, -32600, false, false), + (true, false, -32603, true, true), ] { let pending_profile = !recovered_queue && resume_error_code == -32600; let (mut app, mut events, mut ops) = make_test_app_with_channels().await; + app.local_settings.tui.show_server_version_notice = notice_enabled; let id = ThreadId::new(); let cwd = app.config.cwd.clone(); app.config.model = Some("gpt-test".into()); @@ -76,18 +78,21 @@ async fn reconnect_restores_history_permissions_and_keeps_old_input_paused() -> let listener = TcpListener::bind("127.0.0.1:0").await?; let endpoint = crate::resolve_remote_addr(&format!("ws://{}", listener.local_addr()?))?; app.app_server_target = AppServerTarget::Remote { endpoint }; - let (notice, key) = crate::status::remote_connection::pending_server_version_notice( - &app.app_server_target, - /*server_home*/ None, - "2.1.0", - Some("2.0.0"), - /*last_shown*/ None, - ) - .expect("older server should have a pending notice"); - app.reconnect.seen_version_notice = Some(key); - if deferred_notice { - app.pending_server_version_notice = Some(notice); - app.update_server_version_overview_notice("2.1.0", Some("2.0.0")); + if notice_enabled { + let (notice, key) = crate::status::remote_connection::pending_server_version_notice( + &app.local_settings.tui, + &app.app_server_target, + /*server_home*/ None, + "2.1.0", + Some("2.0.0"), + /*last_shown*/ None, + ) + .expect("older server should have a pending notice"); + app.reconnect.seen_version_notice = Some(key); + if deferred_notice { + app.pending_server_version_notice = Some(notice); + app.update_server_version_overview_notice("2.1.0", Some("2.0.0")); + } } let thread = json!({ "id": id, "sessionId": id, "preview": "only once", "ephemeral": false, diff --git a/codex-rs/tui/src/app/tests/startup.rs b/codex-rs/tui/src/app/tests/startup.rs index 0694fda497..1bbcaa135a 100644 --- a/codex-rs/tui/src/app/tests/startup.rs +++ b/codex-rs/tui/src/app/tests/startup.rs @@ -1,4 +1,5 @@ use super::*; +use crate::ThreadParamsMode; use crate::app_event_sender::AppEventSender; use crate::bottom_pane::BottomPane; use crate::bottom_pane::BottomPaneParams; @@ -1087,6 +1088,78 @@ async fn fresh_startup_notice_follows_session_attachment() { assert_eq!(app.pending_server_version_notice, None); } +#[tokio::test] +async fn remote_overview_startup_hides_disabled_older_server_notice() -> Result<()> { + let mut app = make_test_app().await; + app.local_settings.tui.show_server_version_notice = false; + let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await?; + let endpoint = crate::resolve_remote_addr(&format!("ws://{}", listener.local_addr()?))?; + app.app_server_target = AppServerTarget::Remote { + endpoint: endpoint.clone(), + }; + let server = tokio::spawn(async move { + let (stream, _) = listener.accept().await?; + super::disconnect::serve_reconnect_requests( + tokio_tungstenite::accept_async(stream).await?, + |_request| std::future::ready(None), + ) + .await + }); + let session = AppServerSession::new( + crate::connect_remote_app_server(endpoint).await?, + ThreadParamsMode::Remote, + ); + + assert_eq!( + app.initialize_server_version_notice("2.1.0", session.server_version()), + None + ); + let view = app.agents_overview_view(Vec::new(), /*selected_thread_id*/ None); + app.chat_widget.show_bottom_pane_view(Box::new(view)); + let rendered = render_bottom_popup(&app.chat_widget, /*width*/ 80); + insta::assert_snapshot!(rendered.lines().take(2).collect::>().join("\n"), @" Agent command center + 0 need input 0 working 0 ready"); + app.chat_widget.remote_connection = + crate::status::remote_connection::remote_connection_status_value( + &app.app_server_target, + session.server_version(), + ); + assert_eq!( + app.chat_widget + .remote_connection + .as_ref() + .expect("remote status") + .version, + "v2.0.0" + ); + app.local_settings.tui.show_server_version_notice = true; + app.refresh_server_version_overview_notice("2.1.0"); + let rendered = render_bottom_popup(&app.chat_widget, /*width*/ 80); + insta::assert_snapshot!(rendered.lines().take(2).collect::>().join("\n"), @" Service v2.0.0 < Codex CLI v2.1.0 + 0 need input 0 working 0 ready"); + app.pending_server_version_notice = + Some(crate::status::remote_connection::ServerVersionNotice { + message: "Older service".to_string(), + offer_update: false, + }); + app.reconnect.seen_version_notice = Some("old-key".to_string()); + app.local_settings.tui.show_server_version_notice = false; + app.refresh_server_version_overview_notice("2.1.0"); + assert_eq!(app.pending_server_version_notice, None); + assert_eq!(app.reconnect.seen_version_notice, None); + assert_eq!( + app.agents_overview + .view_state + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .server_version_notice, + None + ); + session.shutdown().await?; + server.await??; + Ok(()) +} + #[tokio::test] async fn startup_thread_started_discards_another_threads_buffered_events() { let (mut app, _app_event_rx, _op_rx) = make_test_app_with_channels().await; diff --git a/codex-rs/tui/src/app/working_directory.rs b/codex-rs/tui/src/app/working_directory.rs index b0906f858d..7f6375496e 100644 --- a/codex-rs/tui/src/app/working_directory.rs +++ b/codex-rs/tui/src/app/working_directory.rs @@ -449,6 +449,7 @@ impl App { name_error, } = attach; self.local_settings = local_settings; + self.refresh_server_version_overview_notice(CODEX_CLI_VERSION); self.config = *config; self.file_search .update_search_dir(self.config.cwd.to_path_buf()); diff --git a/codex-rs/tui/src/history_cell/tests.rs b/codex-rs/tui/src/history_cell/tests.rs index a2e321cf2b..6374f75949 100644 --- a/codex-rs/tui/src/history_cell/tests.rs +++ b/codex-rs/tui/src/history_cell/tests.rs @@ -46,7 +46,12 @@ fn connected_server_version_notice_snapshot() { .expect("absolute socket path"), }, }; + let settings = codex_config::types::Tui { + show_server_version_notice: true, + ..Default::default() + }; let (notice, _) = crate::status::remote_connection::pending_server_version_notice( + &settings, &target, /*server_home*/ None, "0.153.0", @@ -68,7 +73,12 @@ fn local_daemon_version_notice_snapshot() { .expect("absolute socket path"), }, }; + let settings = codex_config::types::Tui { + show_server_version_notice: true, + ..Default::default() + }; let (notice, _) = crate::status::remote_connection::pending_server_version_notice( + &settings, &target, /*server_home*/ None, "0.153.0", diff --git a/codex-rs/tui/src/local_settings.rs b/codex-rs/tui/src/local_settings.rs index db12daa6f1..6a588f4502 100644 --- a/codex-rs/tui/src/local_settings.rs +++ b/codex-rs/tui/src/local_settings.rs @@ -29,6 +29,7 @@ impl From<&Config> for LocalSettings { animations: config.animations, whimsy: config.tui_whimsy, show_tooltips: config.show_tooltips, + show_server_version_notice: config.tui_show_server_version_notice, auto_recap: config.tui_auto_recap, disable_paste_burst: Some(config.disable_paste_burst), vim_mode_default: config.tui_vim_mode_default, diff --git a/codex-rs/tui/src/local_settings_tests.rs b/codex-rs/tui/src/local_settings_tests.rs index 6205fa6847..c01838ee32 100644 --- a/codex-rs/tui/src/local_settings_tests.rs +++ b/codex-rs/tui/src/local_settings_tests.rs @@ -14,6 +14,7 @@ async fn local_load_preserves_defaults_and_resolved_overrides() -> anyhow::Resul animations = false whimsy = false show_tooltips = false +show_server_version_notice = false auto_recap = false vim_mode_default = true terminal_resize_reflow_max_rows = 0 @@ -44,6 +45,7 @@ fast_default_opt_out = true expected.animations = false; expected.whimsy = false; expected.show_tooltips = false; + expected.show_server_version_notice = false; expected.auto_recap = false; expected.vim_mode_default = true; expected.terminal_resize_reflow_max_rows = Some(0); diff --git a/codex-rs/tui/src/status/remote_connection.rs b/codex-rs/tui/src/status/remote_connection.rs index f75263a8e6..34bd903f2b 100644 --- a/codex-rs/tui/src/status/remote_connection.rs +++ b/codex-rs/tui/src/status/remote_connection.rs @@ -51,6 +51,18 @@ pub(crate) fn server_version_notice(client: &str, server: Option<&str>) -> Optio }) } +pub(crate) fn server_version_notice_for_tui( + settings: &codex_config::types::Tui, + client: &str, + server: Option<&str>, +) -> Option { + if settings.show_server_version_notice { + server_version_notice(client, server) + } else { + None + } +} + fn hash_identity_part(hasher: &mut Sha256, tag: &[u8], value: &[u8]) { hasher.update(tag); hasher.update((value.len() as u64).to_le_bytes()); @@ -123,13 +135,14 @@ pub(crate) fn server_version_notice_key( } pub(crate) fn pending_server_version_notice( + settings: &codex_config::types::Tui, target: &AppServerTarget, server_home: Option<&str>, client: &str, server: Option<&str>, last_shown: Option<&str>, ) -> Option<(ServerVersionNotice, String)> { - let message = server_version_notice(client, server)?; + let message = server_version_notice_for_tui(settings, client, server)?; let notice = ServerVersionNotice { message, offer_update: matches!(target, AppServerTarget::LocalDaemon { .. }), @@ -210,8 +223,13 @@ mod tests { endpoint: endpoint.clone(), }; let remote = AppServerTarget::Remote { endpoint }; + let settings = codex_config::types::Tui { + show_server_version_notice: true, + ..Default::default() + }; let notice = |target| { pending_server_version_notice( + &settings, target, /*server_home*/ None, "0.153.0",