Skip to content

Commit 02dfe97

Browse files
feat(spa+agentos-server): registry page, agent-less /completion route
SPA: split the agent list into its own searchable RegistryPage (hosted + library sections, register-from-grid), keep HomePage focused on launching new chats. App.tsx loses ~190 lines of sidebar/list scaffolding by routing to the new page instead. agentos-server: new /agentos/api/completion route — agent-less Claude Messages streaming SSE. Wired in routes/completion.ts and mounted from index.ts alongside the existing /chat-sandbox and /run routes. Lets the SPA's "quick chat" surface talk to Claude without spinning up an agent. SSE client: agentos/src/sse.ts ports the multi-dialect parser from test.html — handles claude-agent-sdk (nested), gitagent (flat), and deepagents (LangGraph) payload shapes so the same component reads any of them. Minor: AGENTOS_DEFAULT_MODEL default bumped from claude-sonnet-4-6 to claude-haiku-4-5 (cheaper smoke).
1 parent 747ab61 commit 02dfe97

8 files changed

Lines changed: 684 additions & 359 deletions

File tree

agentos/src/App.tsx

Lines changed: 28 additions & 161 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,5 @@
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";
133
import { api, type Agent } from "./api.ts";
144
import { LogsTab } from "./components/LogsTab.tsx";
155
import { WorkspaceTab } from "./components/WorkspaceTab.tsx";
@@ -18,29 +8,15 @@ import { HomePage } from "./components/HomePage.tsx";
188
import { PolicyTab } from "./components/PolicyTab.tsx";
199
import { PoliciesPage } from "./components/PoliciesPage.tsx";
2010
import { 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";
2312
import { Tabs, TabsList, TabsTrigger } from "./components/ui/tabs.tsx";
2413
import { Badge } from "./components/ui/badge.tsx";
25-
import { ScrollArea } from "./components/ui/scroll-area.tsx";
26-
import { Skeleton } from "./components/ui/skeleton.tsx";
2714
import { Separator } from "./components/ui/separator.tsx";
28-
import { Input } from "./components/ui/input.tsx";
29-
import { StatusDot } from "./components/composite/StatusDot.tsx";
3015
import { PageHeader } from "./components/composite/PageHeader.tsx";
3116
import { cn } from "./lib/cn.ts";
3217

3318
type 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

4521
const 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-
7539
function 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

Comments
 (0)