diff --git a/desktop/src/components/chat/MessageList.test.tsx b/desktop/src/components/chat/MessageList.test.tsx index 195d7e3a..f7f8f6ad 100644 --- a/desktop/src/components/chat/MessageList.test.tsx +++ b/desktop/src/components/chat/MessageList.test.tsx @@ -120,14 +120,22 @@ describe('MessageList nested tool calls', () => { }) }) - it('window-renders long transcripts while keeping the latest messages visible', () => { + it('keeps full long transcripts mounted so variable-height messages cannot leave spacer gaps', () => { useChatStore.setState({ sessions: { [ACTIVE_TAB]: makeSessionState({ messages: Array.from({ length: 220 }, (_, index) => ({ id: `assistant-${index}`, type: 'assistant_text', - content: `assistant transcript line ${index}`, + content: index % 25 === 0 + ? [ + `assistant transcript line ${index}`, + '', + '```ts', + 'const value = "this intentionally makes the row much taller"', + '```', + ].join('\n') + : `assistant transcript line ${index}`, timestamp: index, })), }), @@ -136,9 +144,10 @@ describe('MessageList nested tool calls', () => { const { container } = render() - expect(screen.queryByText('assistant transcript line 0')).toBeNull() + expect(screen.getByText('assistant transcript line 0')).toBeTruthy() expect(screen.getByText('assistant transcript line 219')).toBeTruthy() - expect(container.querySelectorAll('[data-message-shell="assistant"]').length).toBeLessThan(140) + expect(container.querySelectorAll('[data-message-shell="assistant"]').length).toBe(220) + expect(container.querySelector('[data-virtual-message-item]')).toBeNull() }) it('renders goal events as visible status cards', () => { @@ -221,7 +230,7 @@ describe('MessageList nested tool calls', () => { expect(container.querySelectorAll('[data-message-shell="assistant"]').length).toBe(220) }) - it('window-renders long histories that include tool-call groups', async () => { + it('keeps long histories with tool-call groups mounted while scrolling history', async () => { useChatStore.setState({ sessions: { [ACTIVE_TAB]: makeSessionState({ @@ -259,7 +268,7 @@ describe('MessageList nested tool calls', () => { Object.defineProperty(scrollArea, 'scrollHeight', { configurable: true, value: 222 * 112 }) await waitForProgrammaticScrollReset() - expect(screen.queryByText('Read')).toBeNull() + expect(screen.getByText('Read')).toBeTruthy() expect(screen.getByText('assistant transcript line 219')).toBeTruthy() scrollArea.scrollTop = 0 @@ -269,7 +278,7 @@ describe('MessageList nested tool calls', () => { expect(screen.getByText('Read')).toBeTruthy() expect(screen.getByText('assistant transcript line 219')).toBeTruthy() - expect(container.querySelectorAll('[data-virtual-message-item]').length).toBe(221) + expect(container.querySelector('[data-virtual-message-item]')).toBeNull() }) it('renders sub-agent tool calls inline beneath the parent agent tool call', () => { diff --git a/desktop/src/components/chat/MessageList.tsx b/desktop/src/components/chat/MessageList.tsx index abd74ca9..edfefaec 100644 --- a/desktop/src/components/chat/MessageList.tsx +++ b/desktop/src/components/chat/MessageList.tsx @@ -537,8 +537,6 @@ type MessageListProps = { const AUTO_SCROLL_BOTTOM_THRESHOLD_PX = 48 const MAX_SCROLL_SNAPSHOTS = 100 -const VIRTUALIZE_MIN_ITEMS = 160 -const VIRTUAL_INITIAL_ITEMS = 96 const CHAT_SCROLL_AREA_CLASS = [ 'chat-scroll-area', '[scrollbar-width:auto]', @@ -590,10 +588,6 @@ function getBottomScrollTop(element: HTMLElement) { return Math.max(0, element.scrollHeight - element.clientHeight) } -function getRenderItemKey(item: RenderItem) { - return item.kind === 'tool_group' ? item.id : item.message.id -} - export function MessageList({ sessionId, compact = false }: MessageListProps = {}) { const activeTabId = useTabStore((s) => s.activeTabId) const resolvedSessionId = sessionId ?? activeTabId @@ -623,7 +617,6 @@ export function MessageList({ sessionId, compact = false }: MessageListProps = { const isProgrammaticScrollingRef = useRef(false) const lastSessionIdRef = useRef(resolvedSessionId) const lastTailMessageIdBySessionRef = useRef(new Map()) - const updateVirtualWindowRef = useRef<((element: HTMLElement) => void) | null>(null) const t = useTranslation() const [turnChangeCards, setTurnChangeCards] = useState([]) const [turnChangeLoadError, setTurnChangeLoadError] = useState(null) @@ -693,7 +686,6 @@ export function MessageList({ sessionId, compact = false }: MessageListProps = { if (resolvedSessionId) { rememberSessionScroll(resolvedSessionId, container) } - updateVirtualWindowRef.current?.(container) }, [resolvedSessionId]) useLayoutEffect(() => { @@ -759,58 +751,6 @@ export function MessageList({ sessionId, compact = false }: MessageListProps = { () => buildRenderModel(messages), [messages], ) - const shouldVirtualize = renderItems.length > VIRTUALIZE_MIN_ITEMS - const [virtualRange, setVirtualRange] = useState(() => ({ start: 0, end: 0 })) - - const updateVirtualWindow = useCallback((container: HTMLElement | null) => { - if (!shouldVirtualize) { - setVirtualRange((current) => - current.start === 0 && current.end === renderItems.length - ? current - : { start: 0, end: renderItems.length }, - ) - return - } - - if (!container || shouldAutoScrollRef.current) { - const start = Math.max(0, renderItems.length - VIRTUAL_INITIAL_ITEMS) - setVirtualRange((current) => - current.start === start && current.end === renderItems.length - ? current - : { start, end: renderItems.length }, - ) - return - } - - setVirtualRange((current) => - current.start === 0 && current.end === renderItems.length - ? current - : { start: 0, end: renderItems.length }, - ) - }, [renderItems.length, shouldVirtualize]) - - useEffect(() => { - updateVirtualWindowRef.current = updateVirtualWindow - return () => { - if (updateVirtualWindowRef.current === updateVirtualWindow) { - updateVirtualWindowRef.current = null - } - } - }, [updateVirtualWindow]) - - useLayoutEffect(() => { - updateVirtualWindow(scrollContainerRef.current) - }, [renderItems.length, resolvedSessionId, updateVirtualWindow]) - - const visibleRenderItems = shouldVirtualize - ? renderItems.slice(virtualRange.start, virtualRange.end) - : renderItems - const topVirtualSpacerHeight = shouldVirtualize - ? Math.max(0, virtualRange.start * 96) - : 0 - const bottomVirtualSpacerHeight = shouldVirtualize - ? Math.max(0, renderItems.length - virtualRange.end) * 96 - : 0 const completedTurnTargets = useMemo(() => getCompletedTurnTargets(messages), [messages]) const latestCompletedTurnId = completedTurnTargets.length > 0 @@ -961,25 +901,13 @@ export function MessageList({ sessionId, compact = false }: MessageListProps = {
- {topVirtualSpacerHeight > 0 && ( -