Update files

This commit is contained in:
Mohamed Boudra
2026-01-25 20:40:00 +07:00
parent d1a47c22f9
commit be235e14d7
4 changed files with 138 additions and 78 deletions

View File

@@ -4,9 +4,9 @@ import {
Pressable,
Modal,
RefreshControl,
FlatList,
SectionList,
type ViewToken,
type ListRenderItem,
type SectionListRenderItem,
} from "react-native";
import { useCallback, useMemo, useRef, useState, type ReactElement } from "react";
import { router, usePathname } from "expo-router";
@@ -36,6 +36,40 @@ interface AgentListProps {
listFooterComponent?: ReactElement | null;
}
interface AgentListSection {
key: string;
title: string;
data: AggregatedAgent[];
}
function deriveDateSectionLabel(lastActivityAt: Date): string {
const now = new Date();
const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
const yesterdayStart = new Date(todayStart.getTime() - 24 * 60 * 60 * 1000);
const activityStart = new Date(
lastActivityAt.getFullYear(),
lastActivityAt.getMonth(),
lastActivityAt.getDate()
);
if (activityStart.getTime() >= todayStart.getTime()) {
return "Today";
}
if (activityStart.getTime() >= yesterdayStart.getTime()) {
return "Yesterday";
}
const diffTime = todayStart.getTime() - activityStart.getTime();
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));
if (diffDays <= 7) {
return "This week";
}
if (diffDays <= 30) {
return "This month";
}
return "Older";
}
export function AgentList({
agents,
isRefreshing = false,
@@ -213,9 +247,37 @@ export function AgentList({
]
);
const renderAgentItem = useCallback<ListRenderItem<AggregatedAgent>>(
({ item: agent }) => <AgentListRow agent={agent} />,
[AgentListRow]
const sections = useMemo((): AgentListSection[] => {
const order = ["Today", "Yesterday", "This week", "This month", "Older"] as const;
const buckets = new Map<string, AggregatedAgent[]>();
for (const agent of agents) {
const label = deriveDateSectionLabel(agent.lastActivityAt);
const existing = buckets.get(label) ?? [];
existing.push(agent);
buckets.set(label, existing);
}
const result: AgentListSection[] = [];
for (const label of order) {
const data = buckets.get(label);
if (!data || data.length === 0) {
continue;
}
result.push({ key: `date:${label}`, title: label, data });
}
return result;
}, [agents]);
const renderAgentItem: SectionListRenderItem<AggregatedAgent, AgentListSection> =
useCallback(({ item: agent }) => <AgentListRow agent={agent} />, [AgentListRow]);
const renderSectionHeader = useCallback(
({ section }: { section: AgentListSection }) => (
<View style={styles.sectionHeader}>
<Text style={styles.sectionTitle}>{section.title}</Text>
</View>
),
[]
);
const keyExtractor = useCallback(
@@ -225,12 +287,14 @@ export function AgentList({
return (
<>
<FlatList
data={agents}
<SectionList
sections={sections}
style={styles.list}
contentContainerStyle={styles.listContent}
keyExtractor={keyExtractor}
renderItem={renderAgentItem}
renderSectionHeader={renderSectionHeader}
stickySectionHeadersEnabled={false}
extraData={selectedAgentId}
showsVerticalScrollIndicator={false}
keyboardShouldPersistTaps="handled"
@@ -315,6 +379,16 @@ const styles = StyleSheet.create((theme) => ({
paddingTop: theme.spacing[2],
paddingBottom: theme.spacing[4],
},
sectionHeader: {
paddingVertical: theme.spacing[2],
marginTop: theme.spacing[2],
},
sectionTitle: {
fontSize: theme.fontSize.sm,
fontWeight: "500",
color: theme.colors.foregroundMuted,
textAlign: "left",
},
agentItem: {
paddingVertical: theme.spacing[2],
paddingHorizontal: theme.spacing[3],

View File

@@ -20,13 +20,11 @@ import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { useQueries, useQueryClient, type UseQueryOptions } from "@tanstack/react-query";
import { ChevronDown, ChevronRight } from "lucide-react-native";
import { formatTimeAgo } from "@/utils/time";
import { shortenPath } from "@/utils/shorten-path";
import { deriveBranchLabel, deriveProjectPath } from "@/utils/agent-display-info";
import {
groupAgents,
parseRepoNameFromRemoteUrl,
parseRepoShortNameFromRemoteUrl,
type ProjectGroup,
type DateGroup,
} from "@/utils/agent-grouping";
import { type AggregatedAgent } from "@/hooks/use-aggregated-agents";
import { useSessionStore } from "@/stores/session-store";
@@ -42,13 +40,12 @@ import {
} from "@/utils/navigation-timing";
type SectionType =
| { type: "project"; data: ProjectGroup }
| { type: "date"; data: DateGroup };
| { type: "project"; data: ProjectGroup };
interface SectionData {
key: string;
title: string;
type: "project" | "date";
type: "project";
data: AggregatedAgent[];
/** For project sections, the first agent's serverId (to lookup checkout status) */
firstAgentServerId?: string;
@@ -68,14 +65,12 @@ interface GroupedAgentListProps {
interface SectionHeaderProps {
section: SectionData;
isCollapsed: boolean;
agentCount: number;
onToggle: () => void;
}
function SectionHeader({
section,
isCollapsed,
agentCount,
onToggle,
}: SectionHeaderProps) {
const { theme } = useUnistyles();
@@ -90,7 +85,14 @@ function SectionHeader({
// Derive display title: prefer repo name from remote URL, fallback to path-based name
let displayTitle = section.title;
if (section.type === "project" && checkout?.isGit && checkout.remoteUrl) {
const repoName = parseRepoShortNameFromRemoteUrl(checkout.remoteUrl);
const isGitHubRemote =
checkout.remoteUrl.includes("github.com") ||
checkout.remoteUrl.includes("git@github.com:");
const repoName = isGitHubRemote
? parseRepoNameFromRemoteUrl(checkout.remoteUrl)
: parseRepoShortNameFromRemoteUrl(checkout.remoteUrl);
if (repoName) {
displayTitle = repoName;
}
@@ -105,24 +107,17 @@ function SectionHeader({
onPress={onToggle}
>
<View style={styles.sectionHeaderLeft}>
{isCollapsed ? (
<ChevronRight
size={14}
color={theme.colors.foregroundMuted}
style={styles.chevron}
/>
) : (
<ChevronDown
size={14}
color={theme.colors.foregroundMuted}
style={styles.chevron}
/>
)}
<Text style={styles.sectionTitle} numberOfLines={1}>
{displayTitle}
</Text>
</View>
<Text style={styles.sectionCount}>{agentCount}</Text>
<View style={styles.sectionHeaderRight}>
{isCollapsed ? (
<ChevronRight size={14} color={theme.colors.foregroundMuted} />
) : (
<ChevronDown size={14} color={theme.colors.foregroundMuted} />
)}
</View>
</Pressable>
);
}
@@ -241,7 +236,7 @@ export function GroupedAgentList({
}, [agents, checkoutCacheQueries]);
// Group agents
const { activeGroups, inactiveGroups } = useMemo(
const { activeGroups } = useMemo(
() =>
groupAgents(agents, {
getRemoteUrl: (agent) =>
@@ -268,19 +263,8 @@ export function GroupedAgentList({
});
}
for (const group of inactiveGroups) {
const sectionKey = `date:${group.label}`;
const isCollapsed = collapsedSections.has(sectionKey);
result.push({
key: sectionKey,
title: group.label,
type: "date",
data: isCollapsed ? [] : group.agents,
});
}
return result;
}, [activeGroups, inactiveGroups, collapsedSections]);
}, [activeGroups, collapsedSections]);
const viewabilityConfig = useMemo(
() => ({ itemVisiblePercentThreshold: 30 }),
@@ -338,8 +322,9 @@ export function GroupedAgentList({
agentId: agent.id,
});
const checkout = checkoutQuery.data ?? null;
const projectPath = deriveProjectPath(agent.cwd, checkout);
const branchLabel = deriveBranchLabel(checkout);
const activeBranchLabel = checkout?.isGit
? (checkout.currentBranch ?? checkout.baseRef ?? "git")
: null;
return (
<Pressable
@@ -373,8 +358,7 @@ export function GroupedAgentList({
</View>
<Text style={styles.secondaryRow} numberOfLines={1}>
{shortenPath(projectPath)}
{branchLabel ? ` · ${branchLabel}` : ""} · {timeAgo}
{activeBranchLabel ? `${activeBranchLabel} · ${timeAgo}` : timeAgo}
</Text>
</View>
)}
@@ -391,31 +375,21 @@ export function GroupedAgentList({
);
const renderItem: SectionListRenderItem<AggregatedAgent, SectionData> =
useCallback(
({ item: agent }) => <AgentListRow agent={agent} />,
[AgentListRow]
);
useCallback(({ item: agent }) => <AgentListRow agent={agent} />, [AgentListRow]);
const renderSectionHeader = useCallback(
({ section }: { section: SectionData }) => {
const isCollapsed = collapsedSections.has(section.key);
const agentCount =
section.type === "project"
? activeGroups.find((g) => `project:${g.projectKey}` === section.key)
?.agents.length ?? 0
: inactiveGroups.find((g) => `date:${g.label}` === section.key)
?.agents.length ?? 0;
return (
<SectionHeader
section={section}
isCollapsed={isCollapsed}
agentCount={agentCount}
onToggle={() => toggleSection(section.key)}
/>
);
},
[collapsedSections, activeGroups, inactiveGroups, toggleSection]
[collapsedSections, toggleSection]
);
const keyExtractor = useCallback(
@@ -521,7 +495,8 @@ const styles = StyleSheet.create((theme) => ({
alignItems: "center",
justifyContent: "space-between",
paddingVertical: theme.spacing[2],
paddingHorizontal: theme.spacing[2],
paddingHorizontal: theme.spacing[4] + theme.spacing[2],
marginHorizontal: -theme.spacing[4],
marginTop: theme.spacing[2],
borderRadius: theme.borderRadius.md,
},
@@ -531,22 +506,21 @@ const styles = StyleSheet.create((theme) => ({
sectionHeaderLeft: {
flexDirection: "row",
alignItems: "center",
justifyContent: "flex-start",
flex: 1,
minWidth: 0,
},
chevron: {
marginRight: theme.spacing[1],
sectionHeaderRight: {
alignItems: "center",
justifyContent: "center",
marginLeft: theme.spacing[2],
},
sectionTitle: {
fontSize: theme.fontSize.sm,
fontWeight: theme.fontWeight.medium,
fontWeight: "500",
color: theme.colors.foregroundMuted,
flex: 1,
},
sectionCount: {
fontSize: theme.fontSize.xs,
color: theme.colors.foregroundMuted,
marginLeft: theme.spacing[2],
textAlign: "left",
},
agentItem: {
paddingVertical: theme.spacing[2],

View File

@@ -9,7 +9,7 @@ import Animated, {
} from "react-native-reanimated";
import { Gesture, GestureDetector } from "react-native-gesture-handler";
import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles";
import { Plus, Settings } from "lucide-react-native";
import { ChevronRight, Plus, Settings } from "lucide-react-native";
import { router } from "expo-router";
import { usePanelStore } from "@/stores/panel-store";
import { GroupedAgentList } from "./grouped-agent-list";
@@ -75,7 +75,6 @@ export function SlidingSidebar({ selectedAgentId }: SlidingSidebarProps) {
() => sortedAgents.slice(0, SIDEBAR_AGENT_LIMIT),
[sortedAgents]
);
const hasMore = agents.length > SIDEBAR_AGENT_LIMIT;
const handleClose = useCallback(() => {
closeToAgent();
@@ -171,20 +170,20 @@ export function SlidingSidebar({ selectedAgentId }: SlidingSidebarProps) {
pointerEvents: backdropOpacity.value > 0.01 ? "auto" : "none",
}));
const viewMoreButton = hasMore ? (
const viewMoreButton = (
<View style={styles.viewMoreContainer}>
<Pressable
style={({ hovered }) => [
styles.newAgentButton,
style={({ hovered, pressed }) => [
styles.viewMoreButton,
hovered && styles.newAgentButtonHovered,
(hovered || pressed) && styles.viewMoreButtonHovered,
]}
onPress={handleViewMore}
>
<Text style={styles.viewMoreButtonText}>View More</Text>
<Text style={styles.viewMoreButtonText}>All agents</Text>
<ChevronRight size={16} color={theme.colors.foregroundMuted} />
</Pressable>
</View>
) : null;
);
// Render mobile sidebar
// On web, use "auto" instead of "box-none" because web's pointer-events: none blocks scroll
@@ -354,12 +353,25 @@ const styles = StyleSheet.create((theme) => ({
paddingTop: theme.spacing[2],
},
viewMoreButton: {
flexDirection: "row",
alignItems: "center",
justifyContent: "flex-start",
gap: theme.spacing[1],
paddingVertical: theme.spacing[2],
paddingHorizontal: 0,
borderWidth: 0,
backgroundColor: "transparent",
alignSelf: "flex-start",
transitionProperty: "opacity",
transitionDuration: "150ms",
},
viewMoreButtonHovered: {
opacity: 0.8,
},
viewMoreButtonText: {
fontSize: theme.fontSize.base,
fontWeight: theme.fontWeight.normal,
color: theme.colors.foreground,
color: theme.colors.foregroundMuted,
},
sidebarFooter: {
paddingHorizontal: theme.spacing[4],

View File

@@ -204,7 +204,7 @@ export interface GroupedAgents {
inactiveGroups: DateGroup[];
}
const ACTIVE_GRACE_PERIOD_MS = 5 * 60 * 1000; // 5 minutes
const ACTIVE_GRACE_PERIOD_MS = 15 * 60 * 1000; // 15 minutes
interface GroupAgentsOptions {
/**