From 8468871e2b733cb28c5bc35b9760f53b23f44cbb Mon Sep 17 00:00:00 2001 From: Matthew Zeng Date: Fri, 13 Feb 2026 11:54:16 -0800 Subject: [PATCH] [apps] Improve app listing filtering. (#11697) - [x] If an installed app is not on the app listing, remove it from the final list. --- .../app-server/src/codex_message_processor.rs | 3 +- codex-rs/chatgpt/src/connectors.rs | 53 ++++++++++++++++++- codex-rs/tui/src/app.rs | 2 +- codex-rs/tui/src/chatwidget.rs | 2 + codex-rs/tui/src/chatwidget/tests.rs | 16 +++--- 5 files changed, 67 insertions(+), 9 deletions(-) diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index 1eecb5da3..198892d0a 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -4724,9 +4724,10 @@ impl CodexMessageProcessor { all_connectors: Option<&[AppInfo]>, accessible_connectors: Option<&[AppInfo]>, ) -> Vec { + let all_connectors_loaded = all_connectors.is_some(); let all = all_connectors.map_or_else(Vec::new, <[AppInfo]>::to_vec); let accessible = accessible_connectors.map_or_else(Vec::new, <[AppInfo]>::to_vec); - connectors::merge_connectors_with_accessible(all, accessible) + connectors::merge_connectors_with_accessible(all, accessible, all_connectors_loaded) } fn paginate_apps( diff --git a/codex-rs/chatgpt/src/connectors.rs b/codex-rs/chatgpt/src/connectors.rs index 97929158f..bafb0273a 100644 --- a/codex-rs/chatgpt/src/connectors.rs +++ b/codex-rs/chatgpt/src/connectors.rs @@ -1,4 +1,5 @@ use std::collections::HashMap; +use std::collections::HashSet; use std::sync::LazyLock; use std::sync::Mutex as StdMutex; @@ -75,7 +76,7 @@ pub async fn list_connectors(config: &Config) -> anyhow::Result> { let connectors = connectors_result?; let accessible = accessible_result?; Ok(with_app_enabled_state( - merge_connectors_with_accessible(connectors, accessible), + merge_connectors_with_accessible(connectors, accessible, true), config, )) } @@ -185,7 +186,20 @@ fn write_cached_all_connectors(cache_key: AllConnectorsCacheKey, connectors: &[A pub fn merge_connectors_with_accessible( connectors: Vec, accessible_connectors: Vec, + all_connectors_loaded: bool, ) -> Vec { + let accessible_connectors = if all_connectors_loaded { + let connector_ids: HashSet<&str> = connectors + .iter() + .map(|connector| connector.id.as_str()) + .collect(); + accessible_connectors + .into_iter() + .filter(|connector| connector_ids.contains(connector.id.as_str())) + .collect() + } else { + accessible_connectors + }; let merged = merge_connectors(connectors, accessible_connectors); filter_disallowed_connectors(merged) } @@ -406,4 +420,41 @@ mod tests { ]); assert_eq!(filtered, vec![app("delta")]); } + + fn merged_app(id: &str, is_accessible: bool) -> AppInfo { + AppInfo { + id: id.to_string(), + name: id.to_string(), + description: None, + logo_url: None, + logo_url_dark: None, + distribution_channel: None, + install_url: Some(connector_install_url(id, id)), + is_accessible, + is_enabled: true, + } + } + + #[test] + fn excludes_accessible_connectors_not_in_all_when_all_loaded() { + let merged = merge_connectors_with_accessible( + vec![app("alpha")], + vec![app("alpha"), app("beta")], + true, + ); + assert_eq!(merged, vec![merged_app("alpha", true)]); + } + + #[test] + fn keeps_accessible_connectors_not_in_all_while_all_loading() { + let merged = merge_connectors_with_accessible( + vec![app("alpha")], + vec![app("alpha"), app("beta")], + false, + ); + assert_eq!( + merged, + vec![merged_app("alpha", true), merged_app("beta", true)] + ); + } } diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index a62a5b2a0..599b2cf91 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -3387,7 +3387,7 @@ mod tests { let mut app = make_test_app().await; let codex_home = tempdir()?; app.config.codex_home = codex_home.path().to_path_buf(); - let app_id = "connector_1".to_string(); + let app_id = "unit_test_refresh_in_memory_config_connector".to_string(); assert_eq!(app_enabled_in_effective_config(&app.config, &app_id), None); diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index af79a136a..e36147ae7 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -4582,6 +4582,7 @@ impl ChatWidget { let connectors = connectors::merge_connectors_with_accessible( all_connectors, accessible_connectors, + true, ); Ok(ConnectorsSnapshot { connectors }) } @@ -6736,6 +6737,7 @@ impl ChatWidget { snapshot.connectors = connectors::merge_connectors_with_accessible( Vec::new(), snapshot.connectors, + false, ); } snapshot.connectors = diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index f7b8e3304..f0af50415 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -3868,11 +3868,13 @@ async fn apps_popup_refreshes_when_connectors_snapshot_updates() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None).await; chat.config.features.enable(Feature::Apps); chat.bottom_pane.set_connectors_enabled(true); + let notion_id = "unit_test_apps_popup_refresh_connector_1"; + let linear_id = "unit_test_apps_popup_refresh_connector_2"; chat.on_connectors_loaded( Ok(ConnectorsSnapshot { connectors: vec![codex_chatgpt::connectors::AppInfo { - id: "connector_1".to_string(), + id: notion_id.to_string(), name: "Notion".to_string(), description: Some("Workspace docs".to_string()), logo_url: None, @@ -3905,7 +3907,7 @@ async fn apps_popup_refreshes_when_connectors_snapshot_updates() { Ok(ConnectorsSnapshot { connectors: vec![ codex_chatgpt::connectors::AppInfo { - id: "connector_1".to_string(), + id: notion_id.to_string(), name: "Notion".to_string(), description: Some("Workspace docs".to_string()), logo_url: None, @@ -3916,7 +3918,7 @@ async fn apps_popup_refreshes_when_connectors_snapshot_updates() { is_enabled: true, }, codex_chatgpt::connectors::AppInfo { - id: "connector_2".to_string(), + id: linear_id.to_string(), name: "Linear".to_string(), description: Some("Project tracking".to_string()), logo_url: None, @@ -3947,10 +3949,12 @@ async fn apps_refresh_failure_keeps_existing_full_snapshot() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None).await; chat.config.features.enable(Feature::Apps); chat.bottom_pane.set_connectors_enabled(true); + let notion_id = "unit_test_apps_refresh_failure_connector_1"; + let linear_id = "unit_test_apps_refresh_failure_connector_2"; let full_connectors = vec![ codex_chatgpt::connectors::AppInfo { - id: "connector_1".to_string(), + id: notion_id.to_string(), name: "Notion".to_string(), description: Some("Workspace docs".to_string()), logo_url: None, @@ -3961,7 +3965,7 @@ async fn apps_refresh_failure_keeps_existing_full_snapshot() { is_enabled: true, }, codex_chatgpt::connectors::AppInfo { - id: "connector_2".to_string(), + id: linear_id.to_string(), name: "Linear".to_string(), description: Some("Project tracking".to_string()), logo_url: None, @@ -3982,7 +3986,7 @@ async fn apps_refresh_failure_keeps_existing_full_snapshot() { chat.on_connectors_loaded( Ok(ConnectorsSnapshot { connectors: vec![codex_chatgpt::connectors::AppInfo { - id: "connector_1".to_string(), + id: notion_id.to_string(), name: "Notion".to_string(), description: Some("Workspace docs".to_string()), logo_url: None,