Fix built-in agents route

This commit is contained in:
Dotta committed 2026-07-09 18:50:07 +00:00
1 parent 41ebe3579c
commit 622e0cf799
3 files changed
+134 -23

No files matched your search

+7 -5
View File
@@ -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() {
<Route path="plugins/:pluginId" element={<PluginPage />} />
<Route path="org" element={<OrgChart />} />
<Route path="agents" element={<Navigate to="/agents/all" replace />} />
<Route path="agents/all" element={<Agents />} />
<Route path="agents/active" element={<Agents />} />
<Route path="agents/paused" element={<Agents />} />
<Route path="agents/error" element={<Agents />} />
{AGENT_FILTER_TABS.map((tab) => (
<Route key={tab} path={`agents/${tab}`} element={<Agents />} />
))}
<Route path="agents/new" element={<NewAgent />} />
<Route path="agents/:agentId" element={<AgentDetail />} />
<Route path="agents/:agentId/:tab" element={<AgentDetail />} />
@@ -461,6 +460,9 @@ export function App() {
<Route path="settings" element={<LegacySettingsRedirect />} />
<Route path="settings/*" element={<LegacySettingsRedirect />} />
<Route path="agents" element={<UnprefixedBoardRedirect />} />
{AGENT_FILTER_TABS.map((tab) => (
<Route key={tab} path={`agents/${tab}`} element={<UnprefixedBoardRedirect />} />
))}
<Route path="agents/new" element={<UnprefixedBoardRedirect />} />
<Route path="agents/:agentId" element={<UnprefixedBoardRedirect />} />
<Route path="agents/:agentId/:tab" element={<UnprefixedBoardRedirect />} />
+90 -2
View File
@@ -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 }) => (
<a href={to} {...props}>{children}</a>
),
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>): Agent {
};
}
function makeBuiltInAgentState(overrides: Partial<BuiltInAgentState> = {}): 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>): 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(
<QueryClientProvider client={queryClient}>
<ToastProvider>
<Agents />
</ToastProvider>
</QueryClientProvider>,
);
});
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({
+37 -16
View File
@@ -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<string, BuiltInAgentState>();
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<BuiltInAgentState | null>(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 <EmptyState icon={Bot} message="Select a company to view agents." />;
}
@@ -469,13 +496,7 @@ export function Agents() {
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<Tabs value={tab} onValueChange={(v) => navigate(`/agents/${v}`)}>
<PageTabBar
items={[
{ value: "all", label: "All" },
{ value: "active", label: "Active" },
{ value: "paused", label: "Paused" },
{ value: "error", label: "Error" },
{ value: "builtin", label: "Built-in" },
]}
items={visibleTabItems}
value={tab}
onValueChange={(v) => navigate(`/agents/${v}`)}
/>