feat(domain,application,backend,app-tauri,frontend): lot utilisateur #328 #329 #330 #331 — section plugins projet, erreurs menus plugin visibles, refresh auto des pages, droplist agents (QA verte)
Lot en un seul commit: les quatre tickets partagent les mêmes fichiers (événements domaine/DTO, ProjectPluginsPanel, useProjectPlugins) — une séparation par hunks aurait risqué des états intermédiaires non compilables (convention lot des batches #310-#313, #315 et #325-#326). #328 — section dédiée plugins dans Paramètres du projet: - frontend: ProjectPluginsPanel intégré à ProjectSettingsPanel (Panneaux > Paramètres du projet) — activation/désactivation par plugin avec état actif/inactif explicite par projet, distinct de l'assignation agents/skills/tools - domaine: événement ProjectPluginSettingsChanged publié par SetProjectPluginEnabled, MutateAgentPluginSkill et MutateAgentPluginTool après persistance de .ideai/project-plugins.json (application + backend + DTO camelCase, test dto.rs) #329 — les menus plugin qui échouent ne sont plus muets: - usePluginMenus: dispatcher strict (runCommandStrict) pour les commandes réellement enregistrées au lieu du dispatcher laxiste qui journalisait et avalait les rejets — rendait les échecs d'ouverture de fenêtres natifs invisibles pour l'hôte de menus - le no-op SDK historique (commande déclarée sans handler) est préservé - échec remonté à l'hôte via commandError/clearCommandError (journal console + état exposé); test window_usecases adapté à un plugin installé - le correctif côté SDK/GitPlugin vit dans le projet GitPlugin (son commit/merge local y est fait, hors périmètre de ce dépôt) #330 — refresh automatique des pages: - frontend/domain: typage des événements pluginEnabled, pluginDisabled (restartRequired), pluginUninstalled (restartRequired), pluginLoadFailed et projectPluginSettingsChanged - useProjectPlugins et PluginRuntimeProvider: souscription onDomainEvent — refresh sur changement catalogue global ou sur projectPluginSettingsChanged du projet courant; gardes révision/projet contre les réponses obsolètes - useProjectAgents: refresh auto quand un agent est ajouté au projet; ProjectsView et AgentsPanel suivent (erreur de rafraîchissement affichée avec bouton Réessayer) #331 — droplist agents des cellules: - SmallDropdown: liste bg-overlay + bordure border-strong (plus de fond blanc hors thème), hover/focus bg-raised, sélection bg-primary/15, disabled text-faint opacity-60, role=combobox - LayoutGrid: sélecteur d'agent des cellules migré du <select> natif vers SmallDropdown (mêmes libellés Terminal/visible ailleurs/arrière-plan) QA verte réelle sur les quatre lots (tests ciblés + exécution). sdk/IdeaSDK: contenu modifié non committable ici (pointeur inchangé). Aucun push.
This commit is contained in:
@ -4,10 +4,11 @@ use std::collections::HashSet;
|
||||
use std::sync::Arc;
|
||||
|
||||
use domain::{
|
||||
AgentContextStore, AgentId, AgentPluginAssignments, MarkdownDoc, PluginId, PluginManifest,
|
||||
PluginManifestValidator, PluginMcpServerId, PluginMcpServerSpec, PluginPackageStore,
|
||||
PluginRegistryStore, PluginSkillId, PluginSkillRef, PluginStoreError, PluginToolRef, Project,
|
||||
ProjectPluginActivation, ProjectPluginSettings, ProjectPluginSettingsStore,
|
||||
AgentContextStore, AgentId, AgentPluginAssignments, DomainEvent, EventBus, MarkdownDoc,
|
||||
PluginId, PluginManifest, PluginManifestValidator, PluginMcpServerId, PluginMcpServerSpec,
|
||||
PluginPackageStore, PluginRegistryStore, PluginSkillId, PluginSkillRef, PluginStoreError,
|
||||
PluginToolRef, Project, ProjectPluginActivation, ProjectPluginSettings,
|
||||
ProjectPluginSettingsStore,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@ -292,6 +293,7 @@ impl ListProjectPluginSettings {
|
||||
pub struct SetProjectPluginEnabled {
|
||||
store: Arc<dyn ProjectPluginSettingsStore>,
|
||||
packages: Arc<dyn PluginPackageStore>,
|
||||
events: Arc<dyn EventBus>,
|
||||
}
|
||||
|
||||
impl SetProjectPluginEnabled {
|
||||
@ -300,8 +302,13 @@ impl SetProjectPluginEnabled {
|
||||
pub fn new(
|
||||
store: Arc<dyn ProjectPluginSettingsStore>,
|
||||
packages: Arc<dyn PluginPackageStore>,
|
||||
events: Arc<dyn EventBus>,
|
||||
) -> Self {
|
||||
Self { store, packages }
|
||||
Self {
|
||||
store,
|
||||
packages,
|
||||
events,
|
||||
}
|
||||
}
|
||||
|
||||
/// Persists activation. Enabling requires a currently installed plugin;
|
||||
@ -336,6 +343,10 @@ impl SetProjectPluginEnabled {
|
||||
.push(ProjectPluginActivation { plugin_id, enabled }),
|
||||
}
|
||||
self.store.save(&project.root, &settings).await?;
|
||||
self.events
|
||||
.publish(DomainEvent::ProjectPluginSettingsChanged {
|
||||
project_id: project.id,
|
||||
});
|
||||
Ok(settings)
|
||||
}
|
||||
}
|
||||
@ -354,6 +365,7 @@ pub struct MutateAgentPluginSkill {
|
||||
store: Arc<dyn ProjectPluginSettingsStore>,
|
||||
contexts: Arc<dyn AgentContextStore>,
|
||||
resolver: Arc<ProjectPluginResolver>,
|
||||
events: Arc<dyn EventBus>,
|
||||
}
|
||||
|
||||
impl MutateAgentPluginSkill {
|
||||
@ -363,11 +375,13 @@ impl MutateAgentPluginSkill {
|
||||
store: Arc<dyn ProjectPluginSettingsStore>,
|
||||
contexts: Arc<dyn AgentContextStore>,
|
||||
resolver: Arc<ProjectPluginResolver>,
|
||||
events: Arc<dyn EventBus>,
|
||||
) -> Self {
|
||||
Self {
|
||||
store,
|
||||
contexts,
|
||||
resolver,
|
||||
events,
|
||||
}
|
||||
}
|
||||
|
||||
@ -412,6 +426,10 @@ impl MutateAgentPluginSkill {
|
||||
_ => {}
|
||||
}
|
||||
self.store.save(&project.root, &settings).await?;
|
||||
self.events
|
||||
.publish(DomainEvent::ProjectPluginSettingsChanged {
|
||||
project_id: project.id,
|
||||
});
|
||||
Ok(settings)
|
||||
}
|
||||
}
|
||||
@ -421,6 +439,7 @@ pub struct MutateAgentPluginTool {
|
||||
store: Arc<dyn ProjectPluginSettingsStore>,
|
||||
contexts: Arc<dyn AgentContextStore>,
|
||||
resolver: Arc<ProjectPluginResolver>,
|
||||
events: Arc<dyn EventBus>,
|
||||
}
|
||||
|
||||
impl MutateAgentPluginTool {
|
||||
@ -430,11 +449,13 @@ impl MutateAgentPluginTool {
|
||||
store: Arc<dyn ProjectPluginSettingsStore>,
|
||||
contexts: Arc<dyn AgentContextStore>,
|
||||
resolver: Arc<ProjectPluginResolver>,
|
||||
events: Arc<dyn EventBus>,
|
||||
) -> Self {
|
||||
Self {
|
||||
store,
|
||||
contexts,
|
||||
resolver,
|
||||
events,
|
||||
}
|
||||
}
|
||||
|
||||
@ -483,6 +504,10 @@ impl MutateAgentPluginTool {
|
||||
_ => {}
|
||||
}
|
||||
self.store.save(&project.root, &settings).await?;
|
||||
self.events
|
||||
.publish(DomainEvent::ProjectPluginSettingsChanged {
|
||||
project_id: project.id,
|
||||
});
|
||||
Ok(settings)
|
||||
}
|
||||
}
|
||||
@ -550,16 +575,82 @@ mod tests {
|
||||
use super::*;
|
||||
use async_trait::async_trait;
|
||||
use domain::{
|
||||
ContentHash, LocalPath, PluginActivationScope, PluginCapability, PluginContributionSet,
|
||||
PluginInstallSource, PluginLifecycleState, PluginManifestBytes, PluginManifestError,
|
||||
PluginMcpServerContribution, PluginPackageRef, PluginRegistry, PluginRegistryEntry,
|
||||
PluginTrustLevel, PluginVersion, ProjectPath, RelativePath, RemovalOutcome,
|
||||
StagedPluginPackage,
|
||||
Agent, AgentManifest, AgentOrigin, ContentHash, LocalPath, PluginActivationScope,
|
||||
PluginCapability, PluginContributionSet, PluginInstallSource, PluginLifecycleState,
|
||||
PluginManifestBytes, PluginManifestError, PluginMcpServerContribution, PluginPackageRef,
|
||||
PluginRegistry, PluginRegistryEntry, PluginTrustLevel, PluginVersion, ProfileId,
|
||||
ProjectPath, RelativePath, RemoteRef, RemovalOutcome, StagedPluginPackage,
|
||||
};
|
||||
use std::sync::Mutex;
|
||||
|
||||
struct MemorySettings {
|
||||
settings: Mutex<ProjectPluginSettings>,
|
||||
fail_save: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
struct SpyEvents(Arc<Mutex<Vec<DomainEvent>>>);
|
||||
|
||||
impl SpyEvents {
|
||||
fn events(&self) -> Vec<DomainEvent> {
|
||||
self.0.lock().unwrap().clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl EventBus for SpyEvents {
|
||||
fn publish(&self, event: DomainEvent) {
|
||||
self.0.lock().unwrap().push(event);
|
||||
}
|
||||
|
||||
fn subscribe(&self) -> domain::EventStream {
|
||||
Box::new(std::iter::empty())
|
||||
}
|
||||
}
|
||||
|
||||
struct FakeContexts;
|
||||
|
||||
#[async_trait]
|
||||
impl AgentContextStore for FakeContexts {
|
||||
async fn read_context(
|
||||
&self,
|
||||
_project: &Project,
|
||||
_agent: &AgentId,
|
||||
) -> Result<MarkdownDoc, domain::StoreError> {
|
||||
Ok(MarkdownDoc::new("# Agent"))
|
||||
}
|
||||
|
||||
async fn write_context(
|
||||
&self,
|
||||
_project: &Project,
|
||||
_agent: &AgentId,
|
||||
_md: &MarkdownDoc,
|
||||
) -> Result<(), domain::StoreError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn load_manifest(
|
||||
&self,
|
||||
_project: &Project,
|
||||
) -> Result<AgentManifest, domain::StoreError> {
|
||||
let agent = Agent::new(
|
||||
agent_id(),
|
||||
"Dev",
|
||||
"agents/dev.md",
|
||||
ProfileId::from_uuid(uuid::Uuid::from_u128(2)),
|
||||
AgentOrigin::Scratch,
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
Ok(AgentManifest::new(1, vec![domain::ManifestEntry::from_agent(&agent)]).unwrap())
|
||||
}
|
||||
|
||||
async fn save_manifest(
|
||||
&self,
|
||||
_project: &Project,
|
||||
_manifest: &AgentManifest,
|
||||
) -> Result<(), domain::StoreError> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
@ -576,6 +667,9 @@ mod tests {
|
||||
_project_root: &ProjectPath,
|
||||
settings: &ProjectPluginSettings,
|
||||
) -> Result<(), domain::StoreError> {
|
||||
if self.fail_save {
|
||||
return Err(domain::StoreError::Io("save failed".to_owned()));
|
||||
}
|
||||
*self.settings.lock().unwrap() = settings.clone();
|
||||
Ok(())
|
||||
}
|
||||
@ -684,6 +778,17 @@ mod tests {
|
||||
ProjectPath::new("/project").unwrap()
|
||||
}
|
||||
|
||||
fn project() -> Project {
|
||||
Project::new(
|
||||
domain::ProjectId::from_uuid(uuid::Uuid::from_u128(328)),
|
||||
"Plugin project",
|
||||
project_root(),
|
||||
RemoteRef::Local,
|
||||
0,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn manifest(auto_start: bool) -> PluginManifest {
|
||||
PluginManifest {
|
||||
idea_plugin_manifest_version: 1,
|
||||
@ -769,6 +874,7 @@ mod tests {
|
||||
ProjectPluginResolver::new(
|
||||
Arc::new(MemorySettings {
|
||||
settings: Mutex::new(settings),
|
||||
fail_save: false,
|
||||
}),
|
||||
Arc::new(MemoryPackages {
|
||||
packages: vec![PluginPackageRef {
|
||||
@ -869,4 +975,111 @@ mod tests {
|
||||
.unwrap()
|
||||
.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn project_plugin_activation_publishes_change_after_successful_save() {
|
||||
let store = Arc::new(MemorySettings {
|
||||
settings: Mutex::new(ProjectPluginSettings::default()),
|
||||
fail_save: false,
|
||||
});
|
||||
let packages = Arc::new(MemoryPackages {
|
||||
packages: vec![PluginPackageRef {
|
||||
plugin_id: Some(plugin_id()),
|
||||
root: "/plugins/unity".to_owned(),
|
||||
}],
|
||||
app_data_dir: None,
|
||||
});
|
||||
let events = SpyEvents::default();
|
||||
let use_case = SetProjectPluginEnabled::new(store, packages, Arc::new(events.clone()));
|
||||
let project = project();
|
||||
|
||||
use_case
|
||||
.execute(project.clone(), plugin_id(), true)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
events.events(),
|
||||
vec![DomainEvent::ProjectPluginSettingsChanged {
|
||||
project_id: project.id,
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn project_plugin_activation_does_not_publish_when_save_fails() {
|
||||
let store = Arc::new(MemorySettings {
|
||||
settings: Mutex::new(ProjectPluginSettings::default()),
|
||||
fail_save: true,
|
||||
});
|
||||
let packages = Arc::new(MemoryPackages {
|
||||
packages: vec![PluginPackageRef {
|
||||
plugin_id: Some(plugin_id()),
|
||||
root: "/plugins/unity".to_owned(),
|
||||
}],
|
||||
app_data_dir: None,
|
||||
});
|
||||
let events = SpyEvents::default();
|
||||
let use_case = SetProjectPluginEnabled::new(store, packages, Arc::new(events.clone()));
|
||||
|
||||
let error = use_case
|
||||
.execute(project(), plugin_id(), true)
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(error.code(), "STORE");
|
||||
assert!(events.events().is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn project_plugin_assignment_publishes_change_after_successful_save() {
|
||||
let store = Arc::new(MemorySettings {
|
||||
settings: Mutex::new(settings(true, Vec::new())),
|
||||
fail_save: false,
|
||||
});
|
||||
let packages = Arc::new(MemoryPackages {
|
||||
packages: vec![PluginPackageRef {
|
||||
plugin_id: Some(plugin_id()),
|
||||
root: "/plugins/unity".to_owned(),
|
||||
}],
|
||||
app_data_dir: None,
|
||||
});
|
||||
let resolver = Arc::new(ProjectPluginResolver::new(
|
||||
store.clone(),
|
||||
packages,
|
||||
Arc::new(MemoryRegistry {
|
||||
registry: registry(PluginLifecycleState::Enabled),
|
||||
}),
|
||||
Arc::new(FixedManifest(manifest(true))),
|
||||
));
|
||||
let events = SpyEvents::default();
|
||||
let use_case = MutateAgentPluginTool::new(
|
||||
store,
|
||||
Arc::new(FakeContexts),
|
||||
resolver,
|
||||
Arc::new(events.clone()),
|
||||
);
|
||||
let project = project();
|
||||
|
||||
use_case
|
||||
.execute(
|
||||
project.clone(),
|
||||
agent_id(),
|
||||
PluginToolRef {
|
||||
plugin_id: plugin_id(),
|
||||
server_id: PluginMcpServerId::new("unity-editor").unwrap(),
|
||||
tool_name: "unity_detect_project".to_owned(),
|
||||
},
|
||||
AssignmentMutation::Assign,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
events.events(),
|
||||
vec![DomainEvent::ProjectPluginSettingsChanged {
|
||||
project_id: project.id,
|
||||
}]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -108,7 +108,10 @@ impl FakePluginPackages {
|
||||
#[async_trait]
|
||||
impl PluginPackageStore for FakePluginPackages {
|
||||
async fn list_installed(&self) -> Result<Vec<PluginPackageRef>, PluginStoreError> {
|
||||
Ok(Vec::new())
|
||||
Ok(vec![PluginPackageRef {
|
||||
plugin_id: Some(PluginId::new("dev.idea.android-plugin").unwrap()),
|
||||
root: "/plugin".to_owned(),
|
||||
}])
|
||||
}
|
||||
|
||||
async fn read_manifest(
|
||||
|
||||
Reference in New Issue
Block a user