fix: fix some ui displays and features miss implemented

This commit is contained in:
2026-06-06 16:15:19 +02:00
parent 9736c42424
commit 2332b7f815
22 changed files with 1599 additions and 14 deletions

View File

@ -4,6 +4,7 @@ use serde::{Deserialize, Serialize};
use crate::error::DomainError;
use crate::ids::{AgentId, ProfileId, TemplateId};
use crate::skill::SkillRef;
use crate::template::TemplateVersion;
/// Origin of an agent: created from scratch, or derived from a template.
@ -65,6 +66,10 @@ pub struct Agent {
pub origin: AgentOrigin,
/// Whether the agent tracks its template (only valid for template origins).
pub synchronized: bool,
/// Skills assigned to this agent, injected into its convention file at
/// activation (ARCHITECTURE §14.2). Empty by default.
#[serde(default)]
pub skills: Vec<SkillRef>,
}
impl Agent {
@ -98,8 +103,37 @@ impl Agent {
profile_id,
origin,
synchronized,
skills: Vec::new(),
})
}
/// Returns a copy of this agent carrying the given assigned skills,
/// deduplicated by `skill_id` (keeping first occurrence).
#[must_use]
pub fn with_skills(mut self, skills: Vec<SkillRef>) -> Self {
self.skills = Vec::new();
for skill in skills {
self.assign_skill(skill);
}
self
}
/// Assigns a skill to this agent. Idempotent: re-assigning the same
/// `skill_id` is a no-op (returns `false`); a new assignment returns `true`.
pub fn assign_skill(&mut self, skill: SkillRef) -> bool {
if self.skills.iter().any(|s| s.skill_id == skill.skill_id) {
return false;
}
self.skills.push(skill);
true
}
/// Removes a skill assignment by id. Returns `true` if a skill was removed.
pub fn unassign_skill(&mut self, skill_id: crate::ids::SkillId) -> bool {
let before = self.skills.len();
self.skills.retain(|s| s.skill_id != skill_id);
self.skills.len() != before
}
}
/// One entry in the project agent manifest (`.ideai/agents.json`).
@ -135,6 +169,10 @@ pub struct ManifestEntry {
/// Template version recorded at the last sync.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub synced_template_version: Option<TemplateVersion>,
/// Skills assigned to this agent (ARCHITECTURE §14.2). Defaults to empty for
/// backward-compatible deserialisation of pre-L12 manifests.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub skills: Vec<SkillRef>,
}
impl ManifestEntry {
@ -172,6 +210,7 @@ impl ManifestEntry {
template_id,
synchronized,
synced_template_version,
skills: Vec::new(),
})
}
@ -196,6 +235,7 @@ impl ManifestEntry {
template_id,
synchronized: agent.synchronized,
synced_template_version,
skills: agent.skills.clone(),
}
}
@ -213,14 +253,15 @@ impl ManifestEntry {
},
_ => AgentOrigin::Scratch,
};
Agent::new(
Ok(Agent::new(
self.agent_id,
self.name.clone(),
self.md_path.clone(),
self.profile_id,
origin,
self.synchronized,
)
)?
.with_skills(self.skills.clone()))
}
}

View File

