mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
[plugins] Add dark-mode logo metadata (#29488)
Adds additive dark-mode plugin logo metadata across manifests, remote catalogs, and the app-server protocol while keeping uninstalled Git listings free of synthetic local paths. Supersedes #28945. This replacement uses an upstream branch so trusted CI can use the repository-provided remote Bazel configuration. ## Current state Plugin interfaces expose only the default logo asset. Clients therefore cannot select a dedicated dark-mode logo even when a plugin provides one. ## What this PR changes - Adds nullable `logoDark` and `logoUrlDark` fields to `PluginInterface`. - Resolves local `interface.logoDark` assets and maps remote `logo_url_dark` values. - Removes path-backed interface assets, including `logoDark`, from uninstalled Git fallback listings until the plugin has a real local root. - Updates the bundled plugin validator and manifest reference. - Regenerates the app-server JSON schemas and TypeScript types. Local manifests expose `interface.logoDark` as a package-relative asset path. Remote catalog responses expose `logo_url_dark`. These values map into separate app-server fields so clients can preserve local-path and remote-URL handling. ## Risk The fields are additive and nullable, so existing clients retain their current logo behavior. The main risks are an incomplete mapping path or exposing a synthetic local path for an uninstalled Git plugin. Local-manifest, remote-catalog, fallback-listing, protocol serialization, and app-server integration tests cover those paths. Spiciness: 2/5 ## Testing - `just write-app-server-schema` - `just fmt` - Regression test first failed with `logo_dark` resolved to `/assets/logo-dark.png`, then passed after the fallback-listing fix. - `just test -p codex-core-plugins` (267 tests passed) - `just test -p codex-app-server 'suite::v2::plugin'` (114 tests passed) - `just test -p codex-app-server-protocol -p codex-core-plugins -p codex-plugin -p codex-skills` (517 tests passed before the follow-up) - `just test -p codex-tui plugin` (47 tests passed) - Validated a local plugin manifest containing `interface.logoDark` with the bundled validator. ## Manual verification Create a local plugin with both `interface.logo` and `interface.logoDark`, then call `plugin/list` or `plugin/read`. Confirm the response contains separate `logo` and `logoDark` paths. For a remote catalog entry, confirm `logoUrlDark` is populated from `logo_url_dark`. For an uninstalled Git marketplace entry, confirm path-backed interface assets remain absent until installation. Issue: N/A - coordinated maintainer change.
This commit is contained in:
+18
@@ -13407,6 +13407,17 @@
|
||||
],
|
||||
"description": "Local logo path, resolved from the installed plugin package."
|
||||
},
|
||||
"logoDark": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/AbsolutePathBuf"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Local dark-mode logo path, resolved from the installed plugin package."
|
||||
},
|
||||
"logoUrl": {
|
||||
"description": "Remote logo URL from the plugin catalog.",
|
||||
"type": [
|
||||
@@ -13414,6 +13425,13 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"logoUrlDark": {
|
||||
"description": "Remote dark-mode logo URL from the plugin catalog.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"longDescription": {
|
||||
"type": [
|
||||
"string",
|
||||
|
||||
+18
@@ -9811,6 +9811,17 @@
|
||||
],
|
||||
"description": "Local logo path, resolved from the installed plugin package."
|
||||
},
|
||||
"logoDark": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Local dark-mode logo path, resolved from the installed plugin package."
|
||||
},
|
||||
"logoUrl": {
|
||||
"description": "Remote logo URL from the plugin catalog.",
|
||||
"type": [
|
||||
@@ -9818,6 +9829,13 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"logoUrlDark": {
|
||||
"description": "Remote dark-mode logo URL from the plugin catalog.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"longDescription": {
|
||||
"type": [
|
||||
"string",
|
||||
|
||||
@@ -134,6 +134,17 @@
|
||||
],
|
||||
"description": "Local logo path, resolved from the installed plugin package."
|
||||
},
|
||||
"logoDark": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Local dark-mode logo path, resolved from the installed plugin package."
|
||||
},
|
||||
"logoUrl": {
|
||||
"description": "Remote logo URL from the plugin catalog.",
|
||||
"type": [
|
||||
@@ -141,6 +152,13 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"logoUrlDark": {
|
||||
"description": "Remote dark-mode logo URL from the plugin catalog.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"longDescription": {
|
||||
"type": [
|
||||
"string",
|
||||
|
||||
@@ -134,6 +134,17 @@
|
||||
],
|
||||
"description": "Local logo path, resolved from the installed plugin package."
|
||||
},
|
||||
"logoDark": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Local dark-mode logo path, resolved from the installed plugin package."
|
||||
},
|
||||
"logoUrl": {
|
||||
"description": "Remote logo URL from the plugin catalog.",
|
||||
"type": [
|
||||
@@ -141,6 +152,13 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"logoUrlDark": {
|
||||
"description": "Remote dark-mode logo URL from the plugin catalog.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"longDescription": {
|
||||
"type": [
|
||||
"string",
|
||||
|
||||
@@ -313,6 +313,17 @@
|
||||
],
|
||||
"description": "Local logo path, resolved from the installed plugin package."
|
||||
},
|
||||
"logoDark": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Local dark-mode logo path, resolved from the installed plugin package."
|
||||
},
|
||||
"logoUrl": {
|
||||
"description": "Remote logo URL from the plugin catalog.",
|
||||
"type": [
|
||||
@@ -320,6 +331,13 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"logoUrlDark": {
|
||||
"description": "Remote dark-mode logo URL from the plugin catalog.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"longDescription": {
|
||||
"type": [
|
||||
"string",
|
||||
|
||||
@@ -108,6 +108,17 @@
|
||||
],
|
||||
"description": "Local logo path, resolved from the installed plugin package."
|
||||
},
|
||||
"logoDark": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Local dark-mode logo path, resolved from the installed plugin package."
|
||||
},
|
||||
"logoUrl": {
|
||||
"description": "Remote logo URL from the plugin catalog.",
|
||||
"type": [
|
||||
@@ -115,6 +126,13 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"logoUrlDark": {
|
||||
"description": "Remote dark-mode logo URL from the plugin catalog.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"longDescription": {
|
||||
"type": [
|
||||
"string",
|
||||
|
||||
@@ -21,10 +21,18 @@ composerIconUrl: string | null,
|
||||
* Local logo path, resolved from the installed plugin package.
|
||||
*/
|
||||
logo: AbsolutePathBuf | null,
|
||||
/**
|
||||
* Local dark-mode logo path, resolved from the installed plugin package.
|
||||
*/
|
||||
logoDark: AbsolutePathBuf | null,
|
||||
/**
|
||||
* Remote logo URL from the plugin catalog.
|
||||
*/
|
||||
logoUrl: string | null,
|
||||
/**
|
||||
* Remote dark-mode logo URL from the plugin catalog.
|
||||
*/
|
||||
logoUrlDark: string | null,
|
||||
/**
|
||||
* Local screenshot paths, resolved from the installed plugin package.
|
||||
*/
|
||||
|
||||
@@ -716,8 +716,12 @@ pub struct PluginInterface {
|
||||
pub composer_icon_url: Option<String>,
|
||||
/// Local logo path, resolved from the installed plugin package.
|
||||
pub logo: Option<AbsolutePathBuf>,
|
||||
/// Local dark-mode logo path, resolved from the installed plugin package.
|
||||
pub logo_dark: Option<AbsolutePathBuf>,
|
||||
/// Remote logo URL from the plugin catalog.
|
||||
pub logo_url: Option<String>,
|
||||
/// Remote dark-mode logo URL from the plugin catalog.
|
||||
pub logo_url_dark: Option<String>,
|
||||
/// Local screenshot paths, resolved from the installed plugin package.
|
||||
pub screenshots: Vec<AbsolutePathBuf>,
|
||||
/// Remote screenshot URLs from the plugin catalog.
|
||||
|
||||
@@ -2938,6 +2938,13 @@ fn plugin_interface_serializes_local_paths_and_remote_urls_separately() {
|
||||
};
|
||||
let composer_icon = AbsolutePathBuf::try_from(PathBuf::from(composer_icon)).unwrap();
|
||||
let composer_icon_json = composer_icon.as_path().display().to_string();
|
||||
let logo_dark = if cfg!(windows) {
|
||||
r"C:\plugins\linear\logo-dark.png"
|
||||
} else {
|
||||
"/plugins/linear/logo-dark.png"
|
||||
};
|
||||
let logo_dark = AbsolutePathBuf::try_from(PathBuf::from(logo_dark)).unwrap();
|
||||
let logo_dark_json = logo_dark.as_path().display().to_string();
|
||||
|
||||
let interface = PluginInterface {
|
||||
display_name: Some("Linear".to_string()),
|
||||
@@ -2954,7 +2961,9 @@ fn plugin_interface_serializes_local_paths_and_remote_urls_separately() {
|
||||
composer_icon: Some(composer_icon),
|
||||
composer_icon_url: Some("https://example.com/linear/icon.png".to_string()),
|
||||
logo: None,
|
||||
logo_dark: Some(logo_dark),
|
||||
logo_url: Some("https://example.com/linear/logo.png".to_string()),
|
||||
logo_url_dark: Some("https://example.com/linear/logo-dark.png".to_string()),
|
||||
screenshots: Vec::new(),
|
||||
screenshot_urls: vec!["https://example.com/linear/screenshot.png".to_string()],
|
||||
};
|
||||
@@ -2976,7 +2985,9 @@ fn plugin_interface_serializes_local_paths_and_remote_urls_separately() {
|
||||
"composerIcon": composer_icon_json,
|
||||
"composerIconUrl": "https://example.com/linear/icon.png",
|
||||
"logo": null,
|
||||
"logoDark": logo_dark_json,
|
||||
"logoUrl": "https://example.com/linear/logo.png",
|
||||
"logoUrlDark": "https://example.com/linear/logo-dark.png",
|
||||
"screenshots": [],
|
||||
"screenshotUrls": ["https://example.com/linear/screenshot.png"],
|
||||
}),
|
||||
|
||||
@@ -77,7 +77,9 @@ fn local_plugin_interface_to_info(interface: PluginManifestInterface) -> PluginI
|
||||
composer_icon: interface.composer_icon,
|
||||
composer_icon_url: None,
|
||||
logo: interface.logo,
|
||||
logo_dark: interface.logo_dark,
|
||||
logo_url: None,
|
||||
logo_url_dark: None,
|
||||
screenshots: interface.screenshots,
|
||||
screenshot_urls: Vec::new(),
|
||||
}
|
||||
|
||||
@@ -753,7 +753,9 @@ async fn plugin_list_uses_alternate_discoverable_manifest_and_keeps_undiscoverab
|
||||
composer_icon: None,
|
||||
composer_icon_url: None,
|
||||
logo: None,
|
||||
logo_dark: None,
|
||||
logo_url: None,
|
||||
logo_url_dark: None,
|
||||
screenshots: Vec::new(),
|
||||
screenshot_urls: Vec::new(),
|
||||
}),
|
||||
|
||||
@@ -162,7 +162,8 @@ apps = true
|
||||
"short_description": "Example plugin",
|
||||
"capabilities": [],
|
||||
"default_prompt": "Use the legacy example prompt",
|
||||
"default_prompts": []
|
||||
"default_prompts": [],
|
||||
"logo_url_dark": "https://example.com/example-plugin-dark.png"
|
||||
},
|
||||
"skills": [],
|
||||
"mcp_servers": [
|
||||
@@ -273,6 +274,15 @@ apps = true
|
||||
.and_then(|interface| interface.default_prompt.clone()),
|
||||
Some(vec!["Use the legacy example prompt".to_string()])
|
||||
);
|
||||
assert_eq!(
|
||||
response
|
||||
.plugin
|
||||
.summary
|
||||
.interface
|
||||
.as_ref()
|
||||
.and_then(|interface| interface.logo_url_dark.as_deref()),
|
||||
Some("https://example.com/example-plugin-dark.png")
|
||||
);
|
||||
assert_eq!(
|
||||
response.plugin.mcp_servers,
|
||||
vec!["other-server".to_string()]
|
||||
@@ -1330,6 +1340,7 @@ async fn plugin_read_returns_plugin_details_with_bundle_contents() -> Result<()>
|
||||
"brandColor": "#3B82F6",
|
||||
"composerIcon": "./assets/icon.png",
|
||||
"logo": "./assets/logo.png",
|
||||
"logoDark": "./assets/logo-dark.png",
|
||||
"screenshots": ["./assets/screenshot1.png"]
|
||||
}
|
||||
}"##,
|
||||
@@ -1508,6 +1519,18 @@ enabled = false
|
||||
"Find my next action".to_string()
|
||||
])
|
||||
);
|
||||
assert_eq!(
|
||||
response
|
||||
.plugin
|
||||
.summary
|
||||
.interface
|
||||
.as_ref()
|
||||
.and_then(|interface| interface.logo_dark.as_ref()),
|
||||
Some(
|
||||
&AbsolutePathBuf::try_from(plugin_root.join("assets/logo-dark.png"))
|
||||
.expect("absolute dark logo path")
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
response.plugin.summary.keywords,
|
||||
vec!["api-key".to_string(), "developer tools".to_string()]
|
||||
|
||||
@@ -1357,7 +1357,9 @@ fn expected_plugin_interface() -> PluginInterface {
|
||||
composer_icon: None,
|
||||
composer_icon_url: None,
|
||||
logo: None,
|
||||
logo_dark: None,
|
||||
logo_url: None,
|
||||
logo_url_dark: None,
|
||||
screenshots: Vec::new(),
|
||||
screenshot_urls: Vec::new(),
|
||||
}
|
||||
|
||||
@@ -1032,7 +1032,9 @@ async fn build_remote_installed_plugin_marketplaces_from_cache_uses_remote_metad
|
||||
composer_icon: None,
|
||||
composer_icon_url: None,
|
||||
logo: None,
|
||||
logo_dark: None,
|
||||
logo_url: None,
|
||||
logo_url_dark: None,
|
||||
screenshots: Vec::new(),
|
||||
screenshot_urls: Vec::new(),
|
||||
});
|
||||
|
||||
@@ -74,6 +74,8 @@ struct RawPluginManifestInterface {
|
||||
#[serde(default)]
|
||||
logo: Option<String>,
|
||||
#[serde(default)]
|
||||
logo_dark: Option<String>,
|
||||
#[serde(default)]
|
||||
screenshots: Vec<String>,
|
||||
}
|
||||
|
||||
@@ -175,6 +177,7 @@ pub(crate) fn parse_plugin_manifest(
|
||||
brand_color,
|
||||
composer_icon,
|
||||
logo,
|
||||
logo_dark,
|
||||
screenshots,
|
||||
} = interface;
|
||||
|
||||
@@ -196,6 +199,11 @@ pub(crate) fn parse_plugin_manifest(
|
||||
composer_icon.as_deref(),
|
||||
),
|
||||
logo: resolve_interface_asset_path(plugin_root, "interface.logo", logo.as_deref()),
|
||||
logo_dark: resolve_interface_asset_path(
|
||||
plugin_root,
|
||||
"interface.logoDark",
|
||||
logo_dark.as_deref(),
|
||||
),
|
||||
screenshots: screenshots
|
||||
.iter()
|
||||
.filter_map(|screenshot| {
|
||||
@@ -221,6 +229,7 @@ pub(crate) fn parse_plugin_manifest(
|
||||
|| interface.brand_color.is_some()
|
||||
|| interface.composer_icon.is_some()
|
||||
|| interface.logo.is_some()
|
||||
|| interface.logo_dark.is_some()
|
||||
|| !interface.screenshots.is_empty();
|
||||
|
||||
has_fields.then_some(interface)
|
||||
@@ -594,6 +603,32 @@ mod tests {
|
||||
assert_eq!(interface.default_prompt, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugin_interface_reads_dark_logo_path() {
|
||||
let tmp = tempdir().expect("tempdir");
|
||||
let plugin_root = tmp.path().join("demo-plugin");
|
||||
write_manifest(
|
||||
&plugin_root,
|
||||
/*version*/ None,
|
||||
r#"{
|
||||
"logoDark": "./assets/logo-dark.svg"
|
||||
}"#,
|
||||
);
|
||||
|
||||
let manifest = load_manifest(&plugin_root);
|
||||
let interface = manifest.interface.expect("plugin interface");
|
||||
|
||||
assert_eq!(
|
||||
interface.logo_dark,
|
||||
Some(
|
||||
AbsolutePathBuf::from_absolute_path_checked(
|
||||
plugin_root.join("assets/logo-dark.svg"),
|
||||
)
|
||||
.expect("absolute dark logo path")
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugin_manifest_reads_trimmed_version() {
|
||||
let tmp = tempdir().expect("tempdir");
|
||||
|
||||
@@ -103,6 +103,7 @@ impl MarketplacePluginManifestFallback {
|
||||
if let Some(interface) = manifest.interface.as_mut() {
|
||||
interface.composer_icon = None;
|
||||
interface.logo = None;
|
||||
interface.logo_dark = None;
|
||||
interface.screenshots.clear();
|
||||
}
|
||||
Some(manifest)
|
||||
|
||||
@@ -180,6 +180,51 @@ fn find_marketplace_plugin_supports_git_subdir_sources() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_marketplace_plugin_omits_interface_asset_paths_for_git_sources() {
|
||||
let tmp = tempdir().unwrap();
|
||||
let repo_root = tmp.path().join("repo");
|
||||
fs::create_dir_all(repo_root.join(".git")).unwrap();
|
||||
fs::create_dir_all(repo_root.join(".agents/plugins")).unwrap();
|
||||
fs::write(
|
||||
repo_root.join(".agents/plugins/marketplace.json"),
|
||||
r#"{
|
||||
"name": "codex-curated",
|
||||
"plugins": [
|
||||
{
|
||||
"name": "remote-plugin",
|
||||
"source": {
|
||||
"source": "git-subdir",
|
||||
"url": "openai/joey_marketplace3",
|
||||
"path": "plugins/toolkit"
|
||||
},
|
||||
"interface": {
|
||||
"displayName": "Remote Plugin",
|
||||
"composerIcon": "./assets/icon.svg",
|
||||
"logo": "./assets/logo.png",
|
||||
"logoDark": "./assets/logo-dark.png",
|
||||
"screenshots": ["./assets/shot.png"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let resolved = find_marketplace_plugin(
|
||||
&AbsolutePathBuf::try_from(repo_root.join(".agents/plugins/marketplace.json")).unwrap(),
|
||||
"remote-plugin",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let interface = resolved.interface.expect("fallback interface");
|
||||
assert_eq!(interface.display_name.as_deref(), Some("Remote Plugin"));
|
||||
assert_eq!(interface.composer_icon, None);
|
||||
assert_eq!(interface.logo, None);
|
||||
assert_eq!(interface.logo_dark, None);
|
||||
assert!(interface.screenshots.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_marketplace_plugin_builds_manifest_fallback_from_entry() {
|
||||
let tmp = tempdir().unwrap();
|
||||
@@ -319,6 +364,7 @@ fn find_marketplace_plugin_builds_manifest_fallback_from_entry() {
|
||||
AbsolutePathBuf::try_from(plugin_root.join("assets/icon.svg")).unwrap()
|
||||
),
|
||||
logo: Some(AbsolutePathBuf::try_from(plugin_root.join("assets/logo.png")).unwrap()),
|
||||
logo_dark: None,
|
||||
screenshots: vec![
|
||||
AbsolutePathBuf::try_from(plugin_root.join("assets/shot.png")).unwrap()
|
||||
],
|
||||
@@ -634,6 +680,7 @@ fn list_marketplaces_supports_alternate_manifest_layout() {
|
||||
brand_color: None,
|
||||
composer_icon: None,
|
||||
logo: None,
|
||||
logo_dark: None,
|
||||
screenshots: Vec::new(),
|
||||
}),
|
||||
keywords: Vec::new(),
|
||||
@@ -720,6 +767,7 @@ fn list_marketplaces_supports_repo_root_local_plugin_sources() {
|
||||
brand_color: None,
|
||||
composer_icon: None,
|
||||
logo: None,
|
||||
logo_dark: None,
|
||||
screenshots: Vec::new(),
|
||||
}),
|
||||
keywords: Vec::new(),
|
||||
@@ -1625,6 +1673,7 @@ fn list_marketplaces_resolves_plugin_interface_paths_to_absolute() {
|
||||
AbsolutePathBuf::try_from(plugin_root.join("assets/icon.png")).unwrap(),
|
||||
),
|
||||
logo: Some(AbsolutePathBuf::try_from(plugin_root.join("assets/logo.png")).unwrap()),
|
||||
logo_dark: None,
|
||||
screenshots: vec![
|
||||
AbsolutePathBuf::try_from(plugin_root.join("assets/shot1.png")).unwrap(),
|
||||
],
|
||||
@@ -1739,6 +1788,7 @@ fn list_marketplaces_ignores_plugin_interface_assets_without_dot_slash() {
|
||||
brand_color: None,
|
||||
composer_icon: None,
|
||||
logo: None,
|
||||
logo_dark: None,
|
||||
screenshots: Vec::new(),
|
||||
})
|
||||
);
|
||||
|
||||
@@ -463,6 +463,7 @@ struct RemotePluginReleaseInterfaceResponse {
|
||||
default_prompts: Option<Vec<String>>,
|
||||
composer_icon_url: Option<String>,
|
||||
logo_url: Option<String>,
|
||||
logo_url_dark: Option<String>,
|
||||
#[serde(default)]
|
||||
screenshot_urls: Vec<String>,
|
||||
}
|
||||
@@ -1529,7 +1530,9 @@ fn remote_plugin_interface_to_info(plugin: &RemotePluginDirectoryItem) -> Option
|
||||
composer_icon: None,
|
||||
composer_icon_url: interface.composer_icon_url.clone(),
|
||||
logo: None,
|
||||
logo_dark: None,
|
||||
logo_url: interface.logo_url.clone(),
|
||||
logo_url_dark: interface.logo_url_dark.clone(),
|
||||
screenshots: Vec::new(),
|
||||
screenshot_urls: interface.screenshot_urls.clone(),
|
||||
};
|
||||
@@ -1546,6 +1549,7 @@ fn remote_plugin_interface_to_info(plugin: &RemotePluginDirectoryItem) -> Option
|
||||
|| result.brand_color.is_some()
|
||||
|| result.composer_icon_url.is_some()
|
||||
|| result.logo_url.is_some()
|
||||
|| result.logo_url_dark.is_some()
|
||||
|| !result.screenshot_urls.is_empty();
|
||||
has_fields.then_some(result)
|
||||
}
|
||||
|
||||
@@ -156,7 +156,9 @@ fn expected_plugin_interface() -> PluginInterface {
|
||||
composer_icon: None,
|
||||
composer_icon_url: None,
|
||||
logo: None,
|
||||
logo_dark: None,
|
||||
logo_url: None,
|
||||
logo_url_dark: None,
|
||||
screenshots: Vec::new(),
|
||||
screenshot_urls: Vec::new(),
|
||||
}
|
||||
|
||||
@@ -69,6 +69,7 @@ fn directory_plugin(id: &str, name: &str) -> RemotePluginDirectoryItem {
|
||||
default_prompts: None,
|
||||
composer_icon_url: None,
|
||||
logo_url: None,
|
||||
logo_url_dark: None,
|
||||
screenshot_urls: Vec::new(),
|
||||
},
|
||||
skills: Vec::new(),
|
||||
@@ -76,6 +77,20 @@ fn directory_plugin(id: &str, name: &str) -> RemotePluginDirectoryItem {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remote_plugin_interface_maps_dark_logo_url() {
|
||||
let mut plugin = directory_plugin("plugin-linear", "linear");
|
||||
plugin.release.interface.logo_url_dark =
|
||||
Some("https://example.com/linear/logo-dark.png".to_string());
|
||||
|
||||
assert_eq!(
|
||||
remote_plugin_interface_to_info(&plugin)
|
||||
.expect("plugin interface")
|
||||
.logo_url_dark,
|
||||
Some("https://example.com/linear/logo-dark.png".to_string())
|
||||
);
|
||||
}
|
||||
fn item(name: &str, display_name: &str) -> RecommendedPluginItem {
|
||||
RecommendedPluginItem {
|
||||
id: format!("plugin_{name}"),
|
||||
|
||||
@@ -53,6 +53,7 @@ pub struct PluginManifestInterface<Resource> {
|
||||
pub brand_color: Option<String>,
|
||||
pub composer_icon: Option<Resource>,
|
||||
pub logo: Option<Resource>,
|
||||
pub logo_dark: Option<Resource>,
|
||||
pub screenshots: Vec<Resource>,
|
||||
}
|
||||
|
||||
@@ -72,6 +73,7 @@ impl<Resource> Default for PluginManifestInterface<Resource> {
|
||||
brand_color: None,
|
||||
composer_icon: None,
|
||||
logo: None,
|
||||
logo_dark: None,
|
||||
screenshots: Vec::new(),
|
||||
}
|
||||
}
|
||||
@@ -141,6 +143,7 @@ impl<Resource> PluginManifest<Resource> {
|
||||
brand_color,
|
||||
composer_icon,
|
||||
logo,
|
||||
logo_dark,
|
||||
screenshots,
|
||||
} = interface;
|
||||
Some(PluginManifestInterface {
|
||||
@@ -157,6 +160,7 @@ impl<Resource> PluginManifest<Resource> {
|
||||
brand_color,
|
||||
composer_icon: composer_icon.map(&mut map).transpose()?,
|
||||
logo: logo.map(&mut map).transpose()?,
|
||||
logo_dark: logo_dark.map(&mut map).transpose()?,
|
||||
screenshots: screenshots
|
||||
.into_iter()
|
||||
.map(&mut map)
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"brandColor": "#3B82F6",
|
||||
"composerIcon": "./assets/icon.png",
|
||||
"logo": "./assets/logo.png",
|
||||
"logoDark": "./assets/logo-dark.png",
|
||||
"screenshots": [
|
||||
"./assets/screenshot1.png",
|
||||
"./assets/screenshot2.png",
|
||||
@@ -105,6 +106,7 @@ Or as an object directly in `plugin.json`:
|
||||
- `brandColor` (`string`): Theme color for the plugin card.
|
||||
- `composerIcon` (`string`): Path to icon asset.
|
||||
- `logo` (`string`): Path to logo asset.
|
||||
- `logoDark` (`string`): Optional path to the logo asset used in dark mode.
|
||||
- `screenshots` (`array` of `string`): List of screenshot asset paths.
|
||||
- Screenshot entries must be PNG filenames and stored under `./assets/`.
|
||||
- Keep file paths relative to plugin root.
|
||||
@@ -205,7 +207,7 @@ personal marketplace unless the caller explicitly requests a repo-local destinat
|
||||
- `version` must use strict semver.
|
||||
- `websiteURL`, `privacyPolicyURL`, and `termsOfServiceURL` must be absolute `https://` URLs when
|
||||
present.
|
||||
- `composerIcon`, `logo`, and `screenshots` must point to real files inside the plugin archive when
|
||||
- `composerIcon`, `logo`, `logoDark`, and `screenshots` must point to real files inside the plugin archive when
|
||||
present.
|
||||
- `apps` should appear in `plugin.json` only when `.app.json` actually exists.
|
||||
- `mcpServers` may point to `.mcp.json` or contain the MCP server object directly in
|
||||
|
||||
@@ -153,6 +153,7 @@ def validate_manifest_shape(
|
||||
"brandColor",
|
||||
"composerIcon",
|
||||
"logo",
|
||||
"logoDark",
|
||||
"screenshots",
|
||||
"defaultPrompt",
|
||||
"default_prompt",
|
||||
@@ -184,7 +185,7 @@ def validate_manifest_shape(
|
||||
not isinstance(brand_color, str) or HEX_COLOR_RE.fullmatch(brand_color) is None
|
||||
):
|
||||
errors.append("plugin.json field `interface.brandColor` must use `#RRGGBB`")
|
||||
for field in ("composerIcon", "logo"):
|
||||
for field in ("composerIcon", "logo", "logoDark"):
|
||||
validate_optional_asset_path(plugin_root, plugin_root, interface, field, errors)
|
||||
screenshots = interface.get("screenshots", [])
|
||||
if not isinstance(screenshots, list):
|
||||
|
||||
@@ -1304,7 +1304,9 @@ pub(super) fn plugins_test_interface(
|
||||
composer_icon: None,
|
||||
composer_icon_url: None,
|
||||
logo: None,
|
||||
logo_dark: None,
|
||||
logo_url: None,
|
||||
logo_url_dark: None,
|
||||
screenshots: Vec::new(),
|
||||
screenshot_urls: Vec::new(),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user