import { memo, useCallback, useEffect, useLayoutEffect, useRef, useState, type AnimationEvent as ReactAnimationEvent, type JSX, type PointerEvent as ReactPointerEvent, type RefObject, type ReactNode } from "react"; import { ChevronDown, ChevronUp } from "lucide-react"; import { Button, cn, StatusDot } from "@tutti-os/ui-system"; import { useTranslation } from "../i18n/index"; import { AgentInteractivePromptSurface } from "../shared/agentConversation/components/AgentInteractivePromptSurface"; import { AgentMessageMarkdown } from "../shared/AgentMessageMarkdown"; import { AgentVerticalScrollArea } from "../shared/AgentVerticalScrollArea"; import { workspaceAgentActivityStatusLabel } from "../shared/workspaceAgentActivityStatusLabel"; import type { WorkspaceLinkAction } from "../actions/workspaceLinkActions"; import { isWaitingMessageCenterItem, type WorkspaceAgentMessageCenterItem } from "./workspaceAgentMessageCenterModel"; import { LazyMessageCenterTooltip, MessageCenterIdentityAvatarMark, MessageCenterOpenChatButton, ProjectPathInfo, messageCenterStatusTone, messageCenterStatusToneClass, statusClass } from "./WorkspaceAgentMessageCenterIdentity"; export * from "./WorkspaceAgentMessageCenterIdentity"; import { buildWorkspaceAgentInteractivePromptLabels, messageCenterStackPreviewNodes, messageCenterStackPreviewText } from "./workspaceAgentMessageCenterPreview"; export * from "./workspaceAgentMessageCenterPreview"; export interface WorkspaceAgentMessageCenterCardProps { item: WorkspaceAgentMessageCenterItem; cardRef?: (node: HTMLElement | null) => void; actionsAccessory?: ReactNode; footerAccessory?: ReactNode; headerAccessory?: ReactNode; highlighted?: boolean; interactive?: boolean; isSubmitting: boolean; isInteractionDisabled?: boolean; interactionDisabledReason?: string | null; lazySummary?: boolean; promptVariant?: "compact" | "full"; showSummaryWithPrompt?: boolean; summaryAccessory?: ReactNode; onLinkAction?: (action: WorkspaceLinkAction) => void; onOpenChat: (input: { agentSessionId: string; provider: string }) => void; onSubmitPrompt: (input: { action?: string; optionId?: string; payload?: Record; requestId: string; }) => void; } interface WorkspaceAgentMessageCenterStackProps { className?: string; expanded: boolean; groupId: string; highlightedItemId?: string | null; items: WorkspaceAgentMessageCenterItem[]; renderCard: ( item: WorkspaceAgentMessageCenterItem, options?: { stackedIndex?: number } ) => ReactNode; onCollapse: (groupId: string) => void; onExpand: (groupId: string) => void; } function stopMessageCenterTextPointerPropagation( event: ReactPointerEvent ): void { event.stopPropagation(); } export const WorkspaceAgentMessageCenterCard = memo( function WorkspaceAgentMessageCenterCard({ cardRef, actionsAccessory, footerAccessory, headerAccessory, highlighted = false, interactive = true, item, isSubmitting, isInteractionDisabled = false, interactionDisabledReason = null, lazySummary = false, promptVariant = "compact", showSummaryWithPrompt = true, summaryAccessory, onLinkAction, onOpenChat, onSubmitPrompt }: WorkspaceAgentMessageCenterCardProps): JSX.Element { "use memo"; const { t } = useTranslation(); const prompt = item.pendingPrompt; const displayTitle = item.title.trim(); const summary = messageCenterVisibleSummary(item); const displayStatus = statusClass(item); const statusTone = messageCenterStatusTone(item); const statusLabel = workspaceAgentActivityStatusLabel(displayStatus, t); const promptInteractionDisabled = interactive && prompt !== null && isInteractionDisabled; const promptDisabledReason = interactionDisabledReason?.trim() ?? ""; const promptSurface = prompt ? ( ) : null; const disabledPromptSurface = (
{promptSurface}
); return (

{displayTitle}

{item.cwd ? : null}
{headerAccessory} {statusLabel}
{summary && (showSummaryWithPrompt || !prompt || !interactive) ? ( ) : null} {summaryAccessory} {prompt && interactive ? (
{promptInteractionDisabled && promptDisabledReason ? ( {disabledPromptSurface} ) : promptInteractionDisabled ? ( disabledPromptSurface ) : ( promptSurface )}
) : null} {footerAccessory}
); } ); const STACK_PRESENCE_FALLBACK_MS = 380; const MESSAGE_CENTER_STACK_INITIAL_RENDER_COUNT = 24; const MESSAGE_CENTER_STACK_RENDER_BATCH_SIZE = 24; const MESSAGE_CENTER_STACK_RENDER_BATCH_DELAY_MS = 40; const MESSAGE_CENTER_SUMMARY_LAZY_ROOT_MARGIN = "480px 0px"; const MESSAGE_CENTER_SUMMARY_OVERFLOW_MEASURE_DELAY_MS = 80; function useStackRegionPresence(visible: boolean): { closing: boolean; mounted: boolean; onAnimationEnd: (event: ReactAnimationEvent) => void; } { const [mounted, setMounted] = useState(visible); const closing = mounted && !visible; useLayoutEffect(() => { if (visible) { setMounted(true); } }, [visible]); useEffect(() => { if (!closing) { return undefined; } const timeoutId = window.setTimeout(() => { setMounted(false); }, STACK_PRESENCE_FALLBACK_MS); return () => { window.clearTimeout(timeoutId); }; }, [closing]); const onAnimationEnd = useCallback( (event: ReactAnimationEvent) => { if (event.target === event.currentTarget && closing) { setMounted(false); } }, [closing] ); return { closing, mounted, onAnimationEnd }; } export const WorkspaceAgentMessageCenterStack = memo( function WorkspaceAgentMessageCenterStack({ className, expanded, groupId, highlightedItemId, items, renderCard, onCollapse, onExpand }: WorkspaceAgentMessageCenterStackProps): JSX.Element | null { "use memo"; const { t } = useTranslation(); const summaryRegion = useStackRegionPresence(!expanded); const cardsRegion = useStackRegionPresence(expanded); const visibleItems = useBatchedMessageCenterStackItems({ expanded, highlightedItemId, items, mounted: cardsRegion.mounted }); if (items.length < 2) { return null; } const collapseLabel = t( "agentHost.workspaceAgentMessageCenterCollapseStackAria" ); return (
{summaryRegion.mounted ? (
) : null} {cardsRegion.mounted ? (
{t( "agentHost.workspaceAgentMessageCenterStackSummaryCount", { count: items.length } )}
{visibleItems.map((item, stackedIndex) => renderCard(item, { stackedIndex }) )}
) : null}
); }, areMessageCenterStackPropsEqual ); function areMessageCenterStackPropsEqual( previous: WorkspaceAgentMessageCenterStackProps, next: WorkspaceAgentMessageCenterStackProps ): boolean { if ( previous.className !== next.className || previous.expanded !== next.expanded || previous.groupId !== next.groupId || previous.onCollapse !== next.onCollapse || previous.onExpand !== next.onExpand || previous.renderCard !== next.renderCard ) { return false; } if (next.expanded) { return ( previous.highlightedItemId === next.highlightedItemId && previous.items === next.items ); } return ( messageCenterCollapsedStackSignature(previous.items) === messageCenterCollapsedStackSignature(next.items) ); } function messageCenterCollapsedStackSignature( items: readonly WorkspaceAgentMessageCenterItem[] ): string { const firstItem = items[0]; if (!firstItem) { return "0"; } const hasWaiting = items.some(isWaitingMessageCenterItem) ? "1" : "0"; return [ items.length, firstItem.id, firstItem.provider, firstItem.userId ?? "", firstItem.identity?.userName ?? "", firstItem.identity?.agentName ?? "", hasWaiting, messageCenterStackPreviewText(firstItem) ].join(":"); } function useBatchedMessageCenterStackItems({ expanded, highlightedItemId, items, mounted }: { expanded: boolean; highlightedItemId?: string | null; items: WorkspaceAgentMessageCenterItem[]; mounted: boolean; }): WorkspaceAgentMessageCenterItem[] { const limit = items.length; const highlightedIndex = highlightedItemId === null || highlightedItemId === undefined ? -1 : items.findIndex((item) => item.id === highlightedItemId); const initialCount = Math.min( limit, Math.max(MESSAGE_CENTER_STACK_INITIAL_RENDER_COUNT, highlightedIndex + 1) ); const [renderCount, setRenderCount] = useState(initialCount); useEffect(() => { if (!mounted) { setRenderCount(initialCount); return; } setRenderCount((current) => Math.min(Math.max(current, initialCount), limit) ); }, [initialCount, limit, mounted]); useEffect(() => { if (!expanded || !mounted || renderCount >= limit) { return undefined; } const timeoutId = window.setTimeout(() => { setRenderCount((current) => Math.min(current + MESSAGE_CENTER_STACK_RENDER_BATCH_SIZE, limit) ); }, MESSAGE_CENTER_STACK_RENDER_BATCH_DELAY_MS); return () => { window.clearTimeout(timeoutId); }; }, [expanded, limit, mounted, renderCount]); return items.slice(0, Math.min(renderCount, limit)); } function MessageCenterStackSummary({ groupId, items, onExpand }: { groupId: string; items: WorkspaceAgentMessageCenterItem[]; onExpand: (groupId: string) => void; }): JSX.Element | null { "use memo"; const { t } = useTranslation(); const firstItem = items[0]; const hasWaiting = items.some((item) => isWaitingMessageCenterItem(item)); if (!firstItem) { return null; } return ( ); } function messageCenterVisibleSummary( item: WorkspaceAgentMessageCenterItem ): string { const summary = item.digest.primary.summary.trim(); if ( item.pendingPrompt?.kind === "approval" && isGenericApprovalSummary(summary) ) { return ""; } return summary; } function isGenericApprovalSummary(summary: string): boolean { const normalized = summary.trim().toLowerCase(); return normalized === "approval"; } export function MessageCenterSummary({ emptyLabel, item, lazy, onLinkAction, summary }: { emptyLabel: string; item: WorkspaceAgentMessageCenterItem; lazy: boolean; onLinkAction?: (action: WorkspaceLinkAction) => void; summary: string; }): JSX.Element { "use memo"; const summaryRef = useRef(null); const [isOverflowing, setIsOverflowing] = useState(false); const shouldRenderRichSummary = useLazyMessageCenterSummaryReady( summaryRef, lazy ); const shouldMeasureOverflow = useDeferredMessageCenterSummaryMeasureReady( summaryRef, shouldRenderRichSummary ); useEffect(() => { if (!shouldMeasureOverflow) { setIsOverflowing(false); return undefined; } const element = summaryRef.current; if (!element) { return undefined; } const updateOverflowState = () => { setIsOverflowing(element.scrollHeight > element.clientHeight + 1); }; let resizeObserver: ResizeObserver | null = null; const timeoutId = window.setTimeout(() => { updateOverflowState(); resizeObserver = new ResizeObserver(updateOverflowState); resizeObserver.observe(element); }, MESSAGE_CENTER_SUMMARY_OVERFLOW_MEASURE_DELAY_MS); return () => { window.clearTimeout(timeoutId); resizeObserver?.disconnect(); }; }, [shouldMeasureOverflow, summary]); return ( {summary && shouldRenderRichSummary ? ( ) : summary ? (
{summary}
) : ( emptyLabel )}
); } function useLazyMessageCenterSummaryReady( ref: RefObject, lazy: boolean ): boolean { const [ready, setReady] = useState(!lazy); useEffect(() => { setReady(!lazy); }, [lazy]); useEffect(() => { if (!lazy) { return undefined; } if (ready) { return undefined; } const element = ref.current; if (!element || typeof IntersectionObserver === "undefined") { setReady(true); return undefined; } const observer = new IntersectionObserver( (entries) => { if (entries.some((entry) => entry.isIntersecting)) { setReady(true); observer.disconnect(); } }, { rootMargin: MESSAGE_CENTER_SUMMARY_LAZY_ROOT_MARGIN } ); observer.observe(element); return () => observer.disconnect(); }, [lazy, ready, ref]); return ready; } function useDeferredMessageCenterSummaryMeasureReady( ref: RefObject, enabled: boolean ): boolean { const [ready, setReady] = useState(false); useEffect(() => { if (!enabled) { setReady(false); return undefined; } if (ready) { return undefined; } const element = ref.current; if (!element || typeof IntersectionObserver === "undefined") { const timeoutId = window.setTimeout( () => setReady(true), MESSAGE_CENTER_SUMMARY_OVERFLOW_MEASURE_DELAY_MS ); return () => { window.clearTimeout(timeoutId); }; } let timeoutId: number | null = null; const observer = new IntersectionObserver( (entries) => { if (!entries.some((entry) => entry.isIntersecting)) { return; } observer.disconnect(); timeoutId = window.setTimeout( () => setReady(true), MESSAGE_CENTER_SUMMARY_OVERFLOW_MEASURE_DELAY_MS ); }, { rootMargin: MESSAGE_CENTER_SUMMARY_LAZY_ROOT_MARGIN } ); observer.observe(element); return () => { observer.disconnect(); if (timeoutId !== null) { window.clearTimeout(timeoutId); } }; }, [enabled, ready, ref]); return ready; }