diff --git a/apps/desktop/src/app/chat/sidebar/sessions-section.test.tsx b/apps/desktop/src/app/chat/sidebar/sessions-section.test.tsx
new file mode 100644
index 000000000000..47015dd80cfb
--- /dev/null
+++ b/apps/desktop/src/app/chat/sidebar/sessions-section.test.tsx
@@ -0,0 +1,182 @@
+import { cleanup, render } from '@testing-library/react'
+import type * as React from 'react'
+import { afterEach, describe, expect, it, vi } from 'vitest'
+
+import type { SessionInfo } from '@/hermes'
+import { SidebarSessionsSection, VIRTUALIZE_THRESHOLD } from './sessions-section'
+import type { VirtualSessionListProps } from './virtual-session-list'
+
+afterEach(cleanup)
+
+vi.mock('@/i18n', () => ({
+ useI18n: () => ({
+ t: {
+ sidebar: {
+ dateDivider: {
+ earlierThisMonth: 'Earlier this month',
+ lastMonth: 'Last month',
+ lastWeek: 'Last week',
+ older: 'Older',
+ today: 'Today',
+ yesterday: 'Yesterday'
+ }
+ }
+ }
+ })
+}))
+
+const mockVirtualListPropsHistory: VirtualSessionListProps[] = []
+
+vi.mock('./virtual-session-list', () => ({
+ VirtualSessionList: (props: VirtualSessionListProps) => {
+ mockVirtualListPropsHistory.push(props)
+ return
Virtual List ({props.rows.length} rows)
+ }
+}))
+
+vi.mock('./session-row', () => ({
+ SidebarSessionRow: ({ session }: { session: SessionInfo }) => (
+ {session.id}
+ )
+}))
+
+function makeSession(id: string, startedAt = 1000): SessionInfo {
+ return {
+ handoff_platform: null,
+ handoff_state: null,
+ id,
+ last_active: startedAt,
+ profile: 'default',
+ started_at: startedAt
+ } as unknown as SessionInfo
+}
+
+function generateSessions(count: number): SessionInfo[] {
+ return Array.from({ length: count }, (_, i) => makeSession(`session-${i + 1}`, 10000 - i * 100))
+}
+
+const noop = () => {}
+
+describe('SidebarSessionsSection memoization & virtualizer stability', () => {
+ it('memoizes flatRows and passes the exact same rows array reference across parent re-renders', () => {
+ mockVirtualListPropsHistory.length = 0
+
+ const sessions = generateSessions(VIRTUALIZE_THRESHOLD + 5)
+
+ const { rerender } = render(
+ Empty}
+ label="Sessions"
+ onArchiveSession={noop}
+ onDeleteSession={noop}
+ onResumeSession={noop}
+ onToggle={noop}
+ onTogglePin={noop}
+ open={true}
+ pinned={false}
+ sessions={sessions}
+ workingSessionIdSet={new Set()}
+ />
+ )
+
+ expect(mockVirtualListPropsHistory.length).toBe(1)
+ const initialRowsRef = mockVirtualListPropsHistory[0].rows
+ expect(initialRowsRef.length).toBeGreaterThan(VIRTUALIZE_THRESHOLD)
+
+ // Re-render parent with the exact same sessions array and props
+ rerender(
+ Empty}
+ label="Sessions"
+ onArchiveSession={noop}
+ onDeleteSession={noop}
+ onResumeSession={noop}
+ onToggle={noop}
+ onTogglePin={noop}
+ open={true}
+ pinned={false}
+ sessions={sessions}
+ workingSessionIdSet={new Set()}
+ />
+ )
+
+ expect(mockVirtualListPropsHistory.length).toBe(2)
+ const nextRowsRef = mockVirtualListPropsHistory[1].rows
+
+ // Confirm that the flatRows array reference remains strictly identical across renders (useMemo proof)
+ expect(nextRowsRef).toBe(initialRowsRef)
+ })
+
+ it('re-computes flatRows reference when dateGrouped or sessions change', () => {
+ mockVirtualListPropsHistory.length = 0
+
+ const initialSessions = generateSessions(VIRTUALIZE_THRESHOLD + 2)
+
+ const { rerender } = render(
+ Empty}
+ label="Sessions"
+ onArchiveSession={noop}
+ onDeleteSession={noop}
+ onResumeSession={noop}
+ onToggle={noop}
+ onTogglePin={noop}
+ open={true}
+ pinned={false}
+ sessions={initialSessions}
+ workingSessionIdSet={new Set()}
+ />
+ )
+
+ const firstRowsRef = mockVirtualListPropsHistory[0].rows
+
+ // Change dateGrouped to true
+ rerender(
+ Empty}
+ label="Sessions"
+ onArchiveSession={noop}
+ onDeleteSession={noop}
+ onResumeSession={noop}
+ onToggle={noop}
+ onTogglePin={noop}
+ open={true}
+ pinned={false}
+ sessions={initialSessions}
+ workingSessionIdSet={new Set()}
+ />
+ )
+
+ const secondRowsRef = mockVirtualListPropsHistory[1].rows
+ expect(secondRowsRef).not.toBe(firstRowsRef)
+
+ // Change sessions array identity
+ const updatedSessions = generateSessions(VIRTUALIZE_THRESHOLD + 4)
+ rerender(
+ Empty}
+ label="Sessions"
+ onArchiveSession={noop}
+ onDeleteSession={noop}
+ onResumeSession={noop}
+ onToggle={noop}
+ onTogglePin={noop}
+ open={true}
+ pinned={false}
+ sessions={updatedSessions}
+ workingSessionIdSet={new Set()}
+ />
+ )
+
+ const thirdRowsRef = mockVirtualListPropsHistory[2].rows
+ expect(thirdRowsRef).not.toBe(secondRowsRef)
+ })
+})
diff --git a/apps/desktop/src/app/chat/sidebar/sessions-section.tsx b/apps/desktop/src/app/chat/sidebar/sessions-section.tsx
index 35174a8370e6..95cbe9bf0daa 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'
@@ -225,52 +225,77 @@ 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 &&
diff --git a/apps/desktop/src/app/chat/sidebar/virtual-session-list.tsx b/apps/desktop/src/app/chat/sidebar/virtual-session-list.tsx
index d9cfd6c00438..90e10ddc38d0 100644
--- a/apps/desktop/src/app/chat/sidebar/virtual-session-list.tsx
+++ b/apps/desktop/src/app/chat/sidebar/virtual-session-list.tsx
@@ -27,7 +27,7 @@ interface SessionRowCommonProps {
showProfile?: boolean
}
-interface VirtualSessionListProps {
+export interface VirtualSessionListProps {
activeSessionId: null | string
className?: string
rows: SidebarListRow[]