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:
co-authored by
Claude Opus 4.5
parent
0806fd7965
commit
ecd9ddb4a2
@@ -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],
|
||||
|
||||
Reference in New Issue
Block a user