A React Native app for the ultimate thinking partner.

feat(knowledge): refactor Memory to Knowledge view with tabs and auto-close files

- Rename Memory to Knowledge throughout UI
- Add view switcher between Chat and Knowledge
- Add tabs for Core Memory, Archival Memory, and Files
- Move Files section from sidebar to Knowledge > Files tab
- Add closeAllFiles API call after file upload to prevent context flooding

+742 -250
+724 -248
App.tsx
··· 16 16 Linking, 17 17 Animated, 18 18 Image, 19 + KeyboardAvoidingView, 19 20 } from 'react-native'; 20 21 import { Ionicons } from '@expo/vector-icons'; 21 22 import { StatusBar } from 'expo-status-bar'; 23 + import * as Clipboard from 'expo-clipboard'; 22 24 import { SafeAreaProvider, useSafeAreaInsets } from 'react-native-safe-area-context'; 23 25 import * as ImagePicker from 'expo-image-picker'; 24 26 import { useFonts, Lexend_300Light, Lexend_400Regular, Lexend_500Medium, Lexend_600SemiBold, Lexend_700Bold } from '@expo-google-fonts/lexend'; ··· 93 95 const spacerHeightAnim = useRef(new Animated.Value(0)).current; 94 96 const streamCompleteRef = useRef(false); 95 97 const rainbowAnimValue = useRef(new Animated.Value(0)).current; 98 + const [isInputFocused, setIsInputFocused] = useState(false); 96 99 97 100 // Token buffering for smooth streaming 98 101 const tokenBufferRef = useRef<string>(''); ··· 117 120 // Layout state for responsive design 118 121 const [screenData, setScreenData] = useState(Dimensions.get('window')); 119 122 const [sidebarVisible, setSidebarVisible] = useState(false); 120 - const [activeSidebarTab, setActiveSidebarTab] = useState<'memory' | 'files'>('memory'); 123 + const [activeSidebarTab, setActiveSidebarTab] = useState<'files'>('files'); 124 + const [currentView, setCurrentView] = useState<'chat' | 'knowledge'>('chat'); 125 + const [knowledgeTab, setKnowledgeTab] = useState<'core' | 'archival' | 'files'>('core'); 121 126 const [memoryBlocks, setMemoryBlocks] = useState<MemoryBlock[]>([]); 122 127 const [isLoadingBlocks, setIsLoadingBlocks] = useState(false); 123 128 const [blocksError, setBlocksError] = useState<string | null>(null); 124 129 const [selectedBlock, setSelectedBlock] = useState<MemoryBlock | null>(null); 130 + const [memorySearchQuery, setMemorySearchQuery] = useState(''); 125 131 const sidebarAnimRef = useRef(new Animated.Value(0)).current; 126 - const [developerMode, setDeveloperMode] = useState(false); 132 + const [developerMode, setDeveloperMode] = useState(true); 127 133 const [headerClickCount, setHeaderClickCount] = useState(0); 128 134 const headerClickTimeoutRef = useRef<NodeJS.Timeout | null>(null); 129 135 ··· 307 313 const loadMoreMessages = () => { 308 314 if (hasMoreBefore && !isLoadingMore && earliestCursor) { 309 315 loadMessages(earliestCursor); 316 + } 317 + }; 318 + 319 + const copyToClipboard = async (content: string) => { 320 + try { 321 + await Clipboard.setStringAsync(content); 322 + // Optionally show a brief success feedback 323 + } catch (error) { 324 + console.error('Failed to copy to clipboard:', error); 310 325 } 311 326 }; 312 327 ··· 933 948 if (status.status === 'completed') { 934 949 console.log('File uploaded successfully'); 935 950 await loadFolderFiles(); 951 + 952 + // Close all open files to avoid flooding context 953 + if (coAgent) { 954 + try { 955 + await lettaApi.closeAllFiles(coAgent.id); 956 + console.log('Closed all open files after upload'); 957 + } catch (err) { 958 + console.error('Failed to close files:', err); 959 + } 960 + } 961 + 936 962 setUploadProgress(''); 937 963 Alert.alert('Success', `${file.name} uploaded successfully`); 938 964 break; ··· 944 970 if (jobError.status === 404) { 945 971 console.log('Job not found - assuming completed'); 946 972 await loadFolderFiles(); 973 + 974 + // Close all open files to avoid flooding context 975 + if (coAgent) { 976 + try { 977 + await lettaApi.closeAllFiles(coAgent.id); 978 + console.log('Closed all open files after upload'); 979 + } catch (err) { 980 + console.error('Failed to close files:', err); 981 + } 982 + } 983 + 947 984 setUploadProgress(''); 948 985 Alert.alert('Success', `${file.name} uploaded successfully`); 949 986 break; ··· 961 998 // Upload completed immediately 962 999 console.log('File uploaded immediately'); 963 1000 await loadFolderFiles(); 1001 + 1002 + // Close all open files to avoid flooding context 1003 + if (coAgent) { 1004 + try { 1005 + await lettaApi.closeAllFiles(coAgent.id); 1006 + console.log('Closed all open files after upload'); 1007 + } catch (err) { 1008 + console.error('Failed to close files:', err); 1009 + } 1010 + } 1011 + 964 1012 setUploadProgress(''); 965 1013 Alert.alert('Success', `${file.name} uploaded successfully`); 966 1014 } ··· 983 1031 const deleteFile = async (fileId: string, fileName: string) => { 984 1032 if (!coFolder) return; 985 1033 986 - Alert.alert( 987 - 'Delete File', 988 - `Are you sure you want to delete "${fileName}"?`, 989 - [ 990 - { text: 'Cancel', style: 'cancel' }, 991 - { 992 - text: 'Delete', 993 - style: 'destructive', 994 - onPress: async () => { 995 - try { 996 - await lettaApi.deleteFile(coFolder.id, fileId); 997 - await loadFolderFiles(); 998 - Alert.alert('Success', 'File deleted'); 999 - } catch (error: any) { 1000 - console.error('Delete error:', error); 1001 - Alert.alert('Error', 'Failed to delete file: ' + (error.message || 'Unknown error')); 1002 - } 1003 - }, 1004 - }, 1005 - ] 1006 - ); 1034 + const confirmed = Platform.OS === 'web' 1035 + ? window.confirm(`Are you sure you want to delete "${fileName}"?`) 1036 + : await new Promise<boolean>((resolve) => { 1037 + Alert.alert( 1038 + 'Delete File', 1039 + `Are you sure you want to delete "${fileName}"?`, 1040 + [ 1041 + { text: 'Cancel', style: 'cancel', onPress: () => resolve(false) }, 1042 + { text: 'Delete', style: 'destructive', onPress: () => resolve(true) }, 1043 + ] 1044 + ); 1045 + }); 1046 + 1047 + if (!confirmed) return; 1048 + 1049 + try { 1050 + await lettaApi.deleteFile(coFolder.id, fileId); 1051 + await loadFolderFiles(); 1052 + if (Platform.OS === 'web') { 1053 + window.alert('File deleted successfully'); 1054 + } else { 1055 + Alert.alert('Success', 'File deleted'); 1056 + } 1057 + } catch (error: any) { 1058 + console.error('Delete error:', error); 1059 + if (Platform.OS === 'web') { 1060 + window.alert('Failed to delete file: ' + (error.message || 'Unknown error')); 1061 + } else { 1062 + Alert.alert('Error', 'Failed to delete file: ' + (error.message || 'Unknown error')); 1063 + } 1064 + } 1007 1065 }; 1008 1066 1009 1067 useEffect(() => { 1010 - if (coAgent && sidebarVisible && activeSidebarTab === 'memory') { 1068 + if (coAgent && currentView === 'knowledge') { 1011 1069 loadMemoryBlocks(); 1012 1070 } 1013 - }, [coAgent, sidebarVisible, activeSidebarTab]); 1071 + }, [coAgent, currentView]); 1014 1072 1015 1073 useEffect(() => { 1016 - if (coAgent && sidebarVisible && activeSidebarTab === 'files') { 1074 + if (coAgent && sidebarVisible) { 1017 1075 if (!coFolder) { 1018 1076 initializeCoFolder(); 1019 1077 } else { 1020 1078 loadFolderFiles(); 1021 1079 } 1022 1080 } 1023 - }, [coAgent, sidebarVisible, activeSidebarTab]); 1081 + }, [coAgent, sidebarVisible]); 1024 1082 1025 1083 // Initialize folder when agent is ready 1026 1084 useEffect(() => { ··· 1028 1086 initializeCoFolder(); 1029 1087 } 1030 1088 }, [coAgent]); 1089 + 1090 + // State for tracking expanded reasoning 1091 + const [expandedReasoning, setExpandedReasoning] = useState<Set<string>>(new Set()); 1092 + const [expandedCompaction, setExpandedCompaction] = useState<Set<string>>(new Set()); 1031 1093 1032 1094 // Animate sidebar 1033 1095 useEffect(() => { ··· 1038 1100 }).start(); 1039 1101 }, [sidebarVisible]); 1040 1102 1041 - // Animate rainbow gradient for "co is thinking" 1103 + // Animate rainbow gradient for "co is thinking", input box, and reasoning sections 1042 1104 useEffect(() => { 1043 - if (isReasoningStreaming) { 1105 + if (isReasoningStreaming || isInputFocused || expandedReasoning.size > 0) { 1044 1106 rainbowAnimValue.setValue(0); 1045 1107 Animated.loop( 1046 1108 Animated.timing(rainbowAnimValue, { ··· 1052 1114 } else { 1053 1115 rainbowAnimValue.stopAnimation(); 1054 1116 } 1055 - }, [isReasoningStreaming]); 1056 - 1057 - // State for tracking expanded reasoning 1058 - const [expandedReasoning, setExpandedReasoning] = useState<Set<string>>(new Set()); 1059 - const [expandedCompaction, setExpandedCompaction] = useState<Set<string>>(new Set()); 1117 + }, [isReasoningStreaming, isInputFocused, expandedReasoning]); 1060 1118 1061 1119 const toggleReasoning = (messageId: string) => { 1062 1120 setExpandedReasoning(prev => { ··· 1098 1156 } 1099 1157 }); 1100 1158 1159 + // Build a map to find reasoning messages that precede tool calls 1160 + const reasoningBeforeToolCall = new Map<string, string>(); 1161 + const reasoningMessagesToSkip = new Set<string>(); 1162 + 1163 + for (let i = 0; i < messages.length - 1; i++) { 1164 + const current = messages[i]; 1165 + const next = messages[i + 1]; 1166 + 1167 + // If current message has reasoning and next is a tool_call, associate them 1168 + if (current.reasoning && next.message_type?.includes('tool_call') && next.id) { 1169 + reasoningBeforeToolCall.set(next.id, current.reasoning); 1170 + reasoningMessagesToSkip.add(current.id); // Mark this message to skip 1171 + console.log('🔗 Found reasoning before tool call:', { 1172 + toolCallId: next.id, 1173 + reasoningMsgId: current.id, 1174 + reasoning: current.reasoning.substring(0, 100) + '...' 1175 + }); 1176 + } 1177 + } 1178 + 1101 1179 messages.forEach(msg => { 1102 1180 if (processedIds.has(msg.id)) return; 1181 + 1182 + // Skip reasoning messages that precede tool calls 1183 + if (reasoningMessagesToSkip.has(msg.id)) { 1184 + processedIds.add(msg.id); 1185 + console.log('⏭️ Skipping reasoning message that precedes tool call:', msg.id); 1186 + return; 1187 + } 1103 1188 1104 1189 // Filter out login/heartbeat messages 1105 1190 if (msg.role === 'user' && msg.content) { ··· 1117 1202 if (msg.message_type?.includes('tool_return') && msg.step_id) { 1118 1203 const toolCall = toolCallsMap.get(msg.step_id); 1119 1204 if (toolCall) { 1205 + // Get reasoning from the message that preceded the tool call 1206 + const reasoning = reasoningBeforeToolCall.get(toolCall.id); 1207 + console.log('📦 Creating tool pair group:'); 1208 + console.log(' toolCall.id:', toolCall.id); 1209 + console.log(' reasoning from map:', reasoning); 1210 + 1120 1211 groups.push({ 1121 1212 key: toolCall.id, 1122 1213 type: 'toolPair', 1123 1214 call: toolCall, 1124 1215 ret: msg, 1125 - reasoning: toolCall.reasoning || msg.reasoning, 1216 + reasoning: reasoning, 1126 1217 }); 1127 1218 processedIds.add(toolCall.id); 1128 1219 processedIds.add(msg.id); ··· 1150 1241 const toolCall = item.call.tool_call || item.call.tool_calls?.[0]; 1151 1242 let callText = item.call.content || 'Tool call'; 1152 1243 1244 + // DEBUG LOGGING 1245 + console.log('🔧 Tool Pair Debug:'); 1246 + console.log(' item.reasoning:', item.reasoning); 1247 + console.log(' item.call.reasoning:', item.call.reasoning); 1248 + console.log(' item.ret?.reasoning:', item.ret?.reasoning); 1249 + console.log(' item.call keys:', Object.keys(item.call)); 1250 + console.log(' Full item.call:', JSON.stringify(item.call, null, 2)); 1251 + 1153 1252 if (toolCall) { 1154 1253 // Handle both formats: with and without .function wrapper 1155 1254 const callObj = toolCall.function || toolCall; ··· 1165 1264 } 1166 1265 1167 1266 const resultText = item.ret?.content || undefined; 1267 + const reasoning = item.reasoning || undefined; 1268 + 1269 + console.log(' Final reasoning value:', reasoning); 1168 1270 1169 1271 return ( 1170 1272 <View key={item.key} style={styles.messageContainer}> 1171 1273 <ToolCallItem 1172 1274 callText={callText} 1173 1275 resultText={resultText} 1276 + reasoning={reasoning} 1174 1277 /> 1175 1278 </View> 1176 1279 ); ··· 1322 1425 onPress={() => toggleReasoning(msg.id)} 1323 1426 style={styles.reasoningToggle} 1324 1427 > 1428 + <Ionicons 1429 + name="sparkles" 1430 + size={16} 1431 + color={darkTheme.colors.text.secondary} 1432 + style={{ marginRight: 6 }} 1433 + /> 1325 1434 <Text style={styles.reasoningToggleText}>Reasoning</Text> 1435 + <View style={{ flex: 1 }} /> 1326 1436 <Ionicons 1327 1437 name={isReasoningExpanded ? "chevron-up" : "chevron-down"} 1328 1438 size={16} ··· 1331 1441 </TouchableOpacity> 1332 1442 )} 1333 1443 {item.reasoning && isReasoningExpanded && ( 1334 - <View style={styles.reasoningExpandedContainer}> 1444 + <Animated.View style={[ 1445 + styles.reasoningExpandedContainer, 1446 + { 1447 + borderLeftColor: rainbowAnimValue.interpolate({ 1448 + inputRange: [0, 0.2, 0.4, 0.6, 0.8, 1], 1449 + outputRange: ['#FF6B6B', '#FFD93D', '#6BCF7F', '#4D96FF', '#9D4EDD', '#FF6B6B'] 1450 + }), 1451 + } 1452 + ]}> 1335 1453 <Text style={styles.reasoningExpandedText}>{item.reasoning}</Text> 1336 - </View> 1454 + </Animated.View> 1337 1455 )} 1338 1456 <ExpandableMessageContent 1339 1457 content={msg.content} ··· 1341 1459 isDark={colorScheme === 'dark'} 1342 1460 lineLimit={20} 1343 1461 /> 1462 + <View style={styles.copyButtonContainer}> 1463 + <TouchableOpacity 1464 + onPress={() => copyToClipboard(msg.content)} 1465 + style={styles.copyButton} 1466 + activeOpacity={0.7} 1467 + testID="copy-button" 1468 + > 1469 + <Ionicons 1470 + name="copy-outline" 1471 + size={16} 1472 + color={theme.colors.text.tertiary} 1473 + /> 1474 + </TouchableOpacity> 1475 + </View> 1476 + <View style={styles.messageSeparator} /> 1344 1477 </View> 1345 1478 ); 1346 1479 } ··· 1387 1520 setInputContainerHeight(e.nativeEvent.layout.height || 0); 1388 1521 }; 1389 1522 1390 - const inputStyles = { 1523 + const inputStyles = useMemo(() => ({ 1391 1524 width: '100%', 1392 - height: 76, 1525 + height: 48, 1393 1526 paddingLeft: 18, 1394 1527 paddingRight: 130, 1395 1528 paddingTop: 12, 1396 1529 paddingBottom: 12, 1397 1530 borderRadius: 24, 1398 - color: colorScheme === 'dark' ? '#000000' : '#FFFFFF', // Inverted: black text in dark mode 1531 + color: colorScheme === 'dark' ? '#FFFFFF' : '#000000', 1399 1532 fontFamily: 'Lexend_400Regular', 1400 1533 fontSize: 16, 1401 - lineHeight: 20, 1534 + lineHeight: 24, 1402 1535 borderWidth: 0, 1403 - backgroundColor: 'transparent', 1536 + backgroundColor: theme.colors.background.primary, 1404 1537 ...(Platform.OS === 'web' && { 1405 1538 // @ts-ignore 1406 - background: 'transparent', 1407 - backgroundImage: 'none', 1539 + outline: 'none', 1408 1540 WebkitAppearance: 'none', 1409 1541 MozAppearance: 'none', 1410 1542 }), 1411 - } as const; 1543 + } as const), [colorScheme, theme]); 1544 + 1545 + const inputWrapperStyle = useMemo(() => ({ 1546 + borderRadius: 24, 1547 + borderWidth: 2, 1548 + borderColor: colorScheme === 'dark' ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.1)', 1549 + shadowColor: '#000', 1550 + shadowOffset: { width: 0, height: 2 }, 1551 + shadowOpacity: 0.1, 1552 + shadowRadius: 8, 1553 + elevation: 2, 1554 + }), [colorScheme]); 1412 1555 1413 1556 if (isLoadingToken || !fontsLoaded) { 1414 1557 return ( ··· 1476 1619 <TouchableOpacity 1477 1620 style={[styles.menuItem, { borderBottomColor: theme.colors.border.primary }]} 1478 1621 onPress={() => { 1479 - setActiveSidebarTab('memory'); 1622 + setCurrentView('knowledge'); 1480 1623 loadMemoryBlocks(); 1481 1624 }} 1482 1625 > 1483 1626 <Ionicons name="library-outline" size={24} color={theme.colors.text.primary} /> 1484 - <Text style={[styles.menuItemText, { color: theme.colors.text.primary }]}>Memory</Text> 1485 - </TouchableOpacity> 1486 - 1487 - <TouchableOpacity 1488 - style={[styles.menuItem, { borderBottomColor: theme.colors.border.primary }]} 1489 - onPress={() => { 1490 - setActiveSidebarTab('files'); 1491 - }} 1492 - > 1493 - <Ionicons name="document-text-outline" size={24} color={theme.colors.text.primary} /> 1494 - <Text style={[styles.menuItemText, { color: theme.colors.text.primary }]}>Files</Text> 1627 + <Text style={[styles.menuItemText, { color: theme.colors.text.primary }]}>Knowledge</Text> 1495 1628 </TouchableOpacity> 1496 1629 1497 1630 <TouchableOpacity ··· 1548 1681 try { 1549 1682 if (coAgent) { 1550 1683 console.log('Deleting agent:', coAgent.id); 1551 - await lettaApi.deleteAgent(coAgent.id); 1552 - console.log('Agent deleted, clearing state...'); 1684 + const deleteResult = await lettaApi.deleteAgent(coAgent.id); 1685 + console.log('Delete result:', deleteResult); 1686 + console.log('Agent deleted successfully, clearing state...'); 1553 1687 setCoAgent(null); 1554 1688 setMessages([]); 1689 + setEarliestCursor(null); 1690 + setHasMoreBefore(false); 1555 1691 console.log('Initializing new co agent...'); 1556 1692 await initializeCo(); 1557 1693 console.log('Co agent refreshed successfully'); 1694 + if (Platform.OS === 'web') { 1695 + window.alert('Co agent refreshed successfully'); 1696 + } else { 1697 + Alert.alert('Success', 'Co agent refreshed successfully'); 1698 + } 1558 1699 } 1559 1700 } catch (error: any) { 1560 - console.error('Error refreshing co:', error); 1701 + console.error('=== ERROR REFRESHING CO ==='); 1702 + console.error('Error type:', typeof error); 1703 + console.error('Error message:', error?.message); 1704 + console.error('Error stack:', error?.stack); 1705 + console.error('Full error:', error); 1561 1706 if (Platform.OS === 'web') { 1562 1707 window.alert('Failed to refresh co: ' + (error.message || 'Unknown error')); 1563 1708 } else { ··· 1583 1728 </TouchableOpacity> 1584 1729 </View> 1585 1730 } 1586 - ListFooterComponent={ 1587 - <> 1588 - {/* Memory blocks section - only show when memory tab is active */} 1589 - {activeSidebarTab === 'memory' && ( 1590 - <View style={styles.memorySection}> 1591 - <Text style={[styles.memorySectionTitle, { color: theme.colors.text.secondary }]}>Memory Blocks</Text> 1592 - {isLoadingBlocks ? ( 1593 - <ActivityIndicator size="large" color={theme.colors.text.secondary} /> 1594 - ) : blocksError ? ( 1595 - <Text style={styles.errorText}>{blocksError}</Text> 1596 - ) : ( 1597 - <FlatList 1598 - data={memoryBlocks} 1599 - keyExtractor={(item) => item.id || item.label} 1600 - renderItem={({ item }) => ( 1601 - <TouchableOpacity 1602 - style={[styles.memoryBlockItem, { 1603 - backgroundColor: theme.colors.background.primary, 1604 - borderColor: theme.colors.border.primary 1605 - }]} 1606 - onPress={() => setSelectedBlock(item)} 1607 - > 1608 - <Text style={[styles.memoryBlockLabel, { color: theme.colors.text.primary }]}>{item.label}</Text> 1609 - <Text style={[styles.memoryBlockPreview, { color: theme.colors.text.secondary }]} numberOfLines={2}> 1610 - {item.value} 1611 - </Text> 1612 - </TouchableOpacity> 1613 - )} 1614 - /> 1615 - )} 1616 - </View> 1617 - )} 1618 - 1619 - {/* Files section - only show when files tab is active */} 1620 - {activeSidebarTab === 'files' && ( 1621 - <View style={styles.memorySection}> 1622 - <View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}> 1623 - <Text style={[styles.memorySectionTitle, { color: theme.colors.text.secondary, marginBottom: 0 }]}>Files</Text> 1624 - <TouchableOpacity 1625 - onPress={pickAndUploadFile} 1626 - disabled={isUploadingFile} 1627 - style={{ padding: 4 }} 1628 - > 1629 - {isUploadingFile ? ( 1630 - <ActivityIndicator size="small" color={theme.colors.text.secondary} /> 1631 - ) : ( 1632 - <Ionicons name="add-circle-outline" size={24} color={theme.colors.text.primary} /> 1633 - )} 1634 - </TouchableOpacity> 1635 - </View> 1636 - {uploadProgress && ( 1637 - <View style={{ marginBottom: 12, paddingVertical: 8, paddingHorizontal: 12, backgroundColor: theme.colors.background.tertiary, borderRadius: 8 }}> 1638 - <Text style={{ color: theme.colors.text.secondary, fontSize: 14 }}>{uploadProgress}</Text> 1639 - </View> 1640 - )} 1641 - {isLoadingFiles ? ( 1642 - <ActivityIndicator size="large" color={theme.colors.text.secondary} /> 1643 - ) : filesError ? ( 1644 - <Text style={styles.errorText}>{filesError}</Text> 1645 - ) : folderFiles.length === 0 ? ( 1646 - <Text style={[styles.memoryBlockPreview, { color: theme.colors.text.secondary, textAlign: 'center', marginTop: 20 }]}> 1647 - No files uploaded yet 1648 - </Text> 1649 - ) : ( 1650 - <FlatList 1651 - data={folderFiles} 1652 - keyExtractor={(item) => item.id} 1653 - renderItem={({ item }) => ( 1654 - <View 1655 - style={[styles.memoryBlockItem, { 1656 - backgroundColor: theme.colors.background.primary, 1657 - borderColor: theme.colors.border.primary, 1658 - flexDirection: 'row', 1659 - justifyContent: 'space-between', 1660 - alignItems: 'center' 1661 - }]} 1662 - > 1663 - <View style={{ flex: 1 }}> 1664 - <Text style={[styles.memoryBlockLabel, { color: theme.colors.text.primary }]} numberOfLines={1}> 1665 - {item.fileName || item.name || 'Untitled'} 1666 - </Text> 1667 - <Text style={[styles.memoryBlockPreview, { color: theme.colors.text.secondary, fontSize: 12 }]}> 1668 - {new Date(item.createdAt || item.created_at).toLocaleDateString()} 1669 - </Text> 1670 - </View> 1671 - <TouchableOpacity 1672 - onPress={() => deleteFile(item.id, item.fileName || item.name)} 1673 - style={{ padding: 8 }} 1674 - > 1675 - <Ionicons name="trash-outline" size={20} color={theme.colors.status.error} /> 1676 - </TouchableOpacity> 1677 - </View> 1678 - )} 1679 - /> 1680 - )} 1681 - </View> 1682 - )} 1683 - </> 1684 - } 1685 1731 data={[]} 1686 1732 renderItem={() => null} 1687 1733 /> ··· 1724 1770 <View style={styles.headerSpacer} /> 1725 1771 </View> 1726 1772 1727 - {/* Chat and Memory Row */} 1728 - <View style={styles.chatRow}> 1773 + {/* View Switcher */} 1774 + <View style={[styles.viewSwitcher, { backgroundColor: theme.colors.background.secondary, borderBottomColor: theme.colors.border.primary }]}> 1775 + <TouchableOpacity 1776 + style={[ 1777 + styles.viewSwitcherButton, 1778 + currentView === 'chat' && { backgroundColor: theme.colors.background.tertiary } 1779 + ]} 1780 + onPress={() => setCurrentView('chat')} 1781 + > 1782 + <Text style={[ 1783 + styles.viewSwitcherText, 1784 + { color: currentView === 'chat' ? theme.colors.text.primary : theme.colors.text.tertiary } 1785 + ]}>Chat</Text> 1786 + </TouchableOpacity> 1787 + <TouchableOpacity 1788 + style={[ 1789 + styles.viewSwitcherButton, 1790 + currentView === 'knowledge' && { backgroundColor: theme.colors.background.tertiary } 1791 + ]} 1792 + onPress={() => { 1793 + setCurrentView('knowledge'); 1794 + loadMemoryBlocks(); 1795 + }} 1796 + > 1797 + <Text style={[ 1798 + styles.viewSwitcherText, 1799 + { color: currentView === 'knowledge' ? theme.colors.text.primary : theme.colors.text.tertiary } 1800 + ]}>Knowledge</Text> 1801 + </TouchableOpacity> 1802 + </View> 1803 + 1804 + {/* Chat and Knowledge Row */} 1805 + <KeyboardAvoidingView 1806 + behavior={Platform.OS === 'ios' ? 'padding' : 'height'} 1807 + style={styles.chatRow} 1808 + keyboardVerticalOffset={Platform.OS === 'ios' ? insets.top + 60 : 0} 1809 + > 1810 + {currentView === 'chat' ? ( 1811 + <> 1729 1812 {/* Messages */} 1730 1813 <View style={styles.messagesContainer} onLayout={handleMessagesLayout}> 1731 1814 <FlatList ··· 1773 1856 <Text style={{ fontSize: 24, fontFamily: 'Lexend_400Regular', color: darkTheme.colors.text.tertiary }}> is thinking</Text> 1774 1857 </View> 1775 1858 ) : ( 1776 - <Text style={styles.reasoningToggleText}>Reasoning</Text> 1859 + <> 1860 + <Ionicons 1861 + name="sparkles" 1862 + size={16} 1863 + color={darkTheme.colors.text.secondary} 1864 + style={{ marginRight: 6 }} 1865 + /> 1866 + <Text style={styles.reasoningToggleText}>Reasoning</Text> 1867 + <View style={{ flex: 1 }} /> 1868 + </> 1777 1869 )} 1778 1870 {!isReasoningStreaming && ( 1779 1871 <Ionicons ··· 1784 1876 )} 1785 1877 </TouchableOpacity> 1786 1878 {expandedReasoning.has('streaming') && ( 1787 - <View style={styles.reasoningExpandedContainer}> 1879 + <Animated.View style={[ 1880 + styles.reasoningExpandedContainer, 1881 + { 1882 + borderLeftColor: rainbowAnimValue.interpolate({ 1883 + inputRange: [0, 0.2, 0.4, 0.6, 0.8, 1], 1884 + outputRange: ['#FF6B6B', '#FFD93D', '#6BCF7F', '#4D96FF', '#9D4EDD', '#FF6B6B'] 1885 + }), 1886 + } 1887 + ]}> 1788 1888 <Text style={styles.reasoningExpandedText}> 1789 1889 {streamingReasoning || ''} 1790 1890 </Text> 1791 - </View> 1891 + </Animated.View> 1792 1892 )} 1793 1893 {streamingStep && ( 1794 1894 <Text style={styles.streamingStep}>{streamingStep}</Text> 1795 1895 )} 1796 1896 {streamingMessage && ( 1797 - <MessageContent 1798 - content={streamingMessage + ' ○'} 1799 - isUser={false} 1800 - isDark={colorScheme === 'dark'} 1801 - /> 1897 + <> 1898 + <View style={{ flex: 1 }}> 1899 + <MessageContent 1900 + content={streamingMessage} 1901 + isUser={false} 1902 + isDark={colorScheme === 'dark'} 1903 + /> 1904 + <Text style={{ color: theme.colors.text.tertiary, marginTop: 4 }}>○</Text> 1905 + </View> 1906 + <View style={styles.copyButtonContainer}> 1907 + <TouchableOpacity 1908 + onPress={() => copyToClipboard(streamingMessage)} 1909 + style={styles.copyButton} 1910 + activeOpacity={0.7} 1911 + testID="copy-button" 1912 + > 1913 + <Ionicons 1914 + name="copy-outline" 1915 + size={16} 1916 + color={theme.colors.text.tertiary} 1917 + /> 1918 + </TouchableOpacity> 1919 + </View> 1920 + <View style={styles.messageSeparator} /> 1921 + </> 1802 1922 )} 1803 1923 </Animated.View> 1804 1924 )} ··· 1830 1950 </View> 1831 1951 1832 1952 {/* Input */} 1833 - <View style={[styles.inputContainer, { paddingBottom: Math.max(insets.bottom, 16) }]} onLayout={handleInputLayout}> 1953 + <View 1954 + style={[styles.inputContainer, { paddingBottom: Math.max(insets.bottom, 16) }]} 1955 + onLayout={handleInputLayout} 1956 + > 1834 1957 <View style={styles.inputCentered}> 1835 - {/* Solid backdrop matching theme */} 1836 - <View style={[ 1837 - styles.inputBackdrop, 1838 - { 1839 - backgroundColor: colorScheme === 'dark' ? '#FFFFFF' : '#000000', 1840 - borderWidth: 0, 1841 - } 1842 - ]} /> 1843 - 1844 1958 {/* Image preview section */} 1845 1959 {selectedImages.length > 0 && ( 1846 1960 <View style={styles.imagePreviewContainer}> ··· 1858 1972 </View> 1859 1973 )} 1860 1974 1861 - <View style={styles.inputWrapper}> 1975 + <Animated.View style={[ 1976 + styles.inputWrapper, 1977 + inputWrapperStyle, 1978 + isInputFocused && { 1979 + borderColor: rainbowAnimValue.interpolate({ 1980 + inputRange: [0, 0.2, 0.4, 0.6, 0.8, 1], 1981 + outputRange: ['#FF6B6B', '#FFD93D', '#6BCF7F', '#4D96FF', '#9D4EDD', '#FF6B6B'] 1982 + }), 1983 + shadowColor: rainbowAnimValue.interpolate({ 1984 + inputRange: [0, 0.2, 0.4, 0.6, 0.8, 1], 1985 + outputRange: ['#FF6B6B', '#FFD93D', '#6BCF7F', '#4D96FF', '#9D4EDD', '#FF6B6B'] 1986 + }), 1987 + shadowOpacity: 0.4, 1988 + shadowRadius: 16, 1989 + } 1990 + ]}> 1862 1991 <TouchableOpacity 1863 1992 onPress={pickAndUploadFile} 1864 1993 style={styles.fileButton} 1865 1994 disabled={isSendingMessage || isUploadingFile} 1866 1995 > 1867 - <Ionicons name="attach-outline" size={24} color="#888888" /> 1996 + <Ionicons name="attach-outline" size={20} color="#666666" /> 1868 1997 </TouchableOpacity> 1869 1998 <TouchableOpacity 1870 1999 onPress={pickImage} 1871 2000 style={styles.imageButton} 1872 2001 disabled={isSendingMessage} 1873 2002 > 1874 - <Ionicons name="image-outline" size={24} color="#888888" /> 2003 + <Ionicons name="image-outline" size={20} color="#666666" /> 1875 2004 </TouchableOpacity> 1876 2005 <TextInput 1877 2006 style={inputStyles} 1878 - placeholder="" 2007 + placeholder="What's on your mind?" 1879 2008 placeholderTextColor={colorScheme === 'dark' ? '#666666' : '#999999'} 1880 2009 value={inputText} 1881 2010 onChangeText={setInputText} 2011 + onFocus={() => setIsInputFocused(true)} 2012 + onBlur={() => setIsInputFocused(false)} 1882 2013 multiline 1883 2014 maxLength={4000} 1884 2015 editable={!isSendingMessage} ··· 1888 2019 onPress={sendMessage} 1889 2020 style={[ 1890 2021 styles.sendButton, 1891 - { backgroundColor: theme.colors.background.primary }, 1892 - ((!inputText.trim() && selectedImages.length === 0) || isSendingMessage) && styles.sendButtonDisabled 2022 + { 2023 + backgroundColor: (!inputText.trim() && selectedImages.length === 0) || isSendingMessage 2024 + ? 'transparent' 2025 + : colorScheme === 'dark' ? CoColors.pureWhite : CoColors.deepBlack 2026 + }, 1893 2027 ]} 1894 2028 disabled={(!inputText.trim() && selectedImages.length === 0) || isSendingMessage} 1895 2029 > 1896 2030 {isSendingMessage ? ( 1897 2031 <ActivityIndicator size="small" color={colorScheme === 'dark' ? '#fff' : '#000'} /> 1898 2032 ) : ( 1899 - <View style={[styles.sendRing, { borderColor: colorScheme === 'dark' ? CoColors.pureWhite : CoColors.deepBlack }]} /> 2033 + <Ionicons 2034 + name="arrow-up" 2035 + size={20} 2036 + color={ 2037 + (!inputText.trim() && selectedImages.length === 0) 2038 + ? '#444444' 2039 + : colorScheme === 'dark' ? CoColors.deepBlack : CoColors.pureWhite 2040 + } 2041 + /> 1900 2042 )} 1901 2043 </TouchableOpacity> 1902 - </View> 2044 + </Animated.View> 1903 2045 </View> 2046 + </View> 2047 + </> 2048 + ) : ( 2049 + /* Knowledge View */ 2050 + <View style={styles.memoryViewContainer}> 2051 + {/* Knowledge Tabs */} 2052 + <View style={[styles.knowledgeTabs, { backgroundColor: theme.colors.background.secondary, borderBottomColor: theme.colors.border.primary }]}> 2053 + <TouchableOpacity 2054 + style={[ 2055 + styles.knowledgeTab, 2056 + knowledgeTab === 'core' && { borderBottomColor: theme.colors.text.primary, borderBottomWidth: 2 } 2057 + ]} 2058 + onPress={() => setKnowledgeTab('core')} 2059 + > 2060 + <Text style={[ 2061 + styles.knowledgeTabText, 2062 + { color: knowledgeTab === 'core' ? theme.colors.text.primary : theme.colors.text.tertiary } 2063 + ]}>Core Memory</Text> 2064 + </TouchableOpacity> 2065 + <TouchableOpacity 2066 + style={[ 2067 + styles.knowledgeTab, 2068 + knowledgeTab === 'archival' && { borderBottomColor: theme.colors.text.primary, borderBottomWidth: 2 } 2069 + ]} 2070 + onPress={() => setKnowledgeTab('archival')} 2071 + > 2072 + <Text style={[ 2073 + styles.knowledgeTabText, 2074 + { color: knowledgeTab === 'archival' ? theme.colors.text.primary : theme.colors.text.tertiary } 2075 + ]}>Archival Memory</Text> 2076 + </TouchableOpacity> 2077 + <TouchableOpacity 2078 + style={[ 2079 + styles.knowledgeTab, 2080 + knowledgeTab === 'files' && { borderBottomColor: theme.colors.text.primary, borderBottomWidth: 2 } 2081 + ]} 2082 + onPress={() => setKnowledgeTab('files')} 2083 + > 2084 + <Text style={[ 2085 + styles.knowledgeTabText, 2086 + { color: knowledgeTab === 'files' ? theme.colors.text.primary : theme.colors.text.tertiary } 2087 + ]}>Files</Text> 2088 + </TouchableOpacity> 1904 2089 </View> 1905 2090 1906 - {/* Memory block viewer - right pane on desktop */} 1907 - {isDesktop && selectedBlock && ( 1908 - <MemoryBlockViewer 1909 - block={selectedBlock} 1910 - onClose={() => setSelectedBlock(null)} 1911 - isDark={colorScheme === 'dark'} 1912 - isDesktop={isDesktop} 2091 + {/* Search bar */} 2092 + <View style={styles.memorySearchContainer}> 2093 + <Ionicons name="search" size={20} color={theme.colors.text.tertiary} style={styles.memorySearchIcon} /> 2094 + <TextInput 2095 + style={[styles.memorySearchInput, { 2096 + color: theme.colors.text.primary, 2097 + backgroundColor: theme.colors.background.tertiary, 2098 + borderColor: theme.colors.border.primary, 2099 + }]} 2100 + placeholder="Search memory blocks..." 2101 + placeholderTextColor={theme.colors.text.tertiary} 2102 + value={memorySearchQuery} 2103 + onChangeText={setMemorySearchQuery} 1913 2104 /> 1914 - )} 2105 + </View> 2106 + 2107 + {/* Knowledge blocks grid */} 2108 + <View style={styles.memoryBlocksGrid}> 2109 + {knowledgeTab === 'files' ? ( 2110 + /* Files view */ 2111 + <> 2112 + <View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingHorizontal: 8, paddingVertical: 12 }}> 2113 + <Text style={[styles.memorySectionTitle, { color: theme.colors.text.secondary, marginBottom: 0 }]}>Uploaded Files</Text> 2114 + <TouchableOpacity 2115 + onPress={pickAndUploadFile} 2116 + disabled={isUploadingFile} 2117 + style={{ padding: 4 }} 2118 + > 2119 + {isUploadingFile ? ( 2120 + <ActivityIndicator size="small" color={theme.colors.text.secondary} /> 2121 + ) : ( 2122 + <Ionicons name="add-circle-outline" size={24} color={theme.colors.text.primary} /> 2123 + )} 2124 + </TouchableOpacity> 2125 + </View> 2126 + {uploadProgress && ( 2127 + <View style={{ marginHorizontal: 8, marginBottom: 12, paddingVertical: 8, paddingHorizontal: 12, backgroundColor: theme.colors.background.tertiary, borderRadius: 8 }}> 2128 + <Text style={{ color: theme.colors.text.secondary, fontSize: 14 }}>{uploadProgress}</Text> 2129 + </View> 2130 + )} 2131 + {isLoadingFiles ? ( 2132 + <View style={styles.memoryLoadingContainer}> 2133 + <ActivityIndicator size="large" color={theme.colors.text.secondary} /> 2134 + </View> 2135 + ) : filesError ? ( 2136 + <Text style={[styles.errorText, { textAlign: 'center', marginTop: 40 }]}>{filesError}</Text> 2137 + ) : folderFiles.length === 0 ? ( 2138 + <View style={styles.memoryEmptyState}> 2139 + <Ionicons name="folder-outline" size={64} color={theme.colors.text.tertiary} style={{ opacity: 0.3 }} /> 2140 + <Text style={[styles.memoryEmptyText, { color: theme.colors.text.tertiary }]}>No files uploaded yet</Text> 2141 + </View> 2142 + ) : ( 2143 + <FlatList 2144 + data={folderFiles} 2145 + keyExtractor={(item) => item.id} 2146 + contentContainerStyle={styles.memoryBlocksContent} 2147 + renderItem={({ item }) => ( 2148 + <View 2149 + style={[styles.memoryBlockCard, { 2150 + backgroundColor: theme.colors.background.secondary, 2151 + borderColor: theme.colors.border.primary, 2152 + flexDirection: 'row', 2153 + justifyContent: 'space-between', 2154 + alignItems: 'center', 2155 + minHeight: 'auto' 2156 + }]} 2157 + > 2158 + <View style={{ flex: 1 }}> 2159 + <Text style={[styles.memoryBlockCardLabel, { color: theme.colors.text.primary }]} numberOfLines={1}> 2160 + {item.fileName || item.name || 'Untitled'} 2161 + </Text> 2162 + <Text style={[styles.memoryBlockCardPreview, { color: theme.colors.text.secondary, fontSize: 12 }]}> 2163 + {new Date(item.createdAt || item.created_at).toLocaleDateString()} 2164 + </Text> 2165 + </View> 2166 + <TouchableOpacity 2167 + onPress={() => deleteFile(item.id, item.fileName || item.name)} 2168 + style={{ padding: 8 }} 2169 + > 2170 + <Ionicons name="trash-outline" size={20} color={theme.colors.status.error} /> 2171 + </TouchableOpacity> 2172 + </View> 2173 + )} 2174 + /> 2175 + )} 2176 + </> 2177 + ) : isLoadingBlocks ? ( 2178 + <View style={styles.memoryLoadingContainer}> 2179 + <ActivityIndicator size="large" color={theme.colors.text.secondary} /> 2180 + </View> 2181 + ) : blocksError ? ( 2182 + <Text style={[styles.errorText, { textAlign: 'center', marginTop: 40 }]}>{blocksError}</Text> 2183 + ) : ( 2184 + <FlatList 2185 + data={memoryBlocks.filter(block => { 2186 + // Filter by tab 2187 + if (knowledgeTab === 'archival') { 2188 + // Archival memory: placeholder - show nothing for now 2189 + return false; 2190 + } else if (knowledgeTab === 'files') { 2191 + // Files: show nothing - files are separate from blocks 2192 + return false; 2193 + } 2194 + // Core memory: show all blocks 2195 + 2196 + // Filter by search query 2197 + if (memorySearchQuery) { 2198 + return block.label.toLowerCase().includes(memorySearchQuery.toLowerCase()) || 2199 + block.value.toLowerCase().includes(memorySearchQuery.toLowerCase()); 2200 + } 2201 + 2202 + return true; 2203 + })} 2204 + numColumns={isDesktop ? 2 : 1} 2205 + key={isDesktop ? 'desktop' : 'mobile'} 2206 + keyExtractor={(item) => item.id || item.label} 2207 + contentContainerStyle={styles.memoryBlocksContent} 2208 + renderItem={({ item }) => ( 2209 + <TouchableOpacity 2210 + style={[styles.memoryBlockCard, { 2211 + backgroundColor: theme.colors.background.secondary, 2212 + borderColor: theme.colors.border.primary 2213 + }]} 2214 + onPress={() => setSelectedBlock(item)} 2215 + > 2216 + <View style={styles.memoryBlockCardHeader}> 2217 + <Text style={[styles.memoryBlockCardLabel, { color: theme.colors.text.primary }]}> 2218 + {item.label} 2219 + </Text> 2220 + <Text style={[styles.memoryBlockCardCount, { color: theme.colors.text.tertiary }]}> 2221 + {item.value.length} chars 2222 + </Text> 2223 + </View> 2224 + <Text 2225 + style={[styles.memoryBlockCardPreview, { color: theme.colors.text.secondary }]} 2226 + numberOfLines={4} 2227 + > 2228 + {item.value || 'Empty'} 2229 + </Text> 2230 + </TouchableOpacity> 2231 + )} 2232 + ListEmptyComponent={ 2233 + <View style={styles.memoryEmptyState}> 2234 + <Ionicons name="library-outline" size={64} color={theme.colors.text.tertiary} style={{ opacity: 0.3 }} /> 2235 + <Text style={[styles.memoryEmptyText, { color: theme.colors.text.secondary }]}> 2236 + {memorySearchQuery ? 'No memory blocks found' : 'No memory blocks yet'} 2237 + </Text> 2238 + </View> 2239 + } 2240 + /> 2241 + )} 2242 + </View> 1915 2243 </View> 1916 - </View> 2244 + )} 1917 2245 1918 - {/* Memory block viewer - overlay on mobile */} 2246 + {/* Knowledge block viewer - right pane on desktop */} 2247 + {isDesktop && selectedBlock && ( 2248 + <MemoryBlockViewer 2249 + block={selectedBlock} 2250 + onClose={() => setSelectedBlock(null)} 2251 + isDark={colorScheme === 'dark'} 2252 + isDesktop={isDesktop} 2253 + /> 2254 + )} 2255 + </KeyboardAvoidingView> 2256 + </View> 2257 + 2258 + {/* Knowledge block viewer - overlay on mobile */} 1919 2259 {!isDesktop && selectedBlock && ( 1920 2260 <MemoryBlockViewer 1921 2261 block={selectedBlock} ··· 1981 2321 </View> 1982 2322 </Modal> 1983 2323 1984 - <StatusBar style="auto" /> 2324 + <StatusBar style={colorScheme === 'dark' ? 'light' : 'dark'} /> 1985 2325 </View> 1986 2326 ); 1987 2327 } ··· 2030 2370 borderBottomWidth: 1, 2031 2371 borderBottomColor: darkTheme.colors.border.primary, 2032 2372 }, 2373 + viewSwitcher: { 2374 + flexDirection: 'row', 2375 + paddingHorizontal: 16, 2376 + paddingVertical: 8, 2377 + gap: 8, 2378 + borderBottomWidth: 1, 2379 + }, 2380 + viewSwitcherButton: { 2381 + paddingVertical: 8, 2382 + paddingHorizontal: 16, 2383 + borderRadius: 8, 2384 + }, 2385 + viewSwitcherText: { 2386 + fontSize: 14, 2387 + fontFamily: 'Lexend_500Medium', 2388 + }, 2033 2389 menuButton: { 2034 2390 padding: 8, 2035 2391 }, ··· 2058 2414 flex: 1, 2059 2415 }, 2060 2416 messagesList: { 2061 - maxWidth: 800, 2417 + maxWidth: 700, 2062 2418 width: '100%', 2063 2419 alignSelf: 'center', 2064 2420 paddingBottom: 100, // Space for input at bottom 2065 2421 }, 2066 2422 messageContainer: { 2067 2423 paddingHorizontal: 18, 2068 - paddingVertical: 8, 2424 + paddingVertical: 12, 2069 2425 }, 2070 2426 userMessageContainer: { 2071 2427 alignItems: 'flex-end', ··· 2075 2431 }, 2076 2432 assistantFullWidthContainer: { 2077 2433 paddingHorizontal: 18, 2078 - paddingVertical: 12, 2434 + paddingVertical: 16, 2079 2435 width: '100%', 2080 2436 }, 2081 2437 messageBubble: { ··· 2119 2475 flexDirection: 'row', 2120 2476 alignItems: 'center', 2121 2477 paddingVertical: 8, 2122 - marginBottom: 12, 2478 + paddingHorizontal: 12, 2479 + marginBottom: 8, 2480 + backgroundColor: 'rgba(255, 255, 255, 0.03)', 2481 + borderRadius: 8, 2123 2482 }, 2124 2483 reasoningToggleText: { 2125 - fontSize: 16, 2126 - fontFamily: 'Lexend_400Regular', 2127 - color: darkTheme.colors.text.tertiary, 2128 - marginRight: 4, 2484 + fontSize: 14, 2485 + fontFamily: 'Lexend_500Medium', 2486 + color: darkTheme.colors.text.secondary, 2129 2487 }, 2130 2488 reasoningExpandedContainer: { 2131 - paddingVertical: 8, 2132 - paddingHorizontal: 12, 2489 + paddingVertical: 12, 2490 + paddingHorizontal: 16, 2491 + paddingLeft: 20, 2133 2492 marginBottom: 12, 2134 - backgroundColor: 'rgba(255, 255, 255, 0.02)', 2135 - borderRadius: 6, 2136 - borderLeftWidth: 2, 2137 - borderLeftColor: darkTheme.colors.text.tertiary, 2493 + backgroundColor: 'rgba(255, 255, 255, 0.04)', 2494 + borderRadius: 8, 2495 + borderLeftWidth: 4, 2496 + borderLeftColor: '#555555', 2497 + overflow: 'hidden', 2138 2498 }, 2139 2499 reasoningExpandedText: { 2140 - fontSize: 13, 2500 + fontSize: 14, 2141 2501 fontFamily: 'Lexend_400Regular', 2142 - color: darkTheme.colors.text.tertiary, 2143 - lineHeight: 18, 2144 - fontStyle: 'italic', 2502 + color: darkTheme.colors.text.secondary, 2503 + lineHeight: 22, 2504 + fontStyle: 'normal', 2145 2505 }, 2146 2506 reasoningContainer: { 2147 2507 marginTop: 8, ··· 2218 2578 }, 2219 2579 inputCentered: { 2220 2580 position: 'relative', 2221 - maxWidth: 800, 2581 + maxWidth: 700, 2222 2582 width: '100%', 2223 - }, 2224 - inputBackdrop: { 2225 - position: 'absolute', 2226 - top: 0, 2227 - left: 0, 2228 - right: 0, 2229 - bottom: 0, 2230 - borderRadius: 24, 2231 - zIndex: -1, 2232 2583 }, 2233 2584 inputWrapper: { 2234 2585 position: 'relative', ··· 2238 2589 fileButton: { 2239 2590 position: 'absolute', 2240 2591 right: 88, 2241 - bottom: 6, 2242 - width: 36, 2243 - height: 36, 2244 - borderRadius: 18, 2592 + bottom: 8, 2593 + width: 32, 2594 + height: 32, 2595 + borderRadius: 16, 2245 2596 justifyContent: 'center', 2246 2597 alignItems: 'center', 2247 2598 zIndex: 1, ··· 2249 2600 imageButton: { 2250 2601 position: 'absolute', 2251 2602 right: 52, 2252 - bottom: 6, 2253 - width: 36, 2254 - height: 36, 2255 - borderRadius: 18, 2603 + bottom: 8, 2604 + width: 32, 2605 + height: 32, 2606 + borderRadius: 16, 2256 2607 justifyContent: 'center', 2257 2608 alignItems: 'center', 2258 2609 zIndex: 1, ··· 2284 2635 sendButton: { 2285 2636 position: 'absolute', 2286 2637 right: 10, 2287 - bottom: 6, 2288 - width: 36, 2289 - height: 36, 2290 - borderRadius: 18, 2638 + bottom: 8, 2639 + width: 32, 2640 + height: 32, 2641 + borderRadius: 16, 2291 2642 justifyContent: 'center', 2292 2643 alignItems: 'center', 2293 - }, 2294 - sendRing: { 2295 - width: 20, 2296 - height: 20, 2297 - borderRadius: 10, 2298 - borderWidth: 2, 2299 - borderColor: darkTheme.colors.background.primary, 2300 - }, 2301 - sendButtonDisabled: { 2302 - opacity: 0.5, 2644 + transition: 'all 0.2s ease', 2303 2645 }, 2304 2646 sidebarContainer: { 2305 2647 height: '100%', ··· 2503 2845 fontFamily: 'Lexend_400Regular', 2504 2846 color: darkTheme.colors.text.secondary, 2505 2847 lineHeight: 18, 2848 + }, 2849 + // Memory View Styles 2850 + memoryViewContainer: { 2851 + flex: 1, 2852 + backgroundColor: darkTheme.colors.background.primary, 2853 + }, 2854 + knowledgeTabs: { 2855 + flexDirection: 'row', 2856 + paddingHorizontal: 16, 2857 + borderBottomWidth: 1, 2858 + }, 2859 + knowledgeTab: { 2860 + paddingVertical: 12, 2861 + paddingHorizontal: 16, 2862 + marginHorizontal: 4, 2863 + }, 2864 + knowledgeTabText: { 2865 + fontSize: 14, 2866 + fontFamily: 'Lexend_500Medium', 2867 + }, 2868 + memoryViewHeader: { 2869 + paddingHorizontal: 24, 2870 + paddingVertical: 20, 2871 + borderBottomWidth: 1, 2872 + borderBottomColor: darkTheme.colors.border.primary, 2873 + }, 2874 + backToChat: { 2875 + flexDirection: 'row', 2876 + alignItems: 'center', 2877 + marginBottom: 12, 2878 + }, 2879 + backToChatText: { 2880 + fontSize: 14, 2881 + fontFamily: 'Lexend_400Regular', 2882 + marginLeft: 8, 2883 + }, 2884 + memoryViewTitle: { 2885 + fontSize: 32, 2886 + fontFamily: 'Lexend_700Bold', 2887 + }, 2888 + memorySearchContainer: { 2889 + paddingHorizontal: 24, 2890 + paddingVertical: 16, 2891 + flexDirection: 'row', 2892 + alignItems: 'center', 2893 + }, 2894 + memorySearchIcon: { 2895 + position: 'absolute', 2896 + left: 36, 2897 + zIndex: 1, 2898 + }, 2899 + memorySearchInput: { 2900 + flex: 1, 2901 + height: 44, 2902 + paddingLeft: 40, 2903 + paddingRight: 16, 2904 + borderRadius: 22, 2905 + fontSize: 16, 2906 + fontFamily: 'Lexend_400Regular', 2907 + borderWidth: 1, 2908 + }, 2909 + memoryBlocksGrid: { 2910 + flex: 1, 2911 + paddingHorizontal: 16, 2912 + }, 2913 + memoryBlocksContent: { 2914 + paddingBottom: 24, 2915 + }, 2916 + memoryLoadingContainer: { 2917 + flex: 1, 2918 + justifyContent: 'center', 2919 + alignItems: 'center', 2920 + paddingTop: 60, 2921 + }, 2922 + memoryBlockCard: { 2923 + flex: 1, 2924 + margin: 8, 2925 + padding: 20, 2926 + borderRadius: 12, 2927 + borderWidth: 1, 2928 + minHeight: 160, 2929 + maxWidth: '100%', 2930 + }, 2931 + memoryBlockCardHeader: { 2932 + flexDirection: 'row', 2933 + justifyContent: 'space-between', 2934 + alignItems: 'flex-start', 2935 + marginBottom: 12, 2936 + }, 2937 + memoryBlockCardLabel: { 2938 + fontSize: 18, 2939 + fontFamily: 'Lexend_600SemiBold', 2940 + flex: 1, 2941 + }, 2942 + memoryBlockCardCount: { 2943 + fontSize: 12, 2944 + fontFamily: 'Lexend_400Regular', 2945 + marginLeft: 8, 2946 + }, 2947 + memoryBlockCardPreview: { 2948 + fontSize: 14, 2949 + fontFamily: 'Lexend_400Regular', 2950 + lineHeight: 20, 2951 + }, 2952 + memoryEmptyState: { 2953 + flex: 1, 2954 + justifyContent: 'center', 2955 + alignItems: 'center', 2956 + paddingTop: 80, 2957 + }, 2958 + memoryEmptyText: { 2959 + fontSize: 16, 2960 + fontFamily: 'Lexend_400Regular', 2961 + marginTop: 16, 2962 + }, 2963 + assistantMessageWithCopyContainer: { 2964 + position: 'relative', 2965 + flex: 1, 2966 + }, 2967 + copyButtonContainer: { 2968 + flexDirection: 'row', 2969 + justifyContent: 'flex-end', 2970 + paddingTop: 4, 2971 + }, 2972 + copyButton: { 2973 + padding: 8, 2974 + opacity: 0.3, 2975 + borderRadius: 4, 2976 + }, 2977 + messageSeparator: { 2978 + height: 1, 2979 + backgroundColor: darkTheme.colors.border.primary, 2980 + marginTop: 16, 2981 + opacity: 0.8, 2506 2982 }, 2507 2983 });
+16
src/api/lettaApi.ts
··· 1065 1065 } 1066 1066 } 1067 1067 1068 + async closeAllFiles(agentId: string): Promise<string[]> { 1069 + try { 1070 + if (!this.client) { 1071 + throw new Error('Client not initialized. Please set auth token first.'); 1072 + } 1073 + 1074 + console.log('closeAllFiles - agentId:', agentId); 1075 + const result = await this.client.agents.files.closeAll(agentId); 1076 + console.log('closeAllFiles - result:', result); 1077 + return result; 1078 + } catch (error) { 1079 + console.error('closeAllFiles - error:', error); 1080 + throw this.handleError(error); 1081 + } 1082 + } 1083 + 1068 1084 private handleError(error: any): ApiError { 1069 1085 console.error('=== HANDLE ERROR ==='); 1070 1086 console.error('handleError - Full error object:', error);
+2 -2
src/components/MemoryBlockViewer.tsx
··· 84 84 <View style={styles.headerLeft}> 85 85 <Ionicons name="cube-outline" size={20} color={theme.colors.text.tertiary} /> 86 86 <Text style={[styles.headerLabel, { color: theme.colors.text.tertiary }]}> 87 - MEMORY 87 + KNOWLEDGE 88 88 </Text> 89 89 </View> 90 90 <TouchableOpacity onPress={onClose} style={styles.closeButton}> ··· 148 148 <View style={styles.headerLeft}> 149 149 <Ionicons name="cube-outline" size={20} color={theme.colors.text.tertiary} /> 150 150 <Text style={[styles.headerLabel, { color: theme.colors.text.tertiary }]}> 151 - MEMORY 151 + KNOWLEDGE 152 152 </Text> 153 153 </View> 154 154 <TouchableOpacity onPress={onClose} style={styles.closeButton}>