mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-15 03:51:22 +02:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - Operators use the issue list, blocked-work notices, pipeline body documents, and issue documents to understand what agents are doing > - Some of those surfaces had low-information details: live-work blockers were visually easy to miss, revision history showed generic actor labels, and selected active subtasks carried an extra accent border > - These details matter because Paperclip's default UI should make agent state and work history legible without requiring users to inspect raw logs > - This pull request groups a small set of related operator UI polish changes for those work-state and document-history surfaces > - The benefit is clearer review context for blocked work, better attribution in document revision history, and cleaner active-subtask row styling ## Linked Issues or Issue Description No public GitHub issue found during dedup search. ### Bug Report **Pre-submission checklist** - Searched existing open PRs/issues for `document revision authors` and `blocked notice live work`; no duplicates found. - Reproduced against the current `master` base for this PR. - Confirmed this is core board UI behavior, not adapter/provider/local configuration. **What happened?** Blocked live-work notices, document revision history, pipeline body document revision history, and active subtask rows exposed technically correct but low-signal UI details. Revision menus could show generic `Board`/`Agent` labels instead of the specific author, and selected active subtasks had an extra accent border. **Expected behavior** Operators should see clear blocked-live-work copy, recognizable revision authors using available agent/user profile data, and visually consistent selected task rows. **Steps to reproduce** 1. Open an issue with a live-work blocker and inspect the blocked notice. 2. Open an issue document revision menu with agent/user-authored revisions. 3. Open a pipeline item body document revision menu with agent/user-authored revisions. 4. Inspect a selected active subtask row in the issue list. **Paperclip version or commit** `8b6a06ee2` (`origin/master` at branch creation). **Deployment mode** Local dev / self-hosted board UI. **Installation method** Built from source. **Agent adapter(s) involved** Not adapter-specific (core board UI). **Database mode** Not database-related. **Access context** Board operator UI. **Relevant logs or output** Not applicable. **Relevant config** Not applicable. **Additional context** This PR is a small polish/fix contribution. `ROADMAP.md` allows tightly scoped bugs and polish without roadmap-level coordination. **Privacy checklist** Reviewed the PR body for private instance references and omitted internal ticket links. ## What Changed - Polished the live-work blocked notice copy and tests so operators get a clearer explanation of what is blocking progress. - Show issue document revision authors with agent/user names and avatar treatment instead of generic actor labels. - Show pipeline body document revision authors with the same available agent/user profile data. - Removed the extra left accent border from selected active subtask rows. - Stabilized the document revision test helper for React runtimes where `act` is not exported as a function. ## Verification - `git diff --check origin/master..HEAD` - `git diff --name-only origin/master..HEAD -- .github/workflows pnpm-lock.yaml` produced no output. - `pnpm check:token-gates` passed: all color literal, arbitrary bracket value, and raw font-size gates clean. - `pnpm exec vitest run ui/src/components/IssueBlockedNotice.test.tsx ui/src/components/IssueDocumentsSection.test.tsx ui/src/components/IssueRow.test.tsx` passed: 3 test files, 32 tests. - `pnpm --filter @paperclipai/ui typecheck` passed. - Remote PR checks passed on head `3e18fe3f`: 22 complete, 0 pending, 0 failing. - Greptile completed at 5/5 with no blocking issues after addressing the pipeline revision author feedback. ## Risks Low risk. The change is limited to board UI rendering and component tests. The main risk is a subtle visual regression in issue/document surfaces that are not covered by screenshots; the affected component tests cover the intended copy, attribution, and row-style 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, tool-enabled CLI session. Exact runtime context window is not exposed by this execution 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 searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
187 lines
7.4 KiB
TypeScript
187 lines
7.4 KiB
TypeScript
import type { ReactNode } from "react";
|
|
import { ChevronDown, ChevronRight } from "lucide-react";
|
|
import { cn, relativeTime } from "../lib/utils";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuLabel,
|
|
DropdownMenuRadioGroup,
|
|
DropdownMenuRadioItem,
|
|
DropdownMenuTrigger,
|
|
} from "@/components/ui/dropdown-menu";
|
|
import { AgentIcon } from "./AgentIconPicker";
|
|
import { deriveInitials } from "./Identity";
|
|
|
|
export type DocumentFrameHeaderRevisionActor = {
|
|
kind: "agent" | "user" | "system";
|
|
name: string;
|
|
agentIcon?: string | null;
|
|
imageUrl?: string | null;
|
|
};
|
|
|
|
export type DocumentFrameHeaderRevision = {
|
|
id: string;
|
|
revisionNumber: number;
|
|
createdAt: string | Date;
|
|
actor: DocumentFrameHeaderRevisionActor;
|
|
};
|
|
|
|
export type DocumentFrameHeaderRevisionMenu = {
|
|
open: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
loading: boolean;
|
|
revisions: DocumentFrameHeaderRevision[];
|
|
selectedRevisionId: string | null;
|
|
currentRevisionId: string | null;
|
|
displayedRevisionNumber: number;
|
|
historicalPreview: boolean;
|
|
onSelectRevision: (revisionId: string, isCurrentRevision: boolean) => void;
|
|
};
|
|
|
|
export interface DocumentFrameHeaderProps {
|
|
documentKey: string;
|
|
documentLabel?: string;
|
|
folded: boolean;
|
|
onToggleFolded: () => void;
|
|
revisionMenu?: DocumentFrameHeaderRevisionMenu;
|
|
updatedAt?: string | Date | null;
|
|
updatedHref?: string;
|
|
sourceTrustSlot?: ReactNode;
|
|
annotationSlot?: ReactNode;
|
|
titleSlot?: ReactNode;
|
|
actionsSlot?: ReactNode;
|
|
}
|
|
|
|
function RevisionActorAvatar({ actor }: { actor: DocumentFrameHeaderRevisionActor }) {
|
|
return (
|
|
<Avatar size="xs" shape={actor.kind === "agent" ? "square" : "circle"} className="shrink-0">
|
|
{actor.kind === "agent" ? (
|
|
<AvatarFallback>
|
|
<AgentIcon icon={actor.agentIcon} className="h-3 w-3" />
|
|
</AvatarFallback>
|
|
) : (
|
|
<>
|
|
{actor.imageUrl ? <AvatarImage src={actor.imageUrl} alt={actor.name} /> : null}
|
|
<AvatarFallback>{deriveInitials(actor.name)}</AvatarFallback>
|
|
</>
|
|
)}
|
|
</Avatar>
|
|
);
|
|
}
|
|
|
|
export function DocumentFrameHeader({
|
|
documentKey,
|
|
documentLabel,
|
|
folded,
|
|
onToggleFolded,
|
|
revisionMenu,
|
|
updatedAt,
|
|
updatedHref,
|
|
sourceTrustSlot,
|
|
annotationSlot,
|
|
titleSlot,
|
|
actionsSlot,
|
|
}: DocumentFrameHeaderProps) {
|
|
return (
|
|
<div className="flex items-start justify-between gap-3">
|
|
<div className="min-w-0">
|
|
<div className="flex items-center gap-2 min-w-0">
|
|
<button
|
|
type="button"
|
|
className="inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-sm text-muted-foreground transition-colors hover:bg-accent/60 hover:text-foreground"
|
|
onClick={onToggleFolded}
|
|
aria-label={folded ? `Expand ${documentKey} document` : `Collapse ${documentKey} document`}
|
|
aria-expanded={!folded}
|
|
>
|
|
{folded ? <ChevronRight className="h-3.5 w-3.5" /> : <ChevronDown className="h-3.5 w-3.5" />}
|
|
</button>
|
|
{documentLabel ? (
|
|
<>
|
|
<span className="truncate text-sm font-semibold text-foreground">{documentLabel}</span>
|
|
<Badge variant="outline" className="border-border font-mono text-(length:--text-nano) uppercase tracking-(--tracking-eyebrow) text-muted-foreground">
|
|
{documentKey}
|
|
</Badge>
|
|
</>
|
|
) : (
|
|
<Badge variant="outline" className="border-border font-mono text-(length:--text-nano) uppercase tracking-(--tracking-eyebrow) text-muted-foreground">
|
|
{documentKey}
|
|
</Badge>
|
|
)}
|
|
{sourceTrustSlot}
|
|
{revisionMenu ? (
|
|
<DropdownMenu open={revisionMenu.open} onOpenChange={revisionMenu.onOpenChange}>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className={cn(
|
|
"h-auto px-1.5 py-0 text-(length:--text-micro) font-normal text-muted-foreground hover:text-foreground",
|
|
revisionMenu.historicalPreview && "text-amber-700 hover:text-amber-800 dark:text-amber-300 dark:hover:text-amber-200",
|
|
)}
|
|
>
|
|
rev {revisionMenu.displayedRevisionNumber}
|
|
<ChevronDown className="h-3 w-3" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="start" className="w-72">
|
|
<DropdownMenuLabel>Revision history</DropdownMenuLabel>
|
|
{revisionMenu.loading && revisionMenu.revisions.length === 0 ? (
|
|
<DropdownMenuItem disabled>Loading revisions...</DropdownMenuItem>
|
|
) : revisionMenu.revisions.length > 0 ? (
|
|
<DropdownMenuRadioGroup value={revisionMenu.selectedRevisionId ?? revisionMenu.currentRevisionId ?? ""}>
|
|
{revisionMenu.revisions.map((revision) => {
|
|
const isCurrentRevision = revision.id === revisionMenu.currentRevisionId;
|
|
return (
|
|
<DropdownMenuRadioItem
|
|
key={revision.id}
|
|
value={revision.id}
|
|
onSelect={() => revisionMenu.onSelectRevision(revision.id, isCurrentRevision)}
|
|
className="items-start"
|
|
>
|
|
<div className="flex min-w-0 flex-col">
|
|
<div className="flex items-center gap-2">
|
|
<span className="font-medium">rev {revision.revisionNumber}</span>
|
|
{isCurrentRevision ? (
|
|
<Badge variant="outline" className="border-border px-1.5 text-(length:--text-nano) uppercase tracking-(--tracking-eyebrow) text-muted-foreground">
|
|
Current
|
|
</Badge>
|
|
) : null}
|
|
</div>
|
|
<div className="mt-1 flex min-w-0 items-center gap-1.5 text-(length:--text-micro) text-muted-foreground">
|
|
<RevisionActorAvatar actor={revision.actor} />
|
|
<span className="truncate">
|
|
{relativeTime(revision.createdAt)} • {revision.actor.name}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</DropdownMenuRadioItem>
|
|
);
|
|
})}
|
|
</DropdownMenuRadioGroup>
|
|
) : (
|
|
<DropdownMenuItem disabled>No revisions yet</DropdownMenuItem>
|
|
)}
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
) : null}
|
|
{updatedAt ? (
|
|
<a
|
|
href={updatedHref ?? `#document-${encodeURIComponent(documentKey)}`}
|
|
className="truncate text-(length:--text-micro) text-muted-foreground transition-colors hover:text-foreground hover:underline"
|
|
>
|
|
updated {relativeTime(updatedAt)}
|
|
</a>
|
|
) : null}
|
|
{annotationSlot}
|
|
</div>
|
|
{titleSlot}
|
|
</div>
|
|
{actionsSlot ? <div className="flex items-center gap-1 shrink-0">{actionsSlot}</div> : null}
|
|
</div>
|
|
);
|
|
}
|