diff --git a/codex-rs/app-server/tests/suite/v2/plugin_list.rs b/codex-rs/app-server/tests/suite/v2/plugin_list.rs index 0df7c82ee..ae622db8a 100644 --- a/codex-rs/app-server/tests/suite/v2/plugin_list.rs +++ b/codex-rs/app-server/tests/suite/v2/plugin_list.rs @@ -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") 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 1f1c217ee..f78cd2a08 100644 --- a/codex-rs/app-server/tests/suite/v2/plugin_read.rs +++ b/codex-rs/app-server/tests/suite/v2/plugin_read.rs @@ -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); diff --git a/codex-rs/core-plugins/src/remote.rs b/codex-rs/core-plugins/src/remote.rs index 3c86f6b14..045ad8e18 100644 --- a/codex-rs/core-plugins/src/remote.rs +++ b/codex-rs/core-plugins/src/remote.rs @@ -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, brand_color: Option, default_prompt: Option, + default_prompts: Option>, composer_icon_url: Option, logo_url: Option, #[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 { }) } -fn normalize_remote_default_prompt(prompt: &str) -> Option> { +fn normalize_remote_default_prompts(prompts: &[String]) -> Option> { + let prompts = prompts + .iter() + .filter_map(|prompt| normalize_remote_default_prompt(prompt)) + .take(MAX_REMOTE_DEFAULT_PROMPT_COUNT) + .collect::>(); + (!prompts.is_empty()).then_some(prompts) +} + +fn normalize_remote_default_prompt(prompt: &str) -> Option { 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(