feat(slash): fondation du système de commandes slash pour la CLI custom — #162 (QA verte)

Introduit le contrat unifié des commandes slash consommable par le frontend,
indépendant de la source (native, puis plugin plus tard) :

- domain: modèle pur SlashCommand / SlashCommandSource / SlashCommandAvailability,
  métadonnées UI (nom, description, disponibilité, confirmation requise) et
  validation des noms/préfixes.
- application: registry + list/filter par préfixe + plan d'exécution natif ;
  commandes natives /help et /clean (/clean = nettoyage de la vue de session
  courante ; pas de /reset distinct tant qu'aucune utilité produit ne le justifie).
- backend + app-tauri: exposition du contrat via DTO/transport + tests DTO.

Source neutre : les commandes contribuées par plugins (#165) transiteront par
le même registry sans changer le contrat frontend.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-08-06 14:52:36 +02:00
parent 4e57161e1d
commit 6997138a71
11 changed files with 793 additions and 49 deletions

View File

@ -13,14 +13,15 @@ use application::{
AgentBackgroundTaskState, AgentTicketState, AppError, AppExitWorkGuardDetail,
AppExitWorkGuardState, AttachLiveAgentOutput, BackgroundTaskKindLabel,
ConversationPreviewStatus, ConversationTurnWorkPreview, ConversationWorkSummary,
CreateProjectInput, CreateProjectOutput, GitGraphOutput, HealthInput, HealthReport, LayoutKind,
ListProjectsOutput, LiveSessionKind, LiveSessionSnapshot, OpenProjectOutput, ProjectWorkState,
CreateProjectInput, CreateProjectOutput, ExecuteSlashCommandOutput, GitGraphOutput,
HealthInput, HealthReport, LayoutKind, ListProjectsOutput, ListSlashCommandsOutput,
LiveSessionKind, LiveSessionSnapshot, OpenProjectOutput, ProjectWorkState, SlashCommandEffect,
StopLiveAgentOutput, TicketWorkSource, TicketWorkStatus, TurnPage, TurnSource, TurnView,
};
use domain::ports::{ReplyProgress, ReplyProgressKind, ReplyProgressSource, ReplyProgressStage};
use domain::{
AgentBusyState, PageCursor, PageDirection, Project, ProjectId, ProjectSystemPermissions,
ResolvedAgentSystemPermissions, SystemPermissionSet, TurnRole,
ResolvedAgentSystemPermissions, SlashCommand, SystemPermissionSet, TurnRole,
};
pub use crate::ticket_dto::*;
@ -764,6 +765,106 @@ impl From<HealthReport> for HealthResponseDto {
}
}
/// Request DTO for slash-command list/filter.
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ListSlashCommandsRequestDto {
/// Optional typed prefix, with or without the leading `/`.
#[serde(default)]
pub prefix: Option<String>,
}
/// UI-facing slash-command metadata DTO.
pub type SlashCommandDto = SlashCommand;
/// Slash-command list response DTO.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SlashCommandListDto {
/// Commands in deterministic UI order.
pub commands: Vec<SlashCommandDto>,
}
impl From<ListSlashCommandsOutput> for SlashCommandListDto {
fn from(value: ListSlashCommandsOutput) -> Self {
Self {
commands: value.commands,
}
}
}
/// Request DTO for slash-command execution.
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ExecuteSlashCommandRequestDto {
/// Command name, with or without the leading `/`.
pub name: String,
/// Current structured chat session id, required by `/clean` and future
/// conversation-local commands.
#[serde(default)]
pub session_id: Option<String>,
}
/// Slash-command execution response DTO.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ExecuteSlashCommandResponseDto {
/// Executed command metadata.
pub command: SlashCommandDto,
/// Effect handled by the UI/adapter.
pub effect: SlashCommandEffectDto,
}
impl From<ExecuteSlashCommandOutput> for ExecuteSlashCommandResponseDto {
fn from(value: ExecuteSlashCommandOutput) -> Self {
Self {
command: value.command,
effect: value.effect.into(),
}
}
}
/// Slash-command execution effect DTO.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", tag = "kind")]
pub enum SlashCommandEffectDto {
/// Display command help.
Help {
/// Commands to display.
commands: Vec<SlashCommandDto>,
},
/// The current conversation was cleared.
CleanConversation {
/// Session id that was cleared.
#[serde(rename = "sessionId")]
session_id: String,
/// Number of retained chunks removed by the backend bridge.
#[serde(rename = "clearedChunks")]
cleared_chunks: usize,
},
/// Placeholder for ticket #164.
ProfileSwitch {
/// Session id targeted by the profile flow.
#[serde(rename = "sessionId")]
session_id: String,
},
}
impl From<SlashCommandEffect> for SlashCommandEffectDto {
fn from(value: SlashCommandEffect) -> Self {
match value {
SlashCommandEffect::Help { commands } => Self::Help { commands },
SlashCommandEffect::CleanConversation { session_id } => Self::CleanConversation {
session_id: session_id.to_string(),
cleared_chunks: 0,
},
SlashCommandEffect::ProfileSwitch { session_id } => Self::ProfileSwitch {
session_id: session_id.to_string(),
},
}
}
}
/// Error DTO returned to the frontend in the `Err` arm of every command.
///
/// `code` is a stable machine-readable string (see [`AppError::code`]); the

