diff --git a/packages/app/src/components/file-explorer-pane.tsx b/packages/app/src/components/file-explorer-pane.tsx
index 911709849..a7e555efe 100644
--- a/packages/app/src/components/file-explorer-pane.tsx
+++ b/packages/app/src/components/file-explorer-pane.tsx
@@ -15,6 +15,11 @@ import {
} from "react-native";
import { ScrollView, Gesture, GestureDetector } from "react-native-gesture-handler";
import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles";
+import Animated, {
+ runOnJS,
+ useAnimatedStyle,
+ useSharedValue,
+} from "react-native-reanimated";
import { Fonts } from "@/constants/theme";
import * as Clipboard from "expo-clipboard";
import {
@@ -309,7 +314,6 @@ 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;
@@ -317,60 +321,78 @@ export function FileExplorerPane({ serverId, agentId }: FileExplorerPaneProps) {
? splitRatio
: DEFAULT_EXPLORER_FILES_SPLIT_RATIO;
- const treePaneWidth = useMemo(() => {
- if (!shouldShowInlinePreview || containerWidth <= 0) {
- return null;
+ const splitAvailableWidth = useSharedValue(0);
+ const splitMaxTreeWidth = useSharedValue(minTreeWidth);
+ 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 raw = Math.round(available * safeSplitRatio);
- return Math.max(minTreeWidth, Math.min(maxTree, raw));
- }, [containerWidth, safeSplitRatio, shouldShowInlinePreview]);
+ const desired = Math.round(available * safeSplitRatio);
+ const clamped = Math.max(minTreeWidth, Math.min(maxTree, desired));
- 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(() => {
- if (!shouldShowInlinePreview || containerWidth <= 0 || treePaneWidth === null) {
+ if (isMobile || !shouldShowInlinePreview) {
return Gesture.Pan().enabled(false);
}
- const available = Math.max(0, containerWidth - dividerWidth);
-
return Gesture.Pan()
- .enabled(!isMobile)
- .runOnJS(true)
- .activeOffsetX([-2, 2])
- .failOffsetY([-5, 5])
.hitSlop({ left: 12, right: 12, top: 0, bottom: 0 })
- .onBegin(() => {
- resizeStartRef.current = {
- startWidth: treePaneWidth,
- available,
- };
+ .onStart(() => {
+ splitStartTreeWidth.value = splitTreeWidth.value;
})
.onUpdate((event) => {
- const start = resizeStartRef.current;
- if (!start) {
- return;
- }
- const nextWidth = start.startWidth + (event.translationX ?? 0);
- 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 : safeSplitRatio;
- setSplitRatio(nextRatio);
+ const nextWidth = splitStartTreeWidth.value + event.translationX;
+ const clamped = Math.max(
+ minTreeWidth,
+ Math.min(splitMaxTreeWidth.value, nextWidth)
+ );
+ splitTreeWidth.value = clamped;
})
- .onFinalize(() => {
- resizeStartRef.current = null;
+ .onEnd(() => {
+ const available = splitAvailableWidth.value;
+ const ratio = available > 0 ? splitTreeWidth.value / available : safeSplitRatio;
+ runOnJS(setSplitRatio)(ratio);
});
}, [
- containerWidth,
- dividerWidth,
isMobile,
+ minTreeWidth,
+ safeSplitRatio,
setSplitRatio,
shouldShowInlinePreview,
- safeSplitRatio,
- treePaneWidth,
+ splitAvailableWidth,
+ splitMaxTreeWidth,
+ splitStartTreeWidth,
+ splitTreeWidth,
]);
const renderTreeRow = useCallback(
@@ -432,16 +454,6 @@ export function FileExplorerPane({ serverId, agentId }: FileExplorerPaneProps) {
]
);
- const listHeaderComponent = useMemo(() => {
- return (
-
-
- {currentSortLabel}
-
-
- );
- }, [currentSortLabel, handleSortCycle]);
-
const handlePreviewSheetChange = useCallback(
(index: number) => {
if (index === -1) {
@@ -504,32 +516,36 @@ export function FileExplorerPane({ serverId, agentId }: FileExplorerPaneProps) {
) : (
-
+
+
+
+ {currentSortLabel}
+
+
row.entry.path}
contentContainerStyle={styles.entriesContent}
- ListHeaderComponent={listHeaderComponent}
initialNumToRender={24}
maxToRenderPerBatch={40}
windowSize={12}
/>
-
+
{shouldShowInlinePreview ? (
- <>
+
-
-
-
- {
- if (selectedEntryPath) {
- void Clipboard.setStringAsync(selectedEntryPath);
- }
- }}
- style={({ hovered, pressed }) => [
- styles.previewHeaderRow,
- (hovered || pressed) && styles.previewHeaderRowHovered,
- ]}
- >
-
- {selectedEntryPath?.split("/").pop() ?? "Preview"}
-
- {isPreviewLoading ? (
-
- ) : null}
-
+
+
+ {selectedEntryPath?.split("/").pop() ?? "Preview"}
+
+
+ {isPreviewLoading ? (
+
+ ) : null}
[
styles.iconButton,
- (hovered || pressed) && styles.previewHeaderRowHovered,
+ (hovered || pressed) && styles.iconButtonHovered,
]}
accessibilityRole="button"
accessibilityLabel="Close preview"
@@ -575,7 +578,6 @@ export function FileExplorerPane({ serverId, agentId }: FileExplorerPaneProps) {
- >
) : null}
)}
@@ -697,22 +699,26 @@ function FilePreviewBody({
if (preview.kind === "text") {
if (variant === "sheet") {
return (
-
-
+
+
{preview.content}
);
}
return (
-
-
+
+
{preview.content}
@@ -965,33 +971,56 @@ const styles = StyleSheet.create((theme) => ({
minHeight: 0,
},
treePane: {
- flex: 1,
minWidth: 0,
},
+ treePaneFill: {
+ flex: 1,
+ },
treePaneWithPreview: {
+ flex: 0,
+ flexGrow: 0,
+ flexShrink: 0,
},
splitResizeHandle: {
+ position: "absolute",
+ left: -5,
+ top: 0,
+ bottom: 0,
width: 10,
- backgroundColor: theme.colors.surface0,
- borderLeftWidth: 1,
- borderLeftColor: theme.colors.border,
- borderRightWidth: 1,
- borderRightColor: theme.colors.border,
+ zIndex: 20,
},
previewPane: {
flex: 1,
minWidth: 0,
+ position: "relative",
+ borderLeftWidth: 1,
+ borderLeftColor: theme.colors.border,
},
- headerContainer: {
- paddingHorizontal: theme.spacing[2],
- paddingTop: theme.spacing[2],
- paddingBottom: theme.spacing[1],
- },
- sortButton: {
- alignSelf: "flex-end",
+ paneHeader: {
flexDirection: "row",
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],
borderRadius: theme.borderRadius.md,
borderWidth: theme.borderWidth[1],
@@ -1001,6 +1030,10 @@ const styles = StyleSheet.create((theme) => ({
color: theme.colors.foregroundMuted,
fontSize: theme.fontSize.xs,
},
+ treeList: {
+ flex: 1,
+ minHeight: 0,
+ },
entriesContent: {
paddingBottom: theme.spacing[4],
},
@@ -1119,36 +1152,11 @@ const styles = StyleSheet.create((theme) => ({
fontSize: theme.fontSize.lg,
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: {
flex: 1,
color: theme.colors.foreground,
fontSize: theme.fontSize.sm,
- fontWeight: theme.fontWeight.semibold,
+ fontWeight: theme.fontWeight.normal,
},
iconButton: {
width: 32,
@@ -1157,11 +1165,16 @@ const styles = StyleSheet.create((theme) => ({
alignItems: "center",
justifyContent: "center",
},
+ iconButtonHovered: {
+ backgroundColor: theme.colors.surface2,
+ },
previewContent: {
flex: 1,
},
- previewTextContent: {
- padding: theme.spacing[3],
+ previewCodeScrollContent: {
+ paddingTop: theme.spacing[3],
+ paddingHorizontal: theme.spacing[3],
+ paddingBottom: theme.spacing[3] + theme.spacing[2],
},
codeText: {
color: theme.colors.foreground,
diff --git a/packages/app/src/stores/panel-store.ts b/packages/app/src/stores/panel-store.ts
index a082d80a6..01cde1d30 100644
--- a/packages/app/src/stores/panel-store.ts
+++ b/packages/app/src/stores/panel-store.ts
@@ -30,7 +30,7 @@ interface DesktopSidebarState {
export type ExplorerTab = "changes" | "files";
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;
// Upper bound is intentionally generous; desktop resizing enforces a min-chat-width constraint.
export const MAX_EXPLORER_SIDEBAR_WIDTH = 2000;
@@ -164,7 +164,7 @@ export const usePanelStore = create()(
}),
{
name: "panel-state",
- version: 2,
+ version: 3,
storage: createJSONStorage(() => AsyncStorage),
migrate: (persistedState, version) => {
const state = persistedState as Partial & Record;
@@ -187,6 +187,16 @@ export const usePanelStore = create()(
}
}
+ 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;
},
partialize: (state) => ({