import { BookOpenIcon } from "@heroicons/react/24/solid"; import { type MetaFunction } from "@remix-run/react"; import { type LoaderFunctionArgs } from "@remix-run/server-runtime"; import { Suspense, useMemo, useState } from "react"; import { TypedAwait, typeddefer, useTypedLoaderData } from "remix-typedjson"; import { z } from "zod"; import { BeakerIcon } from "~/assets/icons/BeakerIcon"; import { CubeSparkleIcon } from "~/assets/icons/CubeSparkleIcon"; import { PageBody } from "~/components/layout/AppLayout"; import { DirectionSchema, ListPagination } from "~/components/ListPagination"; import { LinkButton } from "~/components/primitives/Buttons"; import { buildActivityTimeAxis } from "~/components/primitives/charts/activityTimeAxis"; import { ChartCard } from "~/components/primitives/charts/ChartCard"; import { Chart, type ChartConfig } from "~/components/primitives/charts/ChartCompound"; import { ChartSyncProvider } from "~/components/primitives/charts/ChartSyncContext"; import { statusColor } from "~/components/primitives/charts/statusColors"; import { CopyableText } from "~/components/primitives/CopyableText"; import { DateTime } from "~/components/primitives/DateTime"; import { Header2 } from "~/components/primitives/Headers"; import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/PageHeader"; import { Paragraph } from "~/components/primitives/Paragraph"; import * as Property from "~/components/primitives/PropertyTable"; import { ResizableHandle, ResizablePanel, ResizablePanelGroup, } from "~/components/primitives/Resizable"; import { Spinner } from "~/components/primitives/Spinner"; import { TabButton, TabContainer } from "~/components/primitives/Tabs"; import { TimeFilter, timeFilterFromTo } from "~/components/runs/v3/SharedFilters"; import { TaskRunsTable } from "~/components/runs/v3/TaskRunsTable"; import { SessionsTable } from "~/components/sessions/v1/SessionsTable"; import { $replica } from "~/db.server"; import { useEnvironment } from "~/hooks/useEnvironment"; import { useOrganization } from "~/hooks/useOrganizations"; import { useProject } from "~/hooks/useProject"; import { useZoomToTimeFilter } from "~/hooks/useZoomToTimeFilter"; import { findProjectBySlug } from "~/models/project.server"; import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server"; import { AgentDetailPresenter, type AgentActivity, type AgentDetail, } from "~/presenters/v3/AgentDetailPresenter.server"; import { NextRunListPresenter } from "~/presenters/v3/NextRunListPresenter.server"; import { SessionListPresenter } from "~/presenters/v3/SessionListPresenter.server"; import { clickhouseFactory } from "~/services/clickhouse/clickhouseFactoryInstance.server"; import { requireUser } from "~/services/session.server"; import { docsPath, EnvironmentParamSchema, v3EnvironmentPath, v3PlaygroundAgentPath, } from "~/utils/pathBuilder"; import { parseFiniteInt } from "~/utils/searchParams"; export const meta: MetaFunction = ({ data }) => { const slug = (data as { agent?: AgentDetail | null } | undefined)?.agent?.slug; return [{ title: slug ? `${slug} | Agents | Trigger.dev` : "Agent | Trigger.dev" }]; }; const AgentParamSchema = EnvironmentParamSchema.extend({ agentParam: z.string(), }); export const loader = async ({ request, params }: LoaderFunctionArgs) => { const user = await requireUser(request); const userId = user.id; const { organizationSlug, projectParam, envParam, agentParam } = AgentParamSchema.parse(params); const project = await findProjectBySlug(organizationSlug, projectParam, userId); if (!project) { throw new Response("Project not found", { status: 404 }); } const environment = await findEnvironmentBySlug(project.id, envParam, userId); if (!environment) { throw new Response("Environment not found", { status: 404 }); } const url = new URL(request.url); const period = url.searchParams.get("period") ?? undefined; const from = parseFiniteInt(url.searchParams.get("from")); const to = parseFiniteInt(url.searchParams.get("to")); const cursor = url.searchParams.get("cursor") ?? undefined; const directionRaw = url.searchParams.get("direction") ?? undefined; const direction = directionRaw ? DirectionSchema.parse(directionRaw) : undefined; const clickhouse = await clickhouseFactory.getClickhouseForOrganization( project.organizationId, "standard" ); const presenter = new AgentDetailPresenter($replica, clickhouse); const agent = await presenter.findAgent({ environmentId: environment.id, environmentType: environment.type, agentSlug: agentParam, }); if (!agent) { throw new Response("Agent not found", { status: 404 }); } const time = timeFilterFromTo({ period, from, to, defaultPeriod: "7d" }); const runActivity = presenter .getActivity({ organizationId: project.organizationId, projectId: project.id, environmentId: environment.id, agentSlug: agent.slug, from: time.from, to: time.to, }) .catch(() => ({ data: [], statuses: [] }) satisfies AgentActivity); const sessionActivity = presenter .getSessionActivity({ organizationId: project.organizationId, projectId: project.id, environmentId: environment.id, agentSlug: agent.slug, from: time.from, to: time.to, }) .catch(() => ({ data: [], statuses: [] }) satisfies AgentActivity); const llmCostActivity = presenter .getLlmCostActivity({ organizationId: project.organizationId, projectId: project.id, environmentId: environment.id, agentSlug: agent.slug, from: time.from, to: time.to, }) .catch(() => ({ data: [], statuses: [] }) satisfies AgentActivity); const llmTokenActivity = presenter .getLlmTokenActivity({ organizationId: project.organizationId, projectId: project.id, environmentId: environment.id, agentSlug: agent.slug, from: time.from, to: time.to, }) .catch(() => ({ data: [], statuses: [] }) satisfies AgentActivity); const runList = new NextRunListPresenter($replica, clickhouse) .call(project.organizationId, environment.id, { userId, projectId: project.id, tasks: [agent.slug], period, from, to, cursor, direction, }) .catch(() => null); const sessionList = new SessionListPresenter($replica, clickhouse) .call(project.organizationId, environment.id, { userId, projectId: project.id, taskIdentifiers: [agent.slug], period, from, to, cursor, direction, }) .catch(() => null); return typeddefer({ agent, runActivity, sessionActivity, llmCostActivity, llmTokenActivity, runList, sessionList, }); }; type AgentTab = "sessions" | "runs"; export default function Page() { const { agent, runActivity, sessionActivity, llmCostActivity, llmTokenActivity, runList, sessionList, } = useTypedLoaderData(); const organization = useOrganization(); const project = useProject(); const environment = useEnvironment(); const playgroundPath = v3PlaygroundAgentPath(organization, project, environment, agent.slug); const tasksPath = v3EnvironmentPath(organization, project, environment); const [tab, setTab] = useState("sessions"); const zoomToTimeFilter = useZoomToTimeFilter(); const tabLabel = tab === "sessions" ? "Sessions" : "Runs"; return ( <> {agent.slug} } /> Agents docs
{/* Top bar — tabs on the left; TimeFilter + pagination on the right. h-10 matches the right-hand sidebar header height. */}
setTab("sessions")} > Sessions setTab("runs")} > Runs
{tab === "sessions" ? ( {(list) => (list ? : null)} ) : ( {(list) => (list ? : null)} )}
{/* Activity / LLM cost / Token charts */}
{tab === "sessions" ? ( }> } > {(result) => } ) : ( }> } > {(result) => } )} }> } > {(result) => ( )} }> } > {(result) => ( )}
{/* Table */}
); } type LoaderData = ReturnType>; function AgentContentArea({ tab, sessionList, runList, }: { tab: AgentTab } & Pick) { return (
{tab === "sessions" ? ( }> }> {(list) => list ? (
) : ( ) }
) : ( }> }> {(list) => list ? (
) : ( ) }
)}
); } function AgentDetailSidebar({ agent, playgroundPath, }: { agent: AgentDetail; playgroundPath: string; }) { const config = (agent.config ?? {}) as Record; const agentType = typeof config.type === "string" ? config.type : undefined; const model = typeof config.model === "string" ? config.model : undefined; const instructions = typeof config.instructions === "string" ? config.instructions : undefined; return (
{agent.slug} Test agent
Slug File path {agentType && ( Type {agentType} )} {model && ( Model {model} )} {instructions && ( Instructions {instructions} )} Created
); } function ActivityChart({ activity }: { activity: AgentActivity }) { const chartConfig: ChartConfig = useMemo(() => { const cfg: ChartConfig = {}; for (const status of activity.statuses) { cfg[status] = { label: status.charAt(0) + status.slice(1).toLowerCase(), color: statusColor(status), }; } return cfg; }, [activity.statuses]); const { tickFormatter, tooltipLabelFormatter } = useMemo( () => buildActivityTimeAxis(activity.data), [activity.data] ); return ( ); } function ActivityChartSkeleton() { return (
{Array.from({ length: 42 }).map((_, i) => (
))}
); } function TableLoading() { return (
); } function ScalarActivityChart({ activity, seriesKey, label, color, valueFormatter, }: { activity: AgentActivity; seriesKey: string; label: string; color: string; valueFormatter: (value: number) => string; }) { const chartConfig: ChartConfig = useMemo( () => ({ [seriesKey]: { label, color } }), [seriesKey, label, color] ); const { tickFormatter, tooltipLabelFormatter } = useMemo( () => buildActivityTimeAxis(activity.data), [activity.data] ); return ( ); } function formatCurrency(value: number): string { if (value === 0) return "$0"; if (value < 0.01) return `$${value.toFixed(4)}`; if (value < 1) return `$${value.toFixed(3)}`; return `$${value.toFixed(2)}`; } function formatTokens(value: number): string { if (value < 1000) return value.toLocaleString(); if (value < 1_000_000) return `${(value / 1000).toFixed(1)}k`; return `${(value / 1_000_000).toFixed(1)}M`; }