import { useCallback, useState, useRef } from "react" import { View, Text, FlatList, TouchableOpacity, StyleSheet, useColorScheme, RefreshControl, ActivityIndicator, Modal, TextInput, Alert, KeyboardAvoidingView, Platform, } from "react-native" import { router, useFocusEffect } from "expo-router" import { Ionicons } from "@expo/vector-icons" import { useSessions } from "../../src/stores/sessions" import { useConnections } from "../../src/stores/connections" import { useCatalog } from "../../src/stores/catalog" import type BottomSheet from "@gorhom/bottom-sheet" import type { Session } from "../../src/lib/sdk" import { DirectorySwitcher } from "../../src/components/chat" function formatTime(timestamp: number): string { const date = new Date(timestamp) const now = new Date() const diff = now.getTime() - date.getTime() if (diff < 60000) return "Just now" if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago` if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago` if (diff < 604800000) return `${Math.floor(diff / 86400000)}d ago` return date.toLocaleDateString() } function SessionItem({ session, isDark, onRename, onDelete, }: { session: Session isDark: boolean onRename: () => void onDelete: () => void }) { const onPress = () => { router.push(`/session/${session.id}`) } const onLongPress = () => { Alert.alert(session.title || "Untitled Session", undefined, [ { text: "Cancel", style: "cancel" }, { text: "Rename", onPress: onRename }, { text: "Delete", style: "destructive", onPress: onDelete }, ]) } // Extract short directory name from session const shortDir = session.directory ? session.directory.split("/").filter(Boolean).pop() : null return ( {session.title || "Untitled Session"} {formatTime(session.time.updated)} {session.summary && ` · ${session.summary.files} files`} {shortDir && ( {shortDir} )} ) } // Get short directory name (last folder or project name) function getShortPath( project: { path?: { cwd?: string; root?: string; absolute?: string }; name?: string } | null | undefined, ): string { if (!project) return "" if (project.name) return project.name if (!project.path?.absolute) return "" const parts = project.path.absolute.split("/").filter(Boolean) return parts[parts.length - 1] || project.path.absolute } export default function SessionsScreen() { const colorScheme = useColorScheme() const isDark = colorScheme === "dark" const [showNewSession, setShowNewSession] = useState(false) const [customDir, setCustomDir] = useState("") const [isCreating, setIsCreating] = useState(false) const [renaming, setRenaming] = useState(null) const [renameText, setRenameText] = useState("") const renamingInFlight = useRef(false) const { sessions, isLoading, error, loadSessions, createSession, deleteSession } = useSessions() const { activeConnection, client, currentProject, serverHome, refreshProject, clientForDirectory, switchDirectory, addRecentDirectory, recentDirectories, } = useConnections() const loadCatalog = useCatalog((s) => s.load) const dirSheetRef = useRef(null) const [refreshing, setRefreshing] = useState(false) const handleSwitchDirectory = useCallback( async (dir?: string) => { await switchDirectory(dir) loadSessions() refreshProject() loadCatalog() }, [switchDirectory, loadSessions, refreshProject, loadCatalog], ) useFocusEffect( useCallback(() => { if (client) { loadSessions() refreshProject() } }, [client, loadSessions, refreshProject]), ) const onRefresh = useCallback(async () => { setRefreshing(true) try { await Promise.all([loadSessions(), refreshProject()]) } catch (err) { console.error("Refresh failed:", err) } finally { setRefreshing(false) } }, [loadSessions, refreshProject]) const handleRename = useCallback((session: Session) => { setRenameText(session.title || "") setRenaming(session) }, []) const submitRename = useCallback(async () => { const title = renameText.trim() if (!title || !renaming || renamingInFlight.current) return const renameClient = renaming.directory ? (clientForDirectory(renaming.directory) ?? client) : client if (!renameClient) return renamingInFlight.current = true try { await renameClient.session.update(renaming.id, { title }) setRenaming(null) setRenameText("") loadSessions() } catch (err) { console.error("Rename failed:", err) Alert.alert("Rename failed", "Could not rename the session. Please try again.") } finally { renamingInFlight.current = false } }, [renaming, renameText, client, clientForDirectory, loadSessions]) const handleDelete = useCallback( (session: Session) => { Alert.alert("Delete Session", `Delete "${session.title || "Untitled Session"}"?`, [ { text: "Cancel", style: "cancel" }, { text: "Delete", style: "destructive", onPress: async () => { try { await deleteSession(session.id) } catch (err) { console.error("Delete failed:", err) Alert.alert("Delete failed", "Could not delete the session. Please try again.") } }, }, ]) }, [deleteSession], ) const onCreateSession = async () => { const session = await createSession() if (session) { router.push({ pathname: `/session/[id]`, params: { id: session.id, ...(session.directory ? { directory: session.directory } : {}) }, }) } } const onCreateInDirectory = async (dir?: string) => { if (!activeConnection) return setIsCreating(true) // If a custom directory is specified, use a one-off client for that directory // so we don't mutate the connection's default project if (dir && dir.trim()) { const dirClient = clientForDirectory(dir.trim()) if (!dirClient) { setIsCreating(false) return } try { const session = await dirClient.session.create({}) addRecentDirectory(dir.trim()) setIsCreating(false) setShowNewSession(false) setCustomDir("") if (session) { router.push({ pathname: `/session/[id]`, params: { id: session.id, ...(session.directory ? { directory: session.directory } : {}) }, }) } } catch (error) { console.error("Failed to create session in directory:", error) Alert.alert("Error", "Failed to create session in that directory.") setIsCreating(false) } return } const session = await createSession() setIsCreating(false) setShowNewSession(false) setCustomDir("") if (session) { router.push({ pathname: `/session/[id]`, params: { id: session.id, ...(session.directory ? { directory: session.directory } : {}) }, }) } } const onFabPress = () => { // Quick create in current project onCreateSession() } const onFabLongPress = () => { // Show modal with more options setCustomDir("") setShowNewSession(true) } if (!activeConnection) { return ( No Connection Add a server connection to get started router.push("/connection/add")} > Add Connection ) } const shortPath = getShortPath(currentProject) return ( {/* Connection indicator — tap to switch project */} dirSheetRef.current?.expand()} onLongPress={() => router.push("/(tabs)/connections")} activeOpacity={0.7} > {activeConnection.name} {shortPath && ( <> {shortPath} )} {error && ( {error} )} item.id} renderItem={({ item }) => ( handleRename(item)} onDelete={() => handleDelete(item)} /> )} refreshControl={ } ListEmptyComponent={ isLoading ? ( ) : ( No sessions yet ) } contentContainerStyle={sessions.length === 0 ? styles.emptyContent : undefined} /> {/* FAB to create new session */} {/* New Session Info Modal */} setShowNewSession(false)} /> New Session setShowNewSession(false)}> {/* Current directory */} Current Directory {currentProject?.path?.absolute || activeConnection?.directory || "Server default"} {/* Custom directory input */} Or use a different folder { // Expand ~ to server home directory if (serverHome && text.startsWith("~/")) { setCustomDir(serverHome + text.slice(1)) } else if (serverHome && text === "~") { setCustomDir(serverHome) } else { setCustomDir(text) } }} autoCapitalize="none" autoCorrect={false} /> {/* Quick path shortcuts */} {serverHome && ( setCustomDir(serverHome)} > ~ setCustomDir(serverHome + "/")} > ~/ )} {customDir.trim() ? ( onCreateInDirectory(customDir)} disabled={isCreating} > {isCreating ? ( ) : ( Create in {customDir.split("/").filter(Boolean).pop() || customDir} )} ) : ( onCreateInDirectory()} disabled={isCreating} > {isCreating ? ( ) : ( Create Session )} )} {/* Rename modal */} setRenaming(null)} /> Rename Session setRenaming(null)}> Cancel Save setRenaming(null)} /> {/* Directory switcher bottom sheet */} ) } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: "#ffffff", }, containerDark: { backgroundColor: "#0a0a0a", }, connectionBar: { flexDirection: "row", justifyContent: "space-between", alignItems: "center", paddingHorizontal: 16, paddingVertical: 12, borderBottomWidth: 1, borderBottomColor: "#e5e5e5", }, connectionBarDark: { borderBottomColor: "#1a1a1a", }, connectionInfo: { flexDirection: "row", alignItems: "center", gap: 8, }, connectionDot: { width: 8, height: 8, borderRadius: 4, }, connectionName: { fontSize: 14, fontWeight: "600", color: "#0a0a0a", }, connectionUrl: { fontSize: 12, color: "#666666", }, projectPath: { fontSize: 13, color: "#666666", flex: 1, }, errorBar: { backgroundColor: "#fef2f2", padding: 12, borderBottomWidth: 1, borderBottomColor: "#fecaca", }, errorText: { color: "#dc2626", fontSize: 14, }, sessionItem: { flexDirection: "row", alignItems: "center", padding: 16, borderBottomWidth: 1, borderBottomColor: "#e5e5e5", }, sessionItemDark: { borderBottomColor: "#1a1a1a", }, sessionContent: { flex: 1, }, sessionHeader: { flexDirection: "row", alignItems: "center", gap: 8, marginBottom: 2, }, sessionTitle: { fontSize: 16, fontWeight: "500", color: "#0a0a0a", marginBottom: 4, }, textDark: { color: "#ffffff", }, sessionMeta: { fontSize: 13, color: "#666666", }, sessionMetaRow: { flexDirection: "row", alignItems: "center", justifyContent: "space-between", }, sessionDirBadge: { flexDirection: "row", alignItems: "center", gap: 4, backgroundColor: "#f5f5f5", paddingHorizontal: 6, paddingVertical: 2, borderRadius: 4, }, sessionDirText: { fontSize: 11, color: "#666666", }, metaDark: { color: "#888888", }, emptyContainer: { flex: 1, justifyContent: "center", alignItems: "center", padding: 32, backgroundColor: "#ffffff", }, emptyTitle: { fontSize: 20, fontWeight: "600", marginTop: 16, color: "#0a0a0a", }, emptySubtitle: { fontSize: 14, color: "#666666", marginTop: 8, textAlign: "center", }, addButton: { backgroundColor: "#0a0a0a", paddingHorizontal: 24, paddingVertical: 12, borderRadius: 8, marginTop: 24, }, addButtonDark: { backgroundColor: "#ffffff", }, addButtonText: { color: "#ffffff", fontWeight: "600", }, addButtonTextDark: { color: "#0a0a0a", }, loadingContainer: { flex: 1, justifyContent: "center", alignItems: "center", paddingVertical: 64, }, emptyList: { flex: 1, justifyContent: "center", alignItems: "center", paddingVertical: 64, }, emptyListText: { fontSize: 16, color: "#666666", }, emptyContent: { flex: 1, }, fab: { position: "absolute", right: 16, bottom: 16, width: 56, height: 56, borderRadius: 28, backgroundColor: "#0a0a0a", justifyContent: "center", alignItems: "center", shadowColor: "#000", shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.25, shadowRadius: 4, elevation: 5, }, fabDark: { backgroundColor: "#ffffff", }, // Modal styles modalOverlay: { flex: 1, backgroundColor: "rgba(0, 0, 0, 0.5)", justifyContent: "flex-end", }, modalDismiss: { flex: 1, }, modalContent: { backgroundColor: "#ffffff", borderTopLeftRadius: 20, borderTopRightRadius: 20, padding: 20, paddingBottom: 40, }, modalContentDark: { backgroundColor: "#1a1a1a", }, modalHeader: { flexDirection: "row", justifyContent: "space-between", alignItems: "center", marginBottom: 20, }, modalTitle: { fontSize: 20, fontWeight: "600", color: "#0a0a0a", }, modalBody: { marginBottom: 24, }, modalLabel: { fontSize: 13, fontWeight: "600", color: "#666666", marginBottom: 8, textTransform: "uppercase", }, modalDirBox: { flexDirection: "row", alignItems: "center", gap: 12, backgroundColor: "#f5f5f5", padding: 16, borderRadius: 12, }, modalDirBoxDark: { backgroundColor: "#2a2a2a", }, modalDirText: { fontSize: 15, color: "#0a0a0a", flex: 1, }, modalInput: { backgroundColor: "#f5f5f5", borderRadius: 12, paddingHorizontal: 16, paddingVertical: 14, fontSize: 15, color: "#0a0a0a", }, modalInputDark: { backgroundColor: "#2a2a2a", color: "#ffffff", }, pathChips: { flexDirection: "row", gap: 8, marginTop: 8, }, pathChip: { paddingHorizontal: 12, paddingVertical: 6, backgroundColor: "#e8e5f0", borderRadius: 16, }, pathChipDark: { backgroundColor: "#2a2040", }, pathChipText: { fontSize: 13, fontWeight: "600", color: "#6d28d9", }, pathChipTextDark: { color: "#c4b5fd", }, modalHint: { fontSize: 13, color: "#666666", marginTop: 12, }, modalActions: { flexDirection: "row", gap: 12, }, modalButton: { flex: 1, flexDirection: "row", alignItems: "center", justifyContent: "center", gap: 8, padding: 16, borderRadius: 12, }, modalButtonSecondary: { backgroundColor: "#f5f5f5", }, modalButtonSecondaryDark: { backgroundColor: "#2a2a2a", }, modalButtonPrimary: { backgroundColor: "#0a0a0a", }, modalButtonPrimaryDark: { backgroundColor: "#ffffff", }, modalButtonText: { fontSize: 15, fontWeight: "600", color: "#0a0a0a", }, modalButtonTextPrimary: { fontSize: 15, fontWeight: "600", color: "#ffffff", }, modalButtonTextPrimaryDark: { color: "#0a0a0a", }, modalButtonFull: { flex: 0, width: "100%", }, // Rename modal renameCard: { backgroundColor: "#ffffff", borderRadius: 16, padding: 20, marginHorizontal: 32, gap: 16, }, renameCardDark: { backgroundColor: "#1a1a1a", }, renameTitle: { fontSize: 17, fontWeight: "600", color: "#0a0a0a", }, renameActions: { flexDirection: "row", justifyContent: "flex-end", gap: 12, }, renameBtn: { paddingHorizontal: 20, paddingVertical: 10, borderRadius: 10, }, renameBtnCancel: { backgroundColor: "transparent", }, renameBtnCancelText: { fontSize: 15, fontWeight: "600", color: "#888888", }, })