import { Link } from "@/lib/router"; import { Identity } from "./Identity"; import { timeAgo } from "../lib/timeAgo"; import { cn } from "../lib/utils"; import { deriveProjectUrlKey, type ActivityEvent, type Agent } from "@paperclipai/shared"; const ACTION_VERBS: Record = { "issue.created": "created", "issue.updated": "updated", "issue.checked_out": "checked out", "issue.released": "released", "issue.comment_added": "commented on", "issue.attachment_added": "attached file to", "issue.attachment_removed": "removed attachment from", "issue.document_created": "created document for", "issue.document_updated": "updated document on", "issue.document_deleted": "deleted document from", "issue.commented": "commented on", "issue.deleted": "deleted", "agent.created": "created", "agent.updated": "updated", "agent.paused": "paused", "agent.resumed": "resumed", "agent.terminated": "terminated", "agent.key_created": "created API key for", "agent.budget_updated": "updated budget for", "agent.runtime_session_reset": "reset session for", "heartbeat.invoked": "invoked heartbeat for", "heartbeat.cancelled": "cancelled heartbeat for", "approval.created": "requested approval", "approval.approved": "approved", "approval.rejected": "rejected", "project.created": "created", "project.updated": "updated", "project.deleted": "deleted", "goal.created": "created", "goal.updated": "updated", "goal.deleted": "deleted", "cost.reported": "reported cost for", "cost.recorded": "recorded cost for", "company.created": "created company", "company.updated": "updated company", "company.archived": "archived", "company.budget_updated": "updated budget for", }; function humanizeValue(value: unknown): string { if (typeof value !== "string") return String(value ?? "none"); return value.replace(/_/g, " "); } function formatVerb(action: string, details?: Record | null): string { if (action === "issue.updated" && details) { const previous = (details._previous ?? {}) as Record; if (details.status !== undefined) { const from = previous.status; return from ? `changed status from ${humanizeValue(from)} to ${humanizeValue(details.status)} on` : `changed status to ${humanizeValue(details.status)} on`; } if (details.priority !== undefined) { const from = previous.priority; return from ? `changed priority from ${humanizeValue(from)} to ${humanizeValue(details.priority)} on` : `changed priority to ${humanizeValue(details.priority)} on`; } } return ACTION_VERBS[action] ?? action.replace(/[._]/g, " "); } function entityLink(entityType: string, entityId: string, name?: string | null): string | null { switch (entityType) { case "issue": return `/issues/${name ?? entityId}`; case "agent": return `/agents/${entityId}`; case "project": return `/projects/${deriveProjectUrlKey(name, entityId)}`; case "goal": return `/goals/${entityId}`; case "approval": return `/approvals/${entityId}`; default: return null; } } interface ActivityRowProps { event: ActivityEvent; agentMap: Map; entityNameMap: Map; entityTitleMap?: Map; className?: string; } export function ActivityRow({ event, agentMap, entityNameMap, entityTitleMap, className }: ActivityRowProps) { const verb = formatVerb(event.action, event.details); const isHeartbeatEvent = event.entityType === "heartbeat_run"; const heartbeatAgentId = isHeartbeatEvent ? (event.details as Record | null)?.agentId as string | undefined : undefined; const name = isHeartbeatEvent ? (heartbeatAgentId ? entityNameMap.get(`agent:${heartbeatAgentId}`) : null) : entityNameMap.get(`${event.entityType}:${event.entityId}`); const entityTitle = entityTitleMap?.get(`${event.entityType}:${event.entityId}`); const link = isHeartbeatEvent && heartbeatAgentId ? `/agents/${heartbeatAgentId}/runs/${event.entityId}` : entityLink(event.entityType, event.entityId, name); const actor = event.actorType === "agent" ? agentMap.get(event.actorId) : null; const actorName = actor?.name ?? (event.actorType === "system" ? "System" : event.actorType === "user" ? "Board" : event.actorId || "Unknown"); const inner = (

{verb} {name && {name}} {entityTitle && — {entityTitle}}

{timeAgo(event.createdAt)}
); const classes = cn( "px-4 py-2 text-sm", link && "cursor-pointer hover:bg-accent/50 transition-colors", className, ); if (link) { return ( {inner} ); } return (
{inner}
); }