@ -1,7 +1,7 @@
//! Domain events published on the [`crate::ports::EventBus`] and relayed to the
//! presentation layer (ARCHITECTURE §3.2).
use crate::ids::{AgentId, ProjectId, SessionId, TemplateId};
use crate::ids::{AgentId, ProjectId, SessionId, SkillId, TemplateId};
use crate::template::TemplateVersion;
/// Events emitted by the domain/application as state changes occur.
@ -54,6 +54,15 @@ pub enum DomainEvent {
/// Version it was brought up to.
to: TemplateVersion,
},
/// A skill was assigned to (or unassigned from) an agent.
SkillAssigned {
/// The agent whose skill set changed.
agent_id: AgentId,
/// The skill involved.
skill_id: SkillId,
/// `true` if assigned, `false` if unassigned.
assigned: bool,
},
/// A tab's layout changed.
LayoutChanged {
/// The project whose layout changed.

View File

@ -68,6 +68,10 @@ typed_id!(
/// Identifies an [`crate::profile::AgentProfile`].
ProfileId
);
typed_id!(
/// Identifies a [`crate::skill::Skill`].
SkillId
);
typed_id!(
/// Identifies a [`crate::terminal::TerminalSession`].
SessionId

View File

@ -41,6 +41,7 @@ pub mod ports;
pub mod profile;
pub mod project;
pub mod remote;
pub mod skill;
pub mod template;
pub mod terminal;
@ -53,13 +54,16 @@ mod validation;
pub use error::DomainError;
pub use ids::{
AgentId, LayoutId, NodeId, ProfileId, ProjectId, SessionId, TabId, TemplateId, WindowId,
AgentId, LayoutId, NodeId, ProfileId, ProjectId, SessionId, SkillId, TabId, TemplateId,
WindowId,
};
pub use project::{Project, ProjectPath};
pub use agent::{Agent, AgentManifest, AgentOrigin, ManifestEntry};
pub use skill::{Skill, SkillRef, SkillScope};
pub use template::{AgentTemplate, TemplateVersion};
pub use profile::{AgentProfile, ContextInjection};

111
crates/domain/src/skill.rs Normal file
View File

@ -0,0 +1,111 @@
//! Skill entity — reusable, model-agnostic workflows assignable to agents.
//!
//! A [`Skill`] is IdeA's universal equivalent of a CLI's slash-command, but
//! without any dependency on a particular model's `/command` syntax
//! (ARCHITECTURE §14.2). Assigned skills are injected as plain text into the
//! agent's generated convention file at activation — there is no proprietary
//! CLI mechanism involved.
use serde::{Deserialize, Serialize};
use crate::error::DomainError;
use crate::ids::SkillId;
use crate::markdown::MarkdownDoc;
/// Where a skill lives, which also selects the store used to resolve it.
///
/// - [`SkillScope::Global`] skills are stored in the global IDE store
/// (`<app_data>/IdeA/skills/`) and reusable across projects.
/// - [`SkillScope::Project`] skills are stored under `.ideai/skills/` and are
/// specific to one project.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum SkillScope {
/// Reusable across projects (global IDE store).
Global,
/// Specific to a single project (`.ideai/skills/`).
Project,
}
/// A reusable workflow assignable to one or more agents.
///
/// Invariants enforced here:
/// - `name` non-empty,
/// - `content_md` non-empty (an empty skill carries no behaviour).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Skill {
/// Stable identifier.
pub id: SkillId,
/// Display name (also used as the `.md` file stem on disk).
pub name: String,
/// Markdown body — the workflow injected into an agent's convention file.
pub content_md: MarkdownDoc,
/// Scope (selects the backing store).
pub scope: SkillScope,
}
impl Skill {
/// Builds a validated skill.
///
/// # Errors
/// - [`DomainError::EmptyField`] if `name` is empty,
/// - [`DomainError::EmptyField`] if `content_md` is empty.
pub fn new(
id: SkillId,
name: impl Into<String>,
content_md: MarkdownDoc,
scope: SkillScope,
) -> Result<Self, DomainError> {
let name = name.into();
crate::validation::non_empty(&name, "skill.name")?;
if content_md.is_empty() {
return Err(DomainError::EmptyField {
field: "skill.content_md",
});
}
Ok(Self {
id,
name,
content_md,
scope,
})
}
/// Returns a copy of this skill with replaced content, re-validating the
/// non-empty invariant.
///
/// # Errors
/// [`DomainError::EmptyField`] if `content_md` is empty.
pub fn with_content(&self, content_md: MarkdownDoc) -> Result<Self, DomainError> {
Skill::new(self.id, self.name.clone(), content_md, self.scope)
}
}
/// A reference from an agent to one assigned skill.
///
/// Stored in the [`crate::agent::ManifestEntry`]: an agent carries 0..N of these.
/// The `scope` is kept alongside the id so the application layer knows which
/// store to resolve the skill from without a global lookup.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SkillRef {
/// The assigned skill.
pub skill_id: SkillId,
/// Scope of the assigned skill (selects its store).
pub scope: SkillScope,
}
impl SkillRef {
/// Builds a reference to an assigned skill.
#[must_use]
pub const fn new(skill_id: SkillId, scope: SkillScope) -> Self {
Self { skill_id, scope }
}
}
impl From<&Skill> for SkillRef {
fn from(skill: &Skill) -> Self {
Self::new(skill.id, skill.scope)
}
}