From 794e6dcf3ec8b957eb960df0518c61388b3451aa Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Fri, 4 Sep 2026 04:45:09 -0700 Subject: [PATCH] perf(sidebar): share one natural-worktree-id scan and drop a redundant row-key join MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The natural-worktree-id Set was spelled out four times — twice in the drag groups module, once in the drag session hook, once in the drag units module — each as rows.flatMap(row => cond ? [id] : []), which allocates a throwaway array per row. All four memoize on the same rows, so they recompute together. Share one loop-based helper. use-row-measurement also built a joined string of every row key purely as an effect dependency, alongside a Set memoized on the same input. A fresh Set always changes identity, so the string could never fire the effect on its own. --- .../sidebar/natural-worktree-ids.test.ts | 45 +++++++++++++++++++ .../sidebar/natural-worktree-ids.ts | 24 ++++++++++ .../components/sidebar/worktree-drag-units.ts | 7 +-- .../sidebar/worktree-list/drag/groups.ts | 10 +---- .../sidebar/worktree-list/drag/use-session.ts | 11 +---- .../viewport/use-row-measurement.ts | 13 +----- 6 files changed, 75 insertions(+), 35 deletions(-) create mode 100644 src/renderer/src/components/sidebar/natural-worktree-ids.test.ts create mode 100644 src/renderer/src/components/sidebar/natural-worktree-ids.ts diff --git a/src/renderer/src/components/sidebar/natural-worktree-ids.test.ts b/src/renderer/src/components/sidebar/natural-worktree-ids.test.ts new file mode 100644 index 00000000000..b36fc2aa262 --- /dev/null +++ b/src/renderer/src/components/sidebar/natural-worktree-ids.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, it } from 'vitest' +import { PINNED_GROUP_KEY } from './worktree-list/grouping/group-keys' +import { getNaturalWorktreeIds } from './natural-worktree-ids' + +const item = (id: string, sectionKey: string) => ({ + type: 'item' as const, + sectionKey, + worktree: { id } +}) + +describe('getNaturalWorktreeIds', () => { + it('collects item rows outside the pinned section', () => { + expect([...getNaturalWorktreeIds([item('a', 'group-1'), item('b', 'group-2')])]).toEqual([ + 'a', + 'b' + ]) + }) + + it('excludes a pinned duplicate that also renders in its natural group', () => { + const rows = [item('a', PINNED_GROUP_KEY), item('a', 'group-1'), item('b', PINNED_GROUP_KEY)] + + const ids = getNaturalWorktreeIds(rows) + + expect(ids.has('a')).toBe(true) + expect(ids.has('b')).toBe(false) + }) + + it('ignores every non-item row type', () => { + const rows = [ + { type: 'header', key: 'k' }, + { type: 'host-header' }, + { type: 'imported-worktrees-card' }, + { type: 'new-external-worktrees-inbox' }, + { type: 'pending-creation' }, + { type: 'folder-workspace' }, + item('a', 'group-1') + ] + + expect([...getNaturalWorktreeIds(rows)]).toEqual(['a']) + }) + + it('returns an empty set for no rows', () => { + expect(getNaturalWorktreeIds([]).size).toBe(0) + }) +}) diff --git a/src/renderer/src/components/sidebar/natural-worktree-ids.ts b/src/renderer/src/components/sidebar/natural-worktree-ids.ts new file mode 100644 index 00000000000..7ac21ba1f9f --- /dev/null +++ b/src/renderer/src/components/sidebar/natural-worktree-ids.ts @@ -0,0 +1,24 @@ +import { PINNED_GROUP_KEY } from './worktree-list/grouping/group-keys' + +/** The row shape both drag models share: only `item` rows carry a worktree and a section. */ +type NaturalWorktreeIdRow = { type: string } & Partial<{ + sectionKey: string + worktree: { id: string } +}> + +/** + * Ids of worktrees rendered in their own group. + * + * Why: a pinned duplicate of a worktree that also renders in its natural group is not its own drag + * slot. Shared by every drag model so the rule lives in one place, and built with a loop rather + * than `flatMap` — that allocated a throwaway array per row, four times per row-model rebuild. + */ +export function getNaturalWorktreeIds(rows: readonly NaturalWorktreeIdRow[]): Set { + const ids = new Set() + for (const row of rows) { + if (row.type === 'item' && row.sectionKey !== PINNED_GROUP_KEY && row.worktree) { + ids.add(row.worktree.id) + } + } + return ids +} diff --git a/src/renderer/src/components/sidebar/worktree-drag-units.ts b/src/renderer/src/components/sidebar/worktree-drag-units.ts index e53d663a9f0..9b900ab72fc 100644 --- a/src/renderer/src/components/sidebar/worktree-drag-units.ts +++ b/src/renderer/src/components/sidebar/worktree-drag-units.ts @@ -1,5 +1,6 @@ import type { WorktreeDragGroup } from './worktree-manual-order' import { ALL_GROUP_KEY, PINNED_GROUP_KEY } from './worktree-list/grouping/group-keys' +import { getNaturalWorktreeIds } from './natural-worktree-ids' export type WorktreeDragUnitGroup = WorktreeDragGroup & { units: { worktreeId: string; worktreeIds: string[] }[] @@ -19,11 +20,7 @@ export function getWorktreeDragUnitGroups( ): WorktreeDragUnitGroup[] { const groups: WorktreeDragUnitGroup[] = [] let current: { key: string; units: WorktreeDragUnitGroup['units'] } | null = null - const naturalWorktreeIds = new Set( - rows.flatMap((row) => - row.type === 'item' && row.sectionKey !== PINNED_GROUP_KEY ? [row.worktree.id] : [] - ) - ) + const naturalWorktreeIds = getNaturalWorktreeIds(rows) for (const row of rows) { if (row.type === 'header') { diff --git a/src/renderer/src/components/sidebar/worktree-list/drag/groups.ts b/src/renderer/src/components/sidebar/worktree-list/drag/groups.ts index f8ee60c495e..7f1a8e19f49 100644 --- a/src/renderer/src/components/sidebar/worktree-list/drag/groups.ts +++ b/src/renderer/src/components/sidebar/worktree-list/drag/groups.ts @@ -1,16 +1,8 @@ import { ALL_GROUP_KEY, PINNED_GROUP_KEY } from '../grouping/group-keys' +import { getNaturalWorktreeIds } from '../../natural-worktree-ids' import type { HostSectionRow } from '../../host-section-rows' import type { WorktreeDragGroup } from '../../worktree-manual-order' -// A pinned duplicate of a worktree that also renders in its natural group is not its own drag slot. -function getNaturalWorktreeIds(rows: readonly HostSectionRow[]): Set { - return new Set( - rows.flatMap((row) => - row.type === 'item' && row.sectionKey !== PINNED_GROUP_KEY ? [row.worktree.id] : [] - ) - ) -} - export function getWorktreeDragGroups(rows: HostSectionRow[]): WorktreeDragGroup[] { const groups: WorktreeDragGroup[] = [] let current: { key: string; ids: string[] } | null = null diff --git a/src/renderer/src/components/sidebar/worktree-list/drag/use-session.ts b/src/renderer/src/components/sidebar/worktree-list/drag/use-session.ts index 78b25b25f38..92e23ec4928 100644 --- a/src/renderer/src/components/sidebar/worktree-list/drag/use-session.ts +++ b/src/renderer/src/components/sidebar/worktree-list/drag/use-session.ts @@ -24,6 +24,7 @@ import { } from '../../worktree-sidebar-drop-preview' import { getWorktreeDragGroups, getWorktreeDragIndexes } from './groups' import type { WorktreeItemRow } from '../listing/renderable-rows' +import { getNaturalWorktreeIds } from '../../natural-worktree-ids' export type WorktreeStatusDropRequest = { pointerY: number @@ -48,15 +49,7 @@ export function useWorktreeDragSession(args: { const worktreeDragGroups = useMemo(() => getWorktreeDragGroups(rows), [rows]) const worktreeDragUnitGroups = useMemo(() => getWorktreeDragUnitGroups(rows), [rows]) - const naturalDragWorktreeIds = useMemo( - () => - new Set( - rows.flatMap((row) => - row.type === 'item' && row.sectionKey !== PINNED_GROUP_KEY ? [row.worktree.id] : [] - ) - ), - [rows] - ) + const naturalDragWorktreeIds = useMemo(() => getNaturalWorktreeIds(rows), [rows]) const worktreeLineageDragRows = useMemo( () => rows diff --git a/src/renderer/src/components/sidebar/worktree-list/viewport/use-row-measurement.ts b/src/renderer/src/components/sidebar/worktree-list/viewport/use-row-measurement.ts index ff812a950be..a6f795cf392 100644 --- a/src/renderer/src/components/sidebar/worktree-list/viewport/use-row-measurement.ts +++ b/src/renderer/src/components/sidebar/worktree-list/viewport/use-row-measurement.ts @@ -51,10 +51,6 @@ export function useVirtualRowMeasurementSync(args: { const { virtualizer, isCurrentVirtualRowElement } = virtualization const prCacheLen = useAppStore((s) => countRecordKeysByReference(s.prCache)) const issueCacheLen = useAppStore((s) => countRecordKeysByReference(s.issueCache)) - const renderRowKeySignature = useMemo( - () => renderRows.map(getRenderRowKey).join('\n'), - [renderRows] - ) const activeRenderRowKeys = useMemo(() => new Set(renderRows.map(getRenderRowKey)), [renderRows]) const lineageRowRekeys = useMemo(() => buildLineageRowRekeyMap(renderRows), [renderRows]) const totalSize = virtualizer.getTotalSize() @@ -100,14 +96,7 @@ export function useVirtualRowMeasurementSync(args: { measureMountedRows() const frameId = window.requestAnimationFrame(measureMountedRows) return () => window.cancelAnimationFrame(frameId) - }, [ - activeRenderRowKeys, - prCacheLen, - issueCacheLen, - measureMountedRows, - renderRowKeySignature, - virtualizer - ]) + }, [activeRenderRowKeys, prCacheLen, issueCacheLen, measureMountedRows, virtualizer]) useVirtualizedScrollAnchor({ anchorRef: scrollAnchorRef,