import { useEffect, useMemo, useState } from 'react' import type { CommsStatePayload, CommsMessagePayload, CommsRolePayload, CommsMessageItem, } from '../lib/wsClient' interface CommsPanelProps { state: CommsStatePayload | null message: CommsMessagePayload | null onRefresh: () => void onReadMessage: (path: string) => void pollIntervalMs?: number /** When true, renders without its own border/header (embedded in ContextPanel tab). */ embedded?: boolean } export function CommsPanel({ state, message, onRefresh, onReadMessage, pollIntervalMs = 8000, embedded = false, }: CommsPanelProps) { const [selectedPath, setSelectedPath] = useState(null) // Auto-refresh useEffect(() => { if (!pollIntervalMs) return onRefresh() const id = window.setInterval(() => onRefresh(), pollIntervalMs) return () => window.clearInterval(id) // eslint-disable-next-line react-hooks/exhaustive-deps }, [pollIntervalMs]) const totalUnread = useMemo(() => { if (!state?.roles) return 0 return state.roles.reduce((acc, r) => acc + r.unread_count, 0) }, [state?.roles]) const handleSelectMessage = (path: string) => { setSelectedPath(path) onReadMessage(path) } const wrapStyle: React.CSSProperties = embedded ? { fontSize: 13, color: 'var(--text)' } : { border: '1px solid var(--border, #333)', borderRadius: 6, background: 'var(--bg-primary, #1e1e1e)', fontSize: 13, } return (
{/* Toolbar */}
Agent Communications {totalUnread > 0 && {totalUnread} unread}
{/* Body */} {!state ? ( Loading communications... ) : !state.available ? ( Not available{state.reason ? `: ${state.reason}` : ''} ) : state.empty ? ( No communications yet. They will appear once agents start collaborating. ) : ( )} {/* Message viewer overlay */} {message && ( setSelectedPath(null)} /> )}
) } /* ── Body ── */ function CommsBody({ state, selectedPath, onSelectMessage, }: { state: CommsStatePayload selectedPath: string | null onSelectMessage: (path: string) => void }) { const hasRoles = (state.roles?.length ?? 0) > 0 const hasMeetings = (state.meetings?.length ?? 0) > 0 const hasFailures = (state.recent_failures?.length ?? 0) > 0 return (
{hasFailures && (
{(state.recent_failures || []).map((f, i) => (
{f.operation} · {f.from_role} → {f.to_role}
{f.reason}
))}
)} {hasRoles && (
{state.roles!.map((role) => ( ))}
)} {hasMeetings && (
{(state.meetings || []).map((m) => (
{m.status} {m.topic} {m.entry_count} entries
by {m.organizer} · {m.participants.join(', ')}
{m.decision && (
Decision: {m.decision}
)}
))}
)} {!hasRoles && !hasMeetings && !hasFailures && ( No communication activity yet. )}
) } /* ── Role Section ── */ type MessageTab = 'unread' | 'history' | 'sent' function RoleSection({ role, selectedPath, onSelectMessage, }: { role: CommsRolePayload selectedPath: string | null onSelectMessage: (path: string) => void }) { const [expanded, setExpanded] = useState(true) const hasAnyMessages = role.unread_count > 0 || (role.recent_seen?.length ?? 0) > 0 || (role.recent_outbox?.length ?? 0) > 0 const [activeTab, setActiveTab] = useState(role.unread_count > 0 ? 'unread' : 'history') const messages: CommsMessageItem[] = useMemo(() => { switch (activeTab) { case 'unread': return role.recent_unread || [] case 'history': return role.recent_seen || [] case 'sent': return role.recent_outbox || [] default: return [] } }, [activeTab, role.recent_unread, role.recent_seen, role.recent_outbox]) const roleName = role.role_id.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()) return (
{/* Role header */}
setExpanded((e) => !e)} style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '6px 12px', cursor: 'pointer', userSelect: 'none', background: expanded ? 'var(--surface-hover, rgba(255,255,255,0.03))' : 'transparent', borderRadius: 4, }} > {expanded ? '▼' : '▶'} {roleName} {role.has_blocking && BLOCKING} {role.unread_count > 0 && ( {role.unread_count} new )} {role.seen_count} read · {role.outbox_count} sent
{expanded && hasAnyMessages && (
{/* Sub-tabs */}
setActiveTab('unread')}> Unread ({role.unread_count}) setActiveTab('history')}> Read ({role.recent_seen?.length ?? 0}) setActiveTab('sent')}> Sent ({role.recent_outbox?.length ?? 0})
{/* Message list */} {messages.length === 0 ? (
No messages.
) : (
{messages.map((m) => ( onSelectMessage(m.path)} /> ))}
)}
)}
) } /* ── Message Row ── */ function MessageRow({ message, selected, onClick, }: { message: CommsMessageItem selected: boolean onClick: () => void }) { const isSent = message.bucket === 'sent' const direction = isSent ? `To: ${message.to || '?'}` : `From: ${message.from}` return (
{direction}{' '} {message.subject} {message.blocking && ( BLK )} {message.sent_at?.slice(11, 19) || ''}
) } /* ── Message Viewer ── */ function MessageViewer({ message, onClose, }: { message: CommsMessagePayload onClose: () => void }) { return (
{(message.header.subject as string) || '(no subject)'}
From: {(message.header.from as string) || '?'} To: {(message.header.to as string) || '?'} {(message.header.sent_at as string) || ''} {message.header.blocking && ( BLOCKING )}
        {message.body}
      
) } /* ── Shared UI helpers ── */ function Section({ title, children }: { title: string; children: React.ReactNode }) { return (
{title}
{children}
) } function Badge({ color, children, }: { color: string children: React.ReactNode }) { return ( {children} ) } function TabButton({ active, onClick, children, }: { active: boolean onClick: () => void children: React.ReactNode }) { return ( ) } function EmptyState({ children }: { children: React.ReactNode }) { return (
{children}
) }