fix: fix some displays and features
This commit is contained in:
@ -14,6 +14,8 @@ use application::{
|
||||
GitStatusInput, LaunchAgentInput, ListAgentsInput, ListLayoutsInput, LoadLayoutInput,
|
||||
MutateLayoutInput, OpenProjectInput, ReadAgentContextInput, RenameLayoutInput,
|
||||
SetActiveLayoutInput, SyncAgentWithTemplateInput, UpdateAgentContextInput,
|
||||
AssignSkillToAgentInput, CreateSkillInput, DeleteSkillInput, ListSkillsInput,
|
||||
UnassignSkillFromAgentInput, UpdateSkillInput,
|
||||
};
|
||||
use domain::ports::PtyHandle;
|
||||
|
||||
@ -31,8 +33,10 @@ use crate::dto::{
|
||||
ReattachResultDto, RenameLayoutRequestDto, ResizeTerminalRequestDto, SaveProfileRequestDto,
|
||||
SetActiveLayoutRequestDto, SyncAgentWithTemplateRequestDto, SyncResultDto, TemplateDto,
|
||||
TemplateListDto, TerminalClosedDto, TerminalSessionDto, UpdateAgentContextRequestDto,
|
||||
UpdateTemplateRequestDto, WriteTerminalRequestDto,
|
||||
UpdateTemplateRequestDto, WriteTerminalRequestDto, parse_skill_id, AssignSkillRequestDto,
|
||||
CreateSkillRequestDto, SkillDto, SkillListDto, UnassignSkillRequestDto, UpdateSkillRequestDto,
|
||||
};
|
||||
use domain::{SkillRef, SkillScope};
|
||||
use crate::pty::{PtyBridge, PtyChunk};
|
||||
use crate::state::AppState;
|
||||
|
||||
@ -1182,3 +1186,157 @@ pub async fn move_tab_to_new_window(
|
||||
|
||||
Ok(MoveTabResultDto::from(out))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Skills (L12)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// `create_skill` — create a skill in its scope's store.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an [`ErrorDto`] (`INVALID` for an empty name/content or malformed
|
||||
/// project id, `NOT_FOUND` if the project is unknown, `STORE` on failure).
|
||||
#[tauri::command]
|
||||
pub async fn create_skill(
|
||||
request: CreateSkillRequestDto,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<SkillDto, ErrorDto> {
|
||||
let project = resolve_project(&request.project_id, &state).await?;
|
||||
state
|
||||
.create_skill
|
||||
.execute(CreateSkillInput {
|
||||
name: request.name,
|
||||
content: request.content,
|
||||
scope: request.scope,
|
||||
project_root: project.root,
|
||||
})
|
||||
.await
|
||||
.map(SkillDto::from)
|
||||
.map_err(ErrorDto::from)
|
||||
}
|
||||
|
||||
/// `update_skill` — replace a skill's Markdown content.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an [`ErrorDto`] (`INVALID` for malformed ids or empty content,
|
||||
/// `NOT_FOUND` if the project or skill is unknown, `STORE` on failure).
|
||||
#[tauri::command]
|
||||
pub async fn update_skill(
|
||||
request: UpdateSkillRequestDto,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<SkillDto, ErrorDto> {
|
||||
let project = resolve_project(&request.project_id, &state).await?;
|
||||
let skill_id = parse_skill_id(&request.skill_id)?;
|
||||
state
|
||||
.update_skill
|
||||
.execute(UpdateSkillInput {
|
||||
scope: request.scope,
|
||||
skill_id,
|
||||
content: request.content,
|
||||
project_root: project.root,
|
||||
})
|
||||
.await
|
||||
.map(SkillDto::from)
|
||||
.map_err(ErrorDto::from)
|
||||
}
|
||||
|
||||
/// `list_skills` — list the skills in one scope.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an [`ErrorDto`] (`INVALID` for a malformed id, `NOT_FOUND` if the
|
||||
/// project is unknown, `STORE` on failure).
|
||||
#[tauri::command]
|
||||
pub async fn list_skills(
|
||||
project_id: String,
|
||||
scope: SkillScope,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<SkillListDto, ErrorDto> {
|
||||
let project = resolve_project(&project_id, &state).await?;
|
||||
state
|
||||
.list_skills
|
||||
.execute(ListSkillsInput {
|
||||
scope,
|
||||
project_root: project.root,
|
||||
})
|
||||
.await
|
||||
.map(SkillListDto::from)
|
||||
.map_err(ErrorDto::from)
|
||||
}
|
||||
|
||||
/// `delete_skill` — remove a skill from its scope's store.
|
||||
///
|
||||
/// Agents that referenced it keep their `SkillRef`; injection simply skips the
|
||||
/// now-absent skill.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an [`ErrorDto`] (`INVALID` for malformed ids, `NOT_FOUND` if the
|
||||
/// project or skill is unknown, `STORE` on failure).
|
||||
#[tauri::command]
|
||||
pub async fn delete_skill(
|
||||
project_id: String,
|
||||
scope: SkillScope,
|
||||
skill_id: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<(), ErrorDto> {
|
||||
let project = resolve_project(&project_id, &state).await?;
|
||||
let id = parse_skill_id(&skill_id)?;
|
||||
state
|
||||
.delete_skill
|
||||
.execute(DeleteSkillInput {
|
||||
scope,
|
||||
skill_id: id,
|
||||
project_root: project.root,
|
||||
})
|
||||
.await
|
||||
.map_err(ErrorDto::from)
|
||||
}
|
||||
|
||||
/// `assign_skill_to_agent` — record a `SkillRef` in the agent's manifest entry.
|
||||
/// Idempotent.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an [`ErrorDto`] (`INVALID` for malformed ids, `NOT_FOUND` if the
|
||||
/// project or agent is unknown, `STORE` on failure).
|
||||
#[tauri::command]
|
||||
pub async fn assign_skill_to_agent(
|
||||
request: AssignSkillRequestDto,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<(), ErrorDto> {
|
||||
let project = resolve_project(&request.project_id, &state).await?;
|
||||
let agent_id = parse_agent_id(&request.agent_id)?;
|
||||
let skill_id = parse_skill_id(&request.skill_id)?;
|
||||
state
|
||||
.assign_skill
|
||||
.execute(AssignSkillToAgentInput {
|
||||
project,
|
||||
agent_id,
|
||||
skill: SkillRef::new(skill_id, request.scope),
|
||||
})
|
||||
.await
|
||||
.map_err(ErrorDto::from)
|
||||
}
|
||||
|
||||
/// `unassign_skill_from_agent` — drop a skill assignment from an agent.
|
||||
/// Idempotent.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an [`ErrorDto`] (`INVALID` for malformed ids, `NOT_FOUND` if the
|
||||
/// project or agent is unknown, `STORE` on failure).
|
||||
#[tauri::command]
|
||||
pub async fn unassign_skill_from_agent(
|
||||
request: UnassignSkillRequestDto,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<(), ErrorDto> {
|
||||
let project = resolve_project(&request.project_id, &state).await?;
|
||||
let agent_id = parse_agent_id(&request.agent_id)?;
|
||||
let skill_id = parse_skill_id(&request.skill_id)?;
|
||||
state
|
||||
.unassign_skill
|
||||
.execute(UnassignSkillFromAgentInput {
|
||||
project,
|
||||
agent_id,
|
||||
skill_id,
|
||||
})
|
||||
.await
|
||||
.map_err(ErrorDto::from)
|
||||
}
|
||||
|
||||
@ -1342,3 +1342,107 @@ pub fn parse_tab_id(raw: &str) -> Result<TabId, ErrorDto> {
|
||||
message: format!("invalid tab id: {raw}"),
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Skills (L12)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
use application::{CreateSkillOutput, ListSkillsOutput, UpdateSkillOutput};
|
||||
use domain::{Skill, SkillId, SkillScope};
|
||||
|
||||
/// A skill crossing the wire. [`Skill`] already serialises camelCase
|
||||
/// (`id`, `name`, `contentMd`, `scope` as `"global"`/`"project"`), so we embed
|
||||
/// it directly — the TS mirror matches this shape.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct SkillDto(pub Skill);
|
||||
|
||||
/// A list of skills (transparent array on the wire).
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct SkillListDto(pub Vec<SkillDto>);
|
||||
|
||||
impl From<ListSkillsOutput> for SkillListDto {
|
||||
fn from(out: ListSkillsOutput) -> Self {
|
||||
Self(out.skills.into_iter().map(SkillDto).collect())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CreateSkillOutput> for SkillDto {
|
||||
fn from(out: CreateSkillOutput) -> Self {
|
||||
Self(out.skill)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<UpdateSkillOutput> for SkillDto {
|
||||
fn from(out: UpdateSkillOutput) -> Self {
|
||||
Self(out.skill)
|
||||
}
|
||||
}
|
||||
|
||||
/// Request DTO for `create_skill`.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreateSkillRequestDto {
|
||||
/// Owning project (resolved to a root; ignored on disk for `Global`).
|
||||
pub project_id: String,
|
||||
/// Display name.
|
||||
pub name: String,
|
||||
/// Initial Markdown content.
|
||||
pub content: String,
|
||||
/// Scope the skill is created in.
|
||||
pub scope: SkillScope,
|
||||
}
|
||||
|
||||
/// Request DTO for `update_skill`.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UpdateSkillRequestDto {
|
||||
/// Owning project (resolved to a root; ignored on disk for `Global`).
|
||||
pub project_id: String,
|
||||
/// Id of the skill to update.
|
||||
pub skill_id: String,
|
||||
/// Scope the skill lives in.
|
||||
pub scope: SkillScope,
|
||||
/// New Markdown content.
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
/// Request DTO for `assign_skill_to_agent`.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AssignSkillRequestDto {
|
||||
/// Owning project.
|
||||
pub project_id: String,
|
||||
/// Agent receiving the skill.
|
||||
pub agent_id: String,
|
||||
/// Skill to assign.
|
||||
pub skill_id: String,
|
||||
/// Scope of the skill (recorded alongside the ref).
|
||||
pub scope: SkillScope,
|
||||
}
|
||||
|
||||
/// Request DTO for `unassign_skill_from_agent`.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UnassignSkillRequestDto {
|
||||
/// Owning project.
|
||||
pub project_id: String,
|
||||
/// Agent losing the skill.
|
||||
pub agent_id: String,
|
||||
/// Skill to unassign.
|
||||
pub skill_id: String,
|
||||
}
|
||||
|
||||
/// Parses a skill-id string (UUID) coming from the frontend.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an [`ErrorDto`] with code `INVALID` if the string is not a UUID.
|
||||
pub fn parse_skill_id(raw: &str) -> Result<SkillId, ErrorDto> {
|
||||
uuid::Uuid::parse_str(raw)
|
||||
.map(SkillId::from_uuid)
|
||||
.map_err(|_| ErrorDto {
|
||||
code: "INVALID".to_owned(),
|
||||
message: format!("invalid skill id: {raw}"),
|
||||
})
|
||||
}
|
||||
|
||||
@ -119,6 +119,12 @@ pub fn run() {
|
||||
commands::git_log,
|
||||
commands::git_init,
|
||||
commands::git_graph,
|
||||
commands::create_skill,
|
||||
commands::update_skill,
|
||||
commands::list_skills,
|
||||
commands::delete_skill,
|
||||
commands::assign_skill_to_agent,
|
||||
commands::unassign_skill_from_agent,
|
||||
commands::move_tab_to_new_window,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
|
||||
@ -13,18 +13,19 @@ use application::{
|
||||
CreateAgentFromTemplate, CreateLayout, CreateProject, CreateTemplate, DeleteAgent,
|
||||
DeleteLayout, DeleteProfile, DeleteTemplate, DetectAgentDrift, DetectProfiles, FirstRunState,
|
||||
GitBranches, GitCheckout, GitCommit, GitGraph, GitInit, GitLog, GitStage, GitStatus, GitUnstage,
|
||||
HealthUseCase, LaunchAgent, ListAgents, ListLayouts, ListProfiles, ListProjects, ListTemplates,
|
||||
LoadLayout, MoveTabToNewWindow, MutateLayout, OpenProject, OpenTerminal, ReadAgentContext,
|
||||
ReferenceProfiles, RenameLayout, ResizeTerminal, SaveProfile, SetActiveLayout,
|
||||
HealthUseCase, LaunchAgent, ListAgents, ListLayouts, ListProfiles, ListProjects, ListSkills,
|
||||
ListTemplates, LoadLayout, MoveTabToNewWindow, MutateLayout, OpenProject, OpenTerminal,
|
||||
ReadAgentContext, ReferenceProfiles, RenameLayout, ResizeTerminal, SaveProfile, SetActiveLayout,
|
||||
AssignSkillToAgent, CreateSkill, DeleteSkill, UnassignSkillFromAgent, UpdateSkill,
|
||||
SyncAgentWithTemplate, TerminalSessions, UpdateAgentContext, UpdateTemplate, WriteToTerminal,
|
||||
};
|
||||
use domain::ports::{
|
||||
AgentContextStore, AgentRuntime, Clock, EventBus, FileSystem, GitPort, IdGenerator,
|
||||
ProcessSpawner, ProfileStore, ProjectStore, PtyPort, TemplateStore,
|
||||
ProcessSpawner, ProfileStore, ProjectStore, PtyPort, SkillStore, TemplateStore,
|
||||
};
|
||||
|
||||
use infrastructure::{
|
||||
CliAgentRuntime, FsProfileStore, FsProjectStore, FsTemplateStore, Git2Repository,
|
||||
CliAgentRuntime, FsProfileStore, FsProjectStore, FsSkillStore, FsTemplateStore, Git2Repository,
|
||||
IdeaiContextStore, LocalFileSystem, LocalProcessSpawner, PortablePtyAdapter, SystemClock,
|
||||
TokioBroadcastEventBus, UuidGenerator,
|
||||
};
|
||||
@ -147,6 +148,19 @@ pub struct AppState {
|
||||
pub git_init: Arc<GitInit>,
|
||||
/// Return the commit graph for all local branches.
|
||||
pub git_graph: Arc<GitGraph>,
|
||||
// --- Skills (L12) ---
|
||||
/// Create a skill in a scope's store.
|
||||
pub create_skill: Arc<CreateSkill>,
|
||||
/// Update a skill's content.
|
||||
pub update_skill: Arc<UpdateSkill>,
|
||||
/// List skills in a scope.
|
||||
pub list_skills: Arc<ListSkills>,
|
||||
/// Delete a skill from its scope's store.
|
||||
pub delete_skill: Arc<DeleteSkill>,
|
||||
/// Assign a skill to an agent (records a `SkillRef`).
|
||||
pub assign_skill: Arc<AssignSkillToAgent>,
|
||||
/// Unassign a skill from an agent.
|
||||
pub unassign_skill: Arc<UnassignSkillFromAgent>,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
@ -287,6 +301,17 @@ impl AppState {
|
||||
let contexts = Arc::new(IdeaiContextStore::new(Arc::clone(&fs_port)));
|
||||
let contexts_port = Arc::clone(&contexts) as Arc<dyn AgentContextStore>;
|
||||
|
||||
// --- Skill store (L12) ---
|
||||
// Global skills live in the machine-local app-data dir; project skills are
|
||||
// resolved per call from each project's `.ideai/` (so one store serves all
|
||||
// open projects). Shared by the skill use cases and the agent launcher
|
||||
// (assigned-skill injection into the convention file, §14.2).
|
||||
let skill_store = Arc::new(FsSkillStore::new(
|
||||
Arc::clone(&fs_port),
|
||||
app_data_dir.to_string_lossy().into_owned(),
|
||||
));
|
||||
let skill_store_port = Arc::clone(&skill_store) as Arc<dyn SkillStore>;
|
||||
|
||||
let create_agent = Arc::new(CreateAgentFromScratch::new(
|
||||
Arc::clone(&contexts_port),
|
||||
Arc::clone(&ids) as Arc<dyn IdGenerator>,
|
||||
@ -307,6 +332,7 @@ impl AppState {
|
||||
Arc::clone(&runtime_port),
|
||||
Arc::clone(&fs_port),
|
||||
Arc::clone(&pty_port),
|
||||
Arc::clone(&skill_store_port),
|
||||
Arc::clone(&terminal_sessions),
|
||||
Arc::clone(&events_port),
|
||||
));
|
||||
@ -364,6 +390,25 @@ impl AppState {
|
||||
let git_init = Arc::new(GitInit::new(Arc::clone(&git_port), Arc::clone(&events_port)));
|
||||
let git_graph = Arc::new(GitGraph::new(Arc::clone(&git_port)));
|
||||
|
||||
// --- Skill use cases (L12) ---
|
||||
// Reuse the skill store (built above for the launcher) and the shared
|
||||
// agent context store for the agent↔skill assignment.
|
||||
let create_skill = Arc::new(CreateSkill::new(
|
||||
Arc::clone(&skill_store_port),
|
||||
Arc::clone(&ids) as Arc<dyn IdGenerator>,
|
||||
));
|
||||
let update_skill = Arc::new(UpdateSkill::new(Arc::clone(&skill_store_port)));
|
||||
let list_skills = Arc::new(ListSkills::new(Arc::clone(&skill_store_port)));
|
||||
let delete_skill = Arc::new(DeleteSkill::new(Arc::clone(&skill_store_port)));
|
||||
let assign_skill = Arc::new(AssignSkillToAgent::new(
|
||||
Arc::clone(&contexts_port),
|
||||
Arc::clone(&events_port),
|
||||
));
|
||||
let unassign_skill = Arc::new(UnassignSkillFromAgent::new(
|
||||
Arc::clone(&contexts_port),
|
||||
Arc::clone(&events_port),
|
||||
));
|
||||
|
||||
// --- Windows (L10) ---
|
||||
let move_tab = Arc::new(MoveTabToNewWindow::new(
|
||||
Arc::clone(&store_port),
|
||||
@ -422,6 +467,12 @@ impl AppState {
|
||||
git_log,
|
||||
git_init,
|
||||
git_graph,
|
||||
create_skill,
|
||||
update_skill,
|
||||
list_skills,
|
||||
delete_skill,
|
||||
assign_skill,
|
||||
unassign_skill,
|
||||
move_tab,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user