org-chart-sidebar.tsx
1 'use client' 2 3 import { useCallback, useRef, useState } from 'react' 4 import { AgentAvatar } from '@/components/agents/agent-avatar' 5 import type { Agent } from '@/types' 6 7 const TEAM_COLORS = [ 8 '#6366F1', '#8B5CF6', '#EC4899', '#EF4444', 9 '#F59E0B', '#10B981', '#06B6D4', '#3B82F6', 10 ] 11 12 interface TeamInfo { 13 label: string 14 color: string | null 15 agentIds: string[] 16 } 17 18 interface Props { 19 agents: Agent[] 20 allAgents: Record<string, Agent> 21 teams: TeamInfo[] 22 onDragStart?: (e: React.PointerEvent, agentId: string) => void 23 onTeamDragStart?: (e: React.PointerEvent, team: TeamInfo) => void 24 onPlaceTeam?: (team: TeamInfo) => void 25 onBatchPatch: (patches: Array<{ id: string; patch: Partial<Agent> }>) => void 26 } 27 28 type RoleFilter = 'all' | 'worker' | 'coordinator' 29 type SidebarTab = 'agents' | 'teams' 30 31 export function OrgChartSidebar({ agents, allAgents, teams, onDragStart, onTeamDragStart, onPlaceTeam, onBatchPatch }: Props) { 32 const [query, setQuery] = useState('') 33 const [roleFilter, setRoleFilter] = useState<RoleFilter>('all') 34 const [tab, setTab] = useState<SidebarTab>('agents') 35 const [expandedTeam, setExpandedTeam] = useState<string | null>(null) 36 const [editingLabel, setEditingLabel] = useState<string | null>(null) 37 const [editValue, setEditValue] = useState('') 38 const [showAddAgent, setShowAddAgent] = useState<string | null>(null) 39 const [confirmDelete, setConfirmDelete] = useState<string | null>(null) 40 const [colorPickerOpen, setColorPickerOpen] = useState<string | null>(null) 41 const [showNewTeam, setShowNewTeam] = useState(false) 42 const [newTeamName, setNewTeamName] = useState('') 43 const [newTeamColor, setNewTeamColor] = useState(TEAM_COLORS[0]) 44 const [newTeamConfirmed, setNewTeamConfirmed] = useState(false) 45 const [addAgentSearch, setAddAgentSearch] = useState('') 46 const [width, setWidth] = useState(280) 47 const resizing = useRef(false) 48 49 const onResizePointerDown = useCallback((e: React.PointerEvent) => { 50 e.preventDefault() 51 e.stopPropagation() 52 resizing.current = true 53 const startX = e.clientX 54 const startW = width 55 const target = e.currentTarget as HTMLElement 56 target.setPointerCapture(e.pointerId) 57 58 const onMove = (ev: PointerEvent) => { 59 if (!resizing.current) return 60 const newW = Math.max(180, Math.min(400, startW + (ev.clientX - startX))) 61 setWidth(newW) 62 } 63 const onUp = () => { 64 resizing.current = false 65 target.removeEventListener('pointermove', onMove) 66 target.removeEventListener('pointerup', onUp) 67 target.removeEventListener('pointercancel', onUp) 68 } 69 target.addEventListener('pointermove', onMove) 70 target.addEventListener('pointerup', onUp) 71 target.addEventListener('pointercancel', onUp) 72 }, [width]) 73 74 const allAgentCount = Object.values(allAgents).filter(a => !a.trashedAt).length 75 const hasContent = agents.length > 0 || teams.length > 0 || allAgentCount > 0 76 if (!hasContent) return null 77 78 const filtered = agents.filter((a) => { 79 if (query && !a.name.toLowerCase().includes(query.toLowerCase())) return false 80 if (roleFilter !== 'all' && (a.role || 'worker') !== roleFilter) return false 81 return true 82 }) 83 84 const workerCount = agents.filter((a) => (a.role || 'worker') === 'worker').length 85 const coordCount = agents.filter((a) => a.role === 'coordinator').length 86 87 // Agents not assigned to any team 88 const teamAgentIds = new Set(teams.flatMap((t) => t.agentIds)) 89 const unassignedAgents = Object.values(allAgents).filter( 90 (a) => !a.trashedAt && !teamAgentIds.has(a.id), 91 ) 92 93 const renameTeam = (oldLabel: string, newLabel: string) => { 94 const trimmed = newLabel.trim() 95 if (!trimmed || trimmed === oldLabel) { setEditingLabel(null); return } 96 const team = teams.find((t) => t.label === oldLabel) 97 if (!team) return 98 const patches = team.agentIds.map((id) => ({ 99 id, 100 patch: { orgChart: { ...(allAgents[id]?.orgChart || {}), teamLabel: trimmed } } as Partial<Agent>, 101 })) 102 onBatchPatch(patches) 103 setEditingLabel(null) 104 if (expandedTeam === oldLabel) setExpandedTeam(trimmed) 105 } 106 107 const changeTeamColor = (label: string, color: string) => { 108 const team = teams.find((t) => t.label === label) 109 if (!team) return 110 const patches = team.agentIds.map((id) => ({ 111 id, 112 patch: { orgChart: { ...(allAgents[id]?.orgChart || {}), teamColor: color } } as Partial<Agent>, 113 })) 114 onBatchPatch(patches) 115 } 116 117 const deleteTeam = (label: string) => { 118 const team = teams.find((t) => t.label === label) 119 if (!team) return 120 const patches = team.agentIds.map((id) => ({ 121 id, 122 patch: { orgChart: { ...(allAgents[id]?.orgChart || {}), teamLabel: null, teamColor: null } } as Partial<Agent>, 123 })) 124 onBatchPatch(patches) 125 setConfirmDelete(null) 126 if (expandedTeam === label) setExpandedTeam(null) 127 } 128 129 const removeFromTeam = (agentId: string) => { 130 onBatchPatch([{ 131 id: agentId, 132 patch: { orgChart: { ...(allAgents[agentId]?.orgChart || {}), teamLabel: null, teamColor: null } } as Partial<Agent>, 133 }]) 134 } 135 136 const addToTeam = (agentId: string, teamLabel: string) => { 137 const team = teams.find((t) => t.label === teamLabel) 138 const teamColor = team?.color || null 139 // Place near existing team members 140 let x: number | undefined 141 let y: number | undefined 142 const placedMembers = (team?.agentIds || []) 143 .map((id) => allAgents[id]) 144 .filter((a): a is Agent => !!a && a.orgChart?.x != null) 145 if (placedMembers.length > 0) { 146 let maxX = -Infinity 147 let maxY = 0 148 for (const a of placedMembers) { 149 if ((a.orgChart?.x ?? 0) > maxX) { 150 maxX = a.orgChart?.x ?? 0 151 maxY = a.orgChart?.y ?? 0 152 } 153 } 154 x = maxX + 220 155 y = maxY 156 } 157 onBatchPatch([{ 158 id: agentId, 159 patch: { 160 orgChart: { 161 ...(allAgents[agentId]?.orgChart || {}), 162 teamLabel, 163 ...(teamColor ? { teamColor } : {}), 164 ...(x != null ? { x, y } : {}), 165 }, 166 } as Partial<Agent>, 167 }]) 168 setShowAddAgent(null) 169 } 170 171 return ( 172 <div className="absolute top-4 left-4 z-20 max-h-[calc(100%-32px)] flex flex-col bg-raised/90 backdrop-blur-sm border border-white/[0.06] rounded-[12px] shadow-lg overflow-hidden select-none" style={{ width }} onWheel={(e) => e.stopPropagation()} onPointerDown={(e) => e.stopPropagation()}> 173 {/* Resize handle */} 174 <div 175 className="absolute top-0 right-0 w-1.5 h-full cursor-col-resize z-10 hover:bg-accent-bright/10 active:bg-accent-bright/20 transition-colors" 176 onPointerDown={onResizePointerDown} 177 /> 178 {/* Tab switcher */} 179 <div className="flex border-b border-white/[0.06]"> 180 <button 181 onClick={() => setTab('agents')} 182 className={`flex-1 text-[10px] font-600 uppercase tracking-wider py-2.5 transition-colors cursor-pointer bg-transparent border-none ${ 183 tab === 'agents' ? 'text-text border-b-2 border-accent-bright' : 'text-text-3/60 hover:text-text-3' 184 }`} 185 > 186 Agents ({agents.length}) 187 </button> 188 <button 189 onClick={() => setTab('teams')} 190 className={`flex-1 text-[10px] font-600 uppercase tracking-wider py-2.5 transition-colors cursor-pointer bg-transparent border-none ${ 191 tab === 'teams' ? 'text-text border-b-2 border-accent-bright' : 'text-text-3/60 hover:text-text-3' 192 }`} 193 > 194 Teams ({teams.length}) 195 </button> 196 </div> 197 198 {tab === 'agents' && ( 199 <> 200 <div className="px-2 pt-2"> 201 <input 202 value={query} 203 onChange={(e) => setQuery(e.target.value)} 204 placeholder="Search agents..." 205 className="w-full px-2 py-1 text-[11px] bg-white/[0.04] border border-white/[0.08] rounded-[6px] text-text outline-none focus:border-accent-bright/30 placeholder:text-text-3/40" 206 /> 207 </div> 208 <div className="px-2 pt-1.5 pb-1 flex gap-1"> 209 {([['all', 'All', agents.length], ['worker', 'Workers', workerCount], ['coordinator', 'Coords', coordCount]] as const).map(([key, label, count]) => ( 210 <button 211 key={key} 212 onClick={() => setRoleFilter(key as RoleFilter)} 213 className={`text-[9px] font-500 px-1.5 py-0.5 rounded-[4px] border transition-colors cursor-pointer bg-transparent ${ 214 roleFilter === key 215 ? 'border-accent-bright/30 text-accent-bright bg-accent-bright/10' 216 : 'border-white/[0.06] text-text-3/60 hover:text-text-3' 217 }`} 218 > 219 {label} ({count}) 220 </button> 221 ))} 222 </div> 223 <div className="flex-1 overflow-y-auto overscroll-contain p-2 flex flex-col gap-1"> 224 {filtered.map((agent) => ( 225 <div 226 key={agent.id} 227 className="flex items-center gap-2 px-2 py-1.5 rounded-[8px] hover:bg-white/[0.04] cursor-grab active:cursor-grabbing transition-colors touch-none" 228 onPointerDown={(e) => onDragStart?.(e, agent.id)} 229 > 230 <AgentAvatar seed={agent.avatarSeed || null} avatarUrl={agent.avatarUrl} name={agent.name} size={20} /> 231 <span className="text-[11px] font-500 text-text-2 truncate">{agent.name}</span> 232 </div> 233 ))} 234 {filtered.length === 0 && ( 235 <div className="text-[10px] text-text-3/40 text-center py-2">No matches</div> 236 )} 237 </div> 238 </> 239 )} 240 241 {tab === 'teams' && ( 242 <div className="flex-1 overflow-y-auto overscroll-contain p-2 flex flex-col gap-1"> 243 {teams.length === 0 && !showNewTeam && ( 244 <div className="text-[10px] text-text-3/40 text-center py-4">No teams yet</div> 245 )} 246 {teams.map((team) => { 247 const isExpanded = expandedTeam === team.label 248 const unplacedCount = team.agentIds.filter((id) => allAgents[id]?.orgChart?.x == null).length 249 return ( 250 <div key={team.label} className="rounded-[8px]"> 251 {/* Team row */} 252 <div className="flex items-center gap-1.5 px-1 py-1.5 rounded-[8px] hover:bg-white/[0.04] transition-colors group"> 253 {/* Drag grip */} 254 <div 255 className="shrink-0 w-3.5 h-5 flex flex-col items-center justify-center gap-[2px] cursor-grab active:cursor-grabbing text-text-3/25 hover:text-text-3/50 touch-none" 256 onPointerDown={(e) => onTeamDragStart?.(e, team)} 257 title="Drag to chart" 258 > 259 {[0, 1, 2].map((r) => ( 260 <div key={r} className="flex gap-[2px]"> 261 <div className="w-[2px] h-[2px] rounded-full bg-current" /> 262 <div className="w-[2px] h-[2px] rounded-full bg-current" /> 263 </div> 264 ))} 265 </div> 266 {/* Expand */} 267 <button 268 onClick={() => setExpandedTeam(isExpanded ? null : team.label)} 269 className="w-3.5 h-3.5 flex items-center justify-center text-text-3/40 hover:text-text-2 bg-transparent border-none cursor-pointer shrink-0" 270 > 271 <svg width="8" height="8" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round" 272 style={{ transform: isExpanded ? 'rotate(90deg)' : 'none', transition: 'transform 0.15s' }} 273 > 274 <polyline points="9 18 15 12 9 6" /> 275 </svg> 276 </button> 277 {/* Color dot — click to pick */} 278 <div className="relative"> 279 <button 280 className="w-3 h-3 rounded-full border border-white/[0.1] cursor-pointer hover:scale-110 transition-transform shrink-0" 281 style={{ background: team.color || '#6366F1' }} 282 title="Change color" 283 onClick={() => setColorPickerOpen(colorPickerOpen === team.label ? null : team.label)} 284 /> 285 {colorPickerOpen === team.label && ( 286 <div className="absolute top-5 left-0 z-50 bg-raised border border-white/[0.08] rounded-[8px] p-1.5 flex flex-wrap gap-1 shadow-lg w-[76px]"> 287 {TEAM_COLORS.map((c) => ( 288 <button 289 key={c} 290 className="w-4 h-4 rounded-full border border-white/[0.1] cursor-pointer hover:scale-110 transition-transform" 291 style={{ background: c }} 292 onClick={() => { changeTeamColor(team.label, c); setColorPickerOpen(null) }} 293 /> 294 ))} 295 </div> 296 )} 297 </div> 298 {/* Label — click to rename */} 299 {editingLabel === team.label ? ( 300 <input 301 autoFocus 302 value={editValue} 303 onChange={(e) => setEditValue(e.target.value)} 304 onBlur={() => renameTeam(team.label, editValue)} 305 onKeyDown={(e) => { if (e.key === 'Enter') renameTeam(team.label, editValue) }} 306 className="flex-1 px-1 py-0.5 text-[10px] bg-white/[0.04] border border-white/[0.08] rounded-[4px] text-text outline-none focus:border-accent-bright/30 min-w-0" 307 /> 308 ) : ( 309 <span 310 className="flex-1 text-[11px] font-500 text-text-2 truncate cursor-text" 311 onClick={() => { setEditingLabel(team.label); setEditValue(team.label) }} 312 > 313 {team.label} 314 </span> 315 )} 316 <span className="text-[9px] text-text-3/40 tabular-nums">{team.agentIds.length}</span> 317 {/* Place */} 318 {unplacedCount > 0 && ( 319 <button 320 onClick={() => onPlaceTeam?.(team)} 321 className="hidden group-hover:block text-[8px] font-500 px-1 py-0.5 rounded-[3px] border border-accent-bright/20 text-accent-bright bg-accent-bright/5 hover:bg-accent-bright/15 cursor-pointer transition-colors" 322 title="Place on chart" 323 > 324 Place 325 </button> 326 )} 327 {/* Delete */} 328 {confirmDelete === team.label ? ( 329 <div className="flex gap-1"> 330 <button onClick={() => deleteTeam(team.label)} className="text-[9px] text-red-400 bg-transparent border-none cursor-pointer">Yes</button> 331 <button onClick={() => setConfirmDelete(null)} className="text-[9px] text-text-3 bg-transparent border-none cursor-pointer">No</button> 332 </div> 333 ) : ( 334 <button 335 onClick={() => setConfirmDelete(team.label)} 336 className="hidden group-hover:flex w-3.5 h-3.5 rounded-[3px] items-center justify-center text-text-3/40 hover:text-red-400 bg-transparent border-none cursor-pointer transition-colors" 337 > 338 <svg width="8" height="8" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"> 339 <path d="M3 6h18" /><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6" /><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2" /> 340 </svg> 341 </button> 342 )} 343 </div> 344 345 {/* Expanded: members + add */} 346 {isExpanded && ( 347 <div className="flex flex-col gap-0.5 pl-3 pb-1.5"> 348 {team.agentIds.map((id) => { 349 const a = allAgents[id] 350 if (!a) return null 351 const onChart = a.orgChart?.x != null 352 return ( 353 <div 354 key={id} 355 className={`flex items-center gap-2 px-2 py-1 rounded-[6px] group/member transition-colors ${ 356 onChart 357 ? 'hover:bg-white/[0.03]' 358 : 'hover:bg-white/[0.04] cursor-grab active:cursor-grabbing touch-none' 359 }`} 360 onPointerDown={onChart ? undefined : (e) => onDragStart?.(e, id)} 361 > 362 <AgentAvatar seed={a.avatarSeed || null} avatarUrl={a.avatarUrl} name={a.name} size={16} /> 363 <span className="flex-1 text-[10px] text-text-2 truncate">{a.name}</span> 364 <button 365 onClick={(e) => { e.stopPropagation(); removeFromTeam(id) }} 366 className="hidden group-hover/member:flex w-3.5 h-3.5 rounded-[3px] items-center justify-center text-text-3/30 hover:text-red-400 bg-transparent border-none cursor-pointer transition-colors" 367 title="Remove from team" 368 > 369 <svg width="7" height="7" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round"> 370 <path d="M18 6L6 18" /><path d="M6 6l12 12" /> 371 </svg> 372 </button> 373 </div> 374 ) 375 })} 376 {/* Add agent */} 377 {showAddAgent === team.label ? ( 378 <AgentPicker 379 agents={unassignedAgents} 380 search={addAgentSearch} 381 onSearchChange={setAddAgentSearch} 382 onSelect={(id) => addToTeam(id, team.label)} 383 onClose={() => { setShowAddAgent(null); setAddAgentSearch('') }} 384 /> 385 ) : ( 386 <button 387 onClick={() => { setShowAddAgent(team.label); setAddAgentSearch('') }} 388 className="flex items-center gap-1 px-2 py-1 text-[9px] text-text-3/40 hover:text-text-2 bg-transparent border-none cursor-pointer transition-colors" 389 > 390 <svg width="9" height="9" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"> 391 <line x1="12" y1="5" x2="12" y2="19" /><line x1="5" y1="12" x2="19" y2="12" /> 392 </svg> 393 Add agent 394 </button> 395 )} 396 </div> 397 )} 398 </div> 399 ) 400 })} 401 402 {/* New team */} 403 {showNewTeam ? ( 404 <div className="px-1 py-1.5 flex flex-col gap-1.5 rounded-[8px] border border-white/[0.06] bg-white/[0.02]"> 405 {!newTeamConfirmed ? ( 406 <> 407 {/* Name + color in one step */} 408 <div className="flex items-center gap-1.5"> 409 <input 410 autoFocus 411 value={newTeamName} 412 onChange={(e) => setNewTeamName(e.target.value)} 413 onKeyDown={(e) => { 414 if (e.key === 'Enter' && newTeamName.trim()) setNewTeamConfirmed(true) 415 if (e.key === 'Escape') { setShowNewTeam(false); setNewTeamName(''); setNewTeamConfirmed(false); setNewTeamColor(TEAM_COLORS[0]) } 416 }} 417 placeholder="Team name..." 418 className="flex-1 min-w-0 px-2 py-1.5 text-[10px] bg-white/[0.04] border border-white/[0.08] rounded-[5px] text-text outline-none focus:border-accent-bright/30 placeholder:text-text-3/40" 419 /> 420 <button 421 onClick={() => { if (newTeamName.trim()) setNewTeamConfirmed(true) }} 422 disabled={!newTeamName.trim()} 423 className="shrink-0 text-[9px] font-500 px-2 py-1.5 rounded-[5px] border-none cursor-pointer transition-colors disabled:opacity-30 disabled:cursor-default bg-accent-bright/15 text-accent-bright hover:bg-accent-bright/25" 424 > 425 Next 426 </button> 427 </div> 428 <div className="flex items-center gap-1 px-0.5"> 429 <span className="text-[8px] text-text-3/40 mr-0.5">Color</span> 430 {TEAM_COLORS.map((c) => ( 431 <button 432 key={c} 433 className="w-3.5 h-3.5 rounded-full border-2 cursor-pointer hover:scale-110 transition-all" 434 style={{ 435 background: c, 436 borderColor: newTeamColor === c ? 'rgba(255,255,255,0.5)' : 'rgba(255,255,255,0.08)', 437 }} 438 onClick={() => setNewTeamColor(c)} 439 /> 440 ))} 441 </div> 442 </> 443 ) : ( 444 <> 445 <div className="flex items-center gap-1.5 px-1"> 446 <div className="w-2.5 h-2.5 rounded-full shrink-0" style={{ background: newTeamColor }} /> 447 <span className="text-[10px] font-600 text-text-2 truncate flex-1">{newTeamName.trim()}</span> 448 <button onClick={() => setNewTeamConfirmed(false)} className="text-[8px] text-text-3 hover:text-text-2 bg-transparent border-none cursor-pointer">edit</button> 449 </div> 450 <div className="text-[8px] text-text-3/40 px-1">Pick first agent:</div> 451 <AgentPicker 452 agents={unassignedAgents} 453 search={addAgentSearch} 454 onSearchChange={setAddAgentSearch} 455 onSelect={(id) => { 456 const name = newTeamName.trim() 457 // Add agent with team color 458 const agent = allAgents[id] 459 onBatchPatch([{ 460 id, 461 patch: { 462 orgChart: { 463 ...(agent?.orgChart || {}), 464 teamLabel: name, 465 teamColor: newTeamColor, 466 }, 467 } as Partial<Agent>, 468 }]) 469 setShowNewTeam(false) 470 setNewTeamName('') 471 setNewTeamColor(TEAM_COLORS[0]) 472 setNewTeamConfirmed(false) 473 setAddAgentSearch('') 474 setExpandedTeam(name) 475 }} 476 onClose={() => { setShowNewTeam(false); setNewTeamName(''); setNewTeamColor(TEAM_COLORS[0]); setNewTeamConfirmed(false); setAddAgentSearch('') }} 477 /> 478 </> 479 )} 480 <button 481 onClick={() => { setShowNewTeam(false); setNewTeamName(''); setNewTeamColor(TEAM_COLORS[0]); setNewTeamConfirmed(false); setAddAgentSearch('') }} 482 className="text-[8px] text-text-3/40 hover:text-text-2 bg-transparent border-none cursor-pointer self-center py-0.5" 483 > 484 Cancel 485 </button> 486 </div> 487 ) : ( 488 <button 489 onClick={() => setShowNewTeam(true)} 490 className="flex items-center justify-center gap-1 w-full py-1.5 mt-0.5 rounded-[6px] border border-dashed border-white/[0.08] text-[9px] font-500 text-text-3 hover:text-text-2 hover:bg-white/[0.03] bg-transparent cursor-pointer transition-colors" 491 > 492 <svg width="9" height="9" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"> 493 <line x1="12" y1="5" x2="12" y2="19" /><line x1="5" y1="12" x2="19" y2="12" /> 494 </svg> 495 New Team 496 </button> 497 )} 498 </div> 499 )} 500 </div> 501 ) 502 } 503 504 function AgentPicker({ 505 agents, 506 search, 507 onSearchChange, 508 onSelect, 509 onClose, 510 }: { 511 agents: Agent[] 512 search: string 513 onSearchChange: (v: string) => void 514 onSelect: (id: string) => void 515 onClose: () => void 516 }) { 517 const filtered = search 518 ? agents.filter((a) => a.name.toLowerCase().includes(search.toLowerCase())) 519 : agents 520 521 return ( 522 <div className="mt-0.5 flex flex-col gap-0.5 rounded-[6px] border border-white/[0.06] bg-white/[0.02] p-1"> 523 {agents.length > 3 && ( 524 <input 525 autoFocus 526 value={search} 527 onChange={(e) => onSearchChange(e.target.value)} 528 onKeyDown={(e) => { if (e.key === 'Escape') onClose() }} 529 placeholder="Search..." 530 className="w-full px-1.5 py-1 text-[9px] bg-white/[0.04] border border-white/[0.08] rounded-[4px] text-text outline-none focus:border-accent-bright/30 placeholder:text-text-3/40 mb-0.5" 531 /> 532 )} 533 <div className="max-h-[100px] overflow-y-auto flex flex-col gap-0.5"> 534 {agents.length === 0 ? ( 535 <div className="text-[9px] text-text-3/40 text-center py-1.5">All agents assigned</div> 536 ) : filtered.length === 0 ? ( 537 <div className="text-[9px] text-text-3/40 text-center py-1.5">No matches</div> 538 ) : ( 539 filtered.map((a) => ( 540 <button 541 key={a.id} 542 onClick={() => onSelect(a.id)} 543 className="flex items-center gap-2 px-1.5 py-1 rounded-[5px] hover:bg-white/[0.04] bg-transparent border-none cursor-pointer text-left w-full transition-colors" 544 > 545 <AgentAvatar seed={a.avatarSeed || null} avatarUrl={a.avatarUrl} name={a.name} size={14} /> 546 <span className="text-[9px] text-text-3 truncate">{a.name}</span> 547 </button> 548 )) 549 )} 550 </div> 551 </div> 552 ) 553 }