/** * `useSkills` — view-model hook for the skills feature (L12). * * Owns the skill lists for both scopes (`global` + `project`) of one project and * the CRUD actions. Consumes {@link SkillGateway} exclusively; never touches * `invoke()` or `@tauri-apps/api`, keeping the component layer testable with * mock gateways (ARCHITECTURE §1.3). */ import { useCallback, useEffect, useState } from "react"; import type { GatewayError, Skill, SkillScope } from "@/domain"; import type { CreateSkillInput } from "@/ports"; import { useGateways } from "@/app/di"; /** What the skills UI needs from this hook. */ export interface SkillsViewModel { /** Global skills (reusable across projects). */ globalSkills: Skill[]; /** Project-scoped skills. */ projectSkills: Skill[]; /** Last error message, or `null`. */ error: string | null; /** Whether a request is in flight. */ busy: boolean; /** Reloads both scope lists. */ refresh: () => Promise; /** Creates a new skill and refreshes its scope's list. */ createSkill: (input: Omit) => Promise; /** Updates the content of an existing skill. */ updateSkill: ( scope: SkillScope, skillId: string, content: string, ) => Promise; /** Deletes a skill by id from its scope. */ deleteSkill: (scope: SkillScope, skillId: string) => Promise; } function describe(e: unknown): string { if (e && typeof e === "object" && "message" in e) { return String((e as GatewayError).message); } return String(e); } export function useSkills(projectId: string): SkillsViewModel { const { skill } = useGateways(); const [globalSkills, setGlobalSkills] = useState([]); const [projectSkills, setProjectSkills] = useState([]); const [error, setError] = useState(null); const [busy, setBusy] = useState(false); const refresh = useCallback(async () => { if (!projectId) return; setBusy(true); setError(null); try { const [globals, projects] = await Promise.all([ skill.listSkills(projectId, "global"), skill.listSkills(projectId, "project"), ]); setGlobalSkills(globals); setProjectSkills(projects); } catch (e) { setError(describe(e)); } finally { setBusy(false); } }, [skill, projectId]); useEffect(() => { void refresh(); }, [refresh]); const setter = (scope: SkillScope) => scope === "global" ? setGlobalSkills : setProjectSkills; const createSkill = useCallback( async (input: Omit) => { setBusy(true); setError(null); try { const created = await skill.createSkill({ ...input, projectId }); setter(input.scope)((prev) => [...prev, created]); } catch (e) { setError(describe(e)); } finally { setBusy(false); } }, [skill, projectId], ); const updateSkill = useCallback( async (scope: SkillScope, skillId: string, content: string) => { setBusy(true); setError(null); try { const updated = await skill.updateSkill( projectId, scope, skillId, content, ); setter(scope)((prev) => prev.map((s) => (s.id === skillId ? updated : s)), ); } catch (e) { setError(describe(e)); } finally { setBusy(false); } }, [skill, projectId], ); const deleteSkill = useCallback( async (scope: SkillScope, skillId: string) => { setBusy(true); setError(null); try { await skill.deleteSkill(projectId, scope, skillId); setter(scope)((prev) => prev.filter((s) => s.id !== skillId)); } catch (e) { setError(describe(e)); } finally { setBusy(false); } }, [skill, projectId], ); return { globalSkills, projectSkills, error, busy, refresh, createSkill, updateSkill, deleteSkill, }; }