From 261feaa73edd07ebe7f1a60337f6f135a7da41b5 Mon Sep 17 00:00:00 2001 From: Ammar Date: Mon, 13 Oct 2025 10:35:37 -0500 Subject: [PATCH] =?UTF-8?q?=F0=9F=A4=96=20fix:=20reduce=20ProjectSidebar?= =?UTF-8?q?=20re-renders=20during=20streaming?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ProjectSidebar was re-rendering on every streaming delta and Git status poll, causing unnecessary UI churn when idling looking at a streaming chat. Changes: - Stabilize unreadStatus Map identity in useUnreadTracking - Only return new Map reference when values actually change - Prevents ProjectSidebar re-renders when unread state hasn't changed - Keep Git status interval at 3s for interactive updates - Reverted temporary increase to 10s to maintain snappy UI feel - Git status changes don't cause full sidebar re-renders due to WorkspaceGitStatusIndicator reading from context directly Performance impact: - ProjectSidebar only re-renders when inputs that affect rendering change - Streaming in non-selected workspace no longer causes sidebar churn - Git status updates remain fast (3s) without dragging parent tree _Generated with `cmux`_ --- src/contexts/GitStatusContext.tsx | 2 +- src/hooks/useUnreadTracking.ts | 25 +++++++++++++++++++++---- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/contexts/GitStatusContext.tsx b/src/contexts/GitStatusContext.tsx index b21e62115..54b7c5a54 100644 --- a/src/contexts/GitStatusContext.tsx +++ b/src/contexts/GitStatusContext.tsx @@ -31,7 +31,7 @@ interface GitStatusProviderProps { * - Max 5 concurrent git status checks to prevent bash process explosion */ // Configuration - enabled by default, no env variables needed -const GIT_STATUS_INTERVAL_MS = 3000; // 3 seconds +const GIT_STATUS_INTERVAL_MS = 3000; // 3 seconds - interactive updates const MAX_CONCURRENT_GIT_OPS = 5; // Fetch configuration - aggressive intervals for fresh data diff --git a/src/hooks/useUnreadTracking.ts b/src/hooks/useUnreadTracking.ts index 63032d3a1..102567cab 100644 --- a/src/hooks/useUnreadTracking.ts +++ b/src/hooks/useUnreadTracking.ts @@ -73,13 +73,14 @@ export function useUnreadTracking( }, [selectedWorkspace?.workspaceId, workspaceStates, markAsRead]); // Calculate unread status for all workspaces + const unreadStatusRef = useRef>(new Map()); const unreadStatus = useMemo(() => { - const result = new Map(); + const next = new Map(); for (const [workspaceId, state] of workspaceStates) { // Streaming workspaces are never unread if (state.canInterrupt) { - result.set(workspaceId, false); + next.set(workspaceId, false); continue; } @@ -92,10 +93,26 @@ export function useUnreadTracking( msg.type !== "user" && msg.type !== "history-hidden" && (msg.timestamp ?? 0) > lastRead ); - result.set(workspaceId, hasUnread); + next.set(workspaceId, hasUnread); } - return result; + // Return previous Map reference if nothing actually changed to keep identity stable + const prev = unreadStatusRef.current; + if (prev.size === next.size) { + let same = true; + for (const [k, v] of next) { + if (prev.get(k) !== v) { + same = false; + break; + } + } + if (same) { + return prev; + } + } + + unreadStatusRef.current = next; + return next; }, [workspaceStates, lastReadMap]); // Manual toggle function for clicking the indicator