Dernier lot de §17 : seuls les profils pilotables en mode structuré sont proposés à la sélection/création. - domain : AgentProfile::is_selectable() = structured_adapter.is_some(), source unique de vérité du prédicat. - infrastructure : AgentSessionFactory::supports délègue à is_selectable (supports et is_selectable ne peuvent plus diverger). - application : selectable_reference_profiles() = reference_profiles() filtré ; ReferenceProfiles et FirstRunState exposent la liste filtrée (Claude/Codex). reference_profiles() brut reste à 4 (data intacte) ⇒ un agent Gemini/Aider/custom legacy déjà configuré continue de tourner. - frontend : bloc AddCustomProfile retiré du wizard first-run, action addCustom retirée du viewmodel ; wizard n'affiche que la liste filtrée. Tests (QA) : is_selectable (table de vérité), cohérence stricte is_selectable<->supports, liste exposée=2 / data brute=4, garde anti-régression Vitest sur l'absence du bloc custom — validées par mutation. cargo test --workspace : 820 passed. npx vitest run : 344 passed. §17 COMPLET (D0->D7). Suivi restant : D6b (surfacer reply dans le writer wire .response.json pour la délégation par protocole fichier). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
330 lines
11 KiB
Rust
330 lines
11 KiB
Rust
//! Profile use cases (ARCHITECTURE §6, L5). Each is a single-responsibility
|
|
//! struct carrying its ports as `Arc<dyn Port>` and exposing one `execute`.
|
|
//!
|
|
//! - [`DetectProfiles`] — probe a set of candidate profiles via [`AgentRuntime`]
|
|
//! and report which CLIs are installed (first-run availability ✓/✗).
|
|
//! - [`ListProfiles`] / [`SaveProfile`] / [`DeleteProfile`] — CRUD over the
|
|
//! persisted profiles through the [`ProfileStore`].
|
|
//! - [`ConfigureProfiles`] — persist a batch of chosen/edited/custom profiles
|
|
//! (closes the first-run wizard).
|
|
//! - [`ReferenceProfiles`] — expose the pre-filled, editable catalogue.
|
|
//! - [`FirstRunState`] — tell the UI whether the first-run wizard should show
|
|
//! (no `profiles.json` yet) and hand it the reference catalogue.
|
|
|
|
use std::sync::Arc;
|
|
|
|
use domain::ports::{AgentRuntime, ProfileStore};
|
|
use domain::profile::AgentProfile;
|
|
|
|
use crate::error::AppError;
|
|
|
|
use super::catalogue::selectable_reference_profiles;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// DetectProfiles
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// Input for [`DetectProfiles::execute`]: the candidate profiles to probe.
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct DetectProfilesInput {
|
|
/// Profiles whose `detect` command should be run.
|
|
pub candidates: Vec<AgentProfile>,
|
|
}
|
|
|
|
/// Availability of a single candidate after detection.
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct ProfileAvailability {
|
|
/// The probed profile.
|
|
pub profile: AgentProfile,
|
|
/// Whether its CLI was detected as installed (exit code 0).
|
|
pub available: bool,
|
|
}
|
|
|
|
/// Output of [`DetectProfiles::execute`].
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct DetectProfilesOutput {
|
|
/// One entry per candidate (same order), with its availability.
|
|
pub results: Vec<ProfileAvailability>,
|
|
}
|
|
|
|
/// Probes candidate profiles' detection commands and reports availability.
|
|
pub struct DetectProfiles {
|
|
runtime: Arc<dyn AgentRuntime>,
|
|
}
|
|
|
|
impl DetectProfiles {
|
|
/// Builds the use case from the [`AgentRuntime`] port. The runtime itself
|
|
/// holds the [`domain::ports::ProcessSpawner`] used for detection.
|
|
#[must_use]
|
|
pub fn new(runtime: Arc<dyn AgentRuntime>) -> Self {
|
|
Self { runtime }
|
|
}
|
|
|
|
/// Runs detection for each candidate. A detection *error* (e.g. the command
|
|
/// could not even be launched) is reported as `available: false`, not a
|
|
/// hard failure — the wizard just shows ✗ and the user can still keep the
|
|
/// profile.
|
|
///
|
|
/// # Errors
|
|
/// Currently never returns `Err` (failures degrade to `available: false`);
|
|
/// the `Result` keeps the signature uniform with the other use cases.
|
|
pub async fn execute(
|
|
&self,
|
|
input: DetectProfilesInput,
|
|
) -> Result<DetectProfilesOutput, AppError> {
|
|
let results = input
|
|
.candidates
|
|
.into_iter()
|
|
.map(|profile| {
|
|
let available = self.runtime.detect(&profile).unwrap_or(false);
|
|
ProfileAvailability { profile, available }
|
|
})
|
|
.collect();
|
|
Ok(DetectProfilesOutput { results })
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// ListProfiles
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// Output of [`ListProfiles::execute`].
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct ListProfilesOutput {
|
|
/// All configured profiles.
|
|
pub profiles: Vec<AgentProfile>,
|
|
}
|
|
|
|
/// Lists the configured profiles from the store.
|
|
pub struct ListProfiles {
|
|
store: Arc<dyn ProfileStore>,
|
|
}
|
|
|
|
impl ListProfiles {
|
|
/// Builds the use case from the [`ProfileStore`] port.
|
|
#[must_use]
|
|
pub fn new(store: Arc<dyn ProfileStore>) -> Self {
|
|
Self { store }
|
|
}
|
|
|
|
/// Lists configured profiles.
|
|
///
|
|
/// # Errors
|
|
/// [`AppError::Store`] on persistence failure.
|
|
pub async fn execute(&self) -> Result<ListProfilesOutput, AppError> {
|
|
Ok(ListProfilesOutput {
|
|
profiles: self.store.list().await?,
|
|
})
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// SaveProfile
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// Input for [`SaveProfile::execute`]: the profile to upsert.
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct SaveProfileInput {
|
|
/// The profile to create or replace (by id).
|
|
pub profile: AgentProfile,
|
|
}
|
|
|
|
/// Output of [`SaveProfile::execute`].
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct SaveProfileOutput {
|
|
/// The saved profile (echoed back).
|
|
pub profile: AgentProfile,
|
|
}
|
|
|
|
/// Persists (creates or replaces) a single profile.
|
|
pub struct SaveProfile {
|
|
store: Arc<dyn ProfileStore>,
|
|
}
|
|
|
|
impl SaveProfile {
|
|
/// Builds the use case from the [`ProfileStore`] port.
|
|
#[must_use]
|
|
pub fn new(store: Arc<dyn ProfileStore>) -> Self {
|
|
Self { store }
|
|
}
|
|
|
|
/// Saves the profile.
|
|
///
|
|
/// # Errors
|
|
/// [`AppError::Store`] on persistence failure.
|
|
pub async fn execute(&self, input: SaveProfileInput) -> Result<SaveProfileOutput, AppError> {
|
|
self.store.save(&input.profile).await?;
|
|
Ok(SaveProfileOutput {
|
|
profile: input.profile,
|
|
})
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// DeleteProfile
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// Input for [`DeleteProfile::execute`].
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct DeleteProfileInput {
|
|
/// Id of the profile to delete.
|
|
pub id: domain::ids::ProfileId,
|
|
}
|
|
|
|
/// Deletes a profile by id.
|
|
pub struct DeleteProfile {
|
|
store: Arc<dyn ProfileStore>,
|
|
}
|
|
|
|
impl DeleteProfile {
|
|
/// Builds the use case from the [`ProfileStore`] port.
|
|
#[must_use]
|
|
pub fn new(store: Arc<dyn ProfileStore>) -> Self {
|
|
Self { store }
|
|
}
|
|
|
|
/// Deletes the profile.
|
|
///
|
|
/// # Errors
|
|
/// [`AppError::NotFound`] if the id is unknown, [`AppError::Store`] on
|
|
/// persistence failure.
|
|
pub async fn execute(&self, input: DeleteProfileInput) -> Result<(), AppError> {
|
|
self.store.delete(input.id).await?;
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// ConfigureProfiles
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// Input for [`ConfigureProfiles::execute`]: the chosen/edited/custom profiles.
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct ConfigureProfilesInput {
|
|
/// All profiles the user decided to keep (closes the first run).
|
|
pub profiles: Vec<AgentProfile>,
|
|
}
|
|
|
|
/// Output of [`ConfigureProfiles::execute`].
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct ConfigureProfilesOutput {
|
|
/// The persisted profiles.
|
|
pub profiles: Vec<AgentProfile>,
|
|
}
|
|
|
|
/// Persists the batch of profiles chosen at the end of the first-run wizard.
|
|
///
|
|
/// Saving even an empty list creates `profiles.json`, which marks the first run
|
|
/// as done (so the wizard does not reappear).
|
|
pub struct ConfigureProfiles {
|
|
store: Arc<dyn ProfileStore>,
|
|
}
|
|
|
|
impl ConfigureProfiles {
|
|
/// Builds the use case from the [`ProfileStore`] port.
|
|
#[must_use]
|
|
pub fn new(store: Arc<dyn ProfileStore>) -> Self {
|
|
Self { store }
|
|
}
|
|
|
|
/// Persists each chosen profile.
|
|
///
|
|
/// # Errors
|
|
/// [`AppError::Store`] on persistence failure.
|
|
pub async fn execute(
|
|
&self,
|
|
input: ConfigureProfilesInput,
|
|
) -> Result<ConfigureProfilesOutput, AppError> {
|
|
for profile in &input.profiles {
|
|
self.store.save(profile).await?;
|
|
}
|
|
// Ensure `profiles.json` exists even when the user kept nothing, so the
|
|
// first run is recorded as complete.
|
|
if input.profiles.is_empty() {
|
|
self.store.mark_configured().await?;
|
|
}
|
|
Ok(ConfigureProfilesOutput {
|
|
profiles: input.profiles,
|
|
})
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// ReferenceProfiles (catalogue accessor)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// Output of [`ReferenceProfiles::execute`].
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct ReferenceProfilesOutput {
|
|
/// The pre-filled, editable reference catalogue, **restricted to the
|
|
/// selectable profiles** (§17.3, D7): only profiles drivable in structured
|
|
/// mode are offered to selection/creation. Today: Claude + Codex.
|
|
pub profiles: Vec<AgentProfile>,
|
|
}
|
|
|
|
/// Exposes the **selectable** reference catalogue for the agent-creation menu
|
|
/// (§17.3, D7): the structured-drivable profiles only (Claude/Codex). Gemini and
|
|
/// Aider remain in the raw catalogue data but are not proposed here.
|
|
#[derive(Default)]
|
|
pub struct ReferenceProfiles;
|
|
|
|
impl ReferenceProfiles {
|
|
/// Builds the (stateless) use case.
|
|
#[must_use]
|
|
pub fn new() -> Self {
|
|
Self
|
|
}
|
|
|
|
/// Returns the reference catalogue. Infallible.
|
|
///
|
|
/// # Errors
|
|
/// Never; the `Result` keeps the call site uniform.
|
|
#[allow(clippy::unused_async)]
|
|
pub async fn execute(&self) -> Result<ReferenceProfilesOutput, AppError> {
|
|
Ok(ReferenceProfilesOutput {
|
|
profiles: selectable_reference_profiles(),
|
|
})
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// FirstRunState
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// Output of [`FirstRunState::execute`]: whether to show the wizard + catalogue.
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct FirstRunStateOutput {
|
|
/// `true` when no `profiles.json` exists yet ⇒ show the first-run wizard.
|
|
pub is_first_run: bool,
|
|
/// The pre-filled reference catalogue to seed the wizard, **restricted to the
|
|
/// selectable profiles** (§17.3, D7): only structured-drivable profiles
|
|
/// (Claude/Codex) are offered. No custom-profile entry.
|
|
pub reference_profiles: Vec<AgentProfile>,
|
|
}
|
|
|
|
/// Reports whether the IDE is on its first run (no profiles configured yet) and
|
|
/// provides the reference catalogue to seed the wizard.
|
|
pub struct FirstRunState {
|
|
store: Arc<dyn ProfileStore>,
|
|
}
|
|
|
|
impl FirstRunState {
|
|
/// Builds the use case from the [`ProfileStore`] port.
|
|
#[must_use]
|
|
pub fn new(store: Arc<dyn ProfileStore>) -> Self {
|
|
Self { store }
|
|
}
|
|
|
|
/// Computes the first-run state.
|
|
///
|
|
/// # Errors
|
|
/// [`AppError::Store`] on persistence failure.
|
|
pub async fn execute(&self) -> Result<FirstRunStateOutput, AppError> {
|
|
let configured = self.store.is_configured().await?;
|
|
Ok(FirstRunStateOutput {
|
|
is_first_run: !configured,
|
|
reference_profiles: selectable_reference_profiles(),
|
|
})
|
|
}
|
|
}
|