mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
Update files
This commit is contained in:
@@ -3,15 +3,12 @@ import {
|
||||
ActivityIndicator,
|
||||
FlatList,
|
||||
Image as RNImage,
|
||||
LayoutChangeEvent,
|
||||
ListRenderItemInfo,
|
||||
Modal,
|
||||
Pressable,
|
||||
ScrollView as RNScrollView,
|
||||
Text,
|
||||
View,
|
||||
Platform,
|
||||
useWindowDimensions,
|
||||
} from "react-native";
|
||||
import { ScrollView, Gesture, GestureDetector } from "react-native-gesture-handler";
|
||||
import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles";
|
||||
@@ -28,6 +25,8 @@ import {
|
||||
BottomSheetBackdrop,
|
||||
} from "@gorhom/bottom-sheet";
|
||||
import {
|
||||
Copy,
|
||||
Download,
|
||||
File,
|
||||
FileText,
|
||||
Folder,
|
||||
@@ -44,6 +43,13 @@ import type {
|
||||
import { useDaemonConnections } from "@/contexts/daemon-connections-context";
|
||||
import { useSessionStore } from "@/stores/session-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 {
|
||||
usePanelStore,
|
||||
@@ -74,7 +80,6 @@ export function FileExplorerPane({ serverId, agentId }: FileExplorerPaneProps) {
|
||||
const { theme } = useUnistyles();
|
||||
const isMobile =
|
||||
UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
|
||||
const { width: windowWidth, height: windowHeight } = useWindowDimensions();
|
||||
|
||||
const { connectionStates } = useDaemonConnections();
|
||||
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 [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 wasInlinePreviewVisibleRef = useRef(false);
|
||||
|
||||
@@ -236,26 +238,6 @@ export function FileExplorerPane({ serverId, agentId }: FileExplorerPaneProps) {
|
||||
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 handleDownloadEntry = useCallback(
|
||||
(entry: ExplorerEntry) => {
|
||||
@@ -275,23 +257,6 @@ export function FileExplorerPane({ serverId, agentId }: FileExplorerPaneProps) {
|
||||
[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 currentIndex = SORT_OPTIONS.findIndex((opt) => opt.value === sortOption);
|
||||
const nextIndex = (currentIndex + 1) % SORT_OPTIONS.length;
|
||||
@@ -445,23 +410,55 @@ export function FileExplorerPane({ serverId, agentId }: FileExplorerPaneProps) {
|
||||
{entry.name}
|
||||
</Text>
|
||||
</View>
|
||||
<Pressable
|
||||
onPress={(event) => handleOpenMenu(entry, event)}
|
||||
hitSlop={8}
|
||||
style={({ hovered, pressed }) => [
|
||||
styles.menuButton,
|
||||
(hovered || pressed) && styles.menuButtonActive,
|
||||
]}
|
||||
>
|
||||
<MoreVertical size={16} color={theme.colors.foregroundMuted} />
|
||||
</Pressable>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
hitSlop={8}
|
||||
onPressIn={(event) => event.stopPropagation?.()}
|
||||
style={({ hovered, pressed, open }) => [
|
||||
styles.menuButton,
|
||||
(hovered || pressed || open) && styles.menuButtonActive,
|
||||
]}
|
||||
>
|
||||
<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>
|
||||
);
|
||||
},
|
||||
[
|
||||
expandedPaths,
|
||||
handleEntryPress,
|
||||
handleOpenMenu,
|
||||
handleCopyPath,
|
||||
handleDownloadEntry,
|
||||
isDirectoryLoading,
|
||||
selectedEntryPath,
|
||||
theme.colors,
|
||||
@@ -636,67 +633,6 @@ export function FileExplorerPane({ serverId, agentId }: FileExplorerPaneProps) {
|
||||
</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 ? (
|
||||
<BottomSheetModal
|
||||
ref={previewSheetRef}
|
||||
@@ -809,7 +745,7 @@ function FilePreviewBody({
|
||||
return (
|
||||
<View style={styles.sheetCenterState}>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@@ -1144,6 +1080,10 @@ const styles = StyleSheet.create((theme) => ({
|
||||
fontSize: theme.fontSize.base,
|
||||
textAlign: "center",
|
||||
},
|
||||
binaryMetaText: {
|
||||
color: theme.colors.foregroundMuted,
|
||||
fontSize: theme.fontSize.sm,
|
||||
},
|
||||
entryRow: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
@@ -1179,52 +1119,24 @@ const styles = StyleSheet.create((theme) => ({
|
||||
menuButtonActive: {
|
||||
backgroundColor: theme.colors.surface2,
|
||||
},
|
||||
menuOverlay: {
|
||||
flex: 1,
|
||||
contextMetaBlock: {
|
||||
paddingVertical: theme.spacing[1],
|
||||
},
|
||||
menuBackdrop: {
|
||||
...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: {
|
||||
contextMetaRow: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
paddingVertical: theme.spacing[3],
|
||||
paddingHorizontal: theme.spacing[4],
|
||||
minHeight: 32,
|
||||
paddingHorizontal: theme.spacing[3],
|
||||
},
|
||||
entryMenuMeta: {
|
||||
color: theme.colors.foregroundMuted,
|
||||
contextMetaLabel: {
|
||||
fontSize: theme.fontSize.sm,
|
||||
color: theme.colors.foregroundMuted,
|
||||
},
|
||||
entryMenuDivider: {
|
||||
height: 1,
|
||||
backgroundColor: theme.colors.border,
|
||||
},
|
||||
entryMenuItem: {
|
||||
paddingVertical: theme.spacing[4],
|
||||
paddingHorizontal: theme.spacing[4],
|
||||
},
|
||||
entryMenuItemHovered: {
|
||||
backgroundColor: theme.colors.surface2,
|
||||
},
|
||||
entryMenuText: {
|
||||
contextMetaValue: {
|
||||
fontSize: theme.fontSize.sm,
|
||||
color: theme.colors.foreground,
|
||||
fontSize: theme.fontSize.lg,
|
||||
fontWeight: theme.fontWeight.semibold,
|
||||
fontWeight: theme.fontWeight.medium,
|
||||
},
|
||||
previewHeaderText: {
|
||||
flex: 1,
|
||||
|
||||
@@ -15,7 +15,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 { 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 {
|
||||
useCheckoutDiffQuery,
|
||||
@@ -741,6 +741,14 @@ export function GitDiffPane({ serverId, agentId, cwd }: GitDiffPaneProps) {
|
||||
const trimmed = baseRef.replace(/^refs\/(heads|remotes)\//, "").trim();
|
||||
return trimmed.startsWith("origin/") ? trimmed.slice("origin/".length) : trimmed;
|
||||
}, [baseRef]);
|
||||
const committedDiffDescription = useMemo(() => {
|
||||
if (!branchLabel || !baseRefLabel) {
|
||||
return undefined;
|
||||
}
|
||||
return branchLabel === baseRefLabel
|
||||
? undefined
|
||||
: `${branchLabel} -> ${baseRefLabel}`;
|
||||
}, [baseRefLabel, branchLabel]);
|
||||
const commitDisabled = actionsDisabled || commitStatus === "pending";
|
||||
const prDisabled = actionsDisabled || prCreateStatus === "pending";
|
||||
const mergeDisabled =
|
||||
@@ -1101,25 +1109,45 @@ export function GitDiffPane({ serverId, agentId, cwd }: GitDiffPaneProps) {
|
||||
{isGit ? (
|
||||
<View style={styles.diffStatusContainer}>
|
||||
<View style={styles.diffStatusInner}>
|
||||
<Pressable
|
||||
style={({ hovered, pressed }) => [
|
||||
styles.diffStatusRow,
|
||||
(hovered || pressed) && styles.diffStatusRowHovered,
|
||||
]}
|
||||
testID="changes-diff-status"
|
||||
onPress={() => setDiffModeOverride(diffMode === "uncommitted" ? "base" : "uncommitted")}
|
||||
>
|
||||
{({ hovered, pressed }) => (
|
||||
<>
|
||||
<Text style={styles.diffStatusText}>
|
||||
{diffMode === "uncommitted" ? "Uncommitted" : "Committed"}
|
||||
</Text>
|
||||
{(hovered || pressed) ? (
|
||||
<ArrowLeftRight size={12} color={theme.colors.foregroundMuted} />
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</Pressable>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
style={({ hovered, pressed, open }) => [
|
||||
styles.diffModeTrigger,
|
||||
hovered && styles.diffModeTriggerHovered,
|
||||
(pressed || open) && styles.diffModeTriggerPressed,
|
||||
]}
|
||||
testID="changes-diff-status"
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Diff mode"
|
||||
>
|
||||
<Text style={styles.diffStatusText}>
|
||||
{diffMode === "uncommitted" ? "Uncommitted" : "Committed"}
|
||||
</Text>
|
||||
<ChevronDown size={12} color={theme.colors.foregroundMuted} />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="start"
|
||||
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 ? (
|
||||
<Pressable
|
||||
style={({ hovered, pressed }) => [
|
||||
@@ -1193,7 +1221,7 @@ const styles = StyleSheet.create((theme) => ({
|
||||
justifyContent: "space-between",
|
||||
paddingRight: theme.spacing[3],
|
||||
},
|
||||
diffStatusRow: {
|
||||
diffModeTrigger: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing[1],
|
||||
@@ -1204,6 +1232,12 @@ const styles = StyleSheet.create((theme) => ({
|
||||
paddingVertical: theme.spacing[1],
|
||||
borderRadius: theme.borderRadius.base,
|
||||
},
|
||||
diffModeTriggerHovered: {
|
||||
backgroundColor: theme.colors.surface2,
|
||||
},
|
||||
diffModeTriggerPressed: {
|
||||
backgroundColor: theme.colors.surface2,
|
||||
},
|
||||
diffStatusRowHovered: {
|
||||
backgroundColor: theme.colors.surface2,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user