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,
},
});