Inital commit

This commit is contained in:
2026-07-09 12:07:57 -05:00
commit 6fa32c1020
39 changed files with 17590 additions and 0 deletions
+441
View File
@@ -0,0 +1,441 @@
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 (
<Pressable
style={({ pressed }) => [styles.row, pressed && styles.rowPressed]}
onPress={onPress}
>
<View style={[styles.tile, { backgroundColor: tint.bg }]}>
<Text style={[styles.tileLetter, { color: tint.fg }]}>
{name.charAt(0).toUpperCase()}
</Text>
{active ? <View style={styles.tileActiveDot} /> : null}
</View>
<View style={[styles.rowMain, !isLast && styles.rowMainDivider]}>
<View style={styles.rowText}>
<Text style={styles.rowTitle} numberOfLines={1}>
{name}
</Text>
<Text style={styles.rowPath} numberOfLines={1}>
{path}
</Text>
<Text style={styles.rowMeta} numberOfLines={1}>
{meta}
</Text>
</View>
<Ionicons name="chevron-forward" size={16} color={colors.textMuted} />
</View>
</Pressable>
);
}
function EmptyState() {
return (
<View style={styles.empty}>
<View style={styles.emptyIcon}>
<Ionicons name="folder-open-outline" size={22} color={colors.textMuted} />
</View>
<Text style={styles.emptyTitle}>No projects yet</Text>
<Text style={styles.emptyHint}>
Add a folder from your computer to start chatting with Pi.
</Text>
</View>
);
}
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 (
<View style={styles.disconnected}>
<View style={styles.emptyIcon}>
<Ionicons name="cloud-offline-outline" size={22} color={colors.textMuted} />
</View>
<Text style={styles.disconnectedTitle}>Not connected</Text>
<Text style={styles.disconnectedHint}>
Connect to your computer to see your projects.
</Text>
<Pressable
style={({ pressed }) => [styles.secondaryButton, pressed && styles.pressedDim]}
onPress={() => router.replace("/")}
>
<Text style={styles.secondaryButtonText}>Connect</Text>
</Pressable>
</View>
);
}
return (
<View style={styles.container}>
{error ? (
<View style={styles.errorBanner}>
<Text style={styles.errorBannerText}>{error}</Text>
</View>
) : null}
<ScrollView
contentContainerStyle={styles.scroll}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={() => void handleRefresh()}
tintColor={colors.text}
/>
}
>
<View style={styles.statusRow}>
<View style={styles.statusLeft}>
<View style={styles.connectionDot} />
<Text style={styles.connectionText} numberOfLines={1}>
{host}:{port}
</Text>
</View>
<Pressable
onPress={handleDisconnect}
hitSlop={10}
style={({ pressed }) => pressed && styles.pressedDim}
>
<Text style={styles.disconnectText}>Disconnect</Text>
</Pressable>
</View>
<Pressable
style={({ pressed }) => [styles.addButton, pressed && styles.pressedDim]}
onPress={() => router.push("/picker")}
>
<View style={styles.addIcon}>
<Ionicons name="add" size={20} color={colors.text} />
</View>
<View style={styles.addText}>
<Text style={styles.addTitle}>Add project</Text>
<Text style={styles.addHint}>Choose a folder on your computer</Text>
</View>
<Ionicons name="chevron-forward" size={16} color={colors.text} />
</Pressable>
<View style={styles.section}>
<Text style={styles.sectionTitle}>
Projects{projects.length > 0 ? ` · ${projects.length}` : ""}
</Text>
{projects.length === 0 ? (
<EmptyState />
) : (
<View style={styles.listCard}>
{projects.map((project, index) => (
<ProjectRow
key={project.cwd || "(unknown)"}
name={project.name}
path={shortenPath(project.cwd) || "Unknown location"}
meta={projectMeta(project)}
active={project.activeCount > 0}
isLast={index === projects.length - 1}
onPress={() => openProject(project.cwd)}
/>
))}
</View>
)}
</View>
</ScrollView>
</View>
);
}
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,
},
});