Replace reconnecting badge with pull-to-refresh

UX improvements:
- Remove misleading "Reconnecting..." badge overlay
- Implement native pull-to-refresh gesture on agent list
- Add manual refresh capability via pull gesture

Technical changes:
- Add refreshSession() method to session context
- Add refreshAll() to useAggregatedAgents hook
- Integrate RefreshControl in AgentList component
- Use isRevalidating state for refresh indicator
- Apply theme colors to RefreshControl for consistency

Benefits:
- Familiar iOS/Android UX pattern
- No UI overlays blocking content
- Clear user action (pull) → clear feedback (refresh)
- Accurate state representation (syncing vs reconnecting)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Mohamed Boudra
2025-12-03 02:57:38 +00:00
co-authored by Claude
parent 348cb9d637
commit e346b4c026
6 changed files with 58 additions and 33 deletions
+8 -29
View File
@@ -1,4 +1,4 @@
import { View, ActivityIndicator, Text } from "react-native";
import { View, ActivityIndicator } from "react-native";
import { useState, useCallback, useMemo, useRef, useEffect } from "react";
import { useFocusEffect } from "@react-navigation/native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
@@ -19,7 +19,8 @@ export default function HomeScreen() {
const {
agents: aggregatedAgents,
isInitialLoad,
isRevalidating
isRevalidating,
refreshAll,
} = useAggregatedAgents();
const [showCreateModal, setShowCreateModal] = useState(false);
const [showImportModal, setShowImportModal] = useState(false);
@@ -123,16 +124,11 @@ export default function HomeScreen() {
<ActivityIndicator size="large" color={theme.colors.mutedForeground} />
</View>
) : hasAgents ? (
<View style={{ flex: 1 }}>
<AgentList agents={aggregatedAgents} />
{isRevalidating && (
<View style={styles.revalidatingBadge}>
<ActivityIndicator size="small" color={theme.colors.mutedForeground} />
<Text style={styles.revalidatingText}>Reconnecting...</Text>
</View>
)}
</View>
<AgentList
agents={aggregatedAgents}
isRefreshing={isRevalidating}
onRefresh={refreshAll}
/>
) : (
<EmptyState
onCreateAgent={handleCreateAgent}
@@ -172,21 +168,4 @@ const styles = StyleSheet.create((theme) => ({
alignItems: "center",
justifyContent: "center",
},
revalidatingBadge: {
position: "absolute",
top: 8,
right: 8,
flexDirection: "row",
alignItems: "center",
gap: 8,
paddingHorizontal: 12,
paddingVertical: 6,
backgroundColor: theme.colors.muted,
borderRadius: 16,
opacity: 0.9,
},
revalidatingText: {
fontSize: 12,
color: theme.colors.mutedForeground,
},
}));
+14 -2
View File
@@ -1,4 +1,4 @@
import { View, Text, Pressable, FlatList, Modal, type ListRenderItem } from "react-native";
import { View, Text, Pressable, FlatList, Modal, RefreshControl, type ListRenderItem } from "react-native";
import { useCallback, useState } from "react";
import { router } from "expo-router";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
@@ -11,9 +11,11 @@ import { buildAgentNavigationKey, startNavigationTiming } from "@/utils/navigati
interface AgentListProps {
agents: AggregatedAgent[];
isRefreshing?: boolean;
onRefresh?: () => void;
}
export function AgentList({ agents }: AgentListProps) {
export function AgentList({ agents, isRefreshing = false, onRefresh }: AgentListProps) {
const { theme } = useUnistyles();
const [actionAgent, setActionAgent] = useState<AggregatedAgent | null>(null);
const actionSession = useDaemonSession(actionAgent?.serverId, {
@@ -150,6 +152,16 @@ export function AgentList({ agents }: AgentListProps) {
renderItem={renderAgentItem}
showsVerticalScrollIndicator={false}
keyboardShouldPersistTaps="handled"
refreshControl={
onRefresh ? (
<RefreshControl
refreshing={isRefreshing}
onRefresh={onRefresh}
tintColor={theme.colors.mutedForeground}
colors={[theme.colors.mutedForeground]}
/>
) : undefined
}
/>
<Modal
@@ -171,6 +171,7 @@ export interface SessionContextValue {
restartServer: (reason?: string) => void;
initializeAgent: (params: { agentId: string; requestId?: string }) => void;
refreshAgent: (params: { agentId: string; requestId?: string }) => void;
refreshSession: () => void;
cancelAgentRun: (agentId: string) => void;
sendAgentMessage: (
agentId: string,
@@ -1589,6 +1590,17 @@ export function SessionProvider({ children, serverUrl, serverId }: SessionProvid
return targetPath;
}, [updateExplorerState, ws]);
const refreshSession = useCallback(() => {
console.log(`[Session] Manual refresh requested for ${serverId}`);
ws.send({
type: "session",
message: {
type: "load_conversation_request",
conversationId: ws.conversationId ?? "",
},
});
}, [ws, serverId]);
// Cleanup on unmount
useEffect(() => {
return () => {
@@ -1610,6 +1622,7 @@ export function SessionProvider({ children, serverUrl, serverId }: SessionProvid
restartServer,
initializeAgent,
refreshAgent,
refreshSession,
cancelAgentRun,
deleteAgent,
sendAgentMessage,
@@ -1632,6 +1645,7 @@ export function SessionProvider({ children, serverUrl, serverId }: SessionProvid
restartServer,
initializeAgent,
refreshAgent,
refreshSession,
cancelAgentRun,
deleteAgent,
sendAgentMessage,
@@ -1656,6 +1670,7 @@ export function SessionProvider({ children, serverUrl, serverId }: SessionProvid
restartServer,
initializeAgent,
refreshAgent,
refreshSession,
cancelAgentRun,
sendAgentMessage,
sendAgentAudio,
@@ -1674,6 +1689,7 @@ export function SessionProvider({ children, serverUrl, serverId }: SessionProvid
restartServer,
initializeAgent,
refreshAgent,
refreshSession,
cancelAgentRun,
sendAgentMessage,
sendAgentAudio,
@@ -1,4 +1,4 @@
import { useMemo } from "react";
import { useMemo, useCallback } from "react";
import { useDaemonConnections } from "@/contexts/daemon-connections-context";
import { useSessionStore } from "@/stores/session-store";
import type { AgentDirectoryEntry } from "@/types/agent-directory";
@@ -13,13 +13,24 @@ export interface AggregatedAgentsResult {
isLoading: boolean;
isInitialLoad: boolean;
isRevalidating: boolean;
refreshAll: () => void;
}
export function useAggregatedAgents(): AggregatedAgentsResult {
const { connectionStates } = useDaemonConnections();
const sessions = useSessionStore((state) => state.sessions);
return useMemo(() => {
const refreshAll = useCallback(() => {
console.log('[useAggregatedAgents] Manual refresh triggered for all sessions');
for (const [serverId, session] of Object.entries(sessions)) {
if (session?.methods?.refreshSession) {
console.log(`[useAggregatedAgents] Refreshing session ${serverId}`);
session.methods.refreshSession();
}
}
}, [sessions]);
const result = useMemo(() => {
const allAgents: AggregatedAgent[] = [];
// Derive agent directory from all sessions
@@ -124,4 +135,9 @@ export function useAggregatedAgents(): AggregatedAgentsResult {
isRevalidating,
};
}, [sessions, connectionStates]);
return {
...result,
refreshAll,
};
}
@@ -30,6 +30,7 @@ export type DaemonSession = SessionState & {
restartServer: (reason?: string) => void;
initializeAgent: (params: { agentId: string; requestId?: string }) => void;
refreshAgent: (params: { agentId: string; requestId?: string }) => void;
refreshSession: () => void;
cancelAgentRun: (agentId: string) => void;
sendAgentMessage: (
agentId: string,
+1
View File
@@ -169,6 +169,7 @@ export interface SessionState {
restartServer: (reason?: string) => void;
initializeAgent: (params: { agentId: string; requestId?: string }) => void;
refreshAgent: (params: { agentId: string; requestId?: string }) => void;
refreshSession: () => void;
cancelAgentRun: (agentId: string) => void;
sendAgentMessage: (
agentId: string,