A React Native app for the ultimate thinking partner.

feat: Extract AppSidebar component

AppSidebar (✅ Complete):
- Extracted from App.tsx.monolithic lines 1924-2079
- Animated slide-in drawer (0-280px width)
- 6 menu items with proper callbacks
- Developer mode conditional items
- Full inline documentation

Features:
- Memory navigation
- Settings navigation
- Theme toggle (light/dark)
- Open agent in browser
- Refresh Co agent (dev mode, with confirmation)
- Logout

Implementation:
- Uses Animated.View for smooth slide animation
- Safe area insets for proper padding
- Theme-aware colors and styling
- Platform-specific confirmations (Alert vs window.confirm)
- Disabled state for items requiring agent ID

Not yet integrated (zero risk to running app)

Next: Extract view components (YouView, KnowledgeView, SettingsView)

+326
+326
src/components/AppSidebar.tsx
··· 1 + /** 2 + * AppSidebar Component 3 + * 4 + * MIGRATION STATUS: ✅ EXTRACTED - Ready for use 5 + * 6 + * REPLACES: App.tsx.monolithic lines 1924-2079 7 + * - Animated slide-in drawer menu 8 + * - Navigation to Memory and Settings 9 + * - Theme toggle (light/dark mode) 10 + * - Open agent in browser 11 + * - Refresh Co agent (developer mode only) 12 + * - Logout button 13 + * 14 + * FEATURES: 15 + * - Animated slide-in from left (0-280px width) 16 + * - Menu items with icons 17 + * - Conditional items (developer mode) 18 + * - Safe area inset support 19 + * - Theme-aware styling 20 + * 21 + * MENU ITEMS: 22 + * 1. Memory - Navigate to knowledge view 23 + * 2. Settings - Navigate to settings view 24 + * 3. Light/Dark Mode Toggle 25 + * 4. Open in Browser - Opens agent in Letta web app 26 + * 5. Refresh Co (dev mode only) - Deletes and recreates agent 27 + * 6. Logout - Signs out user 28 + * 29 + * DEPENDENCIES: 30 + * - React Native Animated API 31 + * - Ionicons 32 + * - react-native-safe-area-context 33 + * - Theme system 34 + * - Linking (for browser navigation) 35 + * 36 + * USED BY: (not yet integrated) 37 + * - [ ] App.new.tsx (planned) 38 + * 39 + * RELATED COMPONENTS: 40 + * - AppHeader.tsx (menu button triggers this) 41 + * - SettingsView.tsx (navigated to from Settings item) 42 + * - KnowledgeView.tsx (navigated to from Memory item) 43 + */ 44 + 45 + import React from 'react'; 46 + import { 47 + View, 48 + Text, 49 + TouchableOpacity, 50 + StyleSheet, 51 + Platform, 52 + Alert, 53 + Linking, 54 + FlatList, 55 + Animated, 56 + } from 'react-native'; 57 + import { Ionicons } from '@expo/vector-icons'; 58 + import { useSafeAreaInsets } from 'react-native-safe-area-context'; 59 + import type { Theme } from '../theme'; 60 + 61 + interface AppSidebarProps { 62 + theme: Theme; 63 + colorScheme: 'light' | 'dark'; 64 + visible: boolean; 65 + animationValue: Animated.Value; // 0 = hidden, 1 = visible 66 + developerMode: boolean; 67 + agentId?: string; 68 + onClose: () => void; 69 + onMemoryPress: () => void; 70 + onSettingsPress: () => void; 71 + onThemeToggle: () => void; 72 + onRefreshAgent: () => Promise<void>; 73 + onLogout: () => void; 74 + } 75 + 76 + export function AppSidebar({ 77 + theme, 78 + colorScheme, 79 + visible, 80 + animationValue, 81 + developerMode, 82 + agentId, 83 + onClose, 84 + onMemoryPress, 85 + onSettingsPress, 86 + onThemeToggle, 87 + onRefreshAgent, 88 + onLogout, 89 + }: AppSidebarProps) { 90 + const insets = useSafeAreaInsets(); 91 + 92 + const handleRefreshAgent = async () => { 93 + const confirmed = 94 + Platform.OS === 'web' 95 + ? window.confirm( 96 + 'This will delete the current co agent and create a new one. All conversation history will be lost. Are you sure?' 97 + ) 98 + : await new Promise<boolean>((resolve) => { 99 + Alert.alert( 100 + 'Refresh Co Agent', 101 + 'This will delete the current co agent and create a new one. All conversation history will be lost. Are you sure?', 102 + [ 103 + { text: 'Cancel', style: 'cancel', onPress: () => resolve(false) }, 104 + { text: 'Refresh', style: 'destructive', onPress: () => resolve(true) }, 105 + ] 106 + ); 107 + }); 108 + 109 + if (!confirmed) return; 110 + 111 + onClose(); 112 + await onRefreshAgent(); 113 + }; 114 + 115 + const handleOpenInBrowser = () => { 116 + if (agentId) { 117 + Linking.openURL(`https://app.letta.com/agents/${agentId}`); 118 + } 119 + }; 120 + 121 + return ( 122 + <Animated.View 123 + style={[ 124 + styles.sidebarContainer, 125 + { 126 + paddingTop: insets.top, 127 + backgroundColor: theme.colors.background.secondary, 128 + borderRightColor: theme.colors.border.primary, 129 + width: animationValue.interpolate({ 130 + inputRange: [0, 1], 131 + outputRange: [0, 280], 132 + }), 133 + }, 134 + ]} 135 + > 136 + <View 137 + style={[ 138 + styles.sidebarHeader, 139 + { borderBottomColor: theme.colors.border.primary }, 140 + ]} 141 + > 142 + <Text style={[styles.sidebarTitle, { color: theme.colors.text.primary }]}> 143 + Menu 144 + </Text> 145 + <TouchableOpacity onPress={onClose} style={styles.closeSidebar}> 146 + <Ionicons name="close" size={24} color={theme.colors.text.primary} /> 147 + </TouchableOpacity> 148 + </View> 149 + 150 + <FlatList 151 + style={{ flex: 1 }} 152 + contentContainerStyle={{ flexGrow: 1 }} 153 + ListHeaderComponent={ 154 + <View style={styles.menuItems}> 155 + {/* Memory */} 156 + <TouchableOpacity 157 + style={[ 158 + styles.menuItem, 159 + { borderBottomColor: theme.colors.border.primary }, 160 + ]} 161 + onPress={() => { 162 + onClose(); 163 + onMemoryPress(); 164 + }} 165 + > 166 + <Ionicons 167 + name="library-outline" 168 + size={24} 169 + color={theme.colors.text.primary} 170 + /> 171 + <Text style={[styles.menuItemText, { color: theme.colors.text.primary }]}> 172 + Memory 173 + </Text> 174 + </TouchableOpacity> 175 + 176 + {/* Settings */} 177 + <TouchableOpacity 178 + style={[ 179 + styles.menuItem, 180 + { borderBottomColor: theme.colors.border.primary }, 181 + ]} 182 + onPress={() => { 183 + onClose(); 184 + onSettingsPress(); 185 + }} 186 + > 187 + <Ionicons 188 + name="settings-outline" 189 + size={24} 190 + color={theme.colors.text.primary} 191 + /> 192 + <Text style={[styles.menuItemText, { color: theme.colors.text.primary }]}> 193 + Settings 194 + </Text> 195 + </TouchableOpacity> 196 + 197 + {/* Theme Toggle */} 198 + <TouchableOpacity 199 + style={[ 200 + styles.menuItem, 201 + { borderBottomColor: theme.colors.border.primary }, 202 + ]} 203 + onPress={onThemeToggle} 204 + > 205 + <Ionicons 206 + name={colorScheme === 'dark' ? 'sunny-outline' : 'moon-outline'} 207 + size={24} 208 + color={theme.colors.text.primary} 209 + /> 210 + <Text style={[styles.menuItemText, { color: theme.colors.text.primary }]}> 211 + {colorScheme === 'dark' ? 'Light Mode' : 'Dark Mode'} 212 + </Text> 213 + </TouchableOpacity> 214 + 215 + {/* Open in Browser */} 216 + <TouchableOpacity 217 + style={[ 218 + styles.menuItem, 219 + { borderBottomColor: theme.colors.border.primary }, 220 + ]} 221 + onPress={handleOpenInBrowser} 222 + disabled={!agentId} 223 + > 224 + <Ionicons 225 + name="open-outline" 226 + size={24} 227 + color={theme.colors.text.primary} 228 + /> 229 + <Text style={[styles.menuItemText, { color: theme.colors.text.primary }]}> 230 + Open in Browser 231 + </Text> 232 + </TouchableOpacity> 233 + 234 + {/* Refresh Co (Developer Mode Only) */} 235 + {developerMode && ( 236 + <TouchableOpacity 237 + style={[ 238 + styles.menuItem, 239 + { borderBottomColor: theme.colors.border.primary }, 240 + ]} 241 + onPress={handleRefreshAgent} 242 + > 243 + <Ionicons 244 + name="refresh-outline" 245 + size={24} 246 + color={theme.colors.status.error} 247 + /> 248 + <Text 249 + style={[ 250 + styles.menuItemText, 251 + { color: theme.colors.status.error }, 252 + ]} 253 + > 254 + Refresh Co 255 + </Text> 256 + </TouchableOpacity> 257 + )} 258 + 259 + {/* Logout */} 260 + <TouchableOpacity 261 + style={[ 262 + styles.menuItem, 263 + { borderBottomColor: theme.colors.border.primary }, 264 + ]} 265 + onPress={() => { 266 + onClose(); 267 + onLogout(); 268 + }} 269 + > 270 + <Ionicons 271 + name="log-out-outline" 272 + size={24} 273 + color={theme.colors.text.primary} 274 + /> 275 + <Text style={[styles.menuItemText, { color: theme.colors.text.primary }]}> 276 + Logout 277 + </Text> 278 + </TouchableOpacity> 279 + </View> 280 + } 281 + data={[]} 282 + renderItem={() => null} 283 + /> 284 + </Animated.View> 285 + ); 286 + } 287 + 288 + const styles = StyleSheet.create({ 289 + sidebarContainer: { 290 + height: '100%', 291 + borderRightWidth: 1, 292 + overflow: 'hidden', 293 + }, 294 + sidebarHeader: { 295 + flexDirection: 'row', 296 + justifyContent: 'space-between', 297 + alignItems: 'center', 298 + paddingHorizontal: 16, 299 + paddingVertical: 16, 300 + borderBottomWidth: 1, 301 + }, 302 + closeSidebar: { 303 + padding: 8, 304 + }, 305 + sidebarTitle: { 306 + fontSize: 24, 307 + fontFamily: 'Lexend_700Bold', 308 + }, 309 + menuItems: { 310 + paddingTop: 8, 311 + }, 312 + menuItem: { 313 + flexDirection: 'row', 314 + alignItems: 'center', 315 + paddingHorizontal: 20, 316 + paddingVertical: 16, 317 + borderBottomWidth: StyleSheet.hairlineWidth, 318 + }, 319 + menuItemText: { 320 + fontSize: 16, 321 + fontFamily: 'Lexend_400Regular', 322 + marginLeft: 16, 323 + }, 324 + }); 325 + 326 + export default AppSidebar;