"use client" import { useState, useMemo, useEffect, useCallback, useRef } from "react" import Image from "next/image" import { dmSans125ClassName, dmSansClassName } from "@/lib/fonts" import { Dialog, DialogContent, DialogTitle } from "@repo/ui/components/dialog" import { Drawer, DrawerContent, DrawerTitle } from "@repo/ui/components/drawer" import { Avatar, AvatarFallback, AvatarImage } from "@ui/components/avatar" import { cn } from "@lib/utils" import { useIsMobile } from "@hooks/use-mobile" import * as DialogPrimitive from "@radix-ui/react-dialog" import { XIcon, Search, FolderIcon, LayoutGrid, Plus, Trash2, Clock, ArrowRight, BookOpen, Loader, Pencil, Check, Lock, } from "lucide-react" import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" import { toast } from "sonner" import { DEFAULT_PROJECT_ID, SHARED_TEAM_BRAIN_TAG } from "@lib/constants" import { authClient } from "@lib/auth" import { useAuth } from "@lib/auth-context" import type { ContainerTagListType } from "@lib/types" import { compareSpacesUserFirst, isOwnConversationSpace, spaceSelectorDisplayName, } from "@/lib/ingest-auto-space" import { detectPluginSpace, pluginInitial, type PluginSpaceInfo, } from "@/lib/plugin-space" import { usePluginSpaceMeta } from "@/hooks/use-plugin-space-meta" import { groupAgentSpaces, type AgentSpaceGroup } from "@/lib/agent-space" import { PLUGIN_CATALOG, spacePluginIdToCatalogId, type PluginInfo, } from "@/lib/plugin-catalog" import { INSET, InstallSteps, PillButton } from "./integrations/install-steps" import { useProjectMutations } from "@/hooks/use-project-mutations" import { AUTO_CHAT_SPACE_ID } from "@/lib/chat-auto-space" import NovaOrb from "@/components/nova/nova-orb" import { AutoSpaceIcon } from "@/components/nova/auto-space-icon" import { SpaceGlyph } from "./space-glyph" import { useHasCompanyBrain } from "@/hooks/use-company-brain" import { Logo } from "@ui/assets/Logo" interface SelectSpacesModalProps { isOpen: boolean onClose: () => void selectedProjects: string[] onApply: (selected: string[]) => void projects: ContainerTagListType[] recents?: string[] showNewSpace?: boolean includeAuto?: boolean onNewSpace?: () => void enableDelete?: boolean onDeleteRequest?: (project: { id: string name: string containerTag: string }) => void onBulkDeleteRequest?: ( projects: { id: string name: string containerTag: string }[], ) => void } type CategoryId = | "all" | "my" | `plugin:${PluginSpaceInfo["pluginId"]}` | `discover:${string}` type Category = { id: CategoryId label: string iconSrc: string | null emoji: string | null count: number } const AGENT_CATALOG_IDS = [ "claude_code", "codex", "opencode", "cursor", ] as const export function SelectSpacesModal({ isOpen, onClose, selectedProjects, onApply, projects, recents, showNewSpace = false, includeAuto = false, onNewSpace, enableDelete = false, onDeleteRequest, onBulkDeleteRequest, }: SelectSpacesModalProps) { const [searchQuery, setSearchQuery] = useState("") const [isBulkDeleteMode, setIsBulkDeleteMode] = useState(false) const [bulkDeleteTags, setBulkDeleteTags] = useState>(new Set()) const [lastBulkDeleteTag, setLastBulkDeleteTag] = useState( null, ) const [editingProject, setEditingProject] = useState<{ id: string containerTag: string originalName: string name: string } | null>(null) const editInputRef = useRef(null) const editingContainerTag = editingProject?.containerTag const currentSelection = selectedProjects[0] ?? "" const selectedTagSet = useMemo( () => new Set(selectedProjects), [selectedProjects], ) const isMobile = useIsMobile() const pluginTags = useMemo( () => projects .filter((p) => !!detectPluginSpace(p.containerTag)) .map((p) => p.containerTag), [projects], ) const pluginMetaMap = usePluginSpaceMeta(pluginTags) const hasCompanyBrain = useHasCompanyBrain() const agentGroups = useMemo( () => groupAgentSpaces(projects, pluginMetaMap), [projects, pluginMetaMap], ) const agentGroupByTag = useMemo(() => { const map = new Map>() for (const group of agentGroups) { for (const tag of group.containerTags) map.set(tag, group) } return map }, [agentGroups]) const allSpaces = useMemo(() => { const rest = projects .filter((p) => p.containerTag !== DEFAULT_PROJECT_ID) .filter((p) => { const group = agentGroupByTag.get(p.containerTag) return !group || group.representative.containerTag === p.containerTag }) .sort(compareSpacesUserFirst) // Company brain orgs use real Private + Team Brain spaces; skip the // synthetic "My Space" default that would otherwise duplicate Private. if (hasCompanyBrain) return rest const defaultSpace = { id: "default", name: "My Space", emoji: "📁", containerTag: DEFAULT_PROJECT_ID, isExperimental: false, isNova: false, createdAt: "", updatedAt: "", } as ContainerTagListType return [defaultSpace, ...rest] }, [projects, hasCompanyBrain, agentGroupByTag]) const { categories, connectedCatalogIds } = useMemo<{ categories: Category[] connectedCatalogIds: Set }>(() => { const pluginCounts = new Map< PluginSpaceInfo["pluginId"], { label: string; iconSrc: string | null; count: number } >() let myCount = 0 for (const p of allSpaces) { const plugin = detectPluginSpace(p.containerTag) if (plugin) { const prev = pluginCounts.get(plugin.pluginId) pluginCounts.set(plugin.pluginId, { label: plugin.label, iconSrc: plugin.iconSrc, count: (prev?.count ?? 0) + 1, }) } else { myCount += 1 } } const pluginCats: Category[] = Array.from(pluginCounts.entries()) .map(([id, info]) => ({ id: `plugin:${id}` as CategoryId, label: info.label, iconSrc: info.iconSrc, emoji: null, count: info.count, })) .sort((a, b) => b.count - a.count || a.label.localeCompare(b.label)) const connectedIds = new Set() for (const pluginId of pluginCounts.keys()) { if (pluginId === "agents") { for (const catalogId of AGENT_CATALOG_IDS) connectedIds.add(catalogId) continue } const catalogId = spacePluginIdToCatalogId(pluginId) if (catalogId) connectedIds.add(catalogId) } return { categories: [ { id: "all", label: "All Spaces", iconSrc: null, emoji: null, count: allSpaces.length, }, { id: "my", label: "My Spaces", iconSrc: null, emoji: "📁", count: myCount, }, ...pluginCats, ], connectedCatalogIds: connectedIds, } }, [allSpaces]) const defaultCategory = useMemo(() => { if (!currentSelection) return "all" if (currentSelection === AUTO_CHAT_SPACE_ID) return "all" const plugin = detectPluginSpace(currentSelection) if (plugin) return `plugin:${plugin.pluginId}` return "my" }, [currentSelection]) const [activeCategory, setActiveCategory] = useState(defaultCategory) const activeDiscoverId = activeCategory.startsWith("discover:") ? activeCategory.slice("discover:".length) : null useEffect(() => { if (isOpen) setActiveCategory(defaultCategory) }, [isOpen, defaultCategory]) useEffect(() => { if (!activeDiscoverId) return setIsBulkDeleteMode(false) setBulkDeleteTags(new Set()) setLastBulkDeleteTag(null) }, [activeDiscoverId]) const { org, user } = useAuth() const queryClient = useQueryClient() const [connectingPluginId, setConnectingPluginId] = useState( null, ) const [newKey, setNewKey] = useState<{ pluginId: string key: string } | null>(null) const { updateProjectMutation } = useProjectMutations() const { data: availablePluginsData } = useQuery({ queryKey: ["plugins"], queryFn: async () => { const API_URL = process.env.NEXT_PUBLIC_BACKEND_URL ?? "https://api.supermemory.ai" const res = await fetch(`${API_URL}/v3/auth/plugins`, { credentials: "include", }) if (!res.ok) throw new Error("Failed to fetch plugins") return (await res.json()) as { plugins: string[] } }, staleTime: 5 * 60 * 1000, enabled: isOpen && !!activeDiscoverId, }) const { data: apiKeys = [] } = useQuery({ queryKey: ["api-keys", org?.id], enabled: isOpen && !!activeDiscoverId && !!org?.id, queryFn: async () => { if (!org?.id) return [] const data = (await authClient.apiKey.list({ fetchOptions: { query: { metadata: { organizationId: org.id } } }, })) as unknown as { metadata?: Record | null }[] return data.filter((key) => key.metadata?.organizationId === org.id) }, }) const apiKeyConnectedIds = useMemo(() => { const ids = new Set() for (const key of apiKeys) { if (!key.metadata) continue try { const metadata = typeof key.metadata === "string" ? (JSON.parse(key.metadata) as { sm_type?: string sm_client?: string }) : (key.metadata as { sm_type?: string; sm_client?: string }) if (metadata.sm_type === "plugin_auth" && metadata.sm_client) { ids.add(metadata.sm_client) } } catch {} } return ids }, [apiKeys]) const availablePluginIds = useMemo( () => availablePluginsData?.plugins ?? Object.keys(PLUGIN_CATALOG), [availablePluginsData], ) const agentDiscoverCatalogIds = useMemo( () => AGENT_CATALOG_IDS.filter( (id) => availablePluginIds.includes(id) && (!apiKeyConnectedIds.has(id) && !connectedCatalogIds.has(id) ? true : newKey?.pluginId === id), ), [ availablePluginIds, apiKeyConnectedIds, connectedCatalogIds, newKey?.pluginId, ], ) const discoverCategories = useMemo(() => { const categories: Category[] = availablePluginIds .filter((id) => !!PLUGIN_CATALOG[id]) .filter( (id) => !AGENT_CATALOG_IDS.some((agentCatalogId) => agentCatalogId === id), ) .filter( (id) => !apiKeyConnectedIds.has(id) && !connectedCatalogIds.has(id), ) .map((id) => { const info = PLUGIN_CATALOG[id] as PluginInfo return { id: `discover:${id}` as CategoryId, label: info.name, iconSrc: info.icon, emoji: null, count: 0, } }) if (agentDiscoverCatalogIds.length > 0) { categories.unshift({ id: "discover:agents", label: "Agents", iconSrc: null, emoji: null, count: 0, }) } return categories }, [ availablePluginIds, agentDiscoverCatalogIds.length, apiKeyConnectedIds, connectedCatalogIds, ]) const connectMutation = useMutation({ mutationFn: async (pluginId: string) => { const API_URL = process.env.NEXT_PUBLIC_BACKEND_URL ?? "https://api.supermemory.ai" const params = new URLSearchParams({ client: pluginId }) const res = await fetch(`${API_URL}/v3/auth/key?${params}`, { credentials: "include", }) if (!res.ok) { const errorData = (await res.json().catch(() => ({}))) as { message?: string } throw new Error(errorData.message || "Failed to create plugin key") } return (await res.json()) as { key: string } }, onMutate: (pluginId) => setConnectingPluginId(pluginId), onError: (err) => { toast.error("Failed to connect plugin", { description: err instanceof Error ? err.message : "Unknown error", }) }, onSettled: () => { setConnectingPluginId(null) queryClient.invalidateQueries({ queryKey: ["api-keys", org?.id] }) }, onSuccess: (data, pluginId) => { setNewKey({ pluginId, key: data.key }) toast.success("Plugin connected!") }, }) useEffect(() => { if (!isOpen) { setNewKey(null) setEditingProject(null) setIsBulkDeleteMode(false) setBulkDeleteTags(new Set()) setLastBulkDeleteTag(null) } }, [isOpen]) useEffect(() => { if (!editingContainerTag) return const frame = requestAnimationFrame(() => { editInputRef.current?.focus() editInputRef.current?.select() }) return () => cancelAnimationFrame(frame) }, [editingContainerTag]) const handleOpenChange = useCallback( (open: boolean) => { if (!open) { onClose() setSearchQuery("") setEditingProject(null) setIsBulkDeleteMode(false) setBulkDeleteTags(new Set()) setLastBulkDeleteTag(null) } }, [onClose], ) const handleSelect = useCallback( (containerTag: string) => { setEditingProject(null) setIsBulkDeleteMode(false) setBulkDeleteTags(new Set()) setLastBulkDeleteTag(null) onApply( agentGroupByTag.get(containerTag)?.containerTags ?? [containerTag], ) setSearchQuery("") }, [agentGroupByTag, onApply], ) const handleSelectAuto = useCallback(() => { setEditingProject(null) setIsBulkDeleteMode(false) setBulkDeleteTags(new Set()) setLastBulkDeleteTag(null) onApply([AUTO_CHAT_SPACE_ID]) setSearchQuery("") }, [onApply]) const handleBulkModeToggle = useCallback(() => { setEditingProject(null) setBulkDeleteTags(new Set()) setLastBulkDeleteTag(null) setIsBulkDeleteMode((prev) => !prev) }, []) const startEditing = useCallback((project: ContainerTagListType) => { const name = project.name ?? project.containerTag setEditingProject({ id: project.id, containerTag: project.containerTag, originalName: name, name, }) }, []) const cancelEditing = useCallback(() => { setEditingProject(null) }, []) const saveEditing = useCallback(() => { if (!editingProject) return const nextName = editingProject.name.trim() const currentName = editingProject.originalName.trim() if (!nextName || nextName === currentName) return updateProjectMutation.mutate( { containerTag: editingProject.containerTag, name: nextName }, { onSuccess: () => setEditingProject(null), }, ) }, [editingProject, updateProjectMutation]) const handleEditKeyDown = useCallback( (e: React.KeyboardEvent) => { if (e.key === "Enter") { e.preventDefault() saveEditing() } if (e.key === "Escape") { e.preventDefault() cancelEditing() } }, [cancelEditing, saveEditing], ) const filteredProjects = useMemo(() => { const byCategory = allSpaces.filter((p) => { if (activeCategory === "all") return true const plugin = detectPluginSpace(p.containerTag) if (activeCategory === "my") return !plugin return plugin && `plugin:${plugin.pluginId}` === activeCategory }) if (!searchQuery.trim()) return byCategory const query = searchQuery.trim().toLowerCase() return byCategory.filter((p) => { const plugin = detectPluginSpace(p.containerTag) const agentGroup = agentGroupByTag.get(p.containerTag) const projectName = agentGroup?.projectName ?? agentGroup?.label ?? pluginMetaMap.get(p.containerTag)?.projectName const displayName = spaceSelectorDisplayName(p, p.containerTag, { currentUserId: user?.id, }) return ( p.containerTag.toLowerCase().includes(query) || (p.name ?? "").toLowerCase().includes(query) || displayName.toLowerCase().includes(query) || (plugin?.label.toLowerCase().includes(query) ?? false) || (plugin?.projectId?.toLowerCase().includes(query) ?? false) || (projectName?.toLowerCase().includes(query) ?? false) ) }) }, [ allSpaces, activeCategory, searchQuery, pluginMetaMap, agentGroupByTag, user?.id, ]) const recentProjects = useMemo(() => { if (!recents?.length) return [] if (searchQuery.trim()) return [] if (activeCategory !== "all") return [] const byTag = new Map(allSpaces.map((p) => [p.containerTag, p])) const out: ContainerTagListType[] = [] const seen = new Set() for (const tag of recents) { const representativeTag = agentGroupByTag.get(tag)?.representative.containerTag ?? tag const p = byTag.get(representativeTag) if (p && !seen.has(p.containerTag)) { seen.add(p.containerTag) out.push(p) } if (out.length >= 5) break } return out }, [recents, searchQuery, activeCategory, allSpaces, agentGroupByTag]) const recentSet = useMemo( () => new Set(recentProjects.map((p) => p.containerTag)), [recentProjects], ) const mainList = useMemo( () => recentSet.size > 0 ? filteredProjects.filter((p) => !recentSet.has(p.containerTag)) : filteredProjects, [filteredProjects, recentSet], ) const showAutoRow = useMemo(() => { if (!includeAuto) return false if (isBulkDeleteMode) return false if (activeCategory !== "all" && activeCategory !== "my") return false const query = searchQuery.trim().toLowerCase() if (!query) return true return ( "auto".includes(query) || "let nova choose the right spaces".includes(query) || "discover spaces".includes(query) ) }, [includeAuto, isBulkDeleteMode, activeCategory, searchQuery]) const visibleBulkDeleteTags = useMemo( () => [...recentProjects, ...mainList] .filter( (project) => project.containerTag !== DEFAULT_PROJECT_ID && !agentGroupByTag.has(project.containerTag), ) .map((project) => project.containerTag), [recentProjects, mainList, agentGroupByTag], ) const toggleBulkDeleteTag = useCallback( (containerTag: string, shiftKey = false) => { setBulkDeleteTags((prev) => { const next = new Set(prev) const currentIndex = visibleBulkDeleteTags.indexOf(containerTag) const anchorIndex = lastBulkDeleteTag ? visibleBulkDeleteTags.indexOf(lastBulkDeleteTag) : -1 if (shiftKey && currentIndex !== -1 && anchorIndex !== -1) { const start = Math.min(anchorIndex, currentIndex) const end = Math.max(anchorIndex, currentIndex) for (const tag of visibleBulkDeleteTags.slice(start, end + 1)) { next.add(tag) } } else if (next.has(containerTag)) { next.delete(containerTag) } else { next.add(containerTag) } return next }) setLastBulkDeleteTag(containerTag) }, [lastBulkDeleteTag, visibleBulkDeleteTags], ) const bulkDeleteProjects = useMemo( () => allSpaces .filter( (project) => project.containerTag !== DEFAULT_PROJECT_ID && !agentGroupByTag.has(project.containerTag) && bulkDeleteTags.has(project.containerTag), ) .map((project) => ({ id: project.id, name: spaceSelectorDisplayName(project, project.containerTag, { currentUserId: user?.id, }), containerTag: project.containerTag, })), [allSpaces, bulkDeleteTags, agentGroupByTag, user?.id], ) const bulkDeleteCount = bulkDeleteProjects.length const renderRow = useCallback( (project: ContainerTagListType) => { const agentGroup = agentGroupByTag.get(project.containerTag) const isSelected = agentGroup ? agentGroup.containerTags.some((tag) => selectedTagSet.has(tag)) : currentSelection === project.containerTag const plugin = detectPluginSpace(project.containerTag) const pluginProjectName = agentGroup?.projectName ?? agentGroup?.label ?? pluginMetaMap.get(project.containerTag)?.projectName const pluginIdLabel = pluginProjectName || plugin?.projectId const displayName = spaceSelectorDisplayName( project, project.containerTag, { currentUserId: user?.id, }, ) const isDefault = project.containerTag === DEFAULT_PROJECT_ID const isOwnSpace = isOwnConversationSpace(project, user?.id) const isCbSpace = hasCompanyBrain && !plugin && !isOwnSpace && !!project.visibility const isShared = project.visibility === "public" const orgName = org?.name ?? "your team" const orgMembers = org?.members ?? [] const memberCount = orgMembers.length const isDefaultBrain = project.containerTag === SHARED_TEAM_BRAIN_TAG const descriptor = isCbSpace ? isShared ? `${orgName} · ${memberCount} ${ memberCount === 1 ? "member" : "members" }` : "Only you" : null const canEdit = !isDefault && !plugin && !isOwnSpace const canBulkDelete = enableDelete && !isDefault && !agentGroup const isEditing = editingProject?.containerTag === project.containerTag const isBulkDeleteSelected = bulkDeleteTags.has(project.containerTag) const trimmedEditName = editingProject?.name.trim() ?? "" const isSaveDisabled = !trimmedEditName || trimmedEditName === editingProject?.originalName.trim() || updateProjectMutation.isPending const handleRowAction = ( e: React.MouseEvent, ) => { if (isEditing) return if (isBulkDeleteMode) { if (canBulkDelete) { toggleBulkDeleteTag(project.containerTag, e.shiftKey) } return } handleSelect(project.containerTag) } return (
{isEditing ? (
setEditingProject((prev) => prev ? { ...prev, name: e.target.value } : prev, ) } onKeyDown={handleEditKeyDown} className={cn( "min-w-0 flex-1 rounded-[9px] border border-[rgba(82,89,102,0.35)] bg-[#0D121A] px-2.5 py-1.5 text-sm font-medium text-[#fafafa] shadow-inside-out placeholder:text-[#737373] focus:outline-none focus:ring-1 focus:ring-[rgba(75,160,250,0.45)]", dmSansClassName(), )} aria-label="Space name" />
) : ( )} {canEdit && !isEditing && !isBulkDeleteMode && ( )} {enableDelete && !isDefault && !agentGroup && !isEditing && !isBulkDeleteMode && onDeleteRequest && ( )}
) }, [ cancelEditing, agentGroupByTag, bulkDeleteTags, currentSelection, selectedTagSet, editingProject, enableDelete, handleEditKeyDown, handleSelect, hasCompanyBrain, isBulkDeleteMode, onDeleteRequest, pluginMetaMap, saveEditing, startEditing, toggleBulkDeleteTag, updateProjectMutation.isPending, user?.id, org?.name, org?.members, user?.email, user?.image, user?.name, ], ) const renderAutoRow = useCallback(() => { const isSelected = currentSelection === AUTO_CHAT_SPACE_ID return (
{isSelected &&
}
) }, [currentSelection, handleSelectAuto]) const renderCategoryChip = (category: Category, isDiscover: boolean) => { const isActive = activeCategory === category.id return ( ) } const discoverPanelContent = activeDiscoverId === "agents" ? ( connectMutation.mutate(catalogId)} onDismissKey={() => setNewKey(null)} /> ) : ( { if (activeDiscoverId) connectMutation.mutate(activeDiscoverId) }} onDismissKey={() => setNewKey(null)} /> ) const rightPanelContent = activeCategory.startsWith("discover:") ? ( discoverPanelContent ) : ( <>
setSearchQuery(e.target.value)} placeholder="Search spaces..." className={cn( "w-full rounded-[12px] bg-[#14161A] py-2.5 pl-10 pr-4 text-[14px] text-[#fafafa] shadow-inside-out placeholder:text-[#737373] focus:outline-none", dmSansClassName(), )} />
{filteredProjects.length === 0 ? (

No spaces found

) : (
{showAutoRow && ( <>
Mode
{renderAutoRow()}
)} {recentProjects.length > 0 && ( <>
Recently used
{recentProjects.map(renderRow)}
All spaces
)} {hasCompanyBrain && recentProjects.length === 0 ? (() => { const shared = mainList.filter( (p) => p.visibility === "public", ) const personal = mainList.filter( (p) => p.visibility !== "public", ) return ( <> {shared.length > 0 && ( <>
Shared
{shared.map(renderRow)} )} {personal.length > 0 && ( <>
Personal
{personal.map(renderRow)} )} ) })() : mainList.map(renderRow)}
)}
) const footerContent = !activeCategory.startsWith("discover:") && (isBulkDeleteMode || (showNewSpace && onNewSpace)) && (
{isBulkDeleteMode ? ( <>

{bulkDeleteCount === 0 ? "No spaces selected" : `${bulkDeleteCount} ${ bulkDeleteCount === 1 ? "space" : "spaces" } selected`}

) : ( <> {showNewSpace && onNewSpace && ( )} )}
) if (isMobile) { return ( Select Space

Select Space

{isBulkDeleteMode ? "Choose spaces to permanently delete" : "Filter your memories by space"}

{enableDelete && onBulkDeleteRequest && !activeDiscoverId && ( )}
{categories.map((category) => renderCategoryChip(category, false))} {discoverCategories.length > 0 && ( <>
{discoverCategories.map((category) => renderCategoryChip(category, true), )} )}
{rightPanelContent}
{footerContent} ) } return (
Select Space

{isBulkDeleteMode ? "Choose spaces to permanently delete" : "Filter your memories by space"}

{enableDelete && onBulkDeleteRequest && !activeDiscoverId && ( )} Close
{categories.map((category) => { const isActive = activeCategory === category.id return ( ) })} {discoverCategories.length > 0 && ( <>
Discover
{discoverCategories.map((category) => { const isActive = activeCategory === category.id return ( ) })} )}
{activeCategory.startsWith("discover:") ? ( discoverPanelContent ) : ( <>
setSearchQuery(e.target.value)} placeholder="Search spaces..." className={cn( "w-full bg-[#14161A] shadow-inside-out pl-10 pr-4 py-2.5 rounded-[12px] text-[#fafafa] text-[14px] placeholder:text-[#737373] focus:outline-none", dmSansClassName(), )} autoFocus />
{filteredProjects.length === 0 ? (

No spaces found

) : (
{showAutoRow && ( <>
Mode
{renderAutoRow()}
)} {recentProjects.length > 0 && ( <>
Recently used
{recentProjects.map(renderRow)}
All spaces
)} {mainList.map(renderRow)}
)}
)}
{!activeCategory.startsWith("discover:") && (isBulkDeleteMode || (showNewSpace && onNewSpace)) && (
{isBulkDeleteMode ? ( <>

{bulkDeleteCount === 0 ? "No spaces selected" : `${bulkDeleteCount} ${ bulkDeleteCount === 1 ? "space" : "spaces" } selected`}

) : ( <> {showNewSpace && onNewSpace && ( )} )}
)}
) } function AgentsDiscoverPanel({ catalogIds, connectingPluginId, newKey, onConnect, onDismissKey, }: { catalogIds: readonly string[] connectingPluginId: string | null newKey: { pluginId: string; key: string } | null onConnect: (catalogId: string) => void onDismissKey: () => void }) { const [activeCatalogId, setActiveCatalogId] = useState( newKey?.pluginId ?? catalogIds[0] ?? "codex", ) // snap to the just-connected plugin only when the key changes, so manual tab clicks aren't overridden useEffect(() => { if (newKey?.pluginId) setActiveCatalogId(newKey.pluginId) }, [newKey?.pluginId]) useEffect(() => { if (!catalogIds.includes(activeCatalogId)) { setActiveCatalogId(catalogIds[0] ?? "codex") } }, [activeCatalogId, catalogIds]) if (catalogIds.length === 0) { return (

Claude Code, Codex, and OpenCode are connected.

) } return (
{catalogIds.map((catalogId) => { const info = PLUGIN_CATALOG[catalogId] if (!info) return null const isActive = activeCatalogId === catalogId return ( ) })}
onConnect(activeCatalogId)} onDismissKey={onDismissKey} />
) } function DiscoverPanel({ catalogId, isConnecting, newKey, onConnect, onDismissKey, hideHeader, }: { catalogId: string isConnecting: boolean newKey: string | null onConnect: () => void onDismissKey: () => void hideHeader?: boolean }) { const info = PLUGIN_CATALOG[catalogId] if (!info) { return (

Plugin info unavailable.

) } const pluginSteps = info.installSteps ?? [] const stepsEmbedKey = pluginSteps.some((s) => s.code?.includes("sm_...")) const setupSteps = stepsEmbedKey ? pluginSteps : [ { title: "Copy your API key", description: "You won't be able to see it again — store it somewhere safe.", code: newKey ?? "sm_...", copyLabel: "API key", secret: true, }, ...pluginSteps, ] const isConnected = !!newKey return (
{hideHeader ? (

{info.tagline}

) : (
{info.name}

{info.name}

{info.tagline}

)} {isConnected && (

Plugin connected — finish setup

)} {isConnected && (

Your API key is shown once. Hover or focus the blurred command to reveal it.

)} {isConnected ? ( ) : ( <>
{isConnecting ? ( <> Connecting… ) : ( `Connect ${info.name}` )} {info.docsUrl && ( Docs )}

What you'll do next

    {setupSteps.map((step, i) => (
  1. {i + 1} {step.title}
  2. ))}
)}
) }