fix(ui): task-chat agent header uses the agent's assigned icon avatar

Revert the textless gradient capsule in TaskChatBubble to the legacy
Avatar + AgentIcon treatment: the adapter now maps the author agent's
assigned icon (agentMap icon) onto the message item, with an initials
fallback when no icon is set.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
scotttongandClaude Fable 5 committed 2026-08-07 16:55:49 -07:00
1 parent 111f7bac6a
commit f56a0d4b97
4 files changed
+26 -33

No files matched your search

+19 -16
View File
@@ -1,26 +1,26 @@
import { cn } from "@/lib/utils";
import { MarkdownBody } from "@/components/MarkdownBody";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { AgentIcon } from "@/components/AgentIconPicker";
import type { TaskChatMessageItem } from "./task-chat-model";
interface TaskChatBubbleProps {
item: TaskChatMessageItem;
}
/** Brand agent-gradient count (mirrors AgentCapsule's token pairs in index.css). */
const GRADIENT_COUNT = 10;
function capsuleGradient(index: number | undefined): string {
const n = Math.trunc(index ?? 1);
const idx = ((((n - 1) % GRADIENT_COUNT) + GRADIENT_COUNT) % GRADIENT_COUNT) + 1;
return `linear-gradient(to bottom, var(--agent-${idx}a), var(--agent-${idx}b))`;
function initialsForName(name: string) {
const parts = name.trim().split(/\s+/);
if (parts.length >= 2) {
return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
}
return name.slice(0, 2).toUpperCase();
}
/**
* Author-typed message row — the primary legibility signal. Human messages sit
* right in a solid accent bubble; agent messages sit left in a neutral card
* bubble with a capsule-avatar author header (textless vertical brand-gradient
* capsule + name · mode chip, per the v3/v6 prototype decisions); system
* notices are centered and recede.
* bubble with an avatar author header (the agent's assigned icon + name · mode
* chip); system notices are centered and recede.
*/
export function TaskChatBubble({ item }: TaskChatBubbleProps) {
if (item.author === "system") {
@@ -36,12 +36,15 @@ export function TaskChatBubble({ item }: TaskChatBubbleProps) {
<div className={cn("tc-enter-bubble group flex w-full flex-col gap-1", isHuman ? "items-end" : "items-start")}>
{!isHuman && item.authorName ? (
<span className="flex items-center gap-2 px-1">
<span
aria-hidden="true"
data-testid="task-chat-agent-capsule"
className="inline-block h-6 w-2.5 shrink-0 rounded-full"
style={{ background: capsuleGradient(item.agentGradient) }}
/>
<Avatar size="sm" className="shrink-0" data-testid="task-chat-agent-avatar">
{item.agentIcon ? (
<AvatarFallback>
<AgentIcon icon={item.agentIcon} className="h-3.5 w-3.5" />
</AvatarFallback>
) : (
<AvatarFallback>{initialsForName(item.authorName)}</AvatarFallback>
)}
</Avatar>
<span className="text-sm font-semibold text-foreground">{item.authorName}</span>
{item.modeLabel ? (
<span className="rounded-full border border-border px-2 py-px text-(length:--text-micro) font-medium text-muted-foreground">
@@ -27,16 +27,6 @@ function effectiveAgentId(comment: IssueChatComment): string | null {
return comment.authorAgentId ?? comment.derivedAuthorAgentId ?? null;
}
/**
* Deterministic agent-id → brand gradient index (1–10) for the capsule avatar,
* so a given agent always renders the same capsule colors.
*/
export function agentGradientIndex(agentId: string): number {
let hash = 0;
for (let i = 0; i < agentId.length; i++) hash = (hash * 31 + agentId.charCodeAt(i)) | 0;
return (Math.abs(hash) % 10) + 1;
}
function authorKind(comment: IssueChatComment): TaskChatAuthorKind {
if (effectiveAgentId(comment)) return "agent";
if (comment.authorType === "user") return "human";
@@ -60,11 +50,11 @@ export function commentsToTaskChatItems(
if (comment.deletedAt) continue;
const kind = authorKind(comment);
let authorName: string | undefined;
let agentGradient: number | undefined;
let agentIcon: string | null | undefined;
if (kind === "agent") {
const agentId = effectiveAgentId(comment);
authorName = (agentId && ctx.agentMap?.get(agentId)?.name) || "Agent";
agentGradient = agentId ? agentGradientIndex(agentId) : undefined;
agentIcon = agentId ? ctx.agentMap?.get(agentId)?.icon : undefined;
} else if (kind === "human") {
authorName =
(comment.authorUserId && ctx.userLabelMap?.get(comment.authorUserId)) || undefined;
@@ -83,7 +73,7 @@ export function commentsToTaskChatItems(
text: comment.body,
timestamp: formatTimestamp(comment.createdAt),
optimistic,
agentGradient,
agentIcon,
modeLabel: kind === "agent" ? ctx.agentModeLabel : undefined,
});
}
@@ -50,7 +50,7 @@ export function buildScenario(id: TaskChatStateId): TaskChatScenario {
surface: "thread",
items: [
...exchangePrefix(),
{ id: "m-agent-1", kind: "message", author: "agent", authorName: AGENT, agentGradient: 3, modeLabel: "Agent mode", text: "On it — I'll add a token-bucket limiter and wire it into the login route.", timestamp: "2:31 PM" },
{ id: "m-agent-1", kind: "message", author: "agent", authorName: AGENT, agentIcon: "bot", modeLabel: "Agent mode", text: "On it — I'll add a token-bucket limiter and wire it into the login route.", timestamp: "2:31 PM" },
],
};
case "thinking":
@@ -125,7 +125,7 @@ export function buildScenario(id: TaskChatStateId): TaskChatScenario {
{ id: "tool-done-2", kind: "tool", name: "Edit", target: "server/src/routes/auth.ts", toolKind: "edit", status: "completed", diff: { path: "server/src/routes/auth.ts", added: 34, removed: 3 } },
],
},
{ id: "m-done", kind: "message", author: "agent", authorName: AGENT, agentGradient: 3, modeLabel: "Agent mode", text: "Done — added a per-account token-bucket limiter and wired it into the login route. Tests pass.", timestamp: "2:34 PM" },
{ id: "m-done", kind: "message", author: "agent", authorName: AGENT, agentIcon: "bot", modeLabel: "Agent mode", text: "Done — added a per-account token-bucket limiter and wired it into the login route. Tests pass.", timestamp: "2:34 PM" },
],
};
case "awaiting-approval":
@@ -74,8 +74,8 @@ export interface TaskChatMessageItem {
* chip in the agent header and under a sent human bubble (v6 decision).
*/
modeLabel?: string;
/** Brand agent-gradient index (1–10) for the capsule avatar header. */
agentGradient?: number;
/** Assigned agent icon name (AgentIconName) for the avatar header. */
agentIcon?: string | null;
}
/** Collapsed chain-of-thought (ACP agent_thought_chunk). */