import { Ionicons } from "@expo/vector-icons"; import { router } from "expo-router"; import { useCallback, useMemo, useState } from "react"; import { Pressable, RefreshControl, ScrollView, StyleSheet, Text, View, } from "react-native"; import { useFocusEffect } from "@react-navigation/native"; import { useSocket } from "@/contexts/SocketContext"; import { groupByProject, projectMeta, shortenPath } from "@/lib/session-utils"; import { colors, monoFont, tintFor } from "@/lib/theme"; function ProjectRow({ name, path, meta, active, isLast, onPress, }: { name: string; path: string; meta: string; active: boolean; isLast: boolean; onPress: () => void; }) { const tint = tintFor(name); return ( [styles.row, pressed && styles.rowPressed]} onPress={onPress} > {name.charAt(0).toUpperCase()} {active ? : null} {name} {path} {meta} ); } function EmptyState() { return ( No projects yet Add a folder from your computer to start chatting with Pi. ); } export default function SessionsScreen() { const { host, port, connected, error, persistedSessions, liveRuntimes, savedProjects, refreshSessions, disconnect, } = useSocket(); const [refreshing, setRefreshing] = useState(false); const projects = useMemo( () => groupByProject(savedProjects, liveRuntimes, persistedSessions), [savedProjects, liveRuntimes, persistedSessions], ); const handleRefresh = useCallback(async () => { setRefreshing(true); refreshSessions(); setTimeout(() => setRefreshing(false), 600); }, [refreshSessions]); useFocusEffect( useCallback(() => { if (connected) { refreshSessions(); } }, [connected, refreshSessions]), ); const openProject = (cwd: string) => { router.push({ pathname: "/project", params: { cwd } }); }; const handleDisconnect = () => { disconnect(); router.replace("/"); }; if (!connected) { return ( Not connected Connect to your computer to see your projects. [styles.secondaryButton, pressed && styles.pressedDim]} onPress={() => router.replace("/")} > Connect ); } return ( {error ? ( {error} ) : null} void handleRefresh()} tintColor={colors.text} /> } > {host}:{port} pressed && styles.pressedDim} > Disconnect [styles.addButton, pressed && styles.pressedDim]} onPress={() => router.push("/picker")} > Add project Choose a folder on your computer Projects{projects.length > 0 ? ` ยท ${projects.length}` : ""} {projects.length === 0 ? ( ) : ( {projects.map((project, index) => ( 0} isLast={index === projects.length - 1} onPress={() => openProject(project.cwd)} /> ))} )} ); } const styles = StyleSheet.create({ container: { flex: 1, }, scroll: { padding: 16, gap: 16, paddingBottom: 40, }, pressedDim: { opacity: 0.6, }, statusRow: { flexDirection: "row", alignItems: "center", justifyContent: "space-between", paddingHorizontal: 4, paddingVertical: 2, }, statusLeft: { flexDirection: "row", alignItems: "center", gap: 8, flexShrink: 1, }, connectionDot: { width: 7, height: 7, borderRadius: 4, backgroundColor: colors.success, }, connectionText: { color: colors.textSecondary, fontSize: 12, fontFamily: monoFont, }, disconnectText: { color: colors.textMuted, fontSize: 13, fontWeight: "600", }, addButton: { flexDirection: "row", alignItems: "center", gap: 14, backgroundColor: colors.surface, borderRadius: 16, borderWidth: 1, borderColor: colors.borderSubtle, paddingVertical: 14, paddingHorizontal: 16, }, addIcon: { width: 36, height: 36, borderRadius: 10, backgroundColor: "rgba(255, 255, 255, 0.12)", alignItems: "center", justifyContent: "center", }, addText: { flex: 1, gap: 1, }, addTitle: { color: colors.text, fontSize: 16, fontWeight: "700", }, addHint: { color: colors.text, fontSize: 13, }, section: { gap: 10, marginTop: 4, }, sectionTitle: { color: colors.textMuted, fontSize: 12, fontWeight: "700", textTransform: "uppercase", letterSpacing: 1, fontFamily: monoFont, paddingHorizontal: 4, }, listCard: { backgroundColor: colors.surface, borderRadius: 16, borderWidth: 1, borderColor: colors.borderSubtle, overflow: "hidden", }, row: { flexDirection: "row", alignItems: "center", gap: 12, paddingLeft: 14, }, rowPressed: { backgroundColor: colors.surfaceDeep, }, tile: { width: 42, height: 42, borderRadius: 12, alignItems: "center", justifyContent: "center", }, tileLetter: { fontSize: 17, fontWeight: "700", }, tileActiveDot: { position: "absolute", right: -2, top: -2, width: 10, height: 10, borderRadius: 5, backgroundColor: colors.success, borderWidth: 2, borderColor: colors.surface, }, rowMain: { flex: 1, flexDirection: "row", alignItems: "center", gap: 8, paddingVertical: 13, paddingRight: 14, }, rowMainDivider: { borderBottomWidth: StyleSheet.hairlineWidth, borderBottomColor: colors.border, }, rowText: { flex: 1, gap: 2, }, rowTitle: { color: colors.text, fontSize: 16, fontWeight: "600", }, rowPath: { color: colors.textSecondary, fontSize: 12, fontFamily: monoFont, }, rowMeta: { color: colors.textMuted, fontSize: 12, }, empty: { alignItems: "center", gap: 6, paddingVertical: 36, paddingHorizontal: 24, backgroundColor: colors.surface, borderRadius: 16, borderWidth: 1, borderColor: colors.borderSubtle, }, emptyIcon: { width: 48, height: 48, borderRadius: 24, backgroundColor: colors.surfaceDeep, borderWidth: 1, borderColor: colors.border, alignItems: "center", justifyContent: "center", marginBottom: 6, }, emptyTitle: { color: colors.text, fontSize: 15, fontWeight: "600", }, emptyHint: { color: colors.textMuted, fontSize: 13, lineHeight: 19, textAlign: "center", }, secondaryButton: { borderWidth: 1, borderColor: colors.border, borderRadius: 12, paddingHorizontal: 24, paddingVertical: 12, alignItems: "center", marginTop: 8, }, secondaryButtonText: { color: colors.text, fontWeight: "600", fontSize: 15, }, disconnected: { flex: 1, alignItems: "center", justifyContent: "center", gap: 6, padding: 32, }, disconnectedTitle: { color: colors.text, fontSize: 18, fontWeight: "700", }, disconnectedHint: { color: colors.textMuted, fontSize: 15, textAlign: "center", lineHeight: 22, }, errorBanner: { backgroundColor: "#450a0a", paddingHorizontal: 16, paddingVertical: 10, }, errorBannerText: { color: colors.errorMuted, fontSize: 14, }, });