Rename all workspace packages from @paperclip/* to @paperclipai/* and the CLI binary from `paperclip` to `paperclipai` in preparation for npm publishing. Bump CLI version to 0.1.0 and add package metadata (description, keywords, license, repository, files). Update all imports, documentation, user-facing messages, and tests accordingly. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
144 lines
5.0 KiB
TypeScript
144 lines
5.0 KiB
TypeScript
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<string, string> = {
|
|
"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.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<string, unknown> | null): string {
|
|
if (action === "issue.updated" && details) {
|
|
const previous = (details._previous ?? {}) as Record<string, unknown>;
|
|
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<string, Agent>;
|
|
entityNameMap: Map<string, string>;
|
|
entityTitleMap?: Map<string, string>;
|
|
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<string, unknown> | 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 = (
|
|
<div className="flex gap-3">
|
|
<p className="flex-1 min-w-0 truncate">
|
|
<Identity
|
|
name={actorName}
|
|
size="xs"
|
|
className="align-baseline"
|
|
/>
|
|
<span className="text-muted-foreground ml-1">{verb} </span>
|
|
{name && <span className="font-medium">{name}</span>}
|
|
{entityTitle && <span className="text-muted-foreground ml-1">— {entityTitle}</span>}
|
|
</p>
|
|
<span className="text-xs text-muted-foreground shrink-0 pt-0.5">{timeAgo(event.createdAt)}</span>
|
|
</div>
|
|
);
|
|
|
|
const classes = cn(
|
|
"px-4 py-2 text-sm",
|
|
link && "cursor-pointer hover:bg-accent/50 transition-colors",
|
|
className,
|
|
);
|
|
|
|
if (link) {
|
|
return (
|
|
<Link to={link} className={cn(classes, "no-underline text-inherit block")}>
|
|
{inner}
|
|
</Link>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className={classes}>
|
|
{inner}
|
|
</div>
|
|
);
|
|
}
|