Skip to content
3 changes: 2 additions & 1 deletion ui/src/app/actions/agents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -571,8 +571,9 @@ export async function getAgents(opts: { namespace?: string } = {}): Promise<Base
try {
const path = opts.namespace ? `/agents?namespace=${encodeURIComponent(opts.namespace)}` : `/agents`;
const { data } = await fetchApi<BaseResponse<AgentResponse[]>>(path);
const agents = Array.isArray(data) ? data : [];

const sortedData = data?.sort((a, b) => {
const sortedData = agents.sort((a, b) => {
const aRef = k8sRefUtils.toRef(a.agent.metadata.namespace || "", a.agent.metadata.name);
const bRef = k8sRefUtils.toRef(b.agent.metadata.namespace || "", b.agent.metadata.name);
return aRef.localeCompare(bRef);
Expand Down
11 changes: 8 additions & 3 deletions ui/src/app/agents/new/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ const DEFAULT_SYSTEM_PROMPT = `You're a helpful agent, made by the kagent team.
function AgentPageContent({ isEditMode, agentName, agentNamespace }: AgentPageContentProps) {
const router = useRouter();
const { models, loading, error, createNewAgent, updateAgent, getAgent, validateAgentData } = useAgents();
const initialNamespace = !isEditMode && agentNamespace?.trim() ? agentNamespace.trim() : "default";

type SelectedModelType = ModelConfig;

Expand Down Expand Up @@ -100,7 +101,7 @@ function AgentPageContent({ isEditMode, agentName, agentNamespace }: AgentPageCo

const [state, setState] = useState<FormState>({
name: "",
namespace: "default",
namespace: initialNamespace,
description: "",
agentType: "Declarative",
systemPrompt: isEditMode ? "" : DEFAULT_SYSTEM_PROMPT,
Expand Down Expand Up @@ -486,7 +487,11 @@ function AgentPageContent({ isEditMode, agentName, agentNamespace }: AgentPageCo
}

setFormDirty(false);
router.push(`/agents`);
const returnPath =
!isEditMode && agentNamespace
? `/agents?namespace=${encodeURIComponent(state.namespace)}`
: "/agents";
router.push(returnPath);
} catch (e) {
console.error(`Error ${isEditMode ? "updating" : "creating"} agent:`, e);
const errorMessage =
Expand Down Expand Up @@ -881,7 +886,7 @@ export default function AgentPage() {
const isEditMode = searchParams.get("edit") === "true";
const agentName = searchParams.get("name");
const agentNamespace = searchParams.get("namespace");
const formKey = isEditMode ? `edit-${agentName}-${agentNamespace}` : "create";
const formKey = isEditMode ? `edit-${agentName}-${agentNamespace}` : `create-${agentNamespace || "default"}`;

return (
<Suspense fallback={<LoadingState />}>
Expand Down
4 changes: 3 additions & 1 deletion ui/src/components/AgentCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,10 @@ import { cn } from "@/lib/utils";

interface AgentCardProps {
agentResponse: AgentResponse;
onAgentsChanged?: () => Promise<void> | void;
}

export function AgentCard({ agentResponse }: AgentCardProps) {
export function AgentCard({ agentResponse, onAgentsChanged }: AgentCardProps) {
const { agent, model, modelProvider, deploymentReady, accepted } = agentResponse;
const router = useRouter();
const [memoriesOpen, setMemoriesOpen] = useState(false);
Expand Down Expand Up @@ -197,6 +198,7 @@ export function AgentCard({ agentResponse }: AgentCardProps) {
<DeleteButton
agentName={agent.metadata.name}
namespace={agent.metadata.namespace || ''}
onDeleted={onAgentsChanged}
externalOpen={deleteOpen}
onExternalOpenChange={setDeleteOpen}
/>
Expand Down
7 changes: 4 additions & 3 deletions ui/src/components/AgentGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import { k8sRefUtils } from "@/lib/k8sUtils";

interface AgentGridProps {
agentResponse: AgentResponse[];
onAgentsChanged?: () => Promise<void> | void;
}

export function AgentGrid({ agentResponse }: AgentGridProps) {
export function AgentGrid({ agentResponse, onAgentsChanged }: AgentGridProps) {

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

return <AgentCard key={agentRef} agentResponse={item} />
return <AgentCard key={agentRef} agentResponse={item} onAgentsChanged={onAgentsChanged} />
})}
</div>
);
}
}
183 changes: 134 additions & 49 deletions ui/src/components/AgentList.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
"use client";
import { useCallback, useEffect, useState } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import { AgentGrid } from "@/components/AgentGrid";
import { AgentListView } from "@/components/AgentListView";
import { Plus, LayoutGrid, List } from "lucide-react";
import KagentLogo from "@/components/kagent-logo";
import Link from "next/link";
import { useRouter, useSearchParams } from "next/navigation";
import { ErrorState } from "./ErrorState";
import { Button } from "./ui/button";
import { LoadingState } from "./LoadingState";
import { useAgents } from "./AgentsProvider";
import { getAgents } from "@/app/actions/agents";
import { AppPageFrame } from "@/components/layout/AppPageFrame";
import { PageHeader } from "@/components/layout/PageHeader";
import { cn } from "@/lib/utils";
import { NamespaceCombobox } from "@/components/NamespaceCombobox";
import type { AgentResponse } from "@/types";

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

export default function AgentList() {
const { agents , loading, error } = useAgents();
const router = useRouter();
const searchParams = useSearchParams();
const selectedNamespace = searchParams.get("namespace")?.trim() || "";
const [agents, setAgents] = useState<AgentResponse[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
const [view, setView] = useState<AgentsView>("grid");
const latestFetchRequestId = useRef(0);

const fetchAgents = useCallback(async () => {
const requestId = latestFetchRequestId.current + 1;
latestFetchRequestId.current = requestId;

try {
setLoading(true);
const result = await getAgents(selectedNamespace ? { namespace: selectedNamespace } : {});
if (requestId !== latestFetchRequestId.current) {
return;
}
if (result.error) {
throw new Error(result.error || "Failed to fetch agents");
}
setAgents(result.data || []);
setError("");
} catch (err) {
if (requestId !== latestFetchRequestId.current) {
return;
}
setError(err instanceof Error ? err.message : "An unexpected error occurred");
} finally {
if (requestId === latestFetchRequestId.current) {
setLoading(false);
}
}
}, [selectedNamespace]);
Comment thread
maazghani marked this conversation as resolved.

useEffect(() => {
const id = requestAnimationFrame(() => {
Expand All @@ -35,6 +71,10 @@ export default function AgentList() {
return () => cancelAnimationFrame(id);
}, []);

useEffect(() => {
void fetchAgents();
}, [fetchAgents]);

const setViewAndPersist = useCallback((next: AgentsView) => {
setView(next);
try {
Expand All @@ -44,6 +84,22 @@ export default function AgentList() {
}
}, []);

const handleNamespaceChange = useCallback(
(nextNamespace: string) => {
const namespace = nextNamespace.trim();
if (!namespace) {
router.push("/agents");
return;
}
router.push(`/agents?namespace=${encodeURIComponent(namespace)}`);
},
[router],
);

const createHref = selectedNamespace
? `/agents/new?namespace=${encodeURIComponent(selectedNamespace)}`
: "/agents/new";

if (error) {
return <ErrorState message={error} />;
}
Expand All @@ -57,69 +113,98 @@ export default function AgentList() {
<PageHeader
titleId="agents-page-title"
title="Agents"
description={
selectedNamespace ? (
<>
Showing agents in namespace <code>{selectedNamespace}</code>.
</>
) : (
"Showing agents across all namespaces."
)
}
className="mb-8"
end={
agents && agents.length > 0 ? (
<div
className="flex w-full min-w-0 items-center justify-end gap-1 rounded-lg border border-border/60 bg-muted/20 p-1"
role="group"
aria-label="Layout"
>
<Button
type="button"
variant="ghost"
size="sm"
className={cn(
"h-8 gap-1.5 px-2.5 text-muted-foreground",
view === "grid" && "bg-card text-foreground shadow-sm",
)}
aria-pressed={view === "grid"}
aria-label="Show agents as cards"
onClick={() => setViewAndPersist("grid")}
>
<LayoutGrid className="h-4 w-4 shrink-0" aria-hidden />
<span className="hidden sm:inline" aria-hidden>
Cards
</span>
</Button>
<Button
type="button"
variant="ghost"
size="sm"
className={cn(
"h-8 gap-1.5 px-2.5 text-muted-foreground",
view === "list" && "bg-card text-foreground shadow-sm",
)}
aria-pressed={view === "list"}
aria-label="Show agents as a list"
onClick={() => setViewAndPersist("list")}
>
<List className="h-4 w-4 shrink-0" aria-hidden />
<span className="hidden sm:inline" aria-hidden>
List
</span>
</Button>
<div className="flex w-full min-w-0 flex-col gap-2 sm:w-auto sm:flex-row sm:items-center sm:justify-end">
<div className="w-full sm:w-72">
<NamespaceCombobox
value={selectedNamespace}
onValueChange={handleNamespaceChange}
includeAllNamespaces
autoSelectDefault={false}
ariaLabel="Namespace"
placeholder="All namespaces"
/>
</div>
) : null
{agents && agents.length > 0 ? (
<div
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"
role="group"
aria-label="Layout"
>
<Button
type="button"
variant="ghost"
size="sm"
className={cn(
"h-8 gap-1.5 px-2.5 text-muted-foreground",
view === "grid" && "bg-card text-foreground shadow-sm",
)}
aria-pressed={view === "grid"}
aria-label="Show agents as cards"
onClick={() => setViewAndPersist("grid")}
>
<LayoutGrid className="h-4 w-4 shrink-0" aria-hidden />
<span className="hidden sm:inline" aria-hidden>
Cards
</span>
</Button>
<Button
type="button"
variant="ghost"
size="sm"
className={cn(
"h-8 gap-1.5 px-2.5 text-muted-foreground",
view === "list" && "bg-card text-foreground shadow-sm",
)}
aria-pressed={view === "list"}
aria-label="Show agents as a list"
onClick={() => setViewAndPersist("list")}
>
<List className="h-4 w-4 shrink-0" aria-hidden />
<span className="hidden sm:inline" aria-hidden>
List
</span>
</Button>
</div>
) : null}
</div>
}
/>

{agents?.length === 0 ? (
<div className="rounded-xl border border-border/60 bg-card/30 py-12 text-center shadow-sm">
<KagentLogo className="mx-auto mb-4 h-16 w-16" />
<h2 className="mb-2 text-lg font-medium tracking-tight">No agents yet</h2>
<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>
<h2 className="mb-2 text-lg font-medium tracking-tight">
{selectedNamespace
? `No agents found in namespace "${selectedNamespace}".`
: "No agents yet"}
</h2>
<p className="mb-6 text-pretty text-sm text-muted-foreground">
{selectedNamespace
? "Create an agent in this namespace or switch namespaces."
: "Create an agent to run it in your cluster and wire models and tools in one place."}
</p>
<Button asChild size="lg" className="min-w-[12rem]">
<Link href="/agents/new">
<Link href={createHref}>
<Plus className="mr-2 h-4 w-4" aria-hidden />
New Agent
</Link>
</Button>
</div>
) : view === "list" ? (
<AgentListView agentResponse={agents || []} />
<AgentListView agentResponse={agents || []} onAgentsChanged={fetchAgents} />
) : (
<AgentGrid agentResponse={agents || []} />
<AgentGrid agentResponse={agents || []} onAgentsChanged={fetchAgents} />
)}
</AppPageFrame>
);
Expand Down
10 changes: 6 additions & 4 deletions ui/src/components/AgentListView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { isOpenshellSandboxRow, openshellTerminalHref } from "@/lib/openshellSan

interface AgentListViewProps {
agentResponse: AgentResponse[];
onAgentsChanged?: () => Promise<void> | void;
}

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

function AgentListRow({ item }: { item: AgentResponse }) {
function AgentListRow({ item, onAgentsChanged }: { item: AgentResponse; onAgentsChanged?: () => Promise<void> | void }) {
const { agent, deploymentReady, accepted } = item;
const router = useRouter();
const [memoriesOpen, setMemoriesOpen] = useState(false);
Expand Down Expand Up @@ -389,6 +390,7 @@ function AgentListRow({ item }: { item: AgentResponse }) {
<DeleteButton
agentName={name}
namespace={namespace}
onDeleted={onAgentsChanged}
externalOpen={deleteOpen}
onExternalOpenChange={setDeleteOpen}
/>
Expand All @@ -406,7 +408,7 @@ function AgentListRow({ item }: { item: AgentResponse }) {
return trBody;
}

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

const onSort = useCallback((col: SortKey) => {
Expand Down Expand Up @@ -453,10 +455,10 @@ export function AgentListView({ agentResponse }: AgentListViewProps) {
item.agent.metadata.namespace || "",
item.agent.metadata.name || "",
);
return <AgentListRow key={key} item={item} />;
return <AgentListRow key={key} item={item} onAgentsChanged={onAgentsChanged} />;
})}
</tbody>
</table>
</div>
);
}
}
Loading
Loading