Repository Public
1'use client';2import Link from 'next/link';3import { usePathname } from 'next/navigation';4import { PanelLeftClose, PanelLeftOpen, Plus } from 'lucide-react';5import { useSessions } from '@/lib/queries';6import { useUi } from '@/stores/ui';7import { useTerminals } from '@/stores/terminals';8import { useNow } from '@/hooks/use-viewport';9import { useOpenSession } from '@/hooks/use-nav';10import { sortSessions } from '@/hooks/use-current';11import { IconButton } from '@/components/ui/button';12import { Tooltip } from '@/components/ui/tooltip';13import { SessionRow } from '@/components/sessions/session-row';14import { SessionDot } from '@/components/sessions/state-badge';15import { cn } from '@/lib/cn';16import { NAV } from './nav';17import { Logo } from './logo';18import { useBrand } from '@/stores/branding';1920const GROUPS: { label: string; items: string[] }[] = [21 { label: 'Work', items: ['/terminal/', '/projects/', '/sessions/', '/files/', '/tasks/'] },22 { label: 'Platform', items: ['/skills/', '/mcp/', '/cluster/', '/system/', '/settings/'] },23];2425export function Sidebar() {26 const brand = useBrand();27 const pathname = usePathname();28 const collapsed = useUi((s) => s.sidebarCollapsed);29 const setUi = useUi((s) => s.set);30 const openNew = useUi((s) => s.openNewSession);31 const sessions = useSessions().data;32 const activeId = useTerminals((s) => s.activeId);33 const open = useOpenSession();34 const now = useNow(15_000);35 const sorted = sortSessions(sessions ?? []);3637 return (38 <nav aria-label="Primary" data-collapsed={collapsed} className={cn('flex h-full shrink-0 flex-col border-r border-line bg-bg transition-[width] duration-150', collapsed ? 'w-14' : 'w-60')}>39 <div className={cn('flex h-14 items-center', collapsed ? 'justify-center' : 'gap-2.5 px-4')}>40 <Logo className="size-7 shrink-0 rounded-[9px]" />41 {!collapsed && <span className="flex-1 truncate text-[14px] font-bold tracking-tight" data-testid="wordmark">{brand.product}</span>}42 {!collapsed && (43 <IconButton label="Collapse sidebar" size="sm" onClick={() => setUi({ sidebarCollapsed: true })}>44 <PanelLeftClose className="size-4" />45 </IconButton>46 )}47 </div>48 {GROUPS.map((g) => (49 <div key={g.label} className={cn('pt-3', collapsed ? 'px-0' : 'px-2.5')}>50 {!collapsed && <p className="mb-1 px-2.5 text-[11px] font-semibold uppercase tracking-[0.06em] text-faint">{g.label}</p>}51 <ul className={cn('flex flex-col gap-0.5', collapsed && 'items-center')}>52 {NAV.filter((n) => g.items.includes(n.href)).map((n) => {53 const active = pathname === n.href || pathname === n.href.slice(0, -1);54 const link = (55 <Link56 href={n.href}57 aria-current={active ? 'page' : undefined}58 aria-label={collapsed ? n.label : undefined}59 className={cn(60 'flex h-[34px] items-center gap-2.5 rounded-[10px] text-[13px] font-medium transition-colors',61 collapsed ? 'w-9 justify-center' : 'px-2.5',62 active ? 'bg-surface text-text shadow-card' : 'text-text-2 hover:bg-hover hover:text-text',63 )}64 >65 <n.icon className={cn('size-4 shrink-0', active ? 'text-accent' : 'text-muted')} strokeWidth={active ? 2.3 : 2} aria-hidden />66 {!collapsed && n.label}67 </Link>68 );69 return <li key={n.href}>{collapsed ? <Tooltip content={n.label} side="right">{link}</Tooltip> : link}</li>;70 })}71 </ul>72 </div>73 ))}74 <div className="mx-4 mt-3 h-px bg-line" />75 <div className={cn('flex min-h-0 flex-1 flex-col py-3', collapsed ? 'items-center' : 'px-2.5')}>76 {!collapsed && (77 <div className="mb-1 flex h-6 items-center px-2.5">78 <span className="flex-1 text-[11px] font-semibold uppercase tracking-[0.06em] text-faint">Sessions</span>79 <IconButton label="New session" size="sm" onClick={() => openNew()}>80 <Plus className="size-3.5" />81 </IconButton>82 </div>83 )}84 <div className="no-scrollbar min-h-0 flex-1 overflow-y-auto" data-testid="sidebar-sessions">85 {sessions && sorted.length === 0 && !collapsed && <p className="px-2 py-2 text-xs text-muted">No sessions yet</p>}86 {sorted.map((s) =>87 collapsed ? (88 <Tooltip key={s.id} content={s.title} side="right">89 <button aria-label={`Open session ${s.title}`} onClick={() => open(s.id)} className={cn('flex size-8 items-center justify-center rounded-sm hover:bg-surface-2', s.id === activeId && 'bg-surface-2')}>90 <SessionDot state={s.state} />91 </button>92 </Tooltip>93 ) : (94 <SessionRow key={s.id} session={s} dense active={s.id === activeId} onSelect={() => open(s.id)} now={now} />95 ),96 )}97 </div>98 </div>99 {collapsed && (100 <div className="flex justify-center border-t border-line py-2">101 <IconButton label="Expand sidebar" tooltipSide="right" onClick={() => setUi({ sidebarCollapsed: false })}>102 <PanelLeftOpen className="size-4" />103 </IconButton>104 </div>105 )}106 </nav>107 );108}109