-3819
App.old.tsx
-3819
App.old.tsx
···
1
-
import React, { useState, useEffect, useRef, useMemo, useCallback } from 'react';
2
-
import {
3
-
View,
4
-
Text,
5
-
StyleSheet,
6
-
TouchableOpacity,
7
-
Alert,
8
-
TextInput,
9
-
FlatList,
10
-
SafeAreaView,
11
-
ActivityIndicator,
12
-
Modal,
13
-
Dimensions,
14
-
useColorScheme,
15
-
Platform,
16
-
Linking,
17
-
Animated,
18
-
Image,
19
-
KeyboardAvoidingView,
20
-
ScrollView,
21
-
Keyboard,
22
-
} from 'react-native';
23
-
import { Ionicons } from '@expo/vector-icons';
24
-
import { StatusBar } from 'expo-status-bar';
25
-
import * as Clipboard from 'expo-clipboard';
26
-
import { SafeAreaProvider, useSafeAreaInsets } from 'react-native-safe-area-context';
27
-
import * as SystemUI from 'expo-system-ui';
28
-
import Markdown from '@ronradtke/react-native-markdown-display';
29
-
import * as ImagePicker from 'expo-image-picker';
30
-
import { useFonts, Lexend_300Light, Lexend_400Regular, Lexend_500Medium, Lexend_600SemiBold, Lexend_700Bold } from '@expo-google-fonts/lexend';
31
-
import LogoLoader from './src/components/LogoLoader';
32
-
import lettaApi from './src/api/lettaApi';
33
-
import Storage, { STORAGE_KEYS } from './src/utils/storage';
34
-
import { findOrCreateCo } from './src/utils/coAgent';
35
-
import CoLoginScreen from './CoLoginScreen';
36
-
import MessageContent from './src/components/MessageContent';
37
-
import ExpandableMessageContent from './src/components/ExpandableMessageContent';
38
-
import AnimatedStreamingText from './src/components/AnimatedStreamingText';
39
-
import ToolCallItem from './src/components/ToolCallItem';
40
-
import ReasoningToggle from './src/components/ReasoningToggle';
41
-
import MemoryBlockViewer from './src/components/MemoryBlockViewer';
42
-
import MessageInput from './src/components/MessageInput';
43
-
import { createMarkdownStyles } from './src/components/markdownStyles';
44
-
import { darkTheme, lightTheme, CoColors } from './src/theme';
45
-
import type { LettaAgent, LettaMessage, StreamingChunk, MemoryBlock, Passage } from './src/types/letta';
46
-
47
-
// Import web styles for transparent input
48
-
if (Platform.OS === 'web') {
49
-
require('./web-styles.css');
50
-
}
51
-
52
-
function CoApp() {
53
-
const insets = useSafeAreaInsets();
54
-
const systemColorScheme = useColorScheme();
55
-
const [colorScheme, setColorScheme] = useState<'light' | 'dark'>(systemColorScheme || 'dark');
56
-
57
-
// Set Android system UI colors
58
-
useEffect(() => {
59
-
if (Platform.OS === 'android') {
60
-
SystemUI.setBackgroundColorAsync(darkTheme.colors.background.primary);
61
-
}
62
-
}, []);
63
-
64
-
// Track keyboard state for Android
65
-
const [isKeyboardVisible, setIsKeyboardVisible] = useState(false);
66
-
67
-
useEffect(() => {
68
-
if (Platform.OS !== 'android') return;
69
-
70
-
const showSubscription = Keyboard.addListener('keyboardDidShow', () => {
71
-
setIsKeyboardVisible(true);
72
-
});
73
-
const hideSubscription = Keyboard.addListener('keyboardDidHide', () => {
74
-
setIsKeyboardVisible(false);
75
-
});
76
-
77
-
return () => {
78
-
showSubscription.remove();
79
-
hideSubscription.remove();
80
-
};
81
-
}, []);
82
-
83
-
const [fontsLoaded] = useFonts({
84
-
Lexend_300Light,
85
-
Lexend_400Regular,
86
-
Lexend_500Medium,
87
-
Lexend_600SemiBold,
88
-
Lexend_700Bold,
89
-
});
90
-
91
-
const toggleColorScheme = () => {
92
-
setColorScheme(prev => prev === 'dark' ? 'light' : 'dark');
93
-
};
94
-
95
-
const theme = colorScheme === 'dark' ? darkTheme : lightTheme;
96
-
97
-
// Authentication state
98
-
const [apiToken, setApiToken] = useState('');
99
-
const [isConnected, setIsConnected] = useState(false);
100
-
const [isConnecting, setIsConnecting] = useState(false);
101
-
const [isLoadingToken, setIsLoadingToken] = useState(true);
102
-
const [connectionError, setConnectionError] = useState<string | null>(null);
103
-
104
-
// Co agent state
105
-
const [coAgent, setCoAgent] = useState<LettaAgent | null>(null);
106
-
const [isInitializingCo, setIsInitializingCo] = useState(false);
107
-
const [isRefreshingCo, setIsRefreshingCo] = useState(false);
108
-
109
-
// Message state
110
-
const [messages, setMessages] = useState<LettaMessage[]>([]);
111
-
112
-
// Debug logging for messages state changes
113
-
useEffect(() => {
114
-
console.log('[MESSAGES STATE] Changed, now have', messages.length, 'messages');
115
-
if (messages.length > 0) {
116
-
console.log('[MESSAGES STATE] First:', messages[0]?.id?.substring(0, 8), messages[0]?.message_type);
117
-
console.log('[MESSAGES STATE] Last:', messages[messages.length - 1]?.id?.substring(0, 8), messages[messages.length - 1]?.message_type);
118
-
}
119
-
}, [messages]);
120
-
const PAGE_SIZE = 50;
121
-
const INITIAL_LOAD_LIMIT = 100; // Increased to show more history by default
122
-
const [earliestCursor, setEarliestCursor] = useState<string | null>(null);
123
-
const [hasMoreBefore, setHasMoreBefore] = useState<boolean>(false);
124
-
const [isLoadingMore, setIsLoadingMore] = useState<boolean>(false);
125
-
const [selectedImages, setSelectedImages] = useState<Array<{ uri: string; base64: string; mediaType: string }>>([]);
126
-
const [isSendingMessage, setIsSendingMessage] = useState(false);
127
-
const [hasInputText, setHasInputText] = useState(false);
128
-
const inputTextRef = useRef<string>('');
129
-
const [clearInputTrigger, setClearInputTrigger] = useState(0);
130
-
const [isLoadingMessages, setIsLoadingMessages] = useState(false);
131
-
132
-
// Simplified streaming state - everything in one place
133
-
const [currentStream, setCurrentStream] = useState({
134
-
reasoning: '',
135
-
toolCalls: [] as Array<{id: string, name: string, args: string}>,
136
-
assistantMessage: '',
137
-
});
138
-
// Store completed stream blocks (reasoning, assistant messages, tool calls that have finished)
139
-
const [completedStreamBlocks, setCompletedStreamBlocks] = useState<Array<{
140
-
type: 'reasoning' | 'assistant_message',
141
-
content: string
142
-
}>>([]);
143
-
const [isStreaming, setIsStreaming] = useState(false);
144
-
const [lastMessageNeedsSpace, setLastMessageNeedsSpace] = useState(false);
145
-
const spacerHeightAnim = useRef(new Animated.Value(0)).current;
146
-
const rainbowAnimValue = useRef(new Animated.Value(0)).current;
147
-
const [isInputFocused, setIsInputFocused] = useState(false);
148
-
const statusFadeAnim = useRef(new Animated.Value(0)).current;
149
-
const scrollIntervalRef = useRef<NodeJS.Timeout | null>(null);
150
-
151
-
// HITL approval state
152
-
const [approvalVisible, setApprovalVisible] = useState(false);
153
-
const [approvalData, setApprovalData] = useState<{
154
-
id?: string;
155
-
toolName?: string;
156
-
toolArgs?: string;
157
-
reasoning?: string;
158
-
} | null>(null);
159
-
const [approvalReason, setApprovalReason] = useState('');
160
-
const [isApproving, setIsApproving] = useState(false);
161
-
162
-
// Layout state for responsive design
163
-
const [screenData, setScreenData] = useState(Dimensions.get('window'));
164
-
const [sidebarVisible, setSidebarVisible] = useState(false);
165
-
const [activeSidebarTab, setActiveSidebarTab] = useState<'files'>('files');
166
-
const [currentView, setCurrentView] = useState<'you' | 'chat' | 'knowledge' | 'settings'>('chat');
167
-
const [knowledgeTab, setKnowledgeTab] = useState<'core' | 'archival' | 'files'>('core');
168
-
169
-
// Settings
170
-
const [showCompaction, setShowCompaction] = useState(true);
171
-
const [memoryBlocks, setMemoryBlocks] = useState<MemoryBlock[]>([]);
172
-
const [isLoadingBlocks, setIsLoadingBlocks] = useState(false);
173
-
const [blocksError, setBlocksError] = useState<string | null>(null);
174
-
const [selectedBlock, setSelectedBlock] = useState<MemoryBlock | null>(null);
175
-
const [memorySearchQuery, setMemorySearchQuery] = useState('');
176
-
const [youBlockContent, setYouBlockContent] = useState<string>(`## First Conversation
177
-
178
-
I'm here to understand how you think and help you see what you know.
179
-
180
-
As we talk, this space will evolve to reflect:
181
-
- What you're focused on right now and why it matters
182
-
- Patterns in how you approach problems
183
-
- Connections between your ideas that might not be obvious
184
-
- The questions you're holding
185
-
186
-
I'm paying attention not just to what you say, but how you think. Let's start wherever feels natural.`);
187
-
const [hasYouBlock, setHasYouBlock] = useState<boolean>(false);
188
-
const [hasCheckedYouBlock, setHasCheckedYouBlock] = useState<boolean>(false);
189
-
const [isCreatingYouBlock, setIsCreatingYouBlock] = useState<boolean>(false);
190
-
const sidebarAnimRef = useRef(new Animated.Value(0)).current;
191
-
const [developerMode, setDeveloperMode] = useState(true);
192
-
const [headerClickCount, setHeaderClickCount] = useState(0);
193
-
const headerClickTimeoutRef = useRef<NodeJS.Timeout | null>(null);
194
-
195
-
// File management state
196
-
const [coFolder, setCoFolder] = useState<any | null>(null);
197
-
const [folderFiles, setFolderFiles] = useState<any[]>([]);
198
-
const [isLoadingFiles, setIsLoadingFiles] = useState(false);
199
-
200
-
// Archival memory state
201
-
const [passages, setPassages] = useState<Passage[]>([]);
202
-
const [isLoadingPassages, setIsLoadingPassages] = useState(false);
203
-
const [passagesError, setPassagesError] = useState<string | null>(null);
204
-
const [passageSearchQuery, setPassageSearchQuery] = useState('');
205
-
const [selectedPassage, setSelectedPassage] = useState<Passage | null>(null);
206
-
const [isCreatingPassage, setIsCreatingPassage] = useState(false);
207
-
const [isEditingPassage, setIsEditingPassage] = useState(false);
208
-
const [isSavingPassage, setIsSavingPassage] = useState(false);
209
-
const [passageAfterCursor, setPassageAfterCursor] = useState<string | undefined>(undefined);
210
-
const [hasMorePassages, setHasMorePassages] = useState(false);
211
-
const [isUploadingFile, setIsUploadingFile] = useState(false);
212
-
const [uploadProgress, setUploadProgress] = useState<string>('');
213
-
const [filesError, setFilesError] = useState<string | null>(null);
214
-
215
-
const isDesktop = screenData.width >= 768;
216
-
217
-
// Ref for ScrollView to control scrolling
218
-
const scrollViewRef = useRef<FlatList<any>>(null);
219
-
const [scrollY, setScrollY] = useState(0);
220
-
const [contentHeight, setContentHeight] = useState(0);
221
-
const [containerHeight, setContainerHeight] = useState(0);
222
-
const [showScrollToBottom, setShowScrollToBottom] = useState(false);
223
-
const [inputContainerHeight, setInputContainerHeight] = useState(0);
224
-
const pendingJumpToBottomRef = useRef<boolean>(false);
225
-
const pendingJumpRetriesRef = useRef<number>(0);
226
-
const [copiedMessageId, setCopiedMessageId] = useState<string | null>(null);
227
-
228
-
// Load stored API token on mount
229
-
useEffect(() => {
230
-
loadStoredToken();
231
-
}, []);
232
-
233
-
// Cleanup intervals on unmount
234
-
useEffect(() => {
235
-
return () => {
236
-
if (scrollIntervalRef.current) {
237
-
clearInterval(scrollIntervalRef.current);
238
-
}
239
-
};
240
-
}, []);
241
-
242
-
// Initialize Co when connected
243
-
useEffect(() => {
244
-
if (isConnected && !coAgent && !isInitializingCo) {
245
-
initializeCo();
246
-
}
247
-
}, [isConnected, coAgent, isInitializingCo]);
248
-
249
-
// Load messages when Co agent is ready
250
-
useEffect(() => {
251
-
if (coAgent) {
252
-
loadMessages();
253
-
}
254
-
}, [coAgent]);
255
-
256
-
257
-
const loadStoredToken = async () => {
258
-
try {
259
-
const stored = await Storage.getItem(STORAGE_KEYS.API_TOKEN);
260
-
if (stored) {
261
-
setApiToken(stored);
262
-
await connectWithToken(stored);
263
-
}
264
-
} catch (error) {
265
-
console.error('Failed to load stored token:', error);
266
-
} finally {
267
-
setIsLoadingToken(false);
268
-
}
269
-
};
270
-
271
-
const connectWithToken = async (token: string) => {
272
-
setIsConnecting(true);
273
-
setConnectionError(null);
274
-
try {
275
-
lettaApi.setAuthToken(token);
276
-
const isValid = await lettaApi.testConnection();
277
-
278
-
if (isValid) {
279
-
setIsConnected(true);
280
-
await Storage.setItem(STORAGE_KEYS.API_TOKEN, token);
281
-
} else {
282
-
throw new Error('Invalid API token');
283
-
}
284
-
} catch (error: any) {
285
-
console.error('Connection failed:', error);
286
-
setConnectionError(error.message || 'Failed to connect');
287
-
lettaApi.removeAuthToken();
288
-
setIsConnected(false);
289
-
} finally {
290
-
setIsConnecting(false);
291
-
}
292
-
};
293
-
294
-
const handleLogin = async (token: string) => {
295
-
setApiToken(token);
296
-
await connectWithToken(token);
297
-
};
298
-
299
-
const handleLogout = async () => {
300
-
Alert.alert(
301
-
'Logout',
302
-
'Are you sure you want to log out?',
303
-
[
304
-
{ text: 'Cancel', style: 'cancel' },
305
-
{
306
-
text: 'Logout',
307
-
style: 'destructive',
308
-
onPress: async () => {
309
-
await Storage.removeItem(STORAGE_KEYS.API_TOKEN);
310
-
lettaApi.removeAuthToken();
311
-
setApiToken('');
312
-
setIsConnected(false);
313
-
setCoAgent(null);
314
-
setMessages([]);
315
-
setConnectionError(null);
316
-
},
317
-
},
318
-
]
319
-
);
320
-
};
321
-
322
-
const initializeCo = async () => {
323
-
setIsInitializingCo(true);
324
-
try {
325
-
console.log('Initializing Co agent...');
326
-
const agent = await findOrCreateCo('User');
327
-
setCoAgent(agent);
328
-
console.log('=== CO AGENT INITIALIZED ===');
329
-
console.log('Co agent ID:', agent.id);
330
-
console.log('Co agent name:', agent.name);
331
-
console.log('Co agent LLM config:', JSON.stringify(agent.llmConfig, null, 2));
332
-
console.log('LLM model:', agent.llmConfig?.model);
333
-
console.log('LLM context window:', agent.llmConfig?.contextWindow);
334
-
} catch (error: any) {
335
-
console.error('Failed to initialize Co:', error);
336
-
Alert.alert('Error', 'Failed to initialize Co: ' + (error.message || 'Unknown error'));
337
-
} finally {
338
-
setIsInitializingCo(false);
339
-
}
340
-
};
341
-
342
-
// Helper function to filter out any message in the top 5 that contains "More human than human"
343
-
const filterFirstMessage = (msgs: LettaMessage[]): LettaMessage[] => {
344
-
const checkLimit = Math.min(5, msgs.length);
345
-
for (let i = 0; i < checkLimit; i++) {
346
-
if (msgs[i].content.includes('More human than human')) {
347
-
return [...msgs.slice(0, i), ...msgs.slice(i + 1)];
348
-
}
349
-
}
350
-
return msgs;
351
-
};
352
-
353
-
const loadMessages = async (before?: string, limit?: number) => {
354
-
if (!coAgent) return;
355
-
356
-
try {
357
-
if (!before) {
358
-
setIsLoadingMessages(true);
359
-
} else {
360
-
setIsLoadingMore(true);
361
-
}
362
-
363
-
const loadedMessages = await lettaApi.listMessages(coAgent.id, {
364
-
before: before || undefined,
365
-
limit: limit || (before ? PAGE_SIZE : INITIAL_LOAD_LIMIT),
366
-
use_assistant_message: true,
367
-
});
368
-
369
-
console.log('[LOAD MESSAGES] Received', loadedMessages.length, 'messages from server');
370
-
console.log('[LOAD MESSAGES] First message:', loadedMessages[0]?.id, loadedMessages[0]?.message_type);
371
-
console.log('[LOAD MESSAGES] Last message:', loadedMessages[loadedMessages.length - 1]?.id, loadedMessages[loadedMessages.length - 1]?.message_type);
372
-
373
-
if (loadedMessages.length > 0) {
374
-
if (before) {
375
-
const filtered = filterFirstMessage([...loadedMessages, ...prev]);
376
-
console.log('[LOAD MESSAGES] After filtering (load more):', filtered.length);
377
-
setMessages(prev => filterFirstMessage([...loadedMessages, ...prev]));
378
-
setEarliestCursor(loadedMessages[0].id);
379
-
} else {
380
-
const filtered = filterFirstMessage(loadedMessages);
381
-
console.log('[LOAD MESSAGES] After filtering (initial load):', filtered.length, 'from', loadedMessages.length);
382
-
setMessages(filtered);
383
-
if (loadedMessages.length > 0) {
384
-
setEarliestCursor(loadedMessages[0].id);
385
-
pendingJumpToBottomRef.current = true;
386
-
pendingJumpRetriesRef.current = 3;
387
-
// Immediately scroll to bottom without animation on initial load
388
-
setTimeout(() => {
389
-
scrollViewRef.current?.scrollToEnd({ animated: false });
390
-
}, 100);
391
-
}
392
-
}
393
-
setHasMoreBefore(loadedMessages.length === (limit || (before ? PAGE_SIZE : INITIAL_LOAD_LIMIT)));
394
-
} else if (before) {
395
-
// No more messages to load before
396
-
setHasMoreBefore(false);
397
-
}
398
-
// If no messages and not loading before, keep existing messages (don't clear)
399
-
} catch (error: any) {
400
-
console.error('Failed to load messages:', error);
401
-
Alert.alert('Error', 'Failed to load messages: ' + (error.message || 'Unknown error'));
402
-
} finally {
403
-
setIsLoadingMessages(false);
404
-
setIsLoadingMore(false);
405
-
}
406
-
};
407
-
408
-
const loadMoreMessages = () => {
409
-
if (hasMoreBefore && !isLoadingMore && earliestCursor) {
410
-
loadMessages(earliestCursor);
411
-
}
412
-
};
413
-
414
-
const copyToClipboard = useCallback(async (content: string, messageId?: string) => {
415
-
try {
416
-
await Clipboard.setStringAsync(content);
417
-
if (messageId) {
418
-
setCopiedMessageId(messageId);
419
-
setTimeout(() => setCopiedMessageId(null), 2000);
420
-
}
421
-
} catch (error) {
422
-
console.error('Failed to copy to clipboard:', error);
423
-
}
424
-
}, []);
425
-
426
-
const pickImage = async () => {
427
-
try {
428
-
// Request permissions
429
-
const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync();
430
-
if (status !== 'granted') {
431
-
Alert.alert('Permission Required', 'Please allow access to your photo library to upload images.');
432
-
return;
433
-
}
434
-
435
-
// Launch image picker
436
-
const result = await ImagePicker.launchImageLibraryAsync({
437
-
mediaTypes: ['images'],
438
-
allowsMultipleSelection: false,
439
-
quality: 0.8,
440
-
base64: true,
441
-
});
442
-
443
-
console.log('Image picker result:', { canceled: result.canceled, assetsCount: result.assets?.length });
444
-
445
-
if (!result.canceled && result.assets && result.assets.length > 0) {
446
-
const asset = result.assets[0];
447
-
console.log('Asset info:', {
448
-
hasBase64: !!asset.base64,
449
-
base64Length: asset.base64?.length,
450
-
uri: asset.uri
451
-
});
452
-
453
-
if (asset.base64) {
454
-
// Check size: 5MB = 5 * 1024 * 1024 bytes
455
-
const MAX_SIZE = 5 * 1024 * 1024;
456
-
const sizeMB = (asset.base64.length / 1024 / 1024).toFixed(2);
457
-
console.log(`Image size: ${sizeMB}MB, max allowed: 5MB`);
458
-
459
-
if (asset.base64.length > MAX_SIZE) {
460
-
console.error(`IMAGE REJECTED: ${sizeMB}MB exceeds 5MB limit`);
461
-
Alert.alert(
462
-
'Image Too Large',
463
-
`This image is ${sizeMB}MB, but the maximum allowed size is 5MB. Please select a smaller image or compress it first.`
464
-
);
465
-
return; // Discard the image
466
-
}
467
-
468
-
const mediaType = asset.uri.match(/\.(jpg|jpeg)$/i) ? 'image/jpeg' :
469
-
asset.uri.match(/\.png$/i) ? 'image/png' :
470
-
asset.uri.match(/\.gif$/i) ? 'image/gif' :
471
-
asset.uri.match(/\.webp$/i) ? 'image/webp' : 'image/jpeg';
472
-
473
-
console.log('Adding image with mediaType:', mediaType);
474
-
setSelectedImages(prev => [...prev, {
475
-
uri: asset.uri,
476
-
base64: asset.base64,
477
-
mediaType,
478
-
}]);
479
-
} else {
480
-
console.error('No base64 data in asset');
481
-
Alert.alert('Error', 'Failed to read image data');
482
-
}
483
-
} else {
484
-
console.log('Image picker canceled or no assets');
485
-
}
486
-
} catch (error) {
487
-
console.error('Error picking image:', error);
488
-
Alert.alert('Error', 'Failed to pick image');
489
-
}
490
-
};
491
-
492
-
const removeImage = (index: number) => {
493
-
setSelectedImages(prev => prev.filter((_, i) => i !== index));
494
-
};
495
-
496
-
const handleStreamingChunk = useCallback((chunk: StreamingChunk) => {
497
-
console.log('Streaming chunk:', chunk.message_type, 'content:', chunk.content, 'reasoning:', chunk.reasoning);
498
-
499
-
// Handle error chunks
500
-
if ((chunk as any).error) {
501
-
console.error('Error chunk received:', (chunk as any).error);
502
-
setIsStreaming(false);
503
-
setIsSendingMessage(false);
504
-
setCurrentStream({ reasoning: '', toolCalls: [], assistantMessage: '' });
505
-
setCompletedStreamBlocks([]);
506
-
return;
507
-
}
508
-
509
-
// Handle stop_reason chunks
510
-
if ((chunk as any).message_type === 'stop_reason') {
511
-
console.log('Stop reason received:', (chunk as any).stopReason || (chunk as any).stop_reason);
512
-
return;
513
-
}
514
-
515
-
// Accumulate content - when switching message types, save previous content to completed blocks
516
-
if (chunk.message_type === 'reasoning_message' && chunk.reasoning) {
517
-
setCurrentStream(prev => {
518
-
// If we have assistant message, this is a NEW reasoning block - save assistant message first
519
-
if (prev.assistantMessage) {
520
-
setCompletedStreamBlocks(blocks => [...blocks, {
521
-
type: 'assistant_message',
522
-
content: prev.assistantMessage
523
-
}]);
524
-
return {
525
-
reasoning: chunk.reasoning,
526
-
toolCalls: [],
527
-
assistantMessage: ''
528
-
};
529
-
}
530
-
// Otherwise, continue accumulating reasoning
531
-
return {
532
-
...prev,
533
-
reasoning: prev.reasoning + chunk.reasoning
534
-
};
535
-
});
536
-
} else if ((chunk.message_type === 'tool_call_message' || chunk.message_type === 'tool_call') && chunk.tool_call) {
537
-
// Add tool call to list (deduplicate by ID)
538
-
const callObj = chunk.tool_call.function || chunk.tool_call;
539
-
const toolName = callObj?.name || callObj?.tool_name || 'tool';
540
-
const args = callObj?.arguments || callObj?.args || {};
541
-
const toolCallId = chunk.id || `tool_${toolName}_${Date.now()}`;
542
-
543
-
const formatArgsPython = (obj: any): string => {
544
-
if (!obj || typeof obj !== 'object') return '';
545
-
return Object.entries(obj)
546
-
.map(([k, v]) => `${k}=${typeof v === 'string' ? `"${v}"` : JSON.stringify(v)}`)
547
-
.join(', ');
548
-
};
549
-
550
-
const toolLine = `${toolName}(${formatArgsPython(args)})`;
551
-
552
-
setCurrentStream(prev => {
553
-
// Check if this tool call ID already exists
554
-
const exists = prev.toolCalls.some(tc => tc.id === toolCallId);
555
-
if (exists) {
556
-
return prev; // Don't add duplicates
557
-
}
558
-
return {
559
-
...prev,
560
-
toolCalls: [...prev.toolCalls, { id: toolCallId, name: toolName, args: toolLine }]
561
-
};
562
-
});
563
-
} else if (chunk.message_type === 'assistant_message' && chunk.content) {
564
-
// Accumulate assistant message
565
-
let contentText = '';
566
-
const content = chunk.content as any;
567
-
if (typeof content === 'string') {
568
-
contentText = content;
569
-
} else if (typeof content === 'object' && content !== null) {
570
-
if (Array.isArray(content)) {
571
-
contentText = content
572
-
.filter((item: any) => item.type === 'text')
573
-
.map((item: any) => item.text || '')
574
-
.join('');
575
-
} else if (content.text) {
576
-
contentText = content.text;
577
-
}
578
-
}
579
-
580
-
if (contentText) {
581
-
setCurrentStream(prev => {
582
-
// If we have reasoning, this is a NEW assistant message - save reasoning first
583
-
if (prev.reasoning && !prev.assistantMessage) {
584
-
setCompletedStreamBlocks(blocks => [...blocks, {
585
-
type: 'reasoning',
586
-
content: prev.reasoning
587
-
}]);
588
-
return {
589
-
reasoning: '',
590
-
toolCalls: [],
591
-
assistantMessage: contentText
592
-
};
593
-
}
594
-
// Otherwise, continue accumulating assistant message
595
-
return {
596
-
...prev,
597
-
assistantMessage: prev.assistantMessage + contentText
598
-
};
599
-
});
600
-
}
601
-
} else if (chunk.message_type === 'tool_return_message' || chunk.message_type === 'tool_response') {
602
-
// Ignore tool returns during streaming - we'll get them from server
603
-
return;
604
-
} else if (chunk.message_type === 'approval_request_message') {
605
-
// Handle approval request
606
-
const callObj = chunk.tool_call?.function || chunk.tool_call;
607
-
setApprovalData({
608
-
id: chunk.id,
609
-
toolName: callObj?.name || callObj?.tool_name,
610
-
toolArgs: callObj?.arguments || callObj?.args,
611
-
reasoning: chunk.reasoning,
612
-
});
613
-
setApprovalVisible(true);
614
-
}
615
-
}, []);
616
-
617
-
const sendMessage = useCallback(async (messageText: string, imagesToSend: Array<{ uri: string; base64: string; mediaType: string }>) => {
618
-
if ((!messageText.trim() && imagesToSend.length === 0) || !coAgent || isSendingMessage) return;
619
-
620
-
console.log('sendMessage called - messageText:', messageText, 'type:', typeof messageText, 'imagesToSend length:', imagesToSend.length);
621
-
622
-
setIsSendingMessage(true);
623
-
624
-
// Immediately add user message to UI (with images if any)
625
-
let tempMessageContent: any;
626
-
if (imagesToSend.length > 0) {
627
-
const contentParts = [];
628
-
629
-
// Add images using base64 (SDK expects camelCase, converts to snake_case for HTTP)
630
-
for (const img of imagesToSend) {
631
-
contentParts.push({
632
-
type: 'image',
633
-
source: {
634
-
type: 'base64',
635
-
mediaType: img.mediaType,
636
-
data: img.base64,
637
-
},
638
-
});
639
-
}
640
-
641
-
// Add text if present
642
-
console.log('[TEMP] About to check text - messageText:', JSON.stringify(messageText), 'type:', typeof messageText, 'length:', messageText?.length);
643
-
if (messageText && typeof messageText === 'string' && messageText.length > 0) {
644
-
console.log('[TEMP] Adding text to contentParts');
645
-
contentParts.push({
646
-
type: 'text',
647
-
text: messageText,
648
-
});
649
-
}
650
-
651
-
console.log('[TEMP] Final contentParts:', JSON.stringify(contentParts));
652
-
tempMessageContent = contentParts;
653
-
} else {
654
-
tempMessageContent = messageText;
655
-
}
656
-
657
-
const tempUserMessage: LettaMessage = {
658
-
id: `temp-${Date.now()}`,
659
-
role: 'user',
660
-
message_type: 'user_message',
661
-
content: tempMessageContent,
662
-
created_at: new Date().toISOString(),
663
-
} as LettaMessage;
664
-
665
-
console.log('[USER MESSAGE] Adding temp user message:', tempUserMessage.id, 'content type:', typeof tempUserMessage.content);
666
-
setMessages(prev => {
667
-
const newMessages = [...prev, tempUserMessage];
668
-
console.log('[USER MESSAGE] Messages count after add:', newMessages.length);
669
-
return newMessages;
670
-
});
671
-
672
-
// Scroll to bottom immediately to show user message
673
-
setTimeout(() => {
674
-
scrollViewRef.current?.scrollToEnd({ animated: false });
675
-
}, 50);
676
-
677
-
try {
678
-
setIsStreaming(true);
679
-
setLastMessageNeedsSpace(true);
680
-
// Clear streaming state
681
-
setCurrentStream({ reasoning: '', toolCalls: [], assistantMessage: '' });
682
-
setCompletedStreamBlocks([]);
683
-
684
-
// Make status indicator immediately visible
685
-
statusFadeAnim.setValue(1);
686
-
687
-
// Animate spacer growing to push user message up (push previous content out of view)
688
-
const targetHeight = Math.max(containerHeight * 0.9, 450);
689
-
spacerHeightAnim.setValue(0);
690
-
691
-
Animated.timing(spacerHeightAnim, {
692
-
toValue: targetHeight,
693
-
duration: 400,
694
-
useNativeDriver: false, // height animation can't use native driver
695
-
}).start();
696
-
697
-
// During animation, keep scroll at bottom
698
-
if (scrollIntervalRef.current) {
699
-
clearInterval(scrollIntervalRef.current);
700
-
}
701
-
scrollIntervalRef.current = setInterval(() => {
702
-
scrollViewRef.current?.scrollToEnd({ animated: false });
703
-
}, 16); // ~60fps
704
-
705
-
setTimeout(() => {
706
-
if (scrollIntervalRef.current) {
707
-
clearInterval(scrollIntervalRef.current);
708
-
scrollIntervalRef.current = null;
709
-
}
710
-
}, 400);
711
-
712
-
// Build message content based on whether we have images
713
-
let messageContent: any;
714
-
if (imagesToSend.length > 0) {
715
-
// Multi-part content with images
716
-
const contentParts = [];
717
-
718
-
// Add images using base64 (SDK expects camelCase, converts to snake_case for HTTP)
719
-
for (const img of imagesToSend) {
720
-
console.log('Adding image - mediaType:', img.mediaType, 'base64 length:', img.base64?.length);
721
-
722
-
contentParts.push({
723
-
type: 'image',
724
-
source: {
725
-
type: 'base64',
726
-
mediaType: img.mediaType,
727
-
data: img.base64,
728
-
},
729
-
});
730
-
}
731
-
732
-
// Add text if present
733
-
console.log('[API] About to check text - messageText:', JSON.stringify(messageText), 'type:', typeof messageText);
734
-
if (messageText && typeof messageText === 'string' && messageText.length > 0) {
735
-
console.log('[API] Adding text to contentParts - text value:', messageText);
736
-
const textItem = {
737
-
type: 'text' as const,
738
-
text: String(messageText), // Explicitly convert to string as safeguard
739
-
};
740
-
console.log('[API] Text item to push:', JSON.stringify(textItem));
741
-
contentParts.push(textItem);
742
-
}
743
-
744
-
messageContent = contentParts;
745
-
console.log('Built contentParts:', contentParts.length, 'items');
746
-
console.log('Full message structure:', JSON.stringify({role: 'user', content: messageContent}, null, 2).substring(0, 1000));
747
-
} else {
748
-
// Text-only message
749
-
messageContent = messageText;
750
-
console.log('Sending text-only message:', messageText);
751
-
}
752
-
753
-
console.log('=== ABOUT TO SEND TO API ===');
754
-
console.log('messageContent type:', typeof messageContent);
755
-
console.log('messageContent is array?', Array.isArray(messageContent));
756
-
console.log('messageContent:', JSON.stringify(messageContent, null, 2));
757
-
758
-
const payload = {
759
-
messages: [{ role: 'user', content: messageContent }],
760
-
use_assistant_message: true,
761
-
stream_tokens: true,
762
-
};
763
-
764
-
console.log('Full payload being sent:', JSON.stringify(payload, null, 2).substring(0, 2000));
765
-
766
-
await lettaApi.sendMessageStream(
767
-
coAgent.id,
768
-
payload,
769
-
(chunk: StreamingChunk) => {
770
-
handleStreamingChunk(chunk);
771
-
},
772
-
async (response) => {
773
-
console.log('Stream complete');
774
-
console.log('[STREAM COMPLETE] Fetching finalized messages from server');
775
-
776
-
// Reset spacer animation immediately to remove the gap
777
-
spacerHeightAnim.setValue(0);
778
-
779
-
// Wait for server to finalize messages
780
-
setTimeout(async () => {
781
-
try {
782
-
// Use setMessages callback to get current state and calculate fetch limit
783
-
let fetchLimit = 100; // Default minimum increased to be safer
784
-
785
-
setMessages(prev => {
786
-
const currentCount = prev.filter(msg => !msg.id.startsWith('temp-')).length;
787
-
fetchLimit = Math.max(currentCount + 10, 100); // Fetch more buffer
788
-
console.log('[STREAM COMPLETE] Current message count:', currentCount, 'Will fetch:', fetchLimit);
789
-
return prev; // Don't change messages yet
790
-
});
791
-
792
-
// Fetch recent messages with enough limit to cover what we had plus new ones
793
-
const recentMessages = await lettaApi.listMessages(coAgent.id, {
794
-
limit: fetchLimit,
795
-
use_assistant_message: true,
796
-
});
797
-
798
-
console.log('[STREAM COMPLETE] Received', recentMessages.length, 'messages from server');
799
-
console.log('[STREAM COMPLETE] First message ID:', recentMessages[0]?.id);
800
-
console.log('[STREAM COMPLETE] Last message ID:', recentMessages[recentMessages.length - 1]?.id);
801
-
802
-
if (recentMessages.length > 0) {
803
-
// Replace messages entirely with server response (this removes temp messages)
804
-
setMessages(filterFirstMessage(recentMessages));
805
-
console.log('[STREAM COMPLETE] Updated messages state');
806
-
}
807
-
} catch (error) {
808
-
console.error('Failed to fetch finalized messages:', error);
809
-
} finally {
810
-
// Clear streaming state after attempting to load
811
-
setIsStreaming(false);
812
-
setCurrentStream({ reasoning: '', toolCalls: [], assistantMessage: '' });
813
-
setCompletedStreamBlocks([]);
814
-
}
815
-
}, 500);
816
-
},
817
-
(error) => {
818
-
console.error('=== APP STREAMING ERROR CALLBACK ===');
819
-
console.error('Streaming error:', error);
820
-
console.error('Error type:', typeof error);
821
-
console.error('Error keys:', Object.keys(error || {}));
822
-
console.error('Error details:', {
823
-
message: error?.message,
824
-
status: error?.status,
825
-
code: error?.code,
826
-
response: error?.response,
827
-
responseData: error?.responseData
828
-
});
829
-
830
-
// Try to log full error structure
831
-
try {
832
-
console.error('Full error JSON:', JSON.stringify(error, null, 2));
833
-
} catch (e) {
834
-
console.error('Could not stringify error:', e);
835
-
}
836
-
837
-
// Clear scroll interval on error
838
-
if (scrollIntervalRef.current) {
839
-
clearInterval(scrollIntervalRef.current);
840
-
scrollIntervalRef.current = null;
841
-
}
842
-
843
-
// Reset spacer animation
844
-
spacerHeightAnim.setValue(0);
845
-
846
-
setIsStreaming(false);
847
-
setCurrentStream({ reasoning: '', toolCalls: [], assistantMessage: '' });
848
-
setCompletedStreamBlocks([]);
849
-
850
-
// Create detailed error message
851
-
let errorMsg = 'Failed to send message';
852
-
if (error?.message) {
853
-
errorMsg += ': ' + error.message;
854
-
}
855
-
if (error?.status) {
856
-
errorMsg += ' (Status: ' + error.status + ')';
857
-
}
858
-
if (error?.responseData) {
859
-
try {
860
-
const responseStr = typeof error.responseData === 'string'
861
-
? error.responseData
862
-
: JSON.stringify(error.responseData);
863
-
errorMsg += '\nDetails: ' + responseStr;
864
-
} catch (e) {
865
-
// ignore
866
-
}
867
-
}
868
-
869
-
Alert.alert('Error', errorMsg);
870
-
}
871
-
);
872
-
} catch (error: any) {
873
-
console.error('=== APP SEND MESSAGE OUTER CATCH ===');
874
-
console.error('Failed to send message:', error);
875
-
console.error('Error type:', typeof error);
876
-
console.error('Error keys:', Object.keys(error || {}));
877
-
console.error('Error details:', {
878
-
message: error?.message,
879
-
status: error?.status,
880
-
code: error?.code,
881
-
response: error?.response,
882
-
responseData: error?.responseData
883
-
});
884
-
885
-
try {
886
-
console.error('Full error JSON:', JSON.stringify(error, null, 2));
887
-
} catch (e) {
888
-
console.error('Could not stringify error:', e);
889
-
}
890
-
891
-
Alert.alert('Error', 'Failed to send message: ' + (error.message || 'Unknown error'));
892
-
setIsStreaming(false);
893
-
spacerHeightAnim.setValue(0);
894
-
} finally {
895
-
setIsSendingMessage(false);
896
-
}
897
-
}, [coAgent, isSendingMessage, containerHeight, spacerHeightAnim, handleStreamingChunk]);
898
-
899
-
const handleTextChange = useCallback((text: string) => {
900
-
inputTextRef.current = text;
901
-
const hasText = text.trim().length > 0;
902
-
// Only update state when crossing the empty/non-empty boundary
903
-
setHasInputText(prev => prev !== hasText ? hasText : prev);
904
-
}, []);
905
-
906
-
const handleSendFromInput = useCallback(() => {
907
-
const text = inputTextRef.current.trim();
908
-
if (text || selectedImages.length > 0) {
909
-
sendMessage(text, selectedImages);
910
-
setSelectedImages([]);
911
-
inputTextRef.current = '';
912
-
setHasInputText(false);
913
-
setClearInputTrigger(prev => prev + 1);
914
-
}
915
-
}, [sendMessage, selectedImages]);
916
-
917
-
const handleApproval = async (approve: boolean) => {
918
-
if (!approvalData?.id || !coAgent) return;
919
-
920
-
setIsApproving(true);
921
-
try {
922
-
await lettaApi.approveToolRequest(coAgent.id, {
923
-
approval_request_id: approvalData.id,
924
-
approve,
925
-
reason: approvalReason || undefined,
926
-
});
927
-
928
-
setApprovalVisible(false);
929
-
setApprovalData(null);
930
-
setApprovalReason('');
931
-
932
-
// Continue streaming after approval
933
-
} catch (error: any) {
934
-
console.error('Approval error:', error);
935
-
Alert.alert('Error', 'Failed to process approval: ' + (error.message || 'Unknown error'));
936
-
} finally {
937
-
setIsApproving(false);
938
-
}
939
-
};
940
-
941
-
const loadMemoryBlocks = async () => {
942
-
if (!coAgent) return;
943
-
944
-
setIsLoadingBlocks(true);
945
-
setBlocksError(null);
946
-
try {
947
-
const blocks = await lettaApi.listAgentBlocks(coAgent.id);
948
-
setMemoryBlocks(blocks);
949
-
950
-
// Extract the "you" block for the You view
951
-
const youBlock = blocks.find(block => block.label === 'you');
952
-
if (youBlock) {
953
-
setYouBlockContent(youBlock.value);
954
-
setHasYouBlock(true);
955
-
} else {
956
-
setHasYouBlock(false);
957
-
}
958
-
setHasCheckedYouBlock(true);
959
-
} catch (error: any) {
960
-
console.error('Failed to load memory blocks:', error);
961
-
setBlocksError(error.message || 'Failed to load memory blocks');
962
-
setHasCheckedYouBlock(true);
963
-
} finally {
964
-
setIsLoadingBlocks(false);
965
-
}
966
-
};
967
-
968
-
const createYouBlock = async () => {
969
-
if (!coAgent) return;
970
-
971
-
setIsCreatingYouBlock(true);
972
-
try {
973
-
const { YOU_BLOCK } = await import('./src/constants/memoryBlocks');
974
-
const createdBlock = await lettaApi.createAgentBlock(coAgent.id, {
975
-
label: YOU_BLOCK.label,
976
-
value: YOU_BLOCK.value,
977
-
description: YOU_BLOCK.description,
978
-
limit: YOU_BLOCK.limit,
979
-
});
980
-
981
-
// Update state
982
-
setYouBlockContent(createdBlock.value);
983
-
setHasYouBlock(true);
984
-
setMemoryBlocks(prev => [...prev, createdBlock]);
985
-
} catch (error: any) {
986
-
console.error('Failed to create you block:', error);
987
-
Alert.alert('Error', error.message || 'Failed to create you block');
988
-
} finally {
989
-
setIsCreatingYouBlock(false);
990
-
}
991
-
};
992
-
993
-
// Archival Memory (Passages) functions
994
-
const loadPassages = async (resetCursor = false) => {
995
-
if (!coAgent) return;
996
-
997
-
setIsLoadingPassages(true);
998
-
setPassagesError(null);
999
-
try {
1000
-
const params: any = {
1001
-
limit: 50,
1002
-
};
1003
-
1004
-
if (!resetCursor && passageAfterCursor) {
1005
-
params.after = passageAfterCursor;
1006
-
}
1007
-
1008
-
if (passageSearchQuery) {
1009
-
params.search = passageSearchQuery;
1010
-
}
1011
-
1012
-
// Use primary agent for archival memory
1013
-
const result = await lettaApi.listPassages(coAgent.id, params);
1014
-
1015
-
if (resetCursor) {
1016
-
setPassages(result);
1017
-
} else {
1018
-
setPassages(prev => [...prev, ...result]);
1019
-
}
1020
-
1021
-
setHasMorePassages(result.length === 50);
1022
-
if (result.length > 0) {
1023
-
setPassageAfterCursor(result[result.length - 1].id);
1024
-
}
1025
-
} catch (error: any) {
1026
-
console.error('Failed to load passages:', error);
1027
-
setPassagesError(error.message || 'Failed to load passages');
1028
-
} finally {
1029
-
setIsLoadingPassages(false);
1030
-
}
1031
-
};
1032
-
1033
-
const createPassage = async (text: string, tags?: string[]) => {
1034
-
if (!coAgent) return;
1035
-
1036
-
setIsLoadingPassages(true);
1037
-
try {
1038
-
// Use primary agent for archival memory
1039
-
await lettaApi.createPassage(coAgent.id, { text, tags });
1040
-
await loadPassages(true);
1041
-
Alert.alert('Success', 'Passage created successfully');
1042
-
} catch (error: any) {
1043
-
console.error('Failed to create passage:', error);
1044
-
Alert.alert('Error', error.message || 'Failed to create passage');
1045
-
} finally {
1046
-
setIsLoadingPassages(false);
1047
-
}
1048
-
};
1049
-
1050
-
const deletePassage = async (passageId: string) => {
1051
-
if (!coAgent) return;
1052
-
1053
-
const confirmed = Platform.OS === 'web'
1054
-
? window.confirm('Are you sure you want to delete this passage?')
1055
-
: await new Promise<boolean>((resolve) => {
1056
-
Alert.alert(
1057
-
'Delete Passage',
1058
-
'Are you sure you want to delete this passage?',
1059
-
[
1060
-
{ text: 'Cancel', style: 'cancel', onPress: () => resolve(false) },
1061
-
{ text: 'Delete', style: 'destructive', onPress: () => resolve(true) },
1062
-
]
1063
-
);
1064
-
});
1065
-
1066
-
if (!confirmed) return;
1067
-
1068
-
try {
1069
-
// Use primary agent for archival memory
1070
-
await lettaApi.deletePassage(coAgent.id, passageId);
1071
-
await loadPassages(true);
1072
-
if (Platform.OS === 'web') {
1073
-
window.alert('Passage deleted successfully');
1074
-
} else {
1075
-
Alert.alert('Success', 'Passage deleted');
1076
-
}
1077
-
} catch (error: any) {
1078
-
console.error('Delete passage error:', error);
1079
-
if (Platform.OS === 'web') {
1080
-
window.alert('Failed to delete passage: ' + (error.message || 'Unknown error'));
1081
-
} else {
1082
-
Alert.alert('Error', 'Failed to delete passage: ' + (error.message || 'Unknown error'));
1083
-
}
1084
-
}
1085
-
};
1086
-
1087
-
const modifyPassage = async (passageId: string, text: string, tags?: string[]) => {
1088
-
if (!coAgent) return;
1089
-
1090
-
setIsLoadingPassages(true);
1091
-
try {
1092
-
// Use primary agent for archival memory
1093
-
await lettaApi.modifyPassage(coAgent.id, passageId, { text, tags });
1094
-
await loadPassages(true);
1095
-
Alert.alert('Success', 'Passage updated successfully');
1096
-
} catch (error: any) {
1097
-
console.error('Failed to modify passage:', error);
1098
-
Alert.alert('Error', error.message || 'Failed to modify passage');
1099
-
} finally {
1100
-
setIsLoadingPassages(false);
1101
-
}
1102
-
};
1103
-
1104
-
const initializeCoFolder = async () => {
1105
-
if (!coAgent) return;
1106
-
1107
-
try {
1108
-
console.log('Initializing co folder...');
1109
-
1110
-
let folder: any = null;
1111
-
1112
-
// First, try to get cached folder ID
1113
-
const cachedFolderId = await Storage.getItem(STORAGE_KEYS.CO_FOLDER_ID);
1114
-
if (cachedFolderId) {
1115
-
console.log('Found cached folder ID:', cachedFolderId);
1116
-
try {
1117
-
// Try to get the folder by ID directly (we'll need to add this method)
1118
-
const folders = await lettaApi.listFolders({ name: 'co-app' });
1119
-
folder = folders.find(f => f.id === cachedFolderId);
1120
-
if (folder) {
1121
-
console.log('Using cached folder:', folder.id, folder.name);
1122
-
} else {
1123
-
console.log('Cached folder ID not found, will search...');
1124
-
await Storage.removeItem(STORAGE_KEYS.CO_FOLDER_ID);
1125
-
}
1126
-
} catch (error) {
1127
-
console.log('Failed to get cached folder, will search:', error);
1128
-
await Storage.removeItem(STORAGE_KEYS.CO_FOLDER_ID);
1129
-
}
1130
-
}
1131
-
1132
-
// If we don't have a cached folder, search for it
1133
-
if (!folder) {
1134
-
console.log('Searching for co-app folder...');
1135
-
const folders = await lettaApi.listFolders({ name: 'co-app' });
1136
-
console.log('Folder query result:', folders.length, 'folders');
1137
-
folder = folders.length > 0 ? folders[0] : null;
1138
-
console.log('Selected folder:', folder ? { id: folder.id, name: folder.name } : null);
1139
-
}
1140
-
1141
-
// If still no folder, create it
1142
-
if (!folder) {
1143
-
console.log('Creating co-app folder...');
1144
-
try {
1145
-
folder = await lettaApi.createFolder('co-app', 'Files shared with co');
1146
-
console.log('Folder created:', folder.id, 'name:', folder.name);
1147
-
} catch (createError: any) {
1148
-
// If 409 conflict, folder was created by another process - try to find it again
1149
-
if (createError.status === 409) {
1150
-
console.log('Folder already exists (409), retrying fetch...');
1151
-
const foldersRetry = await lettaApi.listFolders({ name: 'co-app' });
1152
-
console.log('Retry folder query result:', foldersRetry.length, 'folders');
1153
-
folder = foldersRetry.length > 0 ? foldersRetry[0] : null;
1154
-
if (!folder) {
1155
-
console.error('Folder "co-app" not found after 409 conflict');
1156
-
setFilesError('Folder "co-app" exists but could not be retrieved. Try refreshing.');
1157
-
return;
1158
-
}
1159
-
} else {
1160
-
throw createError;
1161
-
}
1162
-
}
1163
-
}
1164
-
1165
-
// Cache the folder ID for next time
1166
-
await Storage.setItem(STORAGE_KEYS.CO_FOLDER_ID, folder.id);
1167
-
console.log('Cached folder ID:', folder.id);
1168
-
1169
-
setCoFolder(folder);
1170
-
console.log('Co folder ready:', folder.id);
1171
-
1172
-
// Attach folder to agent if not already attached
1173
-
try {
1174
-
await lettaApi.attachFolderToAgent(coAgent.id, folder.id);
1175
-
console.log('Folder attached to agent');
1176
-
} catch (error: any) {
1177
-
// Might already be attached, ignore error
1178
-
console.log('Folder attach info:', error.message);
1179
-
}
1180
-
1181
-
// Load files
1182
-
await loadFolderFiles(folder.id);
1183
-
} catch (error: any) {
1184
-
console.error('Failed to initialize co folder:', error);
1185
-
setFilesError(error.message || 'Failed to initialize folder');
1186
-
}
1187
-
};
1188
-
1189
-
const loadFolderFiles = async (folderId?: string) => {
1190
-
const id = folderId || coFolder?.id;
1191
-
if (!id) return;
1192
-
1193
-
setIsLoadingFiles(true);
1194
-
setFilesError(null);
1195
-
try {
1196
-
const files = await lettaApi.listFolderFiles(id);
1197
-
setFolderFiles(files);
1198
-
} catch (error: any) {
1199
-
console.error('Failed to load files:', error);
1200
-
setFilesError(error.message || 'Failed to load files');
1201
-
} finally {
1202
-
setIsLoadingFiles(false);
1203
-
}
1204
-
};
1205
-
1206
-
const pickAndUploadFile = async () => {
1207
-
if (!coFolder) {
1208
-
Alert.alert('Error', 'Folder not initialized');
1209
-
return;
1210
-
}
1211
-
1212
-
try {
1213
-
// Create input element for file selection (web)
1214
-
const input = document.createElement('input');
1215
-
input.type = 'file';
1216
-
input.accept = '.pdf,.txt,.md,.json,.csv,.doc,.docx';
1217
-
1218
-
input.onchange = async (e: any) => {
1219
-
const file = e.target?.files?.[0];
1220
-
if (!file) return;
1221
-
1222
-
console.log('Selected file:', file.name, 'size:', file.size);
1223
-
1224
-
// Check file size (10MB limit)
1225
-
const MAX_SIZE = 10 * 1024 * 1024;
1226
-
if (file.size > MAX_SIZE) {
1227
-
Alert.alert('File Too Large', 'Maximum file size is 10MB');
1228
-
return;
1229
-
}
1230
-
1231
-
setIsUploadingFile(true);
1232
-
setUploadProgress(`Uploading ${file.name}...`);
1233
-
1234
-
// Show immediate feedback
1235
-
console.log(`Starting upload: ${file.name}`);
1236
-
1237
-
try {
1238
-
// Upload file - this returns the job info
1239
-
const result = await lettaApi.uploadFileToFolder(coFolder.id, file);
1240
-
console.log('Upload result:', result);
1241
-
1242
-
// The upload might complete immediately or return a job
1243
-
if (result.id && result.id.startsWith('file-')) {
1244
-
// It's a job ID - poll for completion
1245
-
setUploadProgress('Processing file...');
1246
-
let attempts = 0;
1247
-
const maxAttempts = 30; // 30 seconds max
1248
-
1249
-
while (attempts < maxAttempts) {
1250
-
await new Promise(resolve => setTimeout(resolve, 1000));
1251
-
1252
-
try {
1253
-
const status = await lettaApi.getJobStatus(result.id);
1254
-
console.log('Job status:', status.status);
1255
-
1256
-
if (status.status === 'completed') {
1257
-
console.log('File uploaded successfully');
1258
-
await loadFolderFiles();
1259
-
1260
-
// Close all open files to avoid flooding context
1261
-
if (coAgent) {
1262
-
try {
1263
-
await lettaApi.closeAllFiles(coAgent.id);
1264
-
console.log('Closed all open files after upload');
1265
-
} catch (err) {
1266
-
console.error('Failed to close files:', err);
1267
-
}
1268
-
}
1269
-
1270
-
setUploadProgress('');
1271
-
Alert.alert('Success', `${file.name} uploaded successfully`);
1272
-
break;
1273
-
} else if (status.status === 'failed') {
1274
-
throw new Error('Upload failed: ' + (status.metadata || 'Unknown error'));
1275
-
}
1276
-
} catch (jobError: any) {
1277
-
// If job not found (404), it might have completed already
1278
-
if (jobError.status === 404) {
1279
-
console.log('Job not found - assuming completed');
1280
-
await loadFolderFiles();
1281
-
1282
-
// Close all open files to avoid flooding context
1283
-
if (coAgent) {
1284
-
try {
1285
-
await lettaApi.closeAllFiles(coAgent.id);
1286
-
console.log('Closed all open files after upload');
1287
-
} catch (err) {
1288
-
console.error('Failed to close files:', err);
1289
-
}
1290
-
}
1291
-
1292
-
setUploadProgress('');
1293
-
Alert.alert('Success', `${file.name} uploaded successfully`);
1294
-
break;
1295
-
}
1296
-
throw jobError;
1297
-
}
1298
-
1299
-
attempts++;
1300
-
}
1301
-
1302
-
if (attempts >= maxAttempts) {
1303
-
throw new Error('Upload processing timed out');
1304
-
}
1305
-
} else {
1306
-
// Upload completed immediately
1307
-
console.log('File uploaded immediately');
1308
-
await loadFolderFiles();
1309
-
1310
-
// Close all open files to avoid flooding context
1311
-
if (coAgent) {
1312
-
try {
1313
-
await lettaApi.closeAllFiles(coAgent.id);
1314
-
console.log('Closed all open files after upload');
1315
-
} catch (err) {
1316
-
console.error('Failed to close files:', err);
1317
-
}
1318
-
}
1319
-
1320
-
setUploadProgress('');
1321
-
Alert.alert('Success', `${file.name} uploaded successfully`);
1322
-
}
1323
-
} catch (error: any) {
1324
-
console.error('Upload error:', error);
1325
-
setUploadProgress('');
1326
-
Alert.alert('Upload Failed', error.message || 'Failed to upload file');
1327
-
} finally {
1328
-
setIsUploadingFile(false);
1329
-
}
1330
-
};
1331
-
1332
-
input.click();
1333
-
} catch (error: any) {
1334
-
console.error('File picker error:', error);
1335
-
Alert.alert('Error', 'Failed to open file picker');
1336
-
}
1337
-
};
1338
-
1339
-
const deleteFile = async (fileId: string, fileName: string) => {
1340
-
if (!coFolder) return;
1341
-
1342
-
const confirmed = Platform.OS === 'web'
1343
-
? window.confirm(`Are you sure you want to delete "${fileName}"?`)
1344
-
: await new Promise<boolean>((resolve) => {
1345
-
Alert.alert(
1346
-
'Delete File',
1347
-
`Are you sure you want to delete "${fileName}"?`,
1348
-
[
1349
-
{ text: 'Cancel', style: 'cancel', onPress: () => resolve(false) },
1350
-
{ text: 'Delete', style: 'destructive', onPress: () => resolve(true) },
1351
-
]
1352
-
);
1353
-
});
1354
-
1355
-
if (!confirmed) return;
1356
-
1357
-
try {
1358
-
await lettaApi.deleteFile(coFolder.id, fileId);
1359
-
await loadFolderFiles();
1360
-
if (Platform.OS === 'web') {
1361
-
window.alert('File deleted successfully');
1362
-
} else {
1363
-
Alert.alert('Success', 'File deleted');
1364
-
}
1365
-
} catch (error: any) {
1366
-
console.error('Delete error:', error);
1367
-
if (Platform.OS === 'web') {
1368
-
window.alert('Failed to delete file: ' + (error.message || 'Unknown error'));
1369
-
} else {
1370
-
Alert.alert('Error', 'Failed to delete file: ' + (error.message || 'Unknown error'));
1371
-
}
1372
-
}
1373
-
};
1374
-
1375
-
useEffect(() => {
1376
-
if (coAgent && currentView === 'knowledge') {
1377
-
if (knowledgeTab === 'core') {
1378
-
loadMemoryBlocks();
1379
-
} else if (knowledgeTab === 'archival') {
1380
-
loadPassages(true);
1381
-
}
1382
-
}
1383
-
}, [coAgent, currentView, knowledgeTab]);
1384
-
1385
-
useEffect(() => {
1386
-
if (coAgent && sidebarVisible) {
1387
-
if (!coFolder) {
1388
-
initializeCoFolder();
1389
-
} else {
1390
-
loadFolderFiles();
1391
-
}
1392
-
}
1393
-
}, [coAgent, sidebarVisible]);
1394
-
1395
-
// Initialize folder when agent is ready
1396
-
useEffect(() => {
1397
-
if (coAgent && !coFolder) {
1398
-
initializeCoFolder();
1399
-
}
1400
-
}, [coAgent]);
1401
-
1402
-
// State for tracking expanded reasoning
1403
-
const [expandedReasoning, setExpandedReasoning] = useState<Set<string>>(new Set());
1404
-
const [expandedCompaction, setExpandedCompaction] = useState<Set<string>>(new Set());
1405
-
const [expandedToolReturns, setExpandedToolReturns] = useState<Set<string>>(new Set());
1406
-
1407
-
// Auto-expand reasoning blocks by default
1408
-
useEffect(() => {
1409
-
const reasoningMessageIds = messages
1410
-
.filter(msg => msg.reasoning && msg.reasoning.trim().length > 0)
1411
-
.map(msg => msg.id);
1412
-
1413
-
if (reasoningMessageIds.length > 0) {
1414
-
setExpandedReasoning(prev => {
1415
-
const next = new Set(prev);
1416
-
reasoningMessageIds.forEach(id => next.add(id));
1417
-
return next;
1418
-
});
1419
-
}
1420
-
}, [messages]);
1421
-
1422
-
// Animate sidebar
1423
-
useEffect(() => {
1424
-
Animated.timing(sidebarAnimRef, {
1425
-
toValue: sidebarVisible ? 1 : 0,
1426
-
duration: 250,
1427
-
useNativeDriver: false,
1428
-
}).start();
1429
-
}, [sidebarVisible]);
1430
-
1431
-
const toggleReasoning = useCallback((messageId: string) => {
1432
-
setExpandedReasoning(prev => {
1433
-
const next = new Set(prev);
1434
-
if (next.has(messageId)) {
1435
-
next.delete(messageId);
1436
-
} else {
1437
-
next.add(messageId);
1438
-
}
1439
-
return next;
1440
-
});
1441
-
}, []);
1442
-
1443
-
const toggleCompaction = useCallback((messageId: string) => {
1444
-
setExpandedCompaction(prev => {
1445
-
const next = new Set(prev);
1446
-
if (next.has(messageId)) {
1447
-
next.delete(messageId);
1448
-
} else {
1449
-
next.add(messageId);
1450
-
}
1451
-
return next;
1452
-
});
1453
-
}, []);
1454
-
1455
-
const toggleToolReturn = useCallback((messageId: string) => {
1456
-
setExpandedToolReturns(prev => {
1457
-
const next = new Set(prev);
1458
-
if (next.has(messageId)) {
1459
-
next.delete(messageId);
1460
-
} else {
1461
-
next.add(messageId);
1462
-
}
1463
-
return next;
1464
-
});
1465
-
}, []);
1466
-
1467
-
// Group messages for efficient FlatList rendering
1468
-
type MessageGroup =
1469
-
| { key: string; type: 'toolPair'; call: LettaMessage; ret?: LettaMessage; reasoning?: string }
1470
-
| { key: string; type: 'message'; message: LettaMessage; reasoning?: string };
1471
-
1472
-
// Helper to check if a tool call has a result
1473
-
const toolCallHasResult = useMemo(() => {
1474
-
const hasResultMap = new Map<string, boolean>();
1475
-
for (let i = 0; i < messages.length; i++) {
1476
-
const msg = messages[i];
1477
-
if (msg.message_type === 'tool_call_message') {
1478
-
// Check if the next message is a tool_return
1479
-
const nextMsg = messages[i + 1];
1480
-
hasResultMap.set(msg.id, nextMsg?.message_type === 'tool_return_message');
1481
-
}
1482
-
}
1483
-
return hasResultMap;
1484
-
}, [messages]);
1485
-
1486
-
const displayMessages = useMemo(() => {
1487
-
// Sort messages by created_at timestamp to ensure correct chronological order
1488
-
const sortedMessages = [...messages].sort((a, b) => {
1489
-
const timeA = new Date(a.created_at || 0).getTime();
1490
-
const timeB = new Date(b.created_at || 0).getTime();
1491
-
return timeA - timeB;
1492
-
});
1493
-
1494
-
// Filter out system messages and login/heartbeat messages
1495
-
const filtered = sortedMessages.filter(msg => {
1496
-
if (msg.message_type === 'system_message') return false;
1497
-
1498
-
if (msg.message_type === 'user_message' && msg.content) {
1499
-
try {
1500
-
const contentStr = typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content);
1501
-
const parsed = JSON.parse(contentStr);
1502
-
if (parsed?.type === 'login' || parsed?.type === 'heartbeat') {
1503
-
return false;
1504
-
}
1505
-
} catch {
1506
-
// Not JSON, keep the message
1507
-
}
1508
-
}
1509
-
1510
-
return true;
1511
-
});
1512
-
1513
-
// Limit to most recent messages to avoid rendering issues with large history
1514
-
// Keep enough for smooth scrolling but not so many that initial render is slow
1515
-
const MAX_DISPLAY_MESSAGES = 100;
1516
-
const limited = filtered.length > MAX_DISPLAY_MESSAGES
1517
-
? filtered.slice(-MAX_DISPLAY_MESSAGES) // Take LAST N messages (most recent)
1518
-
: filtered;
1519
-
1520
-
console.log('[DISPLAY] Total messages:', limited.length, filtered.length > MAX_DISPLAY_MESSAGES ? `(limited from ${filtered.length})` : '');
1521
-
limited.forEach((msg, idx) => {
1522
-
console.log(`[DISPLAY ${idx}] ${msg.message_type} - ${msg.id.substring(0, 8)} - reasoning: ${!!msg.reasoning}`);
1523
-
});
1524
-
1525
-
return limited;
1526
-
}, [messages]);
1527
-
1528
-
// Animate rainbow gradient for "co is thinking", input box, reasoning sections, and empty state
1529
-
useEffect(() => {
1530
-
if (isStreaming || isInputFocused || expandedReasoning.size > 0 || displayMessages.length === 0) {
1531
-
rainbowAnimValue.setValue(0);
1532
-
Animated.loop(
1533
-
Animated.timing(rainbowAnimValue, {
1534
-
toValue: 1,
1535
-
duration: 3000,
1536
-
useNativeDriver: false,
1537
-
})
1538
-
).start();
1539
-
} else {
1540
-
rainbowAnimValue.stopAnimation();
1541
-
}
1542
-
}, [isStreaming, isInputFocused, expandedReasoning, displayMessages.length]);
1543
-
1544
-
const renderMessage = useCallback(({ item }: { item: LettaMessage }) => {
1545
-
const msg = item;
1546
-
const isUser = msg.message_type === 'user_message';
1547
-
const isSystem = msg.message_type === 'system_message';
1548
-
const isToolCall = msg.message_type === 'tool_call_message';
1549
-
const isToolReturn = msg.message_type === 'tool_return_message';
1550
-
const isAssistant = msg.message_type === 'assistant_message';
1551
-
const isReasoning = msg.message_type === 'reasoning_message';
1552
-
1553
-
if (isSystem) return null;
1554
-
1555
-
// Handle reasoning messages
1556
-
if (isReasoning) {
1557
-
const isReasoningExpanded = expandedReasoning.has(msg.id);
1558
-
1559
-
return (
1560
-
<View style={styles.messageContainer}>
1561
-
<ReasoningToggle
1562
-
reasoning={msg.reasoning || ''}
1563
-
messageId={msg.id}
1564
-
isExpanded={isReasoningExpanded}
1565
-
onToggle={() => toggleReasoning(msg.id)}
1566
-
isDark={colorScheme === 'dark'}
1567
-
/>
1568
-
</View>
1569
-
);
1570
-
}
1571
-
1572
-
// Handle tool calls - find and render with their result
1573
-
if (isToolCall) {
1574
-
// Find the corresponding tool return (next message in the list)
1575
-
const msgIndex = displayMessages.findIndex(m => m.id === msg.id);
1576
-
const nextMsg = msgIndex >= 0 && msgIndex < displayMessages.length - 1 ? displayMessages[msgIndex + 1] : null;
1577
-
const toolReturn = nextMsg && nextMsg.message_type === 'tool_return_message' ? nextMsg : null;
1578
-
1579
-
return (
1580
-
<View style={styles.messageContainer}>
1581
-
<ToolCallItem
1582
-
callText={msg.content}
1583
-
resultText={toolReturn?.content}
1584
-
reasoning={msg.reasoning}
1585
-
hasResult={!!toolReturn}
1586
-
isDark={colorScheme === 'dark'}
1587
-
/>
1588
-
</View>
1589
-
);
1590
-
}
1591
-
1592
-
// Skip tool returns - they're rendered with their tool call
1593
-
if (isToolReturn) {
1594
-
// Check if previous message is a tool call
1595
-
const msgIndex = displayMessages.findIndex(m => m.id === msg.id);
1596
-
const prevMsg = msgIndex > 0 ? displayMessages[msgIndex - 1] : null;
1597
-
if (prevMsg && prevMsg.message_type === 'tool_call_message') {
1598
-
return null; // Already rendered with the tool call
1599
-
}
1600
-
1601
-
// Orphaned tool return (no matching tool call) - render it standalone
1602
-
const isExpanded = expandedToolReturns.has(msg.id);
1603
-
return (
1604
-
<View style={styles.messageContainer}>
1605
-
<View style={styles.toolReturnContainer}>
1606
-
<TouchableOpacity
1607
-
style={styles.toolReturnHeader}
1608
-
onPress={() => toggleToolReturn(msg.id)}
1609
-
activeOpacity={0.7}
1610
-
>
1611
-
<Ionicons
1612
-
name={isExpanded ? 'chevron-down' : 'chevron-forward'}
1613
-
size={12}
1614
-
color={darkTheme.colors.text.tertiary}
1615
-
/>
1616
-
<Text style={styles.toolReturnLabel}>Result (orphaned)</Text>
1617
-
</TouchableOpacity>
1618
-
{isExpanded && (
1619
-
<View style={styles.toolReturnContent}>
1620
-
<MessageContent content={msg.content} isUser={false} isDark={colorScheme === 'dark'} />
1621
-
</View>
1622
-
)}
1623
-
</View>
1624
-
</View>
1625
-
);
1626
-
}
1627
-
1628
-
if (isUser) {
1629
-
// Check if this is a system_alert compaction message
1630
-
let isCompactionAlert = false;
1631
-
let compactionMessage = '';
1632
-
try {
1633
-
const parsed = JSON.parse(msg.content);
1634
-
if (parsed?.type === 'system_alert') {
1635
-
isCompactionAlert = true;
1636
-
// Extract the message field from the embedded JSON in the message text
1637
-
const messageText = parsed.message || '';
1638
-
// Try to extract JSON from the message (it's usually in a code block)
1639
-
const jsonMatch = messageText.match(/```json\s*(\{[\s\S]*?\})\s*```/);
1640
-
if (jsonMatch) {
1641
-
try {
1642
-
const innerJson = JSON.parse(jsonMatch[1]);
1643
-
compactionMessage = innerJson.message || messageText;
1644
-
} catch {
1645
-
compactionMessage = messageText;
1646
-
}
1647
-
} else {
1648
-
compactionMessage = messageText;
1649
-
}
1650
-
1651
-
// Strip out the "Note: prior messages..." preamble
1652
-
compactionMessage = compactionMessage.replace(/^Note: prior messages have been hidden from view.*?The following is a summary of the previous messages:\s*/is, '');
1653
-
}
1654
-
} catch {
1655
-
// Not JSON, treat as normal user message
1656
-
}
1657
-
1658
-
if (isCompactionAlert) {
1659
-
// Hide compaction if user has disabled it in settings
1660
-
if (!showCompaction) {
1661
-
return null;
1662
-
}
1663
-
1664
-
// Render compaction alert as thin grey expandable line
1665
-
const isCompactionExpanded = expandedCompaction.has(msg.id);
1666
-
1667
-
return (
1668
-
<View key={item.key} style={styles.compactionContainer}>
1669
-
<TouchableOpacity
1670
-
onPress={() => toggleCompaction(msg.id)}
1671
-
style={styles.compactionLine}
1672
-
activeOpacity={0.7}
1673
-
>
1674
-
<View style={styles.compactionDivider} />
1675
-
<Text style={styles.compactionLabel}>compaction</Text>
1676
-
<View style={styles.compactionDivider} />
1677
-
<Ionicons
1678
-
name={isCompactionExpanded ? 'chevron-up' : 'chevron-down'}
1679
-
size={12}
1680
-
color={(colorScheme === 'dark' ? darkTheme : lightTheme).colors.text.tertiary}
1681
-
style={styles.compactionChevron}
1682
-
/>
1683
-
</TouchableOpacity>
1684
-
{isCompactionExpanded && (
1685
-
<View style={styles.compactionMessageContainer}>
1686
-
<MessageContent content={compactionMessage} />
1687
-
</View>
1688
-
)}
1689
-
</View>
1690
-
);
1691
-
}
1692
-
1693
-
// Parse message content to check for multipart (images)
1694
-
let textContent: string = '';
1695
-
let imageContent: Array<{type: string, source: {type: string, data: string, mediaType: string}}> = [];
1696
-
1697
-
if (typeof msg.content === 'object' && Array.isArray(msg.content)) {
1698
-
// Multipart message with images
1699
-
imageContent = msg.content.filter((item: any) => item.type === 'image');
1700
-
const textParts = msg.content.filter((item: any) => item.type === 'text');
1701
-
textContent = textParts.map((item: any) => item.text || '').filter(t => t).join('\n');
1702
-
} else if (typeof msg.content === 'string') {
1703
-
textContent = msg.content;
1704
-
} else {
1705
-
// Fallback: convert to string
1706
-
textContent = String(msg.content || '');
1707
-
}
1708
-
1709
-
// Skip rendering if no content at all
1710
-
if (!textContent.trim() && imageContent.length === 0) {
1711
-
return null;
1712
-
}
1713
-
1714
-
return (
1715
-
<View
1716
-
key={item.key}
1717
-
style={[styles.messageContainer, styles.userMessageContainer]}
1718
-
>
1719
-
<View
1720
-
style={[
1721
-
styles.messageBubble,
1722
-
styles.userBubble,
1723
-
{ backgroundColor: colorScheme === 'dark' ? CoColors.pureWhite : CoColors.deepBlack }
1724
-
]}
1725
-
// @ts-ignore - web-only data attribute for CSS targeting
1726
-
dataSet={{ userMessage: 'true' }}
1727
-
>
1728
-
{/* Display images */}
1729
-
{imageContent.length > 0 && (
1730
-
<View style={styles.messageImagesContainer}>
1731
-
{imageContent.map((img: any, idx: number) => {
1732
-
const uri = img.source.type === 'url'
1733
-
? img.source.url
1734
-
: `data:${img.source.media_type || img.source.mediaType};base64,${img.source.data}`;
1735
-
1736
-
return (
1737
-
<Image
1738
-
key={idx}
1739
-
source={{ uri }}
1740
-
style={styles.messageImage}
1741
-
/>
1742
-
);
1743
-
})}
1744
-
</View>
1745
-
)}
1746
-
1747
-
{/* Display text content */}
1748
-
{textContent.trim().length > 0 && (
1749
-
<ExpandableMessageContent
1750
-
content={textContent}
1751
-
isUser={isUser}
1752
-
isDark={colorScheme === 'dark'}
1753
-
lineLimit={3}
1754
-
/>
1755
-
)}
1756
-
</View>
1757
-
</View>
1758
-
);
1759
-
} else {
1760
-
const isReasoningExpanded = expandedReasoning.has(msg.id);
1761
-
const isLastMessage = displayMessages[displayMessages.length - 1]?.id === msg.id;
1762
-
const shouldHaveMinHeight = isLastMessage && lastMessageNeedsSpace;
1763
-
1764
-
return (
1765
-
<View style={[
1766
-
styles.assistantFullWidthContainer,
1767
-
shouldHaveMinHeight && { minHeight: Math.max(containerHeight * 0.9, 450) }
1768
-
]}>
1769
-
{msg.reasoning && (
1770
-
<ReasoningToggle
1771
-
reasoning={msg.reasoning}
1772
-
messageId={msg.id}
1773
-
isExpanded={isReasoningExpanded}
1774
-
onToggle={() => toggleReasoning(msg.id)}
1775
-
isDark={colorScheme === 'dark'}
1776
-
/>
1777
-
)}
1778
-
<Text style={[styles.assistantLabel, { color: theme.colors.text.primary }]}>(co said)</Text>
1779
-
<View style={{ position: 'relative' }}>
1780
-
<ExpandableMessageContent
1781
-
content={msg.content}
1782
-
isUser={isUser}
1783
-
isDark={colorScheme === 'dark'}
1784
-
lineLimit={20}
1785
-
/>
1786
-
<View style={styles.copyButtonContainer}>
1787
-
<TouchableOpacity
1788
-
onPress={() => copyToClipboard(msg.content, msg.id)}
1789
-
style={styles.copyButton}
1790
-
activeOpacity={0.7}
1791
-
testID="copy-button"
1792
-
>
1793
-
<Ionicons
1794
-
name={copiedMessageId === msg.id ? "checkmark-outline" : "copy-outline"}
1795
-
size={16}
1796
-
color={copiedMessageId === msg.id ? (colorScheme === 'dark' ? darkTheme : lightTheme).colors.interactive.primary : (colorScheme === 'dark' ? darkTheme : lightTheme).colors.text.tertiary}
1797
-
/>
1798
-
</TouchableOpacity>
1799
-
</View>
1800
-
</View>
1801
-
</View>
1802
-
);
1803
-
}
1804
-
1805
-
return null;
1806
-
}, [expandedCompaction, expandedReasoning, expandedToolReturns, displayMessages, lastMessageNeedsSpace, containerHeight, colorScheme, copiedMessageId, toggleCompaction, toggleReasoning, toggleToolReturn, copyToClipboard, toolCallHasResult]);
1807
-
1808
-
const keyExtractor = useCallback((item: LettaMessage) => `${item.id}-${item.message_type}`, []);
1809
-
1810
-
const handleScroll = useCallback((e: any) => {
1811
-
const y = e.nativeEvent.contentOffset.y;
1812
-
setScrollY(y);
1813
-
const threshold = 80;
1814
-
const distanceFromBottom = Math.max(0, contentHeight - (y + containerHeight));
1815
-
setShowScrollToBottom(distanceFromBottom > threshold);
1816
-
}, [contentHeight, containerHeight]);
1817
-
1818
-
const handleContentSizeChange = useCallback((_w: number, h: number) => {
1819
-
setContentHeight(h);
1820
-
if (pendingJumpToBottomRef.current && containerHeight > 0 && pendingJumpRetriesRef.current > 0) {
1821
-
const offset = Math.max(0, h - containerHeight);
1822
-
scrollViewRef.current?.scrollToOffset({ offset, animated: false });
1823
-
setShowScrollToBottom(false);
1824
-
pendingJumpRetriesRef.current -= 1;
1825
-
if (pendingJumpRetriesRef.current <= 0) pendingJumpToBottomRef.current = false;
1826
-
}
1827
-
}, [containerHeight]);
1828
-
1829
-
const handleMessagesLayout = (e: any) => {
1830
-
const h = e.nativeEvent.layout.height;
1831
-
setContainerHeight(h);
1832
-
if (pendingJumpToBottomRef.current && contentHeight > 0 && pendingJumpRetriesRef.current > 0) {
1833
-
const offset = Math.max(0, contentHeight - h);
1834
-
scrollViewRef.current?.scrollToOffset({ offset, animated: false });
1835
-
setShowScrollToBottom(false);
1836
-
pendingJumpRetriesRef.current -= 1;
1837
-
if (pendingJumpRetriesRef.current <= 0) pendingJumpToBottomRef.current = false;
1838
-
}
1839
-
};
1840
-
1841
-
const scrollToBottom = () => {
1842
-
scrollViewRef.current?.scrollToEnd({ animated: true });
1843
-
setShowScrollToBottom(false);
1844
-
};
1845
-
1846
-
const handleInputLayout = useCallback((e: any) => {
1847
-
setInputContainerHeight(e.nativeEvent.layout.height || 0);
1848
-
}, []);
1849
-
1850
-
const handleInputFocusChange = useCallback((focused: boolean) => {
1851
-
setIsInputFocused(focused);
1852
-
}, []);
1853
-
1854
-
const inputWrapperStyle = useMemo(() => ({
1855
-
borderRadius: 24,
1856
-
borderWidth: 2,
1857
-
borderColor: colorScheme === 'dark' ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.1)',
1858
-
shadowColor: '#000',
1859
-
shadowOffset: { width: 0, height: 2 },
1860
-
shadowOpacity: 0.1,
1861
-
shadowRadius: 8,
1862
-
elevation: 2,
1863
-
}), [colorScheme]);
1864
-
1865
-
const sendButtonStyle = useMemo(() => ({
1866
-
backgroundColor: (!hasInputText && selectedImages.length === 0) || isSendingMessage
1867
-
? 'transparent'
1868
-
: colorScheme === 'dark' ? CoColors.pureWhite : CoColors.deepBlack
1869
-
}), [hasInputText, selectedImages.length, isSendingMessage, colorScheme]);
1870
-
1871
-
const sendIconColor = useMemo(() =>
1872
-
(!hasInputText && selectedImages.length === 0)
1873
-
? '#444444'
1874
-
: colorScheme === 'dark' ? CoColors.deepBlack : CoColors.pureWhite
1875
-
, [hasInputText, selectedImages.length, colorScheme]);
1876
-
1877
-
if (isLoadingToken || !fontsLoaded) {
1878
-
return (
1879
-
<SafeAreaView style={[styles.loadingContainer, { backgroundColor: theme.colors.background.primary }]}>
1880
-
<ActivityIndicator size="large" color={theme.colors.interactive.primary} />
1881
-
<StatusBar style={colorScheme === 'dark' ? 'light' : 'dark'} />
1882
-
</SafeAreaView>
1883
-
);
1884
-
}
1885
-
1886
-
if (!isConnected) {
1887
-
return (
1888
-
<CoLoginScreen
1889
-
onLogin={handleLogin}
1890
-
isLoading={isConnecting}
1891
-
error={connectionError}
1892
-
/>
1893
-
);
1894
-
}
1895
-
1896
-
if (isRefreshingCo) {
1897
-
return (
1898
-
<SafeAreaView style={[styles.loadingContainer, { backgroundColor: theme.colors.background.primary }]}>
1899
-
<ActivityIndicator size="large" color={theme.colors.interactive.primary} />
1900
-
<Text style={[styles.loadingText, { color: theme.colors.text.secondary }]}>Refreshing co...</Text>
1901
-
<StatusBar style={colorScheme === 'dark' ? 'light' : 'dark'} />
1902
-
</SafeAreaView>
1903
-
);
1904
-
}
1905
-
1906
-
if (isInitializingCo || !coAgent) {
1907
-
return (
1908
-
<SafeAreaView style={[styles.loadingContainer, { backgroundColor: theme.colors.background.primary }]}>
1909
-
<ActivityIndicator size="large" color={theme.colors.interactive.primary} />
1910
-
<Text style={[styles.loadingText, { color: theme.colors.text.secondary }]}>Initializing co...</Text>
1911
-
<StatusBar style={colorScheme === 'dark' ? 'light' : 'dark'} />
1912
-
</SafeAreaView>
1913
-
);
1914
-
}
1915
-
1916
-
// Main chat view
1917
-
return (
1918
-
<View
1919
-
style={[styles.container, { backgroundColor: theme.colors.background.primary }]}
1920
-
// @ts-ignore - web-only data attribute
1921
-
dataSet={{ theme: colorScheme }}
1922
-
>
1923
-
{/* Sidebar */}
1924
-
<Animated.View
1925
-
style={[
1926
-
styles.sidebarContainer,
1927
-
{
1928
-
paddingTop: insets.top,
1929
-
backgroundColor: theme.colors.background.secondary,
1930
-
borderRightColor: theme.colors.border.primary,
1931
-
width: sidebarAnimRef.interpolate({
1932
-
inputRange: [0, 1],
1933
-
outputRange: [0, 280],
1934
-
}),
1935
-
},
1936
-
]}
1937
-
>
1938
-
<View style={styles.sidebarHeader}>
1939
-
<Text style={[styles.sidebarTitle, { color: theme.colors.text.primary }]}>Menu</Text>
1940
-
<TouchableOpacity onPress={() => setSidebarVisible(false)} style={styles.closeSidebar}>
1941
-
<Ionicons name="close" size={24} color={theme.colors.text.primary} />
1942
-
</TouchableOpacity>
1943
-
</View>
1944
-
1945
-
<FlatList
1946
-
style={{ flex: 1 }}
1947
-
contentContainerStyle={{ flexGrow: 1 }}
1948
-
ListHeaderComponent={
1949
-
<View style={styles.menuItems}>
1950
-
<TouchableOpacity
1951
-
style={[styles.menuItem, { borderBottomColor: theme.colors.border.primary }]}
1952
-
onPress={() => {
1953
-
setCurrentView('knowledge');
1954
-
loadMemoryBlocks();
1955
-
}}
1956
-
>
1957
-
<Ionicons name="library-outline" size={24} color={theme.colors.text.primary} />
1958
-
<Text style={[styles.menuItemText, { color: theme.colors.text.primary }]}>Memory</Text>
1959
-
</TouchableOpacity>
1960
-
1961
-
<TouchableOpacity
1962
-
style={[styles.menuItem, { borderBottomColor: theme.colors.border.primary }]}
1963
-
onPress={() => {
1964
-
setCurrentView('settings');
1965
-
}}
1966
-
>
1967
-
<Ionicons name="settings-outline" size={24} color={theme.colors.text.primary} />
1968
-
<Text style={[styles.menuItemText, { color: theme.colors.text.primary }]}>Settings</Text>
1969
-
</TouchableOpacity>
1970
-
1971
-
<TouchableOpacity
1972
-
style={[styles.menuItem, { borderBottomColor: theme.colors.border.primary }]}
1973
-
onPress={() => {
1974
-
toggleColorScheme();
1975
-
}}
1976
-
>
1977
-
<Ionicons
1978
-
name={colorScheme === 'dark' ? 'sunny-outline' : 'moon-outline'}
1979
-
size={24}
1980
-
color={theme.colors.text.primary}
1981
-
/>
1982
-
<Text style={[styles.menuItemText, { color: theme.colors.text.primary }]}>
1983
-
{colorScheme === 'dark' ? 'Light Mode' : 'Dark Mode'}
1984
-
</Text>
1985
-
</TouchableOpacity>
1986
-
1987
-
<TouchableOpacity
1988
-
style={[styles.menuItem, { borderBottomColor: theme.colors.border.primary }]}
1989
-
onPress={() => {
1990
-
if (coAgent) {
1991
-
Linking.openURL(`https://app.letta.com/agents/${coAgent.id}`);
1992
-
}
1993
-
}}
1994
-
disabled={!coAgent}
1995
-
>
1996
-
<Ionicons name="open-outline" size={24} color={theme.colors.text.primary} />
1997
-
<Text style={[styles.menuItemText, { color: theme.colors.text.primary }]}>Open in Browser</Text>
1998
-
</TouchableOpacity>
1999
-
2000
-
{developerMode && (
2001
-
<TouchableOpacity
2002
-
style={[styles.menuItem, { borderBottomColor: theme.colors.border.primary }]}
2003
-
onPress={async () => {
2004
-
console.log('Refresh Co button pressed');
2005
-
const confirmed = Platform.OS === 'web'
2006
-
? window.confirm('This will delete the current co agent and create a new one. All conversation history will be lost. Are you sure?')
2007
-
: await new Promise<boolean>((resolve) => {
2008
-
Alert.alert(
2009
-
'Refresh Co Agent',
2010
-
'This will delete the current co agent and create a new one. All conversation history will be lost. Are you sure?',
2011
-
[
2012
-
{ text: 'Cancel', style: 'cancel', onPress: () => resolve(false) },
2013
-
{ text: 'Refresh', style: 'destructive', onPress: () => resolve(true) },
2014
-
]
2015
-
);
2016
-
});
2017
-
2018
-
if (!confirmed) return;
2019
-
2020
-
console.log('Refresh confirmed, starting process...');
2021
-
setSidebarVisible(false);
2022
-
setIsRefreshingCo(true);
2023
-
try {
2024
-
if (coAgent) {
2025
-
console.log('Deleting agent:', coAgent.id);
2026
-
const deleteResult = await lettaApi.deleteAgent(coAgent.id);
2027
-
console.log('Delete result:', deleteResult);
2028
-
console.log('Agent deleted successfully, clearing state...');
2029
-
setCoAgent(null);
2030
-
setMessages([]);
2031
-
setEarliestCursor(null);
2032
-
setHasMoreBefore(false);
2033
-
console.log('Initializing new co agent...');
2034
-
await initializeCo();
2035
-
console.log('Co agent refreshed successfully');
2036
-
setIsRefreshingCo(false);
2037
-
if (Platform.OS === 'web') {
2038
-
window.alert('Co agent refreshed successfully');
2039
-
} else {
2040
-
Alert.alert('Success', 'Co agent refreshed successfully');
2041
-
}
2042
-
}
2043
-
} catch (error: any) {
2044
-
console.error('=== ERROR REFRESHING CO ===');
2045
-
console.error('Error type:', typeof error);
2046
-
console.error('Error message:', error?.message);
2047
-
console.error('Error stack:', error?.stack);
2048
-
console.error('Full error:', error);
2049
-
setIsRefreshingCo(false);
2050
-
if (Platform.OS === 'web') {
2051
-
window.alert('Failed to refresh co: ' + (error.message || 'Unknown error'));
2052
-
} else {
2053
-
Alert.alert('Error', 'Failed to refresh co: ' + (error.message || 'Unknown error'));
2054
-
}
2055
-
}
2056
-
}}
2057
-
>
2058
-
<Ionicons name="refresh-outline" size={24} color={theme.colors.status.error} />
2059
-
<Text style={[styles.menuItemText, { color: theme.colors.status.error }]}>Refresh Co</Text>
2060
-
</TouchableOpacity>
2061
-
)}
2062
-
2063
-
<TouchableOpacity
2064
-
style={[styles.menuItem, { borderBottomColor: theme.colors.border.primary }]}
2065
-
onPress={() => {
2066
-
setSidebarVisible(false);
2067
-
handleLogout();
2068
-
}}
2069
-
>
2070
-
<Ionicons name="log-out-outline" size={24} color={theme.colors.text.primary} />
2071
-
<Text style={[styles.menuItemText, { color: theme.colors.text.primary }]}>Logout</Text>
2072
-
</TouchableOpacity>
2073
-
</View>
2074
-
}
2075
-
data={[]}
2076
-
renderItem={() => null}
2077
-
/>
2078
-
</Animated.View>
2079
-
2080
-
{/* Main content area */}
2081
-
<View style={styles.mainContent}>
2082
-
{/* Header */}
2083
-
<View style={[
2084
-
styles.header,
2085
-
{ paddingTop: insets.top, backgroundColor: theme.colors.background.secondary, borderBottomColor: theme.colors.border.primary },
2086
-
displayMessages.length === 0 && { backgroundColor: 'transparent', borderBottomWidth: 0 }
2087
-
]}>
2088
-
<TouchableOpacity onPress={() => setSidebarVisible(!sidebarVisible)} style={styles.menuButton}>
2089
-
<Ionicons name="menu" size={24} color={colorScheme === 'dark' ? '#FFFFFF' : theme.colors.text.primary} />
2090
-
</TouchableOpacity>
2091
-
2092
-
{displayMessages.length > 0 && (
2093
-
<>
2094
-
<View style={styles.headerCenter}>
2095
-
<TouchableOpacity
2096
-
onPress={() => {
2097
-
setHeaderClickCount(prev => prev + 1);
2098
-
2099
-
if (headerClickTimeoutRef.current) {
2100
-
clearTimeout(headerClickTimeoutRef.current);
2101
-
}
2102
-
2103
-
headerClickTimeoutRef.current = setTimeout(() => {
2104
-
if (headerClickCount >= 6) {
2105
-
setDeveloperMode(!developerMode);
2106
-
if (Platform.OS === 'web') {
2107
-
window.alert(developerMode ? 'Developer mode disabled' : 'Developer mode enabled');
2108
-
} else {
2109
-
Alert.alert('Developer Mode', developerMode ? 'Disabled' : 'Enabled');
2110
-
}
2111
-
}
2112
-
setHeaderClickCount(0);
2113
-
}, 2000);
2114
-
}}
2115
-
>
2116
-
<Text style={[styles.headerTitle, { color: theme.colors.text.primary }]}>co</Text>
2117
-
</TouchableOpacity>
2118
-
</View>
2119
-
2120
-
<View style={styles.headerSpacer} />
2121
-
</>
2122
-
)}
2123
-
</View>
2124
-
2125
-
{/* View Switcher - hidden when chat is empty */}
2126
-
{displayMessages.length > 0 && (
2127
-
<View style={[styles.viewSwitcher, { backgroundColor: theme.colors.background.secondary }]}>
2128
-
<TouchableOpacity
2129
-
style={[
2130
-
styles.viewSwitcherButton,
2131
-
currentView === 'you' && { backgroundColor: theme.colors.background.tertiary }
2132
-
]}
2133
-
onPress={() => {
2134
-
setCurrentView('you');
2135
-
loadMemoryBlocks();
2136
-
}}
2137
-
>
2138
-
<Text style={[
2139
-
styles.viewSwitcherText,
2140
-
{ color: currentView === 'you' ? theme.colors.text.primary : theme.colors.text.tertiary }
2141
-
]}>You</Text>
2142
-
</TouchableOpacity>
2143
-
<TouchableOpacity
2144
-
style={[
2145
-
styles.viewSwitcherButton,
2146
-
currentView === 'chat' && { backgroundColor: theme.colors.background.tertiary }
2147
-
]}
2148
-
onPress={() => setCurrentView('chat')}
2149
-
>
2150
-
<Text style={[
2151
-
styles.viewSwitcherText,
2152
-
{ color: currentView === 'chat' ? theme.colors.text.primary : theme.colors.text.tertiary }
2153
-
]}>Chat</Text>
2154
-
</TouchableOpacity>
2155
-
<TouchableOpacity
2156
-
style={[
2157
-
styles.viewSwitcherButton,
2158
-
currentView === 'knowledge' && { backgroundColor: theme.colors.background.tertiary }
2159
-
]}
2160
-
onPress={() => {
2161
-
setCurrentView('knowledge');
2162
-
loadMemoryBlocks();
2163
-
}}
2164
-
>
2165
-
<Text style={[
2166
-
styles.viewSwitcherText,
2167
-
{ color: currentView === 'knowledge' ? theme.colors.text.primary : theme.colors.text.tertiary }
2168
-
]}>Memory</Text>
2169
-
</TouchableOpacity>
2170
-
</View>
2171
-
)}
2172
-
2173
-
{/* View Content */}
2174
-
<KeyboardAvoidingView
2175
-
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
2176
-
style={styles.chatRow}
2177
-
keyboardVerticalOffset={Platform.OS === 'ios' ? insets.top + 60 : 0}
2178
-
enabled={Platform.OS === 'ios'}
2179
-
>
2180
-
{/* You View */}
2181
-
<View style={[styles.memoryViewContainer, { display: currentView === 'you' ? 'flex' : 'none', backgroundColor: theme.colors.background.primary }]}>
2182
-
{!hasCheckedYouBlock ? (
2183
-
/* Loading state - checking for You block */
2184
-
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
2185
-
<ActivityIndicator size="large" color={theme.colors.text.primary} />
2186
-
</View>
2187
-
) : !hasYouBlock ? (
2188
-
/* Empty state - no You block */
2189
-
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center', padding: 40 }}>
2190
-
<Text style={{
2191
-
fontSize: 24,
2192
-
fontWeight: 'bold',
2193
-
color: theme.colors.text.primary,
2194
-
marginBottom: 40,
2195
-
textAlign: 'center',
2196
-
}}>
2197
-
Want to understand yourself?
2198
-
</Text>
2199
-
<TouchableOpacity
2200
-
onPress={createYouBlock}
2201
-
disabled={isCreatingYouBlock}
2202
-
style={{
2203
-
width: 80,
2204
-
height: 80,
2205
-
borderRadius: 40,
2206
-
backgroundColor: theme.colors.background.tertiary,
2207
-
borderWidth: 2,
2208
-
borderColor: theme.colors.border.primary,
2209
-
justifyContent: 'center',
2210
-
alignItems: 'center',
2211
-
}}
2212
-
>
2213
-
{isCreatingYouBlock ? (
2214
-
<ActivityIndicator size="large" color={theme.colors.text.primary} />
2215
-
) : (
2216
-
<Ionicons name="add" size={48} color={theme.colors.text.primary} />
2217
-
)}
2218
-
</TouchableOpacity>
2219
-
</View>
2220
-
) : (
2221
-
/* You block exists - show content */
2222
-
<ScrollView
2223
-
style={{ flex: 1 }}
2224
-
contentContainerStyle={{
2225
-
maxWidth: 700,
2226
-
width: '100%',
2227
-
alignSelf: 'center',
2228
-
padding: 20,
2229
-
}}
2230
-
>
2231
-
<Markdown style={createMarkdownStyles({ isUser: false, isDark: colorScheme === 'dark' })}>
2232
-
{youBlockContent}
2233
-
</Markdown>
2234
-
</ScrollView>
2235
-
)}
2236
-
</View>
2237
-
2238
-
{/* Chat View */}
2239
-
<View style={{ display: currentView === 'chat' ? 'flex' : 'none', flex: 1 }}>
2240
-
{/* Messages */}
2241
-
<View style={styles.messagesContainer} onLayout={handleMessagesLayout}>
2242
-
<FlatList
2243
-
ref={scrollViewRef}
2244
-
data={displayMessages}
2245
-
renderItem={renderMessage}
2246
-
keyExtractor={keyExtractor}
2247
-
extraData={{ showCompaction, expandedReasoning, expandedCompaction, copiedMessageId }}
2248
-
onScroll={handleScroll}
2249
-
onContentSizeChange={handleContentSizeChange}
2250
-
maintainVisibleContentPosition={{
2251
-
minIndexForVisible: 0,
2252
-
autoscrollToTopThreshold: 10,
2253
-
}}
2254
-
windowSize={50}
2255
-
removeClippedSubviews={false}
2256
-
maxToRenderPerBatch={20}
2257
-
updateCellsBatchingPeriod={50}
2258
-
initialNumToRender={100}
2259
-
contentContainerStyle={[
2260
-
styles.messagesList,
2261
-
displayMessages.length === 0 && { flexGrow: 1 },
2262
-
{
2263
-
paddingBottom: 120
2264
-
}
2265
-
]}
2266
-
ListHeaderComponent={
2267
-
hasMoreBefore ? (
2268
-
<TouchableOpacity onPress={loadMoreMessages} style={styles.loadMoreButton}>
2269
-
{isLoadingMore ? (
2270
-
<ActivityIndicator size="small" color={theme.colors.text.secondary} />
2271
-
) : (
2272
-
<Text style={styles.loadMoreText}>Load more messages</Text>
2273
-
)}
2274
-
</TouchableOpacity>
2275
-
) : null
2276
-
}
2277
-
ListFooterComponent={
2278
-
<>
2279
-
{isStreaming && (
2280
-
<Animated.View style={[styles.assistantFullWidthContainer, { minHeight: spacerHeightAnim, opacity: statusFadeAnim }]}>
2281
-
{/* Streaming Block - show completed blocks + current stream content */}
2282
-
2283
-
{/* Show completed blocks first (in chronological order) */}
2284
-
{completedStreamBlocks.map((block, index) => {
2285
-
if (block.type === 'reasoning') {
2286
-
return (
2287
-
<React.Fragment key={`completed-${index}`}>
2288
-
<View style={styles.reasoningStreamingContainer}>
2289
-
<Text style={styles.reasoningStreamingText}>{block.content}</Text>
2290
-
</View>
2291
-
</React.Fragment>
2292
-
);
2293
-
} else if (block.type === 'assistant_message') {
2294
-
return (
2295
-
<React.Fragment key={`completed-${index}`}>
2296
-
<View style={{ flex: 1 }}>
2297
-
<MessageContent
2298
-
content={block.content}
2299
-
isUser={false}
2300
-
isDark={colorScheme === 'dark'}
2301
-
/>
2302
-
</View>
2303
-
<View style={styles.messageSeparator} />
2304
-
</React.Fragment>
2305
-
);
2306
-
}
2307
-
return null;
2308
-
})}
2309
-
2310
-
{/* Show current reasoning being accumulated */}
2311
-
{currentStream.reasoning && (
2312
-
<>
2313
-
<View style={styles.reasoningStreamingContainer}>
2314
-
<Text style={styles.reasoningStreamingText}>{currentStream.reasoning}</Text>
2315
-
</View>
2316
-
</>
2317
-
)}
2318
-
2319
-
{/* Show tool calls if we have any */}
2320
-
{currentStream.toolCalls.map((toolCall) => (
2321
-
<View key={toolCall.id} style={styles.toolCallStreamingContainer}>
2322
-
<ToolCallItem
2323
-
callText={toolCall.args}
2324
-
hasResult={false}
2325
-
isDark={colorScheme === 'dark'}
2326
-
/>
2327
-
</View>
2328
-
))}
2329
-
2330
-
{/* Show current assistant message being accumulated */}
2331
-
{currentStream.assistantMessage && (
2332
-
<>
2333
-
<View style={{ flex: 1 }}>
2334
-
<MessageContent
2335
-
content={currentStream.assistantMessage}
2336
-
isUser={false}
2337
-
isDark={colorScheme === 'dark'}
2338
-
/>
2339
-
</View>
2340
-
<View style={styles.messageSeparator} />
2341
-
</>
2342
-
)}
2343
-
2344
-
{/* Show thinking indicator if nothing else to show */}
2345
-
{completedStreamBlocks.length === 0 && !currentStream.reasoning && !currentStream.assistantMessage && currentStream.toolCalls.length === 0 && (
2346
-
<View style={{ paddingVertical: 8 }} />
2347
-
)}
2348
-
</Animated.View>
2349
-
)}
2350
-
</>
2351
-
}
2352
-
ListEmptyComponent={
2353
-
isLoadingMessages ? (
2354
-
<View style={styles.emptyContainer}>
2355
-
<ActivityIndicator size="large" color={theme.colors.text.secondary} />
2356
-
</View>
2357
-
) : null
2358
-
}
2359
-
/>
2360
-
2361
-
{/* Scroll to newest message button */}
2362
-
{showScrollToBottom && (
2363
-
<TouchableOpacity onPress={scrollToBottom} style={styles.scrollToBottomButton}>
2364
-
<Ionicons
2365
-
name="arrow-down"
2366
-
size={24}
2367
-
color="#000"
2368
-
/>
2369
-
</TouchableOpacity>
2370
-
)}
2371
-
</View>
2372
-
2373
-
{/* Input */}
2374
-
<View
2375
-
style={[
2376
-
styles.inputContainer,
2377
-
{
2378
-
paddingBottom: Math.max(insets.bottom, 16),
2379
-
marginBottom: Platform.OS === 'android' && isKeyboardVisible ? 64 : 0
2380
-
},
2381
-
displayMessages.length === 0 && styles.inputContainerCentered
2382
-
]}
2383
-
onLayout={handleInputLayout}
2384
-
>
2385
-
<View style={styles.inputCentered}>
2386
-
{/* Empty state intro - shown above input when chat is empty */}
2387
-
{displayMessages.length === 0 && (
2388
-
<View style={styles.emptyStateIntro}>
2389
-
<Animated.Text
2390
-
style={{
2391
-
fontSize: 72,
2392
-
fontFamily: 'Lexend_700Bold',
2393
-
color: rainbowAnimValue.interpolate({
2394
-
inputRange: [0, 0.2, 0.4, 0.6, 0.8, 1],
2395
-
outputRange: ['#FF6B6B', '#FFD93D', '#6BCF7F', '#4D96FF', '#9D4EDD', '#FF6B6B']
2396
-
}),
2397
-
marginBottom: 16,
2398
-
textAlign: 'center',
2399
-
}}
2400
-
>
2401
-
co
2402
-
</Animated.Text>
2403
-
<Text style={[styles.emptyText, { fontSize: 18, lineHeight: 28, marginBottom: 32, color: theme.colors.text.primary }]}>
2404
-
I'm co, your thinking partner.
2405
-
</Text>
2406
-
</View>
2407
-
)}
2408
-
{/* Image preview section */}
2409
-
{selectedImages.length > 0 && (
2410
-
<View style={styles.imagePreviewContainer}>
2411
-
{selectedImages.map((img, index) => (
2412
-
<View key={index} style={styles.imagePreviewWrapper}>
2413
-
<Image source={{ uri: img.uri }} style={styles.imagePreview} />
2414
-
<TouchableOpacity
2415
-
onPress={() => removeImage(index)}
2416
-
style={styles.removeImageButton}
2417
-
>
2418
-
<Ionicons name="close-circle" size={24} color="#fff" />
2419
-
</TouchableOpacity>
2420
-
</View>
2421
-
))}
2422
-
</View>
2423
-
)}
2424
-
2425
-
<Animated.View style={[
2426
-
styles.inputWrapper,
2427
-
inputWrapperStyle,
2428
-
isInputFocused && {
2429
-
borderColor: rainbowAnimValue.interpolate({
2430
-
inputRange: [0, 0.2, 0.4, 0.6, 0.8, 1],
2431
-
outputRange: ['#FF6B6B', '#FFD93D', '#6BCF7F', '#4D96FF', '#9D4EDD', '#FF6B6B']
2432
-
}),
2433
-
shadowColor: rainbowAnimValue.interpolate({
2434
-
inputRange: [0, 0.2, 0.4, 0.6, 0.8, 1],
2435
-
outputRange: ['#FF6B6B', '#FFD93D', '#6BCF7F', '#4D96FF', '#9D4EDD', '#FF6B6B']
2436
-
}),
2437
-
shadowOpacity: 0.4,
2438
-
shadowRadius: 16,
2439
-
}
2440
-
]}>
2441
-
<TouchableOpacity
2442
-
onPress={pickAndUploadFile}
2443
-
style={styles.fileButton}
2444
-
disabled={isSendingMessage || isUploadingFile}
2445
-
>
2446
-
<Ionicons name="attach-outline" size={20} color="#666666" />
2447
-
</TouchableOpacity>
2448
-
<TouchableOpacity
2449
-
onPress={pickImage}
2450
-
style={styles.imageButton}
2451
-
disabled={isSendingMessage}
2452
-
>
2453
-
<Ionicons name="image-outline" size={20} color="#666666" />
2454
-
</TouchableOpacity>
2455
-
<MessageInput
2456
-
onTextChange={handleTextChange}
2457
-
onSendMessage={handleSendFromInput}
2458
-
isSendingMessage={isSendingMessage}
2459
-
colorScheme={colorScheme}
2460
-
onFocusChange={handleInputFocusChange}
2461
-
clearTrigger={clearInputTrigger}
2462
-
/>
2463
-
<TouchableOpacity
2464
-
onPress={handleSendFromInput}
2465
-
style={[styles.sendButton, sendButtonStyle]}
2466
-
disabled={(!hasInputText && selectedImages.length === 0) || isSendingMessage}
2467
-
>
2468
-
{isSendingMessage ? (
2469
-
<ActivityIndicator size="small" color={colorScheme === 'dark' ? '#fff' : '#000'} />
2470
-
) : (
2471
-
<Ionicons
2472
-
name="arrow-up"
2473
-
size={20}
2474
-
color={sendIconColor}
2475
-
/>
2476
-
)}
2477
-
</TouchableOpacity>
2478
-
</Animated.View>
2479
-
</View>
2480
-
</View>
2481
-
</View>
2482
-
2483
-
{/* Knowledge View */}
2484
-
<View style={[styles.memoryViewContainer, { display: currentView === 'knowledge' ? 'flex' : 'none', backgroundColor: theme.colors.background.primary }]}>
2485
-
{/* Knowledge Tabs */}
2486
-
<View style={[styles.knowledgeTabs, { backgroundColor: theme.colors.background.secondary, borderBottomColor: theme.colors.border.primary }]}>
2487
-
<TouchableOpacity
2488
-
style={[
2489
-
styles.knowledgeTab,
2490
-
knowledgeTab === 'core' && { borderBottomColor: theme.colors.text.primary, borderBottomWidth: 2 }
2491
-
]}
2492
-
onPress={() => setKnowledgeTab('core')}
2493
-
>
2494
-
<Text style={[
2495
-
styles.knowledgeTabText,
2496
-
{ color: knowledgeTab === 'core' ? theme.colors.text.primary : theme.colors.text.tertiary }
2497
-
]}>Core Memory</Text>
2498
-
</TouchableOpacity>
2499
-
<TouchableOpacity
2500
-
style={[
2501
-
styles.knowledgeTab,
2502
-
knowledgeTab === 'archival' && { borderBottomColor: theme.colors.text.primary, borderBottomWidth: 2 }
2503
-
]}
2504
-
onPress={() => setKnowledgeTab('archival')}
2505
-
>
2506
-
<Text style={[
2507
-
styles.knowledgeTabText,
2508
-
{ color: knowledgeTab === 'archival' ? theme.colors.text.primary : theme.colors.text.tertiary }
2509
-
]}>Archival Memory</Text>
2510
-
</TouchableOpacity>
2511
-
<TouchableOpacity
2512
-
style={[
2513
-
styles.knowledgeTab,
2514
-
knowledgeTab === 'files' && { borderBottomColor: theme.colors.text.primary, borderBottomWidth: 2 }
2515
-
]}
2516
-
onPress={() => setKnowledgeTab('files')}
2517
-
>
2518
-
<Text style={[
2519
-
styles.knowledgeTabText,
2520
-
{ color: knowledgeTab === 'files' ? theme.colors.text.primary : theme.colors.text.tertiary }
2521
-
]}>Files</Text>
2522
-
</TouchableOpacity>
2523
-
</View>
2524
-
2525
-
{/* Search bars */}
2526
-
{knowledgeTab === 'core' && (
2527
-
<View style={styles.memorySearchContainer}>
2528
-
<Ionicons name="search" size={20} color={theme.colors.text.tertiary} style={styles.memorySearchIcon} />
2529
-
<TextInput
2530
-
style={[styles.memorySearchInput, {
2531
-
color: theme.colors.text.primary,
2532
-
backgroundColor: theme.colors.background.tertiary,
2533
-
borderColor: theme.colors.border.primary,
2534
-
}]}
2535
-
placeholder="Search memory blocks..."
2536
-
placeholderTextColor={theme.colors.text.tertiary}
2537
-
value={memorySearchQuery}
2538
-
onChangeText={setMemorySearchQuery}
2539
-
/>
2540
-
</View>
2541
-
)}
2542
-
2543
-
{knowledgeTab === 'archival' && (
2544
-
<View style={styles.memorySearchContainer}>
2545
-
<Ionicons name="search" size={20} color={theme.colors.text.tertiary} style={styles.memorySearchIcon} />
2546
-
<TextInput
2547
-
style={[styles.memorySearchInput, {
2548
-
color: theme.colors.text.primary,
2549
-
backgroundColor: theme.colors.background.tertiary,
2550
-
borderColor: theme.colors.border.primary,
2551
-
paddingRight: passageSearchQuery ? 96 : 60,
2552
-
}]}
2553
-
placeholder="Search archival memory..."
2554
-
placeholderTextColor={theme.colors.text.tertiary}
2555
-
value={passageSearchQuery}
2556
-
onChangeText={setPassageSearchQuery}
2557
-
onSubmitEditing={() => loadPassages(true)}
2558
-
/>
2559
-
{passageSearchQuery && (
2560
-
<TouchableOpacity
2561
-
style={{ position: 'absolute', right: 64, padding: 8 }}
2562
-
onPress={() => {
2563
-
setPassageSearchQuery('');
2564
-
loadPassages(true);
2565
-
}}
2566
-
>
2567
-
<Ionicons name="close-circle" size={20} color={theme.colors.text.tertiary} />
2568
-
</TouchableOpacity>
2569
-
)}
2570
-
<TouchableOpacity
2571
-
style={{ position: 'absolute', right: 28, padding: 8 }}
2572
-
onPress={() => setIsCreatingPassage(true)}
2573
-
>
2574
-
<Ionicons name="add-circle-outline" size={24} color={theme.colors.text.primary} />
2575
-
</TouchableOpacity>
2576
-
</View>
2577
-
)}
2578
-
2579
-
{/* Knowledge blocks grid */}
2580
-
<View style={styles.memoryBlocksGrid}>
2581
-
{knowledgeTab === 'files' ? (
2582
-
/* Files view */
2583
-
<>
2584
-
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingHorizontal: 8, paddingVertical: 12 }}>
2585
-
<Text style={[styles.memorySectionTitle, { color: theme.colors.text.secondary, marginBottom: 0 }]}>Uploaded Files</Text>
2586
-
<TouchableOpacity
2587
-
onPress={pickAndUploadFile}
2588
-
disabled={isUploadingFile}
2589
-
style={{ padding: 4 }}
2590
-
>
2591
-
{isUploadingFile ? (
2592
-
<ActivityIndicator size="small" color={theme.colors.text.secondary} />
2593
-
) : (
2594
-
<Ionicons name="add-circle-outline" size={24} color={theme.colors.text.primary} />
2595
-
)}
2596
-
</TouchableOpacity>
2597
-
</View>
2598
-
{uploadProgress && (
2599
-
<View style={{ marginHorizontal: 8, marginBottom: 12, paddingVertical: 8, paddingHorizontal: 12, backgroundColor: theme.colors.background.tertiary, borderRadius: 8 }}>
2600
-
<Text style={{ color: theme.colors.text.secondary, fontSize: 14 }}>{uploadProgress}</Text>
2601
-
</View>
2602
-
)}
2603
-
{isLoadingFiles ? (
2604
-
<View style={styles.memoryLoadingContainer}>
2605
-
<ActivityIndicator size="large" color={theme.colors.text.secondary} />
2606
-
</View>
2607
-
) : filesError ? (
2608
-
<Text style={[styles.errorText, { textAlign: 'center', marginTop: 40 }]}>{filesError}</Text>
2609
-
) : folderFiles.length === 0 ? (
2610
-
<View style={styles.memoryEmptyState}>
2611
-
<Ionicons name="folder-outline" size={64} color={theme.colors.text.tertiary} style={{ opacity: 0.3 }} />
2612
-
<Text style={[styles.memoryEmptyText, { color: theme.colors.text.tertiary }]}>No files uploaded yet</Text>
2613
-
</View>
2614
-
) : (
2615
-
<FlatList
2616
-
data={folderFiles}
2617
-
keyExtractor={(item) => item.id}
2618
-
contentContainerStyle={styles.memoryBlocksContent}
2619
-
renderItem={({ item }) => (
2620
-
<View
2621
-
style={[styles.memoryBlockCard, {
2622
-
backgroundColor: theme.colors.background.secondary,
2623
-
borderColor: theme.colors.border.primary,
2624
-
flexDirection: 'row',
2625
-
justifyContent: 'space-between',
2626
-
alignItems: 'center',
2627
-
minHeight: 'auto'
2628
-
}]}
2629
-
>
2630
-
<View style={{ flex: 1 }}>
2631
-
<Text style={[styles.memoryBlockCardLabel, { color: theme.colors.text.primary }]} numberOfLines={1}>
2632
-
{item.fileName || item.name || 'Untitled'}
2633
-
</Text>
2634
-
<Text style={[styles.memoryBlockCardPreview, { color: theme.colors.text.secondary, fontSize: 12 }]}>
2635
-
{new Date(item.createdAt || item.created_at).toLocaleDateString()}
2636
-
</Text>
2637
-
</View>
2638
-
<TouchableOpacity
2639
-
onPress={() => deleteFile(item.id, item.fileName || item.name)}
2640
-
style={{ padding: 8 }}
2641
-
>
2642
-
<Ionicons name="trash-outline" size={20} color={theme.colors.status.error} />
2643
-
</TouchableOpacity>
2644
-
</View>
2645
-
)}
2646
-
/>
2647
-
)}
2648
-
</>
2649
-
) : knowledgeTab === 'archival' ? (
2650
-
/* Archival Memory view */
2651
-
isLoadingPassages ? (
2652
-
<View style={styles.memoryLoadingContainer}>
2653
-
<ActivityIndicator size="large" color={theme.colors.text.secondary} />
2654
-
</View>
2655
-
) : passagesError ? (
2656
-
<Text style={[styles.errorText, { textAlign: 'center', marginTop: 40 }]}>{passagesError}</Text>
2657
-
) : passages.length === 0 ? (
2658
-
<View style={styles.memoryEmptyState}>
2659
-
<Ionicons name="archive-outline" size={64} color={theme.colors.text.tertiary} style={{ opacity: 0.3 }} />
2660
-
<Text style={[styles.memoryEmptyText, { color: theme.colors.text.tertiary }]}>No archival memories yet</Text>
2661
-
</View>
2662
-
) : (
2663
-
<FlatList
2664
-
data={passages}
2665
-
keyExtractor={(item) => item.id}
2666
-
contentContainerStyle={styles.memoryBlocksContent}
2667
-
renderItem={({ item }) => (
2668
-
<View
2669
-
style={[styles.memoryBlockCard, {
2670
-
backgroundColor: theme.colors.background.secondary,
2671
-
borderColor: theme.colors.border.primary,
2672
-
}]}
2673
-
>
2674
-
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 8 }}>
2675
-
<Text style={[styles.memoryBlockCardPreview, { color: theme.colors.text.tertiary, fontSize: 11, flex: 1 }]}>
2676
-
{new Date(item.created_at).toLocaleString()}
2677
-
</Text>
2678
-
<View style={{ flexDirection: 'row', gap: 8 }}>
2679
-
<TouchableOpacity
2680
-
onPress={() => {
2681
-
setSelectedPassage(item);
2682
-
setIsEditingPassage(true);
2683
-
}}
2684
-
style={{ padding: 4 }}
2685
-
>
2686
-
<Ionicons name="create-outline" size={18} color={theme.colors.text.secondary} />
2687
-
</TouchableOpacity>
2688
-
<TouchableOpacity
2689
-
onPress={() => deletePassage(item.id)}
2690
-
style={{ padding: 4 }}
2691
-
>
2692
-
<Ionicons name="trash-outline" size={18} color={theme.colors.status.error} />
2693
-
</TouchableOpacity>
2694
-
</View>
2695
-
</View>
2696
-
<Text
2697
-
style={[styles.memoryBlockCardPreview, { color: theme.colors.text.primary }]}
2698
-
numberOfLines={6}
2699
-
>
2700
-
{item.text}
2701
-
</Text>
2702
-
{item.tags && item.tags.length > 0 && (
2703
-
<View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 6, marginTop: 8 }}>
2704
-
{item.tags.map((tag, idx) => (
2705
-
<View key={idx} style={{ backgroundColor: theme.colors.background.tertiary, paddingHorizontal: 8, paddingVertical: 4, borderRadius: 4 }}>
2706
-
<Text style={{ color: theme.colors.text.secondary, fontSize: 11 }}>{tag}</Text>
2707
-
</View>
2708
-
))}
2709
-
</View>
2710
-
)}
2711
-
</View>
2712
-
)}
2713
-
ListFooterComponent={
2714
-
hasMorePassages ? (
2715
-
<TouchableOpacity
2716
-
onPress={() => loadPassages(false)}
2717
-
style={{ padding: 16, alignItems: 'center' }}
2718
-
>
2719
-
<Text style={{ color: theme.colors.text.secondary }}>Load more...</Text>
2720
-
</TouchableOpacity>
2721
-
) : null
2722
-
}
2723
-
/>
2724
-
)
2725
-
) : isLoadingBlocks ? (
2726
-
<View style={styles.memoryLoadingContainer}>
2727
-
<ActivityIndicator size="large" color={theme.colors.text.secondary} />
2728
-
</View>
2729
-
) : blocksError ? (
2730
-
<Text style={[styles.errorText, { textAlign: 'center', marginTop: 40 }]}>{blocksError}</Text>
2731
-
) : (
2732
-
<FlatList
2733
-
data={memoryBlocks.filter(block => {
2734
-
// Core memory: show all blocks
2735
-
// Filter by search query
2736
-
if (memorySearchQuery) {
2737
-
return block.label.toLowerCase().includes(memorySearchQuery.toLowerCase()) ||
2738
-
block.value.toLowerCase().includes(memorySearchQuery.toLowerCase());
2739
-
}
2740
-
2741
-
return true;
2742
-
})}
2743
-
numColumns={isDesktop ? 2 : 1}
2744
-
key={isDesktop ? 'desktop' : 'mobile'}
2745
-
keyExtractor={(item) => item.id || item.label}
2746
-
contentContainerStyle={styles.memoryBlocksContent}
2747
-
renderItem={({ item }) => (
2748
-
<TouchableOpacity
2749
-
style={[styles.memoryBlockCard, {
2750
-
backgroundColor: theme.colors.background.secondary,
2751
-
borderColor: theme.colors.border.primary
2752
-
}]}
2753
-
onPress={() => setSelectedBlock(item)}
2754
-
>
2755
-
<View style={styles.memoryBlockCardHeader}>
2756
-
<Text style={[styles.memoryBlockCardLabel, { color: theme.colors.text.primary }]}>
2757
-
{item.label}
2758
-
</Text>
2759
-
<Text style={[styles.memoryBlockCardCount, { color: theme.colors.text.tertiary }]}>
2760
-
{item.value.length} chars
2761
-
</Text>
2762
-
</View>
2763
-
<Text
2764
-
style={[styles.memoryBlockCardPreview, { color: theme.colors.text.secondary }]}
2765
-
numberOfLines={4}
2766
-
>
2767
-
{item.value || 'Empty'}
2768
-
</Text>
2769
-
</TouchableOpacity>
2770
-
)}
2771
-
ListEmptyComponent={
2772
-
<View style={styles.memoryEmptyState}>
2773
-
<Ionicons name="library-outline" size={64} color={theme.colors.text.tertiary} style={{ opacity: 0.3 }} />
2774
-
<Text style={[styles.memoryEmptyText, { color: theme.colors.text.secondary }]}>
2775
-
{memorySearchQuery ? 'No memory blocks found' : 'No memory blocks yet'}
2776
-
</Text>
2777
-
</View>
2778
-
}
2779
-
/>
2780
-
)}
2781
-
</View>
2782
-
</View>
2783
-
2784
-
{/* Settings View */}
2785
-
<View style={[styles.memoryViewContainer, { display: currentView === 'settings' ? 'flex' : 'none', backgroundColor: theme.colors.background.primary }]}>
2786
-
<View style={[styles.settingsHeader, { backgroundColor: theme.colors.background.secondary, borderBottomColor: theme.colors.border.primary }]}>
2787
-
<Text style={[styles.settingsTitle, { color: theme.colors.text.primary }]}>Settings</Text>
2788
-
</View>
2789
-
2790
-
<View style={styles.settingsContent}>
2791
-
{/* Show Compaction Setting */}
2792
-
<View style={[styles.settingItem, { borderBottomColor: theme.colors.border.primary }]}>
2793
-
<View style={styles.settingInfo}>
2794
-
<Text style={[styles.settingLabel, { color: theme.colors.text.primary }]}>Show Compaction</Text>
2795
-
<Text style={[styles.settingDescription, { color: theme.colors.text.secondary }]}>
2796
-
Display compaction bars when conversation history is summarized
2797
-
</Text>
2798
-
</View>
2799
-
<TouchableOpacity
2800
-
style={[styles.toggle, showCompaction && styles.toggleActive]}
2801
-
onPress={() => setShowCompaction(!showCompaction)}
2802
-
>
2803
-
<View style={[styles.toggleThumb, showCompaction && styles.toggleThumbActive]} />
2804
-
</TouchableOpacity>
2805
-
</View>
2806
-
</View>
2807
-
</View>
2808
-
2809
-
{/* Knowledge block viewer - right pane on desktop */}
2810
-
{isDesktop && selectedBlock && (
2811
-
<MemoryBlockViewer
2812
-
block={selectedBlock}
2813
-
onClose={() => setSelectedBlock(null)}
2814
-
isDark={colorScheme === 'dark'}
2815
-
isDesktop={isDesktop}
2816
-
/>
2817
-
)}
2818
-
</KeyboardAvoidingView>
2819
-
</View>
2820
-
2821
-
{/* Knowledge block viewer - overlay on mobile */}
2822
-
{!isDesktop && selectedBlock && (
2823
-
<MemoryBlockViewer
2824
-
block={selectedBlock}
2825
-
onClose={() => setSelectedBlock(null)}
2826
-
isDark={colorScheme === 'dark'}
2827
-
isDesktop={isDesktop}
2828
-
/>
2829
-
)}
2830
-
2831
-
{/* Create/Edit Passage Modal */}
2832
-
{(isCreatingPassage || isEditingPassage) && (
2833
-
<View style={styles.modalOverlay}>
2834
-
<View style={[styles.modalContent, { backgroundColor: theme.colors.background.primary, borderColor: theme.colors.border.primary }]}>
2835
-
<View style={styles.modalHeader}>
2836
-
<Text style={[styles.modalTitle, { color: theme.colors.text.primary }]}>
2837
-
{isCreatingPassage ? 'Create Passage' : 'Edit Passage'}
2838
-
</Text>
2839
-
<TouchableOpacity
2840
-
onPress={() => {
2841
-
if (isSavingPassage) return;
2842
-
setIsCreatingPassage(false);
2843
-
setIsEditingPassage(false);
2844
-
setSelectedPassage(null);
2845
-
}}
2846
-
disabled={isSavingPassage}
2847
-
>
2848
-
<Ionicons name="close" size={24} color={theme.colors.text.primary} style={{ opacity: isSavingPassage ? 0.5 : 1 }} />
2849
-
</TouchableOpacity>
2850
-
</View>
2851
-
<View style={styles.modalBody}>
2852
-
<Text style={[styles.inputLabel, { color: theme.colors.text.secondary }]}>Text</Text>
2853
-
<TextInput
2854
-
style={[styles.textArea, { color: theme.colors.text.primary, backgroundColor: theme.colors.background.secondary, borderColor: theme.colors.border.primary }]}
2855
-
multiline
2856
-
numberOfLines={6}
2857
-
defaultValue={selectedPassage?.text || ''}
2858
-
placeholder="Enter passage text..."
2859
-
placeholderTextColor={theme.colors.text.tertiary}
2860
-
onChangeText={(text) => {
2861
-
if (selectedPassage) {
2862
-
setSelectedPassage({ ...selectedPassage, text });
2863
-
} else {
2864
-
setSelectedPassage({ text, id: '', created_at: new Date().toISOString() } as Passage);
2865
-
}
2866
-
}}
2867
-
/>
2868
-
<Text style={[styles.inputLabel, { color: theme.colors.text.secondary, marginTop: 16 }]}>Tags (comma-separated)</Text>
2869
-
<TextInput
2870
-
style={[styles.textInput, { color: theme.colors.text.primary, backgroundColor: theme.colors.background.secondary, borderColor: theme.colors.border.primary }]}
2871
-
defaultValue={selectedPassage?.tags?.join(', ') || ''}
2872
-
placeholder="tag1, tag2, tag3"
2873
-
placeholderTextColor={theme.colors.text.tertiary}
2874
-
onChangeText={(text) => {
2875
-
const tags = text.split(',').map(t => t.trim()).filter(t => t);
2876
-
if (selectedPassage) {
2877
-
setSelectedPassage({ ...selectedPassage, tags });
2878
-
} else {
2879
-
setSelectedPassage({ text: '', tags, id: '', created_at: new Date().toISOString() } as Passage);
2880
-
}
2881
-
}}
2882
-
/>
2883
-
</View>
2884
-
<View style={styles.modalFooter}>
2885
-
<TouchableOpacity
2886
-
style={[styles.modalButton, styles.modalButtonSecondary, { borderColor: theme.colors.border.primary, opacity: isSavingPassage ? 0.5 : 1 }]}
2887
-
onPress={() => {
2888
-
if (isSavingPassage) return;
2889
-
setIsCreatingPassage(false);
2890
-
setIsEditingPassage(false);
2891
-
setSelectedPassage(null);
2892
-
}}
2893
-
disabled={isSavingPassage}
2894
-
>
2895
-
<Text style={[styles.modalButtonText, { color: theme.colors.text.primary }]}>Cancel</Text>
2896
-
</TouchableOpacity>
2897
-
<TouchableOpacity
2898
-
style={[styles.modalButton, styles.modalButtonPrimary, { backgroundColor: theme.colors.text.primary, opacity: isSavingPassage ? 0.7 : 1 }]}
2899
-
onPress={async () => {
2900
-
if (isSavingPassage) return;
2901
-
if (!selectedPassage?.text) {
2902
-
Alert.alert('Error', 'Please enter passage text');
2903
-
return;
2904
-
}
2905
-
setIsSavingPassage(true);
2906
-
try {
2907
-
if (isEditingPassage && selectedPassage.id) {
2908
-
await modifyPassage(selectedPassage.id, selectedPassage.text, selectedPassage.tags);
2909
-
} else {
2910
-
await createPassage(selectedPassage.text, selectedPassage.tags);
2911
-
}
2912
-
setIsCreatingPassage(false);
2913
-
setIsEditingPassage(false);
2914
-
setSelectedPassage(null);
2915
-
} catch (error) {
2916
-
console.error('Error saving passage:', error);
2917
-
} finally {
2918
-
setIsSavingPassage(false);
2919
-
}
2920
-
}}
2921
-
disabled={isSavingPassage}
2922
-
>
2923
-
{isSavingPassage ? (
2924
-
<ActivityIndicator size="small" color={theme.colors.background.primary} />
2925
-
) : (
2926
-
<Text style={[styles.modalButtonText, { color: theme.colors.background.primary }]}>
2927
-
{isCreatingPassage ? 'Create' : 'Save'}
2928
-
</Text>
2929
-
)}
2930
-
</TouchableOpacity>
2931
-
</View>
2932
-
</View>
2933
-
</View>
2934
-
)}
2935
-
2936
-
{/* Approval modal */}
2937
-
<Modal
2938
-
visible={approvalVisible}
2939
-
animationType="fade"
2940
-
transparent={true}
2941
-
onRequestClose={() => setApprovalVisible(false)}
2942
-
>
2943
-
<View style={styles.approvalOverlay}>
2944
-
<View style={styles.approvalContainer}>
2945
-
<Text style={styles.approvalTitle}>Tool Approval Required</Text>
2946
-
2947
-
{approvalData?.toolName && (
2948
-
<Text style={styles.approvalTool}>Tool: {approvalData.toolName}</Text>
2949
-
)}
2950
-
2951
-
{approvalData?.reasoning && (
2952
-
<View style={styles.approvalReasoning}>
2953
-
<Text style={styles.approvalReasoningLabel}>Reasoning:</Text>
2954
-
<Text style={styles.approvalReasoningText}>{approvalData.reasoning}</Text>
2955
-
</View>
2956
-
)}
2957
-
2958
-
<TextInput
2959
-
style={styles.approvalInput}
2960
-
placeholder="Optional reason..."
2961
-
placeholderTextColor={theme.colors.text.tertiary}
2962
-
value={approvalReason}
2963
-
onChangeText={setApprovalReason}
2964
-
multiline
2965
-
/>
2966
-
2967
-
<View style={styles.approvalButtons}>
2968
-
<TouchableOpacity
2969
-
style={[styles.approvalButton, styles.denyButton]}
2970
-
onPress={() => handleApproval(false)}
2971
-
disabled={isApproving}
2972
-
>
2973
-
<Text style={styles.approvalButtonText}>Deny</Text>
2974
-
</TouchableOpacity>
2975
-
2976
-
<TouchableOpacity
2977
-
style={[styles.approvalButton, styles.approveButton]}
2978
-
onPress={() => handleApproval(true)}
2979
-
disabled={isApproving}
2980
-
>
2981
-
{isApproving ? (
2982
-
<ActivityIndicator size="small" color="#fff" />
2983
-
) : (
2984
-
<Text style={styles.approvalButtonText}>Approve</Text>
2985
-
)}
2986
-
</TouchableOpacity>
2987
-
</View>
2988
-
</View>
2989
-
</View>
2990
-
</Modal>
2991
-
2992
-
<StatusBar style={colorScheme === 'dark' ? 'light' : 'dark'} />
2993
-
</View>
2994
-
);
2995
-
}
2996
-
2997
-
export default function App() {
2998
-
return (
2999
-
<View style={{ flex: 1, backgroundColor: darkTheme.colors.background.primary }}>
3000
-
<SafeAreaProvider style={{ flex: 1, backgroundColor: darkTheme.colors.background.primary }}>
3001
-
<CoApp />
3002
-
</SafeAreaProvider>
3003
-
</View>
3004
-
);
3005
-
}
3006
-
3007
-
const styles = StyleSheet.create({
3008
-
container: {
3009
-
flex: 1,
3010
-
flexDirection: 'row',
3011
-
backgroundColor: darkTheme.colors.background.primary,
3012
-
},
3013
-
mainContent: {
3014
-
flex: 1,
3015
-
flexDirection: 'column',
3016
-
},
3017
-
chatRow: {
3018
-
flex: 1,
3019
-
flexDirection: 'row',
3020
-
},
3021
-
loadingContainer: {
3022
-
flex: 1,
3023
-
justifyContent: 'center',
3024
-
alignItems: 'center',
3025
-
backgroundColor: darkTheme.colors.background.primary,
3026
-
},
3027
-
loadingText: {
3028
-
marginTop: 16,
3029
-
fontSize: 16,
3030
-
fontFamily: 'Lexend_400Regular',
3031
-
color: darkTheme.colors.text.secondary,
3032
-
},
3033
-
header: {
3034
-
flexDirection: 'row',
3035
-
alignItems: 'center',
3036
-
paddingHorizontal: 16,
3037
-
paddingTop: 12,
3038
-
paddingBottom: 12,
3039
-
backgroundColor: darkTheme.colors.background.secondary,
3040
-
borderBottomWidth: 1,
3041
-
borderBottomColor: darkTheme.colors.border.primary,
3042
-
},
3043
-
viewSwitcher: {
3044
-
flexDirection: 'row',
3045
-
justifyContent: 'space-between',
3046
-
alignItems: 'center',
3047
-
paddingHorizontal: 16,
3048
-
paddingVertical: 4,
3049
-
maxWidth: 400,
3050
-
alignSelf: 'center',
3051
-
width: '100%',
3052
-
},
3053
-
viewSwitcherButton: {
3054
-
paddingVertical: 6,
3055
-
paddingHorizontal: 16,
3056
-
borderRadius: 8,
3057
-
flex: 1,
3058
-
alignItems: 'center',
3059
-
justifyContent: 'center',
3060
-
},
3061
-
viewSwitcherText: {
3062
-
fontSize: 14,
3063
-
fontFamily: 'Lexend_500Medium',
3064
-
},
3065
-
menuButton: {
3066
-
padding: 8,
3067
-
},
3068
-
headerCenter: {
3069
-
flex: 1,
3070
-
alignItems: 'center',
3071
-
},
3072
-
headerTitle: {
3073
-
fontSize: 36,
3074
-
fontFamily: 'Lexend_700Bold',
3075
-
color: darkTheme.colors.text.primary,
3076
-
},
3077
-
headerButton: {
3078
-
padding: 8,
3079
-
},
3080
-
headerButtonDisabled: {
3081
-
opacity: 0.3,
3082
-
},
3083
-
headerSpacer: {
3084
-
width: 40,
3085
-
},
3086
-
logoutButton: {
3087
-
padding: 8,
3088
-
},
3089
-
messagesContainer: {
3090
-
flex: 1,
3091
-
},
3092
-
messagesList: {
3093
-
maxWidth: 700,
3094
-
width: '100%',
3095
-
alignSelf: 'center',
3096
-
paddingBottom: 180, // Space for input at bottom (accounts for expanded input height)
3097
-
},
3098
-
messageContainer: {
3099
-
paddingHorizontal: 18,
3100
-
paddingVertical: 12,
3101
-
},
3102
-
userMessageContainer: {
3103
-
alignItems: 'flex-end',
3104
-
},
3105
-
assistantMessageContainer: {
3106
-
alignItems: 'flex-start',
3107
-
},
3108
-
assistantFullWidthContainer: {
3109
-
paddingHorizontal: 18,
3110
-
paddingVertical: 16,
3111
-
width: '100%',
3112
-
},
3113
-
assistantLabel: {
3114
-
fontSize: 16,
3115
-
fontFamily: 'Lexend_500Medium',
3116
-
marginBottom: 8,
3117
-
},
3118
-
messageBubble: {
3119
-
maxWidth: 600,
3120
-
padding: 12,
3121
-
borderRadius: 24,
3122
-
},
3123
-
userBubble: {
3124
-
// Background color set dynamically per theme in render
3125
-
},
3126
-
messageImagesContainer: {
3127
-
flexDirection: 'row',
3128
-
flexWrap: 'wrap',
3129
-
gap: 8,
3130
-
marginBottom: 8,
3131
-
},
3132
-
messageImage: {
3133
-
width: 200,
3134
-
height: 200,
3135
-
borderRadius: 12,
3136
-
resizeMode: 'cover',
3137
-
},
3138
-
assistantBubble: {
3139
-
backgroundColor: darkTheme.colors.background.secondary,
3140
-
borderWidth: 1,
3141
-
borderColor: darkTheme.colors.border.primary,
3142
-
},
3143
-
userMessageText: {
3144
-
color: darkTheme.colors.background.primary,
3145
-
fontSize: 18,
3146
-
fontFamily: 'Lexend_400Regular',
3147
-
lineHeight: 26,
3148
-
},
3149
-
assistantMessageText: {
3150
-
color: darkTheme.colors.text.primary,
3151
-
fontSize: 18,
3152
-
fontFamily: 'Lexend_400Regular',
3153
-
lineHeight: 26,
3154
-
},
3155
-
reasoningContainer: {
3156
-
marginTop: 8,
3157
-
paddingTop: 8,
3158
-
borderTopWidth: 1,
3159
-
borderTopColor: darkTheme.colors.border.primary,
3160
-
},
3161
-
reasoningLabel: {
3162
-
fontSize: 12,
3163
-
fontFamily: 'Lexend_600SemiBold',
3164
-
color: darkTheme.colors.text.secondary,
3165
-
marginBottom: 4,
3166
-
},
3167
-
reasoningText: {
3168
-
fontSize: 12,
3169
-
fontFamily: 'Lexend_400Regular',
3170
-
color: darkTheme.colors.text.tertiary,
3171
-
fontStyle: 'italic',
3172
-
},
3173
-
loadMoreButton: {
3174
-
padding: 16,
3175
-
alignItems: 'center',
3176
-
},
3177
-
loadMoreText: {
3178
-
color: darkTheme.colors.text.secondary,
3179
-
fontSize: 14,
3180
-
fontFamily: 'Lexend_400Regular',
3181
-
},
3182
-
messageSeparator: {
3183
-
height: 16,
3184
-
},
3185
-
emptyContainer: {
3186
-
flex: 1,
3187
-
justifyContent: 'center',
3188
-
alignItems: 'center',
3189
-
paddingHorizontal: 60,
3190
-
paddingVertical: 80,
3191
-
},
3192
-
emptyText: {
3193
-
fontSize: 24,
3194
-
fontFamily: 'Lexend_400Regular',
3195
-
color: darkTheme.colors.text.primary,
3196
-
textAlign: 'center',
3197
-
lineHeight: 36,
3198
-
},
3199
-
scrollToBottomButton: {
3200
-
position: 'absolute',
3201
-
bottom: 16,
3202
-
right: 16,
3203
-
width: 48,
3204
-
height: 48,
3205
-
borderRadius: 24,
3206
-
backgroundColor: darkTheme.colors.interactive.primary,
3207
-
justifyContent: 'center',
3208
-
alignItems: 'center',
3209
-
shadowColor: '#000',
3210
-
shadowOffset: { width: 0, height: 2 },
3211
-
shadowOpacity: 0.25,
3212
-
shadowRadius: 3.84,
3213
-
elevation: 5,
3214
-
},
3215
-
inputContainer: {
3216
-
position: 'absolute',
3217
-
bottom: 0,
3218
-
left: 0,
3219
-
right: 0,
3220
-
paddingTop: 16,
3221
-
paddingBottom: 24,
3222
-
paddingHorizontal: 16,
3223
-
alignItems: 'center',
3224
-
},
3225
-
inputContainerCentered: {
3226
-
top: 0,
3227
-
bottom: 'auto',
3228
-
justifyContent: 'center',
3229
-
height: '100%',
3230
-
},
3231
-
emptyStateIntro: {
3232
-
alignItems: 'center',
3233
-
marginBottom: 0,
3234
-
},
3235
-
inputCentered: {
3236
-
position: 'relative',
3237
-
maxWidth: 700,
3238
-
width: '100%',
3239
-
},
3240
-
inputWrapper: {
3241
-
position: 'relative',
3242
-
flexDirection: 'row',
3243
-
alignItems: 'flex-end',
3244
-
},
3245
-
fileButton: {
3246
-
position: 'absolute',
3247
-
right: 88,
3248
-
bottom: 8,
3249
-
width: 32,
3250
-
height: 32,
3251
-
borderRadius: 16,
3252
-
justifyContent: 'center',
3253
-
alignItems: 'center',
3254
-
zIndex: 1,
3255
-
},
3256
-
imageButton: {
3257
-
position: 'absolute',
3258
-
right: 52,
3259
-
bottom: 8,
3260
-
width: 32,
3261
-
height: 32,
3262
-
borderRadius: 16,
3263
-
justifyContent: 'center',
3264
-
alignItems: 'center',
3265
-
zIndex: 1,
3266
-
},
3267
-
imagePreviewContainer: {
3268
-
flexDirection: 'row',
3269
-
flexWrap: 'wrap',
3270
-
padding: 12,
3271
-
gap: 8,
3272
-
},
3273
-
imagePreviewWrapper: {
3274
-
position: 'relative',
3275
-
width: 80,
3276
-
height: 80,
3277
-
},
3278
-
imagePreview: {
3279
-
width: 80,
3280
-
height: 80,
3281
-
borderRadius: 8,
3282
-
resizeMode: 'cover',
3283
-
},
3284
-
removeImageButton: {
3285
-
position: 'absolute',
3286
-
top: -8,
3287
-
right: -8,
3288
-
backgroundColor: 'rgba(0, 0, 0, 0.6)',
3289
-
borderRadius: 12,
3290
-
},
3291
-
sendButton: {
3292
-
position: 'absolute',
3293
-
right: 10,
3294
-
bottom: 8,
3295
-
width: 32,
3296
-
height: 32,
3297
-
borderRadius: 16,
3298
-
justifyContent: 'center',
3299
-
alignItems: 'center',
3300
-
transition: 'all 0.2s ease',
3301
-
},
3302
-
sidebarContainer: {
3303
-
height: '100%',
3304
-
backgroundColor: darkTheme.colors.background.secondary,
3305
-
borderRightWidth: 1,
3306
-
borderRightColor: darkTheme.colors.border.primary,
3307
-
overflow: 'hidden',
3308
-
},
3309
-
sidebarHeader: {
3310
-
flexDirection: 'row',
3311
-
justifyContent: 'space-between',
3312
-
alignItems: 'center',
3313
-
paddingHorizontal: 16,
3314
-
paddingVertical: 16,
3315
-
borderBottomWidth: 1,
3316
-
borderBottomColor: darkTheme.colors.border.primary,
3317
-
},
3318
-
closeSidebar: {
3319
-
padding: 8,
3320
-
},
3321
-
sidebarTitle: {
3322
-
fontSize: 24,
3323
-
fontFamily: 'Lexend_700Bold',
3324
-
color: darkTheme.colors.text.primary,
3325
-
},
3326
-
menuItems: {
3327
-
paddingTop: 8,
3328
-
},
3329
-
menuItem: {
3330
-
flexDirection: 'row',
3331
-
alignItems: 'center',
3332
-
paddingHorizontal: 20,
3333
-
paddingVertical: 16,
3334
-
borderBottomWidth: StyleSheet.hairlineWidth,
3335
-
borderBottomColor: darkTheme.colors.border.primary,
3336
-
},
3337
-
menuItemText: {
3338
-
fontSize: 16,
3339
-
fontFamily: 'Lexend_400Regular',
3340
-
color: darkTheme.colors.text.primary,
3341
-
marginLeft: 16,
3342
-
},
3343
-
memorySection: {
3344
-
flex: 1,
3345
-
paddingHorizontal: 16,
3346
-
paddingTop: 16,
3347
-
},
3348
-
memorySectionTitle: {
3349
-
fontSize: 14,
3350
-
fontFamily: 'Lexend_600SemiBold',
3351
-
color: darkTheme.colors.text.secondary,
3352
-
marginBottom: 12,
3353
-
textTransform: 'uppercase',
3354
-
letterSpacing: 0.5,
3355
-
},
3356
-
memoryBlockItem: {
3357
-
padding: 16,
3358
-
backgroundColor: darkTheme.colors.background.secondary,
3359
-
borderRadius: 8,
3360
-
marginBottom: 12,
3361
-
borderWidth: 1,
3362
-
borderColor: darkTheme.colors.border.primary,
3363
-
},
3364
-
memoryBlockLabel: {
3365
-
fontSize: 16,
3366
-
fontFamily: 'Lexend_600SemiBold',
3367
-
color: darkTheme.colors.text.primary,
3368
-
marginBottom: 4,
3369
-
},
3370
-
memoryBlockPreview: {
3371
-
fontSize: 14,
3372
-
fontFamily: 'Lexend_400Regular',
3373
-
color: darkTheme.colors.text.secondary,
3374
-
},
3375
-
errorText: {
3376
-
color: darkTheme.colors.status.error,
3377
-
fontSize: 14,
3378
-
fontFamily: 'Lexend_400Regular',
3379
-
textAlign: 'center',
3380
-
},
3381
-
approvalOverlay: {
3382
-
flex: 1,
3383
-
backgroundColor: 'rgba(0, 0, 0, 0.7)',
3384
-
justifyContent: 'center',
3385
-
alignItems: 'center',
3386
-
padding: 20,
3387
-
},
3388
-
approvalContainer: {
3389
-
width: '100%',
3390
-
maxWidth: 400,
3391
-
backgroundColor: darkTheme.colors.background.primary,
3392
-
borderRadius: 16,
3393
-
padding: 20,
3394
-
},
3395
-
approvalTitle: {
3396
-
fontSize: 20,
3397
-
fontFamily: 'Lexend_700Bold',
3398
-
color: darkTheme.colors.text.primary,
3399
-
marginBottom: 16,
3400
-
},
3401
-
approvalTool: {
3402
-
fontSize: 16,
3403
-
fontFamily: 'Lexend_400Regular',
3404
-
color: darkTheme.colors.text.primary,
3405
-
marginBottom: 12,
3406
-
},
3407
-
approvalReasoning: {
3408
-
backgroundColor: darkTheme.colors.background.secondary,
3409
-
padding: 12,
3410
-
borderRadius: 8,
3411
-
marginBottom: 16,
3412
-
},
3413
-
approvalReasoningLabel: {
3414
-
fontSize: 12,
3415
-
fontFamily: 'Lexend_600SemiBold',
3416
-
color: darkTheme.colors.text.secondary,
3417
-
marginBottom: 4,
3418
-
},
3419
-
approvalReasoningText: {
3420
-
fontSize: 14,
3421
-
fontFamily: 'Lexend_400Regular',
3422
-
color: darkTheme.colors.text.primary,
3423
-
},
3424
-
approvalInput: {
3425
-
height: 80,
3426
-
borderWidth: 1,
3427
-
borderColor: darkTheme.colors.border.primary,
3428
-
borderRadius: 8,
3429
-
padding: 12,
3430
-
fontFamily: 'Lexend_400Regular',
3431
-
color: darkTheme.colors.text.primary,
3432
-
backgroundColor: darkTheme.colors.background.secondary,
3433
-
marginBottom: 16,
3434
-
textAlignVertical: 'top',
3435
-
},
3436
-
approvalButtons: {
3437
-
flexDirection: 'row',
3438
-
justifyContent: 'space-between',
3439
-
},
3440
-
approvalButton: {
3441
-
flex: 1,
3442
-
height: 48,
3443
-
borderRadius: 8,
3444
-
justifyContent: 'center',
3445
-
alignItems: 'center',
3446
-
marginHorizontal: 4,
3447
-
},
3448
-
denyButton: {
3449
-
backgroundColor: darkTheme.colors.status.error,
3450
-
},
3451
-
approveButton: {
3452
-
backgroundColor: darkTheme.colors.status.success,
3453
-
},
3454
-
approvalButtonText: {
3455
-
color: darkTheme.colors.background.primary,
3456
-
fontSize: 16,
3457
-
fontFamily: 'Lexend_600SemiBold',
3458
-
},
3459
-
typingCursor: {
3460
-
width: 2,
3461
-
height: 20,
3462
-
backgroundColor: darkTheme.colors.interactive.primary,
3463
-
marginLeft: 2,
3464
-
marginTop: 2,
3465
-
},
3466
-
compactionContainer: {
3467
-
marginVertical: 16,
3468
-
marginHorizontal: 18,
3469
-
},
3470
-
compactionLine: {
3471
-
flexDirection: 'row',
3472
-
alignItems: 'center',
3473
-
paddingVertical: 8,
3474
-
paddingHorizontal: 12,
3475
-
},
3476
-
compactionDivider: {
3477
-
flex: 1,
3478
-
height: 1,
3479
-
backgroundColor: '#3a3a3a',
3480
-
},
3481
-
compactionLabel: {
3482
-
fontSize: 11,
3483
-
fontFamily: 'Lexend_400Regular',
3484
-
color: darkTheme.colors.text.tertiary,
3485
-
marginHorizontal: 12,
3486
-
textTransform: 'lowercase',
3487
-
},
3488
-
compactionChevron: {
3489
-
marginLeft: 4,
3490
-
},
3491
-
compactionMessageContainer: {
3492
-
marginTop: 8,
3493
-
padding: 12,
3494
-
backgroundColor: darkTheme.colors.background.surface,
3495
-
borderRadius: 8,
3496
-
borderWidth: StyleSheet.hairlineWidth,
3497
-
borderColor: darkTheme.colors.border.secondary,
3498
-
},
3499
-
compactionMessageText: {
3500
-
fontSize: 13,
3501
-
fontFamily: 'Lexend_400Regular',
3502
-
color: darkTheme.colors.text.secondary,
3503
-
lineHeight: 18,
3504
-
},
3505
-
// Memory View Styles
3506
-
memoryViewContainer: {
3507
-
flex: 1,
3508
-
backgroundColor: darkTheme.colors.background.primary,
3509
-
},
3510
-
knowledgeTabs: {
3511
-
flexDirection: 'row',
3512
-
paddingHorizontal: 16,
3513
-
borderBottomWidth: 1,
3514
-
},
3515
-
knowledgeTab: {
3516
-
paddingVertical: 12,
3517
-
paddingHorizontal: 16,
3518
-
marginHorizontal: 4,
3519
-
},
3520
-
knowledgeTabText: {
3521
-
fontSize: 14,
3522
-
fontFamily: 'Lexend_500Medium',
3523
-
},
3524
-
memoryViewHeader: {
3525
-
paddingHorizontal: 24,
3526
-
paddingVertical: 20,
3527
-
borderBottomWidth: 1,
3528
-
borderBottomColor: darkTheme.colors.border.primary,
3529
-
},
3530
-
backToChat: {
3531
-
flexDirection: 'row',
3532
-
alignItems: 'center',
3533
-
marginBottom: 12,
3534
-
},
3535
-
backToChatText: {
3536
-
fontSize: 14,
3537
-
fontFamily: 'Lexend_400Regular',
3538
-
marginLeft: 8,
3539
-
},
3540
-
memoryViewTitle: {
3541
-
fontSize: 32,
3542
-
fontFamily: 'Lexend_700Bold',
3543
-
},
3544
-
memorySearchContainer: {
3545
-
paddingHorizontal: 24,
3546
-
paddingVertical: 16,
3547
-
flexDirection: 'row',
3548
-
alignItems: 'center',
3549
-
},
3550
-
memorySearchIcon: {
3551
-
position: 'absolute',
3552
-
left: 36,
3553
-
zIndex: 1,
3554
-
},
3555
-
memorySearchInput: {
3556
-
flex: 1,
3557
-
height: 44,
3558
-
paddingLeft: 40,
3559
-
paddingRight: 16,
3560
-
borderRadius: 22,
3561
-
fontSize: 16,
3562
-
fontFamily: 'Lexend_400Regular',
3563
-
borderWidth: 1,
3564
-
},
3565
-
memoryBlocksGrid: {
3566
-
flex: 1,
3567
-
paddingHorizontal: 16,
3568
-
},
3569
-
memoryBlocksContent: {
3570
-
paddingBottom: 24,
3571
-
},
3572
-
memoryLoadingContainer: {
3573
-
flex: 1,
3574
-
justifyContent: 'center',
3575
-
alignItems: 'center',
3576
-
paddingTop: 60,
3577
-
},
3578
-
memoryBlockCard: {
3579
-
flex: 1,
3580
-
margin: 8,
3581
-
padding: 20,
3582
-
borderRadius: 12,
3583
-
borderWidth: 1,
3584
-
minHeight: 160,
3585
-
maxWidth: '100%',
3586
-
},
3587
-
memoryBlockCardHeader: {
3588
-
flexDirection: 'row',
3589
-
justifyContent: 'space-between',
3590
-
alignItems: 'flex-start',
3591
-
marginBottom: 12,
3592
-
},
3593
-
memoryBlockCardLabel: {
3594
-
fontSize: 18,
3595
-
fontFamily: 'Lexend_600SemiBold',
3596
-
flex: 1,
3597
-
},
3598
-
memoryBlockCardCount: {
3599
-
fontSize: 12,
3600
-
fontFamily: 'Lexend_400Regular',
3601
-
marginLeft: 8,
3602
-
},
3603
-
memoryBlockCardPreview: {
3604
-
fontSize: 14,
3605
-
fontFamily: 'Lexend_400Regular',
3606
-
lineHeight: 20,
3607
-
},
3608
-
memoryEmptyState: {
3609
-
flex: 1,
3610
-
justifyContent: 'center',
3611
-
alignItems: 'center',
3612
-
paddingTop: 80,
3613
-
},
3614
-
memoryEmptyText: {
3615
-
fontSize: 16,
3616
-
fontFamily: 'Lexend_400Regular',
3617
-
marginTop: 16,
3618
-
},
3619
-
assistantMessageWithCopyContainer: {
3620
-
position: 'relative',
3621
-
flex: 1,
3622
-
},
3623
-
copyButtonContainer: {
3624
-
position: 'absolute',
3625
-
top: 0,
3626
-
right: 0,
3627
-
zIndex: 10,
3628
-
},
3629
-
copyButton: {
3630
-
padding: 8,
3631
-
opacity: 0.3,
3632
-
borderRadius: 4,
3633
-
},
3634
-
reasoningStreamingContainer: {
3635
-
paddingVertical: 12,
3636
-
paddingHorizontal: 16,
3637
-
paddingLeft: 20,
3638
-
marginBottom: 12,
3639
-
backgroundColor: 'rgba(255, 255, 255, 0.04)',
3640
-
borderRadius: 8,
3641
-
borderLeftWidth: 4,
3642
-
borderLeftColor: '#555555',
3643
-
overflow: 'hidden',
3644
-
},
3645
-
reasoningStreamingText: {
3646
-
fontSize: 14,
3647
-
fontFamily: 'Lexend_400Regular',
3648
-
color: darkTheme.colors.text.secondary,
3649
-
lineHeight: 22,
3650
-
fontStyle: 'normal',
3651
-
},
3652
-
toolCallStreamingContainer: {
3653
-
paddingLeft: 20,
3654
-
paddingRight: 16,
3655
-
marginBottom: 12,
3656
-
},
3657
-
3658
-
// Modal styles
3659
-
modalOverlay: {
3660
-
position: 'absolute',
3661
-
top: 0,
3662
-
left: 0,
3663
-
right: 0,
3664
-
bottom: 0,
3665
-
backgroundColor: 'rgba(0, 0, 0, 0.7)',
3666
-
justifyContent: 'center',
3667
-
alignItems: 'center',
3668
-
zIndex: 2000,
3669
-
},
3670
-
modalContent: {
3671
-
width: '90%',
3672
-
maxWidth: 600,
3673
-
borderRadius: 16,
3674
-
borderWidth: 1,
3675
-
padding: 24,
3676
-
maxHeight: '80%',
3677
-
},
3678
-
modalHeader: {
3679
-
flexDirection: 'row',
3680
-
justifyContent: 'space-between',
3681
-
alignItems: 'center',
3682
-
marginBottom: 20,
3683
-
},
3684
-
modalTitle: {
3685
-
fontSize: 24,
3686
-
fontFamily: 'Lexend_600SemiBold',
3687
-
},
3688
-
modalBody: {
3689
-
marginBottom: 20,
3690
-
},
3691
-
inputLabel: {
3692
-
fontSize: 14,
3693
-
fontFamily: 'Lexend_500Medium',
3694
-
marginBottom: 8,
3695
-
},
3696
-
textInput: {
3697
-
height: 44,
3698
-
borderRadius: 8,
3699
-
borderWidth: 1,
3700
-
paddingHorizontal: 12,
3701
-
fontSize: 16,
3702
-
fontFamily: 'Lexend_400Regular',
3703
-
},
3704
-
textArea: {
3705
-
minHeight: 120,
3706
-
borderRadius: 8,
3707
-
borderWidth: 1,
3708
-
paddingHorizontal: 12,
3709
-
paddingVertical: 12,
3710
-
fontSize: 16,
3711
-
fontFamily: 'Lexend_400Regular',
3712
-
textAlignVertical: 'top',
3713
-
},
3714
-
modalFooter: {
3715
-
flexDirection: 'row',
3716
-
justifyContent: 'flex-end',
3717
-
gap: 12,
3718
-
},
3719
-
modalButton: {
3720
-
paddingVertical: 12,
3721
-
paddingHorizontal: 24,
3722
-
borderRadius: 8,
3723
-
minWidth: 100,
3724
-
alignItems: 'center',
3725
-
},
3726
-
modalButtonSecondary: {
3727
-
borderWidth: 1,
3728
-
},
3729
-
modalButtonPrimary: {
3730
-
// Background color set dynamically
3731
-
},
3732
-
modalButtonText: {
3733
-
fontSize: 16,
3734
-
fontFamily: 'Lexend_500Medium',
3735
-
},
3736
-
// Settings styles
3737
-
settingsHeader: {
3738
-
paddingVertical: 16,
3739
-
paddingHorizontal: 20,
3740
-
borderBottomWidth: 1,
3741
-
},
3742
-
settingsTitle: {
3743
-
fontSize: 24,
3744
-
fontFamily: 'Lexend_600SemiBold',
3745
-
},
3746
-
settingsContent: {
3747
-
flex: 1,
3748
-
},
3749
-
settingItem: {
3750
-
flexDirection: 'row',
3751
-
justifyContent: 'space-between',
3752
-
alignItems: 'center',
3753
-
paddingVertical: 16,
3754
-
paddingHorizontal: 20,
3755
-
borderBottomWidth: 1,
3756
-
},
3757
-
settingInfo: {
3758
-
flex: 1,
3759
-
marginRight: 16,
3760
-
},
3761
-
settingLabel: {
3762
-
fontSize: 16,
3763
-
fontFamily: 'Lexend_500Medium',
3764
-
marginBottom: 4,
3765
-
},
3766
-
settingDescription: {
3767
-
fontSize: 14,
3768
-
fontFamily: 'Lexend_400Regular',
3769
-
lineHeight: 20,
3770
-
},
3771
-
toggle: {
3772
-
width: 50,
3773
-
height: 30,
3774
-
borderRadius: 15,
3775
-
backgroundColor: '#666',
3776
-
justifyContent: 'center',
3777
-
padding: 2,
3778
-
},
3779
-
toggleActive: {
3780
-
backgroundColor: '#4D96FF',
3781
-
},
3782
-
toggleThumb: {
3783
-
width: 26,
3784
-
height: 26,
3785
-
borderRadius: 13,
3786
-
backgroundColor: '#fff',
3787
-
},
3788
-
toggleThumbActive: {
3789
-
alignSelf: 'flex-end',
3790
-
},
3791
-
toolReturnContainer: {
3792
-
width: '100%',
3793
-
marginTop: -8,
3794
-
marginBottom: 4,
3795
-
},
3796
-
toolReturnHeader: {
3797
-
flexDirection: 'row',
3798
-
alignItems: 'center',
3799
-
gap: 3,
3800
-
paddingVertical: 4,
3801
-
paddingHorizontal: 8,
3802
-
backgroundColor: 'transparent',
3803
-
borderRadius: 4,
3804
-
},
3805
-
toolReturnLabel: {
3806
-
fontSize: 10,
3807
-
fontFamily: 'Lexend_400Regular',
3808
-
color: darkTheme.colors.text.tertiary,
3809
-
opacity: 0.5,
3810
-
},
3811
-
toolReturnContent: {
3812
-
backgroundColor: 'rgba(30, 30, 30, 0.3)',
3813
-
borderRadius: 4,
3814
-
padding: 8,
3815
-
borderWidth: 1,
3816
-
borderColor: 'rgba(255, 255, 255, 0.03)',
3817
-
marginTop: 0,
3818
-
},
3819
-
});
+4
-28
App.tsx
+4
-28
App.tsx
···
1
1
/**
2
-
* App.tsx - Toggle Between Old and New Versions
3
-
*
4
-
* This file serves as a toggle point to switch between:
5
-
* - App.old.tsx: Original 3,826-line monolithic app (fully working)
6
-
* - App.new.tsx: New refactored modular app (~420 lines)
7
-
*
8
-
* To test the refactored version:
9
-
* 1. Change USE_NEW_APP to true
10
-
* 2. Restart the app
11
-
* 3. Test all features against the checklist in MIGRATION_TRACKER.md
12
-
* 4. If any issues, change back to false
2
+
* App.tsx - Main Application Entry Point
13
3
*
14
-
* Once 100% feature parity is achieved, we'll delete this toggle
15
-
* and make the new version the default.
4
+
* Imports the refactored modular application structure.
5
+
* See App.new.tsx for the main app implementation.
16
6
*/
17
7
18
-
// ============================================================================
19
-
// TOGGLE FLAG - Change this to switch versions
20
-
// ============================================================================
21
-
const USE_NEW_APP = true; // Set to true to test refactored version
22
-
// ============================================================================
23
-
24
-
let App: any;
25
-
26
-
if (USE_NEW_APP) {
27
-
console.log('🔄 Using NEW refactored app (App.new.tsx)');
28
-
App = require('./App.new').default;
29
-
} else {
30
-
console.log('✅ Using OLD monolithic app (App.old.tsx)');
31
-
App = require('./App.old').default;
32
-
}
8
+
import App from './App.new';
33
9
34
10
export default App;