1- import { useEffect , useState } from "react" ;
1+ import { useEffect , useMemo , useState } from "react" ;
22import {
33 Home as HomeIcon ,
44 Activity ,
77 ChevronDown ,
88 Folder ,
99 FolderOpen ,
10+ Search ,
11+ X ,
1012} from "lucide-react" ;
1113import { api , type Agent } from "./api.ts" ;
1214import { LogsTab } from "./components/LogsTab.tsx" ;
@@ -16,11 +18,14 @@ import { HomePage } from "./components/HomePage.tsx";
1618import { PolicyTab } from "./components/PolicyTab.tsx" ;
1719import { PoliciesPage } from "./components/PoliciesPage.tsx" ;
1820import { ObservabilityTab } from "./components/observability/ObservabilityTab.tsx" ;
21+ import { AgentCard } from "./components/AgentCard.tsx" ;
22+ import { RegisterAgentForm } from "./components/RegisterAgentForm.tsx" ;
1923import { Tabs , TabsList , TabsTrigger } from "./components/ui/tabs.tsx" ;
2024import { Badge } from "./components/ui/badge.tsx" ;
2125import { ScrollArea } from "./components/ui/scroll-area.tsx" ;
2226import { Skeleton } from "./components/ui/skeleton.tsx" ;
2327import { Separator } from "./components/ui/separator.tsx" ;
28+ import { Input } from "./components/ui/input.tsx" ;
2429import { StatusDot } from "./components/composite/StatusDot.tsx" ;
2530import { PageHeader } from "./components/composite/PageHeader.tsx" ;
2631import { cn } from "./lib/cn.ts" ;
@@ -55,6 +60,18 @@ function typeLogo(harness: string): string | null {
5560 return null ;
5661}
5762
63+ function SectionLabel ( { name, count } : { name : string ; count : number } ) {
64+ return (
65+ < div className = "flex items-center gap-2 px-1" >
66+ < span className = "text-[9.5px] font-semibold uppercase tracking-[0.14em] text-muted-foreground/70" >
67+ { name }
68+ </ span >
69+ < span className = "flex-1 h-px bg-border/60" />
70+ < span className = "text-[9.5px] font-mono text-muted-foreground/50" > { count } </ span >
71+ </ div >
72+ ) ;
73+ }
74+
5875function TypeBadge ( { agent, className = "" } : { agent : Agent ; className ?: string } ) {
5976 const logo = typeLogo ( agent . harness ) ;
6077 return (
@@ -77,6 +94,26 @@ export default function App() {
7794 const [ err , setErr ] = useState < string | null > ( null ) ;
7895 const [ launchMessage , setLaunchMessage ] = useState < string | null > ( null ) ;
7996 const [ agentsOpen , setAgentsOpen ] = useState ( true ) ;
97+ const [ search , setSearch ] = useState ( "" ) ;
98+
99+ const filteredAgents = useMemo ( ( ) => {
100+ if ( ! search . trim ( ) ) return agents ;
101+ const q = search . trim ( ) . toLowerCase ( ) ;
102+ return agents . filter ( ( a ) =>
103+ a . name . toLowerCase ( ) . includes ( q ) ||
104+ ( a . label ?? "" ) . toLowerCase ( ) . includes ( q ) ||
105+ ( a . sourceUrl ?? "" ) . toLowerCase ( ) . includes ( q ) ||
106+ a . harness . toLowerCase ( ) . includes ( q ) ,
107+ ) ;
108+ } , [ agents , search ] ) ;
109+
110+ // Group by origin so Hosted (server-config'd) and Library (registry,
111+ // dashboard- or SDK-registered) are visually separated.
112+ const grouped = useMemo ( ( ) => {
113+ const hosted = filteredAgents . filter ( ( a ) => ( a . origin ?? "in-memory" ) === "in-memory" ) ;
114+ const library = filteredAgents . filter ( ( a ) => a . origin === "registry" ) ;
115+ return { hosted, library } ;
116+ } , [ filteredAgents ] ) ;
80117
81118 useEffect ( ( ) => {
82119 api . agents ( ) . then ( setAgents ) . catch ( ( e ) => setErr ( String ( e ) ) ) ;
@@ -100,7 +137,7 @@ export default function App() {
100137 return (
101138 < div className = "flex h-full bg-background text-foreground" >
102139 { /* Left rail */ }
103- < aside className = "w-72 shrink-0 border-r border-border bg-card flex flex-col" >
140+ < aside className = "w-64 md:w- 72 lg:w-80 shrink-0 border-r border-border bg-card flex flex-col overflow-hidden " >
104141 < div className = "px-5 py-4 border-b border-border" >
105142 < div className = "flex items-center gap-2.5" >
106143 < img src = "/logos/agentos.png" alt = "ComputerAgent" className = "h-8 w-8 rounded-md object-contain" />
@@ -146,42 +183,80 @@ export default function App() {
146183 </ div >
147184
148185 { agentsOpen && (
149- < ScrollArea className = "flex-1 mt-1 mr-2" >
150- < div className = "pl-3 ml-4 border-l border-border space-y-0.5" >
151- { err && < div className = "m-2 text-xs text-destructive" > { err } </ div > }
152- { agents . length === 0 && ! err && (
153- < div className = "m-2 space-y-1.5" >
154- < Skeleton className = "h-12 w-full" />
155- < Skeleton className = "h-12 w-full" />
156- < Skeleton className = "h-12 w-full" />
157- </ div >
158- ) }
159- { agents . map ( ( a ) => (
160- < button
161- key = { a . name }
162- onClick = { ( ) => openAgent ( a . name ) }
163- className = { cn (
164- "w-full text-left rounded-md px-3 py-2 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" ,
165- view === "dashboard" && selected === a . name
166- ? "bg-muted ring-1 ring-primary/40"
167- : "hover:bg-muted/60" ,
168- ) }
169- >
170- < div className = "flex items-center gap-2" >
171- < StatusDot status = { a . activeSandboxes > 0 ? "live" : "idle" } />
172- < span className = "font-medium text-sm truncate flex-1" > { agentNameFromSource ( a . sourceUrl ?? "" ) } </ span >
173- < TypeBadge agent = { a } />
186+ < >
187+ { /* Search — sticky at top of the rail, opaque so cards never
188+ appear "behind" it as they scroll. */ }
189+ < div className = "px-3 pt-2 pb-2 bg-card border-b border-border/60 relative z-10" >
190+ < div className = "relative" >
191+ < Search className = "absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground/60 pointer-events-none" />
192+ < Input
193+ value = { search }
194+ onChange = { ( e ) => setSearch ( e . target . value ) }
195+ placeholder = "Search agents…"
196+ className = "pl-8 pr-7 h-8 text-xs bg-background"
197+ />
198+ { search && (
199+ < button
200+ onClick = { ( ) => setSearch ( "" ) }
201+ className = "absolute right-1.5 top-1/2 -translate-y-1/2 h-5 w-5 grid place-items-center rounded text-muted-foreground/70 hover:text-foreground hover:bg-muted"
202+ title = "Clear"
203+ >
204+ < X className = "h-3 w-3" />
205+ </ button >
206+ ) }
207+ </ div >
208+ </ div >
209+
210+ < div className = "flex-1 overflow-y-auto overflow-x-hidden mt-2 min-w-0" >
211+ < div className = "px-3 pb-3 space-y-3 min-w-0" >
212+ { err && < div className = "text-xs text-destructive" > { err } </ div > }
213+ { agents . length === 0 && ! err && (
214+ < div className = "space-y-2" >
215+ < Skeleton className = "h-20 w-full" />
216+ < Skeleton className = "h-20 w-full" />
217+ < Skeleton className = "h-20 w-full" />
218+ </ div >
219+ ) }
220+ { agents . length > 0 && filteredAgents . length === 0 && (
221+ < div className = "text-center text-xs text-muted-foreground py-6" >
222+ No agents match < span className = "font-mono" > "{ search } "</ span >
223+ </ div >
224+ ) }
225+
226+ { grouped . hosted . length > 0 && (
227+ < div className = "space-y-2 min-w-0" >
228+ < SectionLabel name = "Hosted" count = { grouped . hosted . length } />
229+ { grouped . hosted . map ( ( a ) => (
230+ < AgentCard
231+ key = { a . name }
232+ agent = { a }
233+ selected = { view === "dashboard" && selected === a . name }
234+ onClick = { ( ) => openAgent ( a . name ) }
235+ />
236+ ) ) }
174237 </ div >
175- < div className = "mt-1 text-[11px] text-muted-foreground truncate" > { a . sourceUrl ?? "" } </ div >
176- < div className = "mt-1 flex gap-3 text-[10px] text-muted-foreground/70" >
177- < span > { a . sessionCount } sessions</ span >
178- < span > { a . logCount } logs</ span >
179- < span > { timeAgo ( a . lastActivity ) } </ span >
238+ ) }
239+
240+ { grouped . library . length > 0 && (
241+ < div className = "space-y-2 min-w-0" >
242+ < SectionLabel name = "Library" count = { grouped . library . length } />
243+ { grouped . library . map ( ( a ) => (
244+ < AgentCard
245+ key = { a . name }
246+ agent = { a }
247+ selected = { view === "dashboard" && selected === a . name }
248+ onClick = { ( ) => openAgent ( a . name ) }
249+ />
250+ ) ) }
180251 </ div >
181- </ button >
182- ) ) }
252+ ) }
253+
254+ < div className = "pt-2" >
255+ < RegisterAgentForm onRegistered = { ( ) => api . agents ( ) . then ( setAgents ) . catch ( ( ) => { } ) } />
256+ </ div >
257+ </ div >
183258 </ div >
184- </ ScrollArea >
259+ </ >
185260 ) }
186261
187262 < Separator />
0 commit comments