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:
@@ -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 { useState, useCallback, useMemo, useRef, useEffect } from "react";
|
||||||
import { useFocusEffect } from "@react-navigation/native";
|
import { useFocusEffect } from "@react-navigation/native";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
@@ -19,7 +19,8 @@ export default function HomeScreen() {
|
|||||||
const {
|
const {
|
||||||
agents: aggregatedAgents,
|
agents: aggregatedAgents,
|
||||||
isInitialLoad,
|
isInitialLoad,
|
||||||
isRevalidating
|
isRevalidating,
|
||||||
|
refreshAll,
|
||||||
} = useAggregatedAgents();
|
} = useAggregatedAgents();
|
||||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||||
const [showImportModal, setShowImportModal] = useState(false);
|
const [showImportModal, setShowImportModal] = useState(false);
|
||||||
@@ -123,16 +124,11 @@ export default function HomeScreen() {
|
|||||||
<ActivityIndicator size="large" color={theme.colors.mutedForeground} />
|
<ActivityIndicator size="large" color={theme.colors.mutedForeground} />
|
||||||
</View>
|
</View>
|
||||||
) : hasAgents ? (
|
) : hasAgents ? (
|
||||||
<View style={{ flex: 1 }}>
|
<AgentList
|
||||||
<AgentList agents={aggregatedAgents} />
|
agents={aggregatedAgents}
|
||||||
|
isRefreshing={isRevalidating}
|
||||||
{isRevalidating && (
|
onRefresh={refreshAll}
|
||||||
<View style={styles.revalidatingBadge}>
|
/>
|
||||||
<ActivityIndicator size="small" color={theme.colors.mutedForeground} />
|
|
||||||
<Text style={styles.revalidatingText}>Reconnecting...</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
) : (
|
) : (
|
||||||
<EmptyState
|
<EmptyState
|
||||||
onCreateAgent={handleCreateAgent}
|
onCreateAgent={handleCreateAgent}
|
||||||
@@ -172,21 +168,4 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "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,
|
|
||||||
},
|
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -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 { useCallback, useState } from "react";
|
||||||
import { router } from "expo-router";
|
import { router } from "expo-router";
|
||||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||||
@@ -11,9 +11,11 @@ import { buildAgentNavigationKey, startNavigationTiming } from "@/utils/navigati
|
|||||||
|
|
||||||
interface AgentListProps {
|
interface AgentListProps {
|
||||||
agents: AggregatedAgent[];
|
agents: AggregatedAgent[];
|
||||||
|
isRefreshing?: boolean;
|
||||||
|
onRefresh?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AgentList({ agents }: AgentListProps) {
|
export function AgentList({ agents, isRefreshing = false, onRefresh }: AgentListProps) {
|
||||||
const { theme } = useUnistyles();
|
const { theme } = useUnistyles();
|
||||||
const [actionAgent, setActionAgent] = useState<AggregatedAgent | null>(null);
|
const [actionAgent, setActionAgent] = useState<AggregatedAgent | null>(null);
|
||||||
const actionSession = useDaemonSession(actionAgent?.serverId, {
|
const actionSession = useDaemonSession(actionAgent?.serverId, {
|
||||||
@@ -150,6 +152,16 @@ export function AgentList({ agents }: AgentListProps) {
|
|||||||
renderItem={renderAgentItem}
|
renderItem={renderAgentItem}
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
keyboardShouldPersistTaps="handled"
|
keyboardShouldPersistTaps="handled"
|
||||||
|
refreshControl={
|
||||||
|
onRefresh ? (
|
||||||
|
<RefreshControl
|
||||||
|
refreshing={isRefreshing}
|
||||||
|
onRefresh={onRefresh}
|
||||||
|
tintColor={theme.colors.mutedForeground}
|
||||||
|
colors={[theme.colors.mutedForeground]}
|
||||||
|
/>
|
||||||
|
) : undefined
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
|
|||||||
@@ -171,6 +171,7 @@ export interface SessionContextValue {
|
|||||||
restartServer: (reason?: string) => void;
|
restartServer: (reason?: string) => void;
|
||||||
initializeAgent: (params: { agentId: string; requestId?: string }) => void;
|
initializeAgent: (params: { agentId: string; requestId?: string }) => void;
|
||||||
refreshAgent: (params: { agentId: string; requestId?: string }) => void;
|
refreshAgent: (params: { agentId: string; requestId?: string }) => void;
|
||||||
|
refreshSession: () => void;
|
||||||
cancelAgentRun: (agentId: string) => void;
|
cancelAgentRun: (agentId: string) => void;
|
||||||
sendAgentMessage: (
|
sendAgentMessage: (
|
||||||
agentId: string,
|
agentId: string,
|
||||||
@@ -1589,6 +1590,17 @@ export function SessionProvider({ children, serverUrl, serverId }: SessionProvid
|
|||||||
return targetPath;
|
return targetPath;
|
||||||
}, [updateExplorerState, ws]);
|
}, [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
|
// Cleanup on unmount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
@@ -1610,6 +1622,7 @@ export function SessionProvider({ children, serverUrl, serverId }: SessionProvid
|
|||||||
restartServer,
|
restartServer,
|
||||||
initializeAgent,
|
initializeAgent,
|
||||||
refreshAgent,
|
refreshAgent,
|
||||||
|
refreshSession,
|
||||||
cancelAgentRun,
|
cancelAgentRun,
|
||||||
deleteAgent,
|
deleteAgent,
|
||||||
sendAgentMessage,
|
sendAgentMessage,
|
||||||
@@ -1632,6 +1645,7 @@ export function SessionProvider({ children, serverUrl, serverId }: SessionProvid
|
|||||||
restartServer,
|
restartServer,
|
||||||
initializeAgent,
|
initializeAgent,
|
||||||
refreshAgent,
|
refreshAgent,
|
||||||
|
refreshSession,
|
||||||
cancelAgentRun,
|
cancelAgentRun,
|
||||||
deleteAgent,
|
deleteAgent,
|
||||||
sendAgentMessage,
|
sendAgentMessage,
|
||||||
@@ -1656,6 +1670,7 @@ export function SessionProvider({ children, serverUrl, serverId }: SessionProvid
|
|||||||
restartServer,
|
restartServer,
|
||||||
initializeAgent,
|
initializeAgent,
|
||||||
refreshAgent,
|
refreshAgent,
|
||||||
|
refreshSession,
|
||||||
cancelAgentRun,
|
cancelAgentRun,
|
||||||
sendAgentMessage,
|
sendAgentMessage,
|
||||||
sendAgentAudio,
|
sendAgentAudio,
|
||||||
@@ -1674,6 +1689,7 @@ export function SessionProvider({ children, serverUrl, serverId }: SessionProvid
|
|||||||
restartServer,
|
restartServer,
|
||||||
initializeAgent,
|
initializeAgent,
|
||||||
refreshAgent,
|
refreshAgent,
|
||||||
|
refreshSession,
|
||||||
cancelAgentRun,
|
cancelAgentRun,
|
||||||
sendAgentMessage,
|
sendAgentMessage,
|
||||||
sendAgentAudio,
|
sendAgentAudio,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useMemo } from "react";
|
import { useMemo, useCallback } from "react";
|
||||||
import { useDaemonConnections } from "@/contexts/daemon-connections-context";
|
import { useDaemonConnections } from "@/contexts/daemon-connections-context";
|
||||||
import { useSessionStore } from "@/stores/session-store";
|
import { useSessionStore } from "@/stores/session-store";
|
||||||
import type { AgentDirectoryEntry } from "@/types/agent-directory";
|
import type { AgentDirectoryEntry } from "@/types/agent-directory";
|
||||||
@@ -13,13 +13,24 @@ export interface AggregatedAgentsResult {
|
|||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
isInitialLoad: boolean;
|
isInitialLoad: boolean;
|
||||||
isRevalidating: boolean;
|
isRevalidating: boolean;
|
||||||
|
refreshAll: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useAggregatedAgents(): AggregatedAgentsResult {
|
export function useAggregatedAgents(): AggregatedAgentsResult {
|
||||||
const { connectionStates } = useDaemonConnections();
|
const { connectionStates } = useDaemonConnections();
|
||||||
const sessions = useSessionStore((state) => state.sessions);
|
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[] = [];
|
const allAgents: AggregatedAgent[] = [];
|
||||||
|
|
||||||
// Derive agent directory from all sessions
|
// Derive agent directory from all sessions
|
||||||
@@ -124,4 +135,9 @@ export function useAggregatedAgents(): AggregatedAgentsResult {
|
|||||||
isRevalidating,
|
isRevalidating,
|
||||||
};
|
};
|
||||||
}, [sessions, connectionStates]);
|
}, [sessions, connectionStates]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...result,
|
||||||
|
refreshAll,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export type DaemonSession = SessionState & {
|
|||||||
restartServer: (reason?: string) => void;
|
restartServer: (reason?: string) => void;
|
||||||
initializeAgent: (params: { agentId: string; requestId?: string }) => void;
|
initializeAgent: (params: { agentId: string; requestId?: string }) => void;
|
||||||
refreshAgent: (params: { agentId: string; requestId?: string }) => void;
|
refreshAgent: (params: { agentId: string; requestId?: string }) => void;
|
||||||
|
refreshSession: () => void;
|
||||||
cancelAgentRun: (agentId: string) => void;
|
cancelAgentRun: (agentId: string) => void;
|
||||||
sendAgentMessage: (
|
sendAgentMessage: (
|
||||||
agentId: string,
|
agentId: string,
|
||||||
|
|||||||
@@ -169,6 +169,7 @@ export interface SessionState {
|
|||||||
restartServer: (reason?: string) => void;
|
restartServer: (reason?: string) => void;
|
||||||
initializeAgent: (params: { agentId: string; requestId?: string }) => void;
|
initializeAgent: (params: { agentId: string; requestId?: string }) => void;
|
||||||
refreshAgent: (params: { agentId: string; requestId?: string }) => void;
|
refreshAgent: (params: { agentId: string; requestId?: string }) => void;
|
||||||
|
refreshSession: () => void;
|
||||||
cancelAgentRun: (agentId: string) => void;
|
cancelAgentRun: (agentId: string) => void;
|
||||||
sendAgentMessage: (
|
sendAgentMessage: (
|
||||||
agentId: string,
|
agentId: string,
|
||||||
|
|||||||
Reference in New Issue
Block a user