diff --git a/packages/app/src/components/file-explorer-pane.tsx b/packages/app/src/components/file-explorer-pane.tsx
index 111c46ade..e75b6a0dd 100644
--- a/packages/app/src/components/file-explorer-pane.tsx
+++ b/packages/app/src/components/file-explorer-pane.tsx
@@ -14,9 +14,13 @@ import {
import { ScrollView, Gesture, GestureDetector } from "react-native-gesture-handler";
import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles";
import Animated, {
+ cancelAnimation,
+ Easing,
runOnJS,
useAnimatedStyle,
useSharedValue,
+ withRepeat,
+ withTiming,
} from "react-native-reanimated";
import { Fonts } from "@/constants/theme";
import * as Clipboard from "expo-clipboard";
@@ -294,6 +298,48 @@ export function FileExplorerPane({ serverId, agentId }: FileExplorerPaneProps) {
const handleRefresh = useCallback(() => {
void refetchExplorer();
}, [refetchExplorer]);
+ const refreshIconRotation = useSharedValue(0);
+
+ useEffect(() => {
+ if (isRefreshFetching) {
+ refreshIconRotation.value = 0;
+ refreshIconRotation.value = withRepeat(
+ withTiming(360, {
+ duration: 700,
+ easing: Easing.linear,
+ }),
+ -1,
+ false
+ );
+ return;
+ }
+
+ cancelAnimation(refreshIconRotation);
+ const remainder = refreshIconRotation.value % 360;
+ if (Math.abs(remainder) < 0.001) {
+ refreshIconRotation.value = 0;
+ return;
+ }
+
+ const remaining = 360 - remainder;
+ const duration = Math.max(80, Math.round((remaining / 360) * 700));
+ refreshIconRotation.value = withTiming(
+ 360,
+ {
+ duration,
+ easing: Easing.linear,
+ },
+ (finished) => {
+ if (finished) {
+ refreshIconRotation.value = 0;
+ }
+ }
+ );
+ }, [isRefreshFetching, refreshIconRotation]);
+
+ const refreshIconAnimatedStyle = useAnimatedStyle(() => ({
+ transform: [{ rotate: `${refreshIconRotation.value}deg` }],
+ }));
const currentSortLabel = SORT_OPTIONS.find((opt) => opt.value === sortOption)?.label ?? "Name";
@@ -644,14 +690,13 @@ export function FileExplorerPane({ serverId, agentId }: FileExplorerPaneProps) {
styles.iconButton,
(hovered || pressed) && styles.iconButtonHovered,
pressed && styles.iconButtonPressed,
- isRefreshFetching && styles.iconButtonLoading,
]}
accessibilityRole="button"
accessibilityLabel="Refresh files"
>
-
+
-
+
{currentSortLabel}
@@ -682,14 +727,13 @@ export function FileExplorerPane({ serverId, agentId }: FileExplorerPaneProps) {
styles.iconButton,
(hovered || pressed) && styles.iconButtonHovered,
pressed && styles.iconButtonPressed,
- isRefreshFetching && styles.iconButtonLoading,
]}
accessibilityRole="button"
accessibilityLabel="Refresh files"
>
-
+
-
+
{currentSortLabel}
@@ -1246,11 +1290,11 @@ const styles = StyleSheet.create((theme) => ({
opacity: 0.8,
transform: [{ scale: 0.96 }],
},
- iconButtonLoading: {
- opacity: 0.8,
- },
- refreshIconLoading: {
- opacity: 0.55,
+ refreshIcon: {
+ width: 16,
+ height: 16,
+ alignItems: "center",
+ justifyContent: "center",
},
previewContent: {
flex: 1,