mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
Update files
This commit is contained in:
@@ -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],
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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 {
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user