refactor(app): rename agents route to sessions with improved list layout

This commit is contained in:
Mohamed Boudra
2026-03-21 16:20:17 +07:00
parent 60e833245d
commit c35d649f6d
10 changed files with 119 additions and 172 deletions
+1 -1
View File
@@ -645,7 +645,7 @@ export default function RootLayout() {
options={{ gestureEnabled: false, freezeOnBlur: true }} options={{ gestureEnabled: false, freezeOnBlur: true }}
/> />
<Stack.Screen name="h/[serverId]/index" /> <Stack.Screen name="h/[serverId]/index" />
<Stack.Screen name="h/[serverId]/agents" /> <Stack.Screen name="h/[serverId]/sessions" />
<Stack.Screen name="h/[serverId]/open-project" /> <Stack.Screen name="h/[serverId]/open-project" />
<Stack.Screen name="h/[serverId]/settings" /> <Stack.Screen name="h/[serverId]/settings" />
<Stack.Screen name="pair-scan" /> <Stack.Screen name="pair-scan" />
@@ -1,9 +1,9 @@
import { useLocalSearchParams } from "expo-router"; import { useLocalSearchParams } from "expo-router";
import { AgentsScreen } from "@/screens/agents-screen"; import { SessionsScreen } from "@/screens/sessions-screen";
export default function HostAgentsRoute() { export default function HostAgentsRoute() {
const params = useLocalSearchParams<{ serverId?: string }>(); const params = useLocalSearchParams<{ serverId?: string }>();
const serverId = typeof params.serverId === "string" ? params.serverId : ""; const serverId = typeof params.serverId === "string" ? params.serverId : "";
return <AgentsScreen serverId={serverId} />; return <SessionsScreen serverId={serverId} />;
} }
+51 -94
View File
@@ -15,7 +15,7 @@ import { formatTimeAgo } from '@/utils/time'
import { shortenPath } from '@/utils/shorten-path' import { shortenPath } from '@/utils/shorten-path'
import { type AggregatedAgent } from '@/hooks/use-aggregated-agents' import { type AggregatedAgent } from '@/hooks/use-aggregated-agents'
import { useSessionStore } from '@/stores/session-store' import { useSessionStore } from '@/stores/session-store'
import { AgentStatusDot } from '@/components/agent-status-dot' import { Archive } from 'lucide-react-native'
import { prepareWorkspaceTab } from '@/utils/workspace-navigation' import { prepareWorkspaceTab } from '@/utils/workspace-navigation'
interface AgentListProps { interface AgentListProps {
@@ -29,11 +29,9 @@ interface AgentListProps {
showAttentionIndicator?: boolean showAttentionIndicator?: boolean
} }
interface AgentListSection { type FlatListItem =
key: string | { type: 'header'; key: string; title: string }
title: string | { type: 'agent'; key: string; agent: AggregatedAgent }
data: AggregatedAgent[]
}
function deriveDateSectionLabel(lastActivityAt: Date): string { function deriveDateSectionLabel(lastActivityAt: Date): string {
const now = new Date() const now = new Date()
@@ -82,9 +80,11 @@ function formatStatusLabel(status: AggregatedAgent['status']): string {
function SessionBadge({ function SessionBadge({
label, label,
icon,
tone = 'neutral', tone = 'neutral',
}: { }: {
label: string label: string
icon?: ReactElement
tone?: 'neutral' | 'warning' | 'danger' tone?: 'neutral' | 'warning' | 'danger'
}) { }) {
return ( return (
@@ -95,6 +95,7 @@ function SessionBadge({
tone === 'danger' && styles.badgeDanger, tone === 'danger' && styles.badgeDanger,
]} ]}
> >
{icon}
<Text <Text
style={[ style={[
styles.badgeText, styles.badgeText,
@@ -123,6 +124,7 @@ function SessionRow({
onPress: (agent: AggregatedAgent) => void onPress: (agent: AggregatedAgent) => void
onLongPress: (agent: AggregatedAgent) => void onLongPress: (agent: AggregatedAgent) => void
}) { }) {
const { theme } = useUnistyles()
const timeAgo = formatTimeAgo(agent.lastActivityAt) const timeAgo = formatTimeAgo(agent.lastActivityAt)
const agentKey = `${agent.serverId}:${agent.id}` const agentKey = `${agent.serverId}:${agent.id}`
const isSelected = selectedAgentId === agentKey const isSelected = selectedAgentId === agentKey
@@ -141,9 +143,6 @@ function SessionRow({
onLongPress={() => onLongPress(agent)} onLongPress={() => onLongPress(agent)}
testID={`agent-row-${agent.serverId}-${agent.id}`} testID={`agent-row-${agent.serverId}-${agent.id}`}
> >
<View style={styles.rowLeading}>
<AgentStatusDot status={agent.status} requiresAttention={agent.requiresAttention} />
</View>
<View style={styles.rowContent}> <View style={styles.rowContent}>
<View style={styles.rowTitleRow}> <View style={styles.rowTitleRow}>
<Text <Text
@@ -152,7 +151,12 @@ function SessionRow({
> >
{agent.title || 'New session'} {agent.title || 'New session'}
</Text> </Text>
{agent.archivedAt ? <SessionBadge label="Archived" /> : null} {agent.archivedAt ? (
<SessionBadge
label="Archived"
icon={<Archive size={theme.fontSize.xs} color={theme.colors.foregroundMuted} />}
/>
) : null}
{(agent.pendingPermissionCount ?? 0) > 0 ? ( {(agent.pendingPermissionCount ?? 0) > 0 ? (
<SessionBadge label={`${agent.pendingPermissionCount} pending`} tone="warning" /> <SessionBadge label={`${agent.pendingPermissionCount} pending`} tone="warning" />
) : null} ) : null}
@@ -198,47 +202,6 @@ function SessionRow({
) )
} }
function SessionTableSection({
section,
isMobile,
selectedAgentId,
showAttentionIndicator,
onAgentPress,
onAgentLongPress,
}: {
section: AgentListSection
isMobile: boolean
selectedAgentId?: string
showAttentionIndicator: boolean
onAgentPress: (agent: AggregatedAgent) => void
onAgentLongPress: (agent: AggregatedAgent) => void
}) {
return (
<View style={styles.sectionBlock}>
<View style={styles.sectionHeading}>
<Text style={styles.sectionTitle}>{section.title}</Text>
</View>
<View style={styles.listCard}>
{section.data.map((agent, index) => (
<View
key={`${agent.serverId}:${agent.id}`}
style={index > 0 ? styles.rowDivider : undefined}
>
<SessionRow
agent={agent}
isMobile={isMobile}
selectedAgentId={selectedAgentId}
showAttentionIndicator={showAttentionIndicator}
onPress={onAgentPress}
onLongPress={onAgentLongPress}
/>
</View>
))}
</View>
</View>
)
}
export function AgentList({ export function AgentList({
agents, agents,
@@ -276,6 +239,7 @@ export function AgentList({
serverId, serverId,
workspaceId: agent.cwd, workspaceId: agent.cwd,
target: { kind: 'agent', agentId }, target: { kind: 'agent', agentId },
pin: Boolean(agent.archivedAt),
}) })
router.navigate(route as any) router.navigate(route as any)
}, },
@@ -298,7 +262,7 @@ export function AgentList({
setActionAgent(null) setActionAgent(null)
}, [actionAgent, actionClient]) }, [actionAgent, actionClient])
const sections = useMemo((): AgentListSection[] => { const flatItems = useMemo((): FlatListItem[] => {
const order = ['Today', 'Yesterday', 'This week', 'This month', 'Older'] as const const order = ['Today', 'Yesterday', 'This week', 'This month', 'Older'] as const
const buckets = new Map<string, AggregatedAgent[]>() const buckets = new Map<string, AggregatedAgent[]>()
for (const agent of agents) { for (const agent of agents) {
@@ -308,41 +272,53 @@ export function AgentList({
buckets.set(label, existing) buckets.set(label, existing)
} }
const result: AgentListSection[] = [] const result: FlatListItem[] = []
for (const label of order) { for (const label of order) {
const data = buckets.get(label) const data = buckets.get(label)
if (!data || data.length === 0) { if (!data || data.length === 0) {
continue continue
} }
result.push({ key: `date:${label}`, title: label, data }) result.push({ type: 'header', key: `header:${label}`, title: label })
for (const agent of data) {
result.push({ type: 'agent', key: `${agent.serverId}:${agent.id}`, agent })
}
} }
return result return result
}, [agents]) }, [agents])
const renderSection: ListRenderItem<AgentListSection> = useCallback( const renderItem: ListRenderItem<FlatListItem> = useCallback(
({ item: section }) => ( ({ item }) => {
<SessionTableSection if (item.type === 'header') {
section={section} return (
isMobile={isMobile} <View style={styles.sectionHeading}>
selectedAgentId={selectedAgentId} <Text style={styles.sectionTitle}>{item.title}</Text>
showAttentionIndicator={showAttentionIndicator} </View>
onAgentPress={handleAgentPress} )
onAgentLongPress={handleAgentLongPress} }
/> return (
), <SessionRow
agent={item.agent}
isMobile={isMobile}
selectedAgentId={selectedAgentId}
showAttentionIndicator={showAttentionIndicator}
onPress={handleAgentPress}
onLongPress={handleAgentLongPress}
/>
)
},
[handleAgentLongPress, handleAgentPress, isMobile, selectedAgentId, showAttentionIndicator] [handleAgentLongPress, handleAgentPress, isMobile, selectedAgentId, showAttentionIndicator]
) )
const keyExtractor = useCallback((section: AgentListSection) => section.key, []) const keyExtractor = useCallback((item: FlatListItem) => item.key, [])
return ( return (
<> <>
<FlatList <FlatList
data={sections} data={flatItems}
style={styles.list} style={styles.list}
contentContainerStyle={styles.listContent} contentContainerStyle={styles.listContent}
keyExtractor={keyExtractor} keyExtractor={keyExtractor}
renderItem={renderSection} renderItem={renderItem}
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
keyboardShouldPersistTaps="handled" keyboardShouldPersistTaps="handled"
ListFooterComponent={listFooterComponent} ListFooterComponent={listFooterComponent}
@@ -417,18 +393,16 @@ const styles = StyleSheet.create((theme) => ({
xs: theme.spacing[3], xs: theme.spacing[3],
md: theme.spacing[6], md: theme.spacing[6],
}, },
paddingTop: theme.spacing[2], paddingTop: theme.spacing[4],
paddingBottom: theme.spacing[6], paddingBottom: theme.spacing[6],
gap: theme.spacing[1], gap: theme.spacing[1],
}, },
sectionBlock: {
marginTop: theme.spacing[2],
},
sectionHeading: { sectionHeading: {
marginTop: theme.spacing[2],
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
gap: theme.spacing[3], gap: theme.spacing[3],
paddingHorizontal: theme.spacing[1], paddingHorizontal: theme.spacing[3],
marginBottom: theme.spacing[2], marginBottom: theme.spacing[2],
}, },
sectionTitle: { sectionTitle: {
@@ -436,23 +410,6 @@ const styles = StyleSheet.create((theme) => ({
fontWeight: theme.fontWeight.medium, fontWeight: theme.fontWeight.medium,
color: theme.colors.foregroundMuted, color: theme.colors.foregroundMuted,
}, },
listCard: {
overflow: {
xs: 'hidden' as const,
md: 'visible' as const,
},
borderRadius: {
xs: theme.borderRadius.lg,
md: 0,
},
},
rowDivider: {
borderTopWidth: {
xs: StyleSheet.hairlineWidth,
md: 0,
},
borderTopColor: theme.colors.border,
},
row: { row: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
@@ -467,9 +424,6 @@ const styles = StyleSheet.create((theme) => ({
md: 0, md: 0,
}, },
}, },
rowLeading: {
marginRight: theme.spacing[3],
},
rowContent: { rowContent: {
flex: 1, flex: 1,
minWidth: 0, minWidth: 0,
@@ -502,7 +456,7 @@ const styles = StyleSheet.create((theme) => ({
sessionTitle: { sessionTitle: {
flexShrink: 1, flexShrink: 1,
fontSize: theme.fontSize.sm, fontSize: theme.fontSize.sm,
fontWeight: '500', fontWeight: '400',
color: theme.colors.foreground, color: theme.colors.foreground,
opacity: 0.86, opacity: 0.86,
}, },
@@ -535,6 +489,9 @@ const styles = StyleSheet.create((theme) => ({
textAlign: 'right' as const, textAlign: 'right' as const,
}, },
badge: { badge: {
flexDirection: 'row',
alignItems: 'center',
gap: theme.spacing[1],
paddingHorizontal: theme.spacing[2], paddingHorizontal: theme.spacing[2],
paddingVertical: theme.spacing[1], paddingVertical: theme.spacing[1],
borderRadius: theme.borderRadius.full, borderRadius: theme.borderRadius.full,
+49 -51
View File
@@ -37,7 +37,7 @@ import { getHostRuntimeStore, useHosts } from '@/runtime/host-runtime'
import { formatConnectionStatus } from '@/utils/daemons' import { formatConnectionStatus } from '@/utils/daemons'
import { HEADER_INNER_HEIGHT, HEADER_INNER_HEIGHT_MOBILE } from '@/constants/layout' import { HEADER_INNER_HEIGHT, HEADER_INNER_HEIGHT_MOBILE } from '@/constants/layout'
import { import {
buildHostAgentsRoute, buildHostSessionsRoute,
buildHostSettingsRoute, buildHostSettingsRoute,
mapPathnameToServer, mapPathnameToServer,
parseServerIdFromPathname, parseServerIdFromPathname,
@@ -221,7 +221,7 @@ export const LeftSidebar = memo(function LeftSidebar({ selectedAgentId: _selecte
if (!activeServerId) { if (!activeServerId) {
return return
} }
router.push(buildHostAgentsRoute(activeServerId) as any) router.push(buildHostSessionsRoute(activeServerId) as any)
}, [activeServerId]) }, [activeServerId])
const handleHostSelect = useCallback( const handleHostSelect = useCallback(
@@ -283,6 +283,41 @@ export const LeftSidebar = memo(function LeftSidebar({ selectedAgentId: _selecte
) )
}) })
function SessionsButton({ onPress }: { onPress: () => void }) {
const { theme } = useUnistyles()
const pathname = usePathname()
const isActive = pathname.includes('/sessions')
return (
<Pressable
style={({ hovered }) => [
styles.newAgentButton,
hovered && styles.newAgentButtonHovered,
isActive && styles.newAgentButtonActive,
]}
testID="sidebar-sessions"
accessible
accessibilityRole="button"
accessibilityLabel="Sessions"
onPress={onPress}
>
{({ hovered }) => (
<>
<MessagesSquare
size={theme.iconSize.md}
color={hovered || isActive ? theme.colors.foreground : theme.colors.foregroundMuted}
/>
<Text
style={[styles.newAgentButtonText, (hovered || isActive) && styles.newAgentButtonTextHovered]}
>
Sessions
</Text>
</>
)}
</Pressable>
)
}
function MobileSidebar({ function MobileSidebar({
theme, theme,
activeServerId, activeServerId,
@@ -457,28 +492,7 @@ function MobileSidebar({
<View style={styles.sidebarContent} pointerEvents="auto"> <View style={styles.sidebarContent} pointerEvents="auto">
<View style={styles.sidebarHeader}> <View style={styles.sidebarHeader}>
<View style={styles.sidebarHeaderRow}> <View style={styles.sidebarHeaderRow}>
<Pressable <SessionsButton onPress={handleViewMore} />
style={styles.newAgentButton}
testID="sidebar-sessions"
accessible
accessibilityRole="button"
accessibilityLabel="Sessions"
onPress={handleViewMore}
>
{({ hovered }) => (
<>
<MessagesSquare
size={theme.iconSize.md}
color={hovered ? theme.colors.foreground : theme.colors.foregroundMuted}
/>
<Text
style={[styles.newAgentButtonText, hovered && styles.newAgentButtonTextHovered]}
>
Sessions
</Text>
</>
)}
</Pressable>
</View> </View>
</View> </View>
@@ -623,28 +637,7 @@ function DesktopSidebar({
) : null} ) : null}
<View style={styles.sidebarHeader} {...dragHandlers}> <View style={styles.sidebarHeader} {...dragHandlers}>
<View style={styles.sidebarHeaderRow}> <View style={styles.sidebarHeaderRow}>
<Pressable <SessionsButton onPress={handleViewMore} />
style={styles.newAgentButton}
testID="sidebar-sessions"
accessible
accessibilityRole="button"
accessibilityLabel="Sessions"
onPress={handleViewMore}
>
{({ hovered }) => (
<>
<MessagesSquare
size={theme.iconSize.md}
color={hovered ? theme.colors.foreground : theme.colors.foregroundMuted}
/>
<Text
style={[styles.newAgentButtonText, hovered && styles.newAgentButtonTextHovered]}
>
Sessions
</Text>
</>
)}
</Pressable>
</View> </View>
</View> </View>
@@ -786,15 +779,14 @@ const styles = StyleSheet.create((theme) => ({
gap: theme.spacing[2], gap: theme.spacing[2],
}, },
newAgentButton: { newAgentButton: {
flex: 1,
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
gap: theme.spacing[2], gap: theme.spacing[2],
paddingVertical: theme.spacing[1], paddingVertical: theme.spacing[2],
paddingRight: theme.spacing[1], paddingHorizontal: theme.spacing[3],
paddingLeft: theme.spacing[3], borderRadius: theme.borderRadius.lg,
flexShrink: 0,
}, },
newAgentButtonHovered: {},
newAgentButtonText: { newAgentButtonText: {
fontSize: theme.fontSize.base, fontSize: theme.fontSize.base,
fontWeight: theme.fontWeight.normal, fontWeight: theme.fontWeight.normal,
@@ -803,6 +795,12 @@ const styles = StyleSheet.create((theme) => ({
newAgentButtonTextHovered: { newAgentButtonTextHovered: {
color: theme.colors.foreground, color: theme.colors.foreground,
}, },
newAgentButtonHovered: {
backgroundColor: theme.colors.surface1,
},
newAgentButtonActive: {
backgroundColor: theme.colors.surface1,
},
hostTrigger: { hostTrigger: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
@@ -74,6 +74,7 @@ export function useAggregatedAgents(options?: {
attentionReason: agent.attentionReason, attentionReason: agent.attentionReason,
attentionTimestamp: agent.attentionTimestamp, attentionTimestamp: agent.attentionTimestamp,
archivedAt: agent.archivedAt, archivedAt: agent.archivedAt,
createdAt: agent.createdAt,
labels: agent.labels, labels: agent.labels,
}; };
allAgents.push(nextAgent); allAgents.push(nextAgent);
@@ -31,6 +31,7 @@ function toAggregatedAgent(params: {
attentionReason: source.attentionReason, attentionReason: source.attentionReason,
attentionTimestamp: source.attentionTimestamp ?? null, attentionTimestamp: source.attentionTimestamp ?? null,
archivedAt: source.archivedAt ?? null, archivedAt: source.archivedAt ?? null,
createdAt: source.createdAt,
labels: source.labels, labels: source.labels,
}; };
} }
@@ -2,23 +2,21 @@ import { useMemo, useState, useCallback, useEffect } from "react";
import { View } from "react-native"; import { View } from "react-native";
import { useIsFocused } from "@react-navigation/native"; import { useIsFocused } from "@react-navigation/native";
import { StyleSheet } from "react-native-unistyles"; import { StyleSheet } from "react-native-unistyles";
import { BackHeader } from "@/components/headers/back-header"; import { MenuHeader } from "@/components/headers/menu-header";
import { AgentList } from "@/components/agent-list"; import { AgentList } from "@/components/agent-list";
import { useAllAgentsList } from "@/hooks/use-all-agents-list"; import { useAllAgentsList } from "@/hooks/use-all-agents-list";
import { router } from "expo-router";
import { buildHostRootRoute } from "@/utils/host-routes";
export function AgentsScreen({ serverId }: { serverId: string }) { export function SessionsScreen({ serverId }: { serverId: string }) {
const isFocused = useIsFocused(); const isFocused = useIsFocused();
if (!isFocused) { if (!isFocused) {
return <View style={styles.container} />; return <View style={styles.container} />;
} }
return <AgentsScreenContent serverId={serverId} />; return <SessionsScreenContent serverId={serverId} />;
} }
function AgentsScreenContent({ serverId }: { serverId: string }) { function SessionsScreenContent({ serverId }: { serverId: string }) {
const { agents, isRevalidating, refreshAll } = useAllAgentsList({ const { agents, isRevalidating, refreshAll } = useAllAgentsList({
serverId, serverId,
includeArchived: true, includeArchived: true,
@@ -40,24 +38,14 @@ function AgentsScreenContent({ serverId }: { serverId: string }) {
}, [isRevalidating, isManualRefresh]); }, [isRevalidating, isManualRefresh]);
const sortedAgents = useMemo(() => { const sortedAgents = useMemo(() => {
return [...agents].sort((a, b) => { return [...agents].sort(
const aNeedsInput = (a.pendingPermissionCount ?? 0) > 0; (a, b) => b.createdAt.getTime() - a.createdAt.getTime()
const bNeedsInput = (b.pendingPermissionCount ?? 0) > 0; );
if (aNeedsInput && !bNeedsInput) return -1;
if (!aNeedsInput && bNeedsInput) return 1;
if (a.requiresAttention && !b.requiresAttention) return -1;
if (!a.requiresAttention && b.requiresAttention) return 1;
return 0;
});
}, [agents]); }, [agents]);
return ( return (
<View style={styles.container}> <View style={styles.container}>
<BackHeader <MenuHeader title="Sessions" />
title="Sessions"
onBack={() => router.replace(buildHostRootRoute(serverId) as any)}
/>
<AgentList <AgentList
agents={sortedAgents} agents={sortedAgents}
showCheckoutInfo={false} showCheckoutInfo={false}
+1
View File
@@ -1110,6 +1110,7 @@ export const useSessionStore = create<SessionStore>()(
requiresAttention: agent.requiresAttention ?? false, requiresAttention: agent.requiresAttention ?? false,
attentionReason: agent.attentionReason ?? null, attentionReason: agent.attentionReason ?? null,
attentionTimestamp: agent.attentionTimestamp ?? null, attentionTimestamp: agent.attentionTimestamp ?? null,
createdAt: agent.createdAt,
labels: agent.labels, labels: agent.labels,
}); });
} }
@@ -13,6 +13,7 @@ export type AgentDirectoryEntry = Pick<
| "attentionReason" | "attentionReason"
| "attentionTimestamp" | "attentionTimestamp"
| "archivedAt" | "archivedAt"
| "createdAt"
| "labels" | "labels"
> & { > & {
pendingPermissionCount?: number; pendingPermissionCount?: number;
+4 -4
View File
@@ -323,12 +323,12 @@ export function buildHostRootRoute(serverId: string): string {
return `/h/${encodeSegment(normalized)}`; return `/h/${encodeSegment(normalized)}`;
} }
export function buildHostAgentsRoute(serverId: string): string { export function buildHostSessionsRoute(serverId: string): string {
const base = buildHostRootRoute(serverId); const base = buildHostRootRoute(serverId);
if (base === "/") { if (base === "/") {
return "/"; return "/";
} }
return `${base}/agents`; return `${base}/sessions`;
} }
export function buildHostOpenProjectRoute(serverId: string): string { export function buildHostOpenProjectRoute(serverId: string): string {
@@ -361,8 +361,8 @@ export function mapPathnameToServer(
if (suffix.startsWith("settings")) { if (suffix.startsWith("settings")) {
return `${base}/settings`; return `${base}/settings`;
} }
if (suffix.startsWith("agents")) { if (suffix.startsWith("sessions")) {
return `${base}/agents`; return `${base}/sessions`;
} }
if (suffix.startsWith("open-project")) { if (suffix.startsWith("open-project")) {
return `${base}/open-project`; return `${base}/open-project`;