mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
Merge branch 'streamline-file-explorer'
This commit is contained in:
@@ -10,9 +10,10 @@ import {
|
||||
ScrollView as RNScrollView,
|
||||
Text,
|
||||
View,
|
||||
Platform,
|
||||
useWindowDimensions,
|
||||
} from "react-native";
|
||||
import { ScrollView } from "react-native-gesture-handler";
|
||||
import { ScrollView, Gesture, GestureDetector } from "react-native-gesture-handler";
|
||||
import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles";
|
||||
import { Fonts } from "@/constants/theme";
|
||||
import * as Clipboard from "expo-clipboard";
|
||||
@@ -83,6 +84,8 @@ export function FileExplorerPane({ serverId, agentId }: FileExplorerPaneProps) {
|
||||
} = useFileExplorerActions(serverId);
|
||||
const sortOption = usePanelStore((state) => state.explorerSortOption);
|
||||
const setSortOption = usePanelStore((state) => state.setExplorerSortOption);
|
||||
const splitRatio = usePanelStore((state) => state.explorerFilesSplitRatio);
|
||||
const setSplitRatio = usePanelStore((state) => state.setExplorerFilesSplitRatio);
|
||||
|
||||
const directories = explorerState?.directories ?? new Map();
|
||||
const files = explorerState?.files ?? new Map();
|
||||
@@ -110,6 +113,7 @@ export function FileExplorerPane({ serverId, agentId }: FileExplorerPaneProps) {
|
||||
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);
|
||||
|
||||
// Bottom sheet for file preview (mobile)
|
||||
const previewSheetRef = useRef<BottomSheetModal>(null);
|
||||
@@ -261,7 +265,7 @@ export function FileExplorerPane({ serverId, agentId }: FileExplorerPaneProps) {
|
||||
if (!menuEntry) {
|
||||
return null;
|
||||
}
|
||||
const menuWidth = 180;
|
||||
const menuWidth = 240;
|
||||
const horizontalPadding = theme.spacing[2];
|
||||
const verticalPadding = theme.spacing[2];
|
||||
const maxLeft = Math.max(horizontalPadding, windowWidth - menuWidth - horizontalPadding);
|
||||
@@ -301,6 +305,66 @@ export function FileExplorerPane({ serverId, agentId }: FileExplorerPaneProps) {
|
||||
Boolean(isExplorerLoading && pendingRequest?.mode === "list" && pendingRequest?.path === ".");
|
||||
|
||||
const shouldShowInlinePreview = !isMobile && Boolean(selectedEntryPath);
|
||||
const dividerWidth = 10;
|
||||
const minTreeWidth = 220;
|
||||
const minPreviewWidth = 320;
|
||||
|
||||
const treePaneWidth = useMemo(() => {
|
||||
if (!shouldShowInlinePreview || containerWidth <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const available = Math.max(0, containerWidth - dividerWidth);
|
||||
const maxTree = Math.max(minTreeWidth, available - minPreviewWidth);
|
||||
const raw = Math.round(available * splitRatio);
|
||||
return Math.max(minTreeWidth, Math.min(maxTree, raw));
|
||||
}, [containerWidth, shouldShowInlinePreview, splitRatio]);
|
||||
|
||||
const resizeStartRef = useRef<{ startWidth: number; startX: number; available: number } | null>(
|
||||
null
|
||||
);
|
||||
|
||||
const splitResizeGesture = useMemo(() => {
|
||||
if (!shouldShowInlinePreview || containerWidth <= 0 || treePaneWidth === null) {
|
||||
return Gesture.Pan().enabled(false);
|
||||
}
|
||||
|
||||
const available = Math.max(0, containerWidth - dividerWidth);
|
||||
|
||||
return Gesture.Pan()
|
||||
.enabled(!isMobile)
|
||||
.hitSlop({ left: 12, right: 12, top: 0, bottom: 0 })
|
||||
.onBegin((event) => {
|
||||
resizeStartRef.current = {
|
||||
startWidth: treePaneWidth,
|
||||
startX: event.absoluteX,
|
||||
available,
|
||||
};
|
||||
})
|
||||
.onUpdate((event) => {
|
||||
const start = resizeStartRef.current;
|
||||
if (!start) {
|
||||
return;
|
||||
}
|
||||
const deltaX = event.absoluteX - start.startX;
|
||||
const nextWidth = start.startWidth + deltaX;
|
||||
const maxTree = Math.max(minTreeWidth, start.available - minPreviewWidth);
|
||||
const clamped = Math.max(minTreeWidth, Math.min(maxTree, nextWidth));
|
||||
const nextRatio = start.available > 0 ? clamped / start.available : splitRatio;
|
||||
setSplitRatio(nextRatio);
|
||||
})
|
||||
.onFinalize(() => {
|
||||
resizeStartRef.current = null;
|
||||
});
|
||||
}, [
|
||||
containerWidth,
|
||||
dividerWidth,
|
||||
isMobile,
|
||||
setSplitRatio,
|
||||
shouldShowInlinePreview,
|
||||
splitRatio,
|
||||
treePaneWidth,
|
||||
]);
|
||||
|
||||
const renderTreeRow = useCallback(
|
||||
({ item }: ListRenderItemInfo<TreeRow>) => {
|
||||
@@ -401,7 +465,10 @@ export function FileExplorerPane({ serverId, agentId }: FileExplorerPaneProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View
|
||||
style={styles.container}
|
||||
onLayout={(event) => setContainerWidth(event.nativeEvent.layout.width)}
|
||||
>
|
||||
{error ? (
|
||||
<View style={styles.centerState}>
|
||||
<Text style={styles.errorText}>{error}</Text>
|
||||
@@ -430,7 +497,13 @@ export function FileExplorerPane({ serverId, agentId }: FileExplorerPaneProps) {
|
||||
</View>
|
||||
) : (
|
||||
<View style={styles.desktopSplit}>
|
||||
<View style={[styles.treePane, shouldShowInlinePreview && styles.treePaneWithPreview]}>
|
||||
<View
|
||||
style={[
|
||||
styles.treePane,
|
||||
shouldShowInlinePreview && styles.treePaneWithPreview,
|
||||
shouldShowInlinePreview && treePaneWidth !== null ? { width: treePaneWidth } : null,
|
||||
]}
|
||||
>
|
||||
<FlatList
|
||||
data={treeRows}
|
||||
renderItem={renderTreeRow}
|
||||
@@ -444,7 +517,16 @@ export function FileExplorerPane({ serverId, agentId }: FileExplorerPaneProps) {
|
||||
</View>
|
||||
|
||||
{shouldShowInlinePreview ? (
|
||||
<View style={styles.previewPane}>
|
||||
<>
|
||||
<GestureDetector gesture={splitResizeGesture}>
|
||||
<View
|
||||
style={[
|
||||
styles.splitResizeHandle,
|
||||
Platform.OS === "web" && ({ cursor: "col-resize" } as any),
|
||||
]}
|
||||
/>
|
||||
</GestureDetector>
|
||||
<View style={styles.previewPane}>
|
||||
<View style={styles.previewHeaderContainer}>
|
||||
<View style={styles.previewHeaderInner}>
|
||||
<Pressable
|
||||
@@ -482,6 +564,7 @@ export function FileExplorerPane({ serverId, agentId }: FileExplorerPaneProps) {
|
||||
|
||||
<FilePreviewBody preview={preview} isLoading={isPreviewLoading} variant="inline" />
|
||||
</View>
|
||||
</>
|
||||
) : null}
|
||||
</View>
|
||||
)}
|
||||
@@ -517,7 +600,10 @@ export function FileExplorerPane({ serverId, agentId }: FileExplorerPaneProps) {
|
||||
</View>
|
||||
<View style={styles.entryMenuDivider} />
|
||||
<Pressable
|
||||
style={styles.entryMenuItem}
|
||||
style={({ hovered, pressed }) => [
|
||||
styles.entryMenuItem,
|
||||
(hovered || pressed) && styles.entryMenuItemHovered,
|
||||
]}
|
||||
onPress={() => {
|
||||
handleCopyPath(menuEntry.path);
|
||||
handleCloseMenu();
|
||||
@@ -527,7 +613,10 @@ export function FileExplorerPane({ serverId, agentId }: FileExplorerPaneProps) {
|
||||
</Pressable>
|
||||
{menuEntry.kind === "file" ? (
|
||||
<Pressable
|
||||
style={styles.entryMenuItem}
|
||||
style={({ hovered, pressed }) => [
|
||||
styles.entryMenuItem,
|
||||
(hovered || pressed) && styles.entryMenuItemHovered,
|
||||
]}
|
||||
onPress={async () => {
|
||||
handleCloseMenu();
|
||||
handleDownloadEntry(menuEntry);
|
||||
@@ -869,8 +958,12 @@ const styles = StyleSheet.create((theme) => ({
|
||||
minWidth: 0,
|
||||
},
|
||||
treePaneWithPreview: {
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: theme.colors.border,
|
||||
},
|
||||
splitResizeHandle: {
|
||||
width: 10,
|
||||
backgroundColor: "transparent",
|
||||
borderLeftWidth: 1,
|
||||
borderLeftColor: theme.colors.border,
|
||||
},
|
||||
previewPane: {
|
||||
flex: 1,
|
||||
@@ -974,36 +1067,43 @@ const styles = StyleSheet.create((theme) => ({
|
||||
backgroundColor: "rgba(0, 0, 0, 0.2)",
|
||||
},
|
||||
entryMenu: {
|
||||
borderRadius: theme.borderRadius.md,
|
||||
borderWidth: theme.borderWidth[1],
|
||||
borderColor: theme.colors.border,
|
||||
backgroundColor: theme.colors.surface2,
|
||||
padding: theme.spacing[1],
|
||||
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",
|
||||
justifyContent: "space-between",
|
||||
paddingVertical: theme.spacing[2],
|
||||
paddingHorizontal: theme.spacing[3],
|
||||
paddingVertical: theme.spacing[3],
|
||||
paddingHorizontal: theme.spacing[4],
|
||||
},
|
||||
entryMenuMeta: {
|
||||
color: theme.colors.foregroundMuted,
|
||||
fontSize: theme.fontSize.xs,
|
||||
fontSize: theme.fontSize.sm,
|
||||
},
|
||||
entryMenuDivider: {
|
||||
height: 1,
|
||||
backgroundColor: theme.colors.border,
|
||||
marginHorizontal: theme.spacing[2],
|
||||
marginVertical: theme.spacing[1],
|
||||
},
|
||||
entryMenuItem: {
|
||||
paddingVertical: theme.spacing[2],
|
||||
paddingHorizontal: theme.spacing[3],
|
||||
borderRadius: theme.borderRadius.md,
|
||||
paddingVertical: theme.spacing[4],
|
||||
paddingHorizontal: theme.spacing[4],
|
||||
},
|
||||
entryMenuItemHovered: {
|
||||
backgroundColor: theme.colors.surface2,
|
||||
},
|
||||
entryMenuText: {
|
||||
color: theme.colors.foreground,
|
||||
fontSize: theme.fontSize.sm,
|
||||
fontSize: theme.fontSize.lg,
|
||||
fontWeight: theme.fontWeight.semibold,
|
||||
},
|
||||
previewHeaderContainer: {
|
||||
|
||||
@@ -30,11 +30,15 @@ interface DesktopSidebarState {
|
||||
export type ExplorerTab = "changes" | "files";
|
||||
export type SortOption = "name" | "modified" | "size";
|
||||
|
||||
export const DEFAULT_EXPLORER_SIDEBAR_WIDTH = 400;
|
||||
export const DEFAULT_EXPLORER_SIDEBAR_WIDTH = Platform.OS === "web" ? 520 : 400;
|
||||
export const MIN_EXPLORER_SIDEBAR_WIDTH = 280;
|
||||
// Upper bound is intentionally generous; desktop resizing enforces a min-chat-width constraint.
|
||||
export const MAX_EXPLORER_SIDEBAR_WIDTH = 2000;
|
||||
|
||||
export const DEFAULT_EXPLORER_FILES_SPLIT_RATIO = 0.38;
|
||||
export const MIN_EXPLORER_FILES_SPLIT_RATIO = 0.2;
|
||||
export const MAX_EXPLORER_FILES_SPLIT_RATIO = 0.8;
|
||||
|
||||
interface PanelState {
|
||||
// Mobile: which panel is currently shown
|
||||
mobileView: MobilePanelView;
|
||||
@@ -46,6 +50,7 @@ interface PanelState {
|
||||
explorerTab: ExplorerTab;
|
||||
explorerWidth: number;
|
||||
explorerSortOption: SortOption;
|
||||
explorerFilesSplitRatio: number;
|
||||
|
||||
// Actions
|
||||
openAgentList: () => void;
|
||||
@@ -58,10 +63,19 @@ interface PanelState {
|
||||
setExplorerTab: (tab: ExplorerTab) => void;
|
||||
setExplorerWidth: (width: number) => void;
|
||||
setExplorerSortOption: (option: SortOption) => void;
|
||||
setExplorerFilesSplitRatio: (ratio: number) => void;
|
||||
}
|
||||
|
||||
function clampNumber(value: number, min: number, max: number): number {
|
||||
return Math.max(min, Math.min(max, value));
|
||||
}
|
||||
|
||||
function clampWidth(width: number): number {
|
||||
return Math.max(MIN_EXPLORER_SIDEBAR_WIDTH, Math.min(MAX_EXPLORER_SIDEBAR_WIDTH, width));
|
||||
return clampNumber(width, MIN_EXPLORER_SIDEBAR_WIDTH, MAX_EXPLORER_SIDEBAR_WIDTH);
|
||||
}
|
||||
|
||||
function clampExplorerFilesSplitRatio(ratio: number): number {
|
||||
return clampNumber(ratio, MIN_EXPLORER_FILES_SPLIT_RATIO, MAX_EXPLORER_FILES_SPLIT_RATIO);
|
||||
}
|
||||
|
||||
const DEFAULT_DESKTOP_OPEN = Platform.OS === "web";
|
||||
@@ -82,6 +96,7 @@ export const usePanelStore = create<PanelState>()(
|
||||
explorerTab: "changes",
|
||||
explorerWidth: DEFAULT_EXPLORER_SIDEBAR_WIDTH,
|
||||
explorerSortOption: "name",
|
||||
explorerFilesSplitRatio: DEFAULT_EXPLORER_FILES_SPLIT_RATIO,
|
||||
|
||||
openAgentList: () =>
|
||||
set((state) => ({
|
||||
@@ -137,16 +152,39 @@ export const usePanelStore = create<PanelState>()(
|
||||
setExplorerTab: (tab) => set({ explorerTab: tab }),
|
||||
setExplorerWidth: (width) => set({ explorerWidth: clampWidth(width) }),
|
||||
setExplorerSortOption: (option) => set({ explorerSortOption: option }),
|
||||
setExplorerFilesSplitRatio: (ratio) =>
|
||||
set({ explorerFilesSplitRatio: clampExplorerFilesSplitRatio(ratio) }),
|
||||
}),
|
||||
{
|
||||
name: "panel-state",
|
||||
version: 2,
|
||||
storage: createJSONStorage(() => AsyncStorage),
|
||||
migrate: (persistedState, version) => {
|
||||
const state = persistedState as Partial<PanelState> & Record<string, unknown>;
|
||||
|
||||
if (version < 2) {
|
||||
if (
|
||||
Platform.OS === "web" &&
|
||||
typeof state.explorerWidth === "number" &&
|
||||
state.explorerWidth === 400
|
||||
) {
|
||||
state.explorerWidth = DEFAULT_EXPLORER_SIDEBAR_WIDTH;
|
||||
}
|
||||
|
||||
if (typeof state.explorerFilesSplitRatio !== "number") {
|
||||
state.explorerFilesSplitRatio = DEFAULT_EXPLORER_FILES_SPLIT_RATIO;
|
||||
}
|
||||
}
|
||||
|
||||
return state as PanelState;
|
||||
},
|
||||
partialize: (state) => ({
|
||||
mobileView: state.mobileView,
|
||||
desktop: state.desktop,
|
||||
explorerTab: state.explorerTab,
|
||||
explorerWidth: state.explorerWidth,
|
||||
explorerSortOption: state.explorerSortOption,
|
||||
explorerFilesSplitRatio: state.explorerFilesSplitRatio,
|
||||
}),
|
||||
}
|
||||
)
|
||||
@@ -177,9 +215,11 @@ export function usePanelState(isMobile: boolean) {
|
||||
explorerTab: store.explorerTab,
|
||||
explorerWidth: store.explorerWidth,
|
||||
explorerSortOption: store.explorerSortOption,
|
||||
explorerFilesSplitRatio: store.explorerFilesSplitRatio,
|
||||
setExplorerTab: store.setExplorerTab,
|
||||
setExplorerWidth: store.setExplorerWidth,
|
||||
setExplorerSortOption: store.setExplorerSortOption,
|
||||
setExplorerFilesSplitRatio: store.setExplorerFilesSplitRatio,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -203,8 +243,10 @@ export function usePanelState(isMobile: boolean) {
|
||||
explorerTab: store.explorerTab,
|
||||
explorerWidth: store.explorerWidth,
|
||||
explorerSortOption: store.explorerSortOption,
|
||||
explorerFilesSplitRatio: store.explorerFilesSplitRatio,
|
||||
setExplorerTab: store.setExplorerTab,
|
||||
setExplorerWidth: store.setExplorerWidth,
|
||||
setExplorerSortOption: store.setExplorerSortOption,
|
||||
setExplorerFilesSplitRatio: store.setExplorerFilesSplitRatio,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user