import { Fragment, memo, useCallback, useMemo } from 'react'; import { CompactMarkdownPreview, MarkdownViewer, } from '@renderer/components/chat/viewers/MarkdownViewer'; import { CopyButton } from '@renderer/components/common/CopyButton'; import { AttachmentDisplay } from '@renderer/components/team/attachments/AttachmentDisplay'; import { MemberBadge } from '@renderer/components/team/MemberBadge'; import { TaskTooltip } from '@renderer/components/team/TaskTooltip'; import { ExpandableContent } from '@renderer/components/ui/ExpandableContent'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from '@renderer/components/ui/tooltip'; import { CARD_BG, CARD_BG_ZEBRA, CARD_BORDER_STYLE, CARD_ICON_MUTED, CARD_TEXT_LIGHT, } from '@renderer/constants/cssVariables'; import { getTeamColorSet, getThemedBorder } from '@renderer/constants/teamColors'; import { useTheme } from '@renderer/hooks/useTheme'; import { useStore } from '@renderer/store'; import { getMessageTypeLabel, getStructuredMessageSummary, parseMessageReply, parseStructuredAgentMessage, } from '@renderer/utils/agentMessageFormatting'; import { getBootstrapAcknowledgementDisplay, getBootstrapPromptDisplay, getSanitizedInboxMessageSummary, getSanitizedInboxMessageText, } from '@renderer/utils/bootstrapPromptSanitizer'; import { formatAgentRole } from '@renderer/utils/formatAgentRole'; import { classifyIdleNotification, getIdleNoiseLabel, } from '@renderer/utils/idleNotificationSemantics'; import { linkifyAllMentionsInMarkdown } from '@renderer/utils/mentionLinkify'; import { areInboxMessagesEquivalentForRender, areStringArraysEqual, areStringMapsEqual, } from '@renderer/utils/messageRenderEquality'; import { linkifyTaskIdsInMarkdown, parseTaskLinkHref } from '@renderer/utils/taskReferenceUtils'; import { stripAgentBlocks } from '@shared/constants/agentBlocks'; import { CROSS_TEAM_SENT_SOURCE, CROSS_TEAM_SOURCE, parseCrossTeamPrefix, stripCrossTeamPrefix, } from '@shared/constants/crossTeam'; import { extractMarkdownPlainText } from '@shared/utils/markdownTextSearch'; import { isRateLimitMessage } from '@shared/utils/rateLimitDetector'; import { buildStandaloneSlashCommandMeta, getKnownSlashCommand, parseStandaloneSlashCommand, } from '@shared/utils/slashCommands'; import { formatTaskDisplayLabel } from '@shared/utils/taskIdentity'; import { AlertTriangle, Check, ChevronRight, Clock, Command, ListPlus, Maximize2, MoveRight, RefreshCw, Reply, X, } from 'lucide-react'; import { useShallow } from 'zustand/react/shallow'; import { ReplyQuoteBlock } from './ReplyQuoteBlock'; import type { TeamColorSet } from '@renderer/constants/teamColors'; import type { InboxMessage } from '@shared/types'; type StructuredMessage = Record; function parseQualifiedRecipient( value: string | undefined ): { teamName: string; memberName: string } | null { if (typeof value !== 'string') return null; const trimmed = value.trim(); const dot = trimmed.indexOf('.'); if (dot <= 0 || dot === trimmed.length - 1) return null; return { teamName: trimmed.slice(0, dot), memberName: trimmed.slice(dot + 1), }; } function parseCrossTeamPseudoRecipient(value: string | undefined): string | null { if (typeof value !== 'string') return null; const trimmed = value.trim(); if (!trimmed.startsWith('cross-team:')) return null; const teamName = trimmed.slice('cross-team:'.length).trim(); return teamName.length > 0 ? teamName : null; } function getCommandOutputSummary(text: string): string { const firstLine = text .split(/\r?\n/) .map((line) => line.trim()) .find(Boolean); if (!firstLine) return ''; return firstLine.length > 120 ? `${firstLine.slice(0, 120)}…` : firstLine; } function parseIdlePeerSummaryRoute(summary: string): { recipient: string | null; body: string } { const trimmed = summary.trim(); const match = /^\[to\s+([^\]]+)\]\s*(.*)$/i.exec(trimmed); if (!match) { return { recipient: null, body: trimmed }; } const recipient = match[1]?.trim() || null; const body = match[2]?.trim() || trimmed; return { recipient, body }; } export function isQualifiedExternalRecipient( value: string | undefined, teamName: string, localMemberNames?: Set ): boolean { const recipient = parseQualifiedRecipient(value); if (!recipient) return false; if (recipient.teamName === teamName) return false; return !localMemberNames?.has(value?.trim() ?? ''); } export function getCrossTeamSentTarget( value: string | undefined, teamName: string, localMemberNames?: Set ): string | null { const pseudoTarget = parseCrossTeamPseudoRecipient(value); if (pseudoTarget) return pseudoTarget; const recipient = parseQualifiedRecipient(value); if (!recipient) return null; if (recipient.teamName === teamName) return null; if (localMemberNames?.has(value?.trim() ?? '')) return null; return recipient.teamName; } export function getCrossTeamSentMemberName(value: string | undefined): string | null { return parseQualifiedRecipient(value)?.memberName ?? null; } const CrossTeamTeamBadge = ({ teamName, onClick, }: { teamName: string; onClick?: (teamName: string) => void; }): React.JSX.Element => { if (onClick) { return ( ); } return ( {teamName} ); }; interface ActivityItemProps { message: InboxMessage; teamName: string; localMemberNames?: Set; teamNames?: string[]; memberRole?: string; memberColor?: string; recipientColor?: string; /** When true, show a blue unread dot. */ isUnread?: boolean; /** Map of member name → color name for @mention badge rendering. */ memberColorMap?: Map; /** Team color mapping for team:// links rendered inside markdown. */ teamColorByName?: ReadonlyMap; /** Opens a team tab from cross-team badges or team:// links. */ onTeamClick?: (teamName: string) => void; onMemberNameClick?: (memberName: string) => void; onCreateTask?: (subject: string, description: string) => void; onReply?: (message: InboxMessage) => void; /** Called when a task ID link (e.g. #10) is clicked in message text. */ onTaskIdClick?: (taskId: string) => void; /** Called when the user clicks "Restart team" on an auth error message. */ onRestartTeam?: () => void; /** When true, apply a subtle lighter background for zebra-striped lists. */ zebraShade?: boolean; /** Collapsed-mode primitives stabilized by ActivityTimeline. */ collapseMode?: 'default' | 'managed'; isCollapsed?: boolean; canToggleCollapse?: boolean; collapseToggleKey?: string; onToggleCollapse?: (key: string) => void; /** Compact header mode for narrow message lists. */ compactHeader?: boolean; /** Callback to expand this item into a fullscreen dialog. */ onExpand?: (key: string) => void; /** Stable key for expand identification. */ expandItemKey?: string; /** Called when ExpandableContent is expanded via "Show more". */ onExpandContent?: () => void; } function areMessagesEquivalentForActivityItem(prev: InboxMessage, next: InboxMessage): boolean { return areInboxMessagesEquivalentForRender(prev, next); } function getStringField(obj: StructuredMessage, key: string): string | null { const value = obj[key]; return typeof value === 'string' && value.trim() !== '' ? value : null; } /** Check if a message renders as a compact noise row (idle, shutdown, etc.). */ export function isNoiseMessage(text: string): boolean { return ( getIdleNoiseLabel(text) !== null || ((): boolean => { const parsed = parseStructuredAgentMessage(text); return parsed !== null && getNoiseLabel(parsed) !== null; })() ); } function getNoiseLabel(parsed: StructuredMessage): string | null { const type = getStringField(parsed, 'type'); if (type === 'idle_notification') { return getIdleNoiseLabel(parsed); } if (type === 'shutdown_response') { return parsed.approve === true ? 'Shut down' : 'Rejected shutdown'; } if (type === 'shutdown_request') { return 'Shutdown requested'; } if (type === 'shutdown_approved' || type === 'teammate_terminated') { return type === 'shutdown_approved' ? 'Shutdown confirmed' : 'Terminated'; } if (type === 'task_completed') { const rawTaskId = parsed.taskId; const taskId = typeof rawTaskId === 'string' || typeof rawTaskId === 'number' ? rawTaskId : null; return taskId !== null ? `Completed task ${formatTaskDisplayLabel({ id: String(taskId) })}` : 'Completed a task'; } if (type === 'permission_request') { const toolName = getStringField(parsed, 'tool_name'); return toolName ? `Permission: ${toolName}` : 'Permission request'; } if (type === 'permission_response') { if (parsed.approved === true) return 'Permission granted'; if (parsed.approved === false) return 'Permission denied'; return 'Permission response'; } return null; } // --------------------------------------------------------------------------- // Compact noise row (idle, shutdown, terminated) — minimal dot + name + label // --------------------------------------------------------------------------- const NoiseRow = ({ name, label, colors, icon, }: { name: string; label: string; colors: TeamColorSet; icon?: React.ReactNode; }): React.JSX.Element => (
{name} {label} {icon}
); const PassiveIdlePeerSummaryRow = ({ teamName, senderName, senderColor, summary, timestamp, onMemberNameClick, }: { teamName: string; senderName: string; senderColor?: string; summary: string; timestamp: string; onMemberNameClick?: (memberName: string) => void; }): React.JSX.Element => { const { recipient, body } = parseIdlePeerSummaryRoute(summary); return (
update {recipient ? ( <> {recipient} ) : null} {body} {timestamp}
); }; const BootstrapSystemRow = ({ teamName, senderName, recipientName, runtime, senderColor, recipientColor, timestamp, onMemberNameClick, }: { teamName: string; senderName: string; recipientName: string; runtime?: string; senderColor?: string; recipientColor?: string; timestamp: string; onMemberNameClick?: (memberName: string) => void; }): React.JSX.Element => (
start {runtime || 'Starting teammate'} {timestamp}
); const BootstrapAcknowledgementRow = ({ teamName, senderName, recipientName, senderColor, recipientColor, timestamp, onMemberNameClick, }: { teamName: string; senderName: string; recipientName: string; senderColor?: string; recipientColor?: string; timestamp: string; onMemberNameClick?: (memberName: string) => void; }): React.JSX.Element => (
bootstrap Bootstrap acknowledged {timestamp}
); // --------------------------------------------------------------------------- // Detect historical system/automated messages that should be collapsed by default. // These patterns are kept only for legacy compatibility with old inbox/session rows; // new runtime behavior must not depend on exact legacy wording. // --------------------------------------------------------------------------- const SYSTEM_MESSAGE_PATTERNS: { pattern: RegExp; label: string }[] = [ { pattern: /^New task assigned to you:/, label: 'Task' }, { pattern: /^Task #[A-Za-z0-9-]+\s+approved/, label: 'Task approved' }, { pattern: /^Task #[A-Za-z0-9-]+\s+needs fixes/, label: 'Review changes requested' }, ]; export function getSystemMessageLabel(text: string): string | null { for (const { pattern, label } of SYSTEM_MESSAGE_PATTERNS) { if (pattern.test(text)) return label; } return null; } /** Labels to highlight in task assignment / review messages (bold in markdown). */ const TASK_MESSAGE_LABELS = [ 'New task assigned to you:', 'Description:', 'Task approved', 'Task needs fixes', 'Review changes requested', 'Changes requested:', 'Comments:', 'Reviewer:', 'Related:', 'Blocked by:', 'Blocks:', ]; /** Make known structural labels bold in system/task messages. */ function highlightSystemLabels(text: string, isSystem: boolean): string { if (!isSystem) return text; let result = text; for (const label of TASK_MESSAGE_LABELS) { // Escape any regex-special chars in the label, match at line start or after newline const escaped = label.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); result = result.replace(new RegExp(`(^|\\n)(${escaped})`, 'g'), '$1**$2**'); } return result; } /** Detect authentication/authorization errors that may be resolved by restarting. */ const AUTH_ERROR_PATTERNS = [ /OAuth token has expired/i, /API Error:\s*401/i, /authentication_error/i, /Failed to authenticate/i, /invalid.*api.key/i, /unauthorized/i, ]; // --------------------------------------------------------------------------- // Full message card — left colored border, name badge, collapsible content // --------------------------------------------------------------------------- /** Render `#` in plain text as clickable inline elements with TaskTooltip. */ function linkifyTaskIds(text: string, onClick: (taskId: string) => void): React.ReactNode[] { return text.split(/(#[A-Za-z0-9-]+\b)/g).map((part, i) => { const match = /^#([A-Za-z0-9-]+)$/.exec(part); if (!match) return {part}; const taskId = match[1]; return ( ); }); } /** * Render summary text with inline bold markdown and optional task-id linkification. * Splits on bold markers first, then linkifies task IDs within each segment. */ function renderInlineBoldSummary( text: string, onTaskIdClick?: (taskId: string) => void ): React.ReactNode { // Split by **bold** segments, keeping delimiters const boldPattern = /(\*\*[^*]+\*\*)/g; const parts = text.split(boldPattern); return parts.map((part, i) => { const boldContent = /^\*\*(.+)\*\*$/.exec(part); if (boldContent) { const inner = boldContent[1]; return ( {onTaskIdClick ? linkifyTaskIds(inner, onTaskIdClick) : inner} ); } return onTaskIdClick ? ( {linkifyTaskIds(part, onTaskIdClick)} ) : ( {part} ); }); } const TaskRecipientBadge = ({ taskId, displayId, teamName, onTaskIdClick, }: Readonly<{ taskId: string; displayId: string; teamName?: string; onTaskIdClick?: (taskId: string) => void; }>): React.JSX.Element => { const content = ( {displayId} ); if (!onTaskIdClick) { return content; } return ( ); }; export const ActivityItem = memo( ({ message, teamName, localMemberNames, teamNames = [], memberRole, memberColor, recipientColor, isUnread, memberColorMap, teamColorByName, onTeamClick, onMemberNameClick, onCreateTask, onReply, onTaskIdClick, onRestartTeam, zebraShade, collapseMode = 'default', isCollapsed = false, canToggleCollapse = false, collapseToggleKey, onToggleCollapse, compactHeader = false, onExpand, expandItemKey, onExpandContent, }: Readonly): React.JSX.Element => { const colors = getTeamColorSet(memberColor ?? message.color ?? ''); const { isLight } = useTheme(); // Hide role when it matches the sender name (avoids "lead" badge + "Team Lead" text duplication) const formattedRole = memberRole && memberRole !== message.from ? formatAgentRole(memberRole) : null; const timestamp = useMemo(() => { if (Number.isNaN(Date.parse(message.timestamp))) return message.timestamp; const date = new Date(message.timestamp); const now = new Date(); const isToday = date.getFullYear() === now.getFullYear() && date.getMonth() === now.getMonth() && date.getDate() === now.getDate(); return isToday ? date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : date.toLocaleString(); }, [message.timestamp]); const structured = parseStructuredAgentMessage(message.text); const bootstrapDisplay = getBootstrapPromptDisplay(message); const bootstrapAcknowledgement = getBootstrapAcknowledgementDisplay(message); // Only flag agent messages as rate-limited, not user's own quotes const rateLimited = message.from !== 'user' && isRateLimitMessage(message.text); // Highlight messages containing API errors const isApiError = message.text.includes('API Error'); // Detect auth errors that may be resolved by restarting the team const isAuthError = isApiError && AUTH_ERROR_PATTERNS.some((p) => p.test(message.text)); // Never collapse rate limit messages as noise — they must be visible const noiseLabel = structured && !rateLimited ? getNoiseLabel(structured) : null; const idleSemantic = classifyIdleNotification(message); const systemLabel = !structured && !rateLimited ? getSystemMessageLabel(message.text) : null; const isManaged = collapseMode === 'managed'; const isExpanded = isManaged ? !isCollapsed : true; const parsedCrossTeamPrefix = parseCrossTeamPrefix(message.text); const qualifiedRecipient = parseQualifiedRecipient(message.to); const crossTeamSentTarget = getCrossTeamSentTarget(message.to, teamName, localMemberNames); const crossTeamSentMemberName = getCrossTeamSentMemberName(message.to); const isCrossTeam = message.source === CROSS_TEAM_SOURCE || parsedCrossTeamPrefix !== null; const isCrossTeamSent = message.source === CROSS_TEAM_SENT_SOURCE || crossTeamSentTarget !== null; const isCrossTeamAny = isCrossTeam || isCrossTeamSent; const crossTeamOrigin = useMemo(() => { if (!isCrossTeam) return null; const fromValue = parsedCrossTeamPrefix?.from ?? message.from; const dot = fromValue.indexOf('.'); if (dot <= 0 || dot === fromValue.length - 1) return null; return { teamName: fromValue.substring(0, dot), memberName: fromValue.substring(dot + 1), }; }, [isCrossTeam, message.from, parsedCrossTeamPrefix]); const crossTeamTarget = useMemo(() => { if (!isCrossTeamSent) return null; if (crossTeamSentTarget) return crossTeamSentTarget; if (qualifiedRecipient) return qualifiedRecipient.teamName; if (!message.to) return null; const dot = message.to.indexOf('.'); if (dot <= 0) return message.to; return message.to.substring(0, dot); }, [crossTeamSentTarget, isCrossTeamSent, message.to, qualifiedRecipient]); const senderName = crossTeamOrigin ? crossTeamOrigin.memberName : message.from; const senderColor = crossTeamOrigin ? undefined : (memberColor ?? message.color); const senderHideAvatar = message.from === 'user' || message.from === 'system' || crossTeamOrigin?.memberName === 'user'; const isUserSent = message.source === 'user_sent' || isCrossTeamSent; const isSystemMessage = message.from === 'system'; // Strip agent-only blocks + normalize escape sequences (before linkification) const strippedText = useMemo(() => { if (structured) return null; let stripped = getSanitizedInboxMessageText(message).trim(); if (!bootstrapDisplay) { stripped = stripAgentBlocks(stripped).trim(); } if (!stripped) return null; // All content was agent-only blocks → show summary instead // Strip cross-team metadata tag (e.g. `\n`) // — kept in stored text for CLI agents / durable artifacts. if (isCrossTeamAny) { stripped = stripCrossTeamPrefix(stripped); } // Normalize literal \n from historical CLI-produced text to real newlines return stripped.replace(/\\n/g, '\n').replace(/\\t/g, '\t'); }, [structured, message, bootstrapDisplay, isCrossTeamAny]); const standaloneSlashCommand = useMemo( () => (strippedText ? parseStandaloneSlashCommand(strippedText) : null), [strippedText] ); const slashCommandMeta = useMemo( () => message.slashCommand ?? (standaloneSlashCommand ? buildStandaloneSlashCommandMeta(standaloneSlashCommand.raw) : null), [message.slashCommand, standaloneSlashCommand] ); const knownSlashCommand = useMemo( () => (slashCommandMeta?.name ? (getKnownSlashCommand(slashCommandMeta.name) ?? null) : null), [slashCommandMeta] ); const isSlashCommandResult = message.messageKind === 'slash_command_result' && !!message.commandOutput; const isSlashCommandMessage = !isSlashCommandResult && (message.messageKind === 'slash_command' || (isUserSent && standaloneSlashCommand !== null)); const isCommandOutputError = isSlashCommandResult && message.commandOutput?.stream === 'stderr'; // Parse reply BEFORE linkification — linkifyAllMentionsInMarkdown transforms @name // into markdown links which breaks the reply regex matcher const parsedReply = useMemo( () => (strippedText ? parseMessageReply(strippedText) : null), [strippedText] ); // Linkify task IDs (always, for TaskTooltip) + @mentions for display const displayText = useMemo(() => { if (!strippedText) return null; let result = highlightSystemLabels(strippedText, !!systemLabel); result = linkifyTaskIdsInMarkdown(result, message.taskRefs); if ((memberColorMap && memberColorMap.size > 0) || teamNames.length > 0) result = linkifyAllMentionsInMarkdown(result, memberColorMap ?? new Map(), teamNames); return result; }, [strippedText, memberColorMap, teamNames, systemLabel]); const crossTeamPreview = useMemo(() => { if (!isCrossTeamAny || !strippedText) return ''; const oneLine = strippedText.replace(/\n+/g, ' ').trim(); if (!oneLine) return ''; return oneLine; }, [isCrossTeamAny, strippedText]); const rawSummary = useMemo(() => { if (idleSemantic?.hasPeerSummary && idleSemantic.peerSummary) { return idleSemantic.peerSummary; } if (isSlashCommandResult && message.commandOutput) { return message.summary || getCommandOutputSummary(message.text); } if (isSlashCommandMessage && slashCommandMeta) { if (slashCommandMeta.args) { const oneLine = slashCommandMeta.args.replace(/\n+/g, ' ').trim(); return `${slashCommandMeta.command} ${oneLine}`; } return slashCommandMeta.command; } if (crossTeamPreview) return crossTeamPreview; const s = getSanitizedInboxMessageSummary(message) || (structured ? getStructuredMessageSummary(structured) : '') || ''; if (s) return s; // Fallback: use the beginning of message text as preview for plain-text messages const plain = getSanitizedInboxMessageText(message).trim(); if (!plain) return ''; return plain.replace(/\n+/g, ' '); }, [ crossTeamPreview, isSlashCommandMessage, isSlashCommandResult, message.commandOutput, message, idleSemantic, slashCommandMeta, structured, ]); const summaryText = extractMarkdownPlainText(rawSummary); const compactPreviewMarkdown = useMemo(() => { if (idleSemantic?.hasPeerSummary && idleSemantic.peerSummary) { return idleSemantic.peerSummary; } if (isSlashCommandResult && message.commandOutput) { return message.summary || getCommandOutputSummary(message.text); } if (isSlashCommandMessage && slashCommandMeta) { if (slashCommandMeta.args) { const oneLine = slashCommandMeta.args.replace(/\n+/g, ' ').trim(); return `${slashCommandMeta.command} ${oneLine}`; } return slashCommandMeta.command; } if (crossTeamPreview) return crossTeamPreview; const formattedDisplayText = displayText?.trim() ?? ''; if (formattedDisplayText) { return formattedDisplayText; } return summaryText || rawSummary; }, [ crossTeamPreview, displayText, idleSemantic, isSlashCommandMessage, isSlashCommandResult, message, message.commandOutput, rawSummary, slashCommandMeta, summaryText, ]); const compactPreviewTooltipText = useMemo(() => { const normalized = extractMarkdownPlainText(compactPreviewMarkdown) .replace(/\n+/g, ' ') .trim(); return normalized || compactPreviewMarkdown; }, [compactPreviewMarkdown]); const commentTaskRef = message.messageKind === 'task_comment_notification' ? (message.taskRefs?.[0] ?? null) : null; const commentTaskDisplayId = commentTaskRef?.displayId ?? (commentTaskRef?.taskId ? `#${commentTaskRef.taskId.slice(0, 6)}` : null); // Permission request status icon (check/x/clock) const pendingApprovals = useStore(useShallow((s) => s.pendingApprovals)); const resolvedApprovals = useStore(useShallow((s) => s.resolvedApprovals)); const permissionIcon = useMemo(() => { if (!structured) return null; const type = typeof structured.type === 'string' ? structured.type : null; if (type !== 'permission_request') return null; const requestId = typeof structured.request_id === 'string' ? structured.request_id : null; if (!requestId) return null; const resolved = resolvedApprovals.get(requestId); if (resolved === true) { return ; } if (resolved === false) { return ; } const isPending = pendingApprovals.some((a) => a.requestId === requestId); if (isPending) { return ; } // Not in pending and not resolved — already handled before we started tracking return ; }, [structured, pendingApprovals, resolvedApprovals]); // Noise messages: minimal inline row if (noiseLabel) { return ( ); } if (idleSemantic?.uiPresentation === 'peer_summary' && idleSemantic.peerSummary) { return ( ); } if (bootstrapDisplay) { return ( ); } if (bootstrapAcknowledgement) { return ( ); } const messageType = structured && typeof structured.type === 'string' ? getMessageTypeLabel(structured.type) : null; const autoSummary = structured ? getStructuredMessageSummary(structured) : null; const handleCreateTask = useCallback((): void => { const subject = message.summary || autoSummary || `Task from ${message.from}`; const plainText = structured ? JSON.stringify(structured, null, 2) : getSanitizedInboxMessageText(message); const description = `From: ${message.from}\nAt: ${timestamp}\n\n${plainText}`.slice(0, 2000); onCreateTask?.(subject, description); }, [autoSummary, message.from, message.summary, message, onCreateTask, structured, timestamp]); const isHeaderClickable = isManaged && canToggleCollapse; const showChevron = isHeaderClickable && !compactHeader; const handleHeaderToggle = useCallback(() => { if (isHeaderClickable && collapseToggleKey) { onToggleCollapse?.(collapseToggleKey); } }, [collapseToggleKey, isHeaderClickable, onToggleCollapse]); const useCompactCollapsedHeader = compactHeader && !isExpanded; const senderBadge = isSlashCommandResult ? ( result ) : ( ); const messageTypeBadge = systemLabel ? ( {systemLabel} ) : commentTaskRef ? ( Comment ) : isSlashCommandResult && message.commandOutput ? ( {message.commandOutput.stream} ) : isSlashCommandMessage ? ( command ) : messageType ? ( {messageType} ) : null; const leadSourceBadge = message.source === 'lead_session' && !isSlashCommandResult ? ( session ) : message.source === 'lead_process' && !isSlashCommandResult ? ( live ) : null; const statusBadge = rateLimited ? ( Rate Limited ) : isApiError ? ( API Error ) : null; const recipientBadge = commentTaskRef && commentTaskDisplayId ? ( <> ) : message.to && message.to !== message.from ? ( <> {crossTeamTarget ? ( ) : null} {crossTeamSentMemberName || !crossTeamTarget ? ( ) : null} ) : null; const summaryContent = isSlashCommandResult && message.commandOutput ? ( {message.commandOutput.commandLabel} {message.summary || getCommandOutputSummary(message.text) || rawSummary} ) : isSlashCommandMessage && slashCommandMeta ? ( {slashCommandMeta.command} {slashCommandMeta.args ? ( {slashCommandMeta.args.replace(/\n+/g, ' ')} ) : (slashCommandMeta.knownDescription ?? knownSlashCommand?.description) ? ( {slashCommandMeta.knownDescription ?? knownSlashCommand?.description} ) : null} ) : onTaskIdClick ? ( renderInlineBoldSummary(rawSummary, onTaskIdClick) ) : ( renderInlineBoldSummary(rawSummary) ); return (
{/* Header — div with role=button (cannot use )}
{compactPreviewTooltipText}
) : !isExpanded ? (
{isUnread ? ( ) : null} {showChevron ? ( ) : null} {crossTeamOrigin ? ( ) : null} {senderBadge} {!compactHeader && formattedRole && !isSlashCommandResult ? ( {formattedRole} ) : null} {messageTypeBadge} {leadSourceBadge} {statusBadge} {recipientBadge}
{timestamp} {onExpand && expandItemKey && ( )}
{compactPreviewTooltipText}
) : ( <> {isUnread ? ( ) : null} {showChevron ? ( ) : null} {crossTeamOrigin ? ( ) : null} {senderBadge} {!compactHeader && formattedRole && !isSlashCommandResult ? ( {formattedRole} ) : null} {messageTypeBadge} {leadSourceBadge} {statusBadge} {recipientBadge} {summaryContent}
{timestamp} {onExpand && expandItemKey && ( )}
)} {/* Content — collapsed for system messages, expanded for others */} {isExpanded ? (
{structured ? (
{autoSummary && autoSummary !== messageType ? (

{autoSummary}

) : null}
Raw JSON
                    {JSON.stringify(structured, null, 2)}
                  
) : isSlashCommandResult && message.commandOutput ? (
{message.commandOutput.commandLabel} {message.commandOutput.stream}
                    {message.text}
                  
) : isSlashCommandMessage && slashCommandMeta ? (
{slashCommandMeta.command}
{(slashCommandMeta.knownDescription ?? knownSlashCommand?.description) ? (

{slashCommandMeta.knownDescription ?? knownSlashCommand?.description}

) : null} {slashCommandMeta.args ? (
{slashCommandMeta.args}
) : null}
) : parsedReply ? ( ) : displayText ? (
{onReply ? ( Reply to message ) : null} {onCreateTask ? ( Create task from message ) : null}
{ const link = (e.target as HTMLElement).closest( 'a[href^="task://"]' ); if (link) { e.preventDefault(); e.stopPropagation(); const href = link.getAttribute('href'); const parsedTaskLink = href ? parseTaskLinkHref(href) : null; if (parsedTaskLink?.taskId) onTaskIdClick(parsedTaskLink.taskId); } } : undefined } >
) : summaryText ? (

{summaryText}

) : null} {/* Auth error recovery action */} {isAuthError && onRestartTeam ? (

Authentication failed. Restarting the team will refresh the session and may resolve this issue. If the problem persists, check your API credentials or try again later.

) : null} {message.attachments?.length && message.messageId ? ( ) : null}
) : null}
); }, (prev, next) => prev.teamName === next.teamName && prev.localMemberNames === next.localMemberNames && prev.memberRole === next.memberRole && prev.memberColor === next.memberColor && prev.recipientColor === next.recipientColor && prev.isUnread === next.isUnread && prev.memberColorMap === next.memberColorMap && areStringArraysEqual(prev.teamNames, next.teamNames) && areStringMapsEqual(prev.teamColorByName, next.teamColorByName) && prev.onTeamClick === next.onTeamClick && prev.onMemberNameClick === next.onMemberNameClick && prev.onCreateTask === next.onCreateTask && prev.onReply === next.onReply && prev.onTaskIdClick === next.onTaskIdClick && prev.onRestartTeam === next.onRestartTeam && prev.zebraShade === next.zebraShade && prev.collapseMode === next.collapseMode && prev.isCollapsed === next.isCollapsed && prev.canToggleCollapse === next.canToggleCollapse && prev.collapseToggleKey === next.collapseToggleKey && prev.onToggleCollapse === next.onToggleCollapse && prev.compactHeader === next.compactHeader && prev.onExpand === next.onExpand && prev.expandItemKey === next.expandItemKey && prev.onExpandContent === next.onExpandContent && areMessagesEquivalentForActivityItem(prev.message, next.message) ); ActivityItem.displayName = 'ActivityItem';