From 622e0cf799a6406dce848617f1df903a5c3fd892 Mon Sep 17 00:00:00 2001 From: Dotta Date: Wed, 8 Jul 2026 12:48:53 +0000 Subject: [PATCH] Fix built-in agents route --- ui/src/App.tsx | 12 +++-- ui/src/pages/Agents.test.tsx | 92 +++++++++++++++++++++++++++++++++++- ui/src/pages/Agents.tsx | 53 ++++++++++++++------- 3 files changed, 134 insertions(+), 23 deletions(-) diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 54797e99454..b09498d7e9b 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -10,7 +10,7 @@ import { Dashboard } from "./pages/Dashboard"; import { DashboardLive } from "./pages/DashboardLive"; import { Timeline } from "./pages/Timeline"; import { Companies } from "./pages/Companies"; -import { Agents } from "./pages/Agents"; +import { AGENT_FILTER_TABS, Agents } from "./pages/Agents"; import { AgentDetail } from "./pages/AgentDetail"; import { Projects } from "./pages/Projects"; import { ProjectDetail } from "./pages/ProjectDetail"; @@ -116,10 +116,9 @@ function boardRoutes() { } /> } /> } /> - } /> - } /> - } /> - } /> + {AGENT_FILTER_TABS.map((tab) => ( + } /> + ))} } /> } /> } /> @@ -461,6 +460,9 @@ export function App() { } /> } /> } /> + {AGENT_FILTER_TABS.map((tab) => ( + } /> + ))} } /> } /> } /> diff --git a/ui/src/pages/Agents.test.tsx b/ui/src/pages/Agents.test.tsx index b73c5d8e2de..447fdb094a2 100644 --- a/ui/src/pages/Agents.test.tsx +++ b/ui/src/pages/Agents.test.tsx @@ -7,14 +7,26 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import type { Agent, Environment, EnvironmentCapabilities } from "@paperclipai/shared"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { ToastProvider } from "../context/ToastContext"; +import type { BuiltInAgentState } from "../api/builtInAgents"; import { Agents } from "./Agents"; import type { AgentOrgChainHealth } from "@paperclipai/shared"; +const mockRouterState = vi.hoisted(() => ({ + pathname: "/agents/all", + navigate: vi.fn(), +})); + const mockAgentsApi = vi.hoisted(() => ({ list: vi.fn(), org: vi.fn(), })); +const mockBuiltInAgentsApi = vi.hoisted(() => ({ + list: vi.fn(), + provision: vi.fn(), + reset: vi.fn(), +})); + const mockEnvironmentsApi = vi.hoisted(() => ({ list: vi.fn(), capabilities: vi.fn(), @@ -40,8 +52,8 @@ vi.mock("@/lib/router", () => ({ Link: ({ children, to, ...props }: { children: ReactNode; to: string }) => ( {children} ), - useLocation: () => ({ pathname: "/agents/all", search: "", hash: "", state: null }), - useNavigate: () => vi.fn(), + useLocation: () => ({ pathname: mockRouterState.pathname, search: "", hash: "", state: null }), + useNavigate: () => mockRouterState.navigate, })); vi.mock("../context/CompanyContext", () => ({ @@ -64,6 +76,10 @@ vi.mock("../api/agents", () => ({ agentsApi: mockAgentsApi, })); +vi.mock("../api/builtInAgents", () => ({ + builtInAgentsApi: mockBuiltInAgentsApi, +})); + vi.mock("../api/environments", () => ({ environmentsApi: mockEnvironmentsApi, })); @@ -123,6 +139,24 @@ function makeAgent(overrides: Partial): Agent { }; } +function makeBuiltInAgentState(overrides: Partial = {}): BuiltInAgentState { + return { + definition: { + key: "briefs", + displayName: "Briefs Agent", + featureKeys: ["Briefs"], + shortPurpose: "Generates briefs.", + defaultInstructions: "You are Paperclip's built-in Briefs agent.", + defaultRole: "engineer", + }, + status: "ready", + agentId: "built-in-agent", + agent: null, + pauseReason: null, + ...overrides, + }; +} + function makeEnvironment(overrides: Partial): Environment { return { id: "env-1", @@ -262,6 +296,8 @@ describe("Agents", () => { let queryClient: QueryClient; beforeEach(() => { + mockRouterState.pathname = "/agents/all"; + mockRouterState.navigate.mockClear(); container = document.createElement("div"); document.body.appendChild(container); root = null; @@ -285,6 +321,7 @@ describe("Agents", () => { reports: [], }, ]); + mockBuiltInAgentsApi.list.mockResolvedValue([]); mockEnvironmentsApi.list.mockResolvedValue([ makeEnvironment({ id: "env-daytona" }), ]); @@ -341,6 +378,57 @@ describe("Agents", () => { expect(heartbeatCell?.textContent).not.toContain("\n"); }); + it("uses the built-in agents route segment as the built-in filter", async () => { + mockRouterState.pathname = "/agents/builtin"; + const builtInAgent = makeAgent({ + id: "built-in-agent", + name: "Briefs Agent", + urlKey: "briefs-agent", + }); + const regularAgent = makeAgent({ + id: "regular-agent", + name: "Regular Agent", + urlKey: "regular-agent", + }); + mockAgentsApi.list.mockResolvedValue([builtInAgent, regularAgent]); + mockAgentsApi.org.mockResolvedValue([ + { + id: "built-in-agent", + name: "Briefs Agent", + role: "engineer", + status: "active", + reports: [], + }, + { + id: "regular-agent", + name: "Regular Agent", + role: "engineer", + status: "active", + reports: [], + }, + ]); + mockBuiltInAgentsApi.list.mockResolvedValue([ + makeBuiltInAgentState({ agentId: "built-in-agent", agent: builtInAgent }), + ]); + + root = createRoot(container); + await act(async () => { + root!.render( + + + + + , + ); + }); + await flushReact(); + await flushReact(); + + expect(container.textContent).toContain("1 agent"); + expect(container.textContent).toContain("Briefs Agent"); + expect(container.textContent).not.toContain("Regular Agent"); + }); + it("shows effective environment and sandbox provider beside agents", async () => { mockAgentsApi.list.mockResolvedValue([ makeAgent({ diff --git a/ui/src/pages/Agents.tsx b/ui/src/pages/Agents.tsx index 07783deacf8..3e23f31cb56 100644 --- a/ui/src/pages/Agents.tsx +++ b/ui/src/pages/Agents.tsx @@ -44,7 +44,20 @@ const ConfigureBuiltInAgentModal = lazy(() => })), ); -type FilterTab = "all" | "active" | "paused" | "error" | "builtin"; +export const AGENT_FILTER_TABS = ["all", "active", "paused", "error", "builtin"] as const; +type FilterTab = (typeof AGENT_FILTER_TABS)[number]; + +const AGENT_FILTER_TAB_ITEMS: { value: FilterTab; label: string }[] = [ + { value: "all", label: "All" }, + { value: "active", label: "Active" }, + { value: "paused", label: "Paused" }, + { value: "error", label: "Error" }, + { value: "builtin", label: "Built-in" }, +]; + +function isFilterTab(value: string): value is FilterTab { + return (AGENT_FILTER_TABS as readonly string[]).includes(value); +} interface EnvironmentDescriptor { label: string; @@ -179,23 +192,36 @@ export function Agents() { const location = useLocation(); const { isMobile } = useSidebar(); const pathSegment = location.pathname.split("/").pop() ?? "all"; - const tab: FilterTab = (pathSegment === "all" || pathSegment === "active" || pathSegment === "paused" || pathSegment === "error" || pathSegment === "builtin") ? pathSegment : "all"; + const requestedTab: FilterTab = isFilterTab(pathSegment) ? pathSegment : "all"; const [view, setView] = useState<"list" | "org">("org"); const forceListView = isMobile; const effectiveView: "list" | "org" = forceListView ? "list" : view; + const { data: instanceSettings } = useQuery({ + queryKey: queryKeys.instance.settings, + queryFn: () => instanceSettingsApi.get(), + enabled: !!selectedCompanyId, + }); + const builtInAgentsEnabled = instanceSettings?.experimental.enableBuiltInAgents === true; + const tab: FilterTab = requestedTab === "builtin" && !builtInAgentsEnabled ? "all" : requestedTab; + const visibleTabItems = useMemo( + () => AGENT_FILTER_TAB_ITEMS.filter((item) => item.value !== "builtin" || builtInAgentsEnabled), + [builtInAgentsEnabled], + ); + const { data: builtInAgents } = useQuery({ queryKey: queryKeys.builtInAgents.list(selectedCompanyId!), queryFn: () => builtInAgentsApi.list(selectedCompanyId!), - enabled: !!selectedCompanyId, + enabled: !!selectedCompanyId && builtInAgentsEnabled, }); const builtInByAgentId = useMemo(() => { const map = new Map(); + if (!builtInAgentsEnabled) return map; for (const entry of builtInAgents ?? []) { if (entry.agentId) map.set(entry.agentId, entry); } return map; - }, [builtInAgents]); + }, [builtInAgents, builtInAgentsEnabled]); const builtInAgentIds = useMemo(() => new Set(builtInByAgentId.keys()), [builtInByAgentId]); const [configureState, setConfigureState] = useState(null); @@ -211,11 +237,6 @@ export function Agents() { enabled: !!selectedCompanyId && effectiveView === "org", }); - const { data: instanceSettings } = useQuery({ - queryKey: queryKeys.instance.settings, - queryFn: () => instanceSettingsApi.get(), - enabled: !!selectedCompanyId, - }); const environmentsEnabled = instanceSettings?.experimental.enableEnvironments === true; const { data: environments } = useQuery({ @@ -286,6 +307,12 @@ export function Agents() { setBreadcrumbs([{ label: "Agents" }]); }, [setBreadcrumbs]); + useEffect(() => { + if (selectedCompanyId && requestedTab === "builtin" && instanceSettings && !builtInAgentsEnabled) { + navigate("/agents/all", { replace: true }); + } + }, [builtInAgentsEnabled, instanceSettings, navigate, requestedTab, selectedCompanyId]); + if (!selectedCompanyId) { return ; } @@ -469,13 +496,7 @@ export function Agents() {
navigate(`/agents/${v}`)}> navigate(`/agents/${v}`)} />