import { AgentConversationSidebar } from "./AgentConversationSidebar"; import { queryViewKind } from "./QueryView"; import { useAgentChatNavigation, useOpenAgentChat } from "@/hooks/useAgentChatNavigation"; import { useLocation, useNavigate } from "@/lib/router"; import { useSidebar } from "@/context/SidebarContext"; import { agentRouteRef } from "@/lib/utils"; import { useQueries } from "@tanstack/react-query"; import { agentsApi } from "@/api/agents"; import { queryKeys } from "@/lib/queryKeys"; import { resourceMembershipState, useResourceMemberships } from "@/hooks/useResourceMemberships"; /** Account- and company-scoped data for the secondary chat navigation. */ export function AgentConversationsSidebar() { const { companyId, userId, agents, chats, session } = useAgentChatNavigation(); const openChat = useOpenAgentChat(companyId, userId); const navigate = useNavigate(); const memberships = useResourceMemberships(companyId); const { isMobile, setSidebarOpen } = useSidebar(); const { pathname } = useLocation(); const activeRef = pathname.split("/chats/")[1]?.split("/")[0]; const roster = agents.data ?? []; // The active roster excludes terminated agents, but their history remains readable. const missingIds = agents.isSuccess ? [...new Set((chats.data ?? []) .flatMap(chat => chat.conversationAgentId && !roster.some(agent => agent.id === chat.conversationAgentId) ? [chat.conversationAgentId] : []))] : []; const historyAgents = useQueries({ queries: missingIds.map(id => ({ queryKey: queryKeys.agents.detail(id), queryFn: () => agentsApi.get(id, companyId!), })) }); const byId = new Map(roster.map(agent => [agent.id, agent])); for (const result of historyAgents) if (result.data) byId.set(result.data.id, result.data); const active = [...byId.values()].find(agent => agent.id === activeRef || (agent.status !== "terminated" && encodeURIComponent(agentRouteRef(agent)) === activeRef)); const conversations = (chats.data ?? []).flatMap(chat => { const agent = chat.conversationAgentId ? byId.get(chat.conversationAgentId) : undefined; return agent ? [agent] : []; }); if (active && !conversations.some(agent => agent.id === active.id)) conversations.unshift(active); // Everyone you can chat with is listed, not only agents you've already // talked to: conversations first by recent activity, then the rest of the // roster alphabetically. Same eligibility rule as the Agents nav section — // terminated agents and agents you've left are omitted. const listedIds = new Set(conversations.map(agent => agent.id)); const teammates = roster .filter(agent => !listedIds.has(agent.id) && agent.status !== "terminated" && (!memberships.isSuccess || resourceMembershipState(memberships.data, "agent", agent.id) !== "left")) .sort((left, right) => left.name.localeCompare(right.name, undefined, { sensitivity: "base" })); const previews = Object.fromEntries((chats.data ?? []).filter(chat => chat.conversationState === "active") .map(chat => [chat.conversationAgentId!, "Working…"])); return agent.id)} activeId={active?.id} previews={previews} // Loaded chats stay listed through an outage (the banner covers it); an // outage before the first load is a skeleton, and only a real failure is an error. loading={[agents, chats, session].some(query => { const kind = queryViewKind(query); return kind === "loading" || kind === "reconnecting"; })} error={[agents, chats, session].find(query => queryViewKind(query) === "error")?.error ?? null} onRetry={() => { void agents.refetch(); void chats.refetch(); void session.refetch(); }} historyLoading={historyAgents.some(result => result.isPending)} historyError={historyAgents.find(result => queryViewKind(result) === "error")?.error} onRetryHistory={() => { historyAgents.filter(result => result.isError).forEach(result => { void result.refetch(); }); }} onAddChat={openChat} onSelect={agent => { navigate(`/chats/${encodeURIComponent(agent.status === "terminated" ? agent.id : agentRouteRef(agent))}`); if (isMobile) setSidebarOpen(false); }} />; }