Update files

This commit is contained in:
Mohamed Boudra
2026-02-06 22:21:37 +07:00
parent 07ea82513b
commit 2d74b5407c
2 changed files with 123 additions and 177 deletions
@@ -3,15 +3,12 @@ import {
ActivityIndicator, ActivityIndicator,
FlatList, FlatList,
Image as RNImage, Image as RNImage,
LayoutChangeEvent,
ListRenderItemInfo, ListRenderItemInfo,
Modal,
Pressable, Pressable,
ScrollView as RNScrollView, ScrollView as RNScrollView,
Text, Text,
View, View,
Platform, Platform,
useWindowDimensions,
} from "react-native"; } from "react-native";
import { ScrollView, Gesture, GestureDetector } from "react-native-gesture-handler"; import { ScrollView, Gesture, GestureDetector } from "react-native-gesture-handler";
import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles"; import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles";
@@ -28,6 +25,8 @@ import {
BottomSheetBackdrop, BottomSheetBackdrop,
} from "@gorhom/bottom-sheet"; } from "@gorhom/bottom-sheet";
import { import {
Copy,
Download,
File, File,
FileText, FileText,
Folder, Folder,
@@ -44,6 +43,13 @@ import type {
import { useDaemonConnections } from "@/contexts/daemon-connections-context"; import { useDaemonConnections } from "@/contexts/daemon-connections-context";
import { useSessionStore } from "@/stores/session-store"; import { useSessionStore } from "@/stores/session-store";
import { useDownloadStore } from "@/stores/download-store"; import { useDownloadStore } from "@/stores/download-store";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { useFileExplorerActions } from "@/hooks/use-file-explorer-actions"; import { useFileExplorerActions } from "@/hooks/use-file-explorer-actions";
import { import {
usePanelStore, usePanelStore,
@@ -74,7 +80,6 @@ export function FileExplorerPane({ serverId, agentId }: FileExplorerPaneProps) {
const { theme } = useUnistyles(); const { theme } = useUnistyles();
const isMobile = const isMobile =
UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm"; UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
const { width: windowWidth, height: windowHeight } = useWindowDimensions();
const { connectionStates } = useDaemonConnections(); const { connectionStates } = useDaemonConnections();
const daemonProfile = connectionStates.get(serverId)?.daemon; const daemonProfile = connectionStates.get(serverId)?.daemon;
@@ -123,9 +128,6 @@ export function FileExplorerPane({ serverId, agentId }: FileExplorerPaneProps) {
); );
const [expandedPaths, setExpandedPaths] = useState<Set<string>>(() => new Set(["."])); const [expandedPaths, setExpandedPaths] = useState<Set<string>>(() => new Set(["."]));
const [menuEntry, setMenuEntry] = useState<ExplorerEntry | null>(null);
const [menuAnchor, setMenuAnchor] = useState({ top: 0, left: 0 });
const [menuHeight, setMenuHeight] = useState(0);
const [containerWidth, setContainerWidth] = useState(0); const [containerWidth, setContainerWidth] = useState(0);
const wasInlinePreviewVisibleRef = useRef(false); const wasInlinePreviewVisibleRef = useRef(false);
@@ -236,26 +238,6 @@ export function FileExplorerPane({ serverId, agentId }: FileExplorerPaneProps) {
await Clipboard.setStringAsync(path); await Clipboard.setStringAsync(path);
}, []); }, []);
const handleOpenMenu = useCallback((entry: ExplorerEntry, event: any) => {
event.stopPropagation();
const { pageX, pageY } = event.nativeEvent ?? {};
setMenuAnchor({
left: typeof pageX === "number" ? pageX : 0,
top: typeof pageY === "number" ? pageY : 0,
});
setMenuEntry(entry);
}, []);
const handleCloseMenu = useCallback(() => {
setMenuEntry(null);
setMenuHeight(0);
}, []);
const handleMenuLayout = useCallback((event: LayoutChangeEvent) => {
const { height } = event.nativeEvent.layout;
setMenuHeight((current) => (current === height ? current : height));
}, []);
const startDownload = useDownloadStore((state) => state.startDownload); const startDownload = useDownloadStore((state) => state.startDownload);
const handleDownloadEntry = useCallback( const handleDownloadEntry = useCallback(
(entry: ExplorerEntry) => { (entry: ExplorerEntry) => {
@@ -275,23 +257,6 @@ export function FileExplorerPane({ serverId, agentId }: FileExplorerPaneProps) {
[agentId, serverId, daemonProfile, requestFileDownloadToken, startDownload] [agentId, serverId, daemonProfile, requestFileDownloadToken, startDownload]
); );
const menuPosition = useMemo(() => {
if (!menuEntry) {
return null;
}
const menuWidth = 240;
const horizontalPadding = theme.spacing[2];
const verticalPadding = theme.spacing[2];
const maxLeft = Math.max(horizontalPadding, windowWidth - menuWidth - horizontalPadding);
const maxTop = Math.max(verticalPadding, windowHeight - menuHeight - verticalPadding);
const left = Math.min(
Math.max(menuAnchor.left - menuWidth + horizontalPadding, horizontalPadding),
maxLeft
);
const top = Math.min(Math.max(menuAnchor.top + verticalPadding, verticalPadding), maxTop);
return { top, left, width: menuWidth };
}, [menuAnchor.left, menuAnchor.top, menuEntry, menuHeight, theme.spacing, windowHeight, windowWidth]);
const handleSortCycle = useCallback(() => { const handleSortCycle = useCallback(() => {
const currentIndex = SORT_OPTIONS.findIndex((opt) => opt.value === sortOption); const currentIndex = SORT_OPTIONS.findIndex((opt) => opt.value === sortOption);
const nextIndex = (currentIndex + 1) % SORT_OPTIONS.length; const nextIndex = (currentIndex + 1) % SORT_OPTIONS.length;
@@ -445,23 +410,55 @@ export function FileExplorerPane({ serverId, agentId }: FileExplorerPaneProps) {
{entry.name} {entry.name}
</Text> </Text>
</View> </View>
<Pressable <DropdownMenu>
onPress={(event) => handleOpenMenu(entry, event)} <DropdownMenuTrigger
hitSlop={8} hitSlop={8}
style={({ hovered, pressed }) => [ onPressIn={(event) => event.stopPropagation?.()}
styles.menuButton, style={({ hovered, pressed, open }) => [
(hovered || pressed) && styles.menuButtonActive, styles.menuButton,
]} (hovered || pressed || open) && styles.menuButtonActive,
> ]}
<MoreVertical size={16} color={theme.colors.foregroundMuted} /> >
</Pressable> <MoreVertical size={16} color={theme.colors.foregroundMuted} />
</DropdownMenuTrigger>
<DropdownMenuContent align="end" width={220}>
<View style={styles.contextMetaBlock}>
<View style={styles.contextMetaRow}>
<Text style={styles.contextMetaLabel}>Size</Text>
<Text style={styles.contextMetaValue}>{formatFileSize({ size: entry.size })}</Text>
</View>
<View style={styles.contextMetaRow}>
<Text style={styles.contextMetaLabel}>Modified</Text>
<Text style={styles.contextMetaValue}>{formatTimeAgo(new Date(entry.modifiedAt))}</Text>
</View>
</View>
<DropdownMenuSeparator />
<DropdownMenuItem
leading={<Copy size={14} color={theme.colors.foregroundMuted} />}
onSelect={() => {
void handleCopyPath(entry.path);
}}
>
Copy path
</DropdownMenuItem>
{entry.kind === "file" ? (
<DropdownMenuItem
leading={<Download size={14} color={theme.colors.foregroundMuted} />}
onSelect={() => handleDownloadEntry(entry)}
>
Download
</DropdownMenuItem>
) : null}
</DropdownMenuContent>
</DropdownMenu>
</Pressable> </Pressable>
); );
}, },
[ [
expandedPaths, expandedPaths,
handleEntryPress, handleEntryPress,
handleOpenMenu, handleCopyPath,
handleDownloadEntry,
isDirectoryLoading, isDirectoryLoading,
selectedEntryPath, selectedEntryPath,
theme.colors, theme.colors,
@@ -636,67 +633,6 @@ export function FileExplorerPane({ serverId, agentId }: FileExplorerPaneProps) {
</View> </View>
)} )}
<Modal
visible={Boolean(menuEntry)}
animationType="fade"
transparent={true}
onRequestClose={handleCloseMenu}
>
<View style={styles.menuOverlay}>
<Pressable style={styles.menuBackdrop} onPress={handleCloseMenu} />
{menuEntry && menuPosition ? (
<View
style={[
styles.entryMenu,
{
position: "absolute",
top: menuPosition.top,
left: menuPosition.left,
width: menuPosition.width,
},
]}
onLayout={handleMenuLayout}
>
<View style={styles.entryMenuHeader}>
<Text style={styles.entryMenuMeta}>
{formatFileSize({ size: menuEntry.size })}
</Text>
<Text style={styles.entryMenuMeta}>
{formatTimeAgo(new Date(menuEntry.modifiedAt))}
</Text>
</View>
<View style={styles.entryMenuDivider} />
<Pressable
style={({ hovered, pressed }) => [
styles.entryMenuItem,
(hovered || pressed) && styles.entryMenuItemHovered,
]}
onPress={() => {
handleCopyPath(menuEntry.path);
handleCloseMenu();
}}
>
<Text style={styles.entryMenuText}>Copy path</Text>
</Pressable>
{menuEntry.kind === "file" ? (
<Pressable
style={({ hovered, pressed }) => [
styles.entryMenuItem,
(hovered || pressed) && styles.entryMenuItemHovered,
]}
onPress={async () => {
handleCloseMenu();
handleDownloadEntry(menuEntry);
}}
>
<Text style={styles.entryMenuText}>Download</Text>
</Pressable>
) : null}
</View>
) : null}
</View>
</Modal>
{isMobile ? ( {isMobile ? (
<BottomSheetModal <BottomSheetModal
ref={previewSheetRef} ref={previewSheetRef}
@@ -809,7 +745,7 @@ function FilePreviewBody({
return ( return (
<View style={styles.sheetCenterState}> <View style={styles.sheetCenterState}>
<Text style={styles.emptyText}>Binary preview unavailable</Text> <Text style={styles.emptyText}>Binary preview unavailable</Text>
<Text style={styles.entryMenuMeta}>{formatFileSize({ size: preview.size })}</Text> <Text style={styles.binaryMetaText}>{formatFileSize({ size: preview.size })}</Text>
</View> </View>
); );
} }
@@ -1144,6 +1080,10 @@ const styles = StyleSheet.create((theme) => ({
fontSize: theme.fontSize.base, fontSize: theme.fontSize.base,
textAlign: "center", textAlign: "center",
}, },
binaryMetaText: {
color: theme.colors.foregroundMuted,
fontSize: theme.fontSize.sm,
},
entryRow: { entryRow: {
flexDirection: "row", flexDirection: "row",
alignItems: "center", alignItems: "center",
@@ -1179,52 +1119,24 @@ const styles = StyleSheet.create((theme) => ({
menuButtonActive: { menuButtonActive: {
backgroundColor: theme.colors.surface2, backgroundColor: theme.colors.surface2,
}, },
menuOverlay: { contextMetaBlock: {
flex: 1, paddingVertical: theme.spacing[1],
}, },
menuBackdrop: { contextMetaRow: {
...StyleSheet.absoluteFillObject,
backgroundColor: "rgba(0, 0, 0, 0.2)",
},
entryMenu: {
borderRadius: theme.borderRadius.lg,
backgroundColor: theme.colors.surface1,
overflow: "hidden",
...(Platform.OS === "web"
? ({ boxShadow: "0 10px 30px rgba(0, 0, 0, 0.35)" } as any)
: {
shadowColor: "#000",
shadowOpacity: 0.35,
shadowRadius: 16,
shadowOffset: { width: 0, height: 10 },
elevation: 14,
}),
},
entryMenuHeader: {
flexDirection: "row", flexDirection: "row",
alignItems: "center",
justifyContent: "space-between", justifyContent: "space-between",
paddingVertical: theme.spacing[3], minHeight: 32,
paddingHorizontal: theme.spacing[4], paddingHorizontal: theme.spacing[3],
}, },
entryMenuMeta: { contextMetaLabel: {
color: theme.colors.foregroundMuted,
fontSize: theme.fontSize.sm, fontSize: theme.fontSize.sm,
color: theme.colors.foregroundMuted,
}, },
entryMenuDivider: { contextMetaValue: {
height: 1, fontSize: theme.fontSize.sm,
backgroundColor: theme.colors.border,
},
entryMenuItem: {
paddingVertical: theme.spacing[4],
paddingHorizontal: theme.spacing[4],
},
entryMenuItemHovered: {
backgroundColor: theme.colors.surface2,
},
entryMenuText: {
color: theme.colors.foreground, color: theme.colors.foreground,
fontSize: theme.fontSize.lg, fontWeight: theme.fontWeight.medium,
fontWeight: theme.fontWeight.semibold,
}, },
previewHeaderText: { previewHeaderText: {
flex: 1, flex: 1,
+55 -21
View File
@@ -15,7 +15,7 @@ import { ScrollView, type ScrollView as ScrollViewType } from "react-native-gest
import { StyleSheet, useUnistyles } from "react-native-unistyles"; import { StyleSheet, useUnistyles } from "react-native-unistyles";
import AsyncStorage from "@react-native-async-storage/async-storage"; import AsyncStorage from "@react-native-async-storage/async-storage";
import * as Linking from "expo-linking"; import * as Linking from "expo-linking";
import { Archive, ChevronDown, ChevronRight, GitBranch, MoreVertical, ArrowLeftRight, ListChevronsDownUp, ListChevronsUpDown } from "lucide-react-native"; import { Archive, ChevronDown, ChevronRight, GitBranch, MoreVertical, ListChevronsDownUp, ListChevronsUpDown } from "lucide-react-native";
import { useCheckoutGitActionsStore } from "@/stores/checkout-git-actions-store"; import { useCheckoutGitActionsStore } from "@/stores/checkout-git-actions-store";
import { import {
useCheckoutDiffQuery, useCheckoutDiffQuery,
@@ -741,6 +741,14 @@ export function GitDiffPane({ serverId, agentId, cwd }: GitDiffPaneProps) {
const trimmed = baseRef.replace(/^refs\/(heads|remotes)\//, "").trim(); const trimmed = baseRef.replace(/^refs\/(heads|remotes)\//, "").trim();
return trimmed.startsWith("origin/") ? trimmed.slice("origin/".length) : trimmed; return trimmed.startsWith("origin/") ? trimmed.slice("origin/".length) : trimmed;
}, [baseRef]); }, [baseRef]);
const committedDiffDescription = useMemo(() => {
if (!branchLabel || !baseRefLabel) {
return undefined;
}
return branchLabel === baseRefLabel
? undefined
: `${branchLabel} -> ${baseRefLabel}`;
}, [baseRefLabel, branchLabel]);
const commitDisabled = actionsDisabled || commitStatus === "pending"; const commitDisabled = actionsDisabled || commitStatus === "pending";
const prDisabled = actionsDisabled || prCreateStatus === "pending"; const prDisabled = actionsDisabled || prCreateStatus === "pending";
const mergeDisabled = const mergeDisabled =
@@ -1101,25 +1109,45 @@ export function GitDiffPane({ serverId, agentId, cwd }: GitDiffPaneProps) {
{isGit ? ( {isGit ? (
<View style={styles.diffStatusContainer}> <View style={styles.diffStatusContainer}>
<View style={styles.diffStatusInner}> <View style={styles.diffStatusInner}>
<Pressable <DropdownMenu>
style={({ hovered, pressed }) => [ <DropdownMenuTrigger
styles.diffStatusRow, style={({ hovered, pressed, open }) => [
(hovered || pressed) && styles.diffStatusRowHovered, styles.diffModeTrigger,
]} hovered && styles.diffModeTriggerHovered,
testID="changes-diff-status" (pressed || open) && styles.diffModeTriggerPressed,
onPress={() => setDiffModeOverride(diffMode === "uncommitted" ? "base" : "uncommitted")} ]}
> testID="changes-diff-status"
{({ hovered, pressed }) => ( accessibilityRole="button"
<> accessibilityLabel="Diff mode"
<Text style={styles.diffStatusText}> >
{diffMode === "uncommitted" ? "Uncommitted" : "Committed"} <Text style={styles.diffStatusText}>
</Text> {diffMode === "uncommitted" ? "Uncommitted" : "Committed"}
{(hovered || pressed) ? ( </Text>
<ArrowLeftRight size={12} color={theme.colors.foregroundMuted} /> <ChevronDown size={12} color={theme.colors.foregroundMuted} />
) : null} </DropdownMenuTrigger>
</> <DropdownMenuContent
)} align="start"
</Pressable> width={260}
testID="changes-diff-status-menu"
>
<DropdownMenuItem
testID="changes-diff-mode-uncommitted"
selected={diffMode === "uncommitted"}
onSelect={() => setDiffModeOverride("uncommitted")}
>
Uncommitted
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
testID="changes-diff-mode-committed"
selected={diffMode === "base"}
description={committedDiffDescription}
onSelect={() => setDiffModeOverride("base")}
>
Committed
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
{files.length > 0 ? ( {files.length > 0 ? (
<Pressable <Pressable
style={({ hovered, pressed }) => [ style={({ hovered, pressed }) => [
@@ -1193,7 +1221,7 @@ const styles = StyleSheet.create((theme) => ({
justifyContent: "space-between", justifyContent: "space-between",
paddingRight: theme.spacing[3], paddingRight: theme.spacing[3],
}, },
diffStatusRow: { diffModeTrigger: {
flexDirection: "row", flexDirection: "row",
alignItems: "center", alignItems: "center",
gap: theme.spacing[1], gap: theme.spacing[1],
@@ -1204,6 +1232,12 @@ const styles = StyleSheet.create((theme) => ({
paddingVertical: theme.spacing[1], paddingVertical: theme.spacing[1],
borderRadius: theme.borderRadius.base, borderRadius: theme.borderRadius.base,
}, },
diffModeTriggerHovered: {
backgroundColor: theme.colors.surface2,
},
diffModeTriggerPressed: {
backgroundColor: theme.colors.surface2,
},
diffStatusRowHovered: { diffStatusRowHovered: {
backgroundColor: theme.colors.surface2, backgroundColor: theme.colors.surface2,
}, },