mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-14 01:50:39 +09:00
Polish issue chat actions and overflow
- Scale activity components (events, runs) to ~80% font size with
xs avatars for a quieter visual weight
- Hide succeeded runs from the timeline; only show failed/errored
- Always show three-dots menu on agent comments with "Copy message"
option, plus optional "View run" when available
- User avatar repositioned to top-right (items-start) of message
- Change "Me" → "You" in assignee labels for natural chat phrasing
("You updated this task")
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
94652c6079
commit
f94fe57d10
4 changed files with 55 additions and 42 deletions
|
|
@ -360,7 +360,7 @@ function IssueChatUserMessage() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MessagePrimitive.Root id={anchorId}>
|
<MessagePrimitive.Root id={anchorId}>
|
||||||
<div className="group flex items-end justify-end gap-2">
|
<div className="group flex items-start justify-end gap-2">
|
||||||
<div className="flex max-w-[85%] flex-col items-end">
|
<div className="flex max-w-[85%] flex-col items-end">
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|
@ -435,7 +435,7 @@ function IssueChatUserMessage() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Avatar size="sm" className="mb-6 shrink-0">
|
<Avatar size="sm" className="mt-1 shrink-0">
|
||||||
<AvatarFallback>You</AvatarFallback>
|
<AvatarFallback>You</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -508,26 +508,38 @@ function IssueChatAssistantMessage() {
|
||||||
<a href={anchorId ? `#${anchorId}` : undefined} className="hover:text-foreground hover:underline">
|
<a href={anchorId ? `#${anchorId}` : undefined} className="hover:text-foreground hover:underline">
|
||||||
{message.createdAt ? formatShortDate(message.createdAt) : ""}
|
{message.createdAt ? formatShortDate(message.createdAt) : ""}
|
||||||
</a>
|
</a>
|
||||||
{runHref ? (
|
<DropdownMenu>
|
||||||
<DropdownMenu>
|
<DropdownMenuTrigger asChild>
|
||||||
<DropdownMenuTrigger asChild>
|
<Button
|
||||||
<Button
|
variant="ghost"
|
||||||
variant="ghost"
|
size="icon-xs"
|
||||||
size="icon-xs"
|
className="text-muted-foreground hover:text-foreground"
|
||||||
className="text-muted-foreground hover:text-foreground"
|
title="More actions"
|
||||||
title="More actions"
|
aria-label="More actions"
|
||||||
aria-label="More actions"
|
>
|
||||||
>
|
<MoreHorizontal className="h-3.5 w-3.5" />
|
||||||
<MoreHorizontal className="h-3.5 w-3.5" />
|
</Button>
|
||||||
</Button>
|
</DropdownMenuTrigger>
|
||||||
</DropdownMenuTrigger>
|
<DropdownMenuContent align="end">
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuItem
|
||||||
|
onClick={() => {
|
||||||
|
const text = message.content
|
||||||
|
.filter((p): p is { type: "text"; text: string } => p.type === "text")
|
||||||
|
.map((p) => p.text)
|
||||||
|
.join("\n\n");
|
||||||
|
void navigator.clipboard.writeText(text);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Copy className="mr-2 h-3.5 w-3.5" />
|
||||||
|
Copy message
|
||||||
|
</DropdownMenuItem>
|
||||||
|
{runHref ? (
|
||||||
<DropdownMenuItem asChild>
|
<DropdownMenuItem asChild>
|
||||||
<Link to={runHref}>View run</Link>
|
<Link to={runHref}>View run</Link>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
) : null}
|
||||||
</DropdownMenu>
|
</DropdownMenuContent>
|
||||||
) : null}
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -830,38 +842,38 @@ function IssueChatSystemMessage() {
|
||||||
const agentIcon = isAgent && actorId ? agentMap?.get(actorId)?.icon : undefined;
|
const agentIcon = isAgent && actorId ? agentMap?.get(actorId)?.icon : undefined;
|
||||||
|
|
||||||
const eventContent = (
|
const eventContent = (
|
||||||
<div className="min-w-0 space-y-1.5">
|
<div className="min-w-0 space-y-1">
|
||||||
<div className={cn("flex flex-wrap items-baseline gap-x-1.5 gap-y-1 text-sm", isCurrentUser && "justify-end")}>
|
<div className={cn("flex flex-wrap items-baseline gap-x-1.5 gap-y-0.5 text-xs", isCurrentUser && "justify-end")}>
|
||||||
<span className="font-medium text-foreground">{actorName}</span>
|
<span className="font-medium text-foreground">{actorName}</span>
|
||||||
<span className="text-muted-foreground">updated this task</span>
|
<span className="text-muted-foreground">updated this task</span>
|
||||||
<a
|
<a
|
||||||
href={anchorId ? `#${anchorId}` : undefined}
|
href={anchorId ? `#${anchorId}` : undefined}
|
||||||
className="text-sm text-muted-foreground transition-colors hover:text-foreground hover:underline"
|
className="text-xs text-muted-foreground transition-colors hover:text-foreground hover:underline"
|
||||||
>
|
>
|
||||||
{timeAgo(message.createdAt)}
|
{timeAgo(message.createdAt)}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{statusChange ? (
|
{statusChange ? (
|
||||||
<div className={cn("flex flex-wrap items-center gap-2 text-sm", isCurrentUser && "justify-end")}>
|
<div className={cn("flex flex-wrap items-center gap-1.5 text-xs", isCurrentUser && "justify-end")}>
|
||||||
<span className="text-[10px] font-medium uppercase tracking-[0.14em] text-muted-foreground">
|
<span className="text-[9px] font-medium uppercase tracking-[0.14em] text-muted-foreground">
|
||||||
Status
|
Status
|
||||||
</span>
|
</span>
|
||||||
<span className="text-muted-foreground">{humanizeValue(statusChange.from)}</span>
|
<span className="text-muted-foreground">{humanizeValue(statusChange.from)}</span>
|
||||||
<ArrowRight className="h-3.5 w-3.5 text-muted-foreground" />
|
<ArrowRight className="h-3 w-3 text-muted-foreground" />
|
||||||
<span className="font-medium text-foreground">{humanizeValue(statusChange.to)}</span>
|
<span className="font-medium text-foreground">{humanizeValue(statusChange.to)}</span>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{assigneeChange ? (
|
{assigneeChange ? (
|
||||||
<div className={cn("flex flex-wrap items-center gap-2 text-sm", isCurrentUser && "justify-end")}>
|
<div className={cn("flex flex-wrap items-center gap-1.5 text-xs", isCurrentUser && "justify-end")}>
|
||||||
<span className="text-[10px] font-medium uppercase tracking-[0.14em] text-muted-foreground">
|
<span className="text-[9px] font-medium uppercase tracking-[0.14em] text-muted-foreground">
|
||||||
Assignee
|
Assignee
|
||||||
</span>
|
</span>
|
||||||
<span className="text-muted-foreground">
|
<span className="text-muted-foreground">
|
||||||
{formatTimelineAssigneeLabel(assigneeChange.from, agentMap, currentUserId)}
|
{formatTimelineAssigneeLabel(assigneeChange.from, agentMap, currentUserId)}
|
||||||
</span>
|
</span>
|
||||||
<ArrowRight className="h-3.5 w-3.5 text-muted-foreground" />
|
<ArrowRight className="h-3 w-3 text-muted-foreground" />
|
||||||
<span className="font-medium text-foreground">
|
<span className="font-medium text-foreground">
|
||||||
{formatTimelineAssigneeLabel(assigneeChange.to, agentMap, currentUserId)}
|
{formatTimelineAssigneeLabel(assigneeChange.to, agentMap, currentUserId)}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -873,7 +885,7 @@ function IssueChatSystemMessage() {
|
||||||
if (isCurrentUser) {
|
if (isCurrentUser) {
|
||||||
return (
|
return (
|
||||||
<MessagePrimitive.Root id={anchorId}>
|
<MessagePrimitive.Root id={anchorId}>
|
||||||
<div className="flex items-start justify-end gap-2.5 py-1.5">
|
<div className="flex items-start justify-end gap-2 py-1">
|
||||||
{eventContent}
|
{eventContent}
|
||||||
</div>
|
</div>
|
||||||
</MessagePrimitive.Root>
|
</MessagePrimitive.Root>
|
||||||
|
|
@ -882,12 +894,12 @@ function IssueChatSystemMessage() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MessagePrimitive.Root id={anchorId}>
|
<MessagePrimitive.Root id={anchorId}>
|
||||||
<div className="flex items-start gap-2.5 py-1.5">
|
<div className="flex items-start gap-2 py-1">
|
||||||
<Avatar size="sm" className="mt-0.5">
|
<Avatar size="xs" className="mt-0.5">
|
||||||
{agentIcon ? (
|
{agentIcon ? (
|
||||||
<AvatarFallback><AgentIcon icon={agentIcon} className="h-3.5 w-3.5" /></AvatarFallback>
|
<AvatarFallback><AgentIcon icon={agentIcon} className="h-3 w-3" /></AvatarFallback>
|
||||||
) : (
|
) : (
|
||||||
<AvatarFallback>{initialsForName(actorName)}</AvatarFallback>
|
<AvatarFallback className="text-[9px]">{initialsForName(actorName)}</AvatarFallback>
|
||||||
)}
|
)}
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
|
|
@ -903,24 +915,24 @@ function IssueChatSystemMessage() {
|
||||||
if (custom.kind === "run" && runId && runAgentId && displayedRunAgentName && runStatus) {
|
if (custom.kind === "run" && runId && runAgentId && displayedRunAgentName && runStatus) {
|
||||||
return (
|
return (
|
||||||
<MessagePrimitive.Root id={anchorId}>
|
<MessagePrimitive.Root id={anchorId}>
|
||||||
<div className="flex items-center gap-2.5 py-1.5">
|
<div className="flex items-center gap-2 py-1">
|
||||||
<Avatar size="sm">
|
<Avatar size="xs">
|
||||||
{runAgentIcon ? (
|
{runAgentIcon ? (
|
||||||
<AvatarFallback><AgentIcon icon={runAgentIcon} className="h-3.5 w-3.5" /></AvatarFallback>
|
<AvatarFallback><AgentIcon icon={runAgentIcon} className="h-3 w-3" /></AvatarFallback>
|
||||||
) : (
|
) : (
|
||||||
<AvatarFallback>{initialsForName(displayedRunAgentName)}</AvatarFallback>
|
<AvatarFallback className="text-[9px]">{initialsForName(displayedRunAgentName)}</AvatarFallback>
|
||||||
)}
|
)}
|
||||||
</Avatar>
|
</Avatar>
|
||||||
|
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<div className="flex flex-wrap items-center gap-x-1.5 gap-y-1 text-sm">
|
<div className="flex flex-wrap items-center gap-x-1.5 gap-y-0.5 text-xs">
|
||||||
<Link to={`/agents/${runAgentId}`} className="font-medium text-foreground transition-colors hover:underline">
|
<Link to={`/agents/${runAgentId}`} className="font-medium text-foreground transition-colors hover:underline">
|
||||||
{displayedRunAgentName}
|
{displayedRunAgentName}
|
||||||
</Link>
|
</Link>
|
||||||
<span className="text-muted-foreground">run</span>
|
<span className="text-muted-foreground">run</span>
|
||||||
<Link
|
<Link
|
||||||
to={`/agents/${runAgentId}/runs/${runId}`}
|
to={`/agents/${runAgentId}/runs/${runId}`}
|
||||||
className="inline-flex items-center rounded-md border border-border bg-accent/40 px-2 py-1 font-mono text-xs text-muted-foreground transition-colors hover:bg-accent/60 hover:text-foreground"
|
className="inline-flex items-center rounded-md border border-border bg-accent/40 px-1.5 py-0.5 font-mono text-[10px] text-muted-foreground transition-colors hover:bg-accent/60 hover:text-foreground"
|
||||||
>
|
>
|
||||||
{runId.slice(0, 8)}
|
{runId.slice(0, 8)}
|
||||||
</Link>
|
</Link>
|
||||||
|
|
@ -929,7 +941,7 @@ function IssueChatSystemMessage() {
|
||||||
</span>
|
</span>
|
||||||
<a
|
<a
|
||||||
href={anchorId ? `#${anchorId}` : undefined}
|
href={anchorId ? `#${anchorId}` : undefined}
|
||||||
className="text-sm text-muted-foreground transition-colors hover:text-foreground hover:underline"
|
className="text-xs text-muted-foreground transition-colors hover:text-foreground hover:underline"
|
||||||
>
|
>
|
||||||
{timeAgo(message.createdAt)}
|
{timeAgo(message.createdAt)}
|
||||||
</a>
|
</a>
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ describe("assignee selection helpers", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("formats current and board user labels consistently", () => {
|
it("formats current and board user labels consistently", () => {
|
||||||
expect(formatAssigneeUserLabel("user-1", "user-1")).toBe("Me");
|
expect(formatAssigneeUserLabel("user-1", "user-1")).toBe("You");
|
||||||
expect(formatAssigneeUserLabel("local-board", "someone-else")).toBe("Board");
|
expect(formatAssigneeUserLabel("local-board", "someone-else")).toBe("Board");
|
||||||
expect(formatAssigneeUserLabel("user-abcdef", "someone-else")).toBe("user-");
|
expect(formatAssigneeUserLabel("user-abcdef", "someone-else")).toBe("user-");
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,7 @@ export function formatAssigneeUserLabel(
|
||||||
currentUserId: string | null | undefined,
|
currentUserId: string | null | undefined,
|
||||||
): string | null {
|
): string | null {
|
||||||
if (!userId) return null;
|
if (!userId) return null;
|
||||||
if (currentUserId && userId === currentUserId) return "Me";
|
if (currentUserId && userId === currentUserId) return "You";
|
||||||
if (userId === "local-board") return "Board";
|
if (userId === "local-board") return "Board";
|
||||||
return userId.slice(0, 5);
|
return userId.slice(0, 5);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -485,6 +485,7 @@ export function buildIssueChatMessages(args: {
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const run of [...linkedRuns].sort((a, b) => toTimestamp(runTimestamp(a)) - toTimestamp(runTimestamp(b)))) {
|
for (const run of [...linkedRuns].sort((a, b) => toTimestamp(runTimestamp(a)) - toTimestamp(runTimestamp(b)))) {
|
||||||
|
if (run.status === "succeeded") continue;
|
||||||
orderedMessages.push({
|
orderedMessages.push({
|
||||||
createdAtMs: toTimestamp(runTimestamp(run)),
|
createdAtMs: toTimestamp(runTimestamp(run)),
|
||||||
order: 2,
|
order: 2,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue