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}`)}
/>