feat(plugins): seam slash-commands contribuées par plugin via callback — #165 (QA verte)
Permet à un plugin de contribuer des commandes slash adossées à une callback, transitant par le registry/contrat unifié (#162) : - sdk/IdeaSDK: manifeste contributes.slashCommands (déclaratif) — pointer bumpé. - domain: source Plugin étendue + effet PluginCallback (la commande ne s'exécute pas directement : elle renvoie une référence à la callback du plugin). - application: registration des commandes plugin dans le registry + exécution renvoyant l'effet PluginCallback ; métadonnées UI suffisantes pour l'autocomplete. - backend + app-tauri: DTO + commandes Tauri listant/invoquant les commandes plugin. - frontend (contrat): types adapters/domain/ports/mock pour pluginCallback et l'invocation avec arguments. L'exécution effective de la callback côté runtime UI est livrée par #166. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -110,8 +110,8 @@ pub use chat_attachment::{
|
||||
pub use skill::{Skill, SkillKind, SkillRef, SkillScope};
|
||||
|
||||
pub use slash_command::{
|
||||
filter_slash_commands, native_slash_commands, NativeSlashCommand, SlashCommand,
|
||||
SlashCommandAvailability, SlashCommandSource,
|
||||
filter_slash_commands, native_slash_commands, plugin_slash_command, NativeSlashCommand,
|
||||
PluginSlashCommandCallback, SlashCommand, SlashCommandAvailability, SlashCommandSource,
|
||||
};
|
||||
|
||||
pub use template::{AgentTemplate, TemplateVersion};
|
||||
@ -234,8 +234,9 @@ pub use plugin::{
|
||||
PluginInstallSource, PluginLayoutContribution, PluginLayoutType, PluginLifecycleState,
|
||||
PluginManifest, PluginMcpServerContribution, PluginMcpServerId, PluginMcpServerSpec,
|
||||
PluginMcpStatus, PluginMcpStatusSet, PluginMenuItemContribution, PluginPackageRef,
|
||||
PluginRegistry, PluginRegistryEntry, PluginTopLevelMenuContribution, PluginTrustLevel,
|
||||
PluginVersion, RelativePath, RemovalOutcome, StagedPluginPackage,
|
||||
PluginRegistry, PluginRegistryEntry, PluginSlashCommandContribution,
|
||||
PluginTopLevelMenuContribution, PluginTrustLevel, PluginVersion, RelativePath, RemovalOutcome,
|
||||
StagedPluginPackage,
|
||||
};
|
||||
|
||||
pub use sandbox::{
|
||||
|
||||
@ -411,6 +411,24 @@ pub struct PluginMenuItemContribution {
|
||||
pub when: Option<String>,
|
||||
}
|
||||
|
||||
/// Slash-command contribution backed by a plugin command callback.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PluginSlashCommandContribution {
|
||||
/// Slash name, including the leading `/`.
|
||||
pub name: String,
|
||||
/// Short autocomplete/help description.
|
||||
pub short_description: String,
|
||||
/// Plugin command callback id registered through the SDK command registry.
|
||||
pub command: PluginCommandId,
|
||||
/// Whether the host should ask for confirmation before dispatching callback.
|
||||
#[serde(default)]
|
||||
pub requires_confirmation: bool,
|
||||
/// Optional declarative condition reserved for host-side availability.
|
||||
#[serde(default)]
|
||||
pub when: Option<String>,
|
||||
}
|
||||
|
||||
/// Layout contribution.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@ -472,6 +490,9 @@ pub struct PluginContributionSet {
|
||||
/// Menu items.
|
||||
#[serde(default)]
|
||||
pub menu_items: Vec<PluginMenuItemContribution>,
|
||||
/// Slash commands.
|
||||
#[serde(default)]
|
||||
pub slash_commands: Vec<PluginSlashCommandContribution>,
|
||||
/// Layout contributions.
|
||||
#[serde(default)]
|
||||
pub layouts: Vec<PluginLayoutContribution>,
|
||||
|
||||
@ -17,6 +17,7 @@ pub enum SlashCommandSource {
|
||||
/// Contributed by a plugin.
|
||||
Plugin {
|
||||
/// Opaque plugin id.
|
||||
#[serde(rename = "pluginId")]
|
||||
plugin_id: String,
|
||||
},
|
||||
}
|
||||
@ -54,6 +55,16 @@ pub enum NativeSlashCommand {
|
||||
Profile,
|
||||
}
|
||||
|
||||
/// Plugin callback identity for a contributed slash command.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PluginSlashCommandCallback {
|
||||
/// Opaque plugin id.
|
||||
pub plugin_id: String,
|
||||
/// SDK command callback id registered by the plugin.
|
||||
pub command_id: String,
|
||||
}
|
||||
|
||||
/// UI-facing slash-command metadata.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@ -71,6 +82,9 @@ pub struct SlashCommand {
|
||||
/// Built-in command discriminator when `source == native`.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub native: Option<NativeSlashCommand>,
|
||||
/// Plugin callback identity when `source == plugin`.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub plugin: Option<PluginSlashCommandCallback>,
|
||||
}
|
||||
|
||||
impl SlashCommand {
|
||||
@ -86,6 +100,7 @@ impl SlashCommand {
|
||||
availability: SlashCommandAvailability,
|
||||
source: SlashCommandSource,
|
||||
native: Option<NativeSlashCommand>,
|
||||
plugin: Option<PluginSlashCommandCallback>,
|
||||
) -> Result<Self, DomainError> {
|
||||
let name = name.into();
|
||||
let short_description = short_description.into();
|
||||
@ -103,6 +118,7 @@ impl SlashCommand {
|
||||
availability,
|
||||
source,
|
||||
native,
|
||||
plugin,
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -118,6 +134,7 @@ pub fn native_slash_commands() -> Vec<SlashCommand> {
|
||||
SlashCommandAvailability::Available,
|
||||
SlashCommandSource::Native,
|
||||
Some(NativeSlashCommand::Help),
|
||||
None,
|
||||
)
|
||||
.expect("valid native slash command"),
|
||||
SlashCommand::new(
|
||||
@ -127,6 +144,7 @@ pub fn native_slash_commands() -> Vec<SlashCommand> {
|
||||
SlashCommandAvailability::Available,
|
||||
SlashCommandSource::Native,
|
||||
Some(NativeSlashCommand::Clean),
|
||||
None,
|
||||
)
|
||||
.expect("valid native slash command"),
|
||||
SlashCommand::new(
|
||||
@ -136,11 +154,41 @@ pub fn native_slash_commands() -> Vec<SlashCommand> {
|
||||
SlashCommandAvailability::Available,
|
||||
SlashCommandSource::Native,
|
||||
Some(NativeSlashCommand::Profile),
|
||||
None,
|
||||
)
|
||||
.expect("valid native slash command"),
|
||||
]
|
||||
}
|
||||
|
||||
/// Builds validated metadata for a plugin-contributed slash command.
|
||||
pub fn plugin_slash_command(
|
||||
plugin_id: impl Into<String>,
|
||||
command_id: impl Into<String>,
|
||||
name: impl Into<String>,
|
||||
short_description: impl Into<String>,
|
||||
requires_confirmation: bool,
|
||||
availability: SlashCommandAvailability,
|
||||
) -> Result<SlashCommand, DomainError> {
|
||||
let plugin_id = plugin_id.into();
|
||||
let command_id = command_id.into();
|
||||
crate::validation::non_empty(&plugin_id, "slash_command.plugin_id")?;
|
||||
crate::validation::non_empty(&command_id, "slash_command.command_id")?;
|
||||
SlashCommand::new(
|
||||
name,
|
||||
short_description,
|
||||
requires_confirmation,
|
||||
availability,
|
||||
SlashCommandSource::Plugin {
|
||||
plugin_id: plugin_id.clone(),
|
||||
},
|
||||
None,
|
||||
Some(PluginSlashCommandCallback {
|
||||
plugin_id,
|
||||
command_id,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
/// Filters commands by a user-entered slash prefix.
|
||||
///
|
||||
/// Blank prefixes return every command. Prefixes without a leading `/` are
|
||||
@ -180,6 +228,7 @@ mod tests {
|
||||
assert!(commands[1].availability.is_available());
|
||||
assert!(commands[2].availability.is_available());
|
||||
assert!(commands[2].requires_confirmation);
|
||||
assert!(commands.iter().all(|c| c.plugin.is_none()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -196,4 +245,24 @@ mod tests {
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(names, vec!["/profile"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugin_command_carries_callback_identity() {
|
||||
let command = plugin_slash_command(
|
||||
"dev.acme.plugin",
|
||||
"dev.acme.plugin.explain",
|
||||
"/explain",
|
||||
"Explain selection",
|
||||
true,
|
||||
SlashCommandAvailability::Available,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(command.name, "/explain");
|
||||
assert!(command.native.is_none());
|
||||
assert_eq!(
|
||||
command.plugin.unwrap().command_id,
|
||||
"dev.acme.plugin.explain"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user