diff --git a/frontend/src/app/activity/page.tsx b/frontend/src/app/activity/page.tsx index f4a65c26..3a382956 100644 --- a/frontend/src/app/activity/page.tsx +++ b/frontend/src/app/activity/page.tsx @@ -35,6 +35,11 @@ import { SignedOutPanel } from "@/components/auth/SignedOutPanel"; import { DashboardSidebar } from "@/components/organisms/DashboardSidebar"; import { DashboardShell } from "@/components/templates/DashboardShell"; import { createExponentialBackoff } from "@/lib/backoff"; +import { + DEFAULT_HUMAN_LABEL, + resolveHumanActorName, + resolveMemberDisplayName, +} from "@/lib/display-name"; import { apiDatetimeToMs, parseApiDatetime } from "@/lib/datetime"; import { cn } from "@/lib/utils"; import { usePageActive } from "@/hooks/usePageActive"; @@ -315,6 +320,11 @@ export default function ActivityPage() { membershipQuery.data?.status === 200 ? membershipQuery.data.data : null; return member ? ["owner", "admin"].includes(member.role) : false; }, [membershipQuery.data]); + const currentUserDisplayName = useMemo(() => { + const member = + membershipQuery.data?.status === 200 ? membershipQuery.data.data : null; + return resolveMemberDisplayName(member, DEFAULT_HUMAN_LABEL); + }, [membershipQuery.data]); const [isFeedLoading, setIsFeedLoading] = useState(false); const [feedError, setFeedError] = useState(null); @@ -344,7 +354,10 @@ export default function ActivityPage() { }, []); const resolveAuthor = useCallback( - (agentId: string | null | undefined, fallbackName = "Admin") => { + ( + agentId: string | null | undefined, + fallbackName: string = currentUserDisplayName, + ) => { if (agentId) { const agent = agentsByIdRef.current.get(agentId); if (agent) { @@ -361,7 +374,7 @@ export default function ActivityPage() { role: null, }; }, - [], + [currentUserDisplayName], ); const boardNameForId = useCallback((boardId: string | null | undefined) => { @@ -390,7 +403,7 @@ export default function ActivityPage() { ? taskMetaByIdRef.current.get(event.task_id) : null; const boardId = meta?.boardId ?? null; - const author = resolveAuthor(event.agent_id, "Admin"); + const author = resolveAuthor(event.agent_id, currentUserDisplayName); return { id: `activity:${event.id}`, created_at: event.created_at, @@ -407,7 +420,7 @@ export default function ActivityPage() { meta?.title ?? (event.task_id ? "Unknown task" : "Task activity"), }; }, - [boardNameForId, resolveAuthor], + [boardNameForId, currentUserDisplayName, resolveAuthor], ); const mapTaskComment = useCallback( @@ -416,7 +429,7 @@ export default function ActivityPage() { ? taskMetaByIdRef.current.get(comment.task_id) : null; const boardId = meta?.boardId ?? fallbackBoardId; - const author = resolveAuthor(comment.agent_id, "Admin"); + const author = resolveAuthor(comment.agent_id, currentUserDisplayName); return { id: `comment:${comment.id}`, created_at: comment.created_at, @@ -433,7 +446,7 @@ export default function ActivityPage() { meta?.title ?? (comment.task_id ? "Unknown task" : "Task activity"), }; }, - [boardNameForId, resolveAuthor], + [boardNameForId, currentUserDisplayName, resolveAuthor], ); const mapApprovalEvent = useCallback( @@ -464,7 +477,7 @@ export default function ActivityPage() { ? approval.created_at : (approval.resolved_at ?? approval.created_at); const action = humanizeApprovalAction(approval.action_type); - const author = resolveAuthor(approval.agent_id, "Admin"); + const author = resolveAuthor(approval.agent_id, currentUserDisplayName); const statusText = nextStatus === "approved" ? "approved" @@ -499,13 +512,16 @@ export default function ActivityPage() { title: `Approval ยท ${action}`, }; }, - [boardNameForId, resolveAuthor], + [boardNameForId, currentUserDisplayName, resolveAuthor], ); const mapBoardChat = useCallback( (memory: BoardMemoryRead, boardId: string): FeedItem => { const content = (memory.content ?? "").trim(); - const actorName = (memory.source ?? "User").trim() || "User"; + const actorName = resolveHumanActorName( + memory.source, + currentUserDisplayName, + ); const command = content.startsWith("/"); return { id: `chat:${memory.id}`, @@ -522,7 +538,7 @@ export default function ActivityPage() { title: command ? "Board command" : "Board chat", }; }, - [boardNameForId], + [boardNameForId, currentUserDisplayName], ); const mapAgentEvent = useCallback( diff --git a/frontend/src/app/boards/[boardId]/page.tsx b/frontend/src/app/boards/[boardId]/page.tsx index ae12250d..0836ec89 100644 --- a/frontend/src/app/boards/[boardId]/page.tsx +++ b/frontend/src/app/boards/[boardId]/page.tsx @@ -114,6 +114,11 @@ import { parseApiDatetime, toLocalDateInput, } from "@/lib/datetime"; +import { + DEFAULT_HUMAN_LABEL, + resolveHumanActorName, + resolveMemberDisplayName, +} from "@/lib/display-name"; import { cn } from "@/lib/utils"; import { usePageActive } from "@/hooks/usePageActive"; import { @@ -240,7 +245,7 @@ const toLiveFeedFromComment = (comment: TaskCommentRead): LiveFeedItem => ({ const toLiveFeedFromBoardChat = (memory: BoardChatMessage): LiveFeedItem => { const content = (memory.content ?? "").trim(); - const actorName = (memory.source ?? "User").trim() || "User"; + const actorName = resolveHumanActorName(memory.source, DEFAULT_HUMAN_LABEL); const isCommand = content.startsWith("/"); return { id: `chat:${memory.id}`, @@ -591,15 +596,16 @@ TaskCommentCard.displayName = "TaskCommentCard"; const ChatMessageCard = memo(function ChatMessageCard({ message, + fallbackSource, }: { message: BoardChatMessage; + fallbackSource: string; }) { + const sourceLabel = resolveHumanActorName(message.source, fallbackSource); return (
-

- {message.source ?? "User"} -

+

{sourceLabel}

{formatShortTimestamp(message.created_at)} @@ -776,6 +782,11 @@ export default function BoardDetailPage() { membershipQuery.data?.status === 200 ? membershipQuery.data.data : null; return member ? ["owner", "admin"].includes(member.role) : false; }, [membershipQuery.data]); + const currentUserDisplayName = useMemo(() => { + const member = + membershipQuery.data?.status === 200 ? membershipQuery.data.data : null; + return resolveMemberDisplayName(member, DEFAULT_HUMAN_LABEL); + }, [membershipQuery.data]); const canWrite = boardAccess.canWrite; const [board, setBoard] = useState(null); @@ -2002,6 +2013,7 @@ export default function BoardDetailPage() { { content: trimmed, tags: ["chat"], + source: currentUserDisplayName, }, ); if (result.status !== 200) { @@ -2028,7 +2040,7 @@ export default function BoardDetailPage() { return { ok: false, error: message }; } }, - [boardId, isSignedIn, pushLiveFeed], + [boardId, currentUserDisplayName, isSignedIn, pushLiveFeed], ); const handleSendChat = useCallback( @@ -3869,7 +3881,7 @@ export default function BoardDetailPage() { authorLabel={ comment.agent_id ? (assigneeById.get(comment.agent_id) ?? "Agent") - : "Admin" + : currentUserDisplayName } /> ))} @@ -3918,7 +3930,11 @@ export default function BoardDetailPage() {

) : ( chatMessages.map((message) => ( - + )) )}
@@ -3983,8 +3999,11 @@ export default function BoardDetailPage() { null) : null; const authorName = - item.actor_name?.trim() || - (authorAgent ? authorAgent.name : "Admin"); + authorAgent?.name ?? + resolveHumanActorName( + item.actor_name, + currentUserDisplayName, + ); const authorRole = authorAgent ? agentRoleLabel(authorAgent) : null; diff --git a/frontend/src/lib/display-name.test.ts b/frontend/src/lib/display-name.test.ts new file mode 100644 index 00000000..1cd9c434 --- /dev/null +++ b/frontend/src/lib/display-name.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, it } from "vitest"; + +import type { OrganizationMemberRead } from "@/api/generated/model"; +import { + DEFAULT_HUMAN_LABEL, + normalizeDisplayName, + resolveHumanActorName, + resolveMemberDisplayName, +} from "./display-name"; + +const memberWithUser = (user: { + preferred_name?: string | null; + name?: string | null; +}): OrganizationMemberRead => + ({ + user, + }) as OrganizationMemberRead; + +describe("display-name", () => { + it("normalizes empty strings to null", () => { + expect(normalizeDisplayName(" ")).toBeNull(); + expect(normalizeDisplayName(" Abhimanyu ")).toBe("Abhimanyu"); + }); + + it("resolves generic labels to fallback names", () => { + expect(resolveHumanActorName("Admin", "Abhimanyu")).toBe("Abhimanyu"); + expect(resolveHumanActorName(" user ", "Abhimanyu")).toBe("Abhimanyu"); + }); + + it("keeps explicit non-generic actor labels", () => { + expect(resolveHumanActorName("Abhimanyu", "User")).toBe("Abhimanyu"); + }); + + it("prefers membership preferred_name over name", () => { + const member = memberWithUser({ + preferred_name: "Abhimanyu", + name: "Admin", + }); + expect(resolveMemberDisplayName(member)).toBe("Abhimanyu"); + }); + + it("falls back to membership name when preferred_name missing", () => { + const member = memberWithUser({ + preferred_name: null, + name: "Abhimanyu", + }); + expect(resolveMemberDisplayName(member)).toBe("Abhimanyu"); + }); + + it("returns default user label when member names are unavailable", () => { + expect(resolveMemberDisplayName(null)).toBe(DEFAULT_HUMAN_LABEL); + expect(resolveMemberDisplayName(memberWithUser({ name: "Admin" }))).toBe( + DEFAULT_HUMAN_LABEL, + ); + }); +}); diff --git a/frontend/src/lib/display-name.ts b/frontend/src/lib/display-name.ts new file mode 100644 index 00000000..9d79dde2 --- /dev/null +++ b/frontend/src/lib/display-name.ts @@ -0,0 +1,33 @@ +import type { OrganizationMemberRead } from "@/api/generated/model"; + +export const DEFAULT_HUMAN_LABEL = "User"; + +export const normalizeDisplayName = ( + value: string | null | undefined, +): string | null => { + const trimmed = (value ?? "").trim(); + return trimmed.length > 0 ? trimmed : null; +}; + +export const resolveHumanActorName = ( + value: string | null | undefined, + fallbackName: string = DEFAULT_HUMAN_LABEL, +): string => { + const normalized = normalizeDisplayName(value); + if (!normalized) return fallbackName; + const lowered = normalized.toLowerCase(); + if (lowered === "admin" || lowered === "user") { + return fallbackName; + } + return normalized; +}; + +export const resolveMemberDisplayName = ( + member: OrganizationMemberRead | null | undefined, + fallbackName: string = DEFAULT_HUMAN_LABEL, +): string => + resolveHumanActorName( + normalizeDisplayName(member?.user?.preferred_name) ?? + normalizeDisplayName(member?.user?.name), + fallbackName, + );