import "./MailboxModal.css"; import { useState, useEffect, useCallback, useMemo, useRef, type CSSProperties } from "react"; import { X, Mail, Send, Inbox as InboxIcon, Bot, Trash2, CheckCheck, Loader2, RefreshCw, MessageSquare, User, ChevronRight, ChevronDown, } from "lucide-react"; import type { Message, MessageType, ParticipantType } from "@fusion/core"; import { fetchInbox, fetchOutbox, fetchUnreadCount, fetchAgentMailbox, fetchAllAgentMailbox, markMessageRead, markAllMessagesRead, deleteMessage, fetchConversation, fetchMessage, type InboxResponse, type OutboxResponse, type AgentMailboxResponse, type AllAgentsMailboxResponse, } from "../api"; import { MessageComposer } from "./MessageComposer"; import { MailboxMessageContent } from "./MailboxMessageContent"; import type { Agent } from "../api"; import { useMobileScrollLock } from "../hooks/useMobileScrollLock"; import { useMobileKeyboard } from "../hooks/useMobileKeyboard"; import { useViewportMode } from "./Header"; import { subscribeSse } from "../sse-bus"; import { readCache, SWR_CACHE_KEYS, writeCache } from "../utils/swrCache"; // ── Types ───────────────────────────────────────────────────────────────── type MailboxTab = "inbox" | "outbox" | "agents"; const ALL_AGENTS_MAILBOX_ID = "__all_agents__"; interface MailboxModalProps { isOpen: boolean; onClose: () => void; projectId?: string; addToast?: (msg: string, type?: "success" | "error") => void; agents?: Agent[]; } // ── Helpers ─────────────────────────────────────────────────────────────── function formatTimestamp(ts: string): string { const date = new Date(ts); const now = new Date(); const diffMs = now.getTime() - date.getTime(); const diffMins = Math.floor(diffMs / 60000); const diffHours = Math.floor(diffMs / 3600000); const diffDays = Math.floor(diffMs / 86400000); if (diffMins < 1) return "Just now"; if (diffMins < 60) return `${diffMins}m ago`; if (diffHours < 24) return `${diffHours}h ago`; if (diffDays < 7) return `${diffDays}d ago`; return date.toLocaleDateString(undefined, { month: "short", day: "numeric" }); } function participantLabel( id: string, type: ParticipantType, agentNamesById?: ReadonlyMap, ): string { if (type === "user") return id === "dashboard" ? "You" : `User: ${id}`; if (type === "agent") { const name = agentNamesById?.get(id)?.trim(); if (!name || name === id) return `Agent: ${id}`; return `Agent: ${name}`; } return "System"; } function messageTypeLabel(type: MessageType): string { switch (type) { case "agent-to-agent": return "Agent ↔ Agent"; case "agent-to-user": return "Agent → You"; case "user-to-agent": return "You → Agent"; case "system": return "System"; } } function messagePreview(content: string, max = 80): string { if (content.length <= max) return content; return `${content.slice(0, max)}…`; } function getDeepLinkedMessageId(): string | null { if (typeof window === "undefined") { return null; } const params = new URLSearchParams(window.location.search); const paramId = params.get("mailbox-message"); if (paramId) { return paramId; } const hashMatch = /^#message-(.+)$/.exec(window.location.hash); return hashMatch?.[1] ?? null; } function buildReplyThread(messages: Message[], selectedMessage: Message): Message[] { const allMessages = [...messages]; if (!allMessages.some((message) => message.id === selectedMessage.id)) { allMessages.push(selectedMessage); } const threadIds = new Set([selectedMessage.id]); let changed = true; while (changed) { changed = false; for (const message of allMessages) { const replyToId = message.metadata?.replyTo?.messageId; if (threadIds.has(message.id) && replyToId && !threadIds.has(replyToId)) { threadIds.add(replyToId); changed = true; } if (replyToId && threadIds.has(replyToId) && !threadIds.has(message.id)) { threadIds.add(message.id); changed = true; } } } return allMessages .filter((message) => threadIds.has(message.id)) .sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()); } // ── Component ───────────────────────────────────────────────────────────── export function MailboxModal({ isOpen, onClose, projectId, addToast, agents = [], }: MailboxModalProps) { const cacheSuffix = projectId ?? ""; const inboxCacheKey = `${SWR_CACHE_KEYS.MAILBOX_INBOX_PREFIX}${cacheSuffix}`; const outboxCacheKey = `${SWR_CACHE_KEYS.MAILBOX_OUTBOX_PREFIX}${cacheSuffix}`; const unreadCountCacheKey = `${SWR_CACHE_KEYS.MAILBOX_UNREAD_COUNT_PREFIX}${cacheSuffix}`; const initialInbox = readCache(inboxCacheKey); const initialOutbox = readCache(outboxCacheKey); const initialUnreadCount = readCache(unreadCountCacheKey); useMobileScrollLock(isOpen); const [activeTab, setActiveTab] = useState("inbox"); const [inbox, setInbox] = useState(() => initialInbox ?? null); const [outbox, setOutbox] = useState(() => initialOutbox ?? null); const [unreadCount, setUnreadCount] = useState(initialUnreadCount ?? 0); const [isLoading, setIsLoading] = useState(false); const [selectedMessage, setSelectedMessage] = useState(null); const [conversationMessages, setConversationMessages] = useState([]); const [showComposer, setShowComposer] = useState(false); const [composeRecipient, setComposeRecipient] = useState<{ id: string; type: ParticipantType } | null>(null); const [composeReplyContext, setComposeReplyContext] = useState<{ messageId: string; preview: string } | null>(null); const [selectedAgentId, setSelectedAgentId] = useState(ALL_AGENTS_MAILBOX_ID); const [agentSubTab, setAgentSubTab] = useState<"inbox" | "outbox">("inbox"); const [agentMailbox, setAgentMailbox] = useState(null); const [allAgentsMailbox, setAllAgentsMailbox] = useState(null); const [replyContextExpanded, setReplyContextExpanded] = useState>({}); const [replyContextLoading, setReplyContextLoading] = useState>({}); const [replyContextErrors, setReplyContextErrors] = useState>({}); const [replyContextCache, setReplyContextCache] = useState>(new Map()); const skipOpenSpinnerInboxRef = useRef(false); const skipOpenSpinnerOutboxRef = useRef(false); const agentNamesById = useMemo(() => { const map = new Map(); for (const agent of agents) { if (!agent.id) continue; const name = typeof agent.name === "string" ? agent.name.trim() : ""; if (name.length > 0) { map.set(agent.id, name); } } return map; }, [agents]); const viewportMode = useViewportMode(); const isMobile = viewportMode === "mobile"; const { keyboardOverlap, viewportHeight, viewportOffsetTop, keyboardOpen } = useMobileKeyboard({ enabled: isMobile }); const containerKeyboardStyle = useMemo(() => { if (!keyboardOpen) { return undefined; } return { "--keyboard-overlap": `${keyboardOverlap}px`, "--vv-offset-top": `${viewportOffsetTop}px`, ...(viewportHeight != null ? { "--vv-height": `${viewportHeight}px` } : {}), } as CSSProperties; }, [keyboardOpen, keyboardOverlap, viewportHeight, viewportOffsetTop]); // ── Data fetching ───────────────────────────────────────────────────── const loadInbox = useCallback(async () => { const shouldSkipOpenSpinner = skipOpenSpinnerInboxRef.current; if (!shouldSkipOpenSpinner) { setIsLoading(true); } skipOpenSpinnerInboxRef.current = false; try { const data = await fetchInbox({ limit: 50 }, projectId); setInbox(data); setUnreadCount(data.unreadCount); writeCache( inboxCacheKey, { ...data, messages: data.messages.slice(0, 100) }, { maxBytes: 500_000 }, ); writeCache(unreadCountCacheKey, data.unreadCount, { maxBytes: 500_000 }); } catch { // Silently fail — empty state will show } finally { setIsLoading(false); } }, [inboxCacheKey, projectId, unreadCountCacheKey]); const loadOutbox = useCallback(async () => { const shouldSkipOpenSpinner = skipOpenSpinnerOutboxRef.current; if (!shouldSkipOpenSpinner) { setIsLoading(true); } skipOpenSpinnerOutboxRef.current = false; try { const data = await fetchOutbox({ limit: 50 }, projectId); setOutbox(data); writeCache( outboxCacheKey, { ...data, messages: data.messages.slice(0, 100) }, { maxBytes: 500_000 }, ); } catch { // Silently fail } finally { setIsLoading(false); } }, [outboxCacheKey, projectId]); const loadAgentMailbox = useCallback(async (agentId: string) => { setIsLoading(true); try { const data = await fetchAgentMailbox(agentId, projectId); setAgentMailbox(data); } catch { // Silently fail } finally { setIsLoading(false); } }, [projectId]); const loadAllAgentsMailbox = useCallback(async () => { setIsLoading(true); try { const data = await fetchAllAgentMailbox(projectId); setAllAgentsMailbox(data); } catch { // Silently fail } finally { setIsLoading(false); } }, [projectId]); const refreshUnreadCount = useCallback(async () => { try { const data = await fetchUnreadCount(projectId); setUnreadCount(data.unreadCount); writeCache(unreadCountCacheKey, data.unreadCount, { maxBytes: 500_000 }); } catch { // Silently fail } }, [projectId, unreadCountCacheKey]); useEffect(() => { setInbox(readCache(inboxCacheKey) ?? null); setOutbox(readCache(outboxCacheKey) ?? null); setUnreadCount(readCache(unreadCountCacheKey) ?? 0); }, [inboxCacheKey, outboxCacheKey, unreadCountCacheKey]); useEffect(() => { if (!isOpen) { skipOpenSpinnerInboxRef.current = false; skipOpenSpinnerOutboxRef.current = false; return; } const cachedInbox = readCache(inboxCacheKey); const cachedOutbox = readCache(outboxCacheKey); skipOpenSpinnerInboxRef.current = Boolean(cachedInbox); skipOpenSpinnerOutboxRef.current = Boolean(cachedOutbox); }, [isOpen, inboxCacheKey, outboxCacheKey]); // Load data on tab change useEffect(() => { if (!isOpen) return; if (activeTab === "inbox") loadInbox(); else if (activeTab === "outbox") loadOutbox(); }, [isOpen, activeTab, loadInbox, loadOutbox]); // Load agent mailbox when selected useEffect(() => { if (!isOpen) return; if (selectedAgentId === ALL_AGENTS_MAILBOX_ID) { void loadAllAgentsMailbox(); return; } void loadAgentMailbox(selectedAgentId); }, [isOpen, selectedAgentId, loadAgentMailbox, loadAllAgentsMailbox]); // Refresh unread count on open useEffect(() => { if (isOpen) refreshUnreadCount(); }, [isOpen, refreshUnreadCount]); // Subscribe to mailbox SSE events while the modal is open. useEffect(() => { if (!isOpen || typeof EventSource === "undefined") { return; } const query = projectId ? `?projectId=${encodeURIComponent(projectId)}` : ""; const onMailboxUpdate = () => { void refreshUnreadCount(); if (activeTab === "inbox") { void loadInbox(); } else if (activeTab === "outbox") { void loadOutbox(); } if (selectedAgentId === ALL_AGENTS_MAILBOX_ID) { void loadAllAgentsMailbox(); } else { void loadAgentMailbox(selectedAgentId); } }; return subscribeSse(`/api/events${query}`, { events: { "message:sent": onMailboxUpdate, "message:received": onMailboxUpdate, "message:read": onMailboxUpdate, "message:deleted": onMailboxUpdate, }, }); }, [isOpen, projectId, activeTab, selectedAgentId, refreshUnreadCount, loadInbox, loadOutbox, loadAgentMailbox, loadAllAgentsMailbox]); // ── Actions ─────────────────────────────────────────────────────────── const handleOpenMessage = useCallback(async (message: Message) => { setSelectedMessage(message); setReplyContextExpanded({}); setReplyContextLoading({}); setReplyContextErrors({}); // Only auto-mark as read when viewing the dashboard user's own inbox. // Browsing another agent's mailbox must not consume their unread messages // out from under them — the agent's heartbeat is the one that reads + acks. if (!message.read && activeTab === "inbox") { try { const updated = await markMessageRead(message.id, projectId); // Update inbox state setInbox((prev) => { const next = prev ? { ...prev, messages: prev.messages.map((m) => (m.id === updated.id ? updated : m)), unreadCount: Math.max(0, prev.unreadCount - 1), } : prev; if (next) { writeCache(inboxCacheKey, { ...next, messages: next.messages.slice(0, 100) }, { maxBytes: 500_000 }); } return next; }); setUnreadCount((c) => { const next = Math.max(0, c - 1); writeCache(unreadCountCacheKey, next, { maxBytes: 500_000 }); return next; }); } catch { // Non-critical } } // Load conversation thread try { const conv = await fetchConversation(message.fromId, message.fromType, projectId); setConversationMessages(conv); } catch { setConversationMessages([message]); } }, [activeTab, inboxCacheKey, projectId, unreadCountCacheKey]); // Deep-link: open and highlight a specific message from URL params. useEffect(() => { if (!isOpen) { return; } const deepLinkedMessageId = getDeepLinkedMessageId(); if (!deepLinkedMessageId) { return; } const message = [ ...(inbox?.messages ?? []), ...(outbox?.messages ?? []), ...(agentMailbox?.inbox ?? []), ...(agentMailbox?.outbox ?? []), ...(allAgentsMailbox?.messages ?? []), ...conversationMessages, ].find((candidate) => candidate.id === deepLinkedMessageId); if (!message) { return; } void handleOpenMessage(message); }, [isOpen, inbox, outbox, agentMailbox, allAgentsMailbox, conversationMessages, handleOpenMessage]); useEffect(() => { if (!isOpen) { return; } const deepLinkedMessageId = getDeepLinkedMessageId(); if (!deepLinkedMessageId) { return; } const element = document.getElementById(`message-${deepLinkedMessageId}`); if (!element) { return; } element.scrollIntoView({ behavior: "smooth", block: "center" }); element.classList.add("mailbox-message-highlight"); const timer = window.setTimeout(() => { element.classList.remove("mailbox-message-highlight"); }, 2000); return () => { window.clearTimeout(timer); }; }, [isOpen, selectedMessage, conversationMessages]); const handleCloseMessage = useCallback(() => { setSelectedMessage(null); setConversationMessages([]); setReplyContextExpanded({}); setReplyContextLoading({}); setReplyContextErrors({}); }, []); const handleMarkAllRead = useCallback(async () => { try { const result = await markAllMessagesRead(projectId); setUnreadCount(0); writeCache(unreadCountCacheKey, 0, { maxBytes: 500_000 }); setInbox((prev) => { const next = prev ? { ...prev, messages: prev.messages.map((m) => ({ ...m, read: true })), unreadCount: 0, } : prev; if (next) { writeCache(inboxCacheKey, { ...next, messages: next.messages.slice(0, 100) }, { maxBytes: 500_000 }); } return next; }); addToast?.(`Marked ${result.markedAsRead} messages as read`, "success"); } catch { addToast?.("Failed to mark messages as read", "error"); } }, [addToast, inboxCacheKey, projectId, unreadCountCacheKey]); const handleDeleteMessage = useCallback(async (id: string) => { try { await deleteMessage(id, projectId); setSelectedMessage(null); setConversationMessages([]); // Refresh current tab if (activeTab === "inbox") loadInbox(); else if (activeTab === "outbox") loadOutbox(); else if (selectedAgentId === ALL_AGENTS_MAILBOX_ID) loadAllAgentsMailbox(); else if (selectedAgentId) loadAgentMailbox(selectedAgentId); addToast?.("Message deleted", "success"); } catch { addToast?.("Failed to delete message", "error"); } }, [projectId, activeTab, selectedAgentId, loadInbox, loadOutbox, loadAgentMailbox, loadAllAgentsMailbox, addToast]); const handleReply = useCallback((message: Message) => { setComposeRecipient({ id: message.fromId, type: message.fromType }); setComposeReplyContext({ messageId: message.id, preview: messagePreview(message.content, 120), }); setShowComposer(true); }, []); const handleMessageSent = useCallback(() => { setShowComposer(false); setComposeRecipient(null); setComposeReplyContext(null); addToast?.("Message sent", "success"); // Refresh current tab if (activeTab === "outbox") loadOutbox(); else if (activeTab === "agents" && selectedAgentId === ALL_AGENTS_MAILBOX_ID) loadAllAgentsMailbox(); else if (activeTab === "agents" && selectedAgentId) loadAgentMailbox(selectedAgentId); }, [activeTab, loadOutbox, selectedAgentId, loadAgentMailbox, loadAllAgentsMailbox, addToast]); const handleOpenCompose = useCallback(() => { // Pre-fill recipient from selected agent if available if (activeTab === "agents" && selectedAgentId && selectedAgentId !== ALL_AGENTS_MAILBOX_ID) { setComposeRecipient({ id: selectedAgentId, type: "agent" }); } else { setComposeRecipient(null); } setComposeReplyContext(null); setShowComposer(true); }, [activeTab, selectedAgentId]); const handleComposeCancel = useCallback(() => { setShowComposer(false); setComposeRecipient(null); setComposeReplyContext(null); }, []); const threadMessages = selectedMessage ? buildReplyThread(conversationMessages, selectedMessage) : []; const setReplyExpanded = (key: string, isExpanded: boolean) => { setReplyContextExpanded((prev) => ({ ...prev, [key]: isExpanded })); }; const loadReplyMessage = async (messageId: string) => { const cachedMessage = replyContextCache.get(messageId); if (cachedMessage) { return cachedMessage; } setReplyContextLoading((prev) => ({ ...prev, [messageId]: true })); setReplyContextErrors((prev) => ({ ...prev, [messageId]: "" })); try { const message = await fetchMessage(messageId, projectId); setReplyContextCache((prev) => { const next = new Map(prev); next.set(messageId, message); return next; }); return message; } catch { setReplyContextErrors((prev) => ({ ...prev, [messageId]: "Failed to load replied message. Click to retry." })); return null; } finally { setReplyContextLoading((prev) => ({ ...prev, [messageId]: false })); } }; if (!isOpen) return null; const ReplyContextExpandable = ({ ownerMessageId, replyToId, initialMessage, ancestorIds, testId, }: { ownerMessageId: string; replyToId: string; initialMessage?: Message; ancestorIds: Set; testId?: string; }) => { const cacheMessage = replyContextCache.get(replyToId) ?? initialMessage; const rowKey = `${ownerMessageId}-${replyToId}`; const isExpanded = Boolean(replyContextExpanded[rowKey]); const isLoadingReply = Boolean(replyContextLoading[replyToId]); const errorMessage = replyContextErrors[replyToId]; const hasCycle = ancestorIds.has(replyToId); const handleToggle = async () => { if (isExpanded) { setReplyExpanded(rowKey, false); return; } setReplyExpanded(rowKey, true); if (!cacheMessage && !hasCycle) { await loadReplyMessage(replyToId); } }; const nextAncestorIds = new Set(ancestorIds); nextAncestorIds.add(replyToId); return (
{isExpanded && (
{errorMessage &&
{errorMessage}
} {cacheMessage && ( <>
{participantLabel(cacheMessage.fromId, cacheMessage.fromType, agentNamesById)} {formatTimestamp(cacheMessage.createdAt)}
{cacheMessage.content}
{cacheMessage.metadata?.replyTo?.messageId && !nextAncestorIds.has(cacheMessage.metadata.replyTo.messageId) && ( )} )}
)}
); }; // ── Render ──────────────────────────────────────────────────────────── return (
{ if (e.target === e.currentTarget) onClose(); }} role="dialog" aria-modal="true" data-testid="mailbox-modal-overlay" >
{/* Header */}
Mailbox {unreadCount > 0 && ( {unreadCount} )}
{activeTab === "inbox" && unreadCount > 0 && ( )}
{/* Tabs */}
{/* Content */}
{/* Message Detail View */} {selectedMessage && !showComposer && (
{messageTypeLabel(selectedMessage.type)} {formatTimestamp(selectedMessage.createdAt)}
{selectedMessage.fromType === "agent" && ( )}
From: {selectedMessage.fromType === "agent" ? : } {participantLabel(selectedMessage.fromId, selectedMessage.fromType, agentNamesById)}
To: {selectedMessage.toType === "agent" ? : } {participantLabel(selectedMessage.toId, selectedMessage.toType, agentNamesById)}
{/* Conversation thread */} {threadMessages.length > 1 && (
Conversation
{threadMessages.map((msg) => { const replyToId = msg.metadata?.replyTo?.messageId; const replyToMessage = replyToId ? threadMessages.find((candidate) => candidate.id === replyToId) : undefined; return (
{participantLabel(msg.fromId, msg.fromType, agentNamesById)} {formatTimestamp(msg.createdAt)}
{replyToId && ( )}
); })}
)} {/* Full message content */} {(threadMessages.length <= 1) && ( <> {selectedMessage.metadata?.replyTo?.messageId && ( candidate.id === selectedMessage.metadata?.replyTo?.messageId)} ancestorIds={new Set([selectedMessage.id])} testId="mailbox-selected-reply-context" /> )} )}
)} {/* Message Composer */} {showComposer && ( )} {/* Tab Content — message lists */} {!selectedMessage && !showComposer && ( <> {/* Inbox Tab */} {activeTab === "inbox" && (
{isLoading && !inbox && } {inbox && inbox.messages.length === 0 && (

No messages in your inbox

)} {inbox?.messages.map((msg) => (
handleOpenMessage(msg)} data-testid={`mailbox-item-${msg.id}`} >
{msg.fromType === "agent" ? : }
{participantLabel(msg.fromId, msg.fromType, agentNamesById)} {formatTimestamp(msg.createdAt)}
{msg.content.slice(0, 80)}{msg.content.length > 80 ? "…" : ""}
{!msg.read &&
}
))}
)} {/* Outbox Tab */} {activeTab === "outbox" && (
{isLoading && !outbox && } {outbox && outbox.messages.length === 0 && (

No sent messages

)} {outbox?.messages.map((msg) => (
handleOpenMessage(msg)} data-testid={`mailbox-item-${msg.id}`} >
{msg.toType === "agent" ? : }
To: {participantLabel(msg.toId, msg.toType, agentNamesById)} {formatTimestamp(msg.createdAt)}
{msg.content.slice(0, 80)}{msg.content.length > 80 ? "…" : ""}
))}
)} {/* Agent Mailboxes Tab */} {activeTab === "agents" && (
{agents.length === 0 ? (

No agents found

) : ( <>
{/* Agent Sub-Tabs (Inbox/Outbox) */} {selectedAgentId && selectedAgentId !== ALL_AGENTS_MAILBOX_ID && (
)}
{selectedAgentId === ALL_AGENTS_MAILBOX_ID && isLoading && !allAgentsMailbox && } {selectedAgentId === ALL_AGENTS_MAILBOX_ID && allAgentsMailbox && allAgentsMailbox.messages.length === 0 && (

No agent-to-agent messages

)} {selectedAgentId === ALL_AGENTS_MAILBOX_ID && allAgentsMailbox && allAgentsMailbox.messages.map((msg) => (
handleOpenMessage(msg)} data-testid={`mailbox-item-${msg.id}`} >
{msg.fromType === "agent" ? : }
{participantLabel(msg.fromId, msg.fromType, agentNamesById)} {formatTimestamp(msg.createdAt)}
From: {participantLabel(msg.fromId, msg.fromType, agentNamesById)} To: {participantLabel(msg.toId, msg.toType, agentNamesById)}
{msg.content.slice(0, 80)}{msg.content.length > 80 ? "…" : ""}
{!msg.read &&
}
))} {selectedAgentId && selectedAgentId !== ALL_AGENTS_MAILBOX_ID && isLoading && !agentMailbox && } {selectedAgentId && selectedAgentId !== ALL_AGENTS_MAILBOX_ID && agentMailbox && agentSubTab === "inbox" && agentMailbox.inbox.length === 0 && (

No received messages for this agent

)} {selectedAgentId && selectedAgentId !== ALL_AGENTS_MAILBOX_ID && agentMailbox && agentSubTab === "outbox" && agentMailbox.outbox.length === 0 && (

No sent messages for this agent

)} {selectedAgentId && selectedAgentId !== ALL_AGENTS_MAILBOX_ID && agentMailbox && agentSubTab === "inbox" && agentMailbox.inbox.map((msg) => (
handleOpenMessage(msg)} data-testid={`mailbox-item-${msg.id}`} >
{msg.fromType === "agent" ? : }
{participantLabel(msg.fromId, msg.fromType, agentNamesById)} {formatTimestamp(msg.createdAt)}
{msg.content.slice(0, 80)}{msg.content.length > 80 ? "…" : ""}
))} {selectedAgentId && selectedAgentId !== ALL_AGENTS_MAILBOX_ID && agentMailbox && agentSubTab === "outbox" && agentMailbox.outbox.map((msg) => (
handleOpenMessage(msg)} data-testid={`mailbox-item-${msg.id}`} >
{msg.toType === "agent" ? : }
To: {participantLabel(msg.toId, msg.toType, agentNamesById)} {formatTimestamp(msg.createdAt)}
{msg.content.slice(0, 80)}{msg.content.length > 80 ? "…" : ""}
))}
)}
)} )}
); } // ── Skeleton ────────────────────────────────────────────────────────────── function MailboxSkeleton() { return (
{Array.from({ length: 5 }).map((_, i) => (
))}
); }