From af1543135d42adc2e97dc5243aeef7418cd3b00d Mon Sep 17 00:00:00 2001
From: Matthias Nott <mnott@mnsoft.org>
Date: Sat, 07 Mar 2026 08:39:26 +0100
Subject: [PATCH] feat: dual address auto-switch, custom icon, notifications, image support

---
 components/SessionPicker.tsx |  626 ++++++++++++++++++++++++++++++++++++--------------------
 1 files changed, 398 insertions(+), 228 deletions(-)

diff --git a/components/SessionPicker.tsx b/components/SessionPicker.tsx
index eedb209..349d47a 100644
--- a/components/SessionPicker.tsx
+++ b/components/SessionPicker.tsx
@@ -1,289 +1,459 @@
-import React, { useCallback, useEffect, useState } from "react";
+import React, { useCallback, useEffect, useRef, useState } from "react";
 import {
+  Animated,
+  Keyboard,
+  LayoutAnimation,
   Modal,
+  Platform,
   Pressable,
   ScrollView,
   Text,
   TextInput,
+  UIManager,
   View,
 } from "react-native";
+import {
+  GestureHandlerRootView,
+  PanGestureHandler,
+  PanGestureHandlerGestureEvent,
+  State,
+  Swipeable,
+} from "react-native-gesture-handler";
 import * as Haptics from "expo-haptics";
 import { WsSession } from "../types";
 import { useChat } from "../contexts/ChatContext";
+
+if (
+  Platform.OS === "android" &&
+  UIManager.setLayoutAnimationEnabledExperimental
+) {
+  UIManager.setLayoutAnimationEnabledExperimental(true);
+}
 
 interface SessionPickerProps {
   visible: boolean;
   onClose: () => void;
 }
 
