import Link from "next/link";
import type { Metadata } from "next";
import { revertPushAction } from "@/app/actions/activity";
import { ConfirmButton } from "@/components/confirm-button";
import { ActivityIcon, ChevronLeftIcon, ChevronRightIcon, SparkleIcon } from "@/components/icons";
import { liftRowClass } from "@/components/repo-rows";
import { Blankslate, Box, ButtonLink, Label, PageTitle, Segmented, segmentClass } from "@/components/ui";
import { requireViewer } from "@/lib/auth";
import { describeEvent, eventPath, isRevertable } from "@/lib/event-text";
import { listActiveAgents, listAgentActivity } from "@/lib/events";
import { formatDate, timeAgo } from "@/lib/format";
import { revParse } from "@/lib/git";
import { findRepoById } from "@/lib/repos";
export const metadata: Metadata = { title: "Agent activity" };
const PAGE_SIZE = 40;
export default async function AgentActivityPage({ searchParams }: PageProps<"/settings/activity">) {
const viewer = await requireViewer("/settings/activity");
const query = await searchParams;
const agent = typeof query.agent === "string" ? query.agent : undefined;
const page = Math.max(1, Number(query.page) || 1);
const [agents, { events, total }] = await Promise.all([
listActiveAgents(viewer.id),
listAgentActivity(viewer.id, { agent, page, perPage: PAGE_SIZE }),
]);
const repos = new Map<string, { base: string; name: string; dir: string } | null>();
for (const e of events) {
if (!repos.has(e.repoId)) {
const r = await findRepoById(e.repoId);
repos.set(e.repoId, r ? { base: `/${r.owner}/${r.name}`, name: `${r.owner}/${r.name}`, dir: r.dir } : null);
}
}
// Pushes to branches that are gone can't be undone anymore.
const undoable = new Set<string>();
for (const e of events) {
const repo = repos.get(e.repoId);
if (repo && isRevertable(e) && (await revParse(repo.dir, String(e.payload.ref)))) undoable.add(e.id);
}
const href = (a?: string, p = 1) => `/settings/activity?${new URLSearchParams({ ...(a ? { agent: a } : {}), ...(p > 1 ? { page: String(p) } : {}) })}`;
return (
<div>
<PageTitle
title="Agent activity"
description="Everything your agents did with your access, newest first: commits, pull requests, comments, issues. A push they made can be undone with a revert commit."
/>
{agents.length > 1 && (
<Segmented className="mb-4 flex-wrap">
<Link href={href()} className={segmentClass(!agent)}>
All agents
</Link>
{agents.map((a) => (
<Link key={a.key} href={href(a.key)} className={segmentClass(agent === a.key)}>
<SparkleIcon /> {a.name}
</Link>
))}
</Segmented>
)}
<Box className="p-1.5">
{events.length === 0 ? (
<Blankslate icon={<ActivityIcon />} title="Nothing yet">
When an agent you connected changes something, it shows up here.
</Blankslate>
) : (
<ul>
{events.map((e) => {
const repo = repos.get(e.repoId);
const reverted = e.payload.reverted as { commit: string; pull: number | null } | undefined;
return (
<li key={e.id} className={`${liftRowClass} flex flex-wrap items-center gap-x-3 gap-y-1 px-3 py-2.5`}>
<span className="icon-tile size-8 shrink-0 rounded-row">
<SparkleIcon width={14} height={14} />
</span>
<div className="min-w-0 flex-1">
<p className="type-ui text-ink">
<strong className="font-medium">{e.agent?.name}</strong>{" "}
{repo ? (
<Link href={`${repo.base}${eventPath(e)}`} className="hover:underline">
{describeEvent(e)}
</Link>
) : (
describeEvent(e)
)}
</p>
<p className="type-meta text-ink3">
{repo ? repo.name : "a deleted repository"} · <span title={formatDate(e.createdAt)}>{timeAgo(e.createdAt)}</span>
</p>
</div>
{reverted && (
<Link href={repo ? `${repo.base}${reverted.pull ? `/pulls/${reverted.pull}` : `/commit/${reverted.commit}`}` : "#"}>
<Label>Reverted</Label>
</Link>
)}
{undoable.has(e.id) && (
<ConfirmButton
action={revertPushAction.bind(null, e.id)}
message={`Undo this push? A new commit reverts its changes on the branch (or, if the branch is protected, a pull request does).`}
variant="default"
confirmLabel="Revert"
>
Undo
</ConfirmButton>
)}
</li>
);
})}
</ul>
)}
</Box>
{(page > 1 || page * PAGE_SIZE < total) && (
<div className="mt-6 flex justify-center gap-2">
{page > 1 && (
<ButtonLink href={href(agent, page - 1)} icon={<ChevronLeftIcon />}>
Newer
</ButtonLink>
)}
{page * PAGE_SIZE < total && (
<ButtonLink href={href(agent, page + 1)}>
Older <ChevronRightIcon />
</ButtonLink>
)}
</div>
)}
</div>
);
}