Skip to content

Commit 9ee3809

Browse files
authored
Merge branch 'main' into jetc/feat/a2a-v1-migration-a
2 parents 7dc3ae6 + 1b6f30a commit 9ee3809

14 files changed

Lines changed: 571 additions & 82 deletions

ui/src/app/actions/agents.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -571,8 +571,9 @@ export async function getAgents(opts: { namespace?: string } = {}): Promise<Base
571571
try {
572572
const path = opts.namespace ? `/agents?namespace=${encodeURIComponent(opts.namespace)}` : `/agents`;
573573
const { data } = await fetchApi<BaseResponse<AgentResponse[]>>(path);
574+
const agents = Array.isArray(data) ? data : [];
574575

575-
const sortedData = data?.sort((a, b) => {
576+
const sortedData = agents.sort((a, b) => {
576577
const aRef = k8sRefUtils.toRef(a.agent.metadata.namespace || "", a.agent.metadata.name);
577578
const bRef = k8sRefUtils.toRef(b.agent.metadata.namespace || "", b.agent.metadata.name);
578579
return aRef.localeCompare(bRef);

ui/src/app/agents/new/page.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ const DEFAULT_SYSTEM_PROMPT = `You're a helpful agent, made by the kagent team.
6363
function AgentPageContent({ isEditMode, agentName, agentNamespace }: AgentPageContentProps) {
6464
const router = useRouter();
6565
const { models, loading, error, createNewAgent, updateAgent, getAgent, validateAgentData } = useAgents();
66+
const initialNamespace = !isEditMode && agentNamespace?.trim() ? agentNamespace.trim() : "default";
6667

6768
type SelectedModelType = ModelConfig;
6869

@@ -101,7 +102,7 @@ function AgentPageContent({ isEditMode, agentName, agentNamespace }: AgentPageCo
101102

102103
const [state, setState] = useState<FormState>({
103104
name: "",
104-
namespace: "default",
105+
namespace: initialNamespace,
105106
description: "",
106107
agentType: "Declarative",
107108
systemPrompt: isEditMode ? "" : DEFAULT_SYSTEM_PROMPT,
@@ -487,7 +488,11 @@ function AgentPageContent({ isEditMode, agentName, agentNamespace }: AgentPageCo
487488
}
488489

489490
setFormDirty(false);
490-
router.push(`/agents`);
491+
const returnPath =
492+
!isEditMode && agentNamespace
493+
? `/agents?namespace=${encodeURIComponent(state.namespace)}`
494+
: "/agents";
495+
router.push(returnPath);
491496
} catch (e) {
492497
console.error(`Error ${isEditMode ? "updating" : "creating"} agent:`, e);
493498
const errorMessage =
@@ -882,7 +887,7 @@ export default function AgentPage() {
882887
const isEditMode = searchParams.get("edit") === "true";
883888
const agentName = searchParams.get("name");
884889
const agentNamespace = searchParams.get("namespace");
885-
const formKey = isEditMode ? `edit-${agentName}-${agentNamespace}` : "create";
890+
const formKey = isEditMode ? `edit-${agentName}-${agentNamespace}` : `create-${agentNamespace || "default"}`;
886891

887892
return (
888893
<Suspense fallback={<LoadingState />}>

ui/src/components/AgentCard.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,10 @@ import { cn } from "@/lib/utils";
2929

3030
interface AgentCardProps {
3131
agentResponse: AgentResponse;
32+
onAgentsChanged?: () => Promise<void> | void;
3233
}
3334

34-
export function AgentCard({ agentResponse }: AgentCardProps) {
35+
export function AgentCard({ agentResponse, onAgentsChanged }: AgentCardProps) {
3536
const { agent, model, modelProvider, deploymentReady, accepted } = agentResponse;
3637
const router = useRouter();
3738
const [memoriesOpen, setMemoriesOpen] = useState(false);
@@ -197,6 +198,7 @@ export function AgentCard({ agentResponse }: AgentCardProps) {
197198
<DeleteButton
198199
agentName={agent.metadata.name}
199200
namespace={agent.metadata.namespace || ''}
201+
onDeleted={onAgentsChanged}
200202
externalOpen={deleteOpen}
201203
onExternalOpenChange={setDeleteOpen}
202204
/>

ui/src/components/AgentGrid.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ import { k8sRefUtils } from "@/lib/k8sUtils";
44

55
interface AgentGridProps {
66
agentResponse: AgentResponse[];
7+
onAgentsChanged?: () => Promise<void> | void;
78
}
89

9-
export function AgentGrid({ agentResponse }: AgentGridProps) {
10+
export function AgentGrid({ agentResponse, onAgentsChanged }: AgentGridProps) {
1011

1112
return (
1213
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
@@ -15,8 +16,8 @@ export function AgentGrid({ agentResponse }: AgentGridProps) {
1516
item.agent.metadata.namespace || '',
1617
item.agent.metadata.name || '');
1718

18-
return <AgentCard key={agentRef} agentResponse={item} />
19+
return <AgentCard key={agentRef} agentResponse={item} onAgentsChanged={onAgentsChanged} />
1920
})}
2021
</div>
2122
);
22-
}
23+
}

ui/src/components/AgentList.tsx

Lines changed: 134 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
11
"use client";
2-
import { useCallback, useEffect, useState } from "react";
2+
import { useCallback, useEffect, useRef, useState } from "react";
33
import { AgentGrid } from "@/components/AgentGrid";
44
import { AgentListView } from "@/components/AgentListView";
55
import { Plus, LayoutGrid, List } from "lucide-react";
66
import KagentLogo from "@/components/kagent-logo";
77
import Link from "next/link";
8+
import { useRouter, useSearchParams } from "next/navigation";
89
import { ErrorState } from "./ErrorState";
910
import { Button } from "./ui/button";
1011
import { LoadingState } from "./LoadingState";
11-
import { useAgents } from "./AgentsProvider";
12+
import { getAgents } from "@/app/actions/agents";
1213
import { AppPageFrame } from "@/components/layout/AppPageFrame";
1314
import { PageHeader } from "@/components/layout/PageHeader";
1415
import { cn } from "@/lib/utils";
16+
import { NamespaceCombobox } from "@/components/NamespaceCombobox";
17+
import type { AgentResponse } from "@/types";
1518

1619
const AGENTS_VIEW_KEY = "kagent-agents-view";
1720
type AgentsView = "grid" | "list";
@@ -25,8 +28,41 @@ function readStoredView(): AgentsView {
2528
}
2629

2730
export default function AgentList() {
28-
const { agents , loading, error } = useAgents();
31+
const router = useRouter();
32+
const searchParams = useSearchParams();
33+
const selectedNamespace = searchParams.get("namespace")?.trim() || "";
34+
const [agents, setAgents] = useState<AgentResponse[]>([]);
35+
const [loading, setLoading] = useState(true);
36+
const [error, setError] = useState("");
2937
const [view, setView] = useState<AgentsView>("grid");
38+
const latestFetchRequestId = useRef(0);
39+
40+
const fetchAgents = useCallback(async () => {
41+
const requestId = latestFetchRequestId.current + 1;
42+
latestFetchRequestId.current = requestId;
43+
44+
try {
45+
setLoading(true);
46+
const result = await getAgents(selectedNamespace ? { namespace: selectedNamespace } : {});
47+
if (requestId !== latestFetchRequestId.current) {
48+
return;
49+
}
50+
if (result.error) {
51+
throw new Error(result.error || "Failed to fetch agents");
52+
}
53+
setAgents(result.data || []);
54+
setError("");
55+
} catch (err) {
56+
if (requestId !== latestFetchRequestId.current) {
57+
return;
58+
}
59+
setError(err instanceof Error ? err.message : "An unexpected error occurred");
60+
} finally {
61+
if (requestId === latestFetchRequestId.current) {
62+
setLoading(false);
63+
}
64+
}
65+
}, [selectedNamespace]);
3066

3167
useEffect(() => {
3268
const id = requestAnimationFrame(() => {
@@ -35,6 +71,10 @@ export default function AgentList() {
3571
return () => cancelAnimationFrame(id);
3672
}, []);
3773

74+
useEffect(() => {
75+
void fetchAgents();
76+
}, [fetchAgents]);
77+
3878
const setViewAndPersist = useCallback((next: AgentsView) => {
3979
setView(next);
4080
try {
@@ -44,6 +84,22 @@ export default function AgentList() {
4484
}
4585
}, []);
4686

87+
const handleNamespaceChange = useCallback(
88+
(nextNamespace: string) => {
89+
const namespace = nextNamespace.trim();
90+
if (!namespace) {
91+
router.push("/agents");
92+
return;
93+
}
94+
router.push(`/agents?namespace=${encodeURIComponent(namespace)}`);
95+
},
96+
[router],
97+
);
98+
99+
const createHref = selectedNamespace
100+
? `/agents/new?namespace=${encodeURIComponent(selectedNamespace)}`
101+
: "/agents/new";
102+
47103
if (error) {
48104
return <ErrorState message={error} />;
49105
}
@@ -57,69 +113,98 @@ export default function AgentList() {
57113
<PageHeader
58114
titleId="agents-page-title"
59115
title="Agents"
116+
description={
117+
selectedNamespace ? (
118+
<>
119+
Showing agents in namespace <code>{selectedNamespace}</code>.
120+
</>
121+
) : (
122+
"Showing agents across all namespaces."
123+
)
124+
}
60125
className="mb-8"
61126
end={
62-
agents && agents.length > 0 ? (
63-
<div
64-
className="flex w-full min-w-0 items-center justify-end gap-1 rounded-lg border border-border/60 bg-muted/20 p-1"
65-
role="group"
66-
aria-label="Layout"
67-
>
68-
<Button
69-
type="button"
70-
variant="ghost"
71-
size="sm"
72-
className={cn(
73-
"h-8 gap-1.5 px-2.5 text-muted-foreground",
74-
view === "grid" && "bg-card text-foreground shadow-sm",
75-
)}
76-
aria-pressed={view === "grid"}
77-
aria-label="Show agents as cards"
78-
onClick={() => setViewAndPersist("grid")}
79-
>
80-
<LayoutGrid className="h-4 w-4 shrink-0" aria-hidden />
81-
<span className="hidden sm:inline" aria-hidden>
82-
Cards
83-
</span>
84-
</Button>
85-
<Button
86-
type="button"
87-
variant="ghost"
88-
size="sm"
89-
className={cn(
90-
"h-8 gap-1.5 px-2.5 text-muted-foreground",
91-
view === "list" && "bg-card text-foreground shadow-sm",
92-
)}
93-
aria-pressed={view === "list"}
94-
aria-label="Show agents as a list"
95-
onClick={() => setViewAndPersist("list")}
96-
>
97-
<List className="h-4 w-4 shrink-0" aria-hidden />
98-
<span className="hidden sm:inline" aria-hidden>
99-
List
100-
</span>
101-
</Button>
127+
<div className="flex w-full min-w-0 flex-col gap-2 sm:w-auto sm:flex-row sm:items-center sm:justify-end">
128+
<div className="w-full sm:w-72">
129+
<NamespaceCombobox
130+
value={selectedNamespace}
131+
onValueChange={handleNamespaceChange}
132+
includeAllNamespaces
133+
autoSelectDefault={false}
134+
ariaLabel="Namespace"
135+
placeholder="All namespaces"
136+
/>
102137
</div>
103-
) : null
138+
{agents && agents.length > 0 ? (
139+
<div
140+
className="flex w-full min-w-0 items-center justify-end gap-1 rounded-lg border border-border/60 bg-muted/20 p-1 sm:w-auto"
141+
role="group"
142+
aria-label="Layout"
143+
>
144+
<Button
145+
type="button"
146+
variant="ghost"
147+
size="sm"
148+
className={cn(
149+
"h-8 gap-1.5 px-2.5 text-muted-foreground",
150+
view === "grid" && "bg-card text-foreground shadow-sm",
151+
)}
152+
aria-pressed={view === "grid"}
153+
aria-label="Show agents as cards"
154+
onClick={() => setViewAndPersist("grid")}
155+
>
156+
<LayoutGrid className="h-4 w-4 shrink-0" aria-hidden />
157+
<span className="hidden sm:inline" aria-hidden>
158+
Cards
159+
</span>
160+
</Button>
161+
<Button
162+
type="button"
163+
variant="ghost"
164+
size="sm"
165+
className={cn(
166+
"h-8 gap-1.5 px-2.5 text-muted-foreground",
167+
view === "list" && "bg-card text-foreground shadow-sm",
168+
)}
169+
aria-pressed={view === "list"}
170+
aria-label="Show agents as a list"
171+
onClick={() => setViewAndPersist("list")}
172+
>
173+
<List className="h-4 w-4 shrink-0" aria-hidden />
174+
<span className="hidden sm:inline" aria-hidden>
175+
List
176+
</span>
177+
</Button>
178+
</div>
179+
) : null}
180+
</div>
104181
}
105182
/>
106183

107184
{agents?.length === 0 ? (
108185
<div className="rounded-xl border border-border/60 bg-card/30 py-12 text-center shadow-sm">
109186
<KagentLogo className="mx-auto mb-4 h-16 w-16" />
110-
<h2 className="mb-2 text-lg font-medium tracking-tight">No agents yet</h2>
111-
<p className="mb-6 text-pretty text-sm text-muted-foreground">Create an agent to run it in your cluster and wire models and tools in one place.</p>
187+
<h2 className="mb-2 text-lg font-medium tracking-tight">
188+
{selectedNamespace
189+
? `No agents found in namespace "${selectedNamespace}".`
190+
: "No agents yet"}
191+
</h2>
192+
<p className="mb-6 text-pretty text-sm text-muted-foreground">
193+
{selectedNamespace
194+
? "Create an agent in this namespace or switch namespaces."
195+
: "Create an agent to run it in your cluster and wire models and tools in one place."}
196+
</p>
112197
<Button asChild size="lg" className="min-w-[12rem]">
113-
<Link href="/agents/new">
198+
<Link href={createHref}>
114199
<Plus className="mr-2 h-4 w-4" aria-hidden />
115200
New Agent
116201
</Link>
117202
</Button>
118203
</div>
119204
) : view === "list" ? (
120-
<AgentListView agentResponse={agents || []} />
205+
<AgentListView agentResponse={agents || []} onAgentsChanged={fetchAgents} />
121206
) : (
122-
<AgentGrid agentResponse={agents || []} />
207+
<AgentGrid agentResponse={agents || []} onAgentsChanged={fetchAgents} />
123208
)}
124209
</AppPageFrame>
125210
);

ui/src/components/AgentListView.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { isOpenshellSandboxRow, openshellTerminalHref } from "@/lib/openshellSan
2828

2929
interface AgentListViewProps {
3030
agentResponse: AgentResponse[];
31+
onAgentsChanged?: () => Promise<void> | void;
3132
}
3233

3334
type SortKey = "name" | "type" | "providerModel" | "toolCount" | "skillsCount" | "state";
@@ -214,7 +215,7 @@ function SortableTh({ col, label, className, textAlign = "left", sort, onSort }:
214215
);
215216
}
216217

217-
function AgentListRow({ item }: { item: AgentResponse }) {
218+
function AgentListRow({ item, onAgentsChanged }: { item: AgentResponse; onAgentsChanged?: () => Promise<void> | void }) {
218219
const { agent, deploymentReady, accepted } = item;
219220
const router = useRouter();
220221
const [memoriesOpen, setMemoriesOpen] = useState(false);
@@ -389,6 +390,7 @@ function AgentListRow({ item }: { item: AgentResponse }) {
389390
<DeleteButton
390391
agentName={name}
391392
namespace={namespace}
393+
onDeleted={onAgentsChanged}
392394
externalOpen={deleteOpen}
393395
onExternalOpenChange={setDeleteOpen}
394396
/>
@@ -406,7 +408,7 @@ function AgentListRow({ item }: { item: AgentResponse }) {
406408
return trBody;
407409
}
408410

409-
export function AgentListView({ agentResponse }: AgentListViewProps) {
411+
export function AgentListView({ agentResponse, onAgentsChanged }: AgentListViewProps) {
410412
const [sort, setSort] = useState<{ key: SortKey; dir: SortDir }>({ key: "name", dir: "asc" });
411413

412414
const onSort = useCallback((col: SortKey) => {
@@ -453,10 +455,10 @@ export function AgentListView({ agentResponse }: AgentListViewProps) {
453455
item.agent.metadata.namespace || "",
454456
item.agent.metadata.name || "",
455457
);
456-
return <AgentListRow key={key} item={item} />;
458+
return <AgentListRow key={key} item={item} onAgentsChanged={onAgentsChanged} />;
457459
})}
458460
</tbody>
459461
</table>
460462
</div>
461463
);
462-
}
464+
}

0 commit comments

Comments
 (0)