forked from farhoodlabs/paperclip
a26e1288b6
## Thinking Path > - Paperclip orchestrates AI agents for zero-human companies > - Human operators supervise that work through issue lists, issue detail, comments, inbox groups, markdown references, and profile/activity surfaces > - The branch had many small UI fixes that improve the operator loop but do not need to ship with backend runtime migrations > - These changes belong together as board workflow polish because they affect scanning, navigation, issue context, comment state, and markdown clarity > - This pull request groups the UI-only slice so it can merge independently from runtime/backend changes > - The benefit is a clearer board experience with better issue context, steadier optimistic updates, and more predictable keyboard navigation ## What Changed - Improves issue properties, sub-issue actions, blocker chips, and issue list/detail refresh behavior. - Adds blocker context above the issue composer and stabilizes queued/interrupted comment UI state. - Improves markdown issue/GitHub link rendering and opens external markdown links in a new tab. - Adds inbox group keyboard navigation and fold/unfold support. - Polishes activity/avatar/profile/settings/workspace presentation details. ## Verification - `pnpm exec vitest run ui/src/components/IssueProperties.test.tsx ui/src/components/IssueChatThread.test.tsx ui/src/components/MarkdownBody.test.tsx ui/src/lib/inbox.test.ts ui/src/lib/optimistic-issue-comments.test.ts` ## Risks - Low to medium risk: changes are UI-focused but cover high-traffic issue and inbox surfaces. - This branch intentionally does not include the backend runtime changes from the companion PR; where UI calls newer API filters, unsupported servers should continue to fail visibly through existing API error handling. - Visual screenshots were not captured in this heartbeat; targeted component/helper tests cover the changed 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, GPT-5-based coding agent runtime, shell/git tool use enabled. Exact hosted model build and context window are not exposed in this Paperclip heartbeat environment. ## 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 - [ ] 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
93 lines
3.4 KiB
TypeScript
93 lines
3.4 KiB
TypeScript
import { Link } from "@/lib/router";
|
|
import { Identity } 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 gap-3">
|
|
<p className="flex-1 min-w-0 truncate">
|
|
<Identity
|
|
name={actorName}
|
|
avatarUrl={actorAvatarUrl}
|
|
size="xs"
|
|
className="align-middle"
|
|
/>
|
|
<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>
|
|
<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>
|
|
);
|
|
}
|