From 700138c9b5b7870cb0e11ea44bbb373c901bce0f Mon Sep 17 00:00:00 2001 From: jwcrystal <121911854+jwcrystal@users.noreply.github.com> Date: Sun, 12 Apr 2026 04:29:35 +0800 Subject: [PATCH] fix(git): restore changes panel visibility and sidebar sync (#886) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: restore git changes panel visibility and sidebar sync Two independent fixes: 1. ChangesSection virtualizer returned empty rows due to useMemo caching getVirtualItems() with a stale stable reference. First render produced an empty array, and useMemo never recomputed because rowVirtualizer reference never changed. Removed the useMemo to call getVirtualItems() directly on every render. Also added a ResizeObserver to force remeasurement on visibility transitions (defensive). 2. RightSidebarTabs now keeps git status fresh while the sidebar is open via useRightSidebarGitSync hook (10s polling with ensureStatus). Replaces the GitPollingProvider removed in commit d9821716. * fix(virtualizer): prevent React error #185 from render-phase getVirtualItems Restoring useMemo for virtualRows with totalSize as an invalidation dependency. Calling getVirtualItems() directly during render triggers the virtualizer's maybeNotify() → onChange() → useReducer dispatch, causing React error #185 (https://react.dev/errors/185 — cannot update a component while rendering a different component). The original useMemo([rowVirtualizer, shouldVirtualize]) was removed because rowVirtualizer is a stable useState ref that never changes, leaving the memo permanently stale after the first empty render. Adding totalSize (from getTotalSize()) as a dependency solves this: it changes whenever the virtualizer recalculates after measure/scroll, ensuring getVirtualItems() returns fresh rows while staying wrapped in useMemo. --- .../components/layout/RightSidebarTabs.tsx | 33 ++++++++++++++++++- .../components/views/git/ChangesSection.tsx | 31 +++++++++++++++-- 2 files changed, 61 insertions(+), 3 deletions(-) diff --git a/packages/ui/src/components/layout/RightSidebarTabs.tsx b/packages/ui/src/components/layout/RightSidebarTabs.tsx index 733b3ad9..cc25c247 100644 --- a/packages/ui/src/components/layout/RightSidebarTabs.tsx +++ b/packages/ui/src/components/layout/RightSidebarTabs.tsx @@ -4,13 +4,44 @@ import { RiFolder3Line, RiGitBranchLine } from '@remixicon/react'; import { SortableTabsStrip } from '@/components/ui/sortable-tabs-strip'; import { GitView } from '@/components/views'; import { useUIStore } from '@/stores/useUIStore'; +import { useGitStore } from '@/stores/useGitStore'; +import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs'; +import { useEffectiveDirectory } from '@/hooks/useEffectiveDirectory'; import { SidebarFilesTree } from './SidebarFilesTree'; type RightTab = 'git' | 'files'; +/** + * Keeps git status fresh while the right sidebar is open. + * Replaces the GitPollingProvider removed in commit b2d5ccb4. + * The previous polling ran globally; now we only refresh when the sidebar is open. + */ +function useRightSidebarGitSync(directory: string | undefined, isSidebarOpen: boolean) { + const { git } = useRuntimeAPIs(); + const ensureStatus = useGitStore((state) => state.ensureStatus); + + React.useEffect(() => { + if (!directory || !git || !isSidebarOpen) return; + + void ensureStatus(directory, git); + + const POLL_INTERVAL = 10_000; + const id = setInterval(() => { + if (typeof document !== 'undefined' && document.hidden) return; + void ensureStatus(directory, git); + }, POLL_INTERVAL); + + return () => clearInterval(id); + }, [directory, git, isSidebarOpen, ensureStatus]); +} + export const RightSidebarTabs: React.FC = () => { const rightSidebarTab = useUIStore((state) => state.rightSidebarTab); const setRightSidebarTab = useUIStore((state) => state.setRightSidebarTab); + const isRightSidebarOpen = useUIStore((state) => state.isRightSidebarOpen); + const directory = useEffectiveDirectory(); + + useRightSidebarGitSync(directory, isRightSidebarOpen); const tabItems = React.useMemo(() => [ { @@ -44,4 +75,4 @@ export const RightSidebarTabs: React.FC = () => { ); -}; +}; \ No newline at end of file diff --git a/packages/ui/src/components/views/git/ChangesSection.tsx b/packages/ui/src/components/views/git/ChangesSection.tsx index 824ac6e8..b068e0fb 100644 --- a/packages/ui/src/components/views/git/ChangesSection.tsx +++ b/packages/ui/src/components/views/git/ChangesSection.tsx @@ -67,9 +67,36 @@ export const ChangesSection: React.FC = ({ enabled: shouldVirtualize, }); + // Force virtualizer to remeasure when the scroll container transitions + // from display:none (hidden tab via keep-alive) back to visible layout. + // Without this, the virtualizer uses stale zero-height measurements and + // renders no rows until the user scrolls. + React.useEffect(() => { + if (!shouldVirtualize) return; + const el = scrollRef.current; + if (!el) return; + + const observer = new ResizeObserver(() => { + rowVirtualizer.measure(); + }); + observer.observe(el); + return () => observer.disconnect(); + }, [shouldVirtualize, rowVirtualizer]); + + // Compute virtual rows with useMemo. We include totalSize as a dependency so + // that when the ResizeObserver calls measure() — which clears the itemSizeCache + // and recalculates — the size change invalidates the memo and getVirtualItems() + // returns fresh rows. Using useMemo avoids calling getVirtualItems() directly in + // the render body, which can trigger maybeNotify() → onChange() → useReducer + // dispatch during render (React minified error #185). + const totalSize = rowVirtualizer.getTotalSize(); const virtualRows = React.useMemo( - () => (shouldVirtualize ? rowVirtualizer.getVirtualItems() : []), - [rowVirtualizer, shouldVirtualize], + // totalSize invalidates the memo when the virtualizer recalculates after + // measure/scroll, ensuring getVirtualItems() returns up-to-date rows. + // Without it, the stable rowVirtualizer ref would never invalidate the memo + // and rows would stay empty after measure(). + () => (shouldVirtualize && totalSize >= 0 ? rowVirtualizer.getVirtualItems() : []), + [shouldVirtualize, rowVirtualizer, totalSize], ); React.useEffect(() => {