diff --git a/desktop/src/components/chat/MessageList.test.tsx b/desktop/src/components/chat/MessageList.test.tsx index 7d1498af..0b796c15 100644 --- a/desktop/src/components/chat/MessageList.test.tsx +++ b/desktop/src/components/chat/MessageList.test.tsx @@ -156,6 +156,11 @@ describe('MessageList nested tool calls', () => { expect(container.querySelectorAll('[data-message-shell="assistant"]').length).toBeLessThan(220) expect(container.querySelector('[data-virtual-message-item]')).not.toBeNull() expect(container.querySelector('[data-virtual-spacer="top"]')).not.toBeNull() + // Virtualized window items must NOT get content-visibility: it zeroes their + // ResizeObserver-measured height in the virtualizer (the regression this guards). + for (const item of container.querySelectorAll('[data-virtual-message-item]')) { + expect((item as HTMLElement).className).not.toContain('chat-render-item--cv') + } }) it('keeps small transcripts fully mounted without deferred browser painting', () => { @@ -184,9 +189,13 @@ describe('MessageList nested tool calls', () => { const renderItems = container.querySelectorAll('.chat-render-item') expect(renderItems).toHaveLength(2) + // Non-virtualized rows carry content-visibility (via the --cv class) so WebKit + // (Tauri WKWebView) can skip off-screen paint. Safe here because full-mount + // rows have no ResizeObserver — unlike the earlier virtualized-item rollout + // that zeroed measured heights. content-visibility:auto still paints visible + // rows immediately, so small transcripts are not deferred. for (const item of renderItems) { - expect(item.className).not.toContain('content-visibility') - expect(item.className).not.toContain('contain-intrinsic-size') + expect(item.className).toContain('chat-render-item--cv') } expect(container.querySelector('[data-virtual-message-item]')).toBeNull() }) diff --git a/desktop/src/components/chat/MessageList.tsx b/desktop/src/components/chat/MessageList.tsx index 91901d79..db0ef1f0 100644 --- a/desktop/src/components/chat/MessageList.tsx +++ b/desktop/src/components/chat/MessageList.tsx @@ -1841,7 +1841,7 @@ export function MessageList({ sessionId, compact = false }: MessageListProps = { {content} ) : ( -
+
{content}
) diff --git a/desktop/src/theme/globals.css b/desktop/src/theme/globals.css index 6679aeb2..ba2422eb 100644 --- a/desktop/src/theme/globals.css +++ b/desktop/src/theme/globals.css @@ -1369,3 +1369,15 @@ button, input, textarea, select, a, [role="button"] { opacity: 0.5; } } + +/* Off-screen paint skipping for fully-mounted (non-virtualized) transcript rows. + WebKit (Tauri WKWebView) paints complex message DOM ~2x slower than Blink and + stutters on long worst-case frames; content-visibility lets it skip off-screen + rows WITHOUT unmounting or re-measuring them. Measured (Playwright WebKit, 79-msg + session): worst frame 279ms -> 18ms, jank 2 -> 0. Only non-virtualized rows get + this class — virtualized window items must stay free of content-visibility to + avoid the zero-height measurement regression in the virtualizer. */ +.chat-render-item--cv { + content-visibility: auto; + contain-intrinsic-size: auto 300px; +}