diff --git a/src/apps/desktop/src/lib.rs b/src/apps/desktop/src/lib.rs index ba3118d70..fb1eb3df2 100644 --- a/src/apps/desktop/src/lib.rs +++ b/src/apps/desktop/src/lib.rs @@ -660,7 +660,7 @@ pub async fn run() { // Set up system tray icon. let step_started = Instant::now(); - if let Err(error) = crate::tray::setup_tray(app) { + if let Err(error) = crate::tray::setup_tray(app, &startup_trace) { log::warn!("Failed to set up system tray: {}", error); } startup_trace.record_elapsed_step("native_setup", "setup_tray", step_started); diff --git a/src/apps/desktop/src/tray.rs b/src/apps/desktop/src/tray.rs index 0d76769dc..fa9d021b4 100644 --- a/src/apps/desktop/src/tray.rs +++ b/src/apps/desktop/src/tray.rs @@ -14,6 +14,7 @@ //! periodically, and after locale changes. use std::sync::OnceLock; +use std::time::Instant; use tauri::menu::{CheckMenuItemBuilder, MenuBuilder, MenuItemBuilder}; use tauri::tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent}; @@ -24,6 +25,7 @@ use bitfun_core::service::config::types::AIExperienceConfig; use bitfun_core::service::i18n::LocaleId; use crate::api::app_state::AppState; +use crate::startup_trace::DesktopStartupTrace; static TRAY_ICON: OnceLock = OnceLock::new(); @@ -164,13 +166,19 @@ async fn tray_toggle_desktop_pet(app: &AppHandle) -> Result<(), String> { } /// Build and attach the system tray icon to the Tauri application. -pub fn setup_tray(app: &tauri::App) -> Result<(), Box> { +pub fn setup_tray( + app: &tauri::App, + startup_trace: &DesktopStartupTrace, +) -> Result<(), Box> { + let step_started = Instant::now(); let pet_item = CheckMenuItemBuilder::with_id("toggle_desktop_pet", STRINGS_EN_US.desktop_pet) .checked(false) .build(app)?; let show_item = MenuItemBuilder::with_id("show_window", STRINGS_EN_US.show_app).build(app)?; let quit_item = MenuItemBuilder::with_id("quit", STRINGS_EN_US.quit_app).build(app)?; + startup_trace.record_elapsed_step("native_setup", "setup_tray.menu_items", step_started); + let step_started = Instant::now(); let initial_menu = MenuBuilder::new(app) .item(&pet_item) .separator() @@ -178,12 +186,16 @@ pub fn setup_tray(app: &tauri::App) -> Result<(), Box> { .separator() .item(&quit_item) .build()?; + startup_trace.record_elapsed_step("native_setup", "setup_tray.menu", step_started); + let step_started = Instant::now(); let icon = app .default_window_icon() .ok_or("No default window icon")? .clone(); + startup_trace.record_elapsed_step("native_setup", "setup_tray.icon", step_started); + let step_started = Instant::now(); let tray = TrayIconBuilder::new() .icon(icon) .menu(&initial_menu) @@ -222,9 +234,13 @@ pub fn setup_tray(app: &tauri::App) -> Result<(), Box> { _ => {} }) .build(app)?; + startup_trace.record_elapsed_step("native_setup", "setup_tray.build", step_started); + let step_started = Instant::now(); let _ = TRAY_ICON.set(tray); + startup_trace.record_elapsed_step("native_setup", "setup_tray.store", step_started); + let step_started = Instant::now(); let app_handle = app.handle().clone(); tauri::async_runtime::spawn(async move { tokio::time::sleep(std::time::Duration::from_secs(2)).await; @@ -236,6 +252,7 @@ pub fn setup_tray(app: &tauri::App) -> Result<(), Box> { rebuild_tray_menu(&app_handle).await; } }); + startup_trace.record_elapsed_step("native_setup", "setup_tray.spawn_refresh", step_started); Ok(()) } diff --git a/src/web-ui/src/flow_chat/components/modern/ModernFlowChatContainer.scss b/src/web-ui/src/flow_chat/components/modern/ModernFlowChatContainer.scss index cd9a972c2..1ff69c699 100644 --- a/src/web-ui/src/flow_chat/components/modern/ModernFlowChatContainer.scss +++ b/src/web-ui/src/flow_chat/components/modern/ModernFlowChatContainer.scss @@ -130,15 +130,37 @@ .modern-flowchat-container__history-open-intent-shield { position: absolute; inset: 0; - z-index: 9; + z-index: 11; display: flex; align-items: flex-start; justify-content: center; padding-top: 76px; - pointer-events: none; + pointer-events: auto; + background: var(--color-bg-scene); + contain: paint; +} + +.modern-flowchat-container__history-open-intent-shield::before { + content: ""; + position: absolute; + top: 116px; + left: 50%; + width: min(720px, calc(100% - 48px)); + height: min(220px, calc(100% - 168px)); + min-height: 120px; + transform: translateX(-50%); + border-radius: 8px; + opacity: 0.72; + background: + linear-gradient(90deg, transparent 0 4%, var(--color-surface-hover, rgba(127, 127, 127, 0.12)) 4% 62%, transparent 62% 100%) 0 0 / 100% 24px no-repeat, + linear-gradient(90deg, transparent 0 9%, var(--color-surface-hover, rgba(127, 127, 127, 0.12)) 9% 86%, transparent 86% 100%) 0 48px / 100% 24px no-repeat, + linear-gradient(90deg, transparent 0 6%, var(--color-surface-hover, rgba(127, 127, 127, 0.12)) 6% 74%, transparent 74% 100%) 0 96px / 100% 24px no-repeat, + linear-gradient(90deg, transparent 0 12%, var(--color-surface-hover, rgba(127, 127, 127, 0.12)) 12% 68%, transparent 68% 100%) 0 144px / 100% 24px no-repeat; } .modern-flowchat-container__history-open-intent-spinner { + position: relative; + z-index: 1; width: 18px; height: 18px; border: 2px solid var(--color-border, rgba(255, 255, 255, 0.16)); diff --git a/src/web-ui/src/flow_chat/components/modern/VirtualMessageList.layout.test.ts b/src/web-ui/src/flow_chat/components/modern/VirtualMessageList.layout.test.ts index f19e5416a..38184317f 100644 --- a/src/web-ui/src/flow_chat/components/modern/VirtualMessageList.layout.test.ts +++ b/src/web-ui/src/flow_chat/components/modern/VirtualMessageList.layout.test.ts @@ -3,6 +3,7 @@ import { estimateTextHeightFromLength, estimateVirtualMessageItemHeight, getVirtualMessageDefaultItemHeight, + selectInitialHistoryRenderWindow, } from './virtualMessageListLayout'; import type { VirtualItem } from '../../store/modernFlowChatStore'; @@ -84,3 +85,65 @@ describe('estimateVirtualMessageItemHeight', () => { expect(estimateVirtualMessageItemHeight(item)).toBeLessThanOrEqual(160); }); }); + +describe('selectInitialHistoryRenderWindow', () => { + function userItem(turnIndex: number): VirtualItem { + const id = `turn-${turnIndex}`; + return { + type: 'user-message', + turnId: id, + data: { + id: `user-${id}`, + content: `prompt ${turnIndex}`, + timestamp: turnIndex, + }, + } as VirtualItem; + } + + function modelItem(turnIndex: number, textLength = 2000): VirtualItem { + const id = `turn-${turnIndex}`; + return { + type: 'model-round', + turnId: id, + isLastRound: turnIndex === 7, + isTurnComplete: true, + data: { + id: `round-${id}`, + status: 'completed', + isStreaming: false, + items: [{ + id: `text-${id}`, + type: 'text', + content: 'x'.repeat(textLength), + status: 'completed', + timestamp: turnIndex, + }], + }, + } as VirtualItem; + } + + it('keeps only the latest render window on large partial history tails', () => { + const items = Array.from({ length: 8 }, (_, index) => [ + userItem(index), + modelItem(index), + ]).flat(); + + const window = selectInitialHistoryRenderWindow(items); + + expect(window.startIndex).toBeGreaterThan(0); + expect(window.items.length).toBeLessThan(items.length); + expect(window.items[0]?.turnId).toBe('turn-6'); + expect(window.items.at(-1)?.turnId).toBe('turn-7'); + expect(window.omittedEstimatedHeightPx).toBeGreaterThan(0); + }); + + it('keeps all items when the partial history tail is already small', () => { + const items = [userItem(0), modelItem(0), userItem(1), modelItem(1)]; + + const window = selectInitialHistoryRenderWindow(items); + + expect(window.startIndex).toBe(0); + expect(window.items).toHaveLength(items.length); + expect(window.omittedEstimatedHeightPx).toBe(0); + }); +}); diff --git a/src/web-ui/src/flow_chat/components/modern/VirtualMessageList.scss b/src/web-ui/src/flow_chat/components/modern/VirtualMessageList.scss index 3e873e360..0d9b0f608 100644 --- a/src/web-ui/src/flow_chat/components/modern/VirtualMessageList.scss +++ b/src/web-ui/src/flow_chat/components/modern/VirtualMessageList.scss @@ -83,6 +83,13 @@ width: 100%; } + &__initial-history-spacer { + width: 100%; + flex: 0 0 auto; + pointer-events: none; + overflow-anchor: none; + } + &__projection-handoff-overlay { position: absolute; inset: 0; diff --git a/src/web-ui/src/flow_chat/components/modern/VirtualMessageList.tsx b/src/web-ui/src/flow_chat/components/modern/VirtualMessageList.tsx index 2c762c908..61a0d3f5d 100644 --- a/src/web-ui/src/flow_chat/components/modern/VirtualMessageList.tsx +++ b/src/web-ui/src/flow_chat/components/modern/VirtualMessageList.tsx @@ -43,6 +43,7 @@ import { startupTrace } from '@/shared/utils/startupTrace'; import { estimateVirtualMessageItemHeight, getVirtualMessageDefaultItemHeight, + selectInitialHistoryRenderWindow, } from './virtualMessageListLayout'; import './VirtualMessageList.scss'; @@ -346,6 +347,7 @@ export const VirtualMessageList = forwardRef((_, ref) => ); const [pendingTurnPin, setPendingTurnPin] = useState(null); const [historyProjectionHandoff, setHistoryProjectionHandoff] = useState(null); + const [expandedInitialHistoryRenderKey, setExpandedInitialHistoryRenderKey] = useState(null); const scrollerElementRef = useRef(null); const footerElementRef = useRef(null); @@ -360,6 +362,14 @@ export const VirtualMessageList = forwardRef((_, ref) => const bottomReservationStateRef = useRef(createInitialBottomReservationState()); const previousMeasuredHeightRef = useRef(null); const previousScrollTopRef = useRef(0); + const markedInitialHistoryRenderWindowKeyRef = useRef(null); + const autoScrolledInitialHistoryRenderKeyRef = useRef(null); + const pendingInitialHistoryExpansionRef = useRef<{ + scrollTop: number; + scrollHeight: number; + wasAtBottom: boolean; + } | null>(null); + const initialHistoryRenderWindowCheckFrameRef = useRef(null); const measureFrameRef = useRef(null); const visibleTurnMeasureFrameRef = useRef(null); const pinReservationReconcileFrameRef = useRef(null); @@ -760,6 +770,7 @@ export const VirtualMessageList = forwardRef((_, ref) => activeSession.contextRestoreState === 'pending' && (activeSession.dialogTurns.length ?? 0) <= PARTIAL_HISTORY_INITIAL_TAIL_TURN_BUDGET; const useInitialHistoryRenderBudget = hasPendingHistoryCompletion || hasPartialHistoryInitialViewport; + const useStaticInitialHistoryList = useInitialHistoryRenderBudget; const latestTurnAutoFollowStateRef = useRef<{ turnId: string | null; sawPositiveFloor: boolean; @@ -3042,8 +3053,20 @@ export const VirtualMessageList = forwardRef((_, ref) => const scrollToTurnEndAndClearPin = useCallback((turnId: string) => { const scroller = scrollerElementRef.current; - if (!virtuosoRef.current || !scroller || virtualItems.length === 0) { + const reject = (cause: string, extra: Record = {}) => { + startupTrace.markPhase('flowchat_latest_end_anchor_rejected', { + cause, + turnId, + virtualItemCount: virtualItems.length, + staticInitialHistoryList: useStaticInitialHistoryList, + hasVirtuoso: Boolean(virtuosoRef.current), + ...extra, + }); return false; + }; + + if (!scroller || virtualItems.length === 0) { + return reject(!scroller ? 'missing_scroller' : 'empty_virtual_items'); } let targetIndex = -1; @@ -3055,11 +3078,56 @@ export const VirtualMessageList = forwardRef((_, ref) => } if (targetIndex < 0) { - return false; + return reject('missing_target_index'); } exitFollowOutput('scroll-to-index'); clearAllBottomReservationsForUserNavigation(); + + if (!virtuosoRef.current) { + const targetElement = getRenderedVirtualItemElement(targetIndex); + if (!targetElement) { + return reject('missing_static_target_element', { + targetIndex, + }); + } + + const scrollerRect = scroller.getBoundingClientRect(); + const targetRect = targetElement.getBoundingClientRect(); + const inputOverlayInsetPx = Math.max( + 0, + inputStackFooterPxRef.current - FLOWCHAT_MESSAGE_TAIL_CLEARANCE_PX, + ); + const visibleTop = scrollerRect.top + LATEST_END_ANCHOR_VISIBILITY_MARGIN_PX; + const visibleBottom = Math.max( + visibleTop + 1, + scrollerRect.bottom - inputOverlayInsetPx - LATEST_END_ANCHOR_VISIBILITY_MARGIN_PX, + ); + const maxScrollTop = Math.max(0, scroller.scrollHeight - scroller.clientHeight); + let nextScrollTop = scroller.scrollTop; + if (targetRect.bottom > visibleBottom) { + nextScrollTop += targetRect.bottom - visibleBottom; + } else if (targetRect.top < visibleTop) { + nextScrollTop -= visibleTop - targetRect.top; + } + nextScrollTop = Math.max(0, Math.min(maxScrollTop, nextScrollTop)); + + if (Math.abs(nextScrollTop - scroller.scrollTop) > COMPENSATION_EPSILON_PX) { + scroller.scrollTop = nextScrollTop; + previousScrollTopRef.current = nextScrollTop; + previousMeasuredHeightRef.current = snapshotMeasuredContentHeight(scroller); + } + + startupTrace.markPhase('flowchat_latest_end_anchor_request', { + targetIndex, + turnId, + virtualItemCount: virtualItems.length, + mode: 'static-initial-history', + }); + scheduleVisibleTurnMeasure(1); + return true; + } + latestEndAnchorRequestRef.current = { turnId, targetIndex, @@ -3092,9 +3160,12 @@ export const VirtualMessageList = forwardRef((_, ref) => }, [ clearAllBottomReservationsForUserNavigation, exitFollowOutput, + getRenderedVirtualItemElement, resolveLatestEndAnchorStabilization, scheduleVisibleTurnMeasure, + snapshotMeasuredContentHeight, toVirtuosoIndex, + useStaticInitialHistoryList, virtualItems, ]); @@ -3204,7 +3275,209 @@ export const VirtualMessageList = forwardRef((_, ref) => }, [lastItemInfo.isTurnProcessing, lastItemInfo.lastItem, isProcessing, processingPhase, isContentGrowing]); const footerHeightPx = getFooterHeightPx(getTotalBottomCompensationPx(bottomReservationState)); - const useStaticInitialHistoryList = useInitialHistoryRenderBudget; + const initialHistoryRenderWindow = React.useMemo( + () => useStaticInitialHistoryList + ? selectInitialHistoryRenderWindow(virtualItems) + : { + items: virtualItems, + startIndex: 0, + omittedEstimatedHeightPx: 0, + renderedEstimatedHeightPx: 0, + totalEstimatedHeightPx: 0, + isWindowed: false, + }, + [useStaticInitialHistoryList, virtualItems], + ); + const initialHistoryRenderKey = [ + activeSessionId ?? 'no-active-session', + latestTurnId ?? 'no-latest-turn', + virtualItems.length, + initialHistoryRenderWindow.startIndex, + ].join(':'); + const isInitialHistoryRenderWindowExpanded = + !initialHistoryRenderWindow.isWindowed || + expandedInitialHistoryRenderKey === initialHistoryRenderKey; + const renderedInitialHistoryItems = isInitialHistoryRenderWindowExpanded + ? virtualItems + : initialHistoryRenderWindow.items; + const renderedInitialHistoryStartIndex = isInitialHistoryRenderWindowExpanded + ? 0 + : initialHistoryRenderWindow.startIndex; + const omittedInitialHistoryEstimatedHeightPx = isInitialHistoryRenderWindowExpanded + ? 0 + : initialHistoryRenderWindow.omittedEstimatedHeightPx; + const expandInitialHistoryRenderWindow = useCallback((reason: string) => { + if ( + !useStaticInitialHistoryList || + !initialHistoryRenderWindow.isWindowed || + expandedInitialHistoryRenderKey === initialHistoryRenderKey + ) { + return; + } + + const scroller = scrollerElementRef.current; + if (scroller) { + const maxScrollTop = Math.max(0, scroller.scrollHeight - scroller.clientHeight); + pendingInitialHistoryExpansionRef.current = { + scrollTop: scroller.scrollTop, + scrollHeight: scroller.scrollHeight, + wasAtBottom: Math.abs(maxScrollTop - scroller.scrollTop) <= COMPENSATION_EPSILON_PX, + }; + } else { + pendingInitialHistoryExpansionRef.current = null; + } + + startupTrace.markPhase('flowchat_initial_history_render_window_expanded', { + sessionId: activeSessionId, + reason, + startIndex: initialHistoryRenderWindow.startIndex, + renderedItemCount: initialHistoryRenderWindow.items.length, + totalItemCount: virtualItems.length, + omittedEstimatedHeightPx: Math.round(initialHistoryRenderWindow.omittedEstimatedHeightPx), + }); + setExpandedInitialHistoryRenderKey(initialHistoryRenderKey); + }, [ + activeSessionId, + expandedInitialHistoryRenderKey, + initialHistoryRenderKey, + initialHistoryRenderWindow, + useStaticInitialHistoryList, + virtualItems.length, + ]); + const expandInitialHistoryRenderWindowIfNeeded = useCallback((reason: string) => { + if ( + !useStaticInitialHistoryList || + !initialHistoryRenderWindow.isWindowed || + isInitialHistoryRenderWindowExpanded || + omittedInitialHistoryEstimatedHeightPx <= 0 + ) { + return; + } + + const scroller = scrollerElementRef.current; + if (!scroller || scroller.scrollTop > omittedInitialHistoryEstimatedHeightPx) { + return; + } + + expandInitialHistoryRenderWindow(reason); + }, [ + expandInitialHistoryRenderWindow, + initialHistoryRenderWindow.isWindowed, + isInitialHistoryRenderWindowExpanded, + omittedInitialHistoryEstimatedHeightPx, + useStaticInitialHistoryList, + ]); + const scheduleInitialHistoryRenderWindowCheck = useCallback((reason: string) => { + if ( + !useStaticInitialHistoryList || + !initialHistoryRenderWindow.isWindowed || + isInitialHistoryRenderWindowExpanded + ) { + return; + } + + if (initialHistoryRenderWindowCheckFrameRef.current !== null) { + cancelAnimationFrame(initialHistoryRenderWindowCheckFrameRef.current); + } + + const scheduledSessionId = activeSessionId; + initialHistoryRenderWindowCheckFrameRef.current = requestAnimationFrame(() => { + initialHistoryRenderWindowCheckFrameRef.current = null; + if (activeSessionIdRef.current !== scheduledSessionId) { + return; + } + expandInitialHistoryRenderWindowIfNeeded(reason); + }); + }, [ + activeSessionId, + expandInitialHistoryRenderWindowIfNeeded, + initialHistoryRenderWindow.isWindowed, + isInitialHistoryRenderWindowExpanded, + useStaticInitialHistoryList, + ]); + const handleInitialHistoryStaticScroll = useCallback(() => { + expandInitialHistoryRenderWindowIfNeeded('scroll-near-omitted-history'); + }, [expandInitialHistoryRenderWindowIfNeeded]); + const handleInitialHistoryStaticWheelCapture = useCallback(() => { + scheduleInitialHistoryRenderWindowCheck('wheel-near-omitted-history'); + }, [scheduleInitialHistoryRenderWindowCheck]); + const handleInitialHistoryStaticKeyDownCapture = useCallback((event: React.KeyboardEvent) => { + if ( + event.key === 'Home' || + event.key === 'PageUp' || + event.key === 'ArrowUp' + ) { + scheduleInitialHistoryRenderWindowCheck(`keyboard-${event.key}`); + } + }, [scheduleInitialHistoryRenderWindowCheck]); + useEffect(() => { + if ( + !useStaticInitialHistoryList || + !initialHistoryRenderWindow.isWindowed || + markedInitialHistoryRenderWindowKeyRef.current === initialHistoryRenderKey + ) { + return; + } + + markedInitialHistoryRenderWindowKeyRef.current = initialHistoryRenderKey; + startupTrace.markPhase('flowchat_initial_history_render_window', { + sessionId: activeSessionId, + startIndex: initialHistoryRenderWindow.startIndex, + renderedItemCount: initialHistoryRenderWindow.items.length, + totalItemCount: virtualItems.length, + omittedEstimatedHeightPx: Math.round(initialHistoryRenderWindow.omittedEstimatedHeightPx), + renderedEstimatedHeightPx: Math.round(initialHistoryRenderWindow.renderedEstimatedHeightPx), + }); + }, [ + activeSessionId, + initialHistoryRenderKey, + initialHistoryRenderWindow, + useStaticInitialHistoryList, + virtualItems.length, + ]); + useLayoutEffect(() => { + const pending = pendingInitialHistoryExpansionRef.current; + if (!pending) { + return; + } + + if (expandedInitialHistoryRenderKey !== initialHistoryRenderKey) { + pendingInitialHistoryExpansionRef.current = null; + return; + } + + pendingInitialHistoryExpansionRef.current = null; + const scroller = scrollerElementRef.current; + if (!scroller) { + return; + } + + if (pending.wasAtBottom) { + const nextScrollTop = Math.max(0, scroller.scrollHeight - scroller.clientHeight); + scroller.scrollTop = nextScrollTop; + previousScrollTopRef.current = nextScrollTop; + previousMeasuredHeightRef.current = snapshotMeasuredContentHeight(scroller); + return; + } + + const heightDelta = scroller.scrollHeight - pending.scrollHeight; + const nextScrollTop = Math.max(0, pending.scrollTop + heightDelta); + scroller.scrollTop = nextScrollTop; + previousScrollTopRef.current = nextScrollTop; + previousMeasuredHeightRef.current = snapshotMeasuredContentHeight(scroller); + }, [ + expandedInitialHistoryRenderKey, + initialHistoryRenderKey, + snapshotMeasuredContentHeight, + ]); + useEffect(() => { + return () => { + if (initialHistoryRenderWindowCheckFrameRef.current !== null) { + cancelAnimationFrame(initialHistoryRenderWindowCheckFrameRef.current); + initialHistoryRenderWindowCheckFrameRef.current = null; + } + }; + }, []); const sessionOpenProjectionHandoff = React.useMemo(() => { const previousActiveSessionId = previousActiveSessionIdForOpenHandoffRef.current; const isSessionSwitch = ( @@ -3305,15 +3578,20 @@ export const VirtualMessageList = forwardRef((_, ref) => hasInitialHistoryModelRoundProjection, }); const initialHistoryHeightEstimates = React.useMemo( - () => useInitialHistoryRenderBudget + () => useInitialHistoryRenderBudget && !useStaticInitialHistoryList ? virtualItems.map(estimateVirtualMessageItemHeight) : undefined, - [useInitialHistoryRenderBudget, virtualItems], + [useInitialHistoryRenderBudget, useStaticInitialHistoryList, virtualItems], ); const virtuosoOverscan = { main: 600, reverse: 600 }; const virtuosoViewportIncrease = { top: 600, bottom: 600 }; useLayoutEffect(() => { if (!useStaticInitialHistoryList) { + autoScrolledInitialHistoryRenderKeyRef.current = null; + return; + } + + if (autoScrolledInitialHistoryRenderKeyRef.current === initialHistoryRenderKey) { return; } @@ -3322,6 +3600,7 @@ export const VirtualMessageList = forwardRef((_, ref) => return; } + autoScrolledInitialHistoryRenderKeyRef.current = initialHistoryRenderKey; const nextScrollTop = Math.max(0, scroller.scrollHeight - scroller.clientHeight); scroller.scrollTop = nextScrollTop; previousScrollTopRef.current = nextScrollTop; @@ -3329,12 +3608,11 @@ export const VirtualMessageList = forwardRef((_, ref) => scheduleVisibleTurnMeasure(1); }, [ activeSessionId, - footerHeightPx, + initialHistoryRenderKey, latestTurnId, scheduleVisibleTurnMeasure, snapshotMeasuredContentHeight, useStaticInitialHistoryList, - virtualItems.length, ]); // ── Render ──────────────────────────────────────────────────────────── useLayoutEffect(() => { @@ -3403,14 +3681,28 @@ export const VirtualMessageList = forwardRef((_, ref) => ref={handleScrollerRef} className="virtual-message-list__static-scroller" data-virtuoso-scroller="true" + data-initial-history-render-windowed={initialHistoryRenderWindow.isWindowed ? 'true' : 'false'} + onScroll={handleInitialHistoryStaticScroll} + onWheelCapture={handleInitialHistoryStaticWheelCapture} + onKeyDownCapture={handleInitialHistoryStaticKeyDownCapture} >
+ {omittedInitialHistoryEstimatedHeightPx > 0 ? ( +