From 212735d93a4bde889a5781d0884ae13fc883feeb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 7 May 2026 19:02:04 +0000 Subject: [PATCH 1/6] chore: plan agent namespace filter Agent-Logs-Url: https://github.com/maazghani/kagent/sessions/c373920c-0f64-4998-afa2-0a928633e43e Co-authored-by: maazghani <5009288+maazghani@users.noreply.github.com> --- ui/public/mockServiceWorker.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/public/mockServiceWorker.js b/ui/public/mockServiceWorker.js index 2c0248801..a1e52b477 100644 --- a/ui/public/mockServiceWorker.js +++ b/ui/public/mockServiceWorker.js @@ -7,7 +7,7 @@ * - Please do NOT modify this file. */ -const PACKAGE_VERSION = '2.13.4' +const PACKAGE_VERSION = '2.14.2' const INTEGRITY_CHECKSUM = '4db4a41e972cec1b64cc569c66952d82' const IS_MOCKED_RESPONSE = Symbol('isMockedResponse') const activeClientIds = new Set() From 40e73842f9ab1ec1902f196e6e1ea60434bce992 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 7 May 2026 19:05:58 +0000 Subject: [PATCH 2/6] Add namespace filter to the agents list UI Agent-Logs-Url: https://github.com/maazghani/kagent/sessions/c373920c-0f64-4998-afa2-0a928633e43e Co-authored-by: maazghani <5009288+maazghani@users.noreply.github.com> --- ui/public/mockServiceWorker.js | 2 +- ui/src/components/AgentList.tsx | 152 +++++++++++++----- .../components/__tests__/AgentList.test.tsx | 108 +++++++++++++ 3 files changed, 219 insertions(+), 43 deletions(-) create mode 100644 ui/src/components/__tests__/AgentList.test.tsx diff --git a/ui/public/mockServiceWorker.js b/ui/public/mockServiceWorker.js index a1e52b477..2c0248801 100644 --- a/ui/public/mockServiceWorker.js +++ b/ui/public/mockServiceWorker.js @@ -7,7 +7,7 @@ * - Please do NOT modify this file. */ -const PACKAGE_VERSION = '2.14.2' +const PACKAGE_VERSION = '2.13.4' const INTEGRITY_CHECKSUM = '4db4a41e972cec1b64cc569c66952d82' const IS_MOCKED_RESPONSE = Symbol('isMockedResponse') const activeClientIds = new Set() diff --git a/ui/src/components/AgentList.tsx b/ui/src/components/AgentList.tsx index 45d13786f..61711025e 100644 --- a/ui/src/components/AgentList.tsx +++ b/ui/src/components/AgentList.tsx @@ -1,10 +1,11 @@ "use client"; -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useMemo, 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 { usePathname, useRouter, useSearchParams } from "next/navigation"; import { ErrorState } from "./ErrorState"; import { Button } from "./ui/button"; import { LoadingState } from "./LoadingState"; @@ -12,8 +13,10 @@ import { useAgents } from "./AgentsProvider"; import { AppPageFrame } from "@/components/layout/AppPageFrame"; import { PageHeader } from "@/components/layout/PageHeader"; import { cn } from "@/lib/utils"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"; const AGENTS_VIEW_KEY = "kagent-agents-view"; +const ALL_NAMESPACES = "__all__"; type AgentsView = "grid" | "list"; function readStoredView(): AgentsView { @@ -25,8 +28,12 @@ function readStoredView(): AgentsView { } export default function AgentList() { - const { agents , loading, error } = useAgents(); + const { agents, loading, error } = useAgents(); + const router = useRouter(); + const pathname = usePathname(); + const searchParams = useSearchParams(); const [view, setView] = useState("grid"); + const namespaceFilter = searchParams.get("namespace") || ALL_NAMESPACES; useEffect(() => { const id = requestAnimationFrame(() => { @@ -44,6 +51,36 @@ export default function AgentList() { } }, []); + const onNamespaceChange = useCallback( + (namespace: string) => { + const q = new URLSearchParams(searchParams.toString()); + if (namespace === ALL_NAMESPACES) { + q.delete("namespace"); + } else { + q.set("namespace", namespace); + } + + const query = q.toString(); + router.replace(query ? `${pathname}?${query}` : pathname, { scroll: false }); + }, + [pathname, router, searchParams], + ); + + const namespaces = useMemo(() => { + const uniqueNamespaces = new Set( + (agents || []).map((item) => item.agent.metadata.namespace || "").filter(Boolean), + ); + return Array.from(uniqueNamespaces).sort((a, b) => a.localeCompare(b, undefined, { sensitivity: "base" })); + }, [agents]); + + const filteredAgents = useMemo(() => { + if (!agents || namespaceFilter === ALL_NAMESPACES) { + return agents || []; + } + + return agents.filter((item) => (item.agent.metadata.namespace || "") === namespaceFilter); + }, [agents, namespaceFilter]); + if (error) { return ; } @@ -60,45 +97,65 @@ export default function AgentList() { className="mb-8" end={ agents && agents.length > 0 ? ( -
- - + + +
) : null } @@ -116,10 +173,21 @@ export default function AgentList() { + ) : filteredAgents.length === 0 ? ( +
+ +

No agents in this namespace

+

+ No agents match the {namespaceFilter} namespace filter. +

+ +
) : view === "list" ? ( - + ) : ( - + )} ); diff --git a/ui/src/components/__tests__/AgentList.test.tsx b/ui/src/components/__tests__/AgentList.test.tsx new file mode 100644 index 000000000..0d205edb5 --- /dev/null +++ b/ui/src/components/__tests__/AgentList.test.tsx @@ -0,0 +1,108 @@ +"use client"; + +import { render, screen } from "@testing-library/react"; +import AgentList from "@/components/AgentList"; +import { AgentsContext, type AgentsContextType } from "@/components/AgentsProvider"; +import type { Agent, AgentResponse } from "@/types"; + +const mockUseRouter = jest.fn(); +const mockUsePathname = jest.fn(); +const mockUseSearchParams = jest.fn(); + +jest.mock("next/navigation", () => ({ + useRouter: () => mockUseRouter(), + usePathname: () => mockUsePathname(), + useSearchParams: () => mockUseSearchParams(), +})); + +function createContextValue(agents: AgentResponse[]): AgentsContextType { + return { + agents, + models: [], + loading: false, + error: "", + tools: [], + refreshAgents: async () => {}, + refreshModels: async () => {}, + refreshTools: async () => {}, + createNewAgent: async () => ({ message: "ok", data: {} as Agent }), + updateAgent: async () => ({ message: "ok", data: {} as Agent }), + getAgent: async () => null, + validateAgentData: () => ({}), + }; +} + +const agents: AgentResponse[] = [ + { + id: 1, + agent: { + metadata: { name: "support-bot", namespace: "kagent" }, + spec: { + type: "Declarative", + description: "Answers support questions", + }, + }, + model: "gpt-4o", + modelProvider: "openai", + deploymentReady: true, + accepted: true, + }, + { + id: 2, + agent: { + metadata: { name: "team-analyzer", namespace: "team-a" }, + spec: { + type: "Declarative", + description: "Analyzes incidents", + }, + }, + model: "claude-sonnet", + modelProvider: "anthropic", + deploymentReady: true, + accepted: true, + }, +]; + +describe("AgentList", () => { + beforeEach(() => { + mockUseRouter.mockReturnValue({ + push: jest.fn(), + replace: jest.fn(), + refresh: jest.fn(), + back: jest.fn(), + }); + mockUsePathname.mockReturnValue("/agents"); + mockUseSearchParams.mockReturnValue(new URLSearchParams()); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it("filters agents by namespace from the URL", () => { + mockUseSearchParams.mockReturnValue(new URLSearchParams("namespace=team-a")); + + render( + + + , + ); + + expect(screen.getByLabelText("Namespace filter")).toBeInTheDocument(); + expect(screen.getByText("team-a/team-analyzer")).toBeInTheDocument(); + expect(screen.queryByText("kagent/support-bot")).not.toBeInTheDocument(); + }); + + it("shows a filtered empty state when no agents match the namespace", () => { + mockUseSearchParams.mockReturnValue(new URLSearchParams("namespace=team-b")); + + render( + + + , + ); + + expect(screen.getByText("No agents in this namespace")).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "Show all namespaces" })).toBeInTheDocument(); + }); +}); From 8e817496d4a031d26bea12efc4deb6161a08c5e7 Mon Sep 17 00:00:00 2001 From: Maaz Ghani Date: Thu, 7 May 2026 12:26:50 -0700 Subject: [PATCH 3/6] chefk if namespaces is all namespaces Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Signed-off-by: Maaz Ghani --- ui/src/components/AgentList.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/ui/src/components/AgentList.tsx b/ui/src/components/AgentList.tsx index 61711025e..0853e1482 100644 --- a/ui/src/components/AgentList.tsx +++ b/ui/src/components/AgentList.tsx @@ -70,8 +70,13 @@ export default function AgentList() { const uniqueNamespaces = new Set( (agents || []).map((item) => item.agent.metadata.namespace || "").filter(Boolean), ); + + if (namespaceFilter !== ALL_NAMESPACES) { + uniqueNamespaces.add(namespaceFilter); + } + return Array.from(uniqueNamespaces).sort((a, b) => a.localeCompare(b, undefined, { sensitivity: "base" })); - }, [agents]); + }, [agents, namespaceFilter]); const filteredAgents = useMemo(() => { if (!agents || namespaceFilter === ALL_NAMESPACES) { From e450366a9cc7ade46cb83d8dc0731bad3b116af6 Mon Sep 17 00:00:00 2001 From: Maaz Ghani Date: Thu, 7 May 2026 12:27:37 -0700 Subject: [PATCH 4/6] remove unnecessary ref Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Signed-off-by: Maaz Ghani --- ui/src/components/__tests__/AgentList.test.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/ui/src/components/__tests__/AgentList.test.tsx b/ui/src/components/__tests__/AgentList.test.tsx index 0d205edb5..0312ac217 100644 --- a/ui/src/components/__tests__/AgentList.test.tsx +++ b/ui/src/components/__tests__/AgentList.test.tsx @@ -1,5 +1,3 @@ -"use client"; - import { render, screen } from "@testing-library/react"; import AgentList from "@/components/AgentList"; import { AgentsContext, type AgentsContextType } from "@/components/AgentsProvider"; From e109ee0f250064a32b1b75e50566710a3f2e7f64 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 7 May 2026 19:30:26 +0000 Subject: [PATCH 5/6] test: reuse shared next navigation mock Agent-Logs-Url: https://github.com/maazghani/kagent/sessions/cb9140cb-1ccd-44b2-93a5-cfcbcc5ba043 Co-authored-by: maazghani <5009288+maazghani@users.noreply.github.com> --- ui/jest.setup.ts | 30 +++++++++---------- .../components/__tests__/AgentList.test.tsx | 15 ++++------ 2 files changed, 20 insertions(+), 25 deletions(-) diff --git a/ui/jest.setup.ts b/ui/jest.setup.ts index be96e4bc9..e26fda3ba 100644 --- a/ui/jest.setup.ts +++ b/ui/jest.setup.ts @@ -45,20 +45,20 @@ jest.mock('next/router', () => ({ }, })); +const mockNextUseRouter = jest.fn(() => ({ + push: jest.fn(), + replace: jest.fn(), + refresh: jest.fn(), + back: jest.fn(), +})); + +const mockNextUsePathname = jest.fn(() => ''); + +const mockNextUseSearchParams = jest.fn(() => new URLSearchParams()); + // Mock next/navigation jest.mock('next/navigation', () => ({ - useRouter() { - return { - push: jest.fn(), - replace: jest.fn(), - refresh: jest.fn(), - back: jest.fn(), - }; - }, - usePathname() { - return ''; - }, - useSearchParams() { - return new URLSearchParams(); - }, -})); \ No newline at end of file + useRouter: mockNextUseRouter, + usePathname: mockNextUsePathname, + useSearchParams: mockNextUseSearchParams, +})); diff --git a/ui/src/components/__tests__/AgentList.test.tsx b/ui/src/components/__tests__/AgentList.test.tsx index 0312ac217..174ad1026 100644 --- a/ui/src/components/__tests__/AgentList.test.tsx +++ b/ui/src/components/__tests__/AgentList.test.tsx @@ -1,18 +1,9 @@ import { render, screen } from "@testing-library/react"; +import * as nextNavigation from "next/navigation"; import AgentList from "@/components/AgentList"; import { AgentsContext, type AgentsContextType } from "@/components/AgentsProvider"; import type { Agent, AgentResponse } from "@/types"; -const mockUseRouter = jest.fn(); -const mockUsePathname = jest.fn(); -const mockUseSearchParams = jest.fn(); - -jest.mock("next/navigation", () => ({ - useRouter: () => mockUseRouter(), - usePathname: () => mockUsePathname(), - useSearchParams: () => mockUseSearchParams(), -})); - function createContextValue(agents: AgentResponse[]): AgentsContextType { return { agents, @@ -62,6 +53,10 @@ const agents: AgentResponse[] = [ ]; describe("AgentList", () => { + const mockUseRouter = jest.mocked(nextNavigation.useRouter); + const mockUsePathname = jest.mocked(nextNavigation.usePathname); + const mockUseSearchParams = jest.mocked(nextNavigation.useSearchParams); + beforeEach(() => { mockUseRouter.mockReturnValue({ push: jest.fn(), From 8d35a1e0af616a9cee38df5d8724c7dbe09dd348 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 7 May 2026 19:34:31 +0000 Subject: [PATCH 6/6] fix: normalize agent namespace filtering Agent-Logs-Url: https://github.com/maazghani/kagent/sessions/c5137321-6f88-4e20-b1ce-b8e5138c15a9 Co-authored-by: maazghani <5009288+maazghani@users.noreply.github.com> --- ui/src/components/AgentList.tsx | 11 +++++++---- ui/src/components/__tests__/AgentList.test.tsx | 17 +++++++++++++++-- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/ui/src/components/AgentList.tsx b/ui/src/components/AgentList.tsx index 0853e1482..8492cc89a 100644 --- a/ui/src/components/AgentList.tsx +++ b/ui/src/components/AgentList.tsx @@ -17,6 +17,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from ". const AGENTS_VIEW_KEY = "kagent-agents-view"; const ALL_NAMESPACES = "__all__"; +const DEFAULT_NAMESPACE = "default"; type AgentsView = "grid" | "list"; function readStoredView(): AgentsView { @@ -35,6 +36,8 @@ export default function AgentList() { const [view, setView] = useState("grid"); const namespaceFilter = searchParams.get("namespace") || ALL_NAMESPACES; + const normalizeNamespace = useCallback((namespace?: string) => namespace || DEFAULT_NAMESPACE, []); + useEffect(() => { const id = requestAnimationFrame(() => { setView(readStoredView()); @@ -68,7 +71,7 @@ export default function AgentList() { const namespaces = useMemo(() => { const uniqueNamespaces = new Set( - (agents || []).map((item) => item.agent.metadata.namespace || "").filter(Boolean), + (agents || []).map((item) => normalizeNamespace(item.agent.metadata.namespace)), ); if (namespaceFilter !== ALL_NAMESPACES) { @@ -76,15 +79,15 @@ export default function AgentList() { } return Array.from(uniqueNamespaces).sort((a, b) => a.localeCompare(b, undefined, { sensitivity: "base" })); - }, [agents, namespaceFilter]); + }, [agents, namespaceFilter, normalizeNamespace]); const filteredAgents = useMemo(() => { if (!agents || namespaceFilter === ALL_NAMESPACES) { return agents || []; } - return agents.filter((item) => (item.agent.metadata.namespace || "") === namespaceFilter); - }, [agents, namespaceFilter]); + return agents.filter((item) => normalizeNamespace(item.agent.metadata.namespace) === namespaceFilter); + }, [agents, namespaceFilter, normalizeNamespace]); if (error) { return ; diff --git a/ui/src/components/__tests__/AgentList.test.tsx b/ui/src/components/__tests__/AgentList.test.tsx index 174ad1026..713a82e83 100644 --- a/ui/src/components/__tests__/AgentList.test.tsx +++ b/ui/src/components/__tests__/AgentList.test.tsx @@ -25,7 +25,7 @@ const agents: AgentResponse[] = [ { id: 1, agent: { - metadata: { name: "support-bot", namespace: "kagent" }, + metadata: { name: "support-bot" }, spec: { type: "Declarative", description: "Answers support questions", @@ -83,7 +83,20 @@ describe("AgentList", () => { expect(screen.getByLabelText("Namespace filter")).toBeInTheDocument(); expect(screen.getByText("team-a/team-analyzer")).toBeInTheDocument(); - expect(screen.queryByText("kagent/support-bot")).not.toBeInTheDocument(); + expect(screen.queryByText("default/support-bot")).not.toBeInTheDocument(); + }); + + it("treats missing namespaces as default when filtering", () => { + mockUseSearchParams.mockReturnValue(new URLSearchParams("namespace=default")); + + render( + + + , + ); + + expect(screen.getByText("default/support-bot")).toBeInTheDocument(); + expect(screen.queryByText("team-a/team-analyzer")).not.toBeInTheDocument(); }); it("shows a filtered empty state when no agents match the namespace", () => {