mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-27 19:35:13 +01:00
Fix built-in agents route
This commit is contained in:
1 parent
41ebe3579c
commit
622e0cf799
3 files changed
+134
-23
No files matched your search
+7
-5
@@ -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 />} />
|
||||
|
||||
@@ -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
@@ -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}`)}
|
||||
/>
|
||||
|
||||
Reference in new issue
Block a user