A React Native app for the ultimate thinking partner.
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
45import React from 'react';
46import {
47 View,
48 Text,
49 TouchableOpacity,
50 StyleSheet,
51 Platform,
52 Alert,
53 Linking,
54 FlatList,
55 Animated,
56} from 'react-native';
57import { Ionicons } from '@expo/vector-icons';
58import { useSafeAreaInsets } from 'react-native-safe-area-context';
59import type { Theme } from '../theme';
60
61interface 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
76export 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
288const 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
326export default AppSidebar;