A React Native app for the ultimate thinking partner.

chore: remove old monolithic app and simplify App.tsx

Fully commits to the new refactored architecture by removing the old
monolithic backup (App.old.tsx) and simplifying the toggle system.

Changes:
- Delete App.old.tsx (3,826-line monolithic version)
- Simplify App.tsx to directly import App.new.tsx (no toggle needed)

This fixes the bundling error caused by App.old.tsx importing the
deleted ReasoningToggle component.

The refactored app is now the only version.

Changed files
+4 -3847
-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
··· 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;