feat: improve project picker with recents and server projects (#52)

Adds recent and server-project discovery to the new-session directory picker. Reviewed against current main; Android, iOS Simulator, and mandatory CUA checks are green.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Den
2026-07-15 12:06:46 -07:00
committed by GitHub
parent 5c14ce0a5d
commit a5723bf087

View File

@@ -1,8 +1,9 @@
import { useCallback, useState, useRef } from "react" import { useCallback, useState, useRef, useEffect } from "react"
import { import {
View, View,
Text, Text,
FlatList, FlatList,
ScrollView,
TouchableOpacity, TouchableOpacity,
StyleSheet, StyleSheet,
useColorScheme, useColorScheme,
@@ -20,7 +21,7 @@ import { useSessions } from "../../src/stores/sessions"
import { useConnections } from "../../src/stores/connections" import { useConnections } from "../../src/stores/connections"
import { useCatalog } from "../../src/stores/catalog" import { useCatalog } from "../../src/stores/catalog"
import type BottomSheet from "@gorhom/bottom-sheet" import type BottomSheet from "@gorhom/bottom-sheet"
import type { Session } from "../../src/lib/sdk" import type { Session, Project } from "../../src/lib/sdk"
import { DirectorySwitcher } from "../../src/components/chat" import { DirectorySwitcher } from "../../src/components/chat"
function formatTime(timestamp: number): string { function formatTime(timestamp: number): string {
@@ -112,6 +113,7 @@ export default function SessionsScreen() {
const [renaming, setRenaming] = useState<Session | null>(null) const [renaming, setRenaming] = useState<Session | null>(null)
const [renameText, setRenameText] = useState("") const [renameText, setRenameText] = useState("")
const renamingInFlight = useRef(false) const renamingInFlight = useRef(false)
const [serverProjects, setServerProjects] = useState<Project[]>([])
const { sessions, isLoading, error, loadSessions, createSession, deleteSession } = useSessions() const { sessions, isLoading, error, loadSessions, createSession, deleteSession } = useSessions()
const { const {
@@ -129,6 +131,15 @@ export default function SessionsScreen() {
const dirSheetRef = useRef<BottomSheet>(null) const dirSheetRef = useRef<BottomSheet>(null)
const [refreshing, setRefreshing] = useState(false) const [refreshing, setRefreshing] = useState(false)
// Fetch server-known projects when the new session modal opens
useEffect(() => {
if (!showNewSession || !client) return
client.project
.list()
.then(setServerProjects)
.catch(() => setServerProjects([]))
}, [showNewSession, client])
const handleSwitchDirectory = useCallback( const handleSwitchDirectory = useCallback(
async (dir?: string) => { async (dir?: string) => {
await switchDirectory(dir) await switchDirectory(dir)
@@ -370,19 +381,106 @@ export default function SessionsScreen() {
</TouchableOpacity> </TouchableOpacity>
</View> </View>
<View style={styles.modalBody}> <ScrollView style={styles.modalScrollBody} keyboardShouldPersistTaps="handled">
{/* Current directory */} {/* Current directory — tapping creates session immediately */}
<Text style={[styles.modalLabel, isDark && styles.metaDark]}>Current Directory</Text> <Text style={[styles.modalLabel, isDark && styles.metaDark]}>Current Project</Text>
<View style={[styles.modalDirBox, isDark && styles.modalDirBoxDark]}> <TouchableOpacity
<Ionicons name="folder" size={20} color={isDark ? "#888888" : "#666666"} /> style={[styles.modalDirBox, isDark && styles.modalDirBoxDark]}
onPress={() => onCreateInDirectory()}
disabled={isCreating}
>
<Ionicons name="folder" size={20} color={isDark ? "#8b5cf6" : "#6d28d9"} />
<Text style={[styles.modalDirText, isDark && styles.textDark]} numberOfLines={2}> <Text style={[styles.modalDirText, isDark && styles.textDark]} numberOfLines={2}>
{currentProject?.path?.absolute || activeConnection?.directory || "Server default"} {currentProject?.path?.absolute || activeConnection?.directory || "Server default"}
</Text> </Text>
</View> <Ionicons name="arrow-forward-circle" size={20} color={isDark ? "#8b5cf6" : "#6d28d9"} />
</TouchableOpacity>
{/* Custom directory input */} {/* Recent projects */}
{recentDirectories.length > 0 && (
<>
<Text style={[styles.modalLabel, isDark && styles.metaDark, { marginTop: 16 }]}>
Recent Projects
</Text>
{recentDirectories.map((dir) => {
const short = dir.split("/").filter(Boolean).pop() || dir
const isCurrent =
dir === (currentProject?.path?.absolute || activeConnection?.directory)
return (
<TouchableOpacity
key={dir}
style={[
styles.projectRow,
isDark && styles.projectRowDark,
isCurrent && styles.projectRowActive,
]}
onPress={() => onCreateInDirectory(dir)}
disabled={isCreating}
>
<Ionicons
name="folder-outline"
size={18}
color={isCurrent ? "#8b5cf6" : isDark ? "#888888" : "#666666"}
/>
<View style={styles.projectRowContent}>
<Text
style={[
styles.projectRowName,
isDark && styles.textDark,
isCurrent && styles.projectRowNameActive,
]}
numberOfLines={1}
>
{short}
</Text>
<Text style={[styles.projectRowPath, isDark && styles.metaDark]} numberOfLines={1}>
{dir}
</Text>
</View>
{isCurrent && <Ionicons name="checkmark-circle" size={18} color="#8b5cf6" />}
</TouchableOpacity>
)
})}
</>
)}
{/* Server-known projects (excluding current) */}
{serverProjects.filter((p) => p.path?.absolute !== currentProject?.path?.absolute).length > 0 && (
<>
<Text style={[styles.modalLabel, isDark && styles.metaDark, { marginTop: 16 }]}>
Server Projects
</Text>
{serverProjects
.filter((p) => p.path?.absolute !== currentProject?.path?.absolute)
.map((p) => {
const short = p.name || p.path?.absolute?.split("/").filter(Boolean).pop() || p.id
return (
<TouchableOpacity
key={p.id}
style={[styles.projectRow, isDark && styles.projectRowDark]}
onPress={() => onCreateInDirectory(p.path?.absolute)}
disabled={isCreating}
>
<Ionicons name="code-slash-outline" size={18} color={isDark ? "#888888" : "#666666"} />
<View style={styles.projectRowContent}>
<Text style={[styles.projectRowName, isDark && styles.textDark]} numberOfLines={1}>
{short}
</Text>
{p.path?.absolute && (
<Text style={[styles.projectRowPath, isDark && styles.metaDark]} numberOfLines={1}>
{p.path.absolute}
</Text>
)}
</View>
</TouchableOpacity>
)
})}
</>
)}
{/* Manual path input fallback */}
<Text style={[styles.modalLabel, isDark && styles.metaDark, { marginTop: 16 }]}> <Text style={[styles.modalLabel, isDark && styles.metaDark, { marginTop: 16 }]}>
Or use a different folder Enter Path Manually
</Text> </Text>
<TextInput <TextInput
style={[styles.modalInput, isDark && styles.modalInputDark]} style={[styles.modalInput, isDark && styles.modalInputDark]}
@@ -419,7 +517,7 @@ export default function SessionsScreen() {
</TouchableOpacity> </TouchableOpacity>
</View> </View>
)} )}
</View> </ScrollView>
<View style={styles.modalActions}> <View style={styles.modalActions}>
{customDir.trim() ? ( {customDir.trim() ? (
@@ -730,6 +828,42 @@ const styles = StyleSheet.create({
modalBody: { modalBody: {
marginBottom: 24, marginBottom: 24,
}, },
modalScrollBody: {
maxHeight: 420,
marginBottom: 16,
},
projectRow: {
flexDirection: "row",
alignItems: "center",
gap: 12,
paddingHorizontal: 14,
paddingVertical: 11,
borderRadius: 10,
backgroundColor: "#f5f5f5",
marginBottom: 6,
},
projectRowDark: {
backgroundColor: "#2a2a2a",
},
projectRowActive: {
backgroundColor: "#f5f3ff",
},
projectRowContent: {
flex: 1,
},
projectRowName: {
fontSize: 14,
fontWeight: "600",
color: "#0a0a0a",
},
projectRowNameActive: {
color: "#8b5cf6",
},
projectRowPath: {
fontSize: 11,
color: "#999999",
marginTop: 1,
},
modalLabel: { modalLabel: {
fontSize: 13, fontSize: 13,
fontWeight: "600", fontWeight: "600",