View File

@ -23,18 +23,19 @@ use application::{
CreateSprint, CreateTemplate, DeleteAgent, DeleteEmbedderProfile, DeleteIssue, DeleteLayout,
DeleteMemory, DeleteModelArtifact, DeleteModelServer, DeleteProfile, DeleteSkill, DeleteSprint,
DeleteTemplate, DescribeEmbedderEngines, DetectAgentDrift, DetectProfiles,
DismissEmbedderSuggestion, EnsureLocalModelServer, FirstRunState, GetAppExitWorkGuardState,
GetLiveStateLean, GetMemory, GetProjectPermissions, GetProjectSystemPermissions,
GetProjectWorkState, GitBranches, GitCheckout, GitCommit, GitGraph, GitInit, GitLog, GitStage,
GitStatus, GitUnstage, HarvestMemoryFromTurn, HealthUseCase, ImportChatAttachments,
InspectConversation, InstallPluginFromArchive, InstallPluginFromDirectory,
JsonPluginManifestValidator, LaunchAgent, LaunchAgentInput, LinkIssues, ListAgents,
ListAgentsInput, ListClaudeModels, ListCodexModels, ListDevices, ListEmbedderProfiles,
ListIssues, ListLayouts, ListMemories, ListModelServers, ListOpenCodeProviders,
ListPluginRuntimeContributions, ListPlugins, ListProfiles, ListProjects, ListResumableAgents,
ListSkills, ListSprints, ListTemplates, LiveAgentRegistry, LiveSessions, LiveStateLeanProvider,
LiveStateProvider, LiveStateReadProvider, LoadLayout, MarkIssueAttachmentSummarized,
McpRuntime, McpToolPermissionCatalogue, MoveTabToNewWindow, MutateLayout, OnnxModelView,
DismissEmbedderSuggestion, EnsureLocalModelServer, ExecuteSlashCommand, FirstRunState,
GetAppExitWorkGuardState, GetLiveStateLean, GetMemory, GetProjectPermissions,
GetProjectSystemPermissions, GetProjectWorkState, GitBranches, GitCheckout, GitCommit,
GitGraph, GitInit, GitLog, GitStage, GitStatus, GitUnstage, HarvestMemoryFromTurn,
HealthUseCase, ImportChatAttachments, InspectConversation, InstallPluginFromArchive,
InstallPluginFromDirectory, JsonPluginManifestValidator, LaunchAgent, LaunchAgentInput,
LinkIssues, ListAgents, ListAgentsInput, ListClaudeModels, ListCodexModels, ListDevices,
ListEmbedderProfiles, ListIssues, ListLayouts, ListMemories, ListModelServers,
ListOpenCodeProviders, ListPluginRuntimeContributions, ListPlugins, ListProfiles, ListProjects,
ListResumableAgents, ListSkills, ListSlashCommands, ListSprints, ListTemplates,
LiveAgentRegistry, LiveSessions, LiveStateLeanProvider, LiveStateProvider,
LiveStateReadProvider, LoadLayout, MarkIssueAttachmentSummarized, McpRuntime,
McpToolPermissionCatalogue, MoveTabToNewWindow, MutateLayout, OnnxModelView,
OpenPluginLayoutWindow, OpenProject, OpenTerminal, OpenTicketAssistant, OrchestratorService,
PairAttemptLimiter, PairDevice, PermissionProjectorRegistry, PluginCommandTasks,
PluginConfigDocuments, PluginEventSubscriptions, PluginStorageAccess,
@ -48,15 +49,15 @@ use application::{
ResolveMemoryLinks, RestoreOpenWindows, RetryBackgroundTask, ReviewPluginPackage,
RevokeAllDevices, RevokeDevice, RotateConversationLog, SaveEmbedderProfile, SaveModelServer,
SaveOpenCodeProviderProfile, SaveProfile, SessionLimitService, SetActiveLayout,
SetPluginEnabled, SnapshotOpenWindows, SnapshotRunningAgents, SpawnBackgroundCommand,
StopLiveAgent, StructuredRoutingMode, StructuredSessions, SuggestedThisSession,
SyncAgentWithTemplate, TerminalSessions, TouchDevice, UnassignSkillFromAgent,
UnassignTicketFromSprint, UninstallPlugin, UnlinkIssues, UpdateAgentContext, UpdateAgentEffort,
UpdateAgentMcpToolPermissions, UpdateAgentPermissions, UpdateAgentSystemPermissions,
UpdateIssue, UpdateIssueCarnet, UpdateLiveState, UpdateMemory, UpdateProjectContext,
UpdateProjectMcpToolPermissions, UpdateProjectPermissions, UpdateProjectSystemPermissions,
UpdateSkill, UpdateTemplate, WakeSessionProvider, WriteMemory, WriteToTerminal,
AGENT_MEMORY_RECALL_BUDGET,
SetPluginEnabled, SlashCommandRegistry, SnapshotOpenWindows, SnapshotRunningAgents,
SpawnBackgroundCommand, StopLiveAgent, StructuredRoutingMode, StructuredSessions,
SuggestedThisSession, SyncAgentWithTemplate, TerminalSessions, TouchDevice,
UnassignSkillFromAgent, UnassignTicketFromSprint, UninstallPlugin, UnlinkIssues,
UpdateAgentContext, UpdateAgentEffort, UpdateAgentMcpToolPermissions, UpdateAgentPermissions,
UpdateAgentSystemPermissions, UpdateIssue, UpdateIssueCarnet, UpdateLiveState, UpdateMemory,
UpdateProjectContext, UpdateProjectMcpToolPermissions, UpdateProjectPermissions,
UpdateProjectSystemPermissions, UpdateSkill, UpdateTemplate, WakeSessionProvider, WriteMemory,
WriteToTerminal, AGENT_MEMORY_RECALL_BUDGET,
};
use async_trait::async_trait;
use domain::ports::{
@ -877,6 +878,10 @@ impl AgentResumer for AppAgentResumer {
pub struct BackendCore {
/// Trivial health use case validating the end-to-end wiring.
pub health: Arc<HealthUseCase>,
/// List/filter the unified slash-command registry.
pub list_slash_commands: Arc<ListSlashCommands>,
/// Plan execution of a slash command.
pub execute_slash_command: Arc<ExecuteSlashCommand>,
/// Pair a persistent device session.
pub pair_device: Arc<PairDevice>,
/// Limits failed pairing attempts.
@ -1448,6 +1453,9 @@ impl BackendCore {
Arc::clone(&ids) as Arc<dyn IdGenerator>,
Arc::clone(&events_port),
));
let slash_command_registry = SlashCommandRegistry::new();
let list_slash_commands = Arc::new(ListSlashCommands::new(slash_command_registry.clone()));
let execute_slash_command = Arc::new(ExecuteSlashCommand::new(slash_command_registry));
let pair_device = Arc::new(PairDevice::new(
Arc::clone(&device_session_port),
Arc::clone(&ids) as Arc<dyn IdGenerator>,
@ -2924,6 +2932,8 @@ impl BackendCore {
Self {
health,
list_slash_commands,
execute_slash_command,
pair_device,
pair_attempt_limiter: Arc::clone(&pair_attempt_limiter_port),
authenticate_session,

View File

@ -156,6 +156,20 @@ where
.unwrap_or_default()
}
pub fn clear_scrollback(&self, key: &K) -> usize {
self.entries
.lock()
.ok()
.and_then(|mut m| {
m.get_mut(key).map(|entry| {
let cleared = entry.scrollback.len();
entry.scrollback.clear();
cleared
})
})
.unwrap_or_default()
}
pub fn unregister(&self, key: &K) {
if let Ok(mut map) = self.entries.lock() {
map.remove(key);