1- import { useEffect , useMemo , useState } from "react" ;
2- import {
3- Home as HomeIcon ,
4- Activity ,
5- Shield ,
6- ChevronRight ,
7- ChevronDown ,
8- Folder ,
9- FolderOpen ,
10- Search ,
11- X ,
12- } from "lucide-react" ;
1+ import { useCallback , useEffect , useState } from "react" ;
2+ import { Home as HomeIcon , Activity , Shield , Boxes } from "lucide-react" ;
133import { api , type Agent } from "./api.ts" ;
144import { LogsTab } from "./components/LogsTab.tsx" ;
155import { WorkspaceTab } from "./components/WorkspaceTab.tsx" ;
@@ -18,29 +8,15 @@ import { HomePage } from "./components/HomePage.tsx";
188import { PolicyTab } from "./components/PolicyTab.tsx" ;
199import { PoliciesPage } from "./components/PoliciesPage.tsx" ;
2010import { ObservabilityTab } from "./components/observability/ObservabilityTab.tsx" ;
21- import { AgentCard } from "./components/AgentCard.tsx" ;
22- import { RegisterAgentForm } from "./components/RegisterAgentForm.tsx" ;
11+ import { RegistryPage } from "./components/RegistryPage.tsx" ;
2312import { Tabs , TabsList , TabsTrigger } from "./components/ui/tabs.tsx" ;
2413import { Badge } from "./components/ui/badge.tsx" ;
25- import { ScrollArea } from "./components/ui/scroll-area.tsx" ;
26- import { Skeleton } from "./components/ui/skeleton.tsx" ;
2714import { Separator } from "./components/ui/separator.tsx" ;
28- import { Input } from "./components/ui/input.tsx" ;
29- import { StatusDot } from "./components/composite/StatusDot.tsx" ;
3015import { PageHeader } from "./components/composite/PageHeader.tsx" ;
3116import { cn } from "./lib/cn.ts" ;
3217
3318type Tab = "chat" | "schedules" | "policy" | "logs" ;
34- type View = "home" | "observability" | "policies" | "dashboard" ;
35-
36- function timeAgo ( iso : string | null ) : string {
37- if ( ! iso ) return "never" ;
38- const s = Math . floor ( ( Date . now ( ) - new Date ( iso ) . getTime ( ) ) / 1000 ) ;
39- if ( s < 60 ) return `${ s } s ago` ;
40- if ( s < 3600 ) return `${ Math . floor ( s / 60 ) } m ago` ;
41- if ( s < 86400 ) return `${ Math . floor ( s / 3600 ) } h ago` ;
42- return `${ Math . floor ( s / 86400 ) } d ago` ;
43- }
19+ type View = "home" | "observability" | "policies" | "registry" | "dashboard" ;
4420
4521const NAME_OVERRIDES : Record < string , string > = {
4622 "general-agent" : "General Agent" ,
@@ -60,18 +36,6 @@ function typeLogo(harness: string): string | null {
6036 return null ;
6137}
6238
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-
7539function TypeBadge ( { agent, className = "" } : { agent : Agent ; className ?: string } ) {
7640 const logo = typeLogo ( agent . harness ) ;
7741 return (
@@ -94,35 +58,19 @@ export default function App() {
9458 const [ tab , setTab ] = useState < Tab > ( "chat" ) ;
9559 const [ err , setErr ] = useState < string | null > ( null ) ;
9660 const [ launchMessage , setLaunchMessage ] = useState < string | null > ( null ) ;
97- const [ agentsOpen , setAgentsOpen ] = useState ( true ) ;
98- const [ search , setSearch ] = useState ( "" ) ;
9961
100- const filteredAgents = useMemo ( ( ) => {
101- if ( ! search . trim ( ) ) return agents ;
102- const q = search . trim ( ) . toLowerCase ( ) ;
103- return agents . filter ( ( a ) =>
104- a . name . toLowerCase ( ) . includes ( q ) ||
105- ( a . label ?? "" ) . toLowerCase ( ) . includes ( q ) ||
106- ( a . sourceUrl ?? "" ) . toLowerCase ( ) . includes ( q ) ||
107- a . harness . toLowerCase ( ) . includes ( q ) ,
108- ) ;
109- } , [ agents , search ] ) ;
110-
111- // Group by origin so Hosted (server-config'd) and Library (registry,
112- // dashboard- or SDK-registered) are visually separated.
113- const grouped = useMemo ( ( ) => {
114- const hosted = filteredAgents . filter ( ( a ) => ( a . origin ?? "in-memory" ) === "in-memory" ) ;
115- const library = filteredAgents . filter ( ( a ) => a . origin === "registry" ) ;
116- return { hosted, library } ;
117- } , [ filteredAgents ] ) ;
118-
119- useEffect ( ( ) => {
120- api . agents ( )
62+ const reloadAgents = useCallback ( ( ) => {
63+ api
64+ . agents ( )
12165 . then ( setAgents )
12266 . catch ( ( e ) => setErr ( String ( e ) ) )
12367 . finally ( ( ) => setAgentsLoaded ( true ) ) ;
12468 } , [ ] ) ;
12569
70+ useEffect ( ( ) => {
71+ reloadAgents ( ) ;
72+ } , [ reloadAgents ] ) ;
73+
12674 const agent = agents . find ( ( a ) => a . name === selected ) ?? null ;
12775
12876 const openAgent = ( name : string ) => {
@@ -159,6 +107,12 @@ export default function App() {
159107 active = { view === "home" }
160108 onClick = { ( ) => setView ( "home" ) }
161109 />
110+ < RailButton
111+ icon = { Boxes }
112+ label = "Agent Registry"
113+ active = { view === "registry" || view === "dashboard" }
114+ onClick = { ( ) => setView ( "registry" ) }
115+ />
162116 < RailButton
163117 icon = { Activity }
164118 label = "Observability"
@@ -173,103 +127,7 @@ export default function App() {
173127 />
174128 </ nav >
175129
176- { /* Agents folder */ }
177- < div className = "px-2 pt-3" >
178- < button
179- onClick = { ( ) => setAgentsOpen ( ( o ) => ! o ) }
180- className = "w-full flex items-center gap-1.5 px-3 py-1.5 text-xs uppercase tracking-wider rounded-md hover:bg-muted text-muted-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
181- >
182- { agentsOpen ? < ChevronDown className = "h-3 w-3" /> : < ChevronRight className = "h-3 w-3" /> }
183- { agentsOpen ? < FolderOpen className = "h-3.5 w-3.5" /> : < Folder className = "h-3.5 w-3.5" /> }
184- < span className = "font-medium" > Agents</ span >
185- < span className = "ml-auto text-[10px] text-muted-foreground/70" > { agents . length || "" } </ span >
186- </ button >
187- </ div >
188-
189- { agentsOpen && (
190- < >
191- { /* Search — sticky at top of the rail, opaque so cards never
192- appear "behind" it as they scroll. */ }
193- < div className = "px-3 pt-2 pb-2 bg-card border-b border-border/60 relative z-10" >
194- < div className = "relative" >
195- < 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" />
196- < Input
197- value = { search }
198- onChange = { ( e ) => setSearch ( e . target . value ) }
199- placeholder = "Search agents…"
200- className = "pl-8 pr-7 h-8 text-xs bg-background"
201- />
202- { search && (
203- < button
204- onClick = { ( ) => setSearch ( "" ) }
205- 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"
206- title = "Clear"
207- >
208- < X className = "h-3 w-3" />
209- </ button >
210- ) }
211- </ div >
212- </ div >
213-
214- < div className = "flex-1 overflow-y-auto overflow-x-hidden mt-2 min-w-0" >
215- < div className = "px-3 pb-3 space-y-3 min-w-0" >
216- { err && < div className = "text-xs text-destructive" > { err } </ div > }
217- { ! agentsLoaded && ! err && (
218- < div className = "space-y-2" >
219- < Skeleton className = "h-20 w-full" />
220- < Skeleton className = "h-20 w-full" />
221- < Skeleton className = "h-20 w-full" />
222- </ div >
223- ) }
224- { agentsLoaded && agents . length === 0 && ! err && (
225- < div className = "text-center text-xs text-muted-foreground py-6 px-2 space-y-1" >
226- < div > No agents registered yet.</ div >
227- < div className = "text-muted-foreground/70" >
228- Use the < span className = "font-mono" > + Register</ span > button below, or POST to < span className = "font-mono" > /api/agents/register</ span > .
229- </ div >
230- </ div >
231- ) }
232- { agents . length > 0 && filteredAgents . length === 0 && (
233- < div className = "text-center text-xs text-muted-foreground py-6" >
234- No agents match < span className = "font-mono" > "{ search } "</ span >
235- </ div >
236- ) }
237-
238- { grouped . hosted . length > 0 && (
239- < div className = "space-y-2 min-w-0" >
240- < SectionLabel name = "Hosted" count = { grouped . hosted . length } />
241- { grouped . hosted . map ( ( a ) => (
242- < AgentCard
243- key = { a . name }
244- agent = { a }
245- selected = { view === "dashboard" && selected === a . name }
246- onClick = { ( ) => openAgent ( a . name ) }
247- />
248- ) ) }
249- </ div >
250- ) }
251-
252- { grouped . library . length > 0 && (
253- < div className = "space-y-2 min-w-0" >
254- < SectionLabel name = "Library" count = { grouped . library . length } />
255- { grouped . library . map ( ( a ) => (
256- < AgentCard
257- key = { a . name }
258- agent = { a }
259- selected = { view === "dashboard" && selected === a . name }
260- onClick = { ( ) => openAgent ( a . name ) }
261- />
262- ) ) }
263- </ div >
264- ) }
265-
266- < div className = "pt-2" >
267- < RegisterAgentForm onRegistered = { ( ) => api . agents ( ) . then ( setAgents ) . catch ( ( ) => { } ) } />
268- </ div >
269- </ div >
270- </ div >
271- </ >
272- ) }
130+ < div className = "flex-1" />
273131
274132 < Separator />
275133 < div className = "px-4 py-3 text-[10px] text-muted-foreground/70" > agentos.clawagent.sh</ div >
@@ -280,9 +138,18 @@ export default function App() {
280138 { view === "policies" ? (
281139 < PoliciesPage />
282140 ) : view === "home" ? (
283- < HomePage onLaunch = { launchFromHome } onOpenDashboard = { ( ) => agents [ 0 ] && openAgent ( agents [ 0 ] . name ) } />
141+ < HomePage agents = { agents } onLaunch = { launchFromHome } onOpenDashboard = { ( ) => agents [ 0 ] && openAgent ( agents [ 0 ] . name ) } />
284142 ) : view === "observability" ? (
285143 < ObservabilityTab />
144+ ) : view === "registry" ? (
145+ < RegistryPage
146+ agents = { agents }
147+ loaded = { agentsLoaded }
148+ err = { err }
149+ selected = { selected }
150+ onOpenAgent = { openAgent }
151+ onReload = { reloadAgents }
152+ />
286153 ) : agent ? (
287154 < >
288155 < PageHeader
0 commit comments