feat(skills): UI fuer AI-Skill-Definitionen — Modul 7/16 des UI-Backlogs
Check Cross-Plugin Imports / check (push) Has been cancelled

Zweites Modul via Phase-Q-Manifest-Architektur: Registrierung komplett
ueber das automation-Plugin-Manifest (page_route /skills + menu_item,
Sparkles-Icon) — routes/index.tsx und Sidebar.tsx unangetastet. Der
Komponenten-Map-Generator wired den lazy import (39 Komponenten).

Backend existierte vollstaendig im automation-Plugin (skill_routes.py:
list mit is_active/category-Filtern, create, get, patch, delete;
automation:read/write/delete), Frontend hatte 0% Abdeckung.

- api/skills.ts: TanStack-Hooks (useSkills mit Filtern, useSkill,
  create/update/delete mit Cache-Invalidierung)
- pages/Skills.tsx: Filter-Tabs (alle/aktiv/inaktiv), Skill-Karten (Name,
  Aktiv/Inaktiv-Badge, Kategorie, Beschreibung, Tool-Count),
  Create/Edit-Dialog (Name, Beschreibung, Instructions-Textarea, Kategorie,
  Tool-IDs als Komma-Liste, Aktiv-Toggle), Delete mit Confirm —
  Create/Edit hinter automation:write, Delete hinter automation:delete
- i18n skills.* + nav.skills de/en

Hinweis: Skills sind Orchestrierungs-Metadaten, KEINE
Berechtigungsquelle — erlaubte Tools verweisen auf Tool-IDs, fuer die
Agent und User bereits berechtigt sein muessen (Backend-Docstring).

Verifikation: Vitest 10/10 (Filter-Tabs, Badges inkl. Inaktiv, Tool-Count,
Create-Flow, Edit prefilled, Delete mit+ohne Confirm, separates
Write/Delete-Gating) · tsc exit 0 · production build exit 0 ·
Backend-Regressionen (route-order, m5-miniapps, n4-scope) 28/28 ·
compileall sauber · Cross-Plugin-Checker 0 · Manifest-Check: page_route
+ menu_item korrekt.
This commit is contained in:
Agent Zero
2026-09-13 10:36:02 +02:00
parent a09d611cac
commit 3f8d1bd59d
7 changed files with 785 additions and 2 deletions
+111
View File
@@ -0,0 +1,111 @@
/**
* Skills API client — AI skill definitions (orchestration metadata).
*
* Backend: /api/v1/skills (list, create, get, update, delete). Skills are
* NOT a permission source — they reference tool IDs the agent/user must
* already be permitted to use.
* Permissions: automation:read (list/get) / automation:write (create,
* update) / automation:delete (delete).
*/
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { apiGet, apiPost, apiPatch, apiDelete } from '@/api/client';
export interface SkillDefinition {
id: string;
name: string;
description: string;
instructions: string;
allowed_tool_ids: string[];
context_policy: Record<string, unknown> | null;
category: string;
is_active: boolean;
created_at: string | null;
updated_at: string | null;
}
export interface SkillListResponse {
items: SkillDefinition[];
total: number;
}
export interface SkillCreatePayload {
name: string;
description: string;
instructions: string;
allowed_tool_ids?: string[];
context_policy?: Record<string, unknown> | null;
category?: string;
is_active?: boolean;
}
export type SkillUpdatePayload = Partial<SkillCreatePayload>;
export interface SkillListParams {
isActive?: boolean;
category?: string;
limit?: number;
offset?: number;
}
// ─── Query hooks ─────────────────────────────────────────────
export function useSkills(params: SkillListParams = {}) {
const searchParams = new URLSearchParams();
if (params.isActive !== undefined) searchParams.set('is_active', String(params.isActive));
if (params.category) searchParams.set('category', params.category);
if (params.limit) searchParams.set('limit', String(params.limit));
if (params.offset) searchParams.set('offset', String(params.offset));
const qs = searchParams.toString();
return useQuery<SkillListResponse>({
queryKey: ['skills', params.isActive, params.category, params.limit, params.offset],
queryFn: () => apiGet<SkillListResponse>(`/skills${qs ? `?${qs}` : ''}`),
});
}
export function useSkill(skillId: string | null) {
return useQuery<SkillDefinition>({
queryKey: ['skills', skillId],
queryFn: () => apiGet<SkillDefinition>(`/skills/${skillId}`),
enabled: !!skillId,
});
}
// ─── Mutation hooks ──────────────────────────────────────────
function useInvalidateSkills() {
const qc = useQueryClient();
return () => {
qc.invalidateQueries({ queryKey: ['skills'] });
};
}
export function useCreateSkill() {
const invalidate = useInvalidateSkills();
return useMutation<SkillDefinition, Error, SkillCreatePayload>({
mutationFn: (data) => apiPost<SkillDefinition>('/skills/', data),
onSuccess: invalidate,
});
}
export function useUpdateSkill() {
const invalidate = useInvalidateSkills();
return useMutation<
SkillDefinition,
Error,
{ skillId: string; payload: SkillUpdatePayload }
>({
mutationFn: ({ skillId, payload }) =>
apiPatch<SkillDefinition>(`/skills/${skillId}`, payload),
onSuccess: invalidate,
});
}
export function useDeleteSkill() {
const invalidate = useInvalidateSkills();
return useMutation<{ status: string }, Error, string>({
mutationFn: (skillId) => apiDelete<{ status: string }>(`/skills/${skillId}`),
onSuccess: invalidate,
});
}