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:
dotta 2026-04-06 16:34:56 -05:00
parent 94652c6079
commit f94fe57d10
4 changed files with 55 additions and 42 deletions

View file

@ -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>

View file

@ -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-");
}); });

View file

@ -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);
} }

View file

@ -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,