import type { ReactNode } from "react"; import { deriveStreamTurnTiming, type StreamTurnTiming } from "@/timeline/turn-time"; import type { StreamItem } from "@/types/stream"; import { findMountedWindowStart, getMountedRecentStreamItems } from "./history-window"; import { getWebPartialVirtualizationThreshold } from "./web-virtualization"; import { orderHeadForStreamRenderStrategy, orderTailForStreamRenderStrategy } from "./strategy"; import { resolveStreamRenderStrategy } from "./strategy-resolver"; export interface StreamRenderSegments { historyVirtualized: StreamItem[]; historyMounted: StreamItem[]; liveHead: StreamItem[]; } export interface StreamHistoryBoundary { hasVirtualizedHistory: boolean; hasMountedHistory: boolean; hasLiveHead: boolean; } export interface StreamRenderAuxiliary { pendingPermissions: ReactNode; turnFooter: ReactNode; } export interface AgentStreamRenderModel { history: StreamItem[]; segments: StreamRenderSegments; turnTiming: StreamTurnTiming; boundary: StreamHistoryBoundary; auxiliary: StreamRenderAuxiliary; } export interface BuildAgentStreamRenderModelInput { isTurnActive: boolean; activeTurnStartedAt: Date | null; tail: StreamItem[]; head: StreamItem[]; platform: "web" | "native"; isMobileBreakpoint: boolean; historyStart?: number; } const EMPTY_STREAM_ITEMS: StreamItem[] = []; const EMPTY_AUXILIARY: StreamRenderAuxiliary = { pendingPermissions: null, turnFooter: null, }; const orderedTailCache = new WeakMap>(); const orderedHeadCache = new WeakMap>(); const splitHistoryCache = new WeakMap< StreamItem[], Map> >(); const turnTimingCache = new WeakMap< StreamItem[], WeakMap> >(); function getOrderedItems(params: { cache: WeakMap>; source: StreamItem[]; cacheKey: string; order: (items: StreamItem[]) => StreamItem[]; }): StreamItem[] { const { cache, source, cacheKey, order } = params; let cachedByKey = cache.get(source); if (!cachedByKey) { cachedByKey = new Map(); cache.set(source, cachedByKey); } const cached = cachedByKey.get(cacheKey); if (cached) { return cached; } const ordered = order(source); cachedByKey.set(cacheKey, ordered); return ordered; } function splitOrderedTail(params: { orderedTail: StreamItem[]; platform: "web" | "native"; isMobileBreakpoint: boolean; }): Pick { const { orderedTail, platform, isMobileBreakpoint } = params; const shouldSplitHistory = platform === "web" && !isMobileBreakpoint && orderedTail.length > getWebPartialVirtualizationThreshold(); const cacheKey = `${platform}:${isMobileBreakpoint}:${getMountedRecentStreamItems()}:${shouldSplitHistory}`; let cachedByKey = splitHistoryCache.get(orderedTail); if (!cachedByKey) { cachedByKey = new Map(); splitHistoryCache.set(orderedTail, cachedByKey); } const cached = cachedByKey.get(cacheKey); if (cached) { return cached; } if (!shouldSplitHistory) { const unsplit = { history: orderedTail, segments: { historyVirtualized: EMPTY_STREAM_ITEMS, historyMounted: orderedTail, liveHead: EMPTY_STREAM_ITEMS, }, } satisfies Pick; cachedByKey.set(cacheKey, unsplit); return unsplit; } const mountedWindowStart = findMountedWindowStart({ items: orderedTail, minMountedCount: getMountedRecentStreamItems(), }); const split = { history: orderedTail, segments: { historyVirtualized: orderedTail.slice(0, mountedWindowStart), historyMounted: orderedTail.slice(mountedWindowStart), liveHead: EMPTY_STREAM_ITEMS, }, } satisfies Pick; cachedByKey.set(cacheKey, split); return split; } function getTurnTiming(params: { isTurnActive: boolean; activeTurnStartedAt: Date | null; tail: StreamItem[]; head: StreamItem[]; }): StreamTurnTiming { let cachedByHead = turnTimingCache.get(params.tail); if (!cachedByHead) { cachedByHead = new WeakMap(); turnTimingCache.set(params.tail, cachedByHead); } let cachedByActivity = cachedByHead.get(params.head); if (!cachedByActivity) { cachedByActivity = new Map(); cachedByHead.set(params.head, cachedByActivity); } const activityKey = `${params.isTurnActive}:${params.activeTurnStartedAt?.getTime() ?? "none"}`; const cached = cachedByActivity.get(activityKey); if (cached) { return cached; } const timing = deriveStreamTurnTiming(params); cachedByActivity.set(activityKey, timing); return timing; } export function buildAgentStreamRenderModel( input: BuildAgentStreamRenderModelInput, ): AgentStreamRenderModel { const strategy = resolveStreamRenderStrategy({ platform: input.platform === "web" ? "web" : "native", isMobileBreakpoint: input.isMobileBreakpoint, }); const orderingCacheKey = `${input.platform}:${input.isMobileBreakpoint}`; const renderedTail = input.historyStart ? input.tail.slice(input.historyStart) : input.tail; const orderedTail = getOrderedItems({ cache: orderedTailCache, source: renderedTail, cacheKey: orderingCacheKey, order: (items) => orderTailForStreamRenderStrategy({ strategy, streamItems: items, }), }); const orderedHead = getOrderedItems({ cache: orderedHeadCache, source: input.head, cacheKey: orderingCacheKey, order: (items) => orderHeadForStreamRenderStrategy({ strategy, streamHead: items, }), }); const splitHistory = splitOrderedTail({ orderedTail, platform: input.platform, isMobileBreakpoint: input.isMobileBreakpoint, }); const turnTiming = getTurnTiming({ isTurnActive: input.isTurnActive, activeTurnStartedAt: input.activeTurnStartedAt, tail: renderedTail, head: input.head, }); return { history: splitHistory.history, segments: { ...splitHistory.segments, liveHead: orderedHead, }, turnTiming, boundary: { hasVirtualizedHistory: splitHistory.segments.historyVirtualized.length > 0, hasMountedHistory: splitHistory.segments.historyMounted.length > 0, hasLiveHead: orderedHead.length > 0, }, auxiliary: EMPTY_AUXILIARY, }; }