[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:
Drew authored and GitHub committed 2026-06-22 16:01:27 -07:00
1 parent 7ce84e64d8
commit 2c351cb864
24 files changed
+281 -3

No files matched your search

@@ -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"],
}),