/ src / components / org-chart / org-chart-sidebar.tsx
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  }