Show agent models on agents page

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-04-28 16:32:41 -05:00
1 parent 38b57c5d56
commit 47d862b4b2
2 files changed
+174 -2

No files matched your search

+153
View File
@@ -0,0 +1,153 @@
// @vitest-environment jsdom
import { act } from "react";
import type { ReactNode } from "react";
import { createRoot } from "react-dom/client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import type { Agent } from "@paperclipai/shared";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { Agents } from "./Agents";
const mockAgentsApi = vi.hoisted(() => ({
list: vi.fn(),
org: vi.fn(),
}));
const mockHeartbeatsApi = vi.hoisted(() => ({
liveRunsForCompany: vi.fn(),
}));
const mockOpenNewAgent = vi.hoisted(() => vi.fn());
const mockSetBreadcrumbs = vi.hoisted(() => vi.fn());
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(),
}));
vi.mock("../context/CompanyContext", () => ({
useCompany: () => ({ selectedCompanyId: "company-1" }),
}));
vi.mock("../context/DialogContext", () => ({
useDialogActions: () => ({ openNewAgent: mockOpenNewAgent }),
}));
vi.mock("../context/BreadcrumbContext", () => ({
useBreadcrumbs: () => ({ setBreadcrumbs: mockSetBreadcrumbs }),
}));
vi.mock("../context/SidebarContext", () => ({
useSidebar: () => ({ isMobile: false }),
}));
vi.mock("../api/agents", () => ({
agentsApi: mockAgentsApi,
}));
vi.mock("../api/heartbeats", () => ({
heartbeatsApi: mockHeartbeatsApi,
}));
vi.mock("../adapters/adapter-display-registry", () => ({
getAdapterLabel: (type: string) => type,
}));
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;
function makeAgent(overrides: Partial<Agent>): Agent {
return {
id: "agent-1",
companyId: "company-1",
name: "Alpha",
urlKey: "alpha",
role: "engineer",
title: null,
icon: null,
status: "active",
reportsTo: null,
capabilities: null,
adapterType: "codex_local",
adapterConfig: {},
runtimeConfig: {},
budgetMonthlyCents: 0,
spentMonthlyCents: 0,
pauseReason: null,
pausedAt: null,
permissions: { canCreateAgents: false },
lastHeartbeatAt: null,
metadata: null,
createdAt: new Date("2026-01-01T00:00:00Z"),
updatedAt: new Date("2026-01-01T00:00:00Z"),
...overrides,
};
}
async function flushReact() {
await act(async () => {
await Promise.resolve();
await new Promise((resolve) => window.setTimeout(resolve, 0));
});
}
describe("Agents", () => {
let container: HTMLDivElement;
let root: ReturnType<typeof createRoot> | null;
let queryClient: QueryClient;
beforeEach(() => {
container = document.createElement("div");
document.body.appendChild(container);
root = null;
queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
mockAgentsApi.list.mockResolvedValue([
makeAgent({ adapterConfig: { model: "gpt-5.4" } }),
]);
mockAgentsApi.org.mockResolvedValue([
{
id: "agent-1",
name: "Alpha",
role: "engineer",
status: "active",
reports: [],
},
]);
mockHeartbeatsApi.liveRunsForCompany.mockResolvedValue([]);
});
afterEach(async () => {
const currentRoot = root;
if (currentRoot) {
await act(async () => {
currentRoot.unmount();
});
}
queryClient.clear();
container.remove();
document.body.innerHTML = "";
vi.clearAllMocks();
});
it("shows the configured model beside the adapter on the all agents page", async () => {
root = createRoot(container);
await act(async () => {
root!.render(
<QueryClientProvider client={queryClient}>
<Agents />
</QueryClientProvider>,
);
});
await flushReact();
await flushReact();
expect(container.textContent).toContain("codex_local");
expect(container.textContent).toContain("gpt-5.4");
});
});
+21 -2
View File
@@ -41,6 +41,13 @@ function filterAgents(agents: Agent[], tab: FilterTab, showTerminated: boolean):
.sort((a, b) => a.name.localeCompare(b.name));
}
function getConfiguredModel(agent: Agent): string | null {
const value = agent.adapterConfig?.model;
if (typeof value !== "string") return null;
const model = value.trim();
return model.length > 0 ? model : null;
}
function filterOrgTree(nodes: OrgNode[], tab: FilterTab, showTerminated: boolean): OrgNode[] {
return nodes
.reduce<OrgNode[]>((acc, node) => {
@@ -253,9 +260,15 @@ export function Agents() {
liveCount={liveRunByAgent.get(agent.id)!.liveCount}
/>
)}
<span className="w-28 whitespace-nowrap text-right font-mono text-xs text-muted-foreground">
<span className="w-28 whitespace-nowrap text-left font-mono text-xs text-muted-foreground">
{getAdapterLabel(agent.adapterType)}
</span>
<span
className="w-36 truncate text-left font-mono text-xs text-muted-foreground"
title={getConfiguredModel(agent) ?? undefined}
>
{getConfiguredModel(agent) ?? "—"}
</span>
<span className="text-xs text-muted-foreground w-16 text-right">
{agent.lastHeartbeatAt ? relativeTime(agent.lastHeartbeatAt) : "—"}
</span>
@@ -356,9 +369,15 @@ function OrgTreeNode({
)}
{agent && (
<>
<span className="w-28 whitespace-nowrap text-right font-mono text-xs text-muted-foreground">
<span className="w-28 whitespace-nowrap text-left font-mono text-xs text-muted-foreground">
{getAdapterLabel(agent.adapterType)}
</span>
<span
className="w-36 truncate text-left font-mono text-xs text-muted-foreground"
title={getConfiguredModel(agent) ?? undefined}
>
{getConfiguredModel(agent) ?? "—"}
</span>
<span className="text-xs text-muted-foreground w-16 text-right">
{agent.lastHeartbeatAt ? relativeTime(agent.lastHeartbeatAt) : "—"}
</span>