Flatten home screen agent list and sort by activity
The agent list on the home screen now displays all agents in a single flat list rather than grouped by host. Each row shows a host badge next to the agent title so users can still identify which host an agent belongs to. Agents are sorted globally by status (running first) then by most recent activity, providing a unified view across all hosts. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -14,7 +14,7 @@ import { useLocalSearchParams } from "expo-router";
|
||||
export default function HomeScreen() {
|
||||
const insets = useSafeAreaInsets();
|
||||
const { theme } = useUnistyles();
|
||||
const { groups: aggregatedAgents, isLoading } = useAggregatedAgents();
|
||||
const { agents: aggregatedAgents, isLoading } = useAggregatedAgents();
|
||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||
const [showImportModal, setShowImportModal] = useState(false);
|
||||
const [createModalMounted, setCreateModalMounted] = useState(false);
|
||||
@@ -39,8 +39,7 @@ export default function HomeScreen() {
|
||||
};
|
||||
});
|
||||
|
||||
const aggregatedCount = aggregatedAgents.reduce((count, group) => count + group.agents.length, 0);
|
||||
const hasAgents = aggregatedCount > 0;
|
||||
const hasAgents = aggregatedAgents.length > 0;
|
||||
|
||||
const handleCreateAgent = useCallback(() => {
|
||||
setCreateModalMounted(true);
|
||||
@@ -112,7 +111,7 @@ export default function HomeScreen() {
|
||||
<ActivityIndicator size="large" color={theme.colors.mutedForeground} />
|
||||
</View>
|
||||
) : hasAgents ? (
|
||||
<AgentList agentGroups={aggregatedAgents} />
|
||||
<AgentList agents={aggregatedAgents} />
|
||||
) : (
|
||||
<EmptyState
|
||||
onCreateAgent={handleCreateAgent}
|
||||
|
||||
@@ -2,29 +2,27 @@ import { View, Text, Pressable, ScrollView, Modal } from "react-native";
|
||||
import { useCallback, useState } from "react";
|
||||
import { router } from "expo-router";
|
||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||
import type { Agent } from "@/contexts/session-context";
|
||||
import { formatTimeAgo } from "@/utils/time";
|
||||
import { getAgentStatusColor, getAgentStatusLabel } from "@/utils/agent-status";
|
||||
import { getAgentProviderDefinition } from "@server/server/agent/provider-manifest";
|
||||
import { type AggregatedAgentGroup } from "@/hooks/use-aggregated-agents";
|
||||
import { type AggregatedAgent } from "@/hooks/use-aggregated-agents";
|
||||
import { useDaemonSession } from "@/hooks/use-daemon-session";
|
||||
|
||||
interface AgentListProps {
|
||||
agentGroups: AggregatedAgentGroup[];
|
||||
agents: AggregatedAgent[];
|
||||
}
|
||||
|
||||
export function AgentList({ agentGroups }: AgentListProps) {
|
||||
export function AgentList({ agents }: AgentListProps) {
|
||||
const { theme } = useUnistyles();
|
||||
const [actionAgent, setActionAgent] = useState<Agent | null>(null);
|
||||
const [actionAgentServerId, setActionAgentServerId] = useState<string | null>(null);
|
||||
const actionSession = useDaemonSession(actionAgentServerId ?? undefined, {
|
||||
const [actionAgent, setActionAgent] = useState<AggregatedAgent | null>(null);
|
||||
const actionSession = useDaemonSession(actionAgent?.serverId, {
|
||||
suppressUnavailableAlert: true,
|
||||
allowUnavailable: true,
|
||||
});
|
||||
|
||||
const deleteAgent = actionSession?.deleteAgent;
|
||||
const isActionSheetVisible = actionAgent !== null;
|
||||
const isActionDaemonUnavailable = Boolean(actionAgentServerId && !actionSession);
|
||||
const isActionDaemonUnavailable = Boolean(actionAgent?.serverId && !actionSession);
|
||||
|
||||
const handleAgentPress = useCallback(
|
||||
(serverId: string, agentId: string) => {
|
||||
@@ -42,14 +40,12 @@ export function AgentList({ agentGroups }: AgentListProps) {
|
||||
[isActionSheetVisible]
|
||||
);
|
||||
|
||||
const handleAgentLongPress = useCallback((serverId: string, agent: Agent) => {
|
||||
setActionAgentServerId(serverId);
|
||||
const handleAgentLongPress = useCallback((agent: AggregatedAgent) => {
|
||||
setActionAgent(agent);
|
||||
}, []);
|
||||
|
||||
const handleCloseActionSheet = useCallback(() => {
|
||||
setActionAgent(null);
|
||||
setActionAgentServerId(null);
|
||||
}, []);
|
||||
|
||||
const handleDeleteAgent = useCallback(() => {
|
||||
@@ -58,77 +54,80 @@ export function AgentList({ agentGroups }: AgentListProps) {
|
||||
}
|
||||
deleteAgent(actionAgent.id);
|
||||
setActionAgent(null);
|
||||
setActionAgentServerId(null);
|
||||
}, [actionAgent, deleteAgent]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ScrollView style={styles.container} showsVerticalScrollIndicator={false}>
|
||||
{agentGroups.map(({ serverId, serverLabel, agents }) => (
|
||||
<View key={serverId} style={styles.section}>
|
||||
<Text style={styles.sectionLabel}>{serverLabel}</Text>
|
||||
{agents.map((agent) => {
|
||||
const statusColor = getAgentStatusColor(agent.status);
|
||||
const statusLabel = getAgentStatusLabel(agent.status);
|
||||
const timeAgo = formatTimeAgo(agent.lastActivityAt);
|
||||
const providerLabel = getAgentProviderDefinition(agent.provider).label;
|
||||
const rowServerId = agent.serverId ?? serverId;
|
||||
{agents.map((agent) => {
|
||||
const statusColor = getAgentStatusColor(agent.status);
|
||||
const statusLabel = getAgentStatusLabel(agent.status);
|
||||
const timeAgo = formatTimeAgo(agent.lastActivityAt);
|
||||
const providerLabel = getAgentProviderDefinition(agent.provider).label;
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
key={`${rowServerId}:${agent.id}`}
|
||||
style={({ pressed }) => [
|
||||
styles.agentItem,
|
||||
pressed && styles.agentItemPressed,
|
||||
]}
|
||||
onPress={() => handleAgentPress(rowServerId, agent.id)}
|
||||
onLongPress={() => handleAgentLongPress(rowServerId, agent)}
|
||||
>
|
||||
<View style={styles.agentContent}>
|
||||
return (
|
||||
<Pressable
|
||||
key={`${agent.serverId}:${agent.id}`}
|
||||
style={({ pressed }) => [
|
||||
styles.agentItem,
|
||||
pressed && styles.agentItemPressed,
|
||||
]}
|
||||
onPress={() => handleAgentPress(agent.serverId, agent.id)}
|
||||
onLongPress={() => handleAgentLongPress(agent)}
|
||||
>
|
||||
<View style={styles.agentContent}>
|
||||
<View style={styles.titleRow}>
|
||||
<Text
|
||||
style={styles.agentTitle}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{agent.title || "New Agent"}
|
||||
</Text>
|
||||
<View style={[styles.hostBadge, { backgroundColor: theme.colors.muted }]}>
|
||||
<Text
|
||||
style={styles.agentTitle}
|
||||
style={[styles.hostText, { color: theme.colors.mutedForeground }]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{agent.title || "New Agent"}
|
||||
{agent.serverLabel}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Text style={styles.agentDirectory} numberOfLines={1}>
|
||||
{agent.cwd}
|
||||
</Text>
|
||||
<Text style={styles.agentDirectory} numberOfLines={1}>
|
||||
{agent.cwd}
|
||||
</Text>
|
||||
|
||||
<View style={styles.statusRow}>
|
||||
<View style={styles.statusGroup}>
|
||||
<View
|
||||
style={[styles.providerBadge, { backgroundColor: theme.colors.muted }]}
|
||||
>
|
||||
<Text
|
||||
style={[styles.providerText, { color: theme.colors.mutedForeground }]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{providerLabel}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.statusRow}>
|
||||
<View style={styles.statusGroup}>
|
||||
<View
|
||||
style={[styles.providerBadge, { backgroundColor: theme.colors.muted }]}
|
||||
>
|
||||
<Text
|
||||
style={[styles.providerText, { color: theme.colors.mutedForeground }]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{providerLabel}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.statusBadge}>
|
||||
<View
|
||||
style={[styles.statusDot, { backgroundColor: statusColor }]}
|
||||
/>
|
||||
<Text style={[styles.statusText, { color: statusColor }]}>
|
||||
{statusLabel}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Text style={styles.timeAgo}>
|
||||
{timeAgo}
|
||||
<View style={styles.statusBadge}>
|
||||
<View
|
||||
style={[styles.statusDot, { backgroundColor: statusColor }]}
|
||||
/>
|
||||
<Text style={[styles.statusText, { color: statusColor }]}>
|
||||
{statusLabel}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</Pressable>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
))}
|
||||
|
||||
<Text style={styles.timeAgo}>
|
||||
{timeAgo}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</Pressable>
|
||||
);
|
||||
})}
|
||||
</ScrollView>
|
||||
|
||||
<Modal
|
||||
@@ -182,17 +181,6 @@ const styles = StyleSheet.create((theme) => ({
|
||||
paddingHorizontal: theme.spacing[4],
|
||||
paddingTop: theme.spacing[4],
|
||||
},
|
||||
section: {
|
||||
marginBottom: theme.spacing[4],
|
||||
},
|
||||
sectionLabel: {
|
||||
fontSize: theme.fontSize.xs,
|
||||
fontWeight: theme.fontWeight.semibold,
|
||||
color: theme.colors.mutedForeground,
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: 0.5,
|
||||
marginBottom: theme.spacing[2],
|
||||
},
|
||||
agentItem: {
|
||||
paddingVertical: theme.spacing[4],
|
||||
paddingHorizontal: theme.spacing[4],
|
||||
@@ -207,11 +195,27 @@ const styles = StyleSheet.create((theme) => ({
|
||||
agentContent: {
|
||||
flex: 1,
|
||||
},
|
||||
titleRow: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
gap: theme.spacing[2],
|
||||
marginBottom: theme.spacing[1],
|
||||
},
|
||||
agentTitle: {
|
||||
flex: 1,
|
||||
fontSize: theme.fontSize.base,
|
||||
fontWeight: theme.fontWeight.semibold,
|
||||
color: theme.colors.foreground,
|
||||
marginBottom: theme.spacing[1],
|
||||
},
|
||||
hostBadge: {
|
||||
borderRadius: theme.borderRadius.full,
|
||||
paddingHorizontal: theme.spacing[2],
|
||||
paddingVertical: 2,
|
||||
},
|
||||
hostText: {
|
||||
fontSize: theme.fontSize.xs,
|
||||
fontWeight: theme.fontWeight.normal,
|
||||
},
|
||||
agentDirectory: {
|
||||
fontSize: theme.fontSize.sm,
|
||||
|
||||
@@ -3,82 +3,50 @@ import type { Agent } from "@/contexts/session-context";
|
||||
import { useDaemonConnections } from "@/contexts/daemon-connections-context";
|
||||
import { useSessionDirectory } from "@/hooks/use-session-directory";
|
||||
|
||||
export interface AggregatedAgentGroup {
|
||||
export interface AggregatedAgent extends Agent {
|
||||
serverId: string;
|
||||
serverLabel: string;
|
||||
agents: Agent[];
|
||||
}
|
||||
|
||||
export interface AggregatedAgentsResult {
|
||||
groups: AggregatedAgentGroup[];
|
||||
agents: AggregatedAgent[];
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
const sortAgents = (agents: Agent[]): Agent[] => {
|
||||
return [...agents].sort((left, right) => {
|
||||
const leftRunning = left.status === "running";
|
||||
const rightRunning = right.status === "running";
|
||||
if (leftRunning && !rightRunning) {
|
||||
return -1;
|
||||
}
|
||||
if (!leftRunning && rightRunning) {
|
||||
return 1;
|
||||
}
|
||||
const leftTime = (left.lastUserMessageAt ?? left.lastActivityAt).getTime();
|
||||
const rightTime = (right.lastUserMessageAt ?? right.lastActivityAt).getTime();
|
||||
return rightTime - leftTime;
|
||||
});
|
||||
};
|
||||
|
||||
export function useAggregatedAgents(): AggregatedAgentsResult {
|
||||
const { connectionStates, isLoading: registryLoading } = useDaemonConnections();
|
||||
const sessionDirectory = useSessionDirectory();
|
||||
|
||||
return useMemo(() => {
|
||||
const groups = new Map<string, { serverLabel: string; agents: Agent[] }>();
|
||||
const daemonOrder = new Map<string, number>();
|
||||
Array.from(connectionStates.keys()).forEach((serverId, index) => {
|
||||
daemonOrder.set(serverId, index);
|
||||
const allAgents: AggregatedAgent[] = [];
|
||||
|
||||
sessionDirectory.forEach((session, serverId) => {
|
||||
if (!session) {
|
||||
return;
|
||||
}
|
||||
const serverLabel = connectionStates.get(serverId)?.daemon.label ?? serverId;
|
||||
for (const agent of session.agents.values()) {
|
||||
allAgents.push({
|
||||
...agent,
|
||||
serverId,
|
||||
serverLabel,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (sessionDirectory.size > 0) {
|
||||
sessionDirectory.forEach((session, serverId) => {
|
||||
if (!session) {
|
||||
return;
|
||||
}
|
||||
const label = connectionStates.get(serverId)?.daemon.label ?? serverId;
|
||||
const sessionAgents = Array.from(session.agents.values());
|
||||
if (sessionAgents.length === 0) {
|
||||
return;
|
||||
}
|
||||
const existing = groups.get(serverId);
|
||||
if (existing) {
|
||||
existing.agents.push(...sessionAgents);
|
||||
} else {
|
||||
groups.set(serverId, { serverLabel: label, agents: sessionAgents });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const aggregatedGroups = Array.from(groups.entries()).map(([serverId, { serverLabel, agents }]) => ({
|
||||
serverId,
|
||||
serverLabel,
|
||||
agents: sortAgents(agents),
|
||||
}));
|
||||
|
||||
aggregatedGroups.sort((left, right) => {
|
||||
const leftIndex = daemonOrder.get(left.serverId);
|
||||
const rightIndex = daemonOrder.get(right.serverId);
|
||||
if (leftIndex !== undefined && rightIndex !== undefined && leftIndex !== rightIndex) {
|
||||
return leftIndex - rightIndex;
|
||||
}
|
||||
if (leftIndex !== undefined) {
|
||||
// Sort by: running agents first, then by most recent activity
|
||||
allAgents.sort((left, right) => {
|
||||
const leftRunning = left.status === "running";
|
||||
const rightRunning = right.status === "running";
|
||||
if (leftRunning && !rightRunning) {
|
||||
return -1;
|
||||
}
|
||||
if (rightIndex !== undefined) {
|
||||
if (!leftRunning && rightRunning) {
|
||||
return 1;
|
||||
}
|
||||
return left.serverLabel.localeCompare(right.serverLabel);
|
||||
const leftTime = (left.lastUserMessageAt ?? left.lastActivityAt).getTime();
|
||||
const rightTime = (right.lastUserMessageAt ?? right.lastActivityAt).getTime();
|
||||
return rightTime - leftTime;
|
||||
});
|
||||
|
||||
// Loading if registry is still loading, or if any host is connecting and hasn't
|
||||
@@ -94,6 +62,6 @@ export function useAggregatedAgents(): AggregatedAgentsResult {
|
||||
}
|
||||
}
|
||||
|
||||
return { groups: aggregatedGroups, isLoading };
|
||||
return { agents: allAgents, isLoading };
|
||||
}, [sessionDirectory, connectionStates, registryLoading]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user