From f0cf177147b6b2bc168bb2cb5d87fbff4b99ea37 Mon Sep 17 00:00:00 2001 From: drbronson Date: Tue, 28 Jul 2026 18:10:43 -0400 Subject: [PATCH] fix(desktop): memoize sidebar flatRows and row renderers to prevent virtualizer thrash MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wrap flatRows in useMemo and renderRow/renderListRow/renderRows/ renderRowsDated in useCallback to prevent unnecessary re-renders of the virtualized session list. Previously every store update cascaded through the memo chain and produced a fresh flatRows array, causing @tanstack/react-virtual to remeasure and shift rows — most visibly affecting older sessions at the bottom of the list where estimation errors accumulate. Closes #73629 --- .../src/app/chat/sidebar/sessions-section.tsx | 87 +++++++++++-------- 1 file changed, 51 insertions(+), 36 deletions(-) diff --git a/apps/desktop/src/app/chat/sidebar/sessions-section.tsx b/apps/desktop/src/app/chat/sidebar/sessions-section.tsx index 8cd060bb02a5..564a970d34f3 100644 --- a/apps/desktop/src/app/chat/sidebar/sessions-section.tsx +++ b/apps/desktop/src/app/chat/sidebar/sessions-section.tsx @@ -1,6 +1,6 @@ import type { useSensors } from '@dnd-kit/core' import type * as React from 'react' -import { useMemo } from 'react' +import { useCallback, useMemo } from 'react' import { SidebarPanelLabel } from '@/app/shell/sidebar-label' import { DisclosureCaret } from '@/components/ui/disclosure-caret' @@ -223,52 +223,67 @@ export function SidebarSessionsSection({ [sessions, preserveInputOrder] ) - const renderRow = (session: SessionInfo, draggable: boolean, branchStem?: string) => { - const rowProps = { - branchStem, - isPinned: pinned, - isSelected: session.id === activeSessionId, - isWorking: workingSessionIdSet.has(session.id), - onArchive: () => onArchiveSession(session.id), - onBranch: onBranchSession ? () => onBranchSession(session.id, session.profile) : undefined, - onDelete: () => onDeleteSession(session.id), - onPin: () => onTogglePin(sessionPinId(session)), - onResume: () => onResumeSession(session.id), - reorderable: draggable && !branchStem, - session, - showProfile: showProfileTags - } - - return draggable && !branchStem ? ( - - ) : ( - - ) - } + const renderRow = useCallback( + (session: SessionInfo, draggable: boolean, branchStem?: string) => { + const rowProps = { + branchStem, + isPinned: pinned, + isSelected: session.id === activeSessionId, + isWorking: workingSessionIdSet.has(session.id), + onArchive: () => onArchiveSession(session.id), + onBranch: onBranchSession ? () => onBranchSession(session.id, session.profile) : undefined, + onDelete: () => onDeleteSession(session.id), + onPin: () => onTogglePin(sessionPinId(session)), + onResume: () => onResumeSession(session.id), + reorderable: draggable && !branchStem, + session, + showProfile: showProfileTags + } + + return draggable && !branchStem ? ( + + ) : ( + + ) + }, + [activeSessionId, onArchiveSession, onBranchSession, onDeleteSession, onResumeSession, onTogglePin, pinned, showProfileTags, workingSessionIdSet] + ) // A single flat/virtual/lane list row — either a date divider or a session. - const renderListRow = (row: SidebarListRow, draggable: boolean) => - row.kind === 'divider' ? ( - - ) : ( - renderRow(row.entry.session, draggable, row.entry.branchStem) - ) + const renderListRow = useCallback( + (row: SidebarListRow, draggable: boolean) => + row.kind === 'divider' ? ( + + ) : ( + renderRow(row.entry.session, draggable, row.entry.branchStem) + ), + [dividerLabels, renderRow] + ) // Sessions inside repos/worktrees are date-ordered and static. - const renderRows = (items: SessionInfo[]) => - flattenSessionsWithBranches(items).map(({ branchStem, session }) => renderRow(session, false, branchStem)) + const renderRows = useCallback( + (items: SessionInfo[]) => + flattenSessionsWithBranches(items).map(({ branchStem, session }) => renderRow(session, false, branchStem)), + [renderRow] + ) // Same as `renderRows`, but with date dividers folded in — used for // entered-project lanes so a lane spanning multiple days reads // chronologically, matching the flat recents list. - const renderRowsDated = (items: SessionInfo[]) => { - const entries = flattenSessionsWithBranches(items) + const renderRowsDated = useCallback( + (items: SessionInfo[]) => { + const entries = flattenSessionsWithBranches(items) - return (dateGrouped ? groupEntriesByRecency(entries) : toSessionRows(entries)).map(row => renderListRow(row, false)) - } + return (dateGrouped ? groupEntriesByRecency(entries) : toSessionRows(entries)).map(row => renderListRow(row, false)) + }, + [dateGrouped, renderListRow] + ) // Flat recents as list rows: grouped by recency when enabled, plain otherwise. - const flatRows: SidebarListRow[] = dateGrouped ? groupEntriesByRecency(displayEntries) : toSessionRows(displayEntries) + const flatRows: SidebarListRow[] = useMemo( + () => (dateGrouped ? groupEntriesByRecency(displayEntries) : toSessionRows(displayEntries)), + [dateGrouped, displayEntries] + ) const flatVirtualized = !showEmptyState &&