Update files

This commit is contained in:
Mohamed Boudra
2026-02-04 13:17:22 +07:00
parent d2fd1e954f
commit 052e917ed7
2 changed files with 155 additions and 132 deletions
+143 -130
View File
@@ -15,6 +15,11 @@ import {
} 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";
import Animated, {
runOnJS,
useAnimatedStyle,
useSharedValue,
} from "react-native-reanimated";
import { Fonts } from "@/constants/theme"; import { Fonts } from "@/constants/theme";
import * as Clipboard from "expo-clipboard"; import * as Clipboard from "expo-clipboard";
import { import {
@@ -309,7 +314,6 @@ export function FileExplorerPane({ serverId, agentId }: FileExplorerPaneProps) {
Boolean(isExplorerLoading && pendingRequest?.mode === "list" && pendingRequest?.path === "."); Boolean(isExplorerLoading && pendingRequest?.mode === "list" && pendingRequest?.path === ".");
const shouldShowInlinePreview = !isMobile && Boolean(selectedEntryPath); const shouldShowInlinePreview = !isMobile && Boolean(selectedEntryPath);
const dividerWidth = 10;
const minTreeWidth = 220; const minTreeWidth = 220;
const minPreviewWidth = 320; const minPreviewWidth = 320;
@@ -317,60 +321,78 @@ export function FileExplorerPane({ serverId, agentId }: FileExplorerPaneProps) {
? splitRatio ? splitRatio
: DEFAULT_EXPLORER_FILES_SPLIT_RATIO; : DEFAULT_EXPLORER_FILES_SPLIT_RATIO;
const treePaneWidth = useMemo(() => { const splitAvailableWidth = useSharedValue(0);
if (!shouldShowInlinePreview || containerWidth <= 0) { const splitMaxTreeWidth = useSharedValue(minTreeWidth);
return null; const splitTreeWidth = useSharedValue(minTreeWidth);
const splitStartTreeWidth = useSharedValue(minTreeWidth);
useEffect(() => {
if (!shouldShowInlinePreview) {
return;
}
if (containerWidth <= 0) {
return;
} }
const available = Math.max(0, containerWidth - dividerWidth); const available = Math.max(0, containerWidth);
const maxTree = Math.max(minTreeWidth, available - minPreviewWidth); const maxTree = Math.max(minTreeWidth, available - minPreviewWidth);
const raw = Math.round(available * safeSplitRatio); const desired = Math.round(available * safeSplitRatio);
return Math.max(minTreeWidth, Math.min(maxTree, raw)); const clamped = Math.max(minTreeWidth, Math.min(maxTree, desired));
}, [containerWidth, safeSplitRatio, shouldShowInlinePreview]);
const resizeStartRef = useRef<{ startWidth: number; available: number } | null>(null); splitAvailableWidth.value = available;
splitMaxTreeWidth.value = maxTree;
splitTreeWidth.value = clamped;
}, [
containerWidth,
minPreviewWidth,
minTreeWidth,
safeSplitRatio,
shouldShowInlinePreview,
splitAvailableWidth,
splitMaxTreeWidth,
splitTreeWidth,
]);
const treePaneAnimatedStyle = useAnimatedStyle(() => ({
width: splitTreeWidth.value,
flexBasis: splitTreeWidth.value,
flexGrow: 0,
flexShrink: 0,
}));
const splitResizeGesture = useMemo(() => { const splitResizeGesture = useMemo(() => {
if (!shouldShowInlinePreview || containerWidth <= 0 || treePaneWidth === null) { if (isMobile || !shouldShowInlinePreview) {
return Gesture.Pan().enabled(false); return Gesture.Pan().enabled(false);
} }
const available = Math.max(0, containerWidth - dividerWidth);
return Gesture.Pan() return Gesture.Pan()
.enabled(!isMobile)
.runOnJS(true)
.activeOffsetX([-2, 2])
.failOffsetY([-5, 5])
.hitSlop({ left: 12, right: 12, top: 0, bottom: 0 }) .hitSlop({ left: 12, right: 12, top: 0, bottom: 0 })
.onBegin(() => { .onStart(() => {
resizeStartRef.current = { splitStartTreeWidth.value = splitTreeWidth.value;
startWidth: treePaneWidth,
available,
};
}) })
.onUpdate((event) => { .onUpdate((event) => {
const start = resizeStartRef.current; const nextWidth = splitStartTreeWidth.value + event.translationX;
if (!start) { const clamped = Math.max(
return; minTreeWidth,
} Math.min(splitMaxTreeWidth.value, nextWidth)
const nextWidth = start.startWidth + (event.translationX ?? 0); );
const maxTree = Math.max(minTreeWidth, start.available - minPreviewWidth); splitTreeWidth.value = clamped;
const clamped = Math.max(minTreeWidth, Math.min(maxTree, nextWidth));
const nextRatio = start.available > 0 ? clamped / start.available : safeSplitRatio;
setSplitRatio(nextRatio);
}) })
.onFinalize(() => { .onEnd(() => {
resizeStartRef.current = null; const available = splitAvailableWidth.value;
const ratio = available > 0 ? splitTreeWidth.value / available : safeSplitRatio;
runOnJS(setSplitRatio)(ratio);
}); });
}, [ }, [
containerWidth,
dividerWidth,
isMobile, isMobile,
minTreeWidth,
safeSplitRatio,
setSplitRatio, setSplitRatio,
shouldShowInlinePreview, shouldShowInlinePreview,
safeSplitRatio, splitAvailableWidth,
treePaneWidth, splitMaxTreeWidth,
splitStartTreeWidth,
splitTreeWidth,
]); ]);
const renderTreeRow = useCallback( const renderTreeRow = useCallback(
@@ -432,16 +454,6 @@ export function FileExplorerPane({ serverId, agentId }: FileExplorerPaneProps) {
] ]
); );
const listHeaderComponent = useMemo(() => {
return (
<View style={styles.headerContainer}>
<Pressable style={styles.sortButton} onPress={handleSortCycle}>
<Text style={styles.sortButtonText}>{currentSortLabel}</Text>
</Pressable>
</View>
);
}, [currentSortLabel, handleSortCycle]);
const handlePreviewSheetChange = useCallback( const handlePreviewSheetChange = useCallback(
(index: number) => { (index: number) => {
if (index === -1) { if (index === -1) {
@@ -504,32 +516,36 @@ export function FileExplorerPane({ serverId, agentId }: FileExplorerPaneProps) {
</View> </View>
) : ( ) : (
<View style={styles.desktopSplit}> <View style={styles.desktopSplit}>
<View <Animated.View
style={[ style={[
styles.treePane, styles.treePane,
shouldShowInlinePreview && styles.treePaneWithPreview, shouldShowInlinePreview
shouldShowInlinePreview && treePaneWidth !== null ? [styles.treePaneWithPreview, { minWidth: minTreeWidth }, treePaneAnimatedStyle]
? { width: treePaneWidth, flex: 0, flexGrow: 0, flexShrink: 0 } : styles.treePaneFill,
: null,
]} ]}
> >
<View style={styles.paneHeader} testID="files-pane-header">
<View style={styles.paneHeaderLeft} />
<Pressable style={styles.sortButton} onPress={handleSortCycle}>
<Text style={styles.sortButtonText}>{currentSortLabel}</Text>
</Pressable>
</View>
<FlatList <FlatList
style={styles.treeList}
data={treeRows} data={treeRows}
renderItem={renderTreeRow} renderItem={renderTreeRow}
keyExtractor={(row) => row.entry.path} keyExtractor={(row) => row.entry.path}
contentContainerStyle={styles.entriesContent} contentContainerStyle={styles.entriesContent}
ListHeaderComponent={listHeaderComponent}
initialNumToRender={24} initialNumToRender={24}
maxToRenderPerBatch={40} maxToRenderPerBatch={40}
windowSize={12} windowSize={12}
/> />
</View> </Animated.View>
{shouldShowInlinePreview ? ( {shouldShowInlinePreview ? (
<> <View style={styles.previewPane}>
<GestureDetector gesture={splitResizeGesture}> <GestureDetector gesture={splitResizeGesture}>
<View <View
pointerEvents="box-only"
style={[ style={[
styles.splitResizeHandle, styles.splitResizeHandle,
Platform.OS === "web" && ({ cursor: "col-resize" } as any), Platform.OS === "web" && ({ cursor: "col-resize" } as any),
@@ -537,33 +553,20 @@ export function FileExplorerPane({ serverId, agentId }: FileExplorerPaneProps) {
]} ]}
/> />
</GestureDetector> </GestureDetector>
<View style={styles.previewPane}> <View style={styles.paneHeader} testID="preview-pane-header">
<View style={styles.previewHeaderContainer}> <Text style={styles.previewHeaderText} numberOfLines={1}>
<View style={styles.previewHeaderInner}> {selectedEntryPath?.split("/").pop() ?? "Preview"}
<Pressable </Text>
onPress={() => { <View style={styles.previewHeaderRight}>
if (selectedEntryPath) { {isPreviewLoading ? (
void Clipboard.setStringAsync(selectedEntryPath); <ActivityIndicator size="small" color={theme.colors.foregroundMuted} />
} ) : null}
}}
style={({ hovered, pressed }) => [
styles.previewHeaderRow,
(hovered || pressed) && styles.previewHeaderRowHovered,
]}
>
<Text style={styles.previewHeaderText} numberOfLines={1}>
{selectedEntryPath?.split("/").pop() ?? "Preview"}
</Text>
{isPreviewLoading ? (
<ActivityIndicator size="small" color={theme.colors.foregroundMuted} />
) : null}
</Pressable>
<Pressable <Pressable
onPress={handleClosePreview} onPress={handleClosePreview}
hitSlop={8} hitSlop={8}
style={({ hovered, pressed }) => [ style={({ hovered, pressed }) => [
styles.iconButton, styles.iconButton,
(hovered || pressed) && styles.previewHeaderRowHovered, (hovered || pressed) && styles.iconButtonHovered,
]} ]}
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel="Close preview" accessibilityLabel="Close preview"
@@ -575,7 +578,6 @@ export function FileExplorerPane({ serverId, agentId }: FileExplorerPaneProps) {
<FilePreviewBody preview={preview} isLoading={isPreviewLoading} variant="inline" /> <FilePreviewBody preview={preview} isLoading={isPreviewLoading} variant="inline" />
</View> </View>
</>
) : null} ) : null}
</View> </View>
)} )}
@@ -697,22 +699,26 @@ function FilePreviewBody({
if (preview.kind === "text") { if (preview.kind === "text") {
if (variant === "sheet") { if (variant === "sheet") {
return ( return (
<BottomSheetScrollView <BottomSheetScrollView style={styles.previewContent}>
style={styles.previewContent} <ScrollView
contentContainerStyle={styles.previewTextContent} horizontal
> nestedScrollEnabled
<ScrollView horizontal nestedScrollEnabled showsHorizontalScrollIndicator> showsHorizontalScrollIndicator
contentContainerStyle={styles.previewCodeScrollContent}
>
<Text style={styles.codeText}>{preview.content}</Text> <Text style={styles.codeText}>{preview.content}</Text>
</ScrollView> </ScrollView>
</BottomSheetScrollView> </BottomSheetScrollView>
); );
} }
return ( return (
<RNScrollView <RNScrollView style={styles.previewContent}>
style={styles.previewContent} <RNScrollView
contentContainerStyle={styles.previewTextContent} horizontal
> nestedScrollEnabled
<RNScrollView horizontal nestedScrollEnabled showsHorizontalScrollIndicator> showsHorizontalScrollIndicator
contentContainerStyle={styles.previewCodeScrollContent}
>
<Text style={styles.codeText}>{preview.content}</Text> <Text style={styles.codeText}>{preview.content}</Text>
</RNScrollView> </RNScrollView>
</RNScrollView> </RNScrollView>
@@ -965,33 +971,56 @@ const styles = StyleSheet.create((theme) => ({
minHeight: 0, minHeight: 0,
}, },
treePane: { treePane: {
flex: 1,
minWidth: 0, minWidth: 0,
}, },
treePaneFill: {
flex: 1,
},
treePaneWithPreview: { treePaneWithPreview: {
flex: 0,
flexGrow: 0,
flexShrink: 0,
}, },
splitResizeHandle: { splitResizeHandle: {
position: "absolute",
left: -5,
top: 0,
bottom: 0,
width: 10, width: 10,
backgroundColor: theme.colors.surface0, zIndex: 20,
borderLeftWidth: 1,
borderLeftColor: theme.colors.border,
borderRightWidth: 1,
borderRightColor: theme.colors.border,
}, },
previewPane: { previewPane: {
flex: 1, flex: 1,
minWidth: 0, minWidth: 0,
position: "relative",
borderLeftWidth: 1,
borderLeftColor: theme.colors.border,
}, },
headerContainer: { paneHeader: {
paddingHorizontal: theme.spacing[2],
paddingTop: theme.spacing[2],
paddingBottom: theme.spacing[1],
},
sortButton: {
alignSelf: "flex-end",
flexDirection: "row", flexDirection: "row",
alignItems: "center", alignItems: "center",
paddingVertical: theme.spacing[1], justifyContent: "space-between",
height: 32 + theme.spacing[2] * 2,
paddingHorizontal: theme.spacing[3],
borderBottomWidth: 1,
borderBottomColor: theme.colors.border,
backgroundColor: theme.colors.surface0,
},
paneHeaderLeft: {
flex: 1,
minWidth: 0,
},
previewHeaderRight: {
flexDirection: "row",
alignItems: "center",
gap: theme.spacing[2],
flexShrink: 0,
},
sortButton: {
height: 32,
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
paddingHorizontal: theme.spacing[2], paddingHorizontal: theme.spacing[2],
borderRadius: theme.borderRadius.md, borderRadius: theme.borderRadius.md,
borderWidth: theme.borderWidth[1], borderWidth: theme.borderWidth[1],
@@ -1001,6 +1030,10 @@ const styles = StyleSheet.create((theme) => ({
color: theme.colors.foregroundMuted, color: theme.colors.foregroundMuted,
fontSize: theme.fontSize.xs, fontSize: theme.fontSize.xs,
}, },
treeList: {
flex: 1,
minHeight: 0,
},
entriesContent: { entriesContent: {
paddingBottom: theme.spacing[4], paddingBottom: theme.spacing[4],
}, },
@@ -1119,36 +1152,11 @@ const styles = StyleSheet.create((theme) => ({
fontSize: theme.fontSize.lg, fontSize: theme.fontSize.lg,
fontWeight: theme.fontWeight.semibold, fontWeight: theme.fontWeight.semibold,
}, },
previewHeaderContainer: {
borderBottomWidth: 1,
borderBottomColor: theme.colors.border,
},
previewHeaderInner: {
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
gap: theme.spacing[2],
paddingHorizontal: theme.spacing[2],
paddingVertical: theme.spacing[2],
},
previewHeaderRow: {
flex: 1,
minWidth: 0,
flexDirection: "row",
alignItems: "center",
gap: theme.spacing[2],
borderRadius: theme.borderRadius.md,
paddingVertical: theme.spacing[1],
paddingHorizontal: theme.spacing[2],
},
previewHeaderRowHovered: {
backgroundColor: theme.colors.surface2,
},
previewHeaderText: { previewHeaderText: {
flex: 1, flex: 1,
color: theme.colors.foreground, color: theme.colors.foreground,
fontSize: theme.fontSize.sm, fontSize: theme.fontSize.sm,
fontWeight: theme.fontWeight.semibold, fontWeight: theme.fontWeight.normal,
}, },
iconButton: { iconButton: {
width: 32, width: 32,
@@ -1157,11 +1165,16 @@ const styles = StyleSheet.create((theme) => ({
alignItems: "center", alignItems: "center",
justifyContent: "center", justifyContent: "center",
}, },
iconButtonHovered: {
backgroundColor: theme.colors.surface2,
},
previewContent: { previewContent: {
flex: 1, flex: 1,
}, },
previewTextContent: { previewCodeScrollContent: {
padding: theme.spacing[3], paddingTop: theme.spacing[3],
paddingHorizontal: theme.spacing[3],
paddingBottom: theme.spacing[3] + theme.spacing[2],
}, },
codeText: { codeText: {
color: theme.colors.foreground, color: theme.colors.foreground,
+12 -2
View File
@@ -30,7 +30,7 @@ interface DesktopSidebarState {
export type ExplorerTab = "changes" | "files"; export type ExplorerTab = "changes" | "files";
export type SortOption = "name" | "modified" | "size"; export type SortOption = "name" | "modified" | "size";
export const DEFAULT_EXPLORER_SIDEBAR_WIDTH = Platform.OS === "web" ? 520 : 400; export const DEFAULT_EXPLORER_SIDEBAR_WIDTH = Platform.OS === "web" ? 640 : 400;
export const MIN_EXPLORER_SIDEBAR_WIDTH = 280; export const MIN_EXPLORER_SIDEBAR_WIDTH = 280;
// Upper bound is intentionally generous; desktop resizing enforces a min-chat-width constraint. // Upper bound is intentionally generous; desktop resizing enforces a min-chat-width constraint.
export const MAX_EXPLORER_SIDEBAR_WIDTH = 2000; export const MAX_EXPLORER_SIDEBAR_WIDTH = 2000;
@@ -164,7 +164,7 @@ export const usePanelStore = create<PanelState>()(
}), }),
{ {
name: "panel-state", name: "panel-state",
version: 2, version: 3,
storage: createJSONStorage(() => AsyncStorage), storage: createJSONStorage(() => AsyncStorage),
migrate: (persistedState, version) => { migrate: (persistedState, version) => {
const state = persistedState as Partial<PanelState> & Record<string, unknown>; const state = persistedState as Partial<PanelState> & Record<string, unknown>;
@@ -187,6 +187,16 @@ export const usePanelStore = create<PanelState>()(
} }
} }
if (version < 3) {
if (
Platform.OS === "web" &&
typeof state.explorerWidth === "number" &&
(state.explorerWidth === 400 || state.explorerWidth === 520)
) {
state.explorerWidth = DEFAULT_EXPLORER_SIDEBAR_WIDTH;
}
}
return state as PanelState; return state as PanelState;
}, },
partialize: (state) => ({ partialize: (state) => ({