From 93670c15d9b6542b24078c9cef7b09e09fc8cb47 Mon Sep 17 00:00:00 2001
From: Matthias Nott <mnott@mnsoft.org>
Date: Sat, 07 Mar 2026 13:49:16 +0100
Subject: [PATCH] feat: project picker in session drawer
---
components/SessionDrawer.tsx | 151 +++++++++++++++++++++++++++++++++----
types/index.ts | 15 +++
contexts/ChatContext.tsx | 22 ++++-
3 files changed, 164 insertions(+), 24 deletions(-)
diff --git a/components/SessionDrawer.tsx b/components/SessionDrawer.tsx
index 8019e74..75db7b8 100644
--- a/components/SessionDrawer.tsx
+++ b/components/SessionDrawer.tsx
@@ -25,7 +25,7 @@
ScaleDecorator,
} from "react-native-draggable-flatlist";
import * as Haptics from "expo-haptics";
-import { WsSession } from "../types";
+import { WsSession, PaiProject } from "../types";
import { useChat } from "../contexts/ChatContext";
import { useTheme, type ThemeColors } from "../contexts/ThemeContext";
@@ -262,10 +262,14 @@
renameSession,
removeSession,
createSession,
+ fetchProjects,
+ projects,
unreadCounts,
} = useChat();
const { colors } = useTheme();
const [editingId, setEditingId] = useState<string | null>(null);
+ const [showProjectPicker, setShowProjectPicker] = useState(false);
+ const [customPath, setCustomPath] = useState("");
const slideAnim = useRef(new Animated.Value(-DRAWER_WIDTH)).current;
const fadeAnim = useRef(new Animated.Value(0)).current;
const [rendered, setRendered] = useState(false);
@@ -326,6 +330,8 @@
const handleClose = useCallback(() => {
setEditingId(null);
+ setShowProjectPicker(false);
+ setCustomPath("");
Keyboard.dismiss();
onClose();
}, [onClose]);
@@ -362,8 +368,18 @@
);
const handleNewSession = useCallback(() => {
+ Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
+ setShowProjectPicker((prev) => {
+ if (!prev) fetchProjects();
+ return !prev;
+ });
+ }, [fetchProjects]);
+
+ const launchSession = useCallback((opts?: { project?: string; path?: string }) => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
- createSession();
+ createSession(opts);
+ setShowProjectPicker(false);
+ setCustomPath("");
handleClose();
setTimeout(() => requestSessions(), 2500);
}, [createSession, requestSessions, handleClose]);
@@ -519,23 +535,120 @@
/>
)}
- {/* New session button */}
- <View style={{ alignItems: "center", paddingVertical: 12 }}>
- <Pressable
- onPress={handleNewSession}
- style={{
- flexDirection: "row",
- alignItems: "center",
- gap: 6,
- paddingHorizontal: 20,
- paddingVertical: 10,
- borderRadius: 24,
- backgroundColor: colors.accent,
- }}
- >
- <Text style={{ color: "#FFF", fontSize: 18, fontWeight: "700" }}>+</Text>
- <Text style={{ color: "#FFF", fontSize: 15, fontWeight: "600" }}>New Session</Text>
- </Pressable>
+ {/* New session button + project picker */}
+ <View style={{ paddingVertical: 8 }}>
+ <View style={{ alignItems: "center", paddingBottom: showProjectPicker ? 8 : 0 }}>
+ <Pressable
+ onPress={handleNewSession}
+ style={{
+ flexDirection: "row",
+ alignItems: "center",
+ gap: 6,
+ paddingHorizontal: 20,
+ paddingVertical: 10,
+ borderRadius: 24,
+ backgroundColor: colors.accent,
+ }}
+ >
+ <Text style={{ color: "#FFF", fontSize: 18, fontWeight: "700" }}>+</Text>
+ <Text style={{ color: "#FFF", fontSize: 15, fontWeight: "600" }}>New Session</Text>
+ </Pressable>
+ </View>
+
+ {showProjectPicker && (
+ <View style={{
+ marginHorizontal: 12,
+ borderRadius: 12,
+ backgroundColor: colors.bgTertiary,
+ overflow: "hidden",
+ }}>
+ {/* Home directory β always first */}
+ <Pressable
+ onPress={() => launchSession({ path: "~" })}
+ style={({ pressed }) => ({
+ flexDirection: "row",
+ alignItems: "center",
+ paddingHorizontal: 16,
+ paddingVertical: 12,
+ backgroundColor: pressed ? colors.border : "transparent",
+ })}
+ >
+ <Text style={{ fontSize: 18, marginRight: 10 }}>π </Text>
+ <View style={{ flex: 1 }}>
+ <Text style={{ color: colors.text, fontSize: 15, fontWeight: "600" }}>Home</Text>
+ <Text style={{ color: colors.textMuted, fontSize: 12 }}>~/</Text>
+ </View>
+ </Pressable>
+
+ {/* PAI projects */}
+ {projects.map((p) => (
+ <Pressable
+ key={p.slug}
+ onPress={() => launchSession({ project: p.name })}
+ style={({ pressed }) => ({
+ flexDirection: "row",
+ alignItems: "center",
+ paddingHorizontal: 16,
+ paddingVertical: 12,
+ borderTopWidth: 1,
+ borderTopColor: colors.border,
+ backgroundColor: pressed ? colors.border : "transparent",
+ })}
+ >
+ <Text style={{ fontSize: 18, marginRight: 10 }}>π</Text>
+ <View style={{ flex: 1 }}>
+ <Text style={{ color: colors.text, fontSize: 15, fontWeight: "500" }}>{p.name}</Text>
+ <Text style={{ color: colors.textMuted, fontSize: 12 }} numberOfLines={1}>
+ {p.path.replace(/^\/Users\/[^/]+/, "~")}
+ </Text>
+ </View>
+ </Pressable>
+ ))}
+
+ {/* Custom path input */}
+ <View style={{
+ flexDirection: "row",
+ alignItems: "center",
+ borderTopWidth: 1,
+ borderTopColor: colors.border,
+ paddingHorizontal: 16,
+ paddingVertical: 8,
+ }}>
+ <TextInput
+ value={customPath}
+ onChangeText={setCustomPath}
+ placeholder="Custom pathβ¦"
+ placeholderTextColor={colors.textMuted}
+ autoCapitalize="none"
+ autoCorrect={false}
+ returnKeyType="go"
+ onSubmitEditing={() => {
+ if (customPath.trim()) launchSession({ path: customPath.trim() });
+ }}
+ style={{
+ flex: 1,
+ color: colors.text,
+ fontSize: 14,
+ paddingVertical: 6,
+ }}
+ />
+ {customPath.trim().length > 0 && (
+ <Pressable
+ onPress={() => launchSession({ path: customPath.trim() })}
+ style={{
+ paddingHorizontal: 12,
+ paddingVertical: 6,
+ borderRadius: 8,
+ backgroundColor: colors.accent,
+ marginLeft: 8,
+ }}
+ >
+ <Text style={{ color: "#FFF", fontSize: 13, fontWeight: "600" }}>Go</Text>
+ </Pressable>
+ )}
+ </View>
+ </View>
+ )}
</View>
{/* Footer */}
diff --git a/contexts/ChatContext.tsx b/contexts/ChatContext.tsx
index 54a548c..408e878 100644
--- a/contexts/ChatContext.tsx
+++ b/contexts/ChatContext.tsx
@@ -6,7 +6,7 @@
useRef,
useState,
} from "react";
-import { Message, WsIncoming, WsSession } from "../types";
+import { Message, WsIncoming, WsSession, PaiProject } from "../types";
import { useConnection } from "./ConnectionContext";
import { playAudio, encodeAudioToBase64, saveBase64Audio, canAutoplay } from "../services/audio";
import { requestNotificationPermissions, notifyIncomingMessage } from "../services/notifications";
@@ -128,7 +128,9 @@
switchSession: (sessionId: string) => void;
renameSession: (sessionId: string, name: string) => void;
removeSession: (sessionId: string) => void;
- createSession: () => void;
+ createSession: (opts?: { project?: string; path?: string }) => void;
+ fetchProjects: () => void;
+ projects: PaiProject[];
unreadCounts: Record<string, number>;
latestScreenshot: string | null;
requestScreenshot: () => void;
@@ -151,6 +153,8 @@
const [unreadCounts, setUnreadCounts] = useState<Record<string, number>>({});
// Typing indicator from server
const [isTyping, setIsTyping] = useState(false);
+ // PAI projects list
+ const [projects, setProjects] = useState<PaiProject[]>([]);
const {
status,
@@ -386,6 +390,10 @@
// Connection status update β ignore for now
break;
}
+ case "projects": {
+ setProjects(data.projects ?? []);
+ break;
+ }
case "error": {
const msg: Message = {
id: generateId(),
@@ -534,8 +542,12 @@
[sendCommand]
);
- const createSession = useCallback(() => {
- sendCommand("create");
+ const createSession = useCallback((opts?: { project?: string; path?: string }) => {
+ sendCommand("create", opts ?? {});
+ }, [sendCommand]);
+
+ const fetchProjects = useCallback(() => {
+ sendCommand("projects");
}, [sendCommand]);
// --- Screenshot / navigation ---
@@ -567,6 +579,8 @@
renameSession,
removeSession,
createSession,
+ fetchProjects,
+ projects,
unreadCounts,
latestScreenshot,
requestScreenshot,
diff --git a/types/index.ts b/types/index.ts
index 97846eb..fc57c4a 100644
--- a/types/index.ts
+++ b/types/index.ts
@@ -120,6 +120,18 @@
status: string;
}
+export interface PaiProject {
+ name: string;
+ slug: string;
+ path: string;
+ sessions: number;
+}
+
+export interface WsIncomingProjects {
+ type: "projects";
+ projects: PaiProject[];
+}
+
export type WsIncoming =
| WsIncomingText
| WsIncomingVoice
@@ -130,4 +142,5 @@
| WsIncomingTranscript
| WsIncomingTyping
| WsIncomingError
- | WsIncomingStatus;
+ | WsIncomingStatus
+ | WsIncomingProjects;
--
Gitblit v1.3.1