From 1138b83a069d8d41633725c4c98ca0c2254e0399 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Wed, 4 Feb 2026 10:50:15 +0700 Subject: [PATCH] Update files --- .../app/src/components/file-explorer-pane.tsx | 146 +++++++++++++++--- packages/app/src/stores/panel-store.ts | 46 +++++- 2 files changed, 167 insertions(+), 25 deletions(-) diff --git a/packages/app/src/components/file-explorer-pane.tsx b/packages/app/src/components/file-explorer-pane.tsx index 0082a8c7a..0d2f96309 100644 --- a/packages/app/src/components/file-explorer-pane.tsx +++ b/packages/app/src/components/file-explorer-pane.tsx @@ -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(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(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) => { @@ -401,7 +465,10 @@ export function FileExplorerPane({ serverId, agentId }: FileExplorerPaneProps) { } return ( - + setContainerWidth(event.nativeEvent.layout.width)} + > {error ? ( {error} @@ -430,7 +497,13 @@ export function FileExplorerPane({ serverId, agentId }: FileExplorerPaneProps) { ) : ( - + {shouldShowInlinePreview ? ( - + <> + + + + + ) : null} )} @@ -517,7 +600,10 @@ export function FileExplorerPane({ serverId, agentId }: FileExplorerPaneProps) { [ + styles.entryMenuItem, + (hovered || pressed) && styles.entryMenuItemHovered, + ]} onPress={() => { handleCopyPath(menuEntry.path); handleCloseMenu(); @@ -527,7 +613,10 @@ export function FileExplorerPane({ serverId, agentId }: FileExplorerPaneProps) { {menuEntry.kind === "file" ? ( [ + 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: { diff --git a/packages/app/src/stores/panel-store.ts b/packages/app/src/stores/panel-store.ts index 754f819e6..69e3c11c9 100644 --- a/packages/app/src/stores/panel-store.ts +++ b/packages/app/src/stores/panel-store.ts @@ -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()( 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()( 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 & Record; + + 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, }; }