From 6427ad5febfc58a5dfb5f5d2a634e267450b9e0e Mon Sep 17 00:00:00 2001 From: Dotta Date: Wed, 15 Apr 2026 21:23:48 -0500 Subject: [PATCH] Fix multi-user issue chat identities --- ui/src/components/IssueChatThread.test.tsx | 36 ++- ui/src/components/IssueChatThread.tsx | 254 +++++++++++++-------- ui/src/lib/issue-chat-messages.test.ts | 21 ++ ui/src/lib/issue-chat-messages.ts | 6 +- ui/src/pages/IssueDetail.tsx | 4 + 5 files changed, 227 insertions(+), 94 deletions(-) diff --git a/ui/src/components/IssueChatThread.test.tsx b/ui/src/components/IssueChatThread.test.tsx index dbddfacb3da..13f237c6274 100644 --- a/ui/src/components/IssueChatThread.test.tsx +++ b/ui/src/components/IssueChatThread.test.tsx @@ -5,7 +5,12 @@ import type { ReactNode } from "react"; import { createRoot } from "react-dom/client"; import { MemoryRouter } from "react-router-dom"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { IssueChatThread, canStopIssueChatRun, resolveAssistantMessageFoldedState } from "./IssueChatThread"; +import { + IssueChatThread, + canStopIssueChatRun, + resolveAssistantMessageFoldedState, + resolveIssueChatHumanAuthor, +} from "./IssueChatThread"; const { markdownEditorFocusMock } = vi.hoisted(() => ({ markdownEditorFocusMock: vi.fn(), @@ -661,4 +666,33 @@ describe("IssueChatThread", () => { activeRunIds: new Set(), })).toBe(false); }); + + it("uses company profile data to distinguish the current user from other humans", () => { + const userProfileMap = new Map([ + ["user-1", { label: "Dotta", image: "/avatars/dotta.png" }], + ["user-2", { label: "Alice", image: "/avatars/alice.png" }], + ]); + + expect(resolveIssueChatHumanAuthor({ + authorName: "You", + authorUserId: "user-1", + currentUserId: "user-1", + userProfileMap, + })).toEqual({ + isCurrentUser: true, + authorName: "Dotta", + avatarUrl: "/avatars/dotta.png", + }); + + expect(resolveIssueChatHumanAuthor({ + authorName: "Alice", + authorUserId: "user-2", + currentUserId: "user-1", + userProfileMap, + })).toEqual({ + isCurrentUser: false, + authorName: "Alice", + avatarUrl: "/avatars/alice.png", + }); + }); }); diff --git a/ui/src/components/IssueChatThread.tsx b/ui/src/components/IssueChatThread.tsx index 5c929162248..0208bb2ea3f 100644 --- a/ui/src/components/IssueChatThread.tsx +++ b/ui/src/components/IssueChatThread.tsx @@ -47,7 +47,7 @@ import { import { resolveIssueChatTranscriptRuns } from "../lib/issueChatTranscriptRuns"; import type { IssueTimelineAssignee, IssueTimelineEvent } from "../lib/issue-timeline-events"; import { Button } from "@/components/ui/button"; -import { Avatar, AvatarFallback } from "@/components/ui/avatar"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Dialog, DialogContent, @@ -74,6 +74,7 @@ import { shouldPreserveComposerViewport, } from "../lib/issue-chat-scroll"; import { formatAssigneeUserLabel } from "../lib/assignees"; +import type { CompanyUserProfile } from "../lib/company-members"; import { timeAgo } from "../lib/timeAgo"; import { describeToolInput, @@ -97,6 +98,7 @@ interface IssueChatMessageContext { agentMap?: Map; currentUserId?: string | null; userLabelMap?: ReadonlyMap | null; + userProfileMap?: ReadonlyMap | null; activeRunIds: ReadonlySet; onVote?: ( commentId: string, @@ -219,6 +221,7 @@ interface IssueChatThreadProps { agentMap?: Map; currentUserId?: string | null; userLabelMap?: ReadonlyMap | null; + userProfileMap?: ReadonlyMap | null; onVote?: ( commentId: string, vote: FeedbackVoteValue, @@ -498,6 +501,26 @@ function initialsForName(name: string) { return name.slice(0, 2).toUpperCase(); } +export function resolveIssueChatHumanAuthor(args: { + authorName?: string | null; + authorUserId?: string | null; + currentUserId?: string | null; + userProfileMap?: ReadonlyMap | null; +}) { + const { authorName, authorUserId, currentUserId, userProfileMap } = args; + const profile = authorUserId ? userProfileMap?.get(authorUserId) ?? null : null; + const isCurrentUser = Boolean(authorUserId && currentUserId && authorUserId === currentUserId); + const resolvedAuthorName = profile?.label?.trim() + || authorName?.trim() + || (authorUserId === "local-board" ? "Board" : (isCurrentUser ? "You" : "User")); + + return { + isCurrentUser, + authorName: resolvedAuthorName, + avatarUrl: profile?.image ?? null, + }; +} + function formatRunStatusLabel(status: string) { switch (status) { case "timed_out": @@ -909,108 +932,152 @@ function IssueChatToolPart({ } function IssueChatUserMessage() { - const { onInterruptQueued, onCancelQueued, interruptingQueuedRunId } = useContext(IssueChatCtx); + const { + onInterruptQueued, + onCancelQueued, + interruptingQueuedRunId, + currentUserId, + userProfileMap, + } = useContext(IssueChatCtx); const message = useMessage(); const custom = message.metadata.custom as Record; const anchorId = typeof custom.anchorId === "string" ? custom.anchorId : undefined; const commentId = typeof custom.commentId === "string" ? custom.commentId : message.id; + const authorName = typeof custom.authorName === "string" ? custom.authorName : null; + const authorUserId = typeof custom.authorUserId === "string" ? custom.authorUserId : null; const queued = custom.queueState === "queued" || custom.clientStatus === "queued"; const pending = custom.clientStatus === "pending"; const queueTargetRunId = typeof custom.queueTargetRunId === "string" ? custom.queueTargetRunId : null; const [copied, setCopied] = useState(false); + const { + isCurrentUser, + authorName: resolvedAuthorName, + avatarUrl, + } = resolveIssueChatHumanAuthor({ + authorName, + authorUserId, + currentUserId, + userProfileMap, + }); + const authorAvatar = ( + + {avatarUrl ? : null} + {initialsForName(resolvedAuthorName)} + + ); + const messageBody = ( +
+
+ {resolvedAuthorName} +
+ +
+ {queued ? ( +
+ + Queued + + {queueTargetRunId && onInterruptQueued ? ( + + ) : null} + {onCancelQueued ? ( + + ) : null} +
+ ) : null} +
+ , + }} + /> +
+
+ + {pending ? ( +
+ Sending... +
+ ) : ( +
+ + + + {message.createdAt ? commentDateLabel(message.createdAt) : ""} + + + + {message.createdAt ? formatDateTime(message.createdAt) : ""} + + + +
+ )} +
+ ); return ( -
-
-
- {queued ? ( -
- - Queued - - {queueTargetRunId && onInterruptQueued ? ( - - ) : null} - {onCancelQueued ? ( - - ) : null} -
- ) : null} -
- , - }} - /> -
-
- - {pending ? ( -
Sending...
- ) : ( -
- - - - {message.createdAt ? commentDateLabel(message.createdAt) : ""} - - - - {message.createdAt ? formatDateTime(message.createdAt) : ""} - - - -
- )} -
- - - You - +
+ {isCurrentUser ? ( + <> + {messageBody} + {authorAvatar} + + ) : ( + <> + {authorAvatar} + {messageBody} + + )}
); @@ -1859,6 +1926,7 @@ export function IssueChatThread({ agentMap, currentUserId, userLabelMap, + userProfileMap, onVote, onAdd, onCancelRun, @@ -2035,6 +2103,7 @@ export function IssueChatThread({ agentMap, currentUserId, userLabelMap, + userProfileMap, activeRunIds, onVote, onStopRun, @@ -2051,6 +2120,7 @@ export function IssueChatThread({ agentMap, currentUserId, userLabelMap, + userProfileMap, activeRunIds, onVote, onStopRun, diff --git a/ui/src/lib/issue-chat-messages.test.ts b/ui/src/lib/issue-chat-messages.test.ts index c79759f4a7b..7bd64723550 100644 --- a/ui/src/lib/issue-chat-messages.test.ts +++ b/ui/src/lib/issue-chat-messages.test.ts @@ -234,6 +234,27 @@ describe("buildAssistantPartsFromTranscript", () => { }); describe("buildIssueChatMessages", () => { + it("uses the company user label for current-user comments instead of collapsing to You", () => { + const messages = buildIssueChatMessages({ + comments: [createComment({ authorUserId: "user-1" })], + timelineEvents: [], + linkedRuns: [], + liveRuns: [], + currentUserId: "user-1", + userLabelMap: new Map([["user-1", "Dotta"]]), + }); + + expect(messages[0]).toMatchObject({ + role: "user", + metadata: { + custom: { + authorName: "Dotta", + authorUserId: "user-1", + }, + }, + }); + }); + it("orders events before comments and appends active live runs as running assistant messages", () => { const agentMap = new Map([["agent-1", createAgent("agent-1", "CodexCoder")]]); const comments = [ diff --git a/ui/src/lib/issue-chat-messages.ts b/ui/src/lib/issue-chat-messages.ts index 0f9943989c9..5b52498716c 100644 --- a/ui/src/lib/issue-chat-messages.ts +++ b/ui/src/lib/issue-chat-messages.ts @@ -275,7 +275,11 @@ function authorNameForComment( if (comment.authorAgentId) { return agentMap?.get(comment.authorAgentId)?.name ?? comment.authorAgentId.slice(0, 8); } - return formatAssigneeUserLabel(comment.authorUserId ?? null, currentUserId, userLabelMap) ?? "You"; + const authorUserId = comment.authorUserId ?? null; + if (!authorUserId) return "You"; + const userLabel = userLabelMap?.get(authorUserId)?.trim(); + if (userLabel) return userLabel; + return formatAssigneeUserLabel(authorUserId, currentUserId, userLabelMap) ?? "You"; } function formatStatusLabel(status: string) { diff --git a/ui/src/pages/IssueDetail.tsx b/ui/src/pages/IssueDetail.tsx index 19856f309b4..2d435555445 100644 --- a/ui/src/pages/IssueDetail.tsx +++ b/ui/src/pages/IssueDetail.tsx @@ -504,6 +504,7 @@ type IssueDetailChatTabProps = { agentMap: Map; currentUserId: string | null; userLabelMap: ReadonlyMap | null; + userProfileMap: ReadonlyMap | null; draftKey: string; reassignOptions: Array<{ id: string; label: string; searchText?: string }>; currentAssigneeValue: string; @@ -538,6 +539,7 @@ function IssueDetailChatTab({ agentMap, currentUserId, userLabelMap, + userProfileMap, draftKey, reassignOptions, currentAssigneeValue, @@ -685,6 +687,7 @@ function IssueDetailChatTab({ agentMap={agentMap} currentUserId={currentUserId} userLabelMap={userLabelMap} + userProfileMap={userProfileMap} draftKey={draftKey} enableReassign reassignOptions={reassignOptions} @@ -2583,6 +2586,7 @@ export function IssueDetail() { agentMap={agentMap} currentUserId={currentUserId} userLabelMap={userLabelMap} + userProfileMap={userProfileMap} draftKey={`paperclip:issue-comment-draft:${issue.id}`} reassignOptions={commentReassignOptions} currentAssigneeValue={actualAssigneeValue}