diff --git a/packages/app/src/components/git-diff-pane.tsx b/packages/app/src/components/git-diff-pane.tsx index e945e2ce6..a3a1e380c 100644 --- a/packages/app/src/components/git-diff-pane.tsx +++ b/packages/app/src/components/git-diff-pane.tsx @@ -16,7 +16,7 @@ import { ScrollView, type ScrollView as ScrollViewType } from "react-native-gest import { StyleSheet, useUnistyles } from "react-native-unistyles"; import AsyncStorage from "@react-native-async-storage/async-storage"; import * as Linking from "expo-linking"; -import { ChevronDown, ChevronRight, GitBranch, MoreVertical, ArrowLeftRight } from "lucide-react-native"; +import { ChevronDown, ChevronRight, GitBranch, MoreVertical, ArrowLeftRight, ListChevronsDownUp, ListChevronsUpDown } from "lucide-react-native"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { useSessionStore } from "@/stores/session-store"; import { @@ -583,6 +583,23 @@ export function GitDiffPane({ serverId, agentId, cwd }: GitDiffPaneProps) { })); }, []); + const allExpanded = useMemo(() => { + if (files.length === 0) return false; + return files.every((file) => expandedByPath[file.path]); + }, [files, expandedByPath]); + + const handleToggleExpandAll = useCallback(() => { + if (allExpanded) { + setExpandedByPath({}); + } else { + const newExpanded: Record = {}; + for (const file of files) { + newExpanded[file.path] = true; + } + setExpandedByPath(newExpanded); + } + }, [allExpanded, files]); + // Reset manual refresh flag when fetch completes useEffect(() => { if (!(isDiffFetching || isStatusFetching) && isManualRefresh) { @@ -1157,27 +1174,42 @@ export function GitDiffPane({ serverId, agentId, cwd }: GitDiffPaneProps) { {isGit && (hasUncommittedChanges || aheadCount > 0) ? ( - [ - styles.diffStatusRow, - (hovered || pressed) && styles.diffStatusRowHovered, - ]} - testID="changes-diff-status" - onPress={() => setDiffModeOverride(diffMode === "uncommitted" ? "base" : "uncommitted")} - > - {({ hovered, pressed }) => ( - <> - - {diffMode === "uncommitted" ? "Uncommitted" : "Committed"} - - - + + [ + styles.diffStatusRow, + (hovered || pressed) && styles.diffStatusRowHovered, + ]} + testID="changes-diff-status" + onPress={() => setDiffModeOverride(diffMode === "uncommitted" ? "base" : "uncommitted")} + > + {({ hovered, pressed }) => ( + <> + + {diffMode === "uncommitted" ? "Uncommitted" : "Committed"} + + {(hovered || pressed) ? ( + + ) : null} + )} - + + {files.length > 0 ? ( + [ + styles.expandAllButton, + (hovered || pressed) && styles.diffStatusRowHovered, + ]} + onPress={handleToggleExpandAll} + > + {allExpanded ? ( + + ) : ( + + )} + + ) : null} + ) : null} @@ -1229,15 +1261,20 @@ const styles = StyleSheet.create((theme) => ({ borderBottomWidth: 1, borderBottomColor: theme.colors.border, }, + diffStatusInner: { + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", + paddingRight: theme.spacing[3], + }, diffStatusRow: { flexDirection: "row", alignItems: "center", - alignSelf: "flex-start", gap: theme.spacing[1], - // Align text with chevron/branch icons (at spacing[2] from edge) + // Align text with header branch icon (at spacing[3] from edge, minus our horizontal padding) + marginLeft: theme.spacing[3] - theme.spacing[1], marginVertical: theme.spacing[2], - paddingLeft: theme.spacing[2], - paddingRight: theme.spacing[2], + paddingHorizontal: theme.spacing[1], paddingVertical: theme.spacing[1], borderRadius: theme.borderRadius.base, }, @@ -1251,6 +1288,15 @@ const styles = StyleSheet.create((theme) => ({ diffStatusIconHidden: { opacity: 0, }, + expandAllButton: { + flexDirection: "row", + alignItems: "center", + gap: theme.spacing[1], + marginVertical: theme.spacing[2], + paddingHorizontal: theme.spacing[1], + paddingVertical: theme.spacing[1], + borderRadius: theme.borderRadius.base, + }, splitButton: { flexDirection: "row", alignItems: "stretch",