import "./MailboxModal.css"; import { useState, useEffect, useCallback, useMemo, useRef, type CSSProperties } from "react"; import { Mail, Send, Inbox as InboxIcon, Bot, Trash2, CheckCheck, Loader2, RefreshCw, MessageSquare, User, } from "lucide-react"; import type { Message, MessageType, ParticipantType } from "@fusion/core"; import { fetchInbox, fetchOutbox, fetchUnreadCount, fetchAgentMailbox, fetchAllAgentMailbox, markMessageRead, markAllMessagesRead, deleteMessage, fetchConversation, fetchAgents, fetchApprovals, fetchApprovalDetail, decideApproval, type InboxResponse, type OutboxResponse, type AgentMailboxResponse, type AllAgentsMailboxResponse, type Agent, type ApprovalRequestSummary, type ApprovalRequestDetail, } from "../api"; import { MailboxMessageContent } from "./MailboxMessageContent"; import { MessageComposer } from "./MessageComposer"; import { WorktrunkInstallApprovalDetails } from "./WorktrunkInstallApprovalDetails"; import { subscribeSse } from "../sse-bus"; import { useViewportMode } from "../hooks/useViewportMode"; import { useMobileKeyboard } from "../hooks/useMobileKeyboard"; import { getScopedItem, setScopedItem } from "../utils/projectStorage"; // ── Types ───────────────────────────────────────────────────────────────── type MailboxTab = "inbox" | "outbox" | "agents" | "approvals"; interface MailboxViewProps { projectId?: string; addToast?: (msg: string, type?: "success" | "error") => void; /** Callback when unread count changes (for header badge updates) */ onUnreadCountChange?: (count: number) => void; } const ALL_AGENTS_MAILBOX_ID = "__all_agents__"; const MAILBOX_SIDEBAR_MIN_WIDTH = 280; const MAILBOX_SIDEBAR_MAX_RATIO = 0.65; const MAILBOX_SIDEBAR_KEYBOARD_STEP = 16; const MAILBOX_SIDEBAR_DEFAULT_WIDTH = 320; function getMailboxSidebarMaxWidth(containerWidth: number): number { return Math.max(MAILBOX_SIDEBAR_MIN_WIDTH, containerWidth * MAILBOX_SIDEBAR_MAX_RATIO); } function clampMailboxSidebarWidth(width: number, containerWidth: number): number { const maxWidth = getMailboxSidebarMaxWidth(containerWidth); return Math.min(Math.max(width, MAILBOX_SIDEBAR_MIN_WIDTH), maxWidth); } function readMailboxSidebarWidth(projectId?: string): number { try { const saved = getScopedItem("kb-dashboard-mailbox-sidebar-width", projectId); if (!saved) return MAILBOX_SIDEBAR_DEFAULT_WIDTH; const parsed = Number(saved); if (Number.isFinite(parsed) && parsed > 0) { return parsed; } } catch { // Invalid localStorage data - fall through to default } return MAILBOX_SIDEBAR_DEFAULT_WIDTH; } // ── 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) return `Agent: ${id}`; if (name === id) return `Agent: ${id}`; return `Agent: ${name} (${id})`; } 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 listMessageAnchorId(messageId: string): string { return `mailbox-list-message-${messageId}`; } function detailMessageAnchorId(messageId: string): string { return `mailbox-detail-message-${messageId}`; } 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 MailboxView({ projectId, addToast, onUnreadCountChange, }: MailboxViewProps) { const [activeTab, setActiveTab] = useState("inbox"); const [inbox, setInbox] = useState(null); const [outbox, setOutbox] = useState(null); const [unreadCount, setUnreadCount] = useState(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 [agents, setAgents] = useState([]); const [approvalSubTab, setApprovalSubTab] = useState<"pending" | "history">("pending"); const [approvals, setApprovals] = useState([]); const [approvalPendingCount, setApprovalPendingCount] = useState(0); const [selectedApproval, setSelectedApproval] = useState(null); const [approvalComment, setApprovalComment] = useState(""); const [approvalDecisionLoading, setApprovalDecisionLoading] = useState(false); const agentNamesById = useMemo( () => new Map(agents.map((agent) => [agent.id, agent.name ?? ""])), [agents], ); const getParticipantLabel = useCallback( (id: string, type: ParticipantType) => participantLabel(id, type, agentNamesById), [agentNamesById], ); const viewportMode = useViewportMode(); const isMobile = viewportMode === "mobile"; const isSplitPane = !isMobile; const [sidebarWidth, setSidebarWidth] = useState(() => readMailboxSidebarWidth(projectId)); const splitLayoutRef = useRef(null); 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]); useEffect(() => { setSidebarWidth(readMailboxSidebarWidth(projectId)); }, [projectId]); useEffect(() => { if (!isSplitPane) return; const containerWidth = splitLayoutRef.current?.clientWidth; if (!containerWidth) return; setSidebarWidth((current) => clampMailboxSidebarWidth(current, containerWidth)); }, [isSplitPane]); useEffect(() => { if (!isSplitPane) return; try { setScopedItem("kb-dashboard-mailbox-sidebar-width", String(sidebarWidth), projectId); } catch { // localStorage persistence is best-effort. } }, [isSplitPane, projectId, sidebarWidth]); const handleSplitResizeStart = useCallback((event: React.MouseEvent) => { if (!isSplitPane) return; event.preventDefault(); const container = splitLayoutRef.current; if (!container) return; const rect = container.getBoundingClientRect(); const onMouseMove = (moveEvent: MouseEvent) => { const proposedWidth = moveEvent.clientX - rect.left; setSidebarWidth(clampMailboxSidebarWidth(proposedWidth, rect.width)); }; const onMouseUp = () => { window.removeEventListener("mousemove", onMouseMove); window.removeEventListener("mouseup", onMouseUp); }; window.addEventListener("mousemove", onMouseMove); window.addEventListener("mouseup", onMouseUp); }, [isSplitPane]); const handleSplitResizeKeyDown = useCallback((event: React.KeyboardEvent) => { if (!isSplitPane) return; const measuredWidth = splitLayoutRef.current?.clientWidth ?? 0; const fallbackWidth = sidebarWidth / MAILBOX_SIDEBAR_MAX_RATIO + MAILBOX_SIDEBAR_KEYBOARD_STEP; const containerWidth = Math.max(measuredWidth, fallbackWidth); const maxWidth = getMailboxSidebarMaxWidth(containerWidth); if (event.key === "ArrowLeft" || event.key === "ArrowRight") { event.preventDefault(); const delta = event.key === "ArrowLeft" ? -MAILBOX_SIDEBAR_KEYBOARD_STEP : MAILBOX_SIDEBAR_KEYBOARD_STEP; setSidebarWidth((current) => clampMailboxSidebarWidth(current + delta, containerWidth)); return; } if (event.key === "Home") { event.preventDefault(); setSidebarWidth(MAILBOX_SIDEBAR_MIN_WIDTH); return; } if (event.key === "End") { event.preventDefault(); setSidebarWidth(maxWidth); } }, [isSplitPane, sidebarWidth]); // ── Data fetching ───────────────────────────────────────────────────── const loadInbox = useCallback(async () => { setIsLoading(true); try { const data = await fetchInbox({ limit: 50 }, projectId); setInbox(data); setUnreadCount(data.unreadCount); onUnreadCountChange?.(data.unreadCount); } catch { // Silently fail — empty state will show } finally { setIsLoading(false); } }, [projectId, onUnreadCountChange]); const loadOutbox = useCallback(async () => { setIsLoading(true); try { const data = await fetchOutbox({ limit: 50 }, projectId); setOutbox(data); } catch { // Silently fail } finally { setIsLoading(false); } }, [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 loadAgents = useCallback(async () => { try { const data = await fetchAgents(undefined, projectId); setAgents(data); } catch { // Silently fail } }, [projectId]); const refreshUnreadCount = useCallback(async () => { try { const data = await fetchUnreadCount(projectId); setUnreadCount(data.unreadCount); setApprovalPendingCount(data.pendingApprovalCount ?? 0); onUnreadCountChange?.(data.unreadCount); } catch { // Silently fail } }, [projectId, onUnreadCountChange]); const loadApprovals = useCallback(async (status: "pending" | "history") => { setIsLoading(true); try { const list = await fetchApprovals({ status: status === "pending" ? "pending" : undefined, limit: 100 }, projectId); if (status === "pending") { setApprovals(list.requests); } else { const [approved, denied, completed] = await Promise.all([ fetchApprovals({ status: "approved", limit: 100 }, projectId), fetchApprovals({ status: "denied", limit: 100 }, projectId), fetchApprovals({ status: "completed", limit: 100 }, projectId), ]); setApprovals([...approved.requests, ...denied.requests, ...completed.requests].sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))); } setApprovalPendingCount(list.pendingCount); } catch { // Silently fail } finally { setIsLoading(false); } }, [projectId]); // Load data on tab change useEffect(() => { if (activeTab === "inbox") loadInbox(); else if (activeTab === "outbox") loadOutbox(); else if (activeTab === "agents") loadAgents(); else if (activeTab === "approvals") { void loadApprovals(approvalSubTab); } }, [activeTab, loadInbox, loadOutbox, loadAgents, loadApprovals, approvalSubTab]); // Load agent mailbox when selected useEffect(() => { if (selectedAgentId === ALL_AGENTS_MAILBOX_ID) { void loadAllAgentsMailbox(); return; } void loadAgentMailbox(selectedAgentId); }, [selectedAgentId, loadAgentMailbox, loadAllAgentsMailbox]); // Load unread count on mount useEffect(() => { refreshUnreadCount(); }, [refreshUnreadCount]); // Load agents on mount so they're available for compose from any tab (not just agents tab) useEffect(() => { loadAgents(); }, [loadAgents]); // Subscribe to mailbox SSE events for near-real-time refresh. useEffect(() => { if (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(); } else if (activeTab === "approvals") { void loadApprovals(approvalSubTab); } if (selectedAgentId === ALL_AGENTS_MAILBOX_ID) { void loadAllAgentsMailbox(); } else if (selectedAgentId) { void loadAgentMailbox(selectedAgentId); } }; return subscribeSse(`/api/events${query}`, { events: { "message:sent": onMailboxUpdate, "message:received": onMailboxUpdate, "message:read": onMailboxUpdate, "message:deleted": onMailboxUpdate, "approval:requested": onMailboxUpdate, "approval:updated": onMailboxUpdate, "approval:decided": onMailboxUpdate, }, }); }, [projectId, activeTab, selectedAgentId, refreshUnreadCount, loadInbox, loadOutbox, loadAgentMailbox, loadAllAgentsMailbox, loadApprovals, approvalSubTab]); // ── Actions ─────────────────────────────────────────────────────────── const handleOpenMessage = useCallback(async (message: Message) => { setSelectedMessage(message); // 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 if (updated) { setInbox((prev) => prev ? { ...prev, messages: prev.messages.map((m) => (m.id === updated.id ? updated : m)), unreadCount: Math.max(0, prev.unreadCount - 1), } : prev, ); } const newCount = Math.max(0, unreadCount - 1); setUnreadCount(newCount); onUnreadCountChange?.(newCount); } catch { // Non-critical } } // Load conversation thread try { const conv = await fetchConversation(message.fromId, message.fromType, projectId); setConversationMessages(conv); } catch { setConversationMessages([message]); } }, [projectId, unreadCount, onUnreadCountChange, activeTab]); // Deep-link: open and highlight a specific message from URL params. useEffect(() => { 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); }, [inbox, outbox, agentMailbox, allAgentsMailbox, conversationMessages, handleOpenMessage]); useEffect(() => { const deepLinkedMessageId = getDeepLinkedMessageId(); if (!deepLinkedMessageId) { return; } const element = document.getElementById(detailMessageAnchorId(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); }; }, [selectedMessage, conversationMessages]); const handleCloseMessage = useCallback(() => { setSelectedMessage(null); setConversationMessages([]); }, []); const handleMarkAllRead = useCallback(async () => { try { const result = await markAllMessagesRead(projectId); setUnreadCount(0); onUnreadCountChange?.(0); setInbox((prev) => prev ? { ...prev, messages: prev.messages.map((m) => ({ ...m, read: true })), unreadCount: 0, } : prev, ); addToast?.(`Marked ${result.markedAsRead} messages as read`, "success"); } catch { addToast?.("Failed to mark messages as read", "error"); } }, [projectId, addToast, onUnreadCountChange]); 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); refreshUnreadCount(); }, [activeTab, loadOutbox, selectedAgentId, loadAgentMailbox, loadAllAgentsMailbox, addToast, refreshUnreadCount]); 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 handleOpenApproval = useCallback(async (request: ApprovalRequestSummary) => { try { const detail = await fetchApprovalDetail(request.id, projectId); setSelectedApproval(detail); setApprovalComment(""); } catch { addToast?.("Failed to load approval request", "error"); } }, [projectId, addToast]); const handleApprovalDecision = useCallback(async (decision: "approve" | "deny") => { if (!selectedApproval || approvalDecisionLoading) return; setApprovalDecisionLoading(decision); try { await decideApproval(selectedApproval.id, { decision, comment: approvalComment || undefined }, projectId); await loadApprovals(approvalSubTab); const updated = await fetchApprovalDetail(selectedApproval.id, projectId); setSelectedApproval(updated); setApprovalComment(""); addToast?.(`Request ${decision === "approve" ? "approved" : "denied"}`, "success"); } catch { addToast?.("Failed to submit decision", "error"); } finally { setApprovalDecisionLoading(false); } }, [selectedApproval, approvalDecisionLoading, approvalComment, projectId, loadApprovals, approvalSubTab, addToast]); // ── Render ──────────────────────────────────────────────────────────── const renderMessageDetail = () => { if (!selectedMessage || showComposer) return null; const threadMessages = buildReplyThread(conversationMessages, selectedMessage); return (
{isMobile && ( )}
{messageTypeLabel(selectedMessage.type)} {formatTimestamp(selectedMessage.createdAt)}
{selectedMessage.fromType === "agent" && ( )}
From: {selectedMessage.fromType === "agent" ? : } {getParticipantLabel(selectedMessage.fromId, selectedMessage.fromType)}
To: {selectedMessage.toType === "agent" ? : } {getParticipantLabel(selectedMessage.toId, selectedMessage.toType)}
{threadMessages.length > 1 && (
Conversation
{threadMessages.map((msg) => { const replyToId = msg.metadata?.replyTo?.messageId; const replyToMessage = replyToId ? threadMessages.find((candidate) => candidate.id === replyToId) : undefined; return (
{getParticipantLabel(msg.fromId, msg.fromType)} {formatTimestamp(msg.createdAt)}
{replyToId && (
↪ Replying to {replyToMessage ? messagePreview(replyToMessage.content, 60) : `message ${replyToId}`}
)}
); })}
)} {(threadMessages.length <= 1) && ( <> {selectedMessage.metadata?.replyTo?.messageId && (
↪ Replying to message {selectedMessage.metadata.replyTo.messageId}
)} )}
); }; const renderListPane = () => ( <> {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" ? : }
{getParticipantLabel(msg.fromId, msg.fromType)} {formatTimestamp(msg.createdAt)}
{msg.content.slice(0, 80)}{msg.content.length > 80 ? "…" : ""}
{!msg.read &&
}
))}
)} {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: {getParticipantLabel(msg.toId, msg.toType)} {formatTimestamp(msg.createdAt)}
{msg.content.slice(0, 80)}{msg.content.length > 80 ? "…" : ""}
))}
)} {activeTab === "approvals" && (
{approvals.length === 0 && !isLoading && (

{approvalSubTab === "pending" ? "No pending approvals" : "No historical approvals"}

)} {approvals.map((request) => (
void handleOpenApproval(request)} data-testid={`mailbox-approval-item-${request.id}`} >
{request.agentId} · {request.actionCategory} {formatTimestamp(request.createdAt)}
{request.actionSummary}
{request.status}
))}
)} {activeTab === "agents" && (
{agents.length === 0 ? (

No agents found

) : ( <>
{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" ? : }
{getParticipantLabel(msg.fromId, msg.fromType)} {formatTimestamp(msg.createdAt)}
From: {getParticipantLabel(msg.fromId, msg.fromType)} To: {getParticipantLabel(msg.toId, msg.toType)}
{msg.content.slice(0, 80)}{msg.content.length > 80 ? "…" : ""}
))} {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" ? : }
{getParticipantLabel(msg.fromId, msg.fromType)} {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: {getParticipantLabel(msg.toId, msg.toType)} {formatTimestamp(msg.createdAt)}
{msg.content.slice(0, 80)}{msg.content.length > 80 ? "…" : ""}
))}
)}
)} ); const renderDetailPane = () => { if (showComposer) { return ( ); } if (selectedMessage) { return renderMessageDetail(); } if (activeTab === "approvals" && selectedApproval) { return (
{isMobile && ( )}
{selectedApproval.actionCategory} {selectedApproval.status}
{selectedApproval.actionSummary}

Requester: {selectedApproval.requester.actorName} ({selectedApproval.agentId})

{selectedApproval.taskId &&

Task: {selectedApproval.taskId}

}

Requested: {formatTimestamp(selectedApproval.createdAt)}

{selectedApproval.targetAction.category === "network_api" && selectedApproval.targetAction.action === "worktrunk_install" && ( )}
{selectedApproval.history.map((event) => (
{event.eventType} {event.actor.actorName}
{event.note &&
{event.note}
}
))}
{selectedApproval.status === "pending" && (