diff --git a/codex-rs/app-server-protocol/schema/json/ServerNotification.json b/codex-rs/app-server-protocol/schema/json/ServerNotification.json index 31fe2f31c..a3d07c24a 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerNotification.json +++ b/codex-rs/app-server-protocol/schema/json/ServerNotification.json @@ -241,6 +241,24 @@ "null" ] }, + "iconAssets": { + "additionalProperties": { + "type": "string" + }, + "type": [ + "object", + "null" + ] + }, + "iconDarkAssets": { + "additionalProperties": { + "type": "string" + }, + "type": [ + "object", + "null" + ] + }, "id": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index 4bbf58a12..4d417ffb2 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -6406,6 +6406,24 @@ "null" ] }, + "iconAssets": { + "additionalProperties": { + "type": "string" + }, + "type": [ + "object", + "null" + ] + }, + "iconDarkAssets": { + "additionalProperties": { + "type": "string" + }, + "type": [ + "object", + "null" + ] + }, "id": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index 9cae10233..0df923a0f 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -532,6 +532,24 @@ "null" ] }, + "iconAssets": { + "additionalProperties": { + "type": "string" + }, + "type": [ + "object", + "null" + ] + }, + "iconDarkAssets": { + "additionalProperties": { + "type": "string" + }, + "type": [ + "object", + "null" + ] + }, "id": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/AppListUpdatedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/AppListUpdatedNotification.json index d4e99f508..0b04e2b22 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/AppListUpdatedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/AppListUpdatedNotification.json @@ -78,6 +78,24 @@ "null" ] }, + "iconAssets": { + "additionalProperties": { + "type": "string" + }, + "type": [ + "object", + "null" + ] + }, + "iconDarkAssets": { + "additionalProperties": { + "type": "string" + }, + "type": [ + "object", + "null" + ] + }, "id": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/AppsListResponse.json b/codex-rs/app-server-protocol/schema/json/v2/AppsListResponse.json index 2fb9092cb..6dfa1accb 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/AppsListResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/AppsListResponse.json @@ -78,6 +78,24 @@ "null" ] }, + "iconAssets": { + "additionalProperties": { + "type": "string" + }, + "type": [ + "object", + "null" + ] + }, + "iconDarkAssets": { + "additionalProperties": { + "type": "string" + }, + "type": [ + "object", + "null" + ] + }, "id": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/AppInfo.ts b/codex-rs/app-server-protocol/schema/typescript/v2/AppInfo.ts index ef1f54aa6..7145ce9a9 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/AppInfo.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/AppInfo.ts @@ -7,7 +7,7 @@ import type { AppMetadata } from "./AppMetadata"; /** * EXPERIMENTAL - app metadata returned by app-list APIs. */ -export type AppInfo = { id: string, name: string, description: string | null, logoUrl: string | null, logoUrlDark: string | null, distributionChannel: string | null, branding: AppBranding | null, appMetadata: AppMetadata | null, labels: { [key in string]?: string } | null, installUrl: string | null, isAccessible: boolean, +export type AppInfo = { id: string, name: string, description: string | null, logoUrl: string | null, logoUrlDark: string | null, iconAssets: { [key in string]?: string } | null, iconDarkAssets: { [key in string]?: string } | null, distributionChannel: string | null, branding: AppBranding | null, appMetadata: AppMetadata | null, labels: { [key in string]?: string } | null, installUrl: string | null, isAccessible: boolean, /** * Whether this app is enabled in config.toml. * Example: diff --git a/codex-rs/app-server-protocol/src/protocol/v2/apps.rs b/codex-rs/app-server-protocol/src/protocol/v2/apps.rs index 94545eb78..f14c11b0d 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/apps.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/apps.rs @@ -83,6 +83,8 @@ pub struct AppInfo { pub description: Option, pub logo_url: Option, pub logo_url_dark: Option, + pub icon_assets: Option>, + pub icon_dark_assets: Option>, pub distribution_channel: Option, pub branding: Option, pub app_metadata: Option, diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index ed14ad64f..71557bb73 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -1787,7 +1787,7 @@ To disable a non-managed hook, upsert a state entry at `hooks.state` with `confi To re-enable it, upsert the same hook key with `"enabled": true`. ## Apps -Use `app/list` to fetch available apps (connectors). Each entry includes metadata like the app `id`, display `name`, `installUrl`, `branding`, `appMetadata`, `labels`, whether it is currently accessible, and whether it is enabled in config. +Use `app/list` to fetch available apps (connectors). Each entry includes metadata like the app `id`, display `name`, `installUrl`, legacy logo URLs, structured light and dark icon assets, `branding`, `appMetadata`, `labels`, whether it is currently accessible, and whether it is enabled in config. ```json { "method": "app/list", "id": 50, "params": { @@ -1804,6 +1804,10 @@ Use `app/list` to fetch available apps (connectors). Each entry includes metadat "description": "Example connector for documentation.", "logoUrl": "https://example.com/demo-app.png", "logoUrlDark": null, + "iconAssets": { + "256_square": "https://example.com/demo-app-square.png" + }, + "iconDarkAssets": null, "distributionChannel": null, "branding": null, "appMetadata": null, @@ -1834,6 +1838,10 @@ The server also emits `app/list/updated` notifications whenever either source (a "description": "Example connector for documentation.", "logoUrl": "https://example.com/demo-app.png", "logoUrlDark": null, + "iconAssets": { + "256_square": "https://example.com/demo-app-square.png" + }, + "iconDarkAssets": null, "distributionChannel": null, "branding": null, "appMetadata": null, diff --git a/codex-rs/app-server/src/app_info.rs b/codex-rs/app-server/src/app_info.rs index 5c8686172..19dcad827 100644 --- a/codex-rs/app-server/src/app_info.rs +++ b/codex-rs/app-server/src/app_info.rs @@ -22,6 +22,8 @@ pub(crate) fn app_info_to_api(app: AppInfo) -> ApiAppInfo { description, logo_url, logo_url_dark, + icon_assets, + icon_dark_assets, distribution_channel, branding, app_metadata, @@ -37,6 +39,8 @@ pub(crate) fn app_info_to_api(app: AppInfo) -> ApiAppInfo { description, logo_url, logo_url_dark, + icon_assets, + icon_dark_assets, distribution_channel, branding: branding.map(app_branding_to_api), app_metadata: app_metadata.map(app_metadata_to_api), diff --git a/codex-rs/app-server/tests/suite/v2/app_list.rs b/codex-rs/app-server/tests/suite/v2/app_list.rs index d9a39ca0b..1ddf35526 100644 --- a/codex-rs/app-server/tests/suite/v2/app_list.rs +++ b/codex-rs/app-server/tests/suite/v2/app_list.rs @@ -97,6 +97,8 @@ async fn list_apps_returns_empty_with_api_key_auth() -> Result<()> { description: Some("Beta connector".to_string()), logo_url: None, logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, branding: None, app_metadata: None, @@ -162,6 +164,8 @@ async fn list_apps_returns_empty_when_workspace_codex_plugins_disabled() -> Resu description: Some("Beta connector".to_string()), logo_url: None, logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, branding: None, app_metadata: None, @@ -268,6 +272,8 @@ async fn list_apps_uses_thread_feature_flag_when_thread_id_is_provided() -> Resu description: Some("Beta connector".to_string()), logo_url: None, logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, branding: None, app_metadata: None, @@ -372,6 +378,8 @@ async fn list_apps_keeps_apps_with_app_only_tools_accessible() -> Result<()> { description: Some("Formerly blocked connector".to_string()), logo_url: None, logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, branding: None, app_metadata: None, @@ -439,6 +447,8 @@ async fn list_apps_reports_is_enabled_from_config() -> Result<()> { description: Some("Beta connector".to_string()), logo_url: None, logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, branding: None, app_metadata: None, @@ -549,6 +559,8 @@ async fn list_apps_emits_updates_and_returns_after_both_lists_load() -> Result<( description: Some("Alpha connector".to_string()), logo_url: Some("https://example.com/alpha.png".to_string()), logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, branding: alpha_branding.clone(), app_metadata: alpha_app_metadata.clone(), @@ -564,6 +576,8 @@ async fn list_apps_emits_updates_and_returns_after_both_lists_load() -> Result<( description: None, logo_url: None, logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, branding: None, app_metadata: None, @@ -613,6 +627,8 @@ async fn list_apps_emits_updates_and_returns_after_both_lists_load() -> Result<( description: None, logo_url: None, logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, branding: None, app_metadata: None, @@ -633,6 +649,8 @@ async fn list_apps_emits_updates_and_returns_after_both_lists_load() -> Result<( description: None, logo_url: None, logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, branding: None, app_metadata: None, @@ -648,6 +666,8 @@ async fn list_apps_emits_updates_and_returns_after_both_lists_load() -> Result<( description: Some("Alpha connector".to_string()), logo_url: Some("https://example.com/alpha.png".to_string()), logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, branding: alpha_branding, app_metadata: alpha_app_metadata, @@ -689,6 +709,8 @@ async fn list_apps_waits_for_accessible_data_before_emitting_directory_updates() description: Some("Alpha connector".to_string()), logo_url: Some("https://example.com/alpha.png".to_string()), logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, branding: None, app_metadata: None, @@ -704,6 +726,8 @@ async fn list_apps_waits_for_accessible_data_before_emitting_directory_updates() description: None, logo_url: None, logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, branding: None, app_metadata: None, @@ -754,6 +778,8 @@ async fn list_apps_waits_for_accessible_data_before_emitting_directory_updates() description: None, logo_url: None, logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, branding: None, app_metadata: None, @@ -769,6 +795,8 @@ async fn list_apps_waits_for_accessible_data_before_emitting_directory_updates() description: Some("Alpha connector".to_string()), logo_url: Some("https://example.com/alpha.png".to_string()), logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, branding: None, app_metadata: None, @@ -813,6 +841,8 @@ async fn list_apps_does_not_emit_empty_interim_updates() -> Result<()> { description: Some("Alpha connector".to_string()), logo_url: None, logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, branding: None, app_metadata: None, @@ -869,6 +899,8 @@ async fn list_apps_does_not_emit_empty_interim_updates() -> Result<()> { description: Some("Alpha connector".to_string()), logo_url: None, logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, branding: None, app_metadata: None, @@ -904,6 +936,8 @@ async fn list_apps_paginates_results() -> Result<()> { description: Some("Alpha connector".to_string()), logo_url: None, logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, branding: None, app_metadata: None, @@ -919,6 +953,8 @@ async fn list_apps_paginates_results() -> Result<()> { description: None, logo_url: None, logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, branding: None, app_metadata: None, @@ -977,6 +1013,8 @@ async fn list_apps_paginates_results() -> Result<()> { description: None, logo_url: None, logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, branding: None, app_metadata: None, @@ -1021,6 +1059,8 @@ async fn list_apps_paginates_results() -> Result<()> { description: Some("Alpha connector".to_string()), logo_url: None, logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, branding: None, app_metadata: None, @@ -1046,6 +1086,8 @@ async fn list_apps_force_refetch_preserves_previous_cache_on_failure() -> Result description: Some("Beta connector".to_string()), logo_url: None, logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, branding: None, app_metadata: None, @@ -1151,6 +1193,8 @@ async fn list_apps_force_refetch_patches_updates_from_cached_snapshots() -> Resu description: Some("Alpha v1".to_string()), logo_url: None, logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, branding: None, app_metadata: None, @@ -1166,6 +1210,8 @@ async fn list_apps_force_refetch_patches_updates_from_cached_snapshots() -> Resu description: Some("Beta v1".to_string()), logo_url: None, logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, branding: None, app_metadata: None, @@ -1216,6 +1262,8 @@ async fn list_apps_force_refetch_patches_updates_from_cached_snapshots() -> Resu description: None, logo_url: None, logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, branding: None, app_metadata: None, @@ -1237,6 +1285,8 @@ async fn list_apps_force_refetch_patches_updates_from_cached_snapshots() -> Resu description: Some("Beta v1".to_string()), logo_url: None, logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, branding: None, app_metadata: None, @@ -1252,6 +1302,8 @@ async fn list_apps_force_refetch_patches_updates_from_cached_snapshots() -> Resu description: Some("Alpha v1".to_string()), logo_url: None, logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, branding: None, app_metadata: None, @@ -1282,6 +1334,8 @@ async fn list_apps_force_refetch_patches_updates_from_cached_snapshots() -> Resu description: Some("Alpha v2".to_string()), logo_url: None, logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, branding: None, app_metadata: None, @@ -1312,6 +1366,8 @@ async fn list_apps_force_refetch_patches_updates_from_cached_snapshots() -> Resu description: Some("Beta v1".to_string()), logo_url: None, logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, branding: None, app_metadata: None, @@ -1327,6 +1383,8 @@ async fn list_apps_force_refetch_patches_updates_from_cached_snapshots() -> Resu description: Some("Alpha v1".to_string()), logo_url: None, logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, branding: None, app_metadata: None, @@ -1355,6 +1413,8 @@ async fn list_apps_force_refetch_patches_updates_from_cached_snapshots() -> Resu description: Some("Alpha v2".to_string()), logo_url: None, logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, branding: None, app_metadata: None, diff --git a/codex-rs/app-server/tests/suite/v2/plugin_install.rs b/codex-rs/app-server/tests/suite/v2/plugin_install.rs index cc76ef1eb..34fc9b18a 100644 --- a/codex-rs/app-server/tests/suite/v2/plugin_install.rs +++ b/codex-rs/app-server/tests/suite/v2/plugin_install.rs @@ -1095,6 +1095,8 @@ async fn plugin_install_returns_apps_needing_auth() -> Result<()> { description: Some("Alpha connector".to_string()), logo_url: Some("https://example.com/alpha.png".to_string()), logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: Some("featured".to_string()), branding: None, app_metadata: None, @@ -1110,6 +1112,8 @@ async fn plugin_install_returns_apps_needing_auth() -> Result<()> { description: Some("Beta connector".to_string()), logo_url: None, logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, branding: None, app_metadata: None, @@ -1194,6 +1198,8 @@ async fn plugin_install_skips_mcp_oauth_for_chatgpt_dual_surface_plugin() -> Res description: Some("Sample MCP connector".to_string()), logo_url: Some("https://example.com/alpha.png".to_string()), logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: Some("featured".to_string()), branding: None, app_metadata: None, @@ -1586,6 +1592,8 @@ async fn plugin_install_includes_formerly_disallowed_apps_needing_auth() -> Resu description: Some("Alpha connector".to_string()), logo_url: Some("https://example.com/alpha.png".to_string()), logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: Some("featured".to_string()), branding: None, app_metadata: None, diff --git a/codex-rs/app-server/tests/suite/v2/plugin_read.rs b/codex-rs/app-server/tests/suite/v2/plugin_read.rs index 9d7b7902b..011cbec52 100644 --- a/codex-rs/app-server/tests/suite/v2/plugin_read.rs +++ b/codex-rs/app-server/tests/suite/v2/plugin_read.rs @@ -220,6 +220,8 @@ apps = true description: Some("Example app connector".to_string()), logo_url: Some("https://example.com/example.png".to_string()), logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: Some("featured".to_string()), branding: None, app_metadata: None, @@ -1587,6 +1589,8 @@ async fn plugin_read_returns_app_metadata_category() -> Result<()> { description: Some("Alpha connector".to_string()), logo_url: Some("https://example.com/alpha.png".to_string()), logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: Some("featured".to_string()), branding: None, app_metadata: Some(AppMetadata { @@ -1615,6 +1619,8 @@ async fn plugin_read_returns_app_metadata_category() -> Result<()> { description: Some("Beta connector".to_string()), logo_url: None, logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, branding: None, app_metadata: None, @@ -1690,6 +1696,8 @@ async fn plugin_read_hides_apps_for_api_key_auth() -> Result<()> { description: Some("Alpha connector".to_string()), logo_url: Some("https://example.com/alpha.png".to_string()), logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: Some("featured".to_string()), branding: None, app_metadata: Some(AppMetadata { diff --git a/codex-rs/chatgpt/src/connectors.rs b/codex-rs/chatgpt/src/connectors.rs index 7089acda5..bc84e5126 100644 --- a/codex-rs/chatgpt/src/connectors.rs +++ b/codex-rs/chatgpt/src/connectors.rs @@ -198,6 +198,8 @@ mod tests { description: None, logo_url: None, logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, branding: None, app_metadata: None, @@ -216,6 +218,8 @@ mod tests { description: None, logo_url: None, logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, branding: None, app_metadata: None, diff --git a/codex-rs/connectors/src/accessible.rs b/codex-rs/connectors/src/accessible.rs index 96eb38550..c42752f6d 100644 --- a/codex-rs/connectors/src/accessible.rs +++ b/codex-rs/connectors/src/accessible.rs @@ -41,6 +41,8 @@ where description: connector_description, logo_url: None, logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, branding: None, app_metadata: None, diff --git a/codex-rs/connectors/src/app_info.rs b/codex-rs/connectors/src/app_info.rs index d31204d7a..bead54b6c 100644 --- a/codex-rs/connectors/src/app_info.rs +++ b/codex-rs/connectors/src/app_info.rs @@ -65,6 +65,8 @@ pub struct AppInfo { pub description: Option, pub logo_url: Option, pub logo_url_dark: Option, + pub icon_assets: Option>, + pub icon_dark_assets: Option>, pub distribution_channel: Option, pub branding: Option, pub app_metadata: Option, diff --git a/codex-rs/connectors/src/filter.rs b/codex-rs/connectors/src/filter.rs index 2f876f91d..3fcabd6fb 100644 --- a/codex-rs/connectors/src/filter.rs +++ b/codex-rs/connectors/src/filter.rs @@ -39,6 +39,8 @@ mod tests { description: None, logo_url: None, logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, install_url: None, branding: None, diff --git a/codex-rs/connectors/src/lib.rs b/codex-rs/connectors/src/lib.rs index f2db9d0d2..aa36e3bce 100644 --- a/codex-rs/connectors/src/lib.rs +++ b/codex-rs/connectors/src/lib.rs @@ -84,6 +84,10 @@ pub struct DirectoryApp { logo_url: Option, #[serde(alias = "logoUrlDark")] logo_url_dark: Option, + #[serde(alias = "iconAssets")] + icon_assets: Option>, + #[serde(alias = "iconDarkAssets")] + icon_dark_assets: Option>, #[serde(alias = "distributionChannel")] distribution_channel: Option, visibility: Option, @@ -279,6 +283,8 @@ fn merge_directory_app(existing: &mut DirectoryApp, incoming: DirectoryApp) { labels, logo_url, logo_url_dark, + icon_assets, + icon_dark_assets, distribution_channel, visibility: _, } = incoming; @@ -302,6 +308,23 @@ fn merge_directory_app(existing: &mut DirectoryApp, incoming: DirectoryApp) { if existing.logo_url_dark.is_none() && logo_url_dark.is_some() { existing.logo_url_dark = logo_url_dark; } + if existing.icon_assets.as_ref().is_none_or(HashMap::is_empty) + && icon_assets + .as_ref() + .is_some_and(|assets| !assets.is_empty()) + { + existing.icon_assets = icon_assets; + } + if existing + .icon_dark_assets + .as_ref() + .is_none_or(HashMap::is_empty) + && icon_dark_assets + .as_ref() + .is_some_and(|assets| !assets.is_empty()) + { + existing.icon_dark_assets = icon_dark_assets; + } if existing.distribution_channel.is_none() && distribution_channel.is_some() { existing.distribution_channel = distribution_channel; } @@ -420,6 +443,8 @@ fn directory_app_to_app_info(app: DirectoryApp) -> AppInfo { description: app.description, logo_url: app.logo_url, logo_url_dark: app.logo_url_dark, + icon_assets: app.icon_assets, + icon_dark_assets: app.icon_dark_assets, distribution_channel: app.distribution_channel, branding: app.branding, app_metadata: app.app_metadata, @@ -512,11 +537,63 @@ mod tests { labels: None, logo_url: None, logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, visibility: None, } } + #[test] + fn directory_app_icon_assets_reach_app_info() -> anyhow::Result<()> { + let response: DirectoryListResponse = serde_json::from_value(serde_json::json!({ + "apps": [{ + "id": "alpha", + "name": "Alpha", + "icon_assets": {}, + "icon_dark_assets": {} + }, { + "id": "alpha", + "name": "", + "icon_assets": { + "256_square": "https://example.com/alpha-square.png" + }, + "icon_dark_assets": { + "256_square": "https://example.com/alpha-square-dark.png" + } + }], + "next_token": null + }))?; + + let app_info = directory_app_to_app_info(merge_directory_apps(response.apps).remove(0)); + + assert_eq!( + serde_json::to_value(app_info)?, + serde_json::json!({ + "id": "alpha", + "name": "Alpha", + "description": null, + "logoUrl": null, + "logoUrlDark": null, + "iconAssets": { + "256_square": "https://example.com/alpha-square.png" + }, + "iconDarkAssets": { + "256_square": "https://example.com/alpha-square-dark.png" + }, + "distributionChannel": null, + "branding": null, + "appMetadata": null, + "labels": null, + "installUrl": null, + "isAccessible": false, + "isEnabled": true, + "pluginDisplayNames": [] + }) + ); + Ok(()) + } + #[tokio::test] #[expect( clippy::await_holding_invalid_type, diff --git a/codex-rs/connectors/src/merge.rs b/codex-rs/connectors/src/merge.rs index cbaf0723c..9f906afc5 100644 --- a/codex-rs/connectors/src/merge.rs +++ b/codex-rs/connectors/src/merge.rs @@ -34,6 +34,12 @@ pub fn merge_connectors( if existing.logo_url_dark.is_none() && connector.logo_url_dark.is_some() { existing.logo_url_dark = connector.logo_url_dark; } + if existing.icon_assets.is_none() && connector.icon_assets.is_some() { + existing.icon_assets = connector.icon_assets; + } + if existing.icon_dark_assets.is_none() && connector.icon_dark_assets.is_some() { + existing.icon_dark_assets = connector.icon_dark_assets; + } if existing.distribution_channel.is_none() && connector.distribution_channel.is_some() { existing.distribution_channel = connector.distribution_channel; } @@ -107,6 +113,8 @@ pub fn plugin_connector_to_app_info(connector_id: String) -> AppInfo { description: None, logo_url: None, logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, branding: None, app_metadata: None, @@ -136,6 +144,8 @@ mod tests { description: Some("Plan events".to_string()), logo_url: Some("https://example.com/logo.png".to_string()), logo_url_dark: Some("https://example.com/logo-dark.png".to_string()), + icon_assets: None, + icon_dark_assets: None, distribution_channel: Some("workspace".to_string()), branding: None, app_metadata: None, @@ -162,6 +172,8 @@ mod tests { description: Some("Plan events".to_string()), logo_url: Some("https://example.com/logo.png".to_string()), logo_url_dark: Some("https://example.com/logo-dark.png".to_string()), + icon_assets: None, + icon_dark_assets: None, distribution_channel: Some("workspace".to_string()), branding: None, app_metadata: None, @@ -192,6 +204,8 @@ mod tests { description: Some("Plan events".to_string()), logo_url: Some("https://example.com/logo.png".to_string()), logo_url_dark: Some("https://example.com/logo-dark.png".to_string()), + icon_assets: None, + icon_dark_assets: None, distribution_channel: Some("workspace".to_string()), branding: None, app_metadata: None, diff --git a/codex-rs/core/src/apps/render.rs b/codex-rs/core/src/apps/render.rs index 98d702249..2331e13f7 100644 --- a/codex-rs/core/src/apps/render.rs +++ b/codex-rs/core/src/apps/render.rs @@ -19,6 +19,8 @@ mod tests { description: None, logo_url: None, logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, branding: None, app_metadata: None, diff --git a/codex-rs/core/src/connectors_tests.rs b/codex-rs/core/src/connectors_tests.rs index 5bbd6a1f8..fb2853904 100644 --- a/codex-rs/core/src/connectors_tests.rs +++ b/codex-rs/core/src/connectors_tests.rs @@ -31,6 +31,8 @@ fn app(id: &str) -> AppInfo { description: None, logo_url: None, logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, install_url: None, branding: None, @@ -129,6 +131,8 @@ fn accessible_connectors_from_mcp_tools_carries_plugin_display_names() { description: None, logo_url: None, logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, install_url: Some(connector_install_url("Google Calendar", "calendar")), branding: None, @@ -168,6 +172,8 @@ fn synthetic_links_are_exposed_to_the_agent_but_not_accessible_in_app_list() { description: None, logo_url: None, logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, install_url: Some(connector_install_url("Calendar", "calendar")), branding: None, @@ -191,6 +197,8 @@ fn synthetic_links_are_exposed_to_the_agent_but_not_accessible_in_app_list() { description: None, logo_url: None, logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, install_url: Some(connector_install_url("Gmail", "gmail")), branding: None, @@ -243,6 +251,8 @@ async fn refresh_accessible_connectors_cache_from_mcp_tools_writes_latest_instal description: None, logo_url: None, logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, install_url: Some(connector_install_url("Google Calendar", "calendar")), branding: None, @@ -258,6 +268,8 @@ async fn refresh_accessible_connectors_cache_from_mcp_tools_writes_latest_instal description: None, logo_url: None, logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, install_url: Some(connector_install_url("Hidden", "connector_openai_hidden")), branding: None, @@ -298,6 +310,8 @@ fn accessible_connectors_from_mcp_tools_preserves_description() { description: Some("Plan events".to_string()), logo_url: None, logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, branding: None, app_metadata: None, diff --git a/codex-rs/core/src/mcp_tool_exposure_test.rs b/codex-rs/core/src/mcp_tool_exposure_test.rs index 71c92b558..ff5617b07 100644 --- a/codex-rs/core/src/mcp_tool_exposure_test.rs +++ b/codex-rs/core/src/mcp_tool_exposure_test.rs @@ -23,6 +23,8 @@ fn make_connector(id: &str, name: &str) -> AppInfo { description: None, logo_url: None, logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, branding: None, app_metadata: None, diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index 5b2e1832c..3d95bc208 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -640,6 +640,8 @@ fn make_connector(id: &str, name: &str) -> AppInfo { description: None, logo_url: None, logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, branding: None, app_metadata: None, diff --git a/codex-rs/core/src/tools/handlers/request_plugin_install_tests.rs b/codex-rs/core/src/tools/handlers/request_plugin_install_tests.rs index f7eadd150..93b761be5 100644 --- a/codex-rs/core/src/tools/handlers/request_plugin_install_tests.rs +++ b/codex-rs/core/src/tools/handlers/request_plugin_install_tests.rs @@ -235,6 +235,8 @@ fn connector_tool(id: &str, name: &str) -> DiscoverableTool { description: None, logo_url: None, logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, branding: None, app_metadata: None, diff --git a/codex-rs/tools/src/request_plugin_install_tests.rs b/codex-rs/tools/src/request_plugin_install_tests.rs index 400126dc6..08bec060f 100644 --- a/codex-rs/tools/src/request_plugin_install_tests.rs +++ b/codex-rs/tools/src/request_plugin_install_tests.rs @@ -11,6 +11,8 @@ fn build_request_plugin_install_elicitation_request_uses_expected_shape() { description: Some("Plan events and schedules.".to_string()), logo_url: None, logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, branding: None, app_metadata: None, @@ -124,6 +126,8 @@ fn build_request_plugin_install_meta_uses_expected_shape() { description: None, logo_url: None, logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, branding: None, app_metadata: None, @@ -165,6 +169,8 @@ fn verified_connector_install_completed_requires_accessible_connector() { description: None, logo_url: None, logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, branding: None, app_metadata: None, @@ -193,6 +199,8 @@ fn all_requested_connectors_picked_up_requires_every_expected_connector() { description: None, logo_url: None, logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, branding: None, app_metadata: None, diff --git a/codex-rs/tools/src/tool_discovery_tests.rs b/codex-rs/tools/src/tool_discovery_tests.rs index 8e026dd30..2ec986ec5 100644 --- a/codex-rs/tools/src/tool_discovery_tests.rs +++ b/codex-rs/tools/src/tool_discovery_tests.rs @@ -26,6 +26,8 @@ fn filter_request_plugin_install_discoverable_tools_for_codex_tui_omits_plugins( description: Some("Plan events and schedules.".to_string()), logo_url: None, logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, branding: None, app_metadata: None, @@ -57,6 +59,8 @@ fn filter_request_plugin_install_discoverable_tools_for_codex_tui_omits_plugins( description: Some("Plan events and schedules.".to_string()), logo_url: None, logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, branding: None, app_metadata: None, diff --git a/codex-rs/tui/src/app_info.rs b/codex-rs/tui/src/app_info.rs index ff97bec98..aee33ec04 100644 --- a/codex-rs/tui/src/app_info.rs +++ b/codex-rs/tui/src/app_info.rs @@ -22,6 +22,8 @@ pub(crate) fn app_info_from_api(app: ApiAppInfo) -> AppInfo { description, logo_url, logo_url_dark, + icon_assets, + icon_dark_assets, distribution_channel, branding, app_metadata, @@ -37,6 +39,8 @@ pub(crate) fn app_info_from_api(app: ApiAppInfo) -> AppInfo { description, logo_url, logo_url_dark, + icon_assets, + icon_dark_assets, distribution_channel, branding: branding.map(app_branding_from_api), app_metadata: app_metadata.map(app_metadata_from_api), diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index 53502b53e..e568436b4 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -6135,6 +6135,8 @@ mod tests { description: Some("Workspace docs".to_string()), logo_url: None, logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, branding: None, app_metadata: None, @@ -6177,6 +6179,8 @@ mod tests { description: Some("Workspace docs".to_string()), logo_url: None, logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, branding: None, app_metadata: None, @@ -6314,6 +6318,8 @@ mod tests { description: Some("Look up events and availability".to_string()), logo_url: None, logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, branding: None, app_metadata: None, @@ -6428,6 +6434,8 @@ mod tests { description: Some("Look up events and availability".to_string()), logo_url: None, logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, branding: None, app_metadata: None, @@ -6462,6 +6470,8 @@ mod tests { description: Some("Workspace docs".to_string()), logo_url: None, logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, branding: None, app_metadata: None, diff --git a/codex-rs/tui/src/chatwidget/skills.rs b/codex-rs/tui/src/chatwidget/skills.rs index 134f44115..c408042d5 100644 --- a/codex-rs/tui/src/chatwidget/skills.rs +++ b/codex-rs/tui/src/chatwidget/skills.rs @@ -521,6 +521,8 @@ mod tests { description: None, logo_url: None, logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, branding: None, app_metadata: None, diff --git a/codex-rs/tui/src/chatwidget/tests/composer_submission.rs b/codex-rs/tui/src/chatwidget/tests/composer_submission.rs index 154add1d6..238bc3609 100644 --- a/codex-rs/tui/src/chatwidget/tests/composer_submission.rs +++ b/codex-rs/tui/src/chatwidget/tests/composer_submission.rs @@ -1284,6 +1284,8 @@ async fn submit_user_message_ignores_inaccessible_app_mentions_from_bindings() { description: Some("Directory-only app".to_string()), logo_url: None, logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, branding: None, app_metadata: None, diff --git a/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs b/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs index 47bff114f..a8053dc5a 100644 --- a/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs +++ b/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs @@ -1926,6 +1926,8 @@ async fn apps_popup_stays_loading_until_final_snapshot_updates() { description: Some("Workspace docs".to_string()), logo_url: None, logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, branding: None, app_metadata: None, @@ -1960,6 +1962,8 @@ async fn apps_popup_stays_loading_until_final_snapshot_updates() { description: Some("Workspace docs".to_string()), logo_url: None, logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, branding: None, app_metadata: None, @@ -1975,6 +1979,8 @@ async fn apps_popup_stays_loading_until_final_snapshot_updates() { description: Some("Project tracking".to_string()), logo_url: None, logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, branding: None, app_metadata: None, @@ -2021,6 +2027,8 @@ async fn apps_notification_update_excludes_inaccessible_apps_from_mentions() { description: Some("Connected files".to_string()), logo_url: None, logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, branding: None, app_metadata: None, @@ -2036,6 +2044,8 @@ async fn apps_notification_update_excludes_inaccessible_apps_from_mentions() { description: Some("Directory-only app".to_string()), logo_url: None, logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, branding: None, app_metadata: None, @@ -2090,6 +2100,8 @@ async fn apps_refresh_failure_keeps_existing_full_snapshot() { description: Some("Workspace docs".to_string()), logo_url: None, logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, branding: None, app_metadata: None, @@ -2105,6 +2117,8 @@ async fn apps_refresh_failure_keeps_existing_full_snapshot() { description: Some("Project tracking".to_string()), logo_url: None, logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, branding: None, app_metadata: None, @@ -2130,6 +2144,8 @@ async fn apps_refresh_failure_keeps_existing_full_snapshot() { description: Some("Workspace docs".to_string()), logo_url: None, logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, branding: None, app_metadata: None, @@ -2179,6 +2195,8 @@ async fn apps_popup_preserves_selected_app_across_refresh() { description: Some("Workspace docs".to_string()), logo_url: None, logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, branding: None, app_metadata: None, @@ -2194,6 +2212,8 @@ async fn apps_popup_preserves_selected_app_across_refresh() { description: Some("Team chat".to_string()), logo_url: None, logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, branding: None, app_metadata: None, @@ -2225,6 +2245,8 @@ async fn apps_popup_preserves_selected_app_across_refresh() { description: Some("Spreadsheets".to_string()), logo_url: None, logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, branding: None, app_metadata: None, @@ -2240,6 +2262,8 @@ async fn apps_popup_preserves_selected_app_across_refresh() { description: Some("Workspace docs".to_string()), logo_url: None, logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, branding: None, app_metadata: None, @@ -2255,6 +2279,8 @@ async fn apps_popup_preserves_selected_app_across_refresh() { description: Some("Team chat".to_string()), logo_url: None, logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, branding: None, app_metadata: None, @@ -2298,6 +2324,8 @@ async fn apps_refresh_failure_with_cached_snapshot_triggers_pending_force_refetc description: Some("Workspace docs".to_string()), logo_url: None, logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, branding: None, app_metadata: None, @@ -2341,6 +2369,8 @@ async fn apps_popup_keeps_existing_full_snapshot_while_partial_refresh_loads() { description: Some("Workspace docs".to_string()), logo_url: None, logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, branding: None, app_metadata: None, @@ -2356,6 +2386,8 @@ async fn apps_popup_keeps_existing_full_snapshot_while_partial_refresh_loads() { description: Some("Project tracking".to_string()), logo_url: None, logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, branding: None, app_metadata: None, @@ -2383,6 +2415,8 @@ async fn apps_popup_keeps_existing_full_snapshot_while_partial_refresh_loads() { description: Some("Workspace docs".to_string()), logo_url: None, logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, branding: None, app_metadata: None, @@ -2398,6 +2432,8 @@ async fn apps_popup_keeps_existing_full_snapshot_while_partial_refresh_loads() { description: Some("Should be filtered".to_string()), logo_url: None, logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, branding: None, app_metadata: None, @@ -2446,6 +2482,8 @@ async fn apps_refresh_failure_without_full_snapshot_falls_back_to_installed_apps description: Some("Workspace docs".to_string()), logo_url: None, logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, branding: None, app_metadata: None, @@ -2505,6 +2543,8 @@ async fn apps_popup_shows_disabled_status_for_installed_but_disabled_apps() { description: Some("Workspace docs".to_string()), logo_url: None, logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, branding: None, app_metadata: None, @@ -2548,6 +2588,8 @@ async fn apps_refresh_preserves_toggled_enabled_state() { description: Some("Workspace docs".to_string()), logo_url: None, logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, branding: None, app_metadata: None, @@ -2570,6 +2612,8 @@ async fn apps_refresh_preserves_toggled_enabled_state() { description: Some("Workspace docs".to_string()), logo_url: None, logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, branding: None, app_metadata: None, @@ -2619,6 +2663,8 @@ async fn apps_popup_for_not_installed_app_uses_install_only_selected_description description: Some("Project tracking".to_string()), logo_url: None, logo_url_dark: None, + icon_assets: None, + icon_dark_assets: None, distribution_channel: None, branding: None, app_metadata: None,