diff --git a/desktop/src/features/channels/ui/QuickAddAgentPopover.tsx b/desktop/src/features/channels/ui/QuickAddAgentPopover.tsx index 78087aec65..bcbe82953f 100644 --- a/desktop/src/features/channels/ui/QuickAddAgentPopover.tsx +++ b/desktop/src/features/channels/ui/QuickAddAgentPopover.tsx @@ -77,6 +77,17 @@ 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); @@ -134,7 +145,7 @@ export function QuickAddAgentPopover({ [teams, personas], ); - // Build the sorted item list + // Build the full item list (flat, for mutations) const items: QuickAddAgentItem[] = React.useMemo(() => { const result: QuickAddAgentItem[] = []; @@ -220,6 +231,38 @@ 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) { @@ -292,6 +335,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); @@ -300,7 +352,6 @@ export function QuickAddAgentPopover({ } else { next.add(key); } - // Exit multi-select mode if nothing selected if (next.size === 0) { setMultiSelectMode(false); } @@ -310,29 +361,25 @@ export function QuickAddAgentPopover({ function handleTeamClick(team: AgentTeam) { const resolution = resolveTeamPersonas(team, personas); - const personaKeys = resolution.resolvedPersonas.map( - (p) => `persona:${p.id}`, - ); - // Enter multi-select mode and select all team personas setMultiSelectMode(true); setSelectedKeys((prev) => { const next = new Set(prev); - for (const key of personaKeys) { - // Only select if the item is actually in our addable list - const item = items.find((i) => getItemKey(i) === key); - if (item && item.kind !== "running-in-channel") { - next.add(key); - } - } - // Also select running agents that belong to team personas 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; @@ -365,13 +412,11 @@ export function QuickAddAgentPopover({ } try { - // Attach running agents individually for (const agent of toAttach) { await attachMutation.mutateAsync({ agent, ensureRunning: true }); if (agent.personaId) pushRecent(agent.personaId); } - // Batch-create new agents if (toCreate.length > 0 && defaultProvider) { const inputs = toCreate.map(({ persona, instanceName }) => { const { provider } = resolvePersonaProvider( @@ -419,7 +464,6 @@ export function QuickAddAgentPopover({ if (!channelId) return; if (multiSelectMode) { - // In multi-select mode, toggle selection toggleSelection(getItemKey(item)); } else { // Single-click fast path — add immediately @@ -431,6 +475,20 @@ export function QuickAddAgentPopover({ } } + 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); + } + } + const isLoading = managedAgentsQuery.isLoading || personasQuery.isLoading || @@ -440,6 +498,75 @@ 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} @@ -496,118 +623,61 @@ export function QuickAddAgentPopover({ ) : null} - {/* Scrollable content area — max-height clips mid-item to hint at more */} + {/* Scrollable content — clips mid-item to hint at more */}
{isLoading ? (
+ ) : items.length === 0 ? ( +
+ No agents available. +
) : ( - <> - {/* Teams section */} - {usableTeams.length > 0 ? ( -
- {usableTeams.map((team) => { - const resolution = resolveTeamPersonas(team, personas); - return ( - - ); - })} +
+ {/* Team groups */} + {teamGroups.map(({ team, items: teamItems }) => ( +
+ {/* Team header */} + + {/* Team members (indented) */} + {teamItems.map((item) => renderAgentRow(item, true))} +
+ ))} + + {/* Ungrouped section */} + {ungroupedItems.length > 0 ? ( +
0 && "border-t pt-1")}> + {teamGroups.length > 0 ? ( +
+ + Ungrouped + +
+ ) : null} + {ungroupedItems.map((item) => renderAgentRow(item, false))}
) : null} - - {/* Agents section */} - {items.length === 0 ? ( -
- No agents available. -
- ) : ( -
- {items.map((item) => { - const itemKey = getItemKey(item); - const isInChannel = item.kind === "running-in-channel"; - const isItemPending = - pendingKey === itemKey || pendingKey === "batch"; - const isSelected = selectedKeys.has(itemKey); - - return ( - - ); - })} -
- )} - +
)}