feat: add agent icons with picker and collapsible sidebar section

- Add `icon` text column to agents DB schema with migration
- Add icon field to shared Agent type and validators
- Create AgentIconPicker component with 40+ curated lucide icons and search
- Show clickable icon next to agent name on detail page header
- Replace static Agents nav item with collapsible AGENTS section in sidebar
- Each agent shows its icon (defaulting to Bot) with truncated name

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Forgotten
2026-02-23 12:25:13 -06:00
parent 5b8708eae9
commit cf237d2e7f
9 changed files with 369 additions and 38 deletions

View File

@@ -0,0 +1,79 @@
import { 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 { 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";
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 visibleAgents = (agents ?? []).filter(
(a: Agent) => a.status !== "terminated"
);
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) => (
<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>
</NavLink>
))}
</div>
</CollapsibleContent>
</Collapsible>
);
}