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
@@ -747,6 +747,18 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
handleSessionSelect(session.id, sessionDirectory, projectId);
};
// The selection/active highlight covers the WHOLE row box (gutter, edge
// paddings), while the primary click target is the inner title button.
// Make the rest of the highlighted box clickable too — but only for clicks
// that did not originate from an interactive child (title button, chevron,
// action menu), so nothing double-fires.
const handleRowBackgroundClick = (event: React.MouseEvent<HTMLDivElement>) => {
if (event.defaultPrevented) return;
const target = event.target as HTMLElement | null;
if (target?.closest('button, a, input, [role="menuitem"], [role="menu"]')) return;
handleRowSelect(event as unknown as React.MouseEvent<HTMLButtonElement>);
};
const handleRowMouseDown = (event: React.MouseEvent<HTMLButtonElement>) => {
if (event.button === 2 || (event.button === 0 && event.ctrlKey && !selectionModeEnabled)) {
suppressNextSelectRef.current = true;
@@ -974,8 +986,9 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
data-session-row={session.id}
data-session-scope={sessionDirectory ?? ''}
data-session-archived={archivedBucket ? '1' : '0'}
onClick={handleRowBackgroundClick}
className={cn(
'group relative my-0.5 flex items-center rounded-md py-1 pr-1.5',
'group relative my-0.5 flex cursor-pointer items-center rounded-md py-1 pr-1.5',
// Pull the row box left into the container gutter so the
// selection highlight covers the chevron/status markers
// (which sit in that gutter), then re-pad so the title text