Files
paperclip/ui/src/components/SidebarAgents.tsx
Forgotten 2ec45c49af feat(ui): add auth pages, company rail, inbox redesign, and page improvements
Add Auth sign-in/sign-up page and InviteLanding page for invite acceptance.
Add CloudAccessGate that checks deployment mode and redirects to /auth when
session is required. Add CompanyRail with drag-and-drop company switching.
Add MarkdownBody prose renderer. Redesign Inbox with category filters and
inline join-request approval. Refactor AgentDetail to overview/configure/runs
views with claude-login support. Replace navigate() anti-patterns with <Link>
components in Dashboard and MetricCard. Add live-run indicators in sidebar
agents. Fix LiveUpdatesProvider cache key resolution for issue identifiers.
Add auth, health, and access API clients.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 14:41:21 -06:00

134 lines
4.8 KiB
TypeScript

import { useMemo, useState } from "react";
import { NavLink, useLocation } from "react-router-dom";
import { useQuery } from "@tanstack/react-query";
import { ChevronRight } from "lucide-react";
import { useCompany } from "../context/CompanyContext";
import { useSidebar } from "../context/SidebarContext";
import { agentsApi } from "../api/agents";
import { heartbeatsApi } from "../api/heartbeats";
import { queryKeys } from "../lib/queryKeys";
import { cn } from "../lib/utils";
import { AgentIcon } from "./AgentIconPicker";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import type { Agent } from "@paperclip/shared";
/** BFS sort: roots first (no reportsTo), then their direct reports, etc. */
function sortByHierarchy(agents: Agent[]): Agent[] {
const byId = new Map(agents.map((a) => [a.id, a]));
const childrenOf = new Map<string | null, Agent[]>();
for (const a of agents) {
const parent = a.reportsTo && byId.has(a.reportsTo) ? a.reportsTo : null;
const list = childrenOf.get(parent) ?? [];
list.push(a);
childrenOf.set(parent, list);
}
const sorted: Agent[] = [];
const queue = childrenOf.get(null) ?? [];
while (queue.length > 0) {
const agent = queue.shift()!;
sorted.push(agent);
const children = childrenOf.get(agent.id);
if (children) queue.push(...children);
}
return sorted;
}
export function SidebarAgents() {
const [open, setOpen] = useState(true);
const { selectedCompanyId } = useCompany();
const { isMobile, setSidebarOpen } = useSidebar();
const location = useLocation();
const { data: agents } = useQuery({
queryKey: queryKeys.agents.list(selectedCompanyId!),
queryFn: () => agentsApi.list(selectedCompanyId!),
enabled: !!selectedCompanyId,
});
const { data: liveRuns } = useQuery({
queryKey: queryKeys.liveRuns(selectedCompanyId!),
queryFn: () => heartbeatsApi.liveRunsForCompany(selectedCompanyId!),
enabled: !!selectedCompanyId,
refetchInterval: 10_000,
});
const liveCountByAgent = useMemo(() => {
const counts = new Map<string, number>();
for (const run of liveRuns ?? []) {
counts.set(run.agentId, (counts.get(run.agentId) ?? 0) + 1);
}
return counts;
}, [liveRuns]);
const visibleAgents = useMemo(() => {
const filtered = (agents ?? []).filter(
(a: Agent) => a.status !== "terminated"
);
return sortByHierarchy(filtered);
}, [agents]);
const agentMatch = location.pathname.match(/^\/agents\/([^/]+)/);
const activeAgentId = agentMatch?.[1] ?? null;
return (
<Collapsible open={open} onOpenChange={setOpen}>
<div className="group">
<div className="flex items-center px-3 py-1.5">
<CollapsibleTrigger className="flex items-center gap-1 flex-1 min-w-0">
<ChevronRight
className={cn(
"h-3 w-3 text-muted-foreground/60 transition-transform opacity-0 group-hover:opacity-100",
open && "rotate-90"
)}
/>
<span className="text-[10px] font-medium uppercase tracking-widest font-mono text-muted-foreground/60">
Agents
</span>
</CollapsibleTrigger>
</div>
</div>
<CollapsibleContent>
<div className="flex flex-col gap-0.5 mt-0.5">
{visibleAgents.map((agent: Agent) => {
const runCount = liveCountByAgent.get(agent.id) ?? 0;
return (
<NavLink
key={agent.id}
to={`/agents/${agent.id}`}
onClick={() => {
if (isMobile) setSidebarOpen(false);
}}
className={cn(
"flex items-center gap-2.5 px-3 py-1.5 text-[13px] font-medium transition-colors",
activeAgentId === agent.id
? "bg-accent text-foreground"
: "text-foreground/80 hover:bg-accent/50 hover:text-foreground"
)}
>
<AgentIcon icon={agent.icon} className="shrink-0 h-3.5 w-3.5 text-muted-foreground" />
<span className="flex-1 truncate">{agent.name}</span>
{runCount > 0 && (
<span className="ml-auto flex items-center gap-1.5 shrink-0">
<span className="relative flex h-2 w-2">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-blue-400 opacity-75" />
<span className="relative inline-flex rounded-full h-2 w-2 bg-blue-500" />
</span>
<span className="text-[11px] font-medium text-blue-400">
{runCount} live
</span>
</span>
)}
</NavLink>
);
})}
</div>
</CollapsibleContent>
</Collapsible>
);
}