Add refresh loading feedback in file explorer

This commit is contained in:
Mohamed Boudra
2026-02-12 14:05:46 +07:00
parent 0d0f2826d9
commit bb93fc2a61
2 changed files with 100 additions and 73 deletions
@@ -1,4 +1,5 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { import {
ActivityIndicator, ActivityIndicator,
FlatList, FlatList,
@@ -264,25 +265,35 @@ export function FileExplorerPane({ serverId, agentId }: FileExplorerPaneProps) {
setSortOption(SORT_OPTIONS[nextIndex].value); setSortOption(SORT_OPTIONS[nextIndex].value);
}, [sortOption, setSortOption]); }, [sortOption, setSortOption]);
const handleRefresh = useCallback(() => { const { refetch: refetchExplorer, isFetching: isRefreshFetching } = useQuery({
if (!agentId || !requestDirectoryListing) { queryKey: ["fileExplorerRefresh", serverId, agentId],
return; queryFn: async () => {
} if (!agentId) {
// Refresh the root directory return null;
requestDirectoryListing(agentId, ".", { recordHistory: false, setCurrentPath: false });
// Also refresh any expanded directories
expandedPaths.forEach((path) => {
if (path !== ".") {
requestDirectoryListing(agentId, path, { recordHistory: false, setCurrentPath: false });
} }
});
// Refresh the preview if a file is selected const directoryPaths = Array.from(expandedPaths);
if (selectedEntryPath && requestFilePreview) { if (!directoryPaths.includes(".")) {
requestFilePreview(agentId, selectedEntryPath); directoryPaths.unshift(".");
} }
}, [agentId, expandedPaths, requestDirectoryListing, requestFilePreview, selectedEntryPath]);
await Promise.all([
...directoryPaths.map((path) =>
requestDirectoryListing(agentId, path, {
recordHistory: false,
setCurrentPath: false,
})
),
...(selectedEntryPath ? [requestFilePreview(agentId, selectedEntryPath)] : []),
]);
return null;
},
enabled: false,
});
const handleRefresh = useCallback(() => {
void refetchExplorer();
}, [refetchExplorer]);
const currentSortLabel = SORT_OPTIONS.find((opt) => opt.value === sortOption)?.label ?? "Name"; const currentSortLabel = SORT_OPTIONS.find((opt) => opt.value === sortOption)?.label ?? "Name";
@@ -627,15 +638,20 @@ export function FileExplorerPane({ serverId, agentId }: FileExplorerPaneProps) {
<View style={styles.paneHeaderRight}> <View style={styles.paneHeaderRight}>
<Pressable <Pressable
onPress={handleRefresh} onPress={handleRefresh}
disabled={isRefreshFetching}
hitSlop={8} hitSlop={8}
style={({ hovered, pressed }) => [ style={({ hovered, pressed }) => [
styles.iconButton, styles.iconButton,
(hovered || pressed) && styles.iconButtonHovered, (hovered || pressed) && styles.iconButtonHovered,
pressed && styles.iconButtonPressed,
isRefreshFetching && styles.iconButtonLoading,
]} ]}
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel="Refresh files" accessibilityLabel="Refresh files"
> >
<RotateCw size={16} color={theme.colors.foregroundMuted} /> <View style={isRefreshFetching ? styles.refreshIconLoading : undefined}>
<RotateCw size={16} color={theme.colors.foregroundMuted} />
</View>
</Pressable> </Pressable>
<Pressable style={styles.sortButton} onPress={handleSortCycle}> <Pressable style={styles.sortButton} onPress={handleSortCycle}>
<Text style={styles.sortButtonText}>{currentSortLabel}</Text> <Text style={styles.sortButtonText}>{currentSortLabel}</Text>
@@ -660,15 +676,20 @@ export function FileExplorerPane({ serverId, agentId }: FileExplorerPaneProps) {
<View style={styles.paneHeaderRight}> <View style={styles.paneHeaderRight}>
<Pressable <Pressable
onPress={handleRefresh} onPress={handleRefresh}
disabled={isRefreshFetching}
hitSlop={8} hitSlop={8}
style={({ hovered, pressed }) => [ style={({ hovered, pressed }) => [
styles.iconButton, styles.iconButton,
(hovered || pressed) && styles.iconButtonHovered, (hovered || pressed) && styles.iconButtonHovered,
pressed && styles.iconButtonPressed,
isRefreshFetching && styles.iconButtonLoading,
]} ]}
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel="Refresh files" accessibilityLabel="Refresh files"
> >
<RotateCw size={16} color={theme.colors.foregroundMuted} /> <View style={isRefreshFetching ? styles.refreshIconLoading : undefined}>
<RotateCw size={16} color={theme.colors.foregroundMuted} />
</View>
</Pressable> </Pressable>
<Pressable style={styles.sortButton} onPress={handleSortCycle}> <Pressable style={styles.sortButton} onPress={handleSortCycle}>
<Text style={styles.sortButtonText}>{currentSortLabel}</Text> <Text style={styles.sortButtonText}>{currentSortLabel}</Text>
@@ -1221,6 +1242,16 @@ const styles = StyleSheet.create((theme) => ({
iconButtonHovered: { iconButtonHovered: {
backgroundColor: theme.colors.surface2, backgroundColor: theme.colors.surface2,
}, },
iconButtonPressed: {
opacity: 0.8,
transform: [{ scale: 0.96 }],
},
iconButtonLoading: {
opacity: 0.8,
},
refreshIconLoading: {
opacity: 0.55,
},
previewContent: { previewContent: {
flex: 1, flex: 1,
}, },
@@ -41,7 +41,7 @@ export function useFileExplorerActions(serverId: string) {
); );
const requestDirectoryListing = useCallback( const requestDirectoryListing = useCallback(
( async (
agentId: string, agentId: string,
path: string, path: string,
options?: { recordHistory?: boolean; setCurrentPath?: boolean } options?: { recordHistory?: boolean; setCurrentPath?: boolean }
@@ -77,42 +77,40 @@ export function useFileExplorerActions(serverId: string) {
return; return;
} }
void client try {
.exploreFileSystem(agentId, normalizedPath, "list") const payload = await client.exploreFileSystem(agentId, normalizedPath, "list");
.then((payload) => { updateExplorerState(agentId, (state) => {
updateExplorerState(agentId, (state) => { const nextState: AgentFileExplorerState = {
const nextState: AgentFileExplorerState = {
...state,
isLoading: false,
lastError: payload.error ?? null,
pendingRequest: null,
directories: state.directories,
files: state.files,
};
if (!payload.error && payload.directory) {
const directories = new Map(state.directories);
directories.set(payload.directory.path, payload.directory);
nextState.directories = directories;
}
return nextState;
});
})
.catch((error) => {
updateExplorerState(agentId, (state) => ({
...state, ...state,
isLoading: false, isLoading: false,
lastError: error instanceof Error ? error.message : "Failed to list directory", lastError: payload.error ?? null,
pendingRequest: null, pendingRequest: null,
})); directories: state.directories,
files: state.files,
};
if (!payload.error && payload.directory) {
const directories = new Map(state.directories);
directories.set(payload.directory.path, payload.directory);
nextState.directories = directories;
}
return nextState;
}); });
} catch (error) {
updateExplorerState(agentId, (state) => ({
...state,
isLoading: false,
lastError: error instanceof Error ? error.message : "Failed to list directory",
pendingRequest: null,
}));
}
}, },
[client, updateExplorerState] [client, updateExplorerState]
); );
const requestFilePreview = useCallback( const requestFilePreview = useCallback(
(agentId: string, path: string) => { async (agentId: string, path: string) => {
const normalizedPath = path && path.length > 0 ? path : "."; const normalizedPath = path && path.length > 0 ? path : ".";
updateExplorerState(agentId, (state) => ({ updateExplorerState(agentId, (state) => ({
...state, ...state,
@@ -131,36 +129,34 @@ export function useFileExplorerActions(serverId: string) {
return; return;
} }
void client try {
.exploreFileSystem(agentId, normalizedPath, "file") const payload = await client.exploreFileSystem(agentId, normalizedPath, "file");
.then((payload) => { updateExplorerState(agentId, (state) => {
updateExplorerState(agentId, (state) => { const nextState: AgentFileExplorerState = {
const nextState: AgentFileExplorerState = {
...state,
isLoading: false,
pendingRequest: null,
directories: state.directories,
files: state.files,
};
if (!payload.error && payload.file) {
const files = new Map(state.files);
files.set(payload.file.path, payload.file);
nextState.files = files;
} else if (payload.error) {
nextState.lastError = payload.error;
}
return nextState;
});
})
.catch(() => {
updateExplorerState(agentId, (state) => ({
...state, ...state,
isLoading: false, isLoading: false,
pendingRequest: null, pendingRequest: null,
})); directories: state.directories,
files: state.files,
};
if (!payload.error && payload.file) {
const files = new Map(state.files);
files.set(payload.file.path, payload.file);
nextState.files = files;
} else if (payload.error) {
nextState.lastError = payload.error;
}
return nextState;
}); });
} catch {
updateExplorerState(agentId, (state) => ({
...state,
isLoading: false,
pendingRequest: null,
}));
}
}, },
[client, updateExplorerState] [client, updateExplorerState]
); );