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

@@ -3,34 +3,39 @@ import {
CircleDot,
Target,
LayoutDashboard,
Bot,
DollarSign,
History,
Search,
SquarePen,
BookOpen,
Paperclip,
} from "lucide-react";
import { useQuery } from "@tanstack/react-query";
import { CompanySwitcher } from "./CompanySwitcher";
import { SidebarSection } from "./SidebarSection";
import { SidebarNavItem } from "./SidebarNavItem";
import { SidebarProjects } from "./SidebarProjects";
import { SidebarAgents } from "./SidebarAgents";
import { useDialog } from "../context/DialogContext";
import { useCompany } from "../context/CompanyContext";
import { sidebarBadgesApi } from "../api/sidebarBadges";
import { heartbeatsApi } from "../api/heartbeats";
import { queryKeys } from "../lib/queryKeys";
import { Button } from "@/components/ui/button";
import { ScrollArea } from "@/components/ui/scroll-area";
export function Sidebar() {
const { openNewIssue } = useDialog();
const { selectedCompanyId } = useCompany();
const { selectedCompanyId, selectedCompany } = useCompany();
const { data: sidebarBadges } = useQuery({
queryKey: queryKeys.sidebarBadges(selectedCompanyId!),
queryFn: () => sidebarBadgesApi.get(selectedCompanyId!),
enabled: !!selectedCompanyId,
});
const { data: liveRuns } = useQuery({
queryKey: queryKeys.liveRuns(selectedCompanyId!),
queryFn: () => heartbeatsApi.liveRunsForCompany(selectedCompanyId!),
enabled: !!selectedCompanyId,
refetchInterval: 10_000,
});
const liveRunCount = liveRuns?.length ?? 0;
function openSearch() {
document.dispatchEvent(new KeyboardEvent("keydown", { key: "k", metaKey: true }));
@@ -38,17 +43,11 @@ export function Sidebar() {
return (
<aside className="w-60 h-full border-r border-border bg-background flex flex-col">
{/* Logo */}
<div className="flex items-center gap-2 px-4 py-3">
<Paperclip className="h-5 w-5 text-foreground" />
<span className="text-sm font-semibold tracking-tight text-foreground">Paperclip</span>
</div>
{/* Company switcher + actions */}
<div className="flex items-center gap-1 px-3 pb-3">
<div className="flex-1 min-w-0">
<CompanySwitcher />
</div>
{/* Top bar: Company name (bold) + Search — aligned with top sections (no visible border) */}
<div className="flex items-center gap-1 px-3 h-12 shrink-0">
<span className="flex-1 text-sm font-bold text-foreground truncate pl-1">
{selectedCompany?.name ?? "Select company"}
</span>
<Button
variant="ghost"
size="icon-sm"
@@ -57,20 +56,20 @@ export function Sidebar() {
>
<Search className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon-sm"
className="text-muted-foreground shrink-0"
onClick={() => openNewIssue()}
>
<SquarePen className="h-4 w-4" />
</Button>
</div>
<ScrollArea className="flex-1">
<nav className="flex flex-col gap-4 px-3 py-2">
<div className="flex flex-col gap-0.5">
<SidebarNavItem to="/dashboard" label="Dashboard" icon={LayoutDashboard} />
{/* New Issue button aligned with nav items */}
<button
onClick={() => openNewIssue()}
className="flex items-center gap-2.5 px-3 py-2 text-[13px] font-medium text-muted-foreground hover:bg-accent/50 hover:text-foreground transition-colors"
>
<SquarePen className="h-4 w-4 shrink-0" />
<span className="truncate">New Issue</span>
</button>
<SidebarNavItem to="/dashboard" label="Dashboard" icon={LayoutDashboard} liveCount={liveRunCount} />
<SidebarNavItem
to="/inbox"
label="Inbox"
@@ -88,18 +87,14 @@ export function Sidebar() {
<SidebarNavItem to="/goals" label="Goals" icon={Target} />
</SidebarSection>
<SidebarAgents />
<SidebarSection label="Company">
<SidebarNavItem to="/agents" label="Agents" icon={Bot} />
<SidebarNavItem to="/costs" label="Costs" icon={DollarSign} />
<SidebarNavItem to="/activity" label="Activity" icon={History} />
</SidebarSection>
</nav>
</ScrollArea>
{/* Bottom links */}
<div className="border-t border-border px-3 py-2">
<SidebarNavItem to="/docs" label="Documentation" icon={BookOpen} />
</div>
</aside>
);
}