feat: provider model freshness TTL and diagnostic UI (#426)
* feat: add server-side TTL for provider snapshots and model list UI Provider snapshots cached forever, causing newly available models (e.g. OpenCode Go minimax, glm) to never appear in the picker. Server: ProviderSnapshotManager now returns stale data immediately and kicks off a background re-warm when the snapshot is older than 5 minutes. Injectable TTL/clock for testing. App: Provider settings rows show model count and are tappable to open a diagnostic sheet with a read-only model list (label + ID). * feat: add per-provider refresh for models and diagnostic * refactor: await refresh completion and clean up diagnostic sheet state - ProviderSnapshotManager.refresh is now async; session.ts awaits it so the RPC ACK reflects real completion instead of just queuing work. - Preserve models/modes/fetchedAt on entries during targeted refresh so the list no longer flashes empty mid-refresh. - Show "Updated Xs ago" next to the Models count, plus loading and error states for the list body. - Split resetSnapshotToLoading into full vs targeted branches. - Flatten nested ternary in model list rendering into renderModelsBody. - Drop redundant local refreshing state and cargo useMemo wrappers. * fix(app): widen isImeComposingKeyboardEvent type to accept optional fields TextInputKeyPressEventData has isComposing and keyCode as optional, so Pick<KeyboardEvent, ...> was too narrow and broke typecheck on main.
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
import { useEffect } from "react";
|
||||
import { RefreshCw } from "lucide-react-native";
|
||||
import Animated, {
|
||||
cancelAnimation,
|
||||
Easing,
|
||||
useAnimatedStyle,
|
||||
useSharedValue,
|
||||
withRepeat,
|
||||
withTiming,
|
||||
} from "react-native-reanimated";
|
||||
|
||||
interface SpinningRefreshIconProps {
|
||||
spinning: boolean;
|
||||
size: number;
|
||||
color: string;
|
||||
}
|
||||
|
||||
export function SpinningRefreshIcon({ spinning, size, color }: SpinningRefreshIconProps) {
|
||||
const rotation = useSharedValue(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (spinning) {
|
||||
rotation.value = 0;
|
||||
rotation.value = withRepeat(
|
||||
withTiming(360, {
|
||||
duration: 1000,
|
||||
easing: Easing.linear,
|
||||
}),
|
||||
-1,
|
||||
false,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
cancelAnimation(rotation);
|
||||
const remainder = rotation.value % 360;
|
||||
if (Math.abs(remainder) < 0.001) {
|
||||
rotation.value = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
rotation.value = withTiming(360, {
|
||||
duration: Math.max(80, Math.round(((360 - remainder) / 360) * 1000)),
|
||||
easing: Easing.linear,
|
||||
});
|
||||
}, [rotation, spinning]);
|
||||
|
||||
const animatedStyle = useAnimatedStyle(() => ({
|
||||
transform: [{ rotate: `${rotation.value}deg` }],
|
||||
}));
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
style={[
|
||||
{
|
||||
width: size,
|
||||
height: size,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
animatedStyle,
|
||||
]}
|
||||
>
|
||||
<RefreshCw size={size} color={color} />
|
||||
</Animated.View>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user