feat(sidebar): add "View More" button and all agents screen

- Make AgentList a pure display component (remove sorting/slicing logic)
- Add listFooterComponent prop to AgentList for extensibility
- Add SIDEBAR_AGENT_LIMIT (15) in sidebar, show "View More" when exceeded
- Create /agents screen showing full sorted agent list
- Update settings page styling

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Mohamed Boudra
2026-01-06 00:14:27 +07:00
co-authored by Claude Opus 4.5
parent 0806fd7965
commit ecd9ddb4a2
5 changed files with 399 additions and 344 deletions
+12 -15
View File
@@ -1,6 +1,6 @@
import { View, Text, Pressable, Modal, RefreshControl, type ListRenderItem } from "react-native";
import { FlatList } from "react-native-gesture-handler";
import { useCallback, useMemo, useState } from "react";
import { useCallback, useState, type ReactElement } from "react";
import { router, usePathname } from "expo-router";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { formatTimeAgo } from "@/utils/time";
@@ -14,25 +14,21 @@ interface AgentListProps {
onRefresh?: () => void;
selectedAgentId?: string;
onAgentSelect?: () => void;
listFooterComponent?: ReactElement | null;
}
export function AgentList({ agents, isRefreshing = false, onRefresh, selectedAgentId, onAgentSelect }: AgentListProps) {
export function AgentList({
agents,
isRefreshing = false,
onRefresh,
selectedAgentId,
onAgentSelect,
listFooterComponent,
}: AgentListProps) {
const { theme } = useUnistyles();
const pathname = usePathname();
const [actionAgent, setActionAgent] = useState<AggregatedAgent | null>(null);
// Sort agents with requires attention at the top, limit to 15 for fast rendering
const sortedAgents = useMemo(() => {
return [...agents]
.sort((a, b) => {
// Requires attention first
if (a.requiresAttention && !b.requiresAttention) return -1;
if (!a.requiresAttention && b.requiresAttention) return 1;
return 0;
})
.slice(0, 15);
}, [agents]);
// Get the methods for the specific server
const methods = useSessionStore((state) =>
actionAgent?.serverId ? state.sessions[actionAgent.serverId]?.methods : undefined
@@ -148,7 +144,7 @@ export function AgentList({ agents, isRefreshing = false, onRefresh, selectedAge
return (
<>
<FlatList
data={sortedAgents}
data={agents}
style={styles.list}
contentContainerStyle={styles.listContent}
keyExtractor={keyExtractor}
@@ -161,6 +157,7 @@ export function AgentList({ agents, isRefreshing = false, onRefresh, selectedAge
maxToRenderPerBatch={12}
updateCellsBatchingPeriod={16}
removeClippedSubviews={true}
ListFooterComponent={listFooterComponent}
refreshControl={
onRefresh ? (
<RefreshControl
@@ -1,4 +1,4 @@
import { useCallback } from "react";
import { useCallback, useMemo } from "react";
import { View, Pressable, Text, Platform } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import Animated, {
@@ -17,6 +17,7 @@ import { useAggregatedAgents } from "@/hooks/use-aggregated-agents";
import { useSidebarAnimation } from "@/contexts/sidebar-animation-context";
const DESKTOP_SIDEBAR_WIDTH = 320;
const SIDEBAR_AGENT_LIMIT = 15;
interface SlidingSidebarProps {
selectedAgentId?: string;
@@ -39,6 +40,20 @@ export function SlidingSidebar({ selectedAgentId }: SlidingSidebarProps) {
const isMobile =
UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
const sortedAgents = useMemo(() => {
return [...agents].sort((a, b) => {
if (a.requiresAttention && !b.requiresAttention) return -1;
if (!a.requiresAttention && b.requiresAttention) return 1;
return 0;
});
}, [agents]);
const limitedAgents = useMemo(
() => sortedAgents.slice(0, SIDEBAR_AGENT_LIMIT),
[sortedAgents]
);
const hasMore = agents.length > SIDEBAR_AGENT_LIMIT;
const handleClose = useCallback(() => {
close();
}, [close]);
@@ -74,6 +89,15 @@ export function SlidingSidebar({ selectedAgentId }: SlidingSidebarProps) {
close();
}, [close, translateX, backdropOpacity, windowWidth]);
const handleViewMore = useCallback(() => {
if (isMobile) {
translateX.value = -windowWidth;
backdropOpacity.value = 0;
}
close();
router.push("/agents");
}, [backdropOpacity, close, isMobile, translateX, windowWidth]);
// Close gesture (swipe left to close when sidebar is open)
const closeGesture = Gesture.Pan()
.enabled(isOpen)
@@ -121,6 +145,21 @@ export function SlidingSidebar({ selectedAgentId }: SlidingSidebarProps) {
pointerEvents: backdropOpacity.value > 0.01 ? "auto" : "none",
}));
const viewMoreButton = hasMore ? (
<View style={styles.viewMoreContainer}>
<Pressable
style={({ hovered }) => [
styles.newAgentButton,
styles.viewMoreButton,
hovered && styles.newAgentButtonHovered,
]}
onPress={handleViewMore}
>
<Text style={styles.viewMoreButtonText}>View More</Text>
</Pressable>
</View>
) : null;
// Render mobile sidebar
// On web, use "auto" instead of "box-none" because web's pointer-events: none blocks scroll
const overlayPointerEvents = Platform.OS === "web" ? "auto" : "box-none";
@@ -158,11 +197,12 @@ export function SlidingSidebar({ selectedAgentId }: SlidingSidebarProps) {
{/* Middle: scrollable agent list */}
<AgentList
agents={agents}
agents={limitedAgents}
isRefreshing={isRevalidating}
onRefresh={refreshAll}
selectedAgentId={selectedAgentId}
onAgentSelect={handleAgentSelectMobile}
listFooterComponent={viewMoreButton}
/>
{/* Footer */}
@@ -205,10 +245,11 @@ export function SlidingSidebar({ selectedAgentId }: SlidingSidebarProps) {
{/* Middle: scrollable agent list */}
<AgentList
agents={agents}
agents={limitedAgents}
isRefreshing={isRevalidating}
onRefresh={refreshAll}
selectedAgentId={selectedAgentId}
listFooterComponent={viewMoreButton}
/>
{/* Footer: Settings button */}
@@ -277,6 +318,17 @@ const styles = StyleSheet.create((theme) => ({
fontWeight: theme.fontWeight.normal,
color: theme.colors.foreground,
},
viewMoreContainer: {
paddingTop: theme.spacing[2],
},
viewMoreButton: {
paddingVertical: theme.spacing[2],
},
viewMoreButtonText: {
fontSize: theme.fontSize.base,
fontWeight: theme.fontWeight.normal,
color: theme.colors.foreground,
},
sidebarFooter: {
paddingHorizontal: theme.spacing[4],
paddingTop: theme.spacing[2],