import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, type JSX } from "react"; import { Button, cn, Drawer, DrawerContent, DrawerDescription, DrawerTitle, TooltipProvider } from "@tutti-os/ui-system"; import type { I18nRuntime } from "@tutti-os/ui-i18n-runtime"; import { AgentGuiI18nProvider, useTranslation, type AgentGuiI18nLocale } from "../i18n/index"; import { AgentVerticalScrollArea } from "../shared/AgentVerticalScrollArea"; import type { WorkspaceLinkAction } from "../actions/workspaceLinkActions"; import { isInteractiveMessageCenterItem, selectMessageCenterAttentionDeckItems, type WorkspaceAgentMessageCenterItem, type WorkspaceAgentMessageCenterModel } from "./workspaceAgentMessageCenterModel"; import { WorkspaceAgentMessageCenterAttentionDeck } from "./WorkspaceAgentMessageCenterAttentionDeck"; import { MessageCenterViewMenu } from "./WorkspaceAgentMessageCenterViewControls"; import { resolveMessageCenterNotificationAction, WorkspaceAgentMessageCenterStack, WorkspaceAgentMessageCenterCard, type WorkspaceAgentMessageCenterCardProps } from "./WorkspaceAgentMessageCenterCard"; import { MessageCenterGroupHeading } from "./WorkspaceAgentMessageCenterGroupHeading"; import { buildMessageCenterProviderOptions, buildMessageCenterStatusOptions, groupMessageCenterItems, itemMatchesViewFilters, messageCenterStackRenderId, messageCenterStackScrollSyncSegment, partitionMessageCenterItemsByAgentUser, statusFilterSummary, type MessageCenterGroupBy, type MessageCenterStatusFilter } from "./workspaceAgentMessageCenterViewModel"; import { readMessageCenterFilterPreferences, writeMessageCenterFilterPreferences } from "./messageCenterFilterPreferences"; export { buildWorkspaceAgentInteractivePromptLabels, WorkspaceAgentMessageCenterCard } from "./WorkspaceAgentMessageCenterCard"; export type { WorkspaceAgentMessageCenterCardProps } from "./WorkspaceAgentMessageCenterCard"; const MESSAGE_CENTER_TOOLTIP_DELAY_MS = 300; const MESSAGE_CENTER_STACK_EAGER_SUMMARY_COUNT = 8; export type WorkspaceAgentMessageCenterPanelPresentation = | "drawer" | "embedded"; type WorkspaceAgentMessageCenterPromptInput = Parameters< WorkspaceAgentMessageCenterCardProps["onSubmitPrompt"] >[0]; export interface WorkspaceAgentMessageCenterPanelProps { i18n?: I18nRuntime | null; locale?: AgentGuiI18nLocale; open: boolean; model: WorkspaceAgentMessageCenterModel; highlightedItemId?: string | null; portalContainer?: HTMLElement | null; drawerTopInsetPx?: number; presentation?: WorkspaceAgentMessageCenterPanelPresentation; onClose: () => void; onHighlightedItemSettled?: (itemId: string) => void; onLinkAction?: (action: WorkspaceLinkAction) => void; onNotificationActioned?: (input: { action: string; provider: string; }) => void; onOpenChat: (input: { agentSessionId: string; provider: string }) => void; promptStatus?: ( item: WorkspaceAgentMessageCenterItem ) => "idle" | "responding" | "unknown" | "failed"; onSubmitPrompt: (input: { action?: string; agentSessionId: string; optionId?: string; payload?: Record; promptKind?: string; requestId: string; turnId?: string; }) => Promise | void; } export const WorkspaceAgentMessageCenterPanel = memo( function WorkspaceAgentMessageCenterPanel({ open, i18n, locale, model, highlightedItemId = null, portalContainer = null, drawerTopInsetPx, presentation = "drawer", onClose, onHighlightedItemSettled, onLinkAction, onNotificationActioned, onOpenChat, promptStatus = () => "idle", onSubmitPrompt }: WorkspaceAgentMessageCenterPanelProps): JSX.Element | null { "use memo"; if (!open) { return null; } return ( ); } ); function WorkspaceAgentMessageCenterPanelContent({ open, model, highlightedItemId = null, portalContainer = null, drawerTopInsetPx, presentation = "drawer", onClose, onHighlightedItemSettled, onLinkAction, onNotificationActioned, onOpenChat, promptStatus = () => "idle", onSubmitPrompt }: Omit< WorkspaceAgentMessageCenterPanelProps, "i18n" | "locale" >): JSX.Element | null { "use memo"; const { t } = useTranslation(); const [initialFilters] = useState(readMessageCenterFilterPreferences); const [groupBy, setGroupBy] = useState( initialFilters.groupBy ); const [statusFilters, setStatusFilters] = useState | null>( initialFilters.statusFilters ); const [providerFilters, setProviderFilters] = useState | null>( initialFilters.providerFilters ); useEffect(() => { writeMessageCenterFilterPreferences({ groupBy, statusFilters, providerFilters }); }, [groupBy, statusFilters, providerFilters]); const [expandedStackIds, setExpandedStackIds] = useState>( () => new Set() ); const itemNodesRef = useRef>(new Map()); const lastScrolledHighlightedItemIdRef = useRef(null); const lastFilterResetHighlightedItemIdRef = useRef(null); const statusOptions = useMemo( () => buildMessageCenterStatusOptions(model.counts, t), [model.counts, t] ); const providerOptions = useMemo( () => buildMessageCenterProviderOptions(model.items), [model.items] ); const visibleItems = useMemo( () => model.items.filter((item) => itemMatchesViewFilters({ item, providerFilters, statusFilters }) ), [model.items, providerFilters, statusFilters] ); const deckItems = useMemo( () => selectMessageCenterAttentionDeckItems(visibleItems), [visibleItems] ); const listItems = useMemo( () => visibleItems.filter((item) => !isInteractiveMessageCenterItem(item)), [visibleItems] ); const itemGroups = useMemo( () => groupMessageCenterItems(listItems, groupBy, t), [groupBy, t, listItems] ); const itemGroupStacks = useMemo( () => itemGroups.map((group) => ({ ...group, stacks: partitionMessageCenterItemsByAgentUser(group.items) })), [itemGroups] ); const highlightedItem = useMemo( () => highlightedItemId ? (model.items.find((item) => item.id === highlightedItemId) ?? null) : null, [highlightedItemId, model.items] ); const activeStatusSummary = statusFilterSummary(statusFilters, statusOptions); const scrollSyncKey = useMemo( () => [ groupBy, activeStatusSummary, ...deckItems.map((item) => `deck:${item.id}`), ...itemGroupStacks.flatMap((group) => group.stacks.map((stack) => { const stackId = messageCenterStackRenderId(group.id, stack.id); return messageCenterStackScrollSyncSegment({ expanded: expandedStackIds.has(stackId), groupId: group.id, stack }); }) ) ].join("|"), [activeStatusSummary, deckItems, expandedStackIds, groupBy, itemGroupStacks] ); const hasActiveFilters = statusFilters !== null || providerFilters !== null; const headerSummary = useMemo(() => { if (hasActiveFilters) { return t("agentHost.workspaceAgentMessageCenterSummaryFiltered", { count: visibleItems.length, total: model.counts.all }); } if (model.counts.waiting > 0) { return t("agentHost.workspaceAgentMessageCenterSummaryWaiting", { count: model.counts.all, waiting: model.counts.waiting }); } if (model.counts.completed > 0) { return t("agentHost.workspaceAgentMessageCenterSummaryCompleted", { count: model.counts.all, completed: model.counts.completed }); } return t("agentHost.workspaceAgentMessageCenterSummaryCount", { count: model.counts.all }); }, [ hasActiveFilters, model.counts.all, model.counts.completed, model.counts.waiting, t, visibleItems.length ]); const submitPrompt = useCallback( async ( item: WorkspaceAgentMessageCenterItem, input: { action?: string; optionId?: string; payload?: Record; requestId: string; } ) => { const notificationAction = resolveMessageCenterNotificationAction( item, input ); if (notificationAction) { onNotificationActioned?.({ action: notificationAction, provider: item.provider }); } await onSubmitPrompt({ ...input, agentSessionId: item.pendingInteractionTarget?.agentSessionId ?? item.agentSessionId, ...(item.pendingInteractionTarget ? { turnId: item.pendingInteractionTarget.turnId } : {}), promptKind: item.pendingPrompt?.kind }); }, [onNotificationActioned, onSubmitPrompt] ); const setItemNode = useCallback( (itemId: string, node: HTMLElement | null) => { if (node) { itemNodesRef.current.set(itemId, node); } else { itemNodesRef.current.delete(itemId); } }, [] ); const toggleStatusFilter = useCallback( (status: MessageCenterStatusFilter) => { setStatusFilters((current) => { const next = new Set( current ?? statusOptions.map((option) => option.value) ); if (next.has(status)) { next.delete(status); } else { next.add(status); } return next.size === statusOptions.length ? null : next; }); }, [statusOptions] ); const toggleProviderFilter = useCallback( (provider: string) => { setProviderFilters((current) => { const next = new Set( current ?? providerOptions.map((option) => option.value) ); if (next.has(provider)) { next.delete(provider); } else { next.add(provider); } return next.size === providerOptions.length ? null : next; }); }, [providerOptions] ); const clearFilters = useCallback(() => { setStatusFilters(null); setProviderFilters(null); }, []); const expandStack = useCallback((groupId: string) => { setExpandedStackIds((current) => { if (current.has(groupId)) { return current; } const next = new Set(current); next.add(groupId); return next; }); }, []); const collapseStack = useCallback((groupId: string) => { setExpandedStackIds((current) => { if (!current.has(groupId)) { return current; } const next = new Set(current); next.delete(groupId); return next; }); }, []); useLayoutEffect(() => { if (!open || !highlightedItem) { return; } if (lastFilterResetHighlightedItemIdRef.current === highlightedItem.id) { return; } if ( itemMatchesViewFilters({ item: highlightedItem, providerFilters, statusFilters }) ) { return; } lastFilterResetHighlightedItemIdRef.current = highlightedItem.id; setStatusFilters(null); setProviderFilters(null); }, [highlightedItem, open, providerFilters, statusFilters]); useLayoutEffect(() => { if (!open || !highlightedItemId) { return; } for (const group of itemGroupStacks) { for (const stack of group.stacks) { if ( stack.items.length > 1 && stack.items.some((item) => item.id === highlightedItemId) ) { expandStack(messageCenterStackRenderId(group.id, stack.id)); return; } } } }, [expandStack, highlightedItemId, itemGroupStacks, open]); useLayoutEffect(() => { if (!open || !highlightedItemId) { lastScrolledHighlightedItemIdRef.current = null; return; } if ( highlightedItem && !itemMatchesViewFilters({ item: highlightedItem, providerFilters, statusFilters }) ) { return; } if (lastScrolledHighlightedItemIdRef.current === highlightedItemId) { return; } const target = itemNodesRef.current.get(highlightedItemId); if (!target) { return; } lastScrolledHighlightedItemIdRef.current = highlightedItemId; target.scrollIntoView({ behavior: "smooth", block: "center" }); target.focus({ preventScroll: true }); }, [ expandedStackIds, highlightedItem, highlightedItemId, open, providerFilters, statusFilters, visibleItems ]); useEffect(() => { if (!open || !highlightedItemId) { return undefined; } const timeoutId = window.setTimeout(() => { onHighlightedItemSettled?.(highlightedItemId); }, 3200); return () => { window.clearTimeout(timeoutId); }; }, [highlightedItemId, onHighlightedItemSettled, open]); const renderMessageCenterCard = useCallback( ( item: WorkspaceAgentMessageCenterItem, options: { stackedIndex?: number } = {} ) => { const highlighted = item.id === highlightedItemId; const stackedIndex = options.stackedIndex; const card = ( = MESSAGE_CENTER_STACK_EAGER_SUMMARY_COUNT } registerNode={setItemNode} onLinkAction={onLinkAction} onOpenChat={onOpenChat} onSubmitPrompt={submitPrompt} /> ); if (stackedIndex === undefined) { return card; } return (
{card}
); }, [ highlightedItemId, onLinkAction, onOpenChat, promptStatus, setItemNode, submitPrompt ] ); const panelContents = (
{presentation === "drawer" ? (
{t("agentHost.workspaceAgentMessageCenterTitle")} {headerSummary}
) : (

{headerSummary}

)}
{deckItems.length > 0 || listItems.length > 0 ? (
{deckItems.length > 0 ? ( promptStatus(item) === "responding" || promptStatus(item) === "unknown" } registerNode={setItemNode} onLinkAction={onLinkAction} onOpenChat={onOpenChat} onSubmitPrompt={(item, input) => void submitPrompt(item, input)} /> ) : null} {itemGroupStacks.map((group) => (
{(() => { return group.stacks.map((stack) => { const firstItem = stack.items[0]; if (!firstItem) { return null; } if (stack.items.length === 1) { return renderMessageCenterCard(firstItem); } const stackId = messageCenterStackRenderId( group.id, stack.id ); return ( ); }); })()}
))}
) : model.items.length > 0 ? (
{t("agentHost.workspaceAgentMessageCenterFilteredEmpty")}
) : (
{t("agentHost.workspaceAgentMessageCenterEmpty")}
)}
); if (presentation === "embedded") { return (
{panelContents}
); } return ( { if (!nextOpen) { onClose(); } }} > {panelContents} ); } const MessageCenterCard = WorkspaceAgentMessageCenterCard; const MessageCenterStack = WorkspaceAgentMessageCenterStack; const MessageCenterRenderedCard = memo(function MessageCenterRenderedCard({ highlighted, isSubmitting, item, lazySummary, onLinkAction, onOpenChat, onSubmitPrompt, registerNode }: { highlighted: boolean; isSubmitting: boolean; item: WorkspaceAgentMessageCenterItem; lazySummary: boolean; onLinkAction?: (action: WorkspaceLinkAction) => void; onOpenChat: (input: { agentSessionId: string; provider: string }) => void; onSubmitPrompt: ( item: WorkspaceAgentMessageCenterItem, input: WorkspaceAgentMessageCenterPromptInput ) => void; registerNode: (itemId: string, node: HTMLElement | null) => void; }): JSX.Element { const cardRef = useCallback( (node: HTMLElement | null) => { registerNode(item.id, node); }, [item.id, registerNode] ); const handleSubmitPrompt = useCallback( (input: WorkspaceAgentMessageCenterPromptInput) => { onSubmitPrompt(item, input); }, [item, onSubmitPrompt] ); return ( ); });