+/* ── Swipeable row with delete action ── */
+
+function SessionRow({
+  session,
+  onSwitch,
+  onLongPress,
+  onDelete,
+}: {
+  session: WsSession;
+  onSwitch: () => void;
+  onLongPress: () => void;
+  onDelete: () => void;
+}) {
+  const swipeRef = useRef<Swipeable>(null);
+
+  const renderRightActions = (
+    _progress: Animated.AnimatedInterpolation<number>,
+    dragX: Animated.AnimatedInterpolation<number>,
+  ) => {
+    const scale = dragX.interpolate({
+      inputRange: [-100, -50, 0],
+      outputRange: [1, 0.8, 0],
+      extrapolate: "clamp",
+    });
+
+    return (
+      <Pressable
+        onPress={() => {
+          swipeRef.current?.close();
+          onDelete();
+        }}
+        style={{
+          backgroundColor: "#FF3B30",
+          justifyContent: "center",
+          alignItems: "center",
+          width: 80,
+          borderRadius: 16,
+          marginLeft: 8,
+        }}
+      >
+        <Animated.Text
+          style={{
+            color: "#FFF",
+            fontSize: 14,
+            fontWeight: "600",
+            transform: [{ scale }],
+          }}
+        >
+          Remove
+        </Animated.Text>
+      </Pressable>
+    );
+  };
+
+  return (
+    <Swipeable
+      ref={swipeRef}
+      renderRightActions={renderRightActions}
+      rightThreshold={60}
+      friction={2}
+      overshootRight={false}
+    >
+      <Pressable
+        onPress={onSwitch}
+        onLongPress={onLongPress}
+        delayLongPress={400}
+        style={({ pressed }) => ({
+          width: "100%",
+          backgroundColor: pressed ? "#252538" : "#1E1E2E",
+          borderRadius: 16,
+          padding: 14,
+          flexDirection: "row",
+          alignItems: "center",
+          borderWidth: session.isActive ? 2 : 1,
+          borderColor: session.isActive ? "#4A9EFF" : "#2E2E45",
+        })}
+      >
+        {/* Number badge */}
+        <View
+          style={{
+            width: 32,
+            height: 32,
+            borderRadius: 16,
+            backgroundColor: session.isActive ? "#4A9EFF" : "#252538",
+            alignItems: "center",
+            justifyContent: "center",
+            marginRight: 12,
+          }}
+        >
+          <Text
+            style={{
+              color: session.isActive ? "#FFF" : "#9898B0",
+              fontSize: 14,
+              fontWeight: "700",
+            }}
+          >
+            {session.index}
+          </Text>
+        </View>
+
+        {/* Session info */}
+        <View style={{ flex: 1 }}>
+          <Text
+            style={{
+              color: "#E8E8F0",
+              fontSize: 16,
+              fontWeight: "600",
+            }}
+            numberOfLines={1}
+          >
+            {session.name}
+          </Text>
+          <Text
+            style={{
+              color: "#5A5A78",
+              fontSize: 11,
+              marginTop: 1,
+            }}
+          >
+            {session.kind === "api"
+              ? "Headless"
+              : session.kind === "visual"
+                ? "Visual"
+                : session.type === "terminal"
+                  ? "Terminal"
+                  : "Claude"}
+            {session.isActive ? " — active" : ""}
+          </Text>
+        </View>
+
+        {/* Active indicator */}
+        {session.isActive && (
+          <View
+            style={{
+              width: 8,
+              height: 8,
+              borderRadius: 4,
+              backgroundColor: "#2ED573",
+            }}
+          />
+        )}
+      </Pressable>
+    </Swipeable>
+  );
+}
+
+/* ── Inline rename editor ── */
+
+function RenameEditor({
+  name,
+  onConfirm,
+  onCancel,
+}: {
+  name: string;
+  onConfirm: (newName: string) => void;
+  onCancel: () => void;
+}) {
+  const [editName, setEditName] = useState(name);
+
+  return (
+    <View
+      style={{
+        backgroundColor: "#1E1E2E",
+        borderRadius: 16,
+        padding: 14,
+        borderWidth: 2,
+        borderColor: "#4A9EFF",
+      }}
+    >
+      <TextInput
+        value={editName}
+        onChangeText={setEditName}
+        autoFocus
+        onSubmitEditing={() => onConfirm(editName.trim())}
+        onBlur={onCancel}
+        returnKeyType="done"
+        style={{
+          color: "#E8E8F0",
+          fontSize: 16,
+          fontWeight: "600",
+          padding: 0,
+          marginBottom: 10,
+        }}
+        placeholderTextColor="#5A5A78"
+        placeholder="Session name..."
+      />
+      <View style={{ flexDirection: "row", gap: 8 }}>
+        <Pressable
+          onPress={() => onConfirm(editName.trim())}
+          style={{
+            flex: 1,
+            backgroundColor: "#4A9EFF",
+            borderRadius: 10,
+            paddingVertical: 8,
+            alignItems: "center",
+          }}
+        >
+          <Text style={{ color: "#FFF", fontSize: 14, fontWeight: "600" }}>
+            Save
+          </Text>
+        </Pressable>
+        <Pressable
+          onPress={onCancel}
+          style={{
+            flex: 1,
+            backgroundColor: "#252538",
+            borderRadius: 10,
+            paddingVertical: 8,
+            alignItems: "center",
+          }}
+        >
+          <Text style={{ color: "#9898B0", fontSize: 14 }}>Cancel</Text>
+        </Pressable>
+      </View>
+    </View>
+  );
+}
+
+/* ── Main SessionPicker ── */
+
 export function SessionPicker({ visible, onClose }: SessionPickerProps) {
-  const { sessions, requestSessions, switchSession, renameSession } = useChat();
+  const {
+    sessions,
+    requestSessions,
+    switchSession,
+    renameSession,
+    removeSession,
+  } = useChat();
   const [editingId, setEditingId] = useState<string | null>(null);
-  const [editName, setEditName] = useState("");
+  const [keyboardHeight, setKeyboardHeight] = useState(0);
+
+  // Sort: active first, then by index
+  const sortedSessions = [...sessions].sort((a, b) => {
+    if (a.isActive && !b.isActive) return -1;
+    if (!a.isActive && b.isActive) return 1;
+    return a.index - b.index;
+  });
 
   useEffect(() => {
-    if (visible) {
+    const showSub = Keyboard.addListener("keyboardWillShow", (e) =>
+      setKeyboardHeight(e.endCoordinates.height),
+    );
+    const hideSub = Keyboard.addListener("keyboardWillHide", () =>
+      setKeyboardHeight(0),
+    );
+    return () => {
+      showSub.remove();
+      hideSub.remove();
+    };
+  }, []);
+
+  useEffect(() => {
+    if (!visible) {
+      setEditingId(null);
+    } else {
       requestSessions();
     }
   }, [visible, requestSessions]);
+
+  const handleClose = useCallback(() => {
+    setEditingId(null);
+    Keyboard.dismiss();
+    onClose();
+  }, [onClose]);
 
   const handleSwitch = useCallback(
     (session: WsSession) => {
       Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
       switchSession(session.id);
-      onClose();
+      handleClose();
     },
-    [switchSession, onClose]
+    [switchSession, handleClose],
   );
 
   const handleStartRename = useCallback((session: WsSession) => {
+    Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
     setEditingId(session.id);
-    setEditName(session.name);
   }, []);
 
-  const handleConfirmRename = useCallback(() => {
-    if (editingId && editName.trim()) {
-      renameSession(editingId, editName.trim());
-    }
-    setEditingId(null);
-    setEditName("");
-  }, [editingId, editName, renameSession]);
+  const handleConfirmRename = useCallback(
+    (sessionId: string, newName: string) => {
+      if (newName) {
+        renameSession(sessionId, newName);
+      }
+      setEditingId(null);
+    },
+    [renameSession],
+  );
+
+  const handleRemove = useCallback(
+    (session: WsSession) => {
+      Haptics.notificationAsync(Haptics.NotificationFeedbackType.Warning);
+      LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
+      removeSession(session.id);
+    },
+    [removeSession],
+  );
 
   return (
     <Modal
       visible={visible}
       animationType="slide"
       transparent
-      onRequestClose={onClose}
+      onRequestClose={handleClose}
     >
-      <View
-        style={{
-          flex: 1,
-          backgroundColor: "rgba(0,0,0,0.6)",
-          justifyContent: "flex-end",
-        }}
-      >
-        <Pressable
-          style={{ flex: 1 }}
-          onPress={onClose}
-        />
+      <GestureHandlerRootView style={{ flex: 1 }}>
         <View
           style={{
-            backgroundColor: "#14141F",
-            borderTopLeftRadius: 24,
-            borderTopRightRadius: 24,
-            maxHeight: "70%",
-            paddingBottom: 40,
+            flex: 1,
+            backgroundColor: "rgba(0,0,0,0.6)",
+            justifyContent: "flex-end",
           }}
         >
-          {/* Handle bar */}
-          <View style={{ alignItems: "center", paddingTop: 12, paddingBottom: 8 }}>
-            <View
-              style={{
-                width: 40,
-                height: 4,
-                borderRadius: 2,
-                backgroundColor: "#2E2E45",
-              }}
-            />
-          </View>
-
-          {/* Header */}
+          <Pressable style={{ flex: 1 }} onPress={handleClose} />
           <View
             style={{
-              flexDirection: "row",
-              alignItems: "center",
-              justifyContent: "space-between",
-              paddingHorizontal: 20,
-              paddingBottom: 16,
+              backgroundColor: "#14141F",
+              borderTopLeftRadius: 24,
+              borderTopRightRadius: 24,
+              maxHeight: "70%",
+              paddingBottom: Math.max(40, keyboardHeight),
             }}
           >
-            <Text
+            {/* Handle bar */}
+            <View
+              style={{ alignItems: "center", paddingTop: 12, paddingBottom: 8 }}
+            >
+              <View
+                style={{
+                  width: 40,
+                  height: 4,
+                  borderRadius: 2,
+                  backgroundColor: "#2E2E45",
+                }}
+              />
+            </View>
+
+            {/* Header */}
+            <View
               style={{
-                color: "#E8E8F0",
-                fontSize: 20,
-                fontWeight: "700",
+                flexDirection: "row",
+                alignItems: "center",
+                justifyContent: "space-between",
+                paddingHorizontal: 20,
+                paddingBottom: 12,
               }}
             >
-              Sessions
-            </Text>
-            <Pressable
-              onPress={() => requestSessions()}
-              hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
-              style={{
-                paddingHorizontal: 12,
-                paddingVertical: 6,
-                borderRadius: 12,
-                backgroundColor: "#1E1E2E",
-              }}
+              <Text
+                style={{
+                  color: "#E8E8F0",
+                  fontSize: 20,
+                  fontWeight: "700",
+                }}
+              >
+                Sessions
+              </Text>
+              <Pressable
+                onPress={() => requestSessions()}
+                hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
+                style={({ pressed }) => ({
+                  paddingHorizontal: 12,
+                  paddingVertical: 6,
+                  borderRadius: 12,
+                  backgroundColor: pressed ? "#252538" : "#1E1E2E",
+                })}
+              >
+                <Text style={{ color: "#9898B0", fontSize: 13 }}>Refresh</Text>
+              </Pressable>
+            </View>
+
+            {/* Session list */}
+            <ScrollView
+              style={{ paddingHorizontal: 16 }}
+              showsVerticalScrollIndicator={false}
+              keyboardShouldPersistTaps="handled"
             >
-              <Text style={{ color: "#9898B0", fontSize: 13 }}>Refresh</Text>
-            </Pressable>
-          </View>
-
-          {/* Session list */}
-          <ScrollView
-            style={{ paddingHorizontal: 16 }}
-            showsVerticalScrollIndicator={false}
-          >
-            {sessions.length === 0 ? (
-              <View style={{ alignItems: "center", paddingVertical: 32 }}>
-                <Text style={{ color: "#5A5A78", fontSize: 15 }}>
-                  No sessions found
-                </Text>
-              </View>
-            ) : (
-              sessions.map((session) => (
-                <View key={session.id} style={{ marginBottom: 8 }}>
-                  {editingId === session.id ? (
-                    /* Rename mode */
-                    <View
-                      style={{
-                        backgroundColor: "#1E1E2E",
-                        borderRadius: 16,
-                        padding: 16,
-                        borderWidth: 2,
-                        borderColor: "#4A9EFF",
-                      }}
-                    >
-                      <TextInput
-                        value={editName}
-                        onChangeText={setEditName}
-                        autoFocus
-                        onSubmitEditing={handleConfirmRename}
-                        returnKeyType="done"
-                        style={{
-                          color: "#E8E8F0",
-                          fontSize: 17,
-                          fontWeight: "600",
-                          padding: 0,
-                          marginBottom: 12,
-                        }}
-                        placeholderTextColor="#5A5A78"
-                        placeholder="Session name..."
-                      />
-                      <View style={{ flexDirection: "row", gap: 8 }}>
-                        <Pressable
-                          onPress={handleConfirmRename}
-                          style={{
-                            flex: 1,
-                            backgroundColor: "#4A9EFF",
-                            borderRadius: 10,
-                            paddingVertical: 10,
-                            alignItems: "center",
-                          }}
-                        >
-                          <Text style={{ color: "#FFF", fontSize: 15, fontWeight: "600" }}>
-                            Save
-                          </Text>
-                        </Pressable>
-                        <Pressable
-                          onPress={() => setEditingId(null)}
-                          style={{
-                            flex: 1,
-                            backgroundColor: "#252538",
-                            borderRadius: 10,
-                            paddingVertical: 10,
-                            alignItems: "center",
-                          }}
-                        >
-                          <Text style={{ color: "#9898B0", fontSize: 15 }}>Cancel</Text>
-                        </Pressable>
-                      </View>
-                    </View>
-                  ) : (
-                    /* Normal session row */
-                    <Pressable
-                      onPress={() => handleSwitch(session)}
-                      onLongPress={() => handleStartRename(session)}
-                      style={({ pressed }) => ({
-                        backgroundColor: pressed ? "#252538" : "#1E1E2E",
-                        borderRadius: 16,
-                        padding: 16,
-                        flexDirection: "row",
-                        alignItems: "center",
-                        borderWidth: session.isActive ? 2 : 1,
-                        borderColor: session.isActive ? "#4A9EFF" : "#2E2E45",
-                      })}
-                    >
-                      {/* Number badge */}
-                      <View
-                        style={{
-                          width: 36,
-                          height: 36,
-                          borderRadius: 18,
-                          backgroundColor: session.isActive ? "#4A9EFF" : "#252538",
-                          alignItems: "center",
-                          justifyContent: "center",
-                          marginRight: 14,
-                        }}
-                      >
-                        <Text
-                          style={{
-                            color: session.isActive ? "#FFF" : "#9898B0",
-                            fontSize: 16,
-                            fontWeight: "700",
-                          }}
-                        >
-                          {session.index}
-                        </Text>
-                      </View>
-
-                      {/* Session info */}
-                      <View style={{ flex: 1 }}>
-                        <Text
-                          style={{
-                            color: "#E8E8F0",
-                            fontSize: 17,
-                            fontWeight: "600",
-                          }}
-                          numberOfLines={1}
-                        >
-                          {session.name}
-                        </Text>
-                        <Text
-                          style={{
-                            color: "#5A5A78",
-                            fontSize: 12,
-                            marginTop: 2,
-                          }}
-                        >
-                          {session.kind === "api" ? "Headless" : session.kind === "visual" ? "Visual" : session.type === "terminal" ? "Terminal" : "Claude"}
-                          {session.isActive ? " — active" : ""}
-                        </Text>
-                      </View>
-
-                      {/* Active indicator */}
-                      {session.isActive && (
-                        <View
-                          style={{
-                            width: 10,
-                            height: 10,
-                            borderRadius: 5,
-                            backgroundColor: "#2ED573",
-                          }}
-                        />
-                      )}
-                    </Pressable>
-                  )}
+              {sortedSessions.length === 0 ? (
+                <View style={{ alignItems: "center", paddingVertical: 32 }}>
+                  <Text style={{ color: "#5A5A78", fontSize: 15 }}>
+                    No sessions found
+                  </Text>
                 </View>
-              ))
-            )}
+              ) : (
+                sortedSessions.map((session) => (
+                  <View key={session.id} style={{ marginBottom: 6 }}>
+                    {editingId === session.id ? (
+                      <RenameEditor
+                        name={session.name}
+                        onConfirm={(name) =>
+                          handleConfirmRename(session.id, name)
+                        }
+                        onCancel={() => setEditingId(null)}
+                      />
+                    ) : (
+                      <SessionRow
+                        session={session}
+                        onSwitch={() => handleSwitch(session)}
+                        onLongPress={() => handleStartRename(session)}
+                        onDelete={() => handleRemove(session)}
+                      />
+                    )}
+                  </View>
+                ))
+              )}
 
-            {/* Hint */}
-            <Text
-              style={{
-                color: "#5A5A78",
-                fontSize: 12,
-                textAlign: "center",
-                paddingVertical: 12,
-              }}
-            >
-              Tap to switch — Long press to rename
-            </Text>
-          </ScrollView>
+              <Text
+                style={{
+                  color: "#5A5A78",
+                  fontSize: 11,
+                  textAlign: "center",
+                  paddingVertical: 10,
+                }}
+              >
+                Tap to switch — Long press to rename — Swipe left to remove
+              </Text>
+            </ScrollView>
+          </View>
         </View>
-      </View>
+      </GestureHandlerRootView>
     </Modal>
   );
 }

--
Gitblit v1.3.1