mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-15 02:20:38 +09:00
## Thinking Path > - Paperclip orchestrates AI agents for zero-human companies > - Operators spend most of their day scanning skills, routines, inbox groups, and activity cards > - Several small UI rough edges made those surfaces harder to scan or easier to crash on real API payloads > - These fixes are grouped together because they are low-risk operator quality-of-life improvements rather than separate control-plane contracts > - This pull request polishes skills metadata, routine run-now access, grouped issue creation defaults, monitor activity rendering, and activity row identity layout > - The benefit is a smoother board workflow with fewer small interruptions while keeping the change set compact ## What Changed - Improves company skill source display and the used-by agent list. - Truncates long skill source paths and adds a copy affordance. - Adds a row-level run-now button to the routines table. - Adds grouped issue creation defaults for inbox issue groups and aligns grouped add buttons to the right. - Fixes `IssueMonitorActivityCard` when `monitorNextCheckAt` arrives as an ISO string. - Polishes activity row actor avatar/name layout by using the shared avatar primitive. ## Verification - `pnpm run preflight:workspace-links && pnpm exec vitest run ui/src/pages/Routines.test.tsx ui/src/components/IssuesList.test.tsx ui/src/lib/inbox.test.ts ui/src/components/IssueMonitorActivityCard.test.tsx` — 91 passed. - The routines test emitted the pre-existing Radix warning about missing `DialogTitle`/description in dialog content; tests still passed. - Pairwise merge checks against the other two PR branches reported no textual conflicts. ## Risks - Low: changes are UI-focused and covered by targeted component/lib tests. - Low-to-medium: activity row layout changes could affect dense feed scanability; the implementation uses the shared avatar component and keeps truncation behavior. > For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and discuss it in `#dev` before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See `CONTRIBUTING.md`. ## Model Used - OpenAI Codex coding agent, GPT-5 model family (`gpt-5`), tool-enabled Paperclip heartbeat environment. Context window and internal reasoning mode are not exposed by the runtime. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] If this change affects the UI, I have included before/after screenshots - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
94 lines
3.7 KiB
TypeScript
94 lines
3.7 KiB
TypeScript
import { Link } from "@/lib/router";
|
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
|
import { deriveInitials } from "./Identity";
|
|
import { IssueReferenceActivitySummary } from "./IssueReferenceActivitySummary";
|
|
import { timeAgo } from "../lib/timeAgo";
|
|
import { cn } from "../lib/utils";
|
|
import { formatActivityVerb } from "../lib/activity-format";
|
|
import { deriveProjectUrlKey, type ActivityEvent, type Agent } from "@paperclipai/shared";
|
|
import type { CompanyUserProfile } from "../lib/company-members";
|
|
|
|
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>;
|
|
userProfileMap?: Map<string, CompanyUserProfile>;
|
|
entityNameMap: Map<string, string>;
|
|
entityTitleMap?: Map<string, string>;
|
|
className?: string;
|
|
}
|
|
|
|
export function ActivityRow({ event, agentMap, userProfileMap, entityNameMap, entityTitleMap, className }: ActivityRowProps) {
|
|
const verb = formatActivityVerb(event.action, event.details, { agentMap, userProfileMap });
|
|
|
|
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 userProfile = event.actorType === "user" ? userProfileMap?.get(event.actorId) : null;
|
|
const actorName = actor?.name ?? (event.actorType === "system" ? "System" : userProfile?.label ?? (event.actorType === "user" ? "Board" : event.actorId || "Unknown"));
|
|
const actorAvatarUrl = userProfile?.image ?? null;
|
|
|
|
const inner = (
|
|
<div className="space-y-2">
|
|
<div className="flex items-center gap-3">
|
|
<div className="flex min-w-0 flex-1 items-center gap-2">
|
|
<Avatar size="xs">
|
|
{actorAvatarUrl && <AvatarImage src={actorAvatarUrl} alt={actorName} />}
|
|
<AvatarFallback>{deriveInitials(actorName)}</AvatarFallback>
|
|
</Avatar>
|
|
<p className="min-w-0 flex-1 truncate">
|
|
<span>{actorName}</span>
|
|
<span className="text-muted-foreground"> {verb} </span>
|
|
{name && <span className="font-medium">{name}</span>}
|
|
{entityTitle && <span className="text-muted-foreground"> — {entityTitle}</span>}
|
|
</p>
|
|
</div>
|
|
<span className="text-xs text-muted-foreground shrink-0">{timeAgo(event.createdAt)}</span>
|
|
</div>
|
|
<IssueReferenceActivitySummary event={event} />
|
|
</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>
|
|
);
|
|
}
|