From e495d98e48e41b6c711277f68870f2b934d25ff4 Mon Sep 17 00:00:00 2001 From: Mikkel Georgsen Date: Wed, 1 Apr 2026 03:38:30 +0200 Subject: [PATCH] feat(11-04): extend AgentSkillsTab with groups section and dialogs - Add imports: skillGroupsApi, SkillGroupRow, GroupBadge, Dialog, Separator, ScrollArea, Textarea - Add agentGroups + allGroups + agentEffectiveSkills queries in AgentSkillsTab - Add assignGroup + removeGroup + createGroup mutations - Add state for add/create/remove dialogs, search, new group fields - Insert Assigned Groups section with loading/empty/populated states - Insert Combined Effective Skills collapsible section with ScrollArea - Insert Additional Individual Skills label above existing skill list - Add Add Skill Group, Create Skill Group, Remove group from agent dialogs - Add Skill Groups section to DesignGuide with all GroupBadge variants --- ui/src/pages/AgentDetail.tsx | 352 +++++++++++++++++++++++++++++++++++ ui/src/pages/DesignGuide.tsx | 47 +++++ 2 files changed, 399 insertions(+) diff --git a/ui/src/pages/AgentDetail.tsx b/ui/src/pages/AgentDetail.tsx index ac538ec3..943e3c08 100644 --- a/ui/src/pages/AgentDetail.tsx +++ b/ui/src/pages/AgentDetail.tsx @@ -9,6 +9,7 @@ import { type AgentPermissionUpdate, } from "../api/agents"; import { companySkillsApi } from "../api/companySkills"; +import { skillGroupsApi, type SkillGroupRow } from "../api/skillGroups"; import { budgetsApi } from "../api/budgets"; import { heartbeatsApi } from "../api/heartbeats"; import { instanceSettingsApi } from "../api/instanceSettings"; @@ -75,6 +76,17 @@ import { import { Collapsible, CollapsibleTrigger, CollapsibleContent } from "@/components/ui/collapsible"; import { TooltipProvider } from "@/components/ui/tooltip"; import { Input } from "@/components/ui/input"; +import { Textarea } from "@/components/ui/textarea"; +import { Separator } from "@/components/ui/separator"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { GroupBadge } from "../components/GroupBadge"; import { AgentIcon, AgentIconPicker } from "../components/AgentIconPicker"; import { RunTranscriptView, type TranscriptMode } from "../components/transcript/RunTranscriptView"; import { @@ -2400,6 +2412,63 @@ function AgentSkillsTab({ }, }); + // Skill groups queries + const agentGroupsQuery = useQuery({ + queryKey: queryKeys.skillGroups.agentGroups(agent.id), + queryFn: () => skillGroupsApi.listAgentGroups(agent.id), + }); + + const allGroupsQuery = useQuery({ + queryKey: queryKeys.skillGroups.list, + queryFn: () => skillGroupsApi.listGroups(), + }); + + const agentEffectiveSkillsQuery = useQuery({ + queryKey: queryKeys.skillGroups.agentSkills(agent.id), + queryFn: () => skillGroupsApi.listAgentSkills(agent.id), + }); + + // Group dialog state + const [addGroupOpen, setAddGroupOpen] = useState(false); + const [createGroupOpen, setCreateGroupOpen] = useState(false); + const [removeGroupConfirm, setRemoveGroupConfirm] = useState(null); + const [groupSearch, setGroupSearch] = useState(""); + const [newGroupName, setNewGroupName] = useState(""); + const [newGroupDesc, setNewGroupDesc] = useState(""); + const [effectiveOpen, setEffectiveOpen] = useState(false); + + // Group mutations + const assignGroupMut = useMutation({ + mutationFn: ({ groupId }: { groupId: string }) => + skillGroupsApi.assignGroup(agent.id, groupId, ""), + onSuccess: () => { + void queryClient.invalidateQueries({ queryKey: queryKeys.skillGroups.agentGroups(agent.id) }); + void queryClient.invalidateQueries({ queryKey: queryKeys.skillGroups.agentSkills(agent.id) }); + setAddGroupOpen(false); + }, + }); + + const removeGroupMut = useMutation({ + mutationFn: ({ groupId }: { groupId: string }) => + skillGroupsApi.removeGroup(agent.id, groupId, ""), + onSuccess: () => { + void queryClient.invalidateQueries({ queryKey: queryKeys.skillGroups.agentGroups(agent.id) }); + void queryClient.invalidateQueries({ queryKey: queryKeys.skillGroups.agentSkills(agent.id) }); + setRemoveGroupConfirm(null); + }, + }); + + const createGroupMut = useMutation({ + mutationFn: () => + skillGroupsApi.createGroup({ name: newGroupName, description: newGroupDesc || undefined }), + onSuccess: () => { + void queryClient.invalidateQueries({ queryKey: queryKeys.skillGroups.list }); + setCreateGroupOpen(false); + setNewGroupName(""); + setNewGroupDesc(""); + }, + }); + useEffect(() => { setSkillDraft([]); setLastSavedSkills([]); @@ -2573,6 +2642,289 @@ function AgentSkillsTab({ ) : null} + {/* ---- Assigned Groups Section ---- */} + + +
+

+ Assigned Groups +

+ + {agentGroupsQuery.isLoading ? ( +
+ + + +
+ ) : agentGroupsQuery.data?.length === 0 ? ( +
+

No groups assigned

+

+ Add a skill group to install a bundle of skills for this agent. +

+
+ ) : ( +
+ + {(agentGroupsQuery.data ?? []).map((group) => ( + setRemoveGroupConfirm(group) + } + removing={ + removeGroupMut.isPending && removeGroupConfirm?.id === group.id + } + /> + ))} + +
+ )} + + + + {agentGroupsQuery.isError && ( +

Failed to load groups. Try again.

+ )} +
+ + + + {/* ---- Combined Effective Skills Section ---- */} +
+ +
+

+ Combined Effective Skills +

+ + + +
+ + + {agentEffectiveSkillsQuery.isLoading ? ( +
+ + + +
+ ) : agentEffectiveSkillsQuery.data?.length === 0 ? ( +

+ No skills in assigned groups. Add skills to the group definitions first. +

+ ) : ( + +
    + {(agentEffectiveSkillsQuery.data ?? []).map((skillId) => ( +
  • + {skillId} +
  • + ))} +
+
+ )} +
+
+
+ + + + {/* ---- Additional Individual Skills ---- */} +

+ Additional Individual Skills +

+ + {/* ---- Add Group Dialog ---- */} + + + + Add Skill Group + +
+ setGroupSearch(e.target.value)} + className="h-8 text-sm" + /> +
+ {(() => { + const assignedIds = new Set((agentGroupsQuery.data ?? []).map((g) => g.id)); + const available = (allGroupsQuery.data ?? []).filter( + (g) => + !assignedIds.has(g.id) && + g.name.toLowerCase().includes(groupSearch.toLowerCase()), + ); + if (available.length === 0) { + return ( +

+ {groupSearch ? "No groups match your search." : "No groups available to add."} +

+ ); + } + return available.map((group) => ( +
+
+

{group.name}

+ {group.isBuiltin === 1 && ( +

built-in

+ )} + {group.description && ( +

{group.description}

+ )} +
+ +
+ )); + })()} +
+ {assignGroupMut.isError && ( +

+ Failed to assign group. Check the server logs for details. +

+ )} +
+ + + + +
+
+ + {/* ---- Create Group Dialog ---- */} + + + + Create Skill Group + +
+
+ + setNewGroupName(e.target.value)} + className="h-8 text-sm" + /> +
+
+ +