diff --git a/desktop/src/features/channels/ui/QuickAddAgentPopover.tsx b/desktop/src/features/channels/ui/QuickAddAgentPopover.tsx index bcbe82953f..fd8855cd57 100644 --- a/desktop/src/features/channels/ui/QuickAddAgentPopover.tsx +++ b/desktop/src/features/channels/ui/QuickAddAgentPopover.tsx @@ -1,29 +1,22 @@ -import { Check, Settings2, Users } from "lucide-react"; +import { Check, Settings2 } from "lucide-react"; import * as React from "react"; import { useAcpProvidersQuery, useAttachManagedAgentToChannelMutation, useCreateChannelManagedAgentMutation, - useCreateChannelManagedAgentsMutation, useManagedAgentsQuery, usePersonasQuery, - useTeamsQuery, } from "@/features/agents/hooks"; import { useChannelMembersQuery } from "@/features/channels/hooks"; import { getActivePersonas } from "@/features/agents/lib/catalog"; import { resolvePersonaProvider } from "@/features/agents/lib/resolvePersonaProvider"; import { pickBotName } from "@/features/agents/lib/pickBotName"; import { useBotRecents } from "@/features/agents/lib/useBotRecents"; -import { - getUsableTeams, - resolveTeamPersonas, -} from "@/features/agents/lib/teamPersonas"; -import type { AgentPersona, AgentTeam, ManagedAgent } from "@/shared/api/types"; +import type { AgentPersona, ManagedAgent } from "@/shared/api/types"; import { normalizePubkey } from "@/shared/lib/pubkey"; import { rewriteRelayUrl } from "@/shared/lib/mediaUrl"; import { cn } from "@/shared/lib/cn"; -import { Button } from "@/shared/ui/button"; import { Popover, PopoverContent, PopoverTrigger } from "@/shared/ui/popover"; import { Spinner } from "@/shared/ui/spinner"; @@ -77,17 +70,6 @@ function getItemKey(item: QuickAddAgentItem): string { } } -/** Get the persona ID associated with an item (for team membership checks). */ -function getItemPersonaId(item: QuickAddAgentItem): string | null { - switch (item.kind) { - case "persona": - return item.persona.id; - case "running-available": - case "running-in-channel": - return item.agent.personaId ?? null; - } -} - function safeBotName(persona: AgentPersona, usedNames: Set): string { const pool = persona.namePool ?? []; const name = pickBotName(pool, usedNames); @@ -107,22 +89,16 @@ export function QuickAddAgentPopover({ const managedAgentsQuery = useManagedAgentsQuery(); const personasQuery = usePersonasQuery(); const providersQuery = useAcpProvidersQuery(); - const teamsQuery = useTeamsQuery(); const membersQuery = useChannelMembersQuery( channelId, open && channelId !== null, ); const attachMutation = useAttachManagedAgentToChannelMutation(channelId); const createMutation = useCreateChannelManagedAgentMutation(channelId); - const batchCreateMutation = useCreateChannelManagedAgentsMutation(channelId); const { recentIds, pushRecent } = useBotRecents(); const [pendingKey, setPendingKey] = React.useState(null); const [errorMessage, setErrorMessage] = React.useState(null); - const [multiSelectMode, setMultiSelectMode] = React.useState(false); - const [selectedKeys, setSelectedKeys] = React.useState>( - new Set(), - ); const managedAgents = managedAgentsQuery.data ?? []; const personas = React.useMemo( @@ -132,20 +108,13 @@ export function QuickAddAgentPopover({ const providers = providersQuery.data ?? []; const defaultProvider = providers[0] ?? null; const members = membersQuery.data ?? []; - const teams = teamsQuery.data ?? []; const channelMemberPubkeys = React.useMemo( () => new Set(members.map((m) => normalizePubkey(m.pubkey))), [members], ); - // Usable teams (all personas resolved) - const usableTeams = React.useMemo( - () => getUsableTeams(teams, personas), - [teams, personas], - ); - - // Build the full item list (flat, for mutations) + // Build the sorted item list const items: QuickAddAgentItem[] = React.useMemo(() => { const result: QuickAddAgentItem[] = []; @@ -231,50 +200,14 @@ export function QuickAddAgentPopover({ return result; }, [managedAgents, personas, channelMemberPubkeys, recentIds]); - // ── Grouped layout: team sections + ungrouped ───────────────────────────── - - // Set of all persona IDs that belong to at least one usable team - const teamedPersonaIds = React.useMemo(() => { - const ids = new Set(); - for (const team of usableTeams) { - for (const pid of team.personaIds) { - ids.add(pid); - } - } - return ids; - }, [usableTeams]); - - // Items grouped by team - const teamGroups = React.useMemo(() => { - return usableTeams.map((team) => { - const memberItems = items.filter((item) => { - const personaId = getItemPersonaId(item); - return personaId !== null && team.personaIds.includes(personaId); - }); - return { team, items: memberItems }; - }); - }, [usableTeams, items]); - - // Ungrouped items (not in any team) - const ungroupedItems = React.useMemo(() => { - return items.filter((item) => { - const personaId = getItemPersonaId(item); - return personaId === null || !teamedPersonaIds.has(personaId); - }); - }, [items, teamedPersonaIds]); - // Reset state when popover closes React.useEffect(() => { if (!open) { setPendingKey(null); setErrorMessage(null); - setMultiSelectMode(false); - setSelectedKeys(new Set()); } }, [open]); - // ── Single-add handlers (preserved fast path) ───────────────────────────── - async function handleAddRunningAgent(agent: ManagedAgent) { if (!channelId) return; const key = `agent:${agent.pubkey}`; @@ -333,159 +266,15 @@ export function QuickAddAgentPopover({ } } - // ── Multi-select handlers ───────────────────────────────────────────────── - - function enterMultiSelect(key: string) { - setMultiSelectMode(true); - setSelectedKeys((prev) => { - const next = new Set(prev); - next.add(key); - return next; - }); - } - - function toggleSelection(key: string) { - setSelectedKeys((prev) => { - const next = new Set(prev); - if (next.has(key)) { - next.delete(key); - } else { - next.add(key); - } - if (next.size === 0) { - setMultiSelectMode(false); - } - return next; - }); - } - - function handleTeamClick(team: AgentTeam) { - const resolution = resolveTeamPersonas(team, personas); - - setMultiSelectMode(true); - setSelectedKeys((prev) => { - const next = new Set(prev); - for (const persona of resolution.resolvedPersonas) { - // Find the matching item (running agent or persona) - const runningItem = items.find( - (i) => - i.kind === "running-available" && i.agent.personaId === persona.id, - ); - if (runningItem) { - next.add(getItemKey(runningItem)); - } else { - const personaItem = items.find( - (i) => i.kind === "persona" && i.persona.id === persona.id, - ); - if (personaItem && personaItem.kind !== "running-in-channel") { - next.add(getItemKey(personaItem)); - } - } - } - return next; - }); - } - - async function handleBatchAdd() { - if (!channelId || selectedKeys.size === 0) return; - setPendingKey("batch"); - setErrorMessage(null); - - const usedNames = new Set(managedAgents.map((a) => a.name)); - const toAttach: ManagedAgent[] = []; - const toCreate: Array<{ - persona: AgentPersona; - instanceName: string; - }> = []; - - for (const key of selectedKeys) { - const item = items.find((i) => getItemKey(i) === key); - if (!item || item.kind === "running-in-channel") continue; - - if (item.kind === "running-available") { - toAttach.push(item.agent); - } else { - const instanceName = safeBotName(item.persona, usedNames); - usedNames.add(instanceName); - toCreate.push({ persona: item.persona, instanceName }); - } - } - - try { - for (const agent of toAttach) { - await attachMutation.mutateAsync({ agent, ensureRunning: true }); - if (agent.personaId) pushRecent(agent.personaId); - } - - if (toCreate.length > 0 && defaultProvider) { - const inputs = toCreate.map(({ persona, instanceName }) => { - const { provider } = resolvePersonaProvider( - persona.provider, - providers, - defaultProvider, - ); - const providerToUse = provider ?? defaultProvider; - return { - provider: { - id: providerToUse.id, - label: providerToUse.label, - command: providerToUse.command, - defaultArgs: providerToUse.defaultArgs, - mcpCommand: providerToUse.mcpCommand, - }, - name: instanceName, - systemPrompt: persona.systemPrompt, - avatarUrl: persona.avatarUrl ?? undefined, - personaId: persona.id, - model: persona.model ?? undefined, - }; - }); - - await batchCreateMutation.mutateAsync(inputs); - for (const { persona } of toCreate) { - pushRecent(persona.id); - } - } - - onOpenChange(false); - } catch (err) { - setErrorMessage( - err instanceof Error ? err.message : "Failed to add agents.", - ); - setPendingKey(null); - } - } - - // ── Item click dispatcher ───────────────────────────────────────────────── - function handleItemClick(item: QuickAddAgentItem) { if (item.kind === "running-in-channel") return; if (pendingKey) return; if (!channelId) return; - if (multiSelectMode) { - toggleSelection(getItemKey(item)); + if (item.kind === "running-available") { + void handleAddRunningAgent(item.agent); } else { - // Single-click fast path — add immediately - if (item.kind === "running-available") { - void handleAddRunningAgent(item.agent); - } else { - void handleAddPersona(item.persona); - } - } - } - - function handleCheckboxClick(e: React.MouseEvent, item: QuickAddAgentItem) { - e.stopPropagation(); - if (item.kind === "running-in-channel") return; - if (pendingKey) return; - - const key = getItemKey(item); - if (multiSelectMode) { - toggleSelection(key); - } else { - // Clicking checkbox enters multi-select mode - enterMultiSelect(key); + void handleAddPersona(item.persona); } } @@ -498,75 +287,6 @@ export function QuickAddAgentPopover({ return <>{children}; } - // ── Render helpers ──────────────────────────────────────────────────────── - - function renderAgentRow(item: QuickAddAgentItem, indented: boolean) { - const itemKey = getItemKey(item); - const isInChannel = item.kind === "running-in-channel"; - const isItemPending = pendingKey === itemKey || pendingKey === "batch"; - const isSelected = selectedKeys.has(itemKey); - - return ( - - ); - } - return ( {children} @@ -602,25 +322,10 @@ export function QuickAddAgentPopover({ } }} > -
+

Add agent

- {multiSelectMode && selectedKeys.size > 0 ? ( - - ) : null}
{/* Scrollable content — clips mid-item to hint at more */} @@ -639,44 +344,50 @@ export function QuickAddAgentPopover({ className="py-1" role="listbox" > - {/* Team groups */} - {teamGroups.map(({ team, items: teamItems }) => ( -
- {/* Team header */} + {items.map((item) => { + const itemKey = getItemKey(item); + const isInChannel = item.kind === "running-in-channel"; + const isItemPending = pendingKey === itemKey; + + return ( - {/* Team members (indented) */} - {teamItems.map((item) => renderAgentRow(item, true))} -
- ))} - - {/* Ungrouped section */} - {ungroupedItems.length > 0 ? ( -
0 && "border-t pt-1")}> - {teamGroups.length > 0 ? ( -
- - Ungrouped + {isInChannel ? ( + + ) : item.kind === "running-available" ? ( + + running -
- ) : null} - {ungroupedItems.map((item) => renderAgentRow(item, false))} -
- ) : null} + ) : null} + {isItemPending ? ( + + ) : null} + + ); + })}
)}