From 6322263b8b41aa5b056fd948ba935442305c891a Mon Sep 17 00:00:00 2001 From: Ammar Date: Sun, 19 Oct 2025 12:20:31 -0500 Subject: [PATCH 1/6] Preserve hunk expand/collapse state across remounts - Added workspaceId prop to HunkViewer - Use usePersistedState to store per-workspace hunk expand state - Manual expand/collapse choices persisted in localStorage - Priority: manual state > read status > size (>200 lines) - State survives remounts/reloads but clears when workspace changes Result: Users' manual expand/collapse preferences persist across sessions, improving workflow when reviewing large diffs incrementally. --- .../RightSidebar/CodeReview/HunkViewer.tsx | 57 +++++++++++++++++-- .../RightSidebar/CodeReview/ReviewPanel.tsx | 1 + 2 files changed, 52 insertions(+), 6 deletions(-) diff --git a/src/components/RightSidebar/CodeReview/HunkViewer.tsx b/src/components/RightSidebar/CodeReview/HunkViewer.tsx index 4d802b980..487c0b294 100644 --- a/src/components/RightSidebar/CodeReview/HunkViewer.tsx +++ b/src/components/RightSidebar/CodeReview/HunkViewer.tsx @@ -7,10 +7,12 @@ import styled from "@emotion/styled"; import type { DiffHunk } from "@/types/review"; import { SelectableDiffRenderer } from "../../shared/DiffRenderer"; import { Tooltip, TooltipWrapper } from "../../Tooltip"; +import { usePersistedState } from "@/hooks/usePersistedState"; interface HunkViewerProps { hunk: DiffHunk; hunkId: string; + workspaceId: string; isSelected?: boolean; isRead?: boolean; onClick?: (e: React.MouseEvent) => void; @@ -165,7 +167,16 @@ const ToggleReadButton = styled.button` `; export const HunkViewer = React.memo( - ({ hunk, hunkId, isSelected, isRead = false, onClick, onToggleRead, onReviewNote }) => { + ({ + hunk, + hunkId, + workspaceId, + isSelected, + isRead = false, + onClick, + onToggleRead, + onReviewNote, + }) => { // Parse diff lines (memoized - only recompute if hunk.content changes) // Must be done before state initialization to determine initial collapse state const { lineCount, additions, deletions, isLargeHunk } = React.useMemo(() => { @@ -179,22 +190,56 @@ export const HunkViewer = React.memo( }; }, [hunk.content]); - // Collapse by default if marked as read OR if hunk has >200 lines - const [isExpanded, setIsExpanded] = useState(() => !isRead && !isLargeHunk); + // Persist manual expand/collapse state across remounts per workspace + // Maps hunkId -> isExpanded for user's manual preferences + const [expandStateMap, setExpandStateMap] = usePersistedState>( + `review:expand:${workspaceId}`, + {} + ); + + // Check if user has manually set expand state for this hunk + const hasManualState = hunkId in expandStateMap; + const manualExpandState = expandStateMap[hunkId]; - // Auto-collapse when marked as read, auto-expand when unmarked (but respect large hunk threshold) + // Determine initial expand state (priority: manual > read status > size) + const [isExpanded, setIsExpanded] = useState(() => { + if (hasManualState) { + return manualExpandState; + } + return !isRead && !isLargeHunk; + }); + + // Auto-collapse when marked as read, auto-expand when unmarked (unless user manually set) React.useEffect(() => { + // Don't override manual expand/collapse choices + if (hasManualState) { + return; + } + if (isRead) { setIsExpanded(false); } else if (!isLargeHunk) { setIsExpanded(true); } // Note: When unmarking as read, large hunks remain collapsed - }, [isRead, isLargeHunk]); + }, [isRead, isLargeHunk, hasManualState]); + + // Sync local state with persisted state when it changes + React.useEffect(() => { + if (hasManualState) { + setIsExpanded(manualExpandState); + } + }, [hasManualState, manualExpandState]); const handleToggleExpand = (e: React.MouseEvent) => { e.stopPropagation(); - setIsExpanded(!isExpanded); + const newExpandState = !isExpanded; + setIsExpanded(newExpandState); + // Persist manual expand/collapse choice + setExpandStateMap((prev) => ({ + ...prev, + [hunkId]: newExpandState, + })); }; const handleToggleRead = (e: React.MouseEvent) => { diff --git a/src/components/RightSidebar/CodeReview/ReviewPanel.tsx b/src/components/RightSidebar/CodeReview/ReviewPanel.tsx index c4e9ca808..01e19caf5 100644 --- a/src/components/RightSidebar/CodeReview/ReviewPanel.tsx +++ b/src/components/RightSidebar/CodeReview/ReviewPanel.tsx @@ -698,6 +698,7 @@ export const ReviewPanel: React.FC = ({ key={hunk.id} hunk={hunk} hunkId={hunk.id} + workspaceId={workspaceId} isSelected={isSelected} isRead={hunkIsRead} onClick={handleHunkClick} From 937e2c1a1c611019aebdb6ce4681451d8ba3939f Mon Sep 17 00:00:00 2001 From: Ammar Date: Sun, 19 Oct 2025 12:27:23 -0500 Subject: [PATCH 2/6] Address review: Use centralized storage pattern, cleanup in frontend - Added getReviewExpandStateKey() helper to constants/storage.ts - Follows established colon pattern: reviewExpandState:{workspaceId} - DRY: Refactored to shared PERSISTENT_WORKSPACE_KEY_FUNCTIONS list - Added deleteWorkspaceStorage() to clean up on workspace removal - Frontend calls deleteWorkspaceStorage() in useWorkspaceManagement - Fork copies expand state via existing copyWorkspaceStorage() mechanism Result: Hunk expand state properly integrated into workspace lifecycle. Copied on fork, deleted on removal, follows SoC (frontend handles localStorage). --- .../RightSidebar/CodeReview/HunkViewer.tsx | 3 +- src/constants/storage.ts | 59 +++++++++++++++---- src/hooks/useWorkspaceManagement.ts | 4 ++ 3 files changed, 52 insertions(+), 14 deletions(-) diff --git a/src/components/RightSidebar/CodeReview/HunkViewer.tsx b/src/components/RightSidebar/CodeReview/HunkViewer.tsx index 487c0b294..e2d9a0bc5 100644 --- a/src/components/RightSidebar/CodeReview/HunkViewer.tsx +++ b/src/components/RightSidebar/CodeReview/HunkViewer.tsx @@ -8,6 +8,7 @@ import type { DiffHunk } from "@/types/review"; import { SelectableDiffRenderer } from "../../shared/DiffRenderer"; import { Tooltip, TooltipWrapper } from "../../Tooltip"; import { usePersistedState } from "@/hooks/usePersistedState"; +import { getReviewExpandStateKey } from "@/constants/storage"; interface HunkViewerProps { hunk: DiffHunk; @@ -193,7 +194,7 @@ export const HunkViewer = React.memo( // Persist manual expand/collapse state across remounts per workspace // Maps hunkId -> isExpanded for user's manual preferences const [expandStateMap, setExpandStateMap] = usePersistedState>( - `review:expand:${workspaceId}`, + getReviewExpandStateKey(workspaceId), {} ); diff --git a/src/constants/storage.ts b/src/constants/storage.ts index bcd58a663..65964dd18 100644 --- a/src/constants/storage.ts +++ b/src/constants/storage.ts @@ -84,23 +84,43 @@ export function getCompactContinueMessageKey(workspaceId: string): string { return `compactContinueMessage:${workspaceId}`; } +/** + * Get the localStorage key for hunk expand/collapse state in Review tab + * Stores user's manual expand/collapse preferences per hunk + * Format: "reviewExpandState:{workspaceId}" + */ +export function getReviewExpandStateKey(workspaceId: string): string { + return `reviewExpandState:${workspaceId}`; +} + +/** + * List of workspace-scoped key functions that should be copied on fork and deleted on removal + * Note: Excludes ephemeral keys like getCompactContinueMessageKey + */ +const PERSISTENT_WORKSPACE_KEY_FUNCTIONS: Array<(workspaceId: string) => string> = [ + getModelKey, + getInputKey, + getModeKey, + getThinkingLevelKey, + getAutoRetryKey, + getRetryStateKey, + getReviewExpandStateKey, +]; + +/** + * Additional ephemeral keys to delete on workspace removal (not copied on fork) + */ +const EPHEMERAL_WORKSPACE_KEY_FUNCTIONS: Array<(workspaceId: string) => string> = [ + getCancelledCompactionKey, + getCompactContinueMessageKey, +]; + /** * Copy all workspace-specific localStorage keys from source to destination workspace - * This includes: model, input, mode, thinking level, auto-retry, retry state + * This includes: model, input, mode, thinking level, auto-retry, retry state, review expand state */ export function copyWorkspaceStorage(sourceWorkspaceId: string, destWorkspaceId: string): void { - // List of key-generating functions to copy - // Note: We deliberately skip getCompactContinueMessageKey as it's ephemeral - const keyFunctions: Array<(workspaceId: string) => string> = [ - getModelKey, - getInputKey, - getModeKey, - getThinkingLevelKey, - getAutoRetryKey, - getRetryStateKey, - ]; - - for (const getKey of keyFunctions) { + for (const getKey of PERSISTENT_WORKSPACE_KEY_FUNCTIONS) { const sourceKey = getKey(sourceWorkspaceId); const destKey = getKey(destWorkspaceId); const value = localStorage.getItem(sourceKey); @@ -109,3 +129,16 @@ export function copyWorkspaceStorage(sourceWorkspaceId: string, destWorkspaceId: } } } + +/** + * Delete all workspace-specific localStorage keys for a workspace + * Should be called when a workspace is deleted to prevent orphaned data + */ +export function deleteWorkspaceStorage(workspaceId: string): void { + const allKeyFunctions = [...PERSISTENT_WORKSPACE_KEY_FUNCTIONS, ...EPHEMERAL_WORKSPACE_KEY_FUNCTIONS]; + + for (const getKey of allKeyFunctions) { + const key = getKey(workspaceId); + localStorage.removeItem(key); + } +} diff --git a/src/hooks/useWorkspaceManagement.ts b/src/hooks/useWorkspaceManagement.ts index 0e32847bb..3f54a52b0 100644 --- a/src/hooks/useWorkspaceManagement.ts +++ b/src/hooks/useWorkspaceManagement.ts @@ -2,6 +2,7 @@ import { useState, useEffect, useCallback } from "react"; import type { FrontendWorkspaceMetadata } from "@/types/workspace"; import type { WorkspaceSelection } from "@/components/ProjectSidebar"; import type { ProjectConfig } from "@/config"; +import { deleteWorkspaceStorage } from "@/constants/storage"; interface UseWorkspaceManagementProps { selectedWorkspace: WorkspaceSelection | null; @@ -118,6 +119,9 @@ export function useWorkspaceManagement({ ): Promise<{ success: boolean; error?: string }> => { const result = await window.api.workspace.remove(workspaceId, options); if (result.success) { + // Clean up workspace-specific localStorage keys + deleteWorkspaceStorage(workspaceId); + // Backend has already updated the config - reload projects to get updated state const projectsList = await window.api.projects.list(); const loadedProjects = new Map(projectsList); From 577362b830257d66369066f2897a424e45bc51b2 Mon Sep 17 00:00:00 2001 From: Ammar Date: Sun, 19 Oct 2025 12:30:50 -0500 Subject: [PATCH 3/6] Add spacebar toggle and improve collapse button UX - Space now toggles expand/collapse for all hunks (not just selection) - Enter still selects the hunk - Show collapse button for all manually-expanded hunks (not just >200 lines) - Updated text: "Click here or press [Space] to collapse" - Pure renames: Space falls back to selection (no content to collapse) Result: Consistent UX - users can always collapse what they manually expand, and spacebar provides quick keyboard toggle for any hunk. --- .../RightSidebar/CodeReview/HunkViewer.tsx | 17 ++++++++++++++--- src/constants/storage.ts | 7 +++++-- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/components/RightSidebar/CodeReview/HunkViewer.tsx b/src/components/RightSidebar/CodeReview/HunkViewer.tsx index e2d9a0bc5..de50259bd 100644 --- a/src/components/RightSidebar/CodeReview/HunkViewer.tsx +++ b/src/components/RightSidebar/CodeReview/HunkViewer.tsx @@ -261,10 +261,19 @@ export const HunkViewer = React.memo( tabIndex={0} data-hunk-id={hunkId} onKeyDown={(e) => { - if (e.key === "Enter" || e.key === " ") { + if (e.key === "Enter") { e.preventDefault(); // Cast to MouseEvent-like for onClick handler onClick?.(e as unknown as React.MouseEvent); + } else if (e.key === " ") { + e.preventDefault(); + // Space toggles expand/collapse for non-rename hunks + if (!isPureRename) { + handleToggleExpand(e as unknown as React.MouseEvent); + } else { + // For renames, Space selects the hunk + onClick?.(e as unknown as React.MouseEvent); + } } }} > @@ -333,8 +342,10 @@ export const HunkViewer = React.memo( )} - {isLargeHunk && isExpanded && !isPureRename && ( - Click to collapse + {hasManualState && isExpanded && !isPureRename && ( + + Click here or press [Space] to collapse + )} ); diff --git a/src/constants/storage.ts b/src/constants/storage.ts index 65964dd18..7873f7eb7 100644 --- a/src/constants/storage.ts +++ b/src/constants/storage.ts @@ -135,8 +135,11 @@ export function copyWorkspaceStorage(sourceWorkspaceId: string, destWorkspaceId: * Should be called when a workspace is deleted to prevent orphaned data */ export function deleteWorkspaceStorage(workspaceId: string): void { - const allKeyFunctions = [...PERSISTENT_WORKSPACE_KEY_FUNCTIONS, ...EPHEMERAL_WORKSPACE_KEY_FUNCTIONS]; - + const allKeyFunctions = [ + ...PERSISTENT_WORKSPACE_KEY_FUNCTIONS, + ...EPHEMERAL_WORKSPACE_KEY_FUNCTIONS, + ]; + for (const getKey of allKeyFunctions) { const key = getKey(workspaceId); localStorage.removeItem(key); From 3d34d11d217c19a0f36ab66ca3c5fdcdac19d553 Mon Sep 17 00:00:00 2001 From: Ammar Date: Sun, 19 Oct 2025 12:38:27 -0500 Subject: [PATCH 4/6] =?UTF-8?q?=F0=9F=A4=96=20Centralize=20keybinds=20and?= =?UTF-8?q?=20fix=20Space=20key=20to=20operate=20on=20selected=20hunk?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added TOGGLE_HUNK_COLLAPSE keybind to centralized keybinds registry - Updated HunkViewer to use formatKeybind() for dynamic keybind display - Updated Mark as read tooltip to use formatKeybind() for consistency - Removed blue focus ring from HunkContainer (confusing dual-state) - Moved Space key handler from HunkViewer to ReviewPanel level - Space now operates on selected (yellow border) hunk, not focused hunk - Added onRegisterToggleExpand prop pattern for parent control - All keyboard navigation (j/k/m/Space) now consistently operates on selection Fixes UX confusion where Space would collapse the blue-bordered (focused) hunk instead of the yellow-bordered (selected) hunk during j/k navigation. --- .../RightSidebar/CodeReview/HunkViewer.tsx | 64 ++++++++++--------- .../RightSidebar/CodeReview/ReviewPanel.tsx | 16 ++++- src/utils/ui/keybinds.ts | 3 + 3 files changed, 52 insertions(+), 31 deletions(-) diff --git a/src/components/RightSidebar/CodeReview/HunkViewer.tsx b/src/components/RightSidebar/CodeReview/HunkViewer.tsx index de50259bd..0cf8d8eb4 100644 --- a/src/components/RightSidebar/CodeReview/HunkViewer.tsx +++ b/src/components/RightSidebar/CodeReview/HunkViewer.tsx @@ -9,6 +9,7 @@ import { SelectableDiffRenderer } from "../../shared/DiffRenderer"; import { Tooltip, TooltipWrapper } from "../../Tooltip"; import { usePersistedState } from "@/hooks/usePersistedState"; import { getReviewExpandStateKey } from "@/constants/storage"; +import { KEYBINDS, formatKeybind } from "@/utils/ui/keybinds"; interface HunkViewerProps { hunk: DiffHunk; @@ -18,6 +19,7 @@ interface HunkViewerProps { isRead?: boolean; onClick?: (e: React.MouseEvent) => void; onToggleRead?: (e: React.MouseEvent) => void; + onRegisterToggleExpand?: (hunkId: string, toggleFn: () => void) => void; onReviewNote?: (note: string) => void; } @@ -30,6 +32,12 @@ const HunkContainer = styled.div<{ isSelected: boolean; isRead: boolean }>` cursor: pointer; transition: all 0.2s ease; + /* Remove default focus ring - keyboard navigation uses isSelected state */ + &:focus, + &:focus-visible { + outline: none; + } + ${(props) => props.isRead && ` @@ -176,6 +184,7 @@ export const HunkViewer = React.memo( isRead = false, onClick, onToggleRead, + onRegisterToggleExpand, onReviewNote, }) => { // Parse diff lines (memoized - only recompute if hunk.content changes) @@ -232,16 +241,26 @@ export const HunkViewer = React.memo( } }, [hasManualState, manualExpandState]); - const handleToggleExpand = (e: React.MouseEvent) => { - e.stopPropagation(); - const newExpandState = !isExpanded; - setIsExpanded(newExpandState); - // Persist manual expand/collapse choice - setExpandStateMap((prev) => ({ - ...prev, - [hunkId]: newExpandState, - })); - }; + const handleToggleExpand = React.useCallback( + (e?: React.MouseEvent) => { + e?.stopPropagation(); + const newExpandState = !isExpanded; + setIsExpanded(newExpandState); + // Persist manual expand/collapse choice + setExpandStateMap((prev) => ({ + ...prev, + [hunkId]: newExpandState, + })); + }, + [isExpanded, hunkId, setExpandStateMap] + ); + + // Register toggle method with parent component + React.useEffect(() => { + if (onRegisterToggleExpand) { + onRegisterToggleExpand(hunkId, handleToggleExpand); + } + }, [hunkId, onRegisterToggleExpand, handleToggleExpand]); const handleToggleRead = (e: React.MouseEvent) => { e.stopPropagation(); @@ -260,22 +279,6 @@ export const HunkViewer = React.memo( role="button" tabIndex={0} data-hunk-id={hunkId} - onKeyDown={(e) => { - if (e.key === "Enter") { - e.preventDefault(); - // Cast to MouseEvent-like for onClick handler - onClick?.(e as unknown as React.MouseEvent); - } else if (e.key === " ") { - e.preventDefault(); - // Space toggles expand/collapse for non-rename hunks - if (!isPureRename) { - handleToggleExpand(e as unknown as React.MouseEvent); - } else { - // For renames, Space selects the hunk - onClick?.(e as unknown as React.MouseEvent); - } - } - }} > {isRead && ( @@ -302,12 +305,12 @@ export const HunkViewer = React.memo( {isRead ? "○" : "◉"} - Mark as read (m) + Mark as read ({formatKeybind(KEYBINDS.TOGGLE_HUNK_READ)}) )} @@ -338,13 +341,14 @@ export const HunkViewer = React.memo( ) : ( - {isRead && "Hunk marked as read. "}Click to expand ({lineCount} lines) + {isRead && "Hunk marked as read. "}Click to expand ({lineCount} lines) or press{" "} + {formatKeybind(KEYBINDS.TOGGLE_HUNK_COLLAPSE)} )} {hasManualState && isExpanded && !isPureRename && ( - Click here or press [Space] to collapse + Click here or press {formatKeybind(KEYBINDS.TOGGLE_HUNK_COLLAPSE)} to collapse )} diff --git a/src/components/RightSidebar/CodeReview/ReviewPanel.tsx b/src/components/RightSidebar/CodeReview/ReviewPanel.tsx index 01e19caf5..d02bb2f0e 100644 --- a/src/components/RightSidebar/CodeReview/ReviewPanel.tsx +++ b/src/components/RightSidebar/CodeReview/ReviewPanel.tsx @@ -3,7 +3,7 @@ * Displays diff hunks for viewing changes in the workspace */ -import React, { useState, useEffect, useMemo, useCallback } from "react"; +import React, { useState, useEffect, useMemo, useCallback, useRef } from "react"; import styled from "@emotion/styled"; import { HunkViewer } from "./HunkViewer"; import { ReviewControls } from "./ReviewControls"; @@ -286,6 +286,8 @@ export const ReviewPanel: React.FC = ({ const [refreshTrigger, setRefreshTrigger] = useState(0); const [fileTree, setFileTree] = useState(null); const [commonPrefix, setCommonPrefix] = useState(null); + // Map of hunkId -> toggle function for expand/collapse + const toggleExpandFnsRef = useRef void>>(new Map()); // Persist file filter per workspace const [selectedFilePath, setSelectedFilePath] = usePersistedState( @@ -539,6 +541,10 @@ export const ReviewPanel: React.FC = ({ [handleToggleRead] ); + const handleRegisterToggleExpand = useCallback((hunkId: string, toggleFn: () => void) => { + toggleExpandFnsRef.current.set(hunkId, toggleFn); + }, []); + // Calculate stats const stats = useMemo(() => { const total = hunks.length; @@ -598,6 +604,13 @@ export const ReviewPanel: React.FC = ({ // Toggle read state of selected hunk e.preventDefault(); handleToggleRead(selectedHunkId); + } else if (matchesKeybind(e, KEYBINDS.TOGGLE_HUNK_COLLAPSE)) { + // Toggle expand/collapse state of selected hunk + e.preventDefault(); + const toggleFn = toggleExpandFnsRef.current.get(selectedHunkId); + if (toggleFn) { + toggleFn(); + } } }; @@ -703,6 +716,7 @@ export const ReviewPanel: React.FC = ({ isRead={hunkIsRead} onClick={handleHunkClick} onToggleRead={handleHunkToggleRead} + onRegisterToggleExpand={handleRegisterToggleExpand} onReviewNote={onReviewNote} /> ); diff --git a/src/utils/ui/keybinds.ts b/src/utils/ui/keybinds.ts index ad1aabd0c..02dec5d2c 100644 --- a/src/utils/ui/keybinds.ts +++ b/src/utils/ui/keybinds.ts @@ -257,4 +257,7 @@ export const KEYBINDS = { /** Mark selected hunk as read/unread in Code Review panel */ TOGGLE_HUNK_READ: { key: "m" }, + + /** Toggle hunk expand/collapse in Code Review panel */ + TOGGLE_HUNK_COLLAPSE: { key: " " }, } as const; From 7ab2e4e81759fab9d10fc68eeed5cce62dd9cac2 Mon Sep 17 00:00:00 2001 From: Ammar Date: Sun, 19 Oct 2025 12:39:33 -0500 Subject: [PATCH 5/6] =?UTF-8?q?=F0=9F=A4=96=20Handle=20space=20key=20displ?= =?UTF-8?q?ay=20in=20formatKeybind()?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Display space key as 'Space' instead of invisible whitespace character. Without this, keybind hints like 'press Space' would show blank text. --- src/utils/ui/keybinds.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/utils/ui/keybinds.ts b/src/utils/ui/keybinds.ts index 02dec5d2c..cd8c73d74 100644 --- a/src/utils/ui/keybinds.ts +++ b/src/utils/ui/keybinds.ts @@ -163,8 +163,15 @@ export function formatKeybind(keybind: Keybind): string { if (keybind.meta) parts.push("Meta"); } - // Add the key (capitalize single letters) - const key = keybind.key.length === 1 ? keybind.key.toUpperCase() : keybind.key; + // Add the key (handle special cases, then capitalize single letters) + let key: string; + if (keybind.key === " ") { + key = "Space"; + } else if (keybind.key.length === 1) { + key = keybind.key.toUpperCase(); + } else { + key = keybind.key; + } parts.push(key); return isMac() ? parts.join("\u00B7") : parts.join("+"); // · on Mac, + elsewhere From f9afea8109b331742541e59f22d12d472eb53c7d Mon Sep 17 00:00:00 2001 From: Ammar Date: Sun, 19 Oct 2025 12:42:20 -0500 Subject: [PATCH 6/6] =?UTF-8?q?=F0=9F=A4=96=20Fix:=20Enable=20listener=20o?= =?UTF-8?q?ption=20to=20sync=20expand=20state=20across=20hunks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each HunkViewer instance now subscribes to storage changes via the listener option in usePersistedState. When one hunk's expand state changes, all other instances immediately see the update and won't overwrite it with stale data. This prevents the race condition where toggling multiple hunks would lose all but the most recent choice. Fixes P1 Codex review comment: PRRT_kwDOPxxmWM5ehWa0 --- src/components/RightSidebar/CodeReview/HunkViewer.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/RightSidebar/CodeReview/HunkViewer.tsx b/src/components/RightSidebar/CodeReview/HunkViewer.tsx index 0cf8d8eb4..d1f1c6774 100644 --- a/src/components/RightSidebar/CodeReview/HunkViewer.tsx +++ b/src/components/RightSidebar/CodeReview/HunkViewer.tsx @@ -202,9 +202,11 @@ export const HunkViewer = React.memo( // Persist manual expand/collapse state across remounts per workspace // Maps hunkId -> isExpanded for user's manual preferences + // Enable listener to synchronize updates across all HunkViewer instances const [expandStateMap, setExpandStateMap] = usePersistedState>( getReviewExpandStateKey(workspaceId), - {} + {}, + { listener: true } ); // Check if user has manually set expand state for this hunk