feat(ui): unify list virtualization on @tanstack/react-virtual and polish scroll behavior

- Migrate sidebar session groups, git changes panel, virtualized code
  blocks, and JSON tree viewer from virtua to @tanstack/react-virtual;
  virtua remains only inside the Pierre diff viewer integration
- Sidebar: preserve scroll position when virtualization enables
  mid-session (enable only once the ancestor scroll element is resolved,
  seed initial offset from its live scrollTop, render plain rows for the
  single pre-paint frame); disable native scroll anchoring on the
  sessions scroller; keep row spacing identical between plain and
  virtualized modes; absolute row positioning so variable-height rows
  cannot drift past the container
- Chat: expand tool/thinking blocks downward by only adjusting scroll
  for rows growing above the viewport; raise the desktop history-load
  lead to 1.5 viewports so prepends land above the visible area
- Git changes: compute the prefetch window from the first visible row,
  skipping overscan rows above the viewport
- Sidebar rows: make the whole highlighted row area clickable, guarded
  against double-firing from interactive children
This commit is contained in:
Bohdan Triapitsyn
2026-07-03 18:44:10 +03:00
parent 2bce38cfbb
commit 3f5151d424
8 changed files with 234 additions and 87 deletions
@@ -3,7 +3,7 @@
*
* Renders large code/read outputs without mounting one highlighter per line:
* 1. ONE worker tokenization of the whole block (off the main thread)
* 2. virtua to only render visible rows
* 2. @tanstack/react-virtual to only render visible rows
*
* Tokenizing the whole block at once also preserves cross-line syntax context
* (multi-line strings/comments) that per-line highlighting loses. Colors resolve
@@ -11,7 +11,7 @@
*/
import React from 'react';
import { Virtualizer } from 'virtua';
import { useVirtualizer } from '@tanstack/react-virtual';
import { useThemeSystem } from '@/contexts/useThemeSystem';
import { getMarkdownSyntaxVars } from '@/components/chat/markdown/markdownTheme';
import { useWorkerHighlightedLines } from '@/components/code/useWorkerHighlightedLines';
@@ -114,28 +114,41 @@ const VirtualizedRows: React.FC<VirtualizedRowsProps> = React.memo(({
const parentRef = React.useRef<HTMLDivElement>(null);
const viewportHeight = `min(${lines.length * ROW_HEIGHT}px, ${maxHeight})`;
const virtualizer = useVirtualizer<HTMLDivElement, HTMLDivElement>({
count: lines.length,
getScrollElement: () => parentRef.current,
estimateSize: () => ROW_HEIGHT,
overscan: 20,
});
const virtualItems = virtualizer.getVirtualItems();
return (
<div
ref={parentRef}
className="typography-code font-mono w-full min-w-0"
style={{ ...(syntaxVars as React.CSSProperties), height: viewportHeight, maxHeight, overflow: 'auto' }}
>
<Virtualizer
data={lines}
itemSize={ROW_HEIGHT}
bufferSize={ROW_HEIGHT * 20}
scrollRef={parentRef}
>
{(line, index) => (
<Row
key={index}
line={line}
html={highlighted?.[index]}
showLineNumbers={showLineNumbers}
style={lineStyles?.(line)}
/>
)}
</Virtualizer>
<div style={{ height: virtualizer.getTotalSize(), position: 'relative' }}>
{virtualItems.map((item) => {
const line = lines[item.index];
if (!line) return null;
return (
<div
key={item.index}
data-index={item.index}
ref={virtualizer.measureElement}
style={{ position: 'absolute', top: 0, left: 0, width: '100%', transform: `translateY(${item.start}px)` }}
>
<Row
line={line}
html={highlighted?.[item.index]}
showLineNumbers={showLineNumbers}
style={lineStyles?.(line)}
/>
</div>
);
})}
</div>
</div>
);
});