diff --git a/packages/app/src/app/_layout.tsx b/packages/app/src/app/_layout.tsx index fff114c09..b5be156b2 100644 --- a/packages/app/src/app/_layout.tsx +++ b/packages/app/src/app/_layout.tsx @@ -645,7 +645,7 @@ export default function RootLayout() { options={{ gestureEnabled: false, freezeOnBlur: true }} /> - + diff --git a/packages/app/src/app/h/[serverId]/agents.tsx b/packages/app/src/app/h/[serverId]/sessions.tsx similarity index 69% rename from packages/app/src/app/h/[serverId]/agents.tsx rename to packages/app/src/app/h/[serverId]/sessions.tsx index 7772cfa71..29bc04c20 100644 --- a/packages/app/src/app/h/[serverId]/agents.tsx +++ b/packages/app/src/app/h/[serverId]/sessions.tsx @@ -1,9 +1,9 @@ import { useLocalSearchParams } from "expo-router"; -import { AgentsScreen } from "@/screens/agents-screen"; +import { SessionsScreen } from "@/screens/sessions-screen"; export default function HostAgentsRoute() { const params = useLocalSearchParams<{ serverId?: string }>(); const serverId = typeof params.serverId === "string" ? params.serverId : ""; - return ; + return ; } diff --git a/packages/app/src/components/agent-list.tsx b/packages/app/src/components/agent-list.tsx index f5d1d0738..4fdd503ea 100644 --- a/packages/app/src/components/agent-list.tsx +++ b/packages/app/src/components/agent-list.tsx @@ -15,7 +15,7 @@ import { formatTimeAgo } from '@/utils/time' import { shortenPath } from '@/utils/shorten-path' import { type AggregatedAgent } from '@/hooks/use-aggregated-agents' 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' interface AgentListProps { @@ -29,11 +29,9 @@ interface AgentListProps { showAttentionIndicator?: boolean } -interface AgentListSection { - key: string - title: string - data: AggregatedAgent[] -} +type FlatListItem = + | { type: 'header'; key: string; title: string } + | { type: 'agent'; key: string; agent: AggregatedAgent } function deriveDateSectionLabel(lastActivityAt: Date): string { const now = new Date() @@ -82,9 +80,11 @@ function formatStatusLabel(status: AggregatedAgent['status']): string { function SessionBadge({ label, + icon, tone = 'neutral', }: { label: string + icon?: ReactElement tone?: 'neutral' | 'warning' | 'danger' }) { return ( @@ -95,6 +95,7 @@ function SessionBadge({ tone === 'danger' && styles.badgeDanger, ]} > + {icon} void onLongPress: (agent: AggregatedAgent) => void }) { + const { theme } = useUnistyles() const timeAgo = formatTimeAgo(agent.lastActivityAt) const agentKey = `${agent.serverId}:${agent.id}` const isSelected = selectedAgentId === agentKey @@ -141,9 +143,6 @@ function SessionRow({ onLongPress={() => onLongPress(agent)} testID={`agent-row-${agent.serverId}-${agent.id}`} > - - - {agent.title || 'New session'} - {agent.archivedAt ? : null} + {agent.archivedAt ? ( + } + /> + ) : null} {(agent.pendingPermissionCount ?? 0) > 0 ? ( ) : 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 ( - - - {section.title} - - - - {section.data.map((agent, index) => ( - 0 ? styles.rowDivider : undefined} - > - - - ))} - - - ) -} export function AgentList({ agents, @@ -276,6 +239,7 @@ export function AgentList({ serverId, workspaceId: agent.cwd, target: { kind: 'agent', agentId }, + pin: Boolean(agent.archivedAt), }) router.navigate(route as any) }, @@ -298,7 +262,7 @@ export function AgentList({ setActionAgent(null) }, [actionAgent, actionClient]) - const sections = useMemo((): AgentListSection[] => { + const flatItems = useMemo((): FlatListItem[] => { const order = ['Today', 'Yesterday', 'This week', 'This month', 'Older'] as const const buckets = new Map() for (const agent of agents) { @@ -308,41 +272,53 @@ export function AgentList({ buckets.set(label, existing) } - const result: AgentListSection[] = [] + const result: FlatListItem[] = [] for (const label of order) { const data = buckets.get(label) if (!data || data.length === 0) { 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 }, [agents]) - const renderSection: ListRenderItem = useCallback( - ({ item: section }) => ( - - ), + const renderItem: ListRenderItem = useCallback( + ({ item }) => { + if (item.type === 'header') { + return ( + + {item.title} + + ) + } + return ( + + ) + }, [handleAgentLongPress, handleAgentPress, isMobile, selectedAgentId, showAttentionIndicator] ) - const keyExtractor = useCallback((section: AgentListSection) => section.key, []) + const keyExtractor = useCallback((item: FlatListItem) => item.key, []) return ( <> ({ xs: theme.spacing[3], md: theme.spacing[6], }, - paddingTop: theme.spacing[2], + paddingTop: theme.spacing[4], paddingBottom: theme.spacing[6], gap: theme.spacing[1], }, - sectionBlock: { - marginTop: theme.spacing[2], - }, sectionHeading: { + marginTop: theme.spacing[2], flexDirection: 'row', alignItems: 'center', gap: theme.spacing[3], - paddingHorizontal: theme.spacing[1], + paddingHorizontal: theme.spacing[3], marginBottom: theme.spacing[2], }, sectionTitle: { @@ -436,23 +410,6 @@ const styles = StyleSheet.create((theme) => ({ fontWeight: theme.fontWeight.medium, 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: { flexDirection: 'row', alignItems: 'center', @@ -467,9 +424,6 @@ const styles = StyleSheet.create((theme) => ({ md: 0, }, }, - rowLeading: { - marginRight: theme.spacing[3], - }, rowContent: { flex: 1, minWidth: 0, @@ -502,7 +456,7 @@ const styles = StyleSheet.create((theme) => ({ sessionTitle: { flexShrink: 1, fontSize: theme.fontSize.sm, - fontWeight: '500', + fontWeight: '400', color: theme.colors.foreground, opacity: 0.86, }, @@ -535,6 +489,9 @@ const styles = StyleSheet.create((theme) => ({ textAlign: 'right' as const, }, badge: { + flexDirection: 'row', + alignItems: 'center', + gap: theme.spacing[1], paddingHorizontal: theme.spacing[2], paddingVertical: theme.spacing[1], borderRadius: theme.borderRadius.full, diff --git a/packages/app/src/components/left-sidebar.tsx b/packages/app/src/components/left-sidebar.tsx index fde97c700..20eb2e3e8 100644 --- a/packages/app/src/components/left-sidebar.tsx +++ b/packages/app/src/components/left-sidebar.tsx @@ -37,7 +37,7 @@ import { getHostRuntimeStore, useHosts } from '@/runtime/host-runtime' import { formatConnectionStatus } from '@/utils/daemons' import { HEADER_INNER_HEIGHT, HEADER_INNER_HEIGHT_MOBILE } from '@/constants/layout' import { - buildHostAgentsRoute, + buildHostSessionsRoute, buildHostSettingsRoute, mapPathnameToServer, parseServerIdFromPathname, @@ -221,7 +221,7 @@ export const LeftSidebar = memo(function LeftSidebar({ selectedAgentId: _selecte if (!activeServerId) { return } - router.push(buildHostAgentsRoute(activeServerId) as any) + router.push(buildHostSessionsRoute(activeServerId) as any) }, [activeServerId]) 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 ( + [ + styles.newAgentButton, + hovered && styles.newAgentButtonHovered, + isActive && styles.newAgentButtonActive, + ]} + testID="sidebar-sessions" + accessible + accessibilityRole="button" + accessibilityLabel="Sessions" + onPress={onPress} + > + {({ hovered }) => ( + <> + + + Sessions + + + )} + + ) +} + function MobileSidebar({ theme, activeServerId, @@ -457,28 +492,7 @@ function MobileSidebar({ - - {({ hovered }) => ( - <> - - - Sessions - - - )} - + @@ -623,28 +637,7 @@ function DesktopSidebar({ ) : null} - - {({ hovered }) => ( - <> - - - Sessions - - - )} - + @@ -786,15 +779,14 @@ const styles = StyleSheet.create((theme) => ({ gap: theme.spacing[2], }, newAgentButton: { + flex: 1, flexDirection: 'row', alignItems: 'center', gap: theme.spacing[2], - paddingVertical: theme.spacing[1], - paddingRight: theme.spacing[1], - paddingLeft: theme.spacing[3], - flexShrink: 0, + paddingVertical: theme.spacing[2], + paddingHorizontal: theme.spacing[3], + borderRadius: theme.borderRadius.lg, }, - newAgentButtonHovered: {}, newAgentButtonText: { fontSize: theme.fontSize.base, fontWeight: theme.fontWeight.normal, @@ -803,6 +795,12 @@ const styles = StyleSheet.create((theme) => ({ newAgentButtonTextHovered: { color: theme.colors.foreground, }, + newAgentButtonHovered: { + backgroundColor: theme.colors.surface1, + }, + newAgentButtonActive: { + backgroundColor: theme.colors.surface1, + }, hostTrigger: { flexDirection: 'row', alignItems: 'center', diff --git a/packages/app/src/hooks/use-aggregated-agents.ts b/packages/app/src/hooks/use-aggregated-agents.ts index 176fee769..f9a6af058 100644 --- a/packages/app/src/hooks/use-aggregated-agents.ts +++ b/packages/app/src/hooks/use-aggregated-agents.ts @@ -74,6 +74,7 @@ export function useAggregatedAgents(options?: { attentionReason: agent.attentionReason, attentionTimestamp: agent.attentionTimestamp, archivedAt: agent.archivedAt, + createdAt: agent.createdAt, labels: agent.labels, }; allAgents.push(nextAgent); diff --git a/packages/app/src/hooks/use-all-agents-list.ts b/packages/app/src/hooks/use-all-agents-list.ts index 77cd21fee..1dda484dd 100644 --- a/packages/app/src/hooks/use-all-agents-list.ts +++ b/packages/app/src/hooks/use-all-agents-list.ts @@ -31,6 +31,7 @@ function toAggregatedAgent(params: { attentionReason: source.attentionReason, attentionTimestamp: source.attentionTimestamp ?? null, archivedAt: source.archivedAt ?? null, + createdAt: source.createdAt, labels: source.labels, }; } diff --git a/packages/app/src/screens/agents-screen.tsx b/packages/app/src/screens/sessions-screen.tsx similarity index 62% rename from packages/app/src/screens/agents-screen.tsx rename to packages/app/src/screens/sessions-screen.tsx index 0cb90d731..8610b0b1b 100644 --- a/packages/app/src/screens/agents-screen.tsx +++ b/packages/app/src/screens/sessions-screen.tsx @@ -2,23 +2,21 @@ import { useMemo, useState, useCallback, useEffect } from "react"; import { View } from "react-native"; import { useIsFocused } from "@react-navigation/native"; 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 { 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(); if (!isFocused) { return ; } - return ; + return ; } -function AgentsScreenContent({ serverId }: { serverId: string }) { +function SessionsScreenContent({ serverId }: { serverId: string }) { const { agents, isRevalidating, refreshAll } = useAllAgentsList({ serverId, includeArchived: true, @@ -40,24 +38,14 @@ function AgentsScreenContent({ serverId }: { serverId: string }) { }, [isRevalidating, isManualRefresh]); const sortedAgents = useMemo(() => { - return [...agents].sort((a, b) => { - const aNeedsInput = (a.pendingPermissionCount ?? 0) > 0; - 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; - }); + return [...agents].sort( + (a, b) => b.createdAt.getTime() - a.createdAt.getTime() + ); }, [agents]); return ( - router.replace(buildHostRootRoute(serverId) as any)} - /> + ()( requiresAttention: agent.requiresAttention ?? false, attentionReason: agent.attentionReason ?? null, attentionTimestamp: agent.attentionTimestamp ?? null, + createdAt: agent.createdAt, labels: agent.labels, }); } diff --git a/packages/app/src/types/agent-directory.ts b/packages/app/src/types/agent-directory.ts index 18bd68c56..b7d9be9b3 100644 --- a/packages/app/src/types/agent-directory.ts +++ b/packages/app/src/types/agent-directory.ts @@ -13,6 +13,7 @@ export type AgentDirectoryEntry = Pick< | "attentionReason" | "attentionTimestamp" | "archivedAt" + | "createdAt" | "labels" > & { pendingPermissionCount?: number; diff --git a/packages/app/src/utils/host-routes.ts b/packages/app/src/utils/host-routes.ts index 902f7ac4f..8f7ccb452 100644 --- a/packages/app/src/utils/host-routes.ts +++ b/packages/app/src/utils/host-routes.ts @@ -323,12 +323,12 @@ export function buildHostRootRoute(serverId: string): string { return `/h/${encodeSegment(normalized)}`; } -export function buildHostAgentsRoute(serverId: string): string { +export function buildHostSessionsRoute(serverId: string): string { const base = buildHostRootRoute(serverId); if (base === "/") { return "/"; } - return `${base}/agents`; + return `${base}/sessions`; } export function buildHostOpenProjectRoute(serverId: string): string { @@ -361,8 +361,8 @@ export function mapPathnameToServer( if (suffix.startsWith("settings")) { return `${base}/settings`; } - if (suffix.startsWith("agents")) { - return `${base}/agents`; + if (suffix.startsWith("sessions")) { + return `${base}/sessions`; } if (suffix.startsWith("open-project")) { return `${base}/open-project`;