| ... | @@ -21,7 +21,7 @@ import { | ... | @@ -21,7 +21,7 @@ import { |
| 21 | import { ChevronDown, ChevronRight, ChevronsDownUp, ChevronsUpDown } from "lucide"; | 21 | import { ChevronDown, ChevronRight, ChevronsDownUp, ChevronsUpDown } from "lucide"; |
| 22 | import * as Schema from "effect/Schema"; | 22 | import * as Schema from "effect/Schema"; |
| 23 | import * as DateTime from "effect/DateTime"; | 23 | import * as DateTime from "effect/DateTime"; |
| 24 | import { useCallback, useEffect, useMemo, useRef, useState } from "react"; | 24 | import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; |
| 25 | import { useCodeViewFileReveal } from "./diffs/useCodeViewFileReveal"; | 25 | import { useCodeViewFileReveal } from "./diffs/useCodeViewFileReveal"; |
| 26 | import { useOpenInPreferredEditor } from "../editorPreferences"; | 26 | import { useOpenInPreferredEditor } from "../editorPreferences"; |
| 27 | import { useFileContextMenuHandler } from "../fileContextMenu"; | 27 | import { useFileContextMenuHandler } from "../fileContextMenu"; |
| ... | @@ -118,6 +118,74 @@ interface CollapsedDiffFilesState { | ... | @@ -118,6 +118,74 @@ interface CollapsedDiffFilesState { |
| 118 | | 118 | |
| 119 | const EMPTY_COLLAPSED_DIFF_FILE_KEYS: ReadonlySet<string> = new Set(); | 119 | const EMPTY_COLLAPSED_DIFF_FILE_KEYS: ReadonlySet<string> = new Set(); |
| 120 | | 120 | |
| | 121 | /** Collapse control for one file header; re-renders only when its own file changes. */ |
| | 122 | function DiffFileCollapseToggle({ |
| | 123 | filePath, |
| | 124 | fileKey, |
| | 125 | collapsed, |
| | 126 | unavailable, |
| | 127 | iconClassName, |
| | 128 | onToggle, |
| | 129 | }: { |
| | 130 | filePath: string; |
| | 131 | fileKey: string; |
| | 132 | collapsed: boolean; |
| | 133 | unavailable: boolean; |
| | 134 | iconClassName: string; |
| | 135 | onToggle: (fileKey: string) => void; |
| | 136 | }) { |
| | 137 | return ( |
| | 138 | <Tooltip> |
| | 139 | <TooltipTrigger |
| | 140 | render={ |
| | 141 | <Button |
| | 142 | size="icon-micro" |
| | 143 | variant="ghost" |
| | 144 | className="-ms-0.5" |
| | 145 | aria-label={collapsed ? `Expand ${filePath}` : `Collapse ${filePath}`} |
| | 146 | aria-expanded={!collapsed} |
| | 147 | disabled={unavailable} |
| | 148 | onClick={(event) => { |
| | 149 | event.stopPropagation(); |
| | 150 | onToggle(fileKey); |
| | 151 | }} |
| | 152 | /> |
| | 153 | } |
| | 154 | > |
| | 155 | <MorphIcon |
| | 156 | className={cn("size-4", iconClassName)} |
| | 157 | icon={collapsed ? ChevronRight : ChevronDown} |
| | 158 | /> |
| | 159 | </TooltipTrigger> |
| | 160 | <TooltipPopup side="top">{collapsed ? "Expand diff" : "Collapse diff"}</TooltipPopup> |
| | 161 | </Tooltip> |
| | 162 | ); |
| | 163 | } |
| | 164 | |
| | 165 | /** Copy and status controls for one file header; re-renders only when its own file changes. */ |
| | 166 | function DiffFileHeaderSuffix({ |
| | 167 | filePath, |
| | 168 | hasStat, |
| | 169 | error, |
| | 170 | truncated, |
| | 171 | onRetry, |
| | 172 | }: { |
| | 173 | filePath: string; |
| | 174 | hasStat: boolean; |
| | 175 | error: boolean; |
| | 176 | truncated: boolean; |
| | 177 | onRetry: (path: string) => void; |
| | 178 | }) { |
| | 179 | return ( |
| | 180 | <> |
| | 181 | <DiffFilePathCopyButton filePath={filePath} /> |
| | 182 | {hasStat ? ( |
| | 183 | <DiffFileStatus error={error} truncated={truncated} retry={() => onRetry(filePath)} /> |
| | 184 | ) : null} |
| | 185 | </> |
| | 186 | ); |
| | 187 | } |
| | 188 | |
| 121 | interface DiffPanelProps { | 189 | interface DiffPanelProps { |
| 122 | mode?: DiffPanelMode; | 190 | mode?: DiffPanelMode; |
| 123 | composerDraftTarget: ScopedThreadRef | DraftId; | 191 | composerDraftTarget: ScopedThreadRef | DraftId; |
| ... | @@ -598,22 +666,24 @@ export default function DiffPanel({ | ... | @@ -598,22 +666,24 @@ export default function DiffPanel({ |
| 598 | }, | 666 | }, |
| 599 | [activeCwd, activeRepositoryRoot, openInPreferredEditor, routeThreadRef], | 667 | [activeCwd, activeRepositoryRoot, openInPreferredEditor, routeThreadRef], |
| 600 | ); | 668 | ); |
| 601 | const toggleDiffFileCollapsed = useCallback( | 669 | const collapseDefaultsRef = useRef({ collapseScopeKey, defaultCollapsedDiffFileKeys }); |
| 602 | (fileKey: string) => { | 670 | useLayoutEffect(() => { |
| 603 | setCollapsedDiffFiles((current) => { | 671 | collapseDefaultsRef.current = { collapseScopeKey, defaultCollapsedDiffFileKeys }; |
| 604 | const next = new Set( | 672 | }, [collapseScopeKey, defaultCollapsedDiffFileKeys]); |
| 605 | current.scopeKey === collapseScopeKey ? current.fileKeys : defaultCollapsedDiffFileKeys, | 673 | const toggleDiffFileCollapsed = useCallback((fileKey: string) => { |
| 606 | ); | 674 | const { collapseScopeKey, defaultCollapsedDiffFileKeys } = collapseDefaultsRef.current; |
| 607 | if (next.has(fileKey)) { | 675 | setCollapsedDiffFiles((current) => { |
| 608 | next.delete(fileKey); | 676 | const next = new Set( |
| 609 | } else { | 677 | current.scopeKey === collapseScopeKey ? current.fileKeys : defaultCollapsedDiffFileKeys, |
| 610 | next.add(fileKey); | 678 | ); |
| 611 | } | 679 | if (next.has(fileKey)) { |
| 612 | return { scopeKey: collapseScopeKey, fileKeys: next }; | 680 | next.delete(fileKey); |
| 613 | }); | 681 | } else { |
| 614 | }, | 682 | next.add(fileKey); |
| 615 | [collapseScopeKey, defaultCollapsedDiffFileKeys], | 683 | } |
| 616 | ); | 684 | return { scopeKey: collapseScopeKey, fileKeys: next }; |
| | 685 | }); |
| | 686 | }, []); |
| 617 | | 687 | |
| 618 | const toggleDiffFileCollapse = useCallback(() => { | 688 | const toggleDiffFileCollapse = useCallback(() => { |
| 619 | setCodeViewRevision((current) => current + 1); | 689 | setCodeViewRevision((current) => current + 1); |
| ... | @@ -1082,14 +1152,15 @@ export default function DiffPanel({ | ... | @@ -1082,14 +1152,15 @@ export default function DiffPanel({ |
| 1082 | composerDraftTarget={composerDraftTarget} | 1152 | composerDraftTarget={composerDraftTarget} |
| 1083 | renderHeaderFilenameSuffix={(fileDiff) => { | 1153 | renderHeaderFilenameSuffix={(fileDiff) => { |
| 1084 | const path = resolveFileDiffPath(fileDiff); | 1154 | const path = resolveFileDiffPath(fileDiff); |
| 1085 | const stat = fileStats.get(path); | 1155 | const state = fileStates.get(path); |
| 1086 | return ( | 1156 | return ( |
| 1087 | <> | 1157 | <DiffFileHeaderSuffix |
| 1088 | <DiffFilePathCopyButton filePath={path} /> | 1158 | filePath={path} |
| 1089 | {stat ? ( | 1159 | hasStat={fileStats.has(path)} |
| 1090 | <DiffFileStatus {...fileStates.get(path)} retry={() => retry(path)} /> | 1160 | error={state?.error ?? false} |
| 1091 | ) : null} | 1161 | truncated={state?.truncated ?? false} |
| 1092 | </> | 1162 | onRetry={retry} |
| | 1163 | /> |
| 1093 | ); | 1164 | ); |
| 1094 | }} | 1165 | }} |
| 1095 | {...(lazySource | 1166 | {...(lazySource |
| ... | @@ -1109,37 +1180,15 @@ export default function DiffPanel({ | ... | @@ -1109,37 +1180,15 @@ export default function DiffPanel({ |
| 1109 | : {})} | 1180 | : {})} |
| 1110 | renderHeaderPrefix={(fileDiff, fileKey) => { | 1181 | renderHeaderPrefix={(fileDiff, fileKey) => { |
| 1111 | const unavailable = fileDiff.cacheKey?.endsWith(":pending") === true; | 1182 | const unavailable = fileDiff.cacheKey?.endsWith(":pending") === true; |
| 1112 | const collapsed = unavailable || collapsedDiffFileKeys.has(fileKey); | | |
| 1113 | const filePath = resolveFileDiffPath(fileDiff); | | |
| 1114 | return ( | 1183 | return ( |
| 1115 | <Tooltip> | 1184 | <DiffFileCollapseToggle |
| 1116 | <TooltipTrigger | 1185 | filePath={resolveFileDiffPath(fileDiff)} |
| 1117 | render={ | 1186 | fileKey={fileKey} |
| 1118 | <Button | 1187 | collapsed={unavailable || collapsedDiffFileKeys.has(fileKey)} |
| 1119 | size="icon-micro" | 1188 | unavailable={unavailable} |
| 1120 | variant="ghost" | 1189 | iconClassName={getDiffCollapseIconClassName(fileDiff)} |
| 1121 | className="-ms-0.5" | 1190 | onToggle={toggleDiffFileCollapsed} |
| 1122 | aria-label={ | 1191 | /> |
| 1123 | collapsed ? `Expand ${filePath}` : `Collapse ${filePath}` | | |
| 1124 | } | | |
| 1125 | aria-expanded={!collapsed} | | |
| 1126 | disabled={unavailable} | | |
| 1127 | onClick={(event) => { | | |
| 1128 | event.stopPropagation(); | | |
| 1129 | toggleDiffFileCollapsed(fileKey); | | |
| 1130 | }} | | |
| 1131 | /> | | |
| 1132 | } | | |
| 1133 | > | | |
| 1134 | <MorphIcon | | |
| 1135 | className={cn("size-4", getDiffCollapseIconClassName(fileDiff))} | | |
| 1136 | icon={collapsed ? ChevronRight : ChevronDown} | | |
| 1137 | /> | | |
| 1138 | </TooltipTrigger> | | |
| 1139 | <TooltipPopup side="top"> | | |
| 1140 | {collapsed ? "Expand diff" : "Collapse diff"} | | |
| 1141 | </TooltipPopup> | | |
| 1142 | </Tooltip> | | |
| 1143 | ); | 1192 | ); |
| 1144 | }} | 1193 | }} |
| 1145 | options={{ | 1194 | options={{ |