Matthias Nott
2026-03-07 93670c15d9b6542b24078c9cef7b09e09fc8cb47
feat: project picker in session drawer

New Session button now opens a picker showing PAI named projects,
home directory, and custom path input. Replaces immediate blank
session creation with an informed choice.
3 files modified
changed files
components/SessionDrawer.tsx patch | view | blame | history
contexts/ChatContext.tsx patch | view | blame | history
types/index.ts patch | view | blame | history
components/SessionDrawer.tsx
....@@ -25,7 +25,7 @@
2525 ScaleDecorator,
2626 } from "react-native-draggable-flatlist";
2727 import * as Haptics from "expo-haptics";
28
-import { WsSession } from "../types";
28
+import { WsSession, PaiProject } from "../types";
2929 import { useChat } from "../contexts/ChatContext";
3030 import { useTheme, type ThemeColors } from "../contexts/ThemeContext";
3131
....@@ -262,10 +262,14 @@
262262 renameSession,
263263 removeSession,
264264 createSession,
265
+ fetchProjects,
266
+ projects,
265267 unreadCounts,
266268 } = useChat();
267269 const { colors } = useTheme();
268270 const [editingId, setEditingId] = useState<string | null>(null);
271
+ const [showProjectPicker, setShowProjectPicker] = useState(false);
272
+ const [customPath, setCustomPath] = useState("");
269273 const slideAnim = useRef(new Animated.Value(-DRAWER_WIDTH)).current;
270274 const fadeAnim = useRef(new Animated.Value(0)).current;
271275 const [rendered, setRendered] = useState(false);
....@@ -326,6 +330,8 @@
326330
327331 const handleClose = useCallback(() => {
328332 setEditingId(null);
333
+ setShowProjectPicker(false);
334
+ setCustomPath("");
329335 Keyboard.dismiss();
330336 onClose();
331337 }, [onClose]);
....@@ -362,8 +368,18 @@
362368 );
363369
364370 const handleNewSession = useCallback(() => {
371
+ Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
372
+ setShowProjectPicker((prev) => {
373
+ if (!prev) fetchProjects();
374
+ return !prev;
375
+ });
376
+ }, [fetchProjects]);
377
+
378
+ const launchSession = useCallback((opts?: { project?: string; path?: string }) => {
365379 Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
366
- createSession();
380
+ createSession(opts);
381
+ setShowProjectPicker(false);
382
+ setCustomPath("");
367383 handleClose();
368384 setTimeout(() => requestSessions(), 2500);
369385 }, [createSession, requestSessions, handleClose]);
....@@ -519,23 +535,120 @@
519535 />
520536 )}
521537
522
- {/* New session button */}
523
- <View style={{ alignItems: "center", paddingVertical: 12 }}>
524
- <Pressable
525
- onPress={handleNewSession}
526
- style={{
527
- flexDirection: "row",
528
- alignItems: "center",
529
- gap: 6,
530
- paddingHorizontal: 20,
531
- paddingVertical: 10,
532
- borderRadius: 24,
533
- backgroundColor: colors.accent,
534
- }}
535
- >
536
- <Text style={{ color: "#FFF", fontSize: 18, fontWeight: "700" }}>+</Text>
537
- <Text style={{ color: "#FFF", fontSize: 15, fontWeight: "600" }}>New Session</Text>
538
- </Pressable>
538
+ {/* New session button + project picker */}
539
+ <View style={{ paddingVertical: 8 }}>
540
+ <View style={{ alignItems: "center", paddingBottom: showProjectPicker ? 8 : 0 }}>
541
+ <Pressable
542
+ onPress={handleNewSession}
543
+ style={{
544
+ flexDirection: "row",
545
+ alignItems: "center",
546
+ gap: 6,
547
+ paddingHorizontal: 20,
548
+ paddingVertical: 10,
549
+ borderRadius: 24,
550
+ backgroundColor: colors.accent,
551
+ }}
552
+ >
553
+ <Text style={{ color: "#FFF", fontSize: 18, fontWeight: "700" }}>+</Text>
554
+ <Text style={{ color: "#FFF", fontSize: 15, fontWeight: "600" }}>New Session</Text>
555
+ </Pressable>
556
+ </View>
557
+
558
+ {showProjectPicker && (
559
+ <View style={{
560
+ marginHorizontal: 12,
561
+ borderRadius: 12,
562
+ backgroundColor: colors.bgTertiary,
563
+ overflow: "hidden",
564
+ }}>
565
+ {/* Home directory β€” always first */}
566
+ <Pressable
567
+ onPress={() => launchSession({ path: "~" })}
568
+ style={({ pressed }) => ({
569
+ flexDirection: "row",
570
+ alignItems: "center",
571
+ paddingHorizontal: 16,
572
+ paddingVertical: 12,
573
+ backgroundColor: pressed ? colors.border : "transparent",
574
+ })}
575
+ >
576
+ <Text style={{ fontSize: 18, marginRight: 10 }}>🏠</Text>
577
+ <View style={{ flex: 1 }}>
578
+ <Text style={{ color: colors.text, fontSize: 15, fontWeight: "600" }}>Home</Text>
579
+ <Text style={{ color: colors.textMuted, fontSize: 12 }}>~/</Text>
580
+ </View>
581
+ </Pressable>
582
+
583
+ {/* PAI projects */}
584
+ {projects.map((p) => (
585
+ <Pressable
586
+ key={p.slug}
587
+ onPress={() => launchSession({ project: p.name })}
588
+ style={({ pressed }) => ({
589
+ flexDirection: "row",
590
+ alignItems: "center",
591
+ paddingHorizontal: 16,
592
+ paddingVertical: 12,
593
+ borderTopWidth: 1,
594
+ borderTopColor: colors.border,
595
+ backgroundColor: pressed ? colors.border : "transparent",
596
+ })}
597
+ >
598
+ <Text style={{ fontSize: 18, marginRight: 10 }}>πŸ“‚</Text>
599
+ <View style={{ flex: 1 }}>
600
+ <Text style={{ color: colors.text, fontSize: 15, fontWeight: "500" }}>{p.name}</Text>
601
+ <Text style={{ color: colors.textMuted, fontSize: 12 }} numberOfLines={1}>
602
+ {p.path.replace(/^\/Users\/[^/]+/, "~")}
603
+ </Text>
604
+ </View>
605
+ </Pressable>
606
+ ))}
607
+
608
+ {/* Custom path input */}
609
+ <View style={{
610
+ flexDirection: "row",
611
+ alignItems: "center",
612
+ borderTopWidth: 1,
613
+ borderTopColor: colors.border,
614
+ paddingHorizontal: 16,
615
+ paddingVertical: 8,
616
+ }}>
617
+ <TextInput
618
+ value={customPath}
619
+ onChangeText={setCustomPath}
620
+ placeholder="Custom path…"
621
+ placeholderTextColor={colors.textMuted}
622
+ autoCapitalize="none"
623
+ autoCorrect={false}
624
+ returnKeyType="go"
625
+ onSubmitEditing={() => {
626
+ if (customPath.trim()) launchSession({ path: customPath.trim() });
627
+ }}
628
+ style={{
629
+ flex: 1,
630
+ color: colors.text,
631
+ fontSize: 14,
632
+ paddingVertical: 6,
633
+ }}
634
+ />
635
+ {customPath.trim().length > 0 && (
636
+ <Pressable
637
+ onPress={() => launchSession({ path: customPath.trim() })}
638
+ style={{
639
+ paddingHorizontal: 12,
640
+ paddingVertical: 6,
641
+ borderRadius: 8,
642
+ backgroundColor: colors.accent,
643
+ marginLeft: 8,
644
+ }}
645
+ >
646
+ <Text style={{ color: "#FFF", fontSize: 13, fontWeight: "600" }}>Go</Text>
647
+ </Pressable>
648
+ )}
649
+ </View>
650
+ </View>
651
+ )}
539652 </View>
540653
541654 {/* Footer */}
contexts/ChatContext.tsx
....@@ -6,7 +6,7 @@
66 useRef,
77 useState,
88 } from "react";
9
-import { Message, WsIncoming, WsSession } from "../types";
9
+import { Message, WsIncoming, WsSession, PaiProject } from "../types";
1010 import { useConnection } from "./ConnectionContext";
1111 import { playAudio, encodeAudioToBase64, saveBase64Audio, canAutoplay } from "../services/audio";
1212 import { requestNotificationPermissions, notifyIncomingMessage } from "../services/notifications";
....@@ -128,7 +128,9 @@
128128 switchSession: (sessionId: string) => void;
129129 renameSession: (sessionId: string, name: string) => void;
130130 removeSession: (sessionId: string) => void;
131
- createSession: () => void;
131
+ createSession: (opts?: { project?: string; path?: string }) => void;
132
+ fetchProjects: () => void;
133
+ projects: PaiProject[];
132134 unreadCounts: Record<string, number>;
133135 latestScreenshot: string | null;
134136 requestScreenshot: () => void;
....@@ -151,6 +153,8 @@
151153 const [unreadCounts, setUnreadCounts] = useState<Record<string, number>>({});
152154 // Typing indicator from server
153155 const [isTyping, setIsTyping] = useState(false);
156
+ // PAI projects list
157
+ const [projects, setProjects] = useState<PaiProject[]>([]);
154158
155159 const {
156160 status,
....@@ -386,6 +390,10 @@
386390 // Connection status update β€” ignore for now
387391 break;
388392 }
393
+ case "projects": {
394
+ setProjects(data.projects ?? []);
395
+ break;
396
+ }
389397 case "error": {
390398 const msg: Message = {
391399 id: generateId(),
....@@ -534,8 +542,12 @@
534542 [sendCommand]
535543 );
536544
537
- const createSession = useCallback(() => {
538
- sendCommand("create");
545
+ const createSession = useCallback((opts?: { project?: string; path?: string }) => {
546
+ sendCommand("create", opts ?? {});
547
+ }, [sendCommand]);
548
+
549
+ const fetchProjects = useCallback(() => {
550
+ sendCommand("projects");
539551 }, [sendCommand]);
540552
541553 // --- Screenshot / navigation ---
....@@ -567,6 +579,8 @@
567579 renameSession,
568580 removeSession,
569581 createSession,
582
+ fetchProjects,
583
+ projects,
570584 unreadCounts,
571585 latestScreenshot,
572586 requestScreenshot,
types/index.ts
....@@ -120,6 +120,18 @@
120120 status: string;
121121 }
122122
123
+export interface PaiProject {
124
+ name: string;
125
+ slug: string;
126
+ path: string;
127
+ sessions: number;
128
+}
129
+
130
+export interface WsIncomingProjects {
131
+ type: "projects";
132
+ projects: PaiProject[];
133
+}
134
+
123135 export type WsIncoming =
124136 | WsIncomingText
125137 | WsIncomingVoice
....@@ -130,4 +142,5 @@
130142 | WsIncomingTranscript
131143 | WsIncomingTyping
132144 | WsIncomingError
133
- | WsIncomingStatus;
145
+ | WsIncomingStatus
146
+ | WsIncomingProjects;