Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 30 additions & 0 deletions codex-rs/app-server-protocol/src/protocol/v2/apps.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,33 @@ pub struct AppInfo {
pub plugin_display_names: Vec<String>,
}

impl AppInfo {
pub fn category(&self) -> Option<String> {
self.branding
.as_ref()
.and_then(|branding| non_empty_category(branding.category.as_deref()))
.or_else(|| {
self.app_metadata
.as_ref()
.and_then(|metadata| metadata.categories.as_ref())
.and_then(|categories| {
categories
.iter()
.find_map(|category| non_empty_category(Some(category.as_str())))
})
})
}
}

fn non_empty_category(category: Option<&str>) -> Option<String> {
let category = category?.trim();
if category.is_empty() {
None
} else {
Some(category.to_string())
}
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
Expand All @@ -111,15 +138,18 @@ pub struct AppSummary {
pub name: String,
pub description: Option<String>,
pub install_url: Option<String>,
pub category: Option<String>,
}

impl From<AppInfo> for AppSummary {
fn from(value: AppInfo) -> Self {
let category = value.category();
Self {
id: value.id,
name: value.name,
description: value.description,
install_url: value.install_url,
category,
}
}
}
Expand Down
1 change: 1 addition & 0 deletions codex-rs/app-server-protocol/src/protocol/v2/plugin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -661,6 +661,7 @@ pub struct AppTemplateSummary {
pub template_id: String,
pub name: String,
pub description: Option<String>,
pub category: Option<String>,
pub canonical_connector_id: Option<String>,
pub logo_url: Option<String>,
pub logo_url_dark: Option<String>,
Expand Down
75 changes: 62 additions & 13 deletions codex-rs/app-server/src/request_processors/plugins.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1035,7 +1035,12 @@ impl PluginRequestProcessor {
}
None => None,
};
let app_summaries = load_plugin_app_summaries(&config, &outcome.plugin.apps).await;
let app_summaries = load_plugin_app_summaries(
&config,
&outcome.plugin.apps,
&outcome.plugin.app_category_by_id,
)
.await;
let visible_skills = outcome
.plugin
.skills
Expand Down Expand Up @@ -1111,7 +1116,13 @@ impl PluginRequestProcessor {
.cloned()
.map(codex_plugin::AppConnectorId)
.collect::<Vec<_>>();
let app_summaries = load_plugin_app_summaries(&config, &plugin_apps).await;
let app_category_by_id = remote_detail
.app_manifest
.as_ref()
.map(plugin_app_category_by_id_from_value)
.unwrap_or_default();
let app_summaries =
load_plugin_app_summaries(&config, &plugin_apps, &app_category_by_id).await;
remote_plugin_detail_to_info(remote_detail, app_summaries)
}
};
Expand Down Expand Up @@ -1558,16 +1569,28 @@ impl PluginRequestProcessor {
.into_iter()
.map(codex_plugin::AppConnectorId)
.collect::<Vec<_>>();
let app_category_by_id = remote_detail
.app_manifest
.as_ref()
.map(plugin_app_category_by_id_from_value)
.unwrap_or_default();
let all_connectors = connectors::list_cached_all_connectors(&config)
.await
.unwrap_or_default();
connectors::connectors_for_plugin_apps(all_connectors, &plugin_apps)
.into_iter()
.map(|connector| AppSummary {
id: connector.id,
name: connector.name,
description: connector.description,
install_url: connector.install_url,
.map(|connector| {
let category = app_category_by_id
.get(&connector.id)
.cloned()
.or_else(|| connector.category());
AppSummary {
category,
id: connector.id,
name: connector.name,
description: connector.description,
install_url: connector.install_url,
}
})
.collect()
}
Expand Down Expand Up @@ -1872,6 +1895,7 @@ impl PluginRequestProcessor {
async fn load_plugin_app_summaries(
config: &Config,
plugin_apps: &[codex_plugin::AppConnectorId],
app_category_by_id: &HashMap<String, String>,
) -> Vec<AppSummary> {
if plugin_apps.is_empty() {
return Vec::new();
Expand All @@ -1889,9 +1913,29 @@ async fn load_plugin_app_summaries(
};

let plugin_connectors = connectors::connectors_for_plugin_apps(connectors, plugin_apps);

plugin_connectors
.into_iter()
.map(AppSummary::from)
.map(|connector| {
let category = app_category_by_id
.get(&connector.id)
.cloned()
.or_else(|| connector.category());
AppSummary {
id: connector.id,
name: connector.name,
description: connector.description,
install_url: connector.install_url,
category,
}
})
.collect()
}

fn plugin_app_category_by_id_from_value(value: &serde_json::Value) -> HashMap<String, String> {
codex_core_plugins::loader::plugin_app_metadata_from_value(value)
.into_iter()
.filter_map(|app| app.category.map(|category| (app.id.0, category)))
.collect()
}

Expand Down Expand Up @@ -1921,11 +1965,15 @@ fn plugin_apps_needing_auth(
&& !accessible_ids.contains(connector.id.as_str())
})
.cloned()
.map(|connector| AppSummary {
id: connector.id,
name: connector.name,
description: connector.description,
install_url: connector.install_url,
.map(|connector| {
let category = connector.category();
AppSummary {
category,
id: connector.id,
name: connector.name,
description: connector.description,
install_url: connector.install_url,
}
})
.collect()
}
Expand Down Expand Up @@ -2013,6 +2061,7 @@ fn remote_plugin_detail_to_info(
template_id: template.template_id,
name: template.name,
description: template.description,
category: template.category,
canonical_connector_id: template.canonical_connector_id,
logo_url: template.logo_url,
logo_url_dark: template.logo_url_dark,
Expand Down
20 changes: 19 additions & 1 deletion codex-rs/app-server/tests/suite/v2/plugin_install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -292,14 +292,29 @@ async fn plugin_install_writes_remote_plugin_to_cloud_and_cache() -> Result<()>
async fn plugin_install_uses_remote_apps_needing_auth_response() -> Result<()> {
let codex_home = TempDir::new()?;
let server = MockServer::start().await;
let remote_app_manifest = json!({
"apps": {
"alpha": {
"id": "alpha",
"category": "Developer Tools"
}
}
});
let bundle_url = mount_remote_plugin_bundle(
&server,
/*status_code*/ 200,
remote_plugin_bundle_tar_gz_bytes("linear")?,
)
.await;
configure_remote_plugin_with_apps_test(codex_home.path(), &server)?;
mount_remote_plugin_detail(&server, REMOTE_PLUGIN_ID, "1.2.3", Some(&bundle_url)).await;
mount_remote_plugin_detail_with_app_manifest(
&server,
REMOTE_PLUGIN_ID,
"1.2.3",
Some(&bundle_url),
remote_app_manifest,
)
.await;
mount_empty_remote_installed_plugins(&server).await;
mount_remote_plugin_install_with_apps_needing_auth(&server, REMOTE_PLUGIN_ID, &["alpha"]).await;

Expand Down Expand Up @@ -327,6 +342,7 @@ async fn plugin_install_uses_remote_apps_needing_auth_response() -> Result<()> {
name: "alpha".to_string(),
description: None,
install_url: Some("https://chatgpt.com/apps/alpha/alpha".to_string()),
category: Some("Developer Tools".to_string()),
}],
}
);
Expand Down Expand Up @@ -1000,6 +1016,7 @@ async fn plugin_install_returns_apps_needing_auth() -> Result<()> {
name: "Alpha".to_string(),
description: Some("Alpha connector".to_string()),
install_url: Some("https://chatgpt.com/apps/alpha/alpha".to_string()),
category: None,
}],
}
);
Expand Down Expand Up @@ -1087,6 +1104,7 @@ async fn plugin_install_filters_disallowed_apps_needing_auth() -> Result<()> {
name: "Alpha".to_string(),
description: Some("Alpha connector".to_string()),
install_url: Some("https://chatgpt.com/apps/alpha/alpha".to_string()),
category: None,
}],
}
);
Expand Down
Loading
Loading