Preserve remote plugin default prompts (#25887)

## Summary

- Read `default_prompts` from remote plugin release metadata.
- Prefer the plural prompt list over legacy `default_prompt`.
- Fall back to `default_prompt` as a single-item list for backward
compatibility.

## Testing

- `just test -p codex-core-plugins`
- `just test -p codex-app-server`
This commit is contained in:
Eric Ning
2026-06-03 12:39:13 -07:00
committed by GitHub
Unverified
parent 10b408080a
commit aeac226d16
3 changed files with 65 additions and 6 deletions
@@ -1754,6 +1754,8 @@ async fn plugin_list_includes_remote_marketplaces_when_remote_plugin_enabled() -
"interface": {
"short_description": "Plan and track work",
"capabilities": ["Read", "Write"],
"default_prompt": "Use the legacy Linear prompt",
"default_prompts": ["Create a Linear issue", "Review my Linear projects"],
"logo_url": "https://example.com/linear.png",
"screenshot_urls": ["https://example.com/linear-shot.png"]
},
@@ -1893,6 +1895,16 @@ async fn plugin_list_includes_remote_marketplaces_when_remote_plugin_enabled() -
.and_then(|interface| interface.display_name.as_deref()),
Some("Linear")
);
assert_eq!(
remote_marketplace.plugins[0]
.interface
.as_ref()
.and_then(|interface| interface.default_prompt.clone()),
Some(vec![
"Create a Linear issue".to_string(),
"Review my Linear projects".to_string(),
])
);
assert_eq!(
remote_marketplace.plugins[0].keywords,
vec![
@@ -1907,6 +1919,10 @@ async fn plugin_list_includes_remote_marketplaces_when_remote_plugin_enabled() -
let cached_catalog: serde_json::Value =
serde_json::from_slice(&std::fs::read(&cache_files[0])?)?;
assert_eq!(cached_catalog["schema_version"], serde_json::json!(1));
assert_eq!(
cached_catalog["plugins"][0]["release"]["interface"]["default_prompts"],
serde_json::json!(["Create a Linear issue", "Review my Linear projects"])
);
let cached_plugin_ids = cached_catalog["plugins"]
.as_array()
.expect("cached plugins should be an array")
@@ -159,7 +159,9 @@ plugins = true
"keywords": [],
"interface": {
"short_description": "Plan and track work",
"capabilities": []
"capabilities": [],
"default_prompt": "Use the legacy Linear prompt",
"default_prompts": []
},
"skills": []
}
@@ -217,6 +219,15 @@ plugins = true
assert_eq!(response.plugin.summary.name, "linear");
assert_eq!(response.plugin.summary.source, PluginSource::Remote);
assert_eq!(response.plugin.summary.share_context, None);
assert_eq!(
response
.plugin
.summary
.interface
.as_ref()
.and_then(|interface| interface.default_prompt.clone()),
Some(vec!["Use the legacy Linear prompt".to_string()])
);
Ok(())
}
@@ -404,6 +415,8 @@ async fn plugin_read_reads_remote_plugin_details_when_remote_plugin_enabled() ->
"interface": {
"short_description": "Plan and track work",
"capabilities": ["Read", "Write"],
"default_prompt": "Use the legacy Linear prompt",
"default_prompts": ["Create a Linear issue", "Review my Linear projects"],
"logo_url": "https://example.com/linear.png",
"screenshot_urls": ["https://example.com/linear-shot.png"]
},
@@ -518,6 +531,18 @@ async fn plugin_read_reads_remote_plugin_details_when_remote_plugin_enabled() ->
"project management".to_string()
]
);
assert_eq!(
response
.plugin
.summary
.interface
.as_ref()
.and_then(|interface| interface.default_prompt.clone()),
Some(vec![
"Create a Linear issue".to_string(),
"Review my Linear projects".to_string(),
])
);
assert_eq!(response.plugin.skills.len(), 1);
assert_eq!(response.plugin.skills[0].name, "plan-work");
assert_eq!(response.plugin.skills[0].path, None);
+23 -5
View File
@@ -66,6 +66,7 @@ pub const REMOTE_WORKSPACE_SHARED_WITH_ME_UNLISTED_MARKETPLACE_DISPLAY_NAME: &st
const OPENAI_CURATED_REMOTE_COLLECTION_KEY: &str = "vertical";
const REMOTE_PLUGIN_CATALOG_TIMEOUT: Duration = Duration::from_secs(30);
const REMOTE_PLUGIN_LIST_PAGE_LIMIT: u32 = 200;
const MAX_REMOTE_DEFAULT_PROMPT_COUNT: usize = 3;
const MAX_REMOTE_DEFAULT_PROMPT_LEN: usize = 128;
const INVALID_REQUEST_ERROR_CODE: i64 = -32600;
const REMOTE_INSTALLED_MARKETPLACE_DISPLAY_ORDER: [(&str, &str); 5] = [
@@ -392,6 +393,7 @@ struct RemotePluginReleaseInterfaceResponse {
terms_of_service_url: Option<String>,
brand_color: Option<String>,
default_prompt: Option<String>,
default_prompts: Option<Vec<String>>,
composer_icon_url: Option<String>,
logo_url: Option<String>,
#[serde(default)]
@@ -1193,9 +1195,16 @@ fn remote_plugin_interface_to_info(plugin: &RemotePluginDirectoryItem) -> Option
let interface = &plugin.release.interface;
let display_name = non_empty_string(Some(&plugin.release.display_name));
let default_prompt = interface
.default_prompt
.as_ref()
.and_then(|prompt| normalize_remote_default_prompt(prompt));
.default_prompts
.as_deref()
.and_then(normalize_remote_default_prompts)
.or_else(|| {
interface
.default_prompt
.as_deref()
.and_then(normalize_remote_default_prompt)
.map(|prompt| vec![prompt])
});
let result = PluginInterface {
display_name,
short_description: interface.short_description.clone(),
@@ -1279,12 +1288,21 @@ fn non_empty_string(value: Option<&str>) -> Option<String> {
})
}
fn normalize_remote_default_prompt(prompt: &str) -> Option<Vec<String>> {
fn normalize_remote_default_prompts(prompts: &[String]) -> Option<Vec<String>> {
let prompts = prompts
.iter()
.filter_map(|prompt| normalize_remote_default_prompt(prompt))
.take(MAX_REMOTE_DEFAULT_PROMPT_COUNT)
.collect::<Vec<_>>();
(!prompts.is_empty()).then_some(prompts)
}
fn normalize_remote_default_prompt(prompt: &str) -> Option<String> {
let prompt = prompt.trim();
if prompt.is_empty() || prompt.chars().count() > MAX_REMOTE_DEFAULT_PROMPT_LEN {
return None;
}
Some(vec![prompt.to_string()])
Some(prompt.to_string())
}
async fn fetch_directory_plugins_for_scope(