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:
@@ -1,8 +1,9 @@
|
||||
import { useCallback, useState, useRef } from "react"
|
||||
import { useCallback, useState, useRef, useEffect } from "react"
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
FlatList,
|
||||
ScrollView,
|
||||
TouchableOpacity,
|
||||
StyleSheet,
|
||||
useColorScheme,
|
||||
@@ -20,7 +21,7 @@ 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 type { Session, Project } from "../../src/lib/sdk"
|
||||
import { DirectorySwitcher } from "../../src/components/chat"
|
||||
|
||||
function formatTime(timestamp: number): string {
|
||||
@@ -112,6 +113,7 @@ export default function SessionsScreen() {
|
||||
const [renaming, setRenaming] = useState<Session | null>(null)
|
||||
const [renameText, setRenameText] = useState("")
|
||||
const renamingInFlight = useRef(false)
|
||||
const [serverProjects, setServerProjects] = useState<Project[]>([])
|
||||
|
||||
const { sessions, isLoading, error, loadSessions, createSession, deleteSession } = useSessions()
|
||||
const {
|
||||
@@ -129,6 +131,15 @@ export default function SessionsScreen() {
|
||||
const dirSheetRef = useRef<BottomSheet>(null)
|
||||
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(
|
||||
async (dir?: string) => {
|
||||
await switchDirectory(dir)
|
||||
@@ -370,19 +381,106 @@ export default function SessionsScreen() {
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<View style={styles.modalBody}>
|
||||
{/* Current directory */}
|
||||
<Text style={[styles.modalLabel, isDark && styles.metaDark]}>Current Directory</Text>
|
||||
<View style={[styles.modalDirBox, isDark && styles.modalDirBoxDark]}>
|
||||
<Ionicons name="folder" size={20} color={isDark ? "#888888" : "#666666"} />
|
||||
<ScrollView style={styles.modalScrollBody} keyboardShouldPersistTaps="handled">
|
||||
{/* Current directory — tapping creates session immediately */}
|
||||
<Text style={[styles.modalLabel, isDark && styles.metaDark]}>Current Project</Text>
|
||||
<TouchableOpacity
|
||||
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}>
|
||||
{currentProject?.path?.absolute || activeConnection?.directory || "Server default"}
|
||||
</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 }]}>
|
||||
Or use a different folder
|
||||
Enter Path Manually
|
||||
</Text>
|
||||
<TextInput
|
||||
style={[styles.modalInput, isDark && styles.modalInputDark]}
|
||||
@@ -419,7 +517,7 @@ export default function SessionsScreen() {
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
<View style={styles.modalActions}>
|
||||
{customDir.trim() ? (
|
||||
@@ -730,6 +828,42 @@ const styles = StyleSheet.create({
|
||||
modalBody: {
|
||||
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: {
|
||||
fontSize: 13,
|
||||
fontWeight: "600",
|
||||
|
||||
Reference in New Issue
Block a user