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