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:
2026-08-06 15:46:52 +02:00
parent ff7696b724
commit fbaca9d5cc
17 changed files with 514 additions and 34 deletions

View File

@ -11,6 +11,7 @@ use tauri::ipc::Channel;
use tauri::{AppHandle, Emitter, Manager, State, WebviewUrl, WebviewWindowBuilder, WindowEvent};
use crate::dto::DismissEmbedderSuggestionRequestDto;
use application::plugin_slash_commands_from_runtime_catalog;
use application::{
AppError, AssignSkillToAgentInput, AttachLiveAgentInput, ChangeAgentProfileInput,
CloseProjectInput, CreateAgentInput, CreateLayoutInput, CreateMemoryInput, CreateSkillInput,
@ -110,17 +111,27 @@ pub fn health(
/// `list_slash_commands` — list/filter the unified slash-command registry.
#[tauri::command]
pub fn list_slash_commands(
pub async fn list_slash_commands(
request: Option<crate::dto::ListSlashCommandsRequestDto>,
state: State<'_, AppState>,
) -> SlashCommandListDto {
) -> Result<SlashCommandListDto, ErrorDto> {
let request = request.unwrap_or_default();
state
let catalog = state
.list_plugin_runtime_contributions
.execute()
.await
.map_err(ErrorDto::from)?;
let plugin_commands =
plugin_slash_commands_from_runtime_catalog(&catalog).map_err(ErrorDto::from)?;
Ok(state
.list_slash_commands
.execute(ListSlashCommandsInput {
prefix: request.prefix,
})
.into()
.execute_with_plugins(
ListSlashCommandsInput {
prefix: request.prefix,
},
plugin_commands,
)
.into())
}
/// `execute_slash_command` — execute a selected slash command.
@ -129,7 +140,7 @@ pub fn list_slash_commands(
/// Returns an [`ErrorDto`] (`INVALID` for unavailable commands/missing context,
/// `NOT_FOUND` for unknown commands or closed sessions).
#[tauri::command]
pub fn execute_slash_command(
pub async fn execute_slash_command(
request: ExecuteSlashCommandRequestDto,
state: State<'_, AppState>,
) -> Result<ExecuteSlashCommandResponseDto, ErrorDto> {
@ -138,12 +149,23 @@ pub fn execute_slash_command(
.as_deref()
.map(parse_session_id)
.transpose()?;
let catalog = state
.list_plugin_runtime_contributions
.execute()
.await
.map_err(ErrorDto::from)?;
let plugin_commands =
plugin_slash_commands_from_runtime_catalog(&catalog).map_err(ErrorDto::from)?;
let output = state
.execute_slash_command
.execute(ExecuteSlashCommandInput {
name: request.name,
session_id,
})
.execute_with_plugins(
ExecuteSlashCommandInput {
name: request.name,
session_id,
arguments: request.arguments,
},
plugin_commands,
)
.map_err(ErrorDto::from)?;
let effect = match output.effect {
@ -163,6 +185,17 @@ pub fn execute_slash_command(
SlashCommandEffect::ProfileSwitch { session_id } => SlashCommandEffectDto::ProfileSwitch {
session_id: session_id.to_string(),
},
SlashCommandEffect::PluginCallback {
plugin_id,
command_id,
session_id,
arguments,
} => SlashCommandEffectDto::PluginCallback {
plugin_id,
command_id,
session_id: session_id.map(|id| id.to_string()),
arguments,
},
};
Ok(ExecuteSlashCommandResponseDto {