| .. | .. |
|---|
| 1 | | -import React, { useCallback, useEffect, useState } from "react"; |
|---|
| 1 | +import React, { useCallback, useEffect, useRef, useState } from "react"; |
|---|
| 2 | 2 | import { |
|---|
| 3 | + Animated, |
|---|
| 4 | + Keyboard, |
|---|
| 5 | + LayoutAnimation, |
|---|
| 3 | 6 | Modal, |
|---|
| 7 | + Platform, |
|---|
| 4 | 8 | Pressable, |
|---|
| 5 | 9 | ScrollView, |
|---|
| 6 | 10 | Text, |
|---|
| 7 | 11 | TextInput, |
|---|
| 12 | + UIManager, |
|---|
| 8 | 13 | View, |
|---|
| 9 | 14 | } from "react-native"; |
|---|
| 15 | +import { |
|---|
| 16 | + GestureHandlerRootView, |
|---|
| 17 | + PanGestureHandler, |
|---|
| 18 | + PanGestureHandlerGestureEvent, |
|---|
| 19 | + State, |
|---|
| 20 | + Swipeable, |
|---|
| 21 | +} from "react-native-gesture-handler"; |
|---|
| 10 | 22 | import * as Haptics from "expo-haptics"; |
|---|
| 11 | 23 | import { WsSession } from "../types"; |
|---|
| 12 | 24 | import { useChat } from "../contexts/ChatContext"; |
|---|
| 25 | + |
|---|
| 26 | +if ( |
|---|
| 27 | + Platform.OS === "android" && |
|---|
| 28 | + UIManager.setLayoutAnimationEnabledExperimental |
|---|
| 29 | +) { |
|---|
| 30 | + UIManager.setLayoutAnimationEnabledExperimental(true); |
|---|
| 31 | +} |
|---|
| 13 | 32 | |
|---|
| 14 | 33 | interface SessionPickerProps { |
|---|
| 15 | 34 | visible: boolean; |
|---|
| 16 | 35 | onClose: () => void; |
|---|
| 17 | 36 | } |
|---|
| 18 | 37 | |
|---|
| 38 | +/* ── Swipeable row with delete action ── */ |
|---|
| 39 | + |
|---|
| 40 | +function SessionRow({ |
|---|
| 41 | + session, |
|---|
| 42 | + onSwitch, |
|---|
| 43 | + onLongPress, |
|---|
| 44 | + onDelete, |
|---|
| 45 | +}: { |
|---|
| 46 | + session: WsSession; |
|---|
| 47 | + onSwitch: () => void; |
|---|
| 48 | + onLongPress: () => void; |
|---|
| 49 | + onDelete: () => void; |
|---|
| 50 | +}) { |
|---|
| 51 | + const swipeRef = useRef<Swipeable>(null); |
|---|
| 52 | + |
|---|
| 53 | + const renderRightActions = ( |
|---|
| 54 | + _progress: Animated.AnimatedInterpolation<number>, |
|---|
| 55 | + dragX: Animated.AnimatedInterpolation<number>, |
|---|
| 56 | + ) => { |
|---|
| 57 | + const scale = dragX.interpolate({ |
|---|
| 58 | + inputRange: [-100, -50, 0], |
|---|
| 59 | + outputRange: [1, 0.8, 0], |
|---|
| 60 | + extrapolate: "clamp", |
|---|
| 61 | + }); |
|---|
| 62 | + |
|---|
| 63 | + return ( |
|---|
| 64 | + <Pressable |
|---|
| 65 | + onPress={() => { |
|---|
| 66 | + swipeRef.current?.close(); |
|---|
| 67 | + onDelete(); |
|---|
| 68 | + }} |
|---|
| 69 | + style={{ |
|---|
| 70 | + backgroundColor: "#FF3B30", |
|---|
| 71 | + justifyContent: "center", |
|---|
| 72 | + alignItems: "center", |
|---|
| 73 | + width: 80, |
|---|
| 74 | + borderRadius: 16, |
|---|
| 75 | + marginLeft: 8, |
|---|
| 76 | + }} |
|---|
| 77 | + > |
|---|
| 78 | + <Animated.Text |
|---|
| 79 | + style={{ |
|---|
| 80 | + color: "#FFF", |
|---|
| 81 | + fontSize: 14, |
|---|
| 82 | + fontWeight: "600", |
|---|
| 83 | + transform: [{ scale }], |
|---|
| 84 | + }} |
|---|
| 85 | + > |
|---|
| 86 | + Remove |
|---|
| 87 | + </Animated.Text> |
|---|
| 88 | + </Pressable> |
|---|
| 89 | + ); |
|---|
| 90 | + }; |
|---|
| 91 | + |
|---|
| 92 | + return ( |
|---|
| 93 | + <Swipeable |
|---|
| 94 | + ref={swipeRef} |
|---|
| 95 | + renderRightActions={renderRightActions} |
|---|
| 96 | + rightThreshold={60} |
|---|
| 97 | + friction={2} |
|---|
| 98 | + overshootRight={false} |
|---|
| 99 | + > |
|---|
| 100 | + <Pressable |
|---|
| 101 | + onPress={onSwitch} |
|---|
| 102 | + onLongPress={onLongPress} |
|---|
| 103 | + delayLongPress={400} |
|---|
| 104 | + style={({ pressed }) => ({ |
|---|
| 105 | + width: "100%", |
|---|
| 106 | + backgroundColor: pressed ? "#252538" : "#1E1E2E", |
|---|
| 107 | + borderRadius: 16, |
|---|
| 108 | + padding: 14, |
|---|
| 109 | + flexDirection: "row", |
|---|
| 110 | + alignItems: "center", |
|---|
| 111 | + borderWidth: session.isActive ? 2 : 1, |
|---|
| 112 | + borderColor: session.isActive ? "#4A9EFF" : "#2E2E45", |
|---|
| 113 | + })} |
|---|
| 114 | + > |
|---|
| 115 | + {/* Number badge */} |
|---|
| 116 | + <View |
|---|
| 117 | + style={{ |
|---|
| 118 | + width: 32, |
|---|
| 119 | + height: 32, |
|---|
| 120 | + borderRadius: 16, |
|---|
| 121 | + backgroundColor: session.isActive ? "#4A9EFF" : "#252538", |
|---|
| 122 | + alignItems: "center", |
|---|
| 123 | + justifyContent: "center", |
|---|
| 124 | + marginRight: 12, |
|---|
| 125 | + }} |
|---|
| 126 | + > |
|---|
| 127 | + <Text |
|---|
| 128 | + style={{ |
|---|
| 129 | + color: session.isActive ? "#FFF" : "#9898B0", |
|---|
| 130 | + fontSize: 14, |
|---|
| 131 | + fontWeight: "700", |
|---|
| 132 | + }} |
|---|
| 133 | + > |
|---|
| 134 | + {session.index} |
|---|
| 135 | + </Text> |
|---|
| 136 | + </View> |
|---|
| 137 | + |
|---|
| 138 | + {/* Session info */} |
|---|
| 139 | + <View style={{ flex: 1 }}> |
|---|
| 140 | + <Text |
|---|
| 141 | + style={{ |
|---|
| 142 | + color: "#E8E8F0", |
|---|
| 143 | + fontSize: 16, |
|---|
| 144 | + fontWeight: "600", |
|---|
| 145 | + }} |
|---|
| 146 | + numberOfLines={1} |
|---|
| 147 | + > |
|---|
| 148 | + {session.name} |
|---|
| 149 | + </Text> |
|---|
| 150 | + <Text |
|---|
| 151 | + style={{ |
|---|
| 152 | + color: "#5A5A78", |
|---|
| 153 | + fontSize: 11, |
|---|
| 154 | + marginTop: 1, |
|---|
| 155 | + }} |
|---|
| 156 | + > |
|---|
| 157 | + {session.kind === "api" |
|---|
| 158 | + ? "Headless" |
|---|
| 159 | + : session.kind === "visual" |
|---|
| 160 | + ? "Visual" |
|---|
| 161 | + : session.type === "terminal" |
|---|
| 162 | + ? "Terminal" |
|---|
| 163 | + : "Claude"} |
|---|
| 164 | + {session.isActive ? " — active" : ""} |
|---|
| 165 | + </Text> |
|---|
| 166 | + </View> |
|---|
| 167 | + |
|---|
| 168 | + {/* Active indicator */} |
|---|
| 169 | + {session.isActive && ( |
|---|
| 170 | + <View |
|---|
| 171 | + style={{ |
|---|
| 172 | + width: 8, |
|---|
| 173 | + height: 8, |
|---|
| 174 | + borderRadius: 4, |
|---|
| 175 | + backgroundColor: "#2ED573", |
|---|
| 176 | + }} |
|---|
| 177 | + /> |
|---|
| 178 | + )} |
|---|
| 179 | + </Pressable> |
|---|
| 180 | + </Swipeable> |
|---|
| 181 | + ); |
|---|
| 182 | +} |
|---|
| 183 | + |
|---|
| 184 | +/* ── Inline rename editor ── */ |
|---|
| 185 | + |
|---|
| 186 | +function RenameEditor({ |
|---|
| 187 | + name, |
|---|
| 188 | + onConfirm, |
|---|
| 189 | + onCancel, |
|---|
| 190 | +}: { |
|---|
| 191 | + name: string; |
|---|
| 192 | + onConfirm: (newName: string) => void; |
|---|
| 193 | + onCancel: () => void; |
|---|
| 194 | +}) { |
|---|
| 195 | + const [editName, setEditName] = useState(name); |
|---|
| 196 | + |
|---|
| 197 | + return ( |
|---|
| 198 | + <View |
|---|
| 199 | + style={{ |
|---|
| 200 | + backgroundColor: "#1E1E2E", |
|---|
| 201 | + borderRadius: 16, |
|---|
| 202 | + padding: 14, |
|---|
| 203 | + borderWidth: 2, |
|---|
| 204 | + borderColor: "#4A9EFF", |
|---|
| 205 | + }} |
|---|
| 206 | + > |
|---|
| 207 | + <TextInput |
|---|
| 208 | + value={editName} |
|---|
| 209 | + onChangeText={setEditName} |
|---|
| 210 | + autoFocus |
|---|
| 211 | + onSubmitEditing={() => onConfirm(editName.trim())} |
|---|
| 212 | + onBlur={onCancel} |
|---|
| 213 | + returnKeyType="done" |
|---|
| 214 | + style={{ |
|---|
| 215 | + color: "#E8E8F0", |
|---|
| 216 | + fontSize: 16, |
|---|
| 217 | + fontWeight: "600", |
|---|
| 218 | + padding: 0, |
|---|
| 219 | + marginBottom: 10, |
|---|
| 220 | + }} |
|---|
| 221 | + placeholderTextColor="#5A5A78" |
|---|
| 222 | + placeholder="Session name..." |
|---|
| 223 | + /> |
|---|
| 224 | + <View style={{ flexDirection: "row", gap: 8 }}> |
|---|
| 225 | + <Pressable |
|---|
| 226 | + onPress={() => onConfirm(editName.trim())} |
|---|
| 227 | + style={{ |
|---|
| 228 | + flex: 1, |
|---|
| 229 | + backgroundColor: "#4A9EFF", |
|---|
| 230 | + borderRadius: 10, |
|---|
| 231 | + paddingVertical: 8, |
|---|
| 232 | + alignItems: "center", |
|---|
| 233 | + }} |
|---|
| 234 | + > |
|---|
| 235 | + <Text style={{ color: "#FFF", fontSize: 14, fontWeight: "600" }}> |
|---|
| 236 | + Save |
|---|
| 237 | + </Text> |
|---|
| 238 | + </Pressable> |
|---|
| 239 | + <Pressable |
|---|
| 240 | + onPress={onCancel} |
|---|
| 241 | + style={{ |
|---|
| 242 | + flex: 1, |
|---|
| 243 | + backgroundColor: "#252538", |
|---|
| 244 | + borderRadius: 10, |
|---|
| 245 | + paddingVertical: 8, |
|---|
| 246 | + alignItems: "center", |
|---|
| 247 | + }} |
|---|
| 248 | + > |
|---|
| 249 | + <Text style={{ color: "#9898B0", fontSize: 14 }}>Cancel</Text> |
|---|
| 250 | + </Pressable> |
|---|
| 251 | + </View> |
|---|
| 252 | + </View> |
|---|
| 253 | + ); |
|---|
| 254 | +} |
|---|
| 255 | + |
|---|
| 256 | +/* ── Main SessionPicker ── */ |
|---|
| 257 | + |
|---|
| 19 | 258 | export function SessionPicker({ visible, onClose }: SessionPickerProps) { |
|---|
| 20 | | - const { sessions, requestSessions, switchSession, renameSession } = useChat(); |
|---|
| 259 | + const { |
|---|
| 260 | + sessions, |
|---|
| 261 | + requestSessions, |
|---|
| 262 | + switchSession, |
|---|
| 263 | + renameSession, |
|---|
| 264 | + removeSession, |
|---|
| 265 | + } = useChat(); |
|---|
| 21 | 266 | const [editingId, setEditingId] = useState<string | null>(null); |
|---|
| 22 | | - const [editName, setEditName] = useState(""); |
|---|
| 267 | + const [keyboardHeight, setKeyboardHeight] = useState(0); |
|---|
| 268 | + |
|---|
| 269 | + // Sort: active first, then by index |
|---|
| 270 | + const sortedSessions = [...sessions].sort((a, b) => { |
|---|
| 271 | + if (a.isActive && !b.isActive) return -1; |
|---|
| 272 | + if (!a.isActive && b.isActive) return 1; |
|---|
| 273 | + return a.index - b.index; |
|---|
| 274 | + }); |
|---|
| 23 | 275 | |
|---|
| 24 | 276 | useEffect(() => { |
|---|
| 25 | | - if (visible) { |
|---|
| 277 | + const showSub = Keyboard.addListener("keyboardWillShow", (e) => |
|---|
| 278 | + setKeyboardHeight(e.endCoordinates.height), |
|---|
| 279 | + ); |
|---|
| 280 | + const hideSub = Keyboard.addListener("keyboardWillHide", () => |
|---|
| 281 | + setKeyboardHeight(0), |
|---|
| 282 | + ); |
|---|
| 283 | + return () => { |
|---|
| 284 | + showSub.remove(); |
|---|
| 285 | + hideSub.remove(); |
|---|
| 286 | + }; |
|---|
| 287 | + }, []); |
|---|
| 288 | + |
|---|
| 289 | + useEffect(() => { |
|---|
| 290 | + if (!visible) { |
|---|
| 291 | + setEditingId(null); |
|---|
| 292 | + } else { |
|---|
| 26 | 293 | requestSessions(); |
|---|
| 27 | 294 | } |
|---|
| 28 | 295 | }, [visible, requestSessions]); |
|---|
| 296 | + |
|---|
| 297 | + const handleClose = useCallback(() => { |
|---|
| 298 | + setEditingId(null); |
|---|
| 299 | + Keyboard.dismiss(); |
|---|
| 300 | + onClose(); |
|---|
| 301 | + }, [onClose]); |
|---|
| 29 | 302 | |
|---|
| 30 | 303 | const handleSwitch = useCallback( |
|---|
| 31 | 304 | (session: WsSession) => { |
|---|
| 32 | 305 | Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium); |
|---|
| 33 | 306 | switchSession(session.id); |
|---|
| 34 | | - onClose(); |
|---|
| 307 | + handleClose(); |
|---|
| 35 | 308 | }, |
|---|
| 36 | | - [switchSession, onClose] |
|---|
| 309 | + [switchSession, handleClose], |
|---|
| 37 | 310 | ); |
|---|
| 38 | 311 | |
|---|
| 39 | 312 | const handleStartRename = useCallback((session: WsSession) => { |
|---|
| 313 | + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); |
|---|
| 40 | 314 | setEditingId(session.id); |
|---|
| 41 | | - setEditName(session.name); |
|---|
| 42 | 315 | }, []); |
|---|
| 43 | 316 | |
|---|
| 44 | | - const handleConfirmRename = useCallback(() => { |
|---|
| 45 | | - if (editingId && editName.trim()) { |
|---|
| 46 | | - renameSession(editingId, editName.trim()); |
|---|
| 47 | | - } |
|---|
| 48 | | - setEditingId(null); |
|---|
| 49 | | - setEditName(""); |
|---|
| 50 | | - }, [editingId, editName, renameSession]); |
|---|
| 317 | + const handleConfirmRename = useCallback( |
|---|
| 318 | + (sessionId: string, newName: string) => { |
|---|
| 319 | + if (newName) { |
|---|
| 320 | + renameSession(sessionId, newName); |
|---|
| 321 | + } |
|---|
| 322 | + setEditingId(null); |
|---|
| 323 | + }, |
|---|
| 324 | + [renameSession], |
|---|
| 325 | + ); |
|---|
| 326 | + |
|---|
| 327 | + const handleRemove = useCallback( |
|---|
| 328 | + (session: WsSession) => { |
|---|
| 329 | + Haptics.notificationAsync(Haptics.NotificationFeedbackType.Warning); |
|---|
| 330 | + LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); |
|---|
| 331 | + removeSession(session.id); |
|---|
| 332 | + }, |
|---|
| 333 | + [removeSession], |
|---|
| 334 | + ); |
|---|
| 51 | 335 | |
|---|
| 52 | 336 | return ( |
|---|
| 53 | 337 | <Modal |
|---|
| 54 | 338 | visible={visible} |
|---|
| 55 | 339 | animationType="slide" |
|---|
| 56 | 340 | transparent |
|---|
| 57 | | - onRequestClose={onClose} |
|---|
| 341 | + onRequestClose={handleClose} |
|---|
| 58 | 342 | > |
|---|
| 59 | | - <View |
|---|
| 60 | | - style={{ |
|---|
| 61 | | - flex: 1, |
|---|
| 62 | | - backgroundColor: "rgba(0,0,0,0.6)", |
|---|
| 63 | | - justifyContent: "flex-end", |
|---|
| 64 | | - }} |
|---|
| 65 | | - > |
|---|
| 66 | | - <Pressable |
|---|
| 67 | | - style={{ flex: 1 }} |
|---|
| 68 | | - onPress={onClose} |
|---|
| 69 | | - /> |
|---|
| 343 | + <GestureHandlerRootView style={{ flex: 1 }}> |
|---|
| 70 | 344 | <View |
|---|
| 71 | 345 | style={{ |
|---|
| 72 | | - backgroundColor: "#14141F", |
|---|
| 73 | | - borderTopLeftRadius: 24, |
|---|
| 74 | | - borderTopRightRadius: 24, |
|---|
| 75 | | - maxHeight: "70%", |
|---|
| 76 | | - paddingBottom: 40, |
|---|
| 346 | + flex: 1, |
|---|
| 347 | + backgroundColor: "rgba(0,0,0,0.6)", |
|---|
| 348 | + justifyContent: "flex-end", |
|---|
| 77 | 349 | }} |
|---|
| 78 | 350 | > |
|---|
| 79 | | - {/* Handle bar */} |
|---|
| 80 | | - <View style={{ alignItems: "center", paddingTop: 12, paddingBottom: 8 }}> |
|---|
| 81 | | - <View |
|---|
| 82 | | - style={{ |
|---|
| 83 | | - width: 40, |
|---|
| 84 | | - height: 4, |
|---|
| 85 | | - borderRadius: 2, |
|---|
| 86 | | - backgroundColor: "#2E2E45", |
|---|
| 87 | | - }} |
|---|
| 88 | | - /> |
|---|
| 89 | | - </View> |
|---|
| 90 | | - |
|---|
| 91 | | - {/* Header */} |
|---|
| 351 | + <Pressable style={{ flex: 1 }} onPress={handleClose} /> |
|---|
| 92 | 352 | <View |
|---|
| 93 | 353 | style={{ |
|---|
| 94 | | - flexDirection: "row", |
|---|
| 95 | | - alignItems: "center", |
|---|
| 96 | | - justifyContent: "space-between", |
|---|
| 97 | | - paddingHorizontal: 20, |
|---|
| 98 | | - paddingBottom: 16, |
|---|
| 354 | + backgroundColor: "#14141F", |
|---|
| 355 | + borderTopLeftRadius: 24, |
|---|
| 356 | + borderTopRightRadius: 24, |
|---|
| 357 | + maxHeight: "70%", |
|---|
| 358 | + paddingBottom: Math.max(40, keyboardHeight), |
|---|
| 99 | 359 | }} |
|---|
| 100 | 360 | > |
|---|
| 101 | | - <Text |
|---|
| 361 | + {/* Handle bar */} |
|---|
| 362 | + <View |
|---|
| 363 | + style={{ alignItems: "center", paddingTop: 12, paddingBottom: 8 }} |
|---|
| 364 | + > |
|---|
| 365 | + <View |
|---|
| 366 | + style={{ |
|---|
| 367 | + width: 40, |
|---|
| 368 | + height: 4, |
|---|
| 369 | + borderRadius: 2, |
|---|
| 370 | + backgroundColor: "#2E2E45", |
|---|
| 371 | + }} |
|---|
| 372 | + /> |
|---|
| 373 | + </View> |
|---|
| 374 | + |
|---|
| 375 | + {/* Header */} |
|---|
| 376 | + <View |
|---|
| 102 | 377 | style={{ |
|---|
| 103 | | - color: "#E8E8F0", |
|---|
| 104 | | - fontSize: 20, |
|---|
| 105 | | - fontWeight: "700", |
|---|
| 378 | + flexDirection: "row", |
|---|
| 379 | + alignItems: "center", |
|---|
| 380 | + justifyContent: "space-between", |
|---|
| 381 | + paddingHorizontal: 20, |
|---|
| 382 | + paddingBottom: 12, |
|---|
| 106 | 383 | }} |
|---|
| 107 | 384 | > |
|---|
| 108 | | - Sessions |
|---|
| 109 | | - </Text> |
|---|
| 110 | | - <Pressable |
|---|
| 111 | | - onPress={() => requestSessions()} |
|---|
| 112 | | - hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }} |
|---|
| 113 | | - style={{ |
|---|
| 114 | | - paddingHorizontal: 12, |
|---|
| 115 | | - paddingVertical: 6, |
|---|
| 116 | | - borderRadius: 12, |
|---|
| 117 | | - backgroundColor: "#1E1E2E", |
|---|
| 118 | | - }} |
|---|
| 385 | + <Text |
|---|
| 386 | + style={{ |
|---|
| 387 | + color: "#E8E8F0", |
|---|
| 388 | + fontSize: 20, |
|---|
| 389 | + fontWeight: "700", |
|---|
| 390 | + }} |
|---|
| 391 | + > |
|---|
| 392 | + Sessions |
|---|
| 393 | + </Text> |
|---|
| 394 | + <Pressable |
|---|
| 395 | + onPress={() => requestSessions()} |
|---|
| 396 | + hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }} |
|---|
| 397 | + style={({ pressed }) => ({ |
|---|
| 398 | + paddingHorizontal: 12, |
|---|
| 399 | + paddingVertical: 6, |
|---|
| 400 | + borderRadius: 12, |
|---|
| 401 | + backgroundColor: pressed ? "#252538" : "#1E1E2E", |
|---|
| 402 | + })} |
|---|
| 403 | + > |
|---|
| 404 | + <Text style={{ color: "#9898B0", fontSize: 13 }}>Refresh</Text> |
|---|
| 405 | + </Pressable> |
|---|
| 406 | + </View> |
|---|
| 407 | + |
|---|
| 408 | + {/* Session list */} |
|---|
| 409 | + <ScrollView |
|---|
| 410 | + style={{ paddingHorizontal: 16 }} |
|---|
| 411 | + showsVerticalScrollIndicator={false} |
|---|
| 412 | + keyboardShouldPersistTaps="handled" |
|---|
| 119 | 413 | > |
|---|
| 120 | | - <Text style={{ color: "#9898B0", fontSize: 13 }}>Refresh</Text> |
|---|
| 121 | | - </Pressable> |
|---|
| 122 | | - </View> |
|---|
| 123 | | - |
|---|
| 124 | | - {/* Session list */} |
|---|
| 125 | | - <ScrollView |
|---|
| 126 | | - style={{ paddingHorizontal: 16 }} |
|---|
| 127 | | - showsVerticalScrollIndicator={false} |
|---|
| 128 | | - > |
|---|
| 129 | | - {sessions.length === 0 ? ( |
|---|
| 130 | | - <View style={{ alignItems: "center", paddingVertical: 32 }}> |
|---|
| 131 | | - <Text style={{ color: "#5A5A78", fontSize: 15 }}> |
|---|
| 132 | | - No sessions found |
|---|
| 133 | | - </Text> |
|---|
| 134 | | - </View> |
|---|
| 135 | | - ) : ( |
|---|
| 136 | | - sessions.map((session) => ( |
|---|
| 137 | | - <View key={session.id} style={{ marginBottom: 8 }}> |
|---|
| 138 | | - {editingId === session.id ? ( |
|---|
| 139 | | - /* Rename mode */ |
|---|
| 140 | | - <View |
|---|
| 141 | | - style={{ |
|---|
| 142 | | - backgroundColor: "#1E1E2E", |
|---|
| 143 | | - borderRadius: 16, |
|---|
| 144 | | - padding: 16, |
|---|
| 145 | | - borderWidth: 2, |
|---|
| 146 | | - borderColor: "#4A9EFF", |
|---|
| 147 | | - }} |
|---|
| 148 | | - > |
|---|
| 149 | | - <TextInput |
|---|
| 150 | | - value={editName} |
|---|
| 151 | | - onChangeText={setEditName} |
|---|
| 152 | | - autoFocus |
|---|
| 153 | | - onSubmitEditing={handleConfirmRename} |
|---|
| 154 | | - returnKeyType="done" |
|---|
| 155 | | - style={{ |
|---|
| 156 | | - color: "#E8E8F0", |
|---|
| 157 | | - fontSize: 17, |
|---|
| 158 | | - fontWeight: "600", |
|---|
| 159 | | - padding: 0, |
|---|
| 160 | | - marginBottom: 12, |
|---|
| 161 | | - }} |
|---|
| 162 | | - placeholderTextColor="#5A5A78" |
|---|
| 163 | | - placeholder="Session name..." |
|---|
| 164 | | - /> |
|---|
| 165 | | - <View style={{ flexDirection: "row", gap: 8 }}> |
|---|
| 166 | | - <Pressable |
|---|
| 167 | | - onPress={handleConfirmRename} |
|---|
| 168 | | - style={{ |
|---|
| 169 | | - flex: 1, |
|---|
| 170 | | - backgroundColor: "#4A9EFF", |
|---|
| 171 | | - borderRadius: 10, |
|---|
| 172 | | - paddingVertical: 10, |
|---|
| 173 | | - alignItems: "center", |
|---|
| 174 | | - }} |
|---|
| 175 | | - > |
|---|
| 176 | | - <Text style={{ color: "#FFF", fontSize: 15, fontWeight: "600" }}> |
|---|
| 177 | | - Save |
|---|
| 178 | | - </Text> |
|---|
| 179 | | - </Pressable> |
|---|
| 180 | | - <Pressable |
|---|
| 181 | | - onPress={() => setEditingId(null)} |
|---|
| 182 | | - style={{ |
|---|
| 183 | | - flex: 1, |
|---|
| 184 | | - backgroundColor: "#252538", |
|---|
| 185 | | - borderRadius: 10, |
|---|
| 186 | | - paddingVertical: 10, |
|---|
| 187 | | - alignItems: "center", |
|---|
| 188 | | - }} |
|---|
| 189 | | - > |
|---|
| 190 | | - <Text style={{ color: "#9898B0", fontSize: 15 }}>Cancel</Text> |
|---|
| 191 | | - </Pressable> |
|---|
| 192 | | - </View> |
|---|
| 193 | | - </View> |
|---|
| 194 | | - ) : ( |
|---|
| 195 | | - /* Normal session row */ |
|---|
| 196 | | - <Pressable |
|---|
| 197 | | - onPress={() => handleSwitch(session)} |
|---|
| 198 | | - onLongPress={() => handleStartRename(session)} |
|---|
| 199 | | - style={({ pressed }) => ({ |
|---|
| 200 | | - backgroundColor: pressed ? "#252538" : "#1E1E2E", |
|---|
| 201 | | - borderRadius: 16, |
|---|
| 202 | | - padding: 16, |
|---|
| 203 | | - flexDirection: "row", |
|---|
| 204 | | - alignItems: "center", |
|---|
| 205 | | - borderWidth: session.isActive ? 2 : 1, |
|---|
| 206 | | - borderColor: session.isActive ? "#4A9EFF" : "#2E2E45", |
|---|
| 207 | | - })} |
|---|
| 208 | | - > |
|---|
| 209 | | - {/* Number badge */} |
|---|
| 210 | | - <View |
|---|
| 211 | | - style={{ |
|---|
| 212 | | - width: 36, |
|---|
| 213 | | - height: 36, |
|---|
| 214 | | - borderRadius: 18, |
|---|
| 215 | | - backgroundColor: session.isActive ? "#4A9EFF" : "#252538", |
|---|
| 216 | | - alignItems: "center", |
|---|
| 217 | | - justifyContent: "center", |
|---|
| 218 | | - marginRight: 14, |
|---|
| 219 | | - }} |
|---|
| 220 | | - > |
|---|
| 221 | | - <Text |
|---|
| 222 | | - style={{ |
|---|
| 223 | | - color: session.isActive ? "#FFF" : "#9898B0", |
|---|
| 224 | | - fontSize: 16, |
|---|
| 225 | | - fontWeight: "700", |
|---|
| 226 | | - }} |
|---|
| 227 | | - > |
|---|
| 228 | | - {session.index} |
|---|
| 229 | | - </Text> |
|---|
| 230 | | - </View> |
|---|
| 231 | | - |
|---|
| 232 | | - {/* Session info */} |
|---|
| 233 | | - <View style={{ flex: 1 }}> |
|---|
| 234 | | - <Text |
|---|
| 235 | | - style={{ |
|---|
| 236 | | - color: "#E8E8F0", |
|---|
| 237 | | - fontSize: 17, |
|---|
| 238 | | - fontWeight: "600", |
|---|
| 239 | | - }} |
|---|
| 240 | | - numberOfLines={1} |
|---|
| 241 | | - > |
|---|
| 242 | | - {session.name} |
|---|
| 243 | | - </Text> |
|---|
| 244 | | - <Text |
|---|
| 245 | | - style={{ |
|---|
| 246 | | - color: "#5A5A78", |
|---|
| 247 | | - fontSize: 12, |
|---|
| 248 | | - marginTop: 2, |
|---|
| 249 | | - }} |
|---|
| 250 | | - > |
|---|
| 251 | | - {session.kind === "api" ? "Headless" : session.kind === "visual" ? "Visual" : session.type === "terminal" ? "Terminal" : "Claude"} |
|---|
| 252 | | - {session.isActive ? " — active" : ""} |
|---|
| 253 | | - </Text> |
|---|
| 254 | | - </View> |
|---|
| 255 | | - |
|---|
| 256 | | - {/* Active indicator */} |
|---|
| 257 | | - {session.isActive && ( |
|---|
| 258 | | - <View |
|---|
| 259 | | - style={{ |
|---|
| 260 | | - width: 10, |
|---|
| 261 | | - height: 10, |
|---|
| 262 | | - borderRadius: 5, |
|---|
| 263 | | - backgroundColor: "#2ED573", |
|---|
| 264 | | - }} |
|---|
| 265 | | - /> |
|---|
| 266 | | - )} |
|---|
| 267 | | - </Pressable> |
|---|
| 268 | | - )} |
|---|
| 414 | + {sortedSessions.length === 0 ? ( |
|---|
| 415 | + <View style={{ alignItems: "center", paddingVertical: 32 }}> |
|---|
| 416 | + <Text style={{ color: "#5A5A78", fontSize: 15 }}> |
|---|
| 417 | + No sessions found |
|---|
| 418 | + </Text> |
|---|
| 269 | 419 | </View> |
|---|
| 270 | | - )) |
|---|
| 271 | | - )} |
|---|
| 420 | + ) : ( |
|---|
| 421 | + sortedSessions.map((session) => ( |
|---|
| 422 | + <View key={session.id} style={{ marginBottom: 6 }}> |
|---|
| 423 | + {editingId === session.id ? ( |
|---|
| 424 | + <RenameEditor |
|---|
| 425 | + name={session.name} |
|---|
| 426 | + onConfirm={(name) => |
|---|
| 427 | + handleConfirmRename(session.id, name) |
|---|
| 428 | + } |
|---|
| 429 | + onCancel={() => setEditingId(null)} |
|---|
| 430 | + /> |
|---|
| 431 | + ) : ( |
|---|
| 432 | + <SessionRow |
|---|
| 433 | + session={session} |
|---|
| 434 | + onSwitch={() => handleSwitch(session)} |
|---|
| 435 | + onLongPress={() => handleStartRename(session)} |
|---|
| 436 | + onDelete={() => handleRemove(session)} |
|---|
| 437 | + /> |
|---|
| 438 | + )} |
|---|
| 439 | + </View> |
|---|
| 440 | + )) |
|---|
| 441 | + )} |
|---|
| 272 | 442 | |
|---|
| 273 | | - {/* Hint */} |
|---|
| 274 | | - <Text |
|---|
| 275 | | - style={{ |
|---|
| 276 | | - color: "#5A5A78", |
|---|
| 277 | | - fontSize: 12, |
|---|
| 278 | | - textAlign: "center", |
|---|
| 279 | | - paddingVertical: 12, |
|---|
| 280 | | - }} |
|---|
| 281 | | - > |
|---|
| 282 | | - Tap to switch — Long press to rename |
|---|
| 283 | | - </Text> |
|---|
| 284 | | - </ScrollView> |
|---|
| 443 | + <Text |
|---|
| 444 | + style={{ |
|---|
| 445 | + color: "#5A5A78", |
|---|
| 446 | + fontSize: 11, |
|---|
| 447 | + textAlign: "center", |
|---|
| 448 | + paddingVertical: 10, |
|---|
| 449 | + }} |
|---|
| 450 | + > |
|---|
| 451 | + Tap to switch — Long press to rename — Swipe left to remove |
|---|
| 452 | + </Text> |
|---|
| 453 | + </ScrollView> |
|---|
| 454 | + </View> |
|---|
| 285 | 455 | </View> |
|---|
| 286 | | - </View> |
|---|
| 456 | + </GestureHandlerRootView> |
|---|
| 287 | 457 | </Modal> |
|---|
| 288 | 458 | ); |
|---|
| 289 | 459 | } |
|---|