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",