A React Native app for the ultimate thinking partner.

Implement dark theme UI redesign with responsive layout

- Add comprehensive dark theme system with modern glass-morphism design
- Create responsive sidebar component for desktop and mobile layouts
- Enhance message display with reasoning bubbles and markdown support
- Add @ronradtke/react-native-markdown-display for rich text rendering
- Improve chat interface with floating input design and better spacing
- Implement agent/project persistence across sessions
- Update API integration for better message handling and filtering

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

+1879 -395
+599 -328
App.tsx
··· 10 10 SafeAreaView, 11 11 ActivityIndicator, 12 12 Modal, 13 - Linking 13 + Linking, 14 + Dimensions 14 15 } from 'react-native'; 15 16 import { StatusBar } from 'expo-status-bar'; 16 17 import lettaApi from './src/api/lettaApi'; ··· 18 19 import CreateAgentScreen from './CreateAgentScreen'; 19 20 import AgentSelectorScreen from './AgentSelectorScreen'; 20 21 import ProjectSelectorModal from './ProjectSelectorModal'; 22 + import Sidebar from './src/components/Sidebar'; 23 + import MessageContent from './src/components/MessageContent'; 24 + import { darkTheme } from './src/theme'; 21 25 import type { LettaAgent, LettaMessage, StreamingChunk, Project } from './src/types/letta'; 22 26 23 27 export default function App() { ··· 49 53 const [isStreaming, setIsStreaming] = useState(false); 50 54 const [streamingStep, setStreamingStep] = useState<string>(''); 51 55 56 + // Layout state for responsive design 57 + const [screenData, setScreenData] = useState(Dimensions.get('window')); 58 + const [sidebarVisible, setSidebarVisible] = useState(false); 59 + 60 + const isDesktop = screenData.width >= 768; 61 + 52 62 // Load saved token on app startup 53 63 useEffect(() => { 54 64 const loadSavedToken = async () => { ··· 63 73 if (isValid) { 64 74 setApiToken(savedToken); 65 75 setIsConnected(true); 66 - await loadSavedProject(); 76 + const project = await loadSavedProject(); 77 + if (project) { 78 + await loadSavedAgent(project); 79 + } 67 80 console.log('Auto-login successful'); 68 81 } else { 69 82 console.log('Saved token is invalid, clearing it'); ··· 98 111 return null; 99 112 }; 100 113 114 + const loadSavedAgent = async (project: Project) => { 115 + try { 116 + const savedAgentId = await Storage.getItem(STORAGE_KEYS.AGENT_ID); 117 + if (savedAgentId && project) { 118 + const agentList = await lettaApi.listAgentsForProject(project.id, { 119 + limit: 50, 120 + }); 121 + const foundAgent = agentList.find(a => a.id === savedAgentId); 122 + if (foundAgent) { 123 + setCurrentAgent(foundAgent); 124 + setShowAgentSelector(false); 125 + setShowChatView(true); 126 + await loadMessagesForAgent(foundAgent.id); 127 + console.log('Restored saved agent:', foundAgent.name); 128 + return foundAgent; 129 + } else { 130 + console.log('Saved agent not found, clearing it'); 131 + await Storage.removeItem(STORAGE_KEYS.AGENT_ID); 132 + } 133 + } 134 + } catch (error) { 135 + console.error('Error loading saved agent:', error); 136 + } 137 + return null; 138 + }; 139 + 101 140 102 141 loadSavedToken(); 142 + }, []); 143 + 144 + // Listen for orientation/screen size changes 145 + useEffect(() => { 146 + const onChange = (result: any) => { 147 + setScreenData(result.window); 148 + }; 149 + 150 + const subscription = Dimensions.addEventListener('change', onChange); 151 + return () => subscription?.remove(); 103 152 }, []); 104 153 105 154 const handleConnect = async () => { ··· 167 216 try { 168 217 const messageHistory = await lettaApi.listMessages(agentId, { limit: 50 }); 169 218 console.log('Loaded messages for agent:', messageHistory); 170 - 219 + 171 220 // Filter and transform messages for display 172 221 const displayMessages = messageHistory 173 - .filter(msg => msg.message_type === 'user_message' || msg.message_type === 'assistant_message') 222 + .filter(msg => { 223 + // Filter by message type 224 + if (msg.message_type !== 'user_message' && msg.message_type !== 'assistant_message') { 225 + return false; 226 + } 227 + 228 + // Filter out heartbeat messages from user messages 229 + if (msg.message_type === 'user_message' && typeof msg.content === 'string') { 230 + try { 231 + const parsed = JSON.parse(msg.content); 232 + if (parsed?.type === 'heartbeat') { 233 + return false; // Hide heartbeat messages 234 + } 235 + } catch { 236 + // Keep message if content is not valid JSON 237 + } 238 + } 239 + 240 + return true; 241 + }) 174 242 .map(msg => ({ 175 243 id: msg.id, 176 - role: msg.message_type === 'user_message' ? 'user' : 'assistant', 244 + role: msg.role as 'user' | 'assistant', 177 245 content: msg.content || '', 178 - created_at: msg.date, 246 + created_at: msg.created_at, 247 + reasoning: msg.reasoning, // Preserve reasoning field from API 179 248 })); 180 - 249 + 181 250 setMessages(displayMessages); 182 251 } catch (error: any) { 183 252 console.error('Failed to load messages:', error); ··· 210 279 // Local accumulator to preserve content through callback closures 211 280 let accumulatedMessage = ''; 212 281 let accumulatedStep = ''; 282 + let accumulatedReasoning: string[] = []; 213 283 214 284 try { 215 285 await lettaApi.sendMessageStream( ··· 221 291 (chunk) => { 222 292 console.log('Stream chunk:', chunk); 223 293 console.log('Chunk keys:', Object.keys(chunk)); 224 - 294 + 225 295 if (chunk.message_type === 'assistant_message' && chunk.content) { 226 296 // Append new content to streaming message 227 297 console.log('Adding assistant message content:', chunk.content); ··· 231 301 // Show reasoning/thinking process 232 302 accumulatedStep = `Thinking: ${chunk.reasoning}`; 233 303 setStreamingStep(accumulatedStep); 304 + // Store reasoning for later inclusion in message 305 + accumulatedReasoning.push(chunk.reasoning); 234 306 } else if (chunk.message_type === 'tool_call') { 235 307 // Show tool execution 236 308 accumulatedStep = `Executing tool: ${chunk.tool_call?.function?.name || 'Unknown'}`; ··· 244 316 // onComplete callback 245 317 (response) => { 246 318 console.log('Stream complete:', response); 247 - 319 + 248 320 // Add the completed assistant message to permanent messages if we have content 249 321 if (accumulatedMessage.trim()) { 250 322 const assistantMessage: LettaMessage = { ··· 252 324 role: 'assistant', 253 325 content: accumulatedMessage.trim(), 254 326 created_at: new Date().toISOString(), 327 + reasoning: accumulatedReasoning.length > 0 ? accumulatedReasoning.join(' ') : undefined, 255 328 }; 256 - 329 + 257 330 setMessages(prev => [...prev, assistantMessage]); 258 331 console.log('Added completed assistant message to chat history'); 259 332 } 260 - 333 + 261 334 // Clear streaming state 262 335 setIsStreaming(false); 263 336 setStreamingMessage(''); ··· 417 490 // Show chat view when agent is selected 418 491 return ( 419 492 <SafeAreaView style={styles.container}> 420 - <View style={styles.header}> 421 - <TouchableOpacity 422 - style={styles.agentSelector} 423 - onPress={handleBackToAgentSelector} 424 - > 425 - <Text style={styles.backButton}>← Back</Text> 426 - <Text style={styles.headerTitle}> 427 - {currentAgent ? currentAgent.name : 'Chat'} 428 - </Text> 429 - </TouchableOpacity> 430 - 431 - <View style={styles.headerButtons}> 432 - <TouchableOpacity onPress={() => setShowCreateAgentScreen(true)}> 433 - <Text style={styles.createAgentButton}>+</Text> 434 - </TouchableOpacity> 435 - <TouchableOpacity onPress={handleLogout}> 436 - <Text style={styles.disconnectButton}>Logout</Text> 437 - </TouchableOpacity> 438 - </View> 439 - </View> 440 - 441 - {isLoadingMessages ? ( 442 - <View style={styles.loadingContainer}> 443 - <ActivityIndicator size="large" color="#007AFF" /> 444 - <Text style={styles.loadingText}>Loading messages...</Text> 445 - </View> 446 - ) : ( 447 - <ScrollView style={styles.messagesContainer}> 448 - {messages.length === 0 && currentAgent && ( 449 - <View style={styles.emptyContainer}> 450 - <Text style={styles.emptyText}> 451 - Start a conversation with {currentAgent.name} 452 - </Text> 453 - </View> 454 - )} 455 - 456 - {messages.filter(message => message.role !== 'system').map((message, index) => ( 457 - <View 458 - key={`${message.id || 'msg'}-${index}-${message.created_at}`} 459 - style={[ 460 - styles.messageBubble, 461 - message.role === 'user' ? styles.userMessage : styles.agentMessage, 462 - ]} 463 - > 464 - <Text style={[ 465 - styles.messageText, 466 - message.role === 'user' ? styles.userText : styles.agentText, 467 - ]}> 468 - {message.content} 493 + <View style={[styles.mainLayout, isDesktop && styles.desktopLayout]}> 494 + {/* Sidebar for desktop */} 495 + {isDesktop && ( 496 + <Sidebar 497 + currentProject={currentProject} 498 + currentAgent={currentAgent} 499 + onAgentSelect={handleAgentSelect} 500 + onProjectPress={() => setShowProjectSelector(true)} 501 + onCreateAgent={() => setShowCreateAgentScreen(true)} 502 + onLogout={handleLogout} 503 + isVisible={true} 504 + /> 505 + )} 506 + 507 + {/* Mobile sidebar modal */} 508 + {!isDesktop && ( 509 + <Modal 510 + visible={sidebarVisible} 511 + animationType="slide" 512 + presentationStyle="overCurrentContext" 513 + onRequestClose={() => setSidebarVisible(false)} 514 + > 515 + <SafeAreaView style={styles.mobileModal}> 516 + <View style={styles.modalHeader}> 517 + <TouchableOpacity onPress={() => setSidebarVisible(false)}> 518 + <Text style={styles.modalCloseText}>✕</Text> 519 + </TouchableOpacity> 520 + </View> 521 + <Sidebar 522 + currentProject={currentProject} 523 + currentAgent={currentAgent} 524 + onAgentSelect={(agent) => { 525 + handleAgentSelect(agent); 526 + setSidebarVisible(false); 527 + }} 528 + onProjectPress={() => { 529 + setSidebarVisible(false); 530 + setShowProjectSelector(true); 531 + }} 532 + onCreateAgent={() => { 533 + setSidebarVisible(false); 534 + setShowCreateAgentScreen(true); 535 + }} 536 + onLogout={handleLogout} 537 + isVisible={true} 538 + /> 539 + </SafeAreaView> 540 + </Modal> 541 + )} 542 + 543 + {/* Chat Area */} 544 + <View style={styles.chatArea}> 545 + {/* Header */} 546 + <View style={styles.chatHeader}> 547 + {!isDesktop && ( 548 + <TouchableOpacity 549 + style={styles.menuButton} 550 + onPress={() => setSidebarVisible(true)} 551 + > 552 + <Text style={styles.menuIcon}>☰</Text> 553 + </TouchableOpacity> 554 + )} 555 + <View style={styles.headerContent}> 556 + <Text style={styles.agentTitle}> 557 + {currentAgent ? currentAgent.name : 'Select an agent'} 469 558 </Text> 470 - </View> 471 - ))} 472 - 473 - {/* Streaming message display */} 474 - {isStreaming && ( 475 - <View style={[styles.messageBubble, styles.agentMessage, styles.streamingMessage]}> 476 - {streamingStep && streamingStep.trim() && streamingStep.trim().length > 0 && ( 477 - <Text style={styles.streamingStep}>{streamingStep.trim()}</Text> 478 - )} 479 - {streamingMessage && String(streamingMessage).trim() && String(streamingMessage).trim().length > 0 && ( 480 - <Text style={[styles.messageText, styles.agentText]}> 481 - {String(streamingMessage).trim()} 482 - <Text style={styles.cursor}>|</Text> 559 + {currentAgent && ( 560 + <Text style={styles.agentSubtitle}> 561 + {currentProject?.name} 483 562 </Text> 484 563 )} 485 - {(!streamingMessage?.trim() && !streamingStep?.trim()) && ( 486 - <View style={styles.thinkingIndicator}> 487 - <ActivityIndicator size="small" color="#666" /> 488 - <Text style={styles.thinkingText}>Agent is thinking...</Text> 489 - </View> 490 - )} 491 564 </View> 492 - )} 493 - 494 - {isSendingMessage && !isStreaming && ( 495 - <View style={[styles.messageBubble, styles.agentMessage]}> 496 - <ActivityIndicator size="small" color="#666" /> 565 + {!isDesktop && ( 566 + <TouchableOpacity onPress={() => setShowCreateAgentScreen(true)}> 567 + <Text style={styles.headerAction}>+</Text> 568 + </TouchableOpacity> 569 + )} 570 + </View> 571 + 572 + {/* Messages */} 573 + {isLoadingMessages ? ( 574 + <View style={styles.loadingContainer}> 575 + <ActivityIndicator size="large" color="#007AFF" /> 576 + <Text style={styles.loadingText}>Loading messages...</Text> 497 577 </View> 498 - )} 499 - </ScrollView> 500 - )} 501 - 502 - <View style={styles.inputContainer}> 503 - <TextInput 504 - style={styles.messageInput} 505 - placeholder="Type a message..." 506 - value={inputText} 507 - onChangeText={setInputText} 508 - multiline 509 - /> 510 - <TouchableOpacity 511 - style={[ 512 - styles.sendButton, 513 - (!inputText.trim() || !currentAgent || isSendingMessage) && styles.sendButtonDisabled 514 - ]} 515 - onPress={handleSendMessage} 516 - disabled={!inputText.trim() || !currentAgent || isSendingMessage} 517 - > 518 - {isSendingMessage ? ( 519 - <ActivityIndicator size="small" color="#fff" /> 520 578 ) : ( 521 - <Text style={styles.sendButtonText}>Send</Text> 522 - )} 523 - </TouchableOpacity> 524 - </View> 525 - 526 - {/* Agent Selector Modal */} 527 - <Modal 528 - visible={showAgentSelector} 529 - animationType="slide" 530 - transparent={true} 531 - onRequestClose={() => setShowAgentSelector(false)} 532 - > 533 - <View style={styles.modalOverlay}> 534 - <View style={styles.modalContent}> 535 - <Text style={styles.modalTitle}>Select Agent</Text> 536 - <ScrollView style={styles.agentList}> 537 - {agents.map((agent) => ( 538 - <TouchableOpacity 539 - key={agent.id} 540 - style={[ 541 - styles.agentItem, 542 - currentAgent?.id === agent.id && styles.selectedAgentItem 543 - ]} 544 - onPress={() => handleAgentSelect(agent)} 545 - > 546 - <Text style={styles.agentName}>{agent.name}</Text> 547 - <Text style={styles.agentDescription}> 548 - Created {new Date(agent.created_at).toLocaleDateString()} 549 - </Text> 550 - </TouchableOpacity> 551 - ))} 579 + <ScrollView style={styles.messagesContainer}> 580 + <View style={styles.messagesList}> 581 + {messages.length === 0 && currentAgent && ( 582 + <View style={styles.emptyContainer}> 583 + <Text style={styles.emptyText}> 584 + Start a conversation with {currentAgent.name} 585 + </Text> 586 + </View> 587 + )} 588 + 589 + {messages.filter(message => message.role !== 'system').map((message, index) => ( 590 + <View key={`${message.id || 'msg'}-${index}-${message.created_at}`} style={styles.messageGroup}> 591 + {/* Show reasoning above assistant messages */} 592 + {message.role === 'assistant' && message.reasoning && ( 593 + <View style={styles.reasoningContainer}> 594 + <Text style={styles.reasoningText}>{message.reasoning}</Text> 595 + </View> 596 + )} 597 + <View style={[ 598 + styles.message, 599 + message.role === 'user' ? styles.userMessage : styles.assistantMessage, 600 + ]}> 601 + <MessageContent 602 + content={message.content} 603 + isUser={message.role === 'user'} 604 + /> 605 + </View> 606 + </View> 607 + ))} 608 + 609 + {/* Streaming message display */} 610 + {isStreaming && ( 611 + <View style={styles.messageGroup}> 612 + {streamingStep && streamingStep.trim().length > 0 && ( 613 + <View style={styles.reasoningContainer}> 614 + <Text style={styles.reasoningText}>{streamingStep.trim()}</Text> 615 + </View> 616 + )} 617 + <View style={[styles.message, styles.assistantMessage, styles.streamingMessage]}> 618 + {streamingMessage && String(streamingMessage).trim().length > 0 ? ( 619 + <> 620 + <MessageContent 621 + content={String(streamingMessage).trim()} 622 + isUser={false} 623 + /> 624 + <Text style={styles.cursor}>|</Text> 625 + </> 626 + ) : ( 627 + <View style={styles.thinkingIndicator}> 628 + <ActivityIndicator size="small" color="#666" /> 629 + <Text style={styles.thinkingText}>Agent is thinking...</Text> 630 + </View> 631 + )} 632 + </View> 633 + </View> 634 + )} 635 + 636 + {isSendingMessage && !isStreaming && ( 637 + <View style={styles.messageGroup}> 638 + <View style={[styles.message, styles.assistantMessage]}> 639 + <ActivityIndicator size="small" color="#666" /> 640 + </View> 641 + </View> 642 + )} 643 + </View> 552 644 </ScrollView> 553 - <TouchableOpacity 554 - style={styles.modalCloseButton} 555 - onPress={() => setShowAgentSelector(false)} 556 - > 557 - <Text style={styles.modalCloseText}>Close</Text> 558 - </TouchableOpacity> 645 + )} 646 + 647 + {/* Input */} 648 + <View style={styles.inputContainer}> 649 + <View style={styles.inputWrapper}> 650 + <TextInput 651 + style={styles.messageInput} 652 + placeholder="Type a message..." 653 + value={inputText} 654 + onChangeText={setInputText} 655 + multiline 656 + /> 657 + <TouchableOpacity 658 + style={[ 659 + styles.sendButton, 660 + (!inputText.trim() || !currentAgent || isSendingMessage) && styles.sendButtonDisabled 661 + ]} 662 + onPress={handleSendMessage} 663 + disabled={!inputText.trim() || !currentAgent || isSendingMessage} 664 + > 665 + {isSendingMessage ? ( 666 + <ActivityIndicator size="small" color="#fff" /> 667 + ) : ( 668 + <Text style={styles.sendButtonText}>Send</Text> 669 + )} 670 + </TouchableOpacity> 671 + </View> 559 672 </View> 560 673 </View> 561 - </Modal> 674 + </View> 675 + 676 + {/* Project Selector Modal for Chat View */} 677 + <ProjectSelectorModal 678 + visible={showProjectSelector} 679 + currentProject={currentProject} 680 + onProjectSelect={handleProjectSelect} 681 + onClose={() => setShowProjectSelector(false)} 682 + /> 683 + 562 684 <StatusBar style="auto" /> 563 685 </SafeAreaView> 564 686 ); ··· 567 689 const styles = StyleSheet.create({ 568 690 container: { 569 691 flex: 1, 570 - backgroundColor: '#f8f8f8', 692 + backgroundColor: darkTheme.colors.background.primary, 571 693 }, 694 + // Setup screen styles 572 695 setupContainer: { 573 696 flex: 1, 574 697 justifyContent: 'center', 575 698 alignItems: 'center', 576 - padding: 20, 699 + padding: darkTheme.spacing[3], 700 + backgroundColor: darkTheme.colors.background.primary, 577 701 }, 578 702 title: { 579 - fontSize: 32, 580 - fontWeight: 'bold', 581 - marginBottom: 8, 582 - color: '#007AFF', 703 + fontSize: darkTheme.typography.h1.fontSize, 704 + fontWeight: darkTheme.typography.h1.fontWeight, 705 + fontFamily: darkTheme.typography.h1.fontFamily, 706 + marginBottom: darkTheme.spacing[1], 707 + color: darkTheme.colors.interactive.primary, 708 + letterSpacing: darkTheme.typography.h1.letterSpacing, 583 709 }, 584 710 subtitle: { 585 - fontSize: 16, 586 - color: '#666', 587 - marginBottom: 30, 711 + fontSize: darkTheme.typography.body.fontSize, 712 + fontFamily: darkTheme.typography.body.fontFamily, 713 + color: darkTheme.colors.text.secondary, 714 + marginBottom: darkTheme.spacing[4], 588 715 textAlign: 'center', 716 + lineHeight: darkTheme.typography.body.lineHeight * darkTheme.typography.body.fontSize, 589 717 }, 590 718 input: { 591 719 width: '100%', 592 720 maxWidth: 400, 593 - height: 50, 721 + height: darkTheme.layout.inputHeight, 594 722 borderWidth: 1, 595 - borderColor: '#ddd', 596 - borderRadius: 8, 597 - paddingHorizontal: 15, 598 - marginBottom: 20, 599 - backgroundColor: '#fff', 723 + borderColor: darkTheme.colors.border.primary, 724 + borderRadius: darkTheme.layout.borderRadius.medium, 725 + paddingHorizontal: darkTheme.spacing[2], 726 + marginBottom: darkTheme.spacing[3], 727 + backgroundColor: darkTheme.colors.background.surface, 728 + color: darkTheme.colors.text.primary, 729 + fontSize: darkTheme.typography.input.fontSize, 730 + fontFamily: darkTheme.typography.input.fontFamily, 600 731 }, 601 732 button: { 602 - backgroundColor: '#007AFF', 603 - paddingHorizontal: 30, 604 - paddingVertical: 15, 605 - borderRadius: 8, 606 - marginBottom: 20, 733 + backgroundColor: darkTheme.colors.interactive.primary, 734 + paddingHorizontal: darkTheme.spacing[4], 735 + paddingVertical: darkTheme.spacing[2], 736 + borderRadius: darkTheme.layout.borderRadius.medium, 737 + marginBottom: darkTheme.spacing[3], 738 + shadowColor: darkTheme.colors.interactive.primary, 739 + shadowOffset: { width: 0, height: 2 }, 740 + shadowOpacity: 0.3, 741 + shadowRadius: 4, 742 + elevation: 4, 607 743 }, 608 744 buttonDisabled: { 609 - backgroundColor: '#cccccc', 745 + backgroundColor: darkTheme.colors.interactive.disabled, 746 + shadowOpacity: 0, 747 + elevation: 0, 610 748 }, 611 749 buttonText: { 612 - color: '#fff', 613 - fontSize: 16, 614 - fontWeight: '600', 750 + color: darkTheme.colors.text.inverse, 751 + fontSize: darkTheme.typography.button.fontSize, 752 + fontWeight: darkTheme.typography.button.fontWeight, 753 + fontFamily: darkTheme.typography.button.fontFamily, 754 + textAlign: 'center', 615 755 }, 616 756 instructions: { 617 - fontSize: 14, 618 - color: '#888', 757 + fontSize: darkTheme.typography.caption.fontSize, 758 + fontFamily: darkTheme.typography.caption.fontFamily, 759 + color: darkTheme.colors.text.secondary, 619 760 textAlign: 'center', 620 - lineHeight: 20, 761 + lineHeight: darkTheme.typography.caption.lineHeight * darkTheme.typography.caption.fontSize, 621 762 }, 622 - header: { 763 + link: { 764 + color: darkTheme.colors.interactive.primary, 765 + textDecorationLine: 'underline', 766 + }, 767 + 768 + // Main layout styles 769 + mainLayout: { 770 + flex: 1, 771 + flexDirection: 'column', 772 + backgroundColor: darkTheme.colors.background.primary, 773 + }, 774 + desktopLayout: { 623 775 flexDirection: 'row', 624 - justifyContent: 'space-between', 625 - alignItems: 'center', 626 - paddingHorizontal: 16, 627 - paddingVertical: 12, 628 - backgroundColor: '#fff', 629 - borderBottomWidth: 1, 630 - borderBottomColor: '#e5e5ea', 631 776 }, 632 - agentSelector: { 777 + 778 + // Mobile sidebar modal 779 + mobileModal: { 633 780 flex: 1, 781 + backgroundColor: darkTheme.colors.background.primary, 634 782 }, 635 - headerTitle: { 636 - fontSize: 18, 637 - fontWeight: '600', 638 - color: '#000', 783 + modalHeader: { 784 + flexDirection: 'row', 785 + justifyContent: 'flex-end', 786 + paddingHorizontal: darkTheme.spacing[2], 787 + paddingVertical: darkTheme.spacing[1.5], 788 + borderBottomWidth: 1, 789 + borderBottomColor: darkTheme.colors.border.primary, 790 + backgroundColor: darkTheme.colors.background.secondary, 639 791 }, 640 - agentCount: { 641 - fontSize: 12, 642 - color: '#666', 643 - marginTop: 2, 792 + modalCloseText: { 793 + fontSize: darkTheme.typography.h6.fontSize, 794 + color: darkTheme.colors.text.secondary, 795 + fontWeight: darkTheme.typography.h6.fontWeight, 796 + fontFamily: darkTheme.typography.h6.fontFamily, 797 + }, 798 + 799 + // Chat area styles 800 + chatArea: { 801 + flex: 1, 802 + flexDirection: 'column', 803 + backgroundColor: darkTheme.colors.background.primary, 644 804 }, 645 - headerButtons: { 805 + chatHeader: { 646 806 flexDirection: 'row', 647 807 alignItems: 'center', 808 + paddingHorizontal: darkTheme.spacing[2], 809 + paddingVertical: darkTheme.spacing[1.5], 810 + backgroundColor: darkTheme.colors.background.secondary, 811 + borderBottomWidth: 1, 812 + borderBottomColor: darkTheme.colors.border.primary, 813 + minHeight: darkTheme.layout.headerHeight, 648 814 }, 649 - createAgentButton: { 650 - fontSize: 24, 651 - color: '#007AFF', 815 + menuButton: { 816 + marginRight: darkTheme.spacing[1.5], 817 + padding: darkTheme.spacing[1], 818 + }, 819 + menuIcon: { 820 + fontSize: darkTheme.typography.h6.fontSize, 821 + color: darkTheme.colors.text.secondary, 822 + fontFamily: darkTheme.typography.h6.fontFamily, 823 + }, 824 + headerContent: { 825 + flex: 1, 826 + }, 827 + agentTitle: { 828 + fontSize: darkTheme.typography.agentName.fontSize, 829 + fontWeight: darkTheme.typography.agentName.fontWeight, 830 + fontFamily: darkTheme.typography.agentName.fontFamily, 831 + color: darkTheme.colors.text.primary, 832 + letterSpacing: darkTheme.typography.agentName.letterSpacing, 833 + }, 834 + agentSubtitle: { 835 + fontSize: darkTheme.typography.caption.fontSize, 836 + fontFamily: darkTheme.typography.caption.fontFamily, 837 + color: darkTheme.colors.text.secondary, 838 + marginTop: darkTheme.spacing[0.5], 839 + }, 840 + headerAction: { 841 + fontSize: darkTheme.typography.h5.fontSize, 842 + color: darkTheme.colors.interactive.secondary, 652 843 fontWeight: '300', 844 + marginLeft: darkTheme.spacing[1.5], 845 + padding: darkTheme.spacing[1], 653 846 }, 654 - disconnectButton: { 655 - fontSize: 16, 656 - color: '#007AFF', 847 + 848 + // Messages styles 849 + messagesContainer: { 850 + flex: 1, 851 + backgroundColor: darkTheme.colors.background.primary, 852 + }, 853 + messagesList: { 854 + maxWidth: darkTheme.layout.maxContentWidth, 855 + alignSelf: 'center', 856 + width: '100%', 857 + paddingHorizontal: darkTheme.spacing[3], 858 + paddingVertical: darkTheme.spacing[2], 859 + }, 860 + messageGroup: { 861 + marginBottom: darkTheme.spacing.messageGap, 657 862 }, 658 863 loadingContainer: { 659 864 flex: 1, 660 865 justifyContent: 'center', 661 866 alignItems: 'center', 662 - padding: 20, 867 + padding: darkTheme.spacing[3], 868 + backgroundColor: darkTheme.colors.background.primary, 663 869 }, 664 870 loadingText: { 665 - marginTop: 12, 666 - fontSize: 16, 667 - color: '#666', 668 - }, 669 - messagesContainer: { 670 - flex: 1, 671 - padding: 16, 871 + marginTop: darkTheme.spacing[1.5], 872 + fontSize: darkTheme.typography.body.fontSize, 873 + fontFamily: darkTheme.typography.body.fontFamily, 874 + color: darkTheme.colors.text.secondary, 672 875 }, 673 876 emptyContainer: { 674 - flex: 1, 675 - justifyContent: 'center', 676 877 alignItems: 'center', 677 - padding: 40, 878 + justifyContent: 'center', 879 + paddingVertical: darkTheme.spacing[10], 678 880 }, 679 881 emptyText: { 680 - fontSize: 16, 681 - color: '#666', 882 + fontSize: darkTheme.typography.body.fontSize, 883 + fontFamily: darkTheme.typography.body.fontFamily, 884 + color: darkTheme.colors.text.secondary, 682 885 textAlign: 'center', 683 - lineHeight: 22, 886 + lineHeight: darkTheme.typography.body.lineHeight * darkTheme.typography.body.fontSize, 684 887 }, 685 - messageBubble: { 686 - marginVertical: 4, 687 - maxWidth: '80%', 688 - padding: 12, 689 - borderRadius: 16, 888 + 889 + // Message styles (Letta design system) 890 + message: { 891 + paddingVertical: darkTheme.spacing[0.5], 690 892 }, 691 893 userMessage: { 692 894 alignSelf: 'flex-end', 693 - backgroundColor: '#007AFF', 694 - borderBottomRightRadius: 4, 895 + maxWidth: '70%', 896 + backgroundColor: darkTheme.colors.background.tertiary, 897 + paddingHorizontal: darkTheme.spacing[1.5], 898 + paddingVertical: darkTheme.spacing[1], 899 + borderRadius: darkTheme.layout.borderRadius.large, 900 + borderBottomRightRadius: darkTheme.layout.borderRadius.small, 901 + borderWidth: 1, 902 + borderColor: darkTheme.colors.border.secondary, 695 903 }, 696 - agentMessage: { 904 + assistantMessage: { 697 905 alignSelf: 'flex-start', 698 - backgroundColor: '#e5e5ea', 699 - borderBottomLeftRadius: 4, 906 + maxWidth: '100%', 907 + borderLeftWidth: 2, 908 + borderLeftColor: 'transparent', 700 909 }, 701 910 messageText: { 702 - fontSize: 16, 703 - lineHeight: 20, 911 + fontSize: darkTheme.typography.chatMessage.fontSize, 912 + fontFamily: darkTheme.typography.chatMessage.fontFamily, 913 + lineHeight: darkTheme.typography.chatMessage.lineHeight * darkTheme.typography.chatMessage.fontSize, 914 + letterSpacing: darkTheme.typography.chatMessage.letterSpacing, 704 915 }, 705 916 userText: { 706 - color: '#fff', 917 + color: darkTheme.colors.text.primary, 918 + }, 919 + assistantText: { 920 + color: darkTheme.colors.text.primary, 921 + }, 922 + 923 + // Reasoning styles (Technical aesthetic) 924 + reasoningContainer: { 925 + backgroundColor: darkTheme.colors.background.tertiary, 926 + padding: darkTheme.spacing[1.5], 927 + marginBottom: darkTheme.spacing[1], 928 + borderRadius: darkTheme.layout.borderRadius.medium, 929 + borderLeftWidth: 4, 930 + borderLeftColor: darkTheme.colors.interactive.primary, 931 + borderStyle: 'solid', 932 + opacity: 0.9, 933 + }, 934 + reasoningText: { 935 + fontSize: darkTheme.typography.reasoning.fontSize, 936 + fontFamily: darkTheme.typography.reasoning.fontFamily, 937 + color: darkTheme.colors.text.secondary, 938 + fontStyle: darkTheme.typography.reasoning.fontStyle, 939 + lineHeight: darkTheme.typography.reasoning.lineHeight * darkTheme.typography.reasoning.fontSize, 940 + letterSpacing: 0.3, 941 + }, 942 + 943 + // Streaming styles (Technical indicators) 944 + streamingMessage: { 945 + borderLeftWidth: 2, 946 + borderLeftColor: darkTheme.colors.interactive.primary, 947 + paddingLeft: darkTheme.spacing[1], 948 + opacity: 0.8, 949 + }, 950 + cursor: { 951 + color: darkTheme.colors.interactive.primary, 952 + fontWeight: 'bold', 953 + opacity: 0.8, 954 + }, 955 + thinkingIndicator: { 956 + flexDirection: 'row', 957 + alignItems: 'center', 958 + padding: darkTheme.spacing[1], 707 959 }, 708 - agentText: { 709 - color: '#000', 960 + thinkingText: { 961 + fontSize: darkTheme.typography.reasoning.fontSize, 962 + fontFamily: darkTheme.typography.reasoning.fontFamily, 963 + color: darkTheme.colors.text.secondary, 964 + fontStyle: darkTheme.typography.reasoning.fontStyle, 965 + marginLeft: darkTheme.spacing[1], 710 966 }, 967 + 968 + // Input styles (Floating glass design) 711 969 inputContainer: { 970 + backgroundColor: 'transparent', 971 + paddingHorizontal: darkTheme.spacing[3], 972 + paddingVertical: darkTheme.spacing[2], 973 + paddingBottom: darkTheme.spacing[3], // Extra bottom padding for floating effect 974 + }, 975 + inputWrapper: { 976 + maxWidth: darkTheme.layout.maxInputWidth, 977 + alignSelf: 'center', 978 + width: '100%', 712 979 flexDirection: 'row', 713 980 alignItems: 'flex-end', 714 - paddingHorizontal: 16, 715 - paddingVertical: 12, 716 - backgroundColor: '#fff', 717 - borderTopWidth: 1, 718 - borderTopColor: '#e5e5ea', 981 + backgroundColor: 'rgba(26, 26, 26, 0.85)', // Semi-transparent dark background 982 + backdropFilter: 'blur(20px)', // Glass blur effect (may not work on all platforms) 983 + borderRadius: darkTheme.layout.borderRadius.round, 984 + padding: darkTheme.spacing[0.5], 985 + borderWidth: 1, 986 + borderColor: 'rgba(255, 255, 255, 0.1)', // Subtle white border for glass effect 987 + shadowColor: '#000000', 988 + shadowOffset: { width: 0, height: 8 }, 989 + shadowOpacity: 0.3, 990 + shadowRadius: 16, 991 + elevation: 8, // Higher elevation for floating effect 719 992 }, 720 993 messageInput: { 721 994 flex: 1, 722 - borderWidth: 1, 723 - borderColor: '#e5e5ea', 724 - borderRadius: 20, 725 - paddingHorizontal: 16, 726 - paddingVertical: 8, 727 - marginRight: 8, 728 - maxHeight: 100, 729 - fontSize: 16, 995 + borderWidth: 0, 996 + borderColor: 'transparent', 997 + borderRadius: darkTheme.layout.borderRadius.large, 998 + paddingHorizontal: darkTheme.spacing[2], 999 + paddingVertical: darkTheme.spacing[1.5], 1000 + marginRight: darkTheme.spacing[1], 1001 + maxHeight: 120, 1002 + fontSize: darkTheme.typography.input.fontSize, 1003 + fontFamily: darkTheme.typography.input.fontFamily, 1004 + backgroundColor: 'transparent', 1005 + color: darkTheme.colors.text.primary, 730 1006 }, 731 1007 sendButton: { 732 - backgroundColor: '#007AFF', 733 - borderRadius: 20, 734 - paddingHorizontal: 16, 735 - paddingVertical: 8, 1008 + backgroundColor: darkTheme.colors.interactive.secondary, 1009 + borderRadius: darkTheme.layout.borderRadius.round, 1010 + paddingHorizontal: darkTheme.spacing[2.5], 1011 + paddingVertical: darkTheme.spacing[1.5], 1012 + minWidth: 50, 1013 + justifyContent: 'center', 1014 + alignItems: 'center', 1015 + shadowColor: darkTheme.colors.interactive.secondary, 1016 + shadowOffset: { width: 0, height: 2 }, 1017 + shadowOpacity: 0.3, 1018 + shadowRadius: 4, 1019 + elevation: 4, 736 1020 }, 737 1021 sendButtonDisabled: { 738 - backgroundColor: '#cccccc', 1022 + backgroundColor: darkTheme.colors.interactive.disabled, 1023 + shadowOpacity: 0, 1024 + elevation: 0, 739 1025 }, 740 1026 sendButtonText: { 741 - color: '#fff', 742 - fontSize: 16, 743 - fontWeight: '600', 1027 + color: darkTheme.colors.text.inverse, 1028 + fontSize: darkTheme.typography.buttonSmall.fontSize, 1029 + fontWeight: darkTheme.typography.buttonSmall.fontWeight, 1030 + fontFamily: darkTheme.typography.buttonSmall.fontFamily, 1031 + textAlign: 'center', 744 1032 }, 1033 + 1034 + // Legacy modal styles (for project selector, etc.) 745 1035 modalOverlay: { 746 1036 flex: 1, 747 - backgroundColor: 'rgba(0, 0, 0, 0.5)', 1037 + backgroundColor: 'rgba(10, 10, 10, 0.8)', 748 1038 justifyContent: 'center', 749 1039 alignItems: 'center', 750 1040 }, 751 1041 modalContent: { 752 - backgroundColor: '#fff', 753 - borderRadius: 12, 754 - padding: 20, 1042 + backgroundColor: darkTheme.colors.background.surface, 1043 + borderRadius: darkTheme.layout.borderRadius.large, 1044 + padding: darkTheme.spacing[3], 755 1045 width: '90%', 756 1046 maxWidth: 400, 757 1047 maxHeight: '80%', 1048 + borderWidth: 1, 1049 + borderColor: darkTheme.colors.border.primary, 1050 + shadowColor: darkTheme.colors.text.primary, 1051 + shadowOffset: { width: 0, height: 8 }, 1052 + shadowOpacity: 0.3, 1053 + shadowRadius: 16, 1054 + elevation: 8, 758 1055 }, 759 1056 modalTitle: { 760 - fontSize: 20, 761 - fontWeight: '600', 762 - color: '#000', 763 - marginBottom: 16, 1057 + fontSize: darkTheme.typography.h5.fontSize, 1058 + fontWeight: darkTheme.typography.h5.fontWeight, 1059 + fontFamily: darkTheme.typography.h5.fontFamily, 1060 + color: darkTheme.colors.text.primary, 1061 + marginBottom: darkTheme.spacing[2], 764 1062 textAlign: 'center', 765 1063 }, 766 1064 agentList: { 767 1065 maxHeight: 300, 768 1066 }, 769 1067 agentItem: { 770 - padding: 16, 1068 + padding: darkTheme.spacing[2], 771 1069 borderBottomWidth: 1, 772 - borderBottomColor: '#e5e5ea', 1070 + borderBottomColor: darkTheme.colors.border.secondary, 1071 + borderRadius: darkTheme.layout.borderRadius.medium, 1072 + marginBottom: darkTheme.spacing[0.5], 773 1073 }, 774 1074 selectedAgentItem: { 775 - backgroundColor: '#f0f8ff', 1075 + backgroundColor: darkTheme.colors.background.tertiary, 1076 + borderLeftWidth: 3, 1077 + borderLeftColor: darkTheme.colors.interactive.primary, 776 1078 }, 777 1079 agentName: { 778 - fontSize: 16, 779 - fontWeight: '600', 780 - color: '#000', 781 - marginBottom: 4, 1080 + fontSize: darkTheme.typography.body.fontSize, 1081 + fontWeight: darkTheme.typography.agentName.fontWeight, 1082 + fontFamily: darkTheme.typography.agentName.fontFamily, 1083 + color: darkTheme.colors.text.primary, 1084 + marginBottom: darkTheme.spacing[0.5], 782 1085 }, 783 1086 agentDescription: { 784 - fontSize: 14, 785 - color: '#666', 1087 + fontSize: darkTheme.typography.caption.fontSize, 1088 + fontFamily: darkTheme.typography.caption.fontFamily, 1089 + color: darkTheme.colors.text.secondary, 786 1090 }, 787 1091 modalCloseButton: { 788 - marginTop: 16, 789 - backgroundColor: '#007AFF', 790 - borderRadius: 8, 791 - paddingVertical: 12, 792 - }, 793 - modalCloseText: { 794 - color: '#fff', 795 - fontSize: 16, 796 - fontWeight: '600', 797 - textAlign: 'center', 1092 + marginTop: darkTheme.spacing[2], 1093 + backgroundColor: darkTheme.colors.interactive.primary, 1094 + borderRadius: darkTheme.layout.borderRadius.medium, 1095 + paddingVertical: darkTheme.spacing[1.5], 798 1096 }, 799 1097 modalInput: { 800 1098 borderWidth: 1, 801 - borderColor: '#ddd', 802 - borderRadius: 8, 803 - paddingHorizontal: 12, 804 - paddingVertical: 10, 805 - marginBottom: 20, 806 - fontSize: 16, 807 - backgroundColor: '#fff', 1099 + borderColor: darkTheme.colors.border.primary, 1100 + borderRadius: darkTheme.layout.borderRadius.medium, 1101 + paddingHorizontal: darkTheme.spacing[1.5], 1102 + paddingVertical: darkTheme.spacing[1.5], 1103 + marginBottom: darkTheme.spacing[3], 1104 + fontSize: darkTheme.typography.input.fontSize, 1105 + fontFamily: darkTheme.typography.input.fontFamily, 1106 + backgroundColor: darkTheme.colors.background.tertiary, 1107 + color: darkTheme.colors.text.primary, 808 1108 }, 809 1109 modalButtons: { 810 1110 flexDirection: 'row', 811 1111 justifyContent: 'space-between', 1112 + gap: darkTheme.spacing[1.5], 812 1113 }, 813 1114 modalCancelButton: { 814 1115 flex: 1, 815 1116 borderWidth: 1, 816 - borderColor: '#ccc', 817 - borderRadius: 8, 818 - paddingVertical: 12, 1117 + borderColor: darkTheme.colors.border.primary, 1118 + borderRadius: darkTheme.layout.borderRadius.medium, 1119 + paddingVertical: darkTheme.spacing[1.5], 819 1120 }, 820 1121 modalCancelText: { 821 - color: '#666', 822 - fontSize: 16, 823 - fontWeight: '600', 1122 + color: darkTheme.colors.text.secondary, 1123 + fontSize: darkTheme.typography.button.fontSize, 1124 + fontWeight: darkTheme.typography.button.fontWeight, 1125 + fontFamily: darkTheme.typography.button.fontFamily, 824 1126 textAlign: 'center', 825 1127 }, 826 1128 modalCreateButton: { 827 1129 flex: 1, 828 - backgroundColor: '#007AFF', 829 - borderRadius: 8, 830 - paddingVertical: 12, 1130 + backgroundColor: darkTheme.colors.interactive.primary, 1131 + borderRadius: darkTheme.layout.borderRadius.medium, 1132 + paddingVertical: darkTheme.spacing[1.5], 1133 + shadowColor: darkTheme.colors.interactive.primary, 1134 + shadowOffset: { width: 0, height: 2 }, 1135 + shadowOpacity: 0.3, 1136 + shadowRadius: 4, 1137 + elevation: 4, 831 1138 }, 832 1139 modalCreateText: { 833 - color: '#fff', 834 - fontSize: 16, 835 - fontWeight: '600', 1140 + color: darkTheme.colors.text.inverse, 1141 + fontSize: darkTheme.typography.button.fontSize, 1142 + fontWeight: darkTheme.typography.button.fontWeight, 1143 + fontFamily: darkTheme.typography.button.fontFamily, 836 1144 textAlign: 'center', 837 - }, 838 - 839 - // Streaming message styles 840 - streamingMessage: { 841 - borderWidth: 1, 842 - borderColor: '#007AFF', 843 - borderStyle: 'dashed', 844 - }, 845 - streamingStep: { 846 - fontSize: 12, 847 - color: '#666', 848 - fontStyle: 'italic', 849 - marginBottom: 4, 850 - }, 851 - cursor: { 852 - color: '#007AFF', 853 - fontWeight: 'bold', 854 - opacity: 0.8, 855 - }, 856 - thinkingIndicator: { 857 - flexDirection: 'row', 858 - alignItems: 'center', 859 - }, 860 - thinkingText: { 861 - fontSize: 14, 862 - color: '#666', 863 - fontStyle: 'italic', 864 - marginLeft: 8, 865 - }, 866 - link: { 867 - color: '#007AFF', 868 - textDecorationLine: 'underline', 869 - }, 870 - backButton: { 871 - fontSize: 16, 872 - color: '#007AFF', 873 - marginBottom: 2, 874 1145 }, 875 1146 });
+121
package-lock.json
··· 14 14 "@react-navigation/drawer": "^7.5.8", 15 15 "@react-navigation/native": "^7.1.17", 16 16 "@react-navigation/stack": "^7.4.8", 17 + "@ronradtke/react-native-markdown-display": "^8.1.0", 17 18 "axios": "^1.11.0", 18 19 "expo": "~53.0.22", 19 20 "expo-secure-store": "~14.2.4", ··· 2779 2780 "react-native-screens": ">= 4.0.0" 2780 2781 } 2781 2782 }, 2783 + "node_modules/@ronradtke/react-native-markdown-display": { 2784 + "version": "8.1.0", 2785 + "resolved": "https://registry.npmjs.org/@ronradtke/react-native-markdown-display/-/react-native-markdown-display-8.1.0.tgz", 2786 + "integrity": "sha512-pAtefWI76vpkxsEgIFivyq1q6ej8rDyR7oVM/cWAxUydyBej9LOvULjLAeFuFLbYAelHTNoYXmGxQOlFLBa0+w==", 2787 + "license": "MIT", 2788 + "dependencies": { 2789 + "css-to-react-native": "^3.2.0", 2790 + "markdown-it": "^13.0.1", 2791 + "prop-types": "^15.7.2", 2792 + "react-native-fit-image": "^1.5.5" 2793 + }, 2794 + "peerDependencies": { 2795 + "react": ">=16.2.0", 2796 + "react-native": ">=0.50.4" 2797 + } 2798 + }, 2782 2799 "node_modules/@sinclair/typebox": { 2783 2800 "version": "0.27.8", 2784 2801 "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", ··· 3640 3657 "node": ">=6" 3641 3658 } 3642 3659 }, 3660 + "node_modules/camelize": { 3661 + "version": "1.0.1", 3662 + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", 3663 + "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", 3664 + "license": "MIT", 3665 + "funding": { 3666 + "url": "https://github.com/sponsors/ljharb" 3667 + } 3668 + }, 3643 3669 "node_modules/caniuse-lite": { 3644 3670 "version": "1.0.30001739", 3645 3671 "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001739.tgz", ··· 4051 4077 "node": ">=8" 4052 4078 } 4053 4079 }, 4080 + "node_modules/css-color-keywords": { 4081 + "version": "1.0.0", 4082 + "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", 4083 + "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", 4084 + "license": "ISC", 4085 + "engines": { 4086 + "node": ">=4" 4087 + } 4088 + }, 4054 4089 "node_modules/css-in-js-utils": { 4055 4090 "version": "3.1.0", 4056 4091 "resolved": "https://registry.npmjs.org/css-in-js-utils/-/css-in-js-utils-3.1.0.tgz", ··· 4058 4093 "license": "MIT", 4059 4094 "dependencies": { 4060 4095 "hyphenate-style-name": "^1.0.3" 4096 + } 4097 + }, 4098 + "node_modules/css-to-react-native": { 4099 + "version": "3.2.0", 4100 + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", 4101 + "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", 4102 + "license": "MIT", 4103 + "dependencies": { 4104 + "camelize": "^1.0.0", 4105 + "css-color-keywords": "^1.0.0", 4106 + "postcss-value-parser": "^4.0.2" 4061 4107 } 4062 4108 }, 4063 4109 "node_modules/csstype": { ··· 4244 4290 "license": "MIT", 4245 4291 "engines": { 4246 4292 "node": ">= 0.8" 4293 + } 4294 + }, 4295 + "node_modules/entities": { 4296 + "version": "3.0.1", 4297 + "resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz", 4298 + "integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==", 4299 + "license": "BSD-2-Clause", 4300 + "engines": { 4301 + "node": ">=0.12" 4302 + }, 4303 + "funding": { 4304 + "url": "https://github.com/fb55/entities?sponsor=1" 4247 4305 } 4248 4306 }, 4249 4307 "node_modules/env-editor": { ··· 5842 5900 "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", 5843 5901 "license": "MIT" 5844 5902 }, 5903 + "node_modules/linkify-it": { 5904 + "version": "4.0.1", 5905 + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-4.0.1.tgz", 5906 + "integrity": "sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==", 5907 + "license": "MIT", 5908 + "dependencies": { 5909 + "uc.micro": "^1.0.1" 5910 + } 5911 + }, 5845 5912 "node_modules/locate-path": { 5846 5913 "version": "6.0.0", 5847 5914 "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", ··· 5982 6049 "tmpl": "1.0.5" 5983 6050 } 5984 6051 }, 6052 + "node_modules/markdown-it": { 6053 + "version": "13.0.2", 6054 + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-13.0.2.tgz", 6055 + "integrity": "sha512-FtwnEuuK+2yVU7goGn/MJ0WBZMM9ZPgU9spqlFs7/A/pDIUNSOQZhUgOqYCficIuR2QaFnrt8LHqBWsbTAoI5w==", 6056 + "license": "MIT", 6057 + "dependencies": { 6058 + "argparse": "^2.0.1", 6059 + "entities": "~3.0.1", 6060 + "linkify-it": "^4.0.1", 6061 + "mdurl": "^1.0.1", 6062 + "uc.micro": "^1.0.5" 6063 + }, 6064 + "bin": { 6065 + "markdown-it": "bin/markdown-it.js" 6066 + } 6067 + }, 5985 6068 "node_modules/marky": { 5986 6069 "version": "1.3.0", 5987 6070 "resolved": "https://registry.npmjs.org/marky/-/marky-1.3.0.tgz", ··· 5996 6079 "engines": { 5997 6080 "node": ">= 0.4" 5998 6081 } 6082 + }, 6083 + "node_modules/mdurl": { 6084 + "version": "1.0.1", 6085 + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", 6086 + "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", 6087 + "license": "MIT" 5999 6088 }, 6000 6089 "node_modules/memoize-one": { 6001 6090 "version": "5.2.1", ··· 7197 7286 "node": ">= 6" 7198 7287 } 7199 7288 }, 7289 + "node_modules/prop-types": { 7290 + "version": "15.8.1", 7291 + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", 7292 + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", 7293 + "license": "MIT", 7294 + "dependencies": { 7295 + "loose-envify": "^1.4.0", 7296 + "object-assign": "^4.1.1", 7297 + "react-is": "^16.13.1" 7298 + } 7299 + }, 7300 + "node_modules/prop-types/node_modules/react-is": { 7301 + "version": "16.13.1", 7302 + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", 7303 + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", 7304 + "license": "MIT" 7305 + }, 7200 7306 "node_modules/proxy-from-env": { 7201 7307 "version": "1.1.0", 7202 7308 "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", ··· 7438 7544 "peerDependencies": { 7439 7545 "react": "*", 7440 7546 "react-native": "*" 7547 + } 7548 + }, 7549 + "node_modules/react-native-fit-image": { 7550 + "version": "1.5.5", 7551 + "resolved": "https://registry.npmjs.org/react-native-fit-image/-/react-native-fit-image-1.5.5.tgz", 7552 + "integrity": "sha512-Wl3Vq2DQzxgsWKuW4USfck9zS7YzhvLNPpkwUUCF90bL32e1a0zOVQ3WsJILJOwzmPdHfzZmWasiiAUNBkhNkg==", 7553 + "license": "Beerware", 7554 + "dependencies": { 7555 + "prop-types": "^15.5.10" 7441 7556 } 7442 7557 }, 7443 7558 "node_modules/react-native-gesture-handler": { ··· 8940 9055 "engines": { 8941 9056 "node": "*" 8942 9057 } 9058 + }, 9059 + "node_modules/uc.micro": { 9060 + "version": "1.0.6", 9061 + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", 9062 + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", 9063 + "license": "MIT" 8943 9064 }, 8944 9065 "node_modules/undici": { 8945 9066 "version": "6.21.3",
+1
package.json
··· 15 15 "@react-navigation/drawer": "^7.5.8", 16 16 "@react-navigation/native": "^7.1.17", 17 17 "@react-navigation/stack": "^7.4.8", 18 + "@ronradtke/react-native-markdown-display": "^8.1.0", 18 19 "axios": "^1.11.0", 19 20 "expo": "~53.0.22", 20 21 "expo-secure-store": "~14.2.4",
+117 -67
src/api/lettaApi.ts
··· 221 221 console.log('sendMessageStream - messageData:', messageData); 222 222 console.log('Client initialized:', !!this.client); 223 223 224 - // Transform messages with detailed logging 225 - const transformedMessages = messageData.messages.map((msg, index) => { 226 - const transformedMsg = { 227 - role: msg.role, 228 - content: [ 229 - { 230 - type: "text", 231 - text: msg.content 232 - } 233 - ] 234 - }; 235 - console.log(`Transformed message ${index}:`, transformedMsg); 236 - return transformedMsg; 237 - }); 238 - 224 + // Simplify: only send required messages field 239 225 const lettaStreamingRequest = { 240 - messages: transformedMessages, 241 - maxSteps: messageData.max_steps, 242 - useAssistantMessage: messageData.use_assistant_message, 243 - enableThinking: messageData.enable_thinking ? 'true' : undefined, 244 - streamTokens: messageData.stream_tokens, 245 - includePings: messageData.include_pings 226 + messages: messageData.messages.map(msg => ({ 227 + role: msg.role, 228 + content: [{ 229 + type: "text", 230 + text: msg.content 231 + }] 232 + })) 246 233 }; 247 234 248 - console.log('=== FULL REQUEST OBJECT ==='); 249 - console.log('lettaStreamingRequest:', JSON.stringify(lettaStreamingRequest, null, 2)); 250 - console.log('=== END REQUEST OBJECT ==='); 251 - 252 - console.log('Individual field values:'); 253 - console.log('- messages.length:', lettaStreamingRequest.messages.length); 254 - console.log('- maxSteps:', lettaStreamingRequest.maxSteps); 255 - console.log('- useAssistantMessage:', lettaStreamingRequest.useAssistantMessage); 256 - console.log('- enableThinking:', lettaStreamingRequest.enableThinking); 257 - console.log('- streamTokens:', lettaStreamingRequest.streamTokens); 258 - console.log('- includePings:', lettaStreamingRequest.includePings); 235 + console.log('=== SIMPLIFIED REQUEST ==='); 236 + console.log('Request:', JSON.stringify(lettaStreamingRequest, null, 2)); 237 + console.log('Messages count:', lettaStreamingRequest.messages.length); 259 238 260 239 const stream = await this.client.agents.messages.createStream(agentId, lettaStreamingRequest); 261 240 262 241 // Handle the stream response using async iteration 263 242 try { 264 243 for await (const chunk of stream) { 265 - console.log('Stream chunk received:', chunk); 244 + console.log('=== RAW CHUNK RECEIVED ==='); 245 + console.log('Raw chunk:', JSON.stringify(chunk, null, 2)); 246 + console.log('Chunk keys:', Object.keys(chunk)); 247 + console.log('message_type variants:', { 248 + messageType: chunk.messageType, 249 + message_type: chunk.message_type 250 + }); 251 + console.log('Content variants:', { 252 + content: chunk.content, 253 + assistantMessage: chunk.assistantMessage, 254 + assistant_message: chunk.assistant_message 255 + }); 256 + 266 257 onChunk({ 267 - message_type: chunk.messageType || chunk.message_type, 268 - content: chunk.content || chunk.assistantMessage, 258 + message_type: chunk.message_type || chunk.messageType, 259 + content: chunk.assistant_message || chunk.assistantMessage || chunk.content, 269 260 reasoning: chunk.reasoning, 270 - tool_call: chunk.toolCall, 271 - tool_response: chunk.toolResponse, 261 + tool_call: chunk.tool_call || chunk.toolCall, 262 + tool_response: chunk.tool_response || chunk.toolResponse, 272 263 step: chunk.step, 273 - run_id: chunk.runId, 274 - seq_id: chunk.seqId 264 + run_id: chunk.run_id || chunk.runId, 265 + seq_id: chunk.seq_id || chunk.seqId 275 266 }); 276 267 } 277 268 ··· 311 302 if (!this.client) { 312 303 throw new Error('Client not initialized. Please set auth token first.'); 313 304 } 314 - 305 + 315 306 console.log('listMessages - agentId:', agentId); 316 307 console.log('listMessages - params:', params); 317 - 308 + 318 309 const response = await this.client.agents.messages.list(agentId, params); 319 310 console.log('listMessages - response count:', response?.length || 0); 320 - 321 - // Transform SDK response to match our LettaMessage interface 322 - const transformedMessages: LettaMessage[] = response.map((message: any) => { 323 - // Map messageType to role for our components 324 - let role: 'user' | 'assistant' | 'system' | 'tool' = 'assistant'; 325 - 326 - if (message.messageType === 'user_message') { 327 - role = 'user'; 328 - } else if (message.messageType === 'system_message') { 329 - role = 'system'; 330 - } else if (message.messageType === 'tool_message' || message.messageType === 'tool_call') { 331 - role = 'tool'; 332 - } else { 333 - role = 'assistant'; // assistant_message, reasoning_message, etc. 311 + 312 + // Group messages by run_id and step_id to associate reasoning with assistant messages 313 + const groupedMessages = new Map<string, any[]>(); 314 + 315 + // First pass: group messages by run_id + step_id 316 + response.forEach((message: any) => { 317 + const key = `${message.runId || 'no-run'}-${message.stepId || 'no-step'}`; 318 + if (!groupedMessages.has(key)) { 319 + groupedMessages.set(key, []); 334 320 } 321 + groupedMessages.get(key)!.push(message); 322 + }); 335 323 336 - return { 337 - id: message.id, 338 - role: role, 339 - content: message.content || message.reasoning || '', 340 - created_at: message.date ? message.date.toISOString() : new Date().toISOString(), 341 - tool_calls: message.tool_calls, 342 - message_type: message.messageType, 343 - sender_id: message.senderId, 344 - step_id: message.stepId, 345 - run_id: message.runId 346 - }; 324 + // Second pass: process groups to combine reasoning with assistant messages 325 + const transformedMessages: LettaMessage[] = []; 326 + 327 + for (const [key, messageGroup] of groupedMessages) { 328 + // Sort messages in the group by creation time or message order 329 + messageGroup.sort((a, b) => { 330 + if (a.date && b.date) { 331 + return new Date(a.date).getTime() - new Date(b.date).getTime(); 332 + } 333 + return 0; 334 + }); 335 + 336 + // Find reasoning and assistant messages in this group 337 + const reasoningMessages = messageGroup.filter(m => m.messageType === 'reasoning_message'); 338 + const otherMessages = messageGroup.filter(m => m.messageType !== 'reasoning_message'); 339 + 340 + // Combine reasoning content 341 + const combinedReasoning = reasoningMessages 342 + .map(m => m.reasoning || m.content || '') 343 + .filter(r => r.trim()) 344 + .join(' '); 345 + 346 + // Process other messages and attach reasoning to assistant messages 347 + otherMessages.forEach((message: any) => { 348 + // Filter out heartbeat messages from user messages 349 + if (message.messageType === 'user_message' && typeof message.content === 'string') { 350 + try { 351 + const parsed = JSON.parse(message.content); 352 + if (parsed?.type === 'heartbeat') { 353 + return; // Skip heartbeat messages 354 + } 355 + } catch { 356 + // Keep message if content is not valid JSON 357 + } 358 + } 359 + 360 + // Map messageType to role for our components 361 + let role: 'user' | 'assistant' | 'system' | 'tool' = 'assistant'; 362 + 363 + if (message.messageType === 'user_message') { 364 + role = 'user'; 365 + } else if (message.messageType === 'system_message') { 366 + role = 'system'; 367 + } else if (message.messageType === 'tool_message' || message.messageType === 'tool_call') { 368 + role = 'tool'; 369 + } else { 370 + role = 'assistant'; // assistant_message, etc. 371 + } 372 + 373 + const transformedMessage: LettaMessage = { 374 + id: message.id, 375 + role: role, 376 + content: message.content || '', 377 + created_at: message.date ? message.date.toISOString() : new Date().toISOString(), 378 + tool_calls: message.tool_calls, 379 + message_type: message.messageType, 380 + sender_id: message.senderId, 381 + step_id: message.stepId, 382 + run_id: message.runId 383 + }; 384 + 385 + // Attach reasoning to assistant messages 386 + if (role === 'assistant' && combinedReasoning) { 387 + transformedMessage.reasoning = combinedReasoning; 388 + } 389 + 390 + transformedMessages.push(transformedMessage); 391 + }); 392 + } 393 + 394 + // Sort final messages by creation time 395 + transformedMessages.sort((a, b) => { 396 + return new Date(a.created_at).getTime() - new Date(b.created_at).getTime(); 347 397 }); 348 - 398 + 349 399 console.log('listMessages - transformed messages:', transformedMessages.slice(0, 2)); 350 400 return transformedMessages; 351 401 } catch (error) {
+174
src/components/MessageContent.tsx
··· 1 + import React from 'react'; 2 + import { StyleSheet } from 'react-native'; 3 + import Markdown from '@ronradtke/react-native-markdown-display'; 4 + import { darkTheme } from '../theme'; 5 + 6 + interface MessageContentProps { 7 + content: string; 8 + isUser: boolean; 9 + } 10 + 11 + const MessageContent: React.FC<MessageContentProps> = ({ content, isUser }) => { 12 + // Define colors based on Letta theme 13 + const userTextColor = darkTheme.colors.text.primary; // White for user messages on dark background 14 + const assistantTextColor = darkTheme.colors.text.primary; // White for assistant messages on dark background 15 + const userAccentColor = darkTheme.colors.text.inverse; // Dark for user message accents 16 + const assistantAccentColor = darkTheme.colors.text.secondary; // Gray for assistant message accents 17 + 18 + const markdownStyles = StyleSheet.create({ 19 + body: { 20 + color: isUser ? userTextColor : assistantTextColor, 21 + fontSize: darkTheme.typography.chatMessage.fontSize, 22 + lineHeight: darkTheme.typography.chatMessage.lineHeight * darkTheme.typography.chatMessage.fontSize, 23 + fontFamily: darkTheme.typography.chatMessage.fontFamily, 24 + }, 25 + paragraph: { 26 + marginTop: 0, 27 + marginBottom: darkTheme.spacing[1], 28 + color: isUser ? userTextColor : assistantTextColor, 29 + fontSize: darkTheme.typography.chatMessage.fontSize, 30 + lineHeight: darkTheme.typography.chatMessage.lineHeight * darkTheme.typography.chatMessage.fontSize, 31 + fontFamily: darkTheme.typography.chatMessage.fontFamily, 32 + }, 33 + strong: { 34 + fontWeight: 'bold', 35 + color: isUser ? userTextColor : assistantTextColor, 36 + }, 37 + em: { 38 + fontStyle: 'italic', 39 + color: isUser ? userTextColor : assistantTextColor, 40 + }, 41 + code_inline: { 42 + backgroundColor: isUser ? darkTheme.colors.background.surface : darkTheme.colors.background.tertiary, 43 + color: isUser ? userTextColor : darkTheme.colors.interactive.primary, 44 + paddingHorizontal: darkTheme.spacing[0.5], 45 + paddingVertical: darkTheme.spacing[0.5], 46 + borderRadius: darkTheme.layout.borderRadius.small, 47 + fontFamily: darkTheme.typography.code.fontFamily, 48 + fontSize: darkTheme.typography.code.fontSize, 49 + }, 50 + code_block: { 51 + backgroundColor: isUser ? darkTheme.colors.background.surface : darkTheme.colors.background.tertiary, 52 + color: isUser ? userTextColor : darkTheme.colors.interactive.primary, 53 + padding: darkTheme.spacing[1], 54 + borderRadius: darkTheme.layout.borderRadius.medium, 55 + fontFamily: darkTheme.typography.code.fontFamily, 56 + fontSize: darkTheme.typography.code.fontSize, 57 + marginVertical: darkTheme.spacing[0.5], 58 + borderLeftWidth: 2, 59 + borderLeftColor: darkTheme.colors.interactive.primary, 60 + }, 61 + fence: { 62 + backgroundColor: isUser ? darkTheme.colors.background.surface : darkTheme.colors.background.tertiary, 63 + color: isUser ? userTextColor : darkTheme.colors.interactive.primary, 64 + padding: darkTheme.spacing[1], 65 + borderRadius: darkTheme.layout.borderRadius.medium, 66 + fontFamily: darkTheme.typography.code.fontFamily, 67 + fontSize: darkTheme.typography.code.fontSize, 68 + marginVertical: darkTheme.spacing[0.5], 69 + borderLeftWidth: 2, 70 + borderLeftColor: darkTheme.colors.interactive.primary, 71 + }, 72 + heading1: { 73 + fontSize: darkTheme.typography.h1.fontSize, 74 + fontWeight: darkTheme.typography.h1.fontWeight, 75 + fontFamily: darkTheme.typography.h1.fontFamily, 76 + color: isUser ? userTextColor : assistantTextColor, 77 + marginVertical: darkTheme.spacing[1], 78 + }, 79 + heading2: { 80 + fontSize: darkTheme.typography.h2.fontSize, 81 + fontWeight: darkTheme.typography.h2.fontWeight, 82 + fontFamily: darkTheme.typography.h2.fontFamily, 83 + color: isUser ? userTextColor : assistantTextColor, 84 + marginVertical: darkTheme.spacing[1], 85 + }, 86 + heading3: { 87 + fontSize: darkTheme.typography.h3.fontSize, 88 + fontWeight: darkTheme.typography.h3.fontWeight, 89 + fontFamily: darkTheme.typography.h3.fontFamily, 90 + color: isUser ? userTextColor : assistantTextColor, 91 + marginVertical: darkTheme.spacing[0.5], 92 + }, 93 + heading4: { 94 + fontSize: darkTheme.typography.h4.fontSize, 95 + fontWeight: darkTheme.typography.h4.fontWeight, 96 + fontFamily: darkTheme.typography.h4.fontFamily, 97 + color: isUser ? userTextColor : assistantTextColor, 98 + marginVertical: darkTheme.spacing[0.5], 99 + }, 100 + heading5: { 101 + fontSize: darkTheme.typography.h5.fontSize, 102 + fontWeight: darkTheme.typography.h5.fontWeight, 103 + fontFamily: darkTheme.typography.h5.fontFamily, 104 + color: isUser ? userTextColor : assistantTextColor, 105 + marginVertical: darkTheme.spacing[0.5], 106 + }, 107 + heading6: { 108 + fontSize: darkTheme.typography.h6.fontSize, 109 + fontWeight: darkTheme.typography.h6.fontWeight, 110 + fontFamily: darkTheme.typography.h6.fontFamily, 111 + color: isUser ? userTextColor : assistantTextColor, 112 + marginVertical: darkTheme.spacing[0.5], 113 + }, 114 + bullet_list: { 115 + marginVertical: darkTheme.spacing[0.5], 116 + }, 117 + ordered_list: { 118 + marginVertical: darkTheme.spacing[0.5], 119 + }, 120 + list_item: { 121 + flexDirection: 'row', 122 + marginVertical: darkTheme.spacing[0.5], 123 + }, 124 + bullet_list_icon: { 125 + color: isUser ? userTextColor : assistantAccentColor, 126 + marginRight: darkTheme.spacing[1], 127 + fontSize: darkTheme.typography.body.fontSize, 128 + }, 129 + bullet_list_content: { 130 + color: isUser ? userTextColor : assistantTextColor, 131 + fontSize: darkTheme.typography.body.fontSize, 132 + fontFamily: darkTheme.typography.body.fontFamily, 133 + lineHeight: darkTheme.typography.body.lineHeight * darkTheme.typography.body.fontSize, 134 + flex: 1, 135 + }, 136 + ordered_list_icon: { 137 + color: isUser ? userTextColor : assistantAccentColor, 138 + marginRight: darkTheme.spacing[1], 139 + fontSize: darkTheme.typography.body.fontSize, 140 + }, 141 + ordered_list_content: { 142 + color: isUser ? userTextColor : assistantTextColor, 143 + fontSize: darkTheme.typography.body.fontSize, 144 + fontFamily: darkTheme.typography.body.fontFamily, 145 + lineHeight: darkTheme.typography.body.lineHeight * darkTheme.typography.body.fontSize, 146 + flex: 1, 147 + }, 148 + blockquote: { 149 + backgroundColor: isUser ? darkTheme.colors.background.surface : darkTheme.colors.background.tertiary, 150 + borderLeftWidth: 4, 151 + borderLeftColor: isUser ? darkTheme.colors.text.secondary : darkTheme.colors.interactive.primary, 152 + paddingLeft: darkTheme.spacing[1.5], 153 + paddingVertical: darkTheme.spacing[1], 154 + marginVertical: darkTheme.spacing[0.5], 155 + }, 156 + link: { 157 + color: isUser ? darkTheme.colors.interactive.primary : darkTheme.colors.interactive.primary, 158 + textDecorationLine: 'underline', 159 + }, 160 + hr: { 161 + backgroundColor: isUser ? darkTheme.colors.border.secondary : darkTheme.colors.border.primary, 162 + height: 1, 163 + marginVertical: darkTheme.spacing[1], 164 + }, 165 + }); 166 + 167 + return ( 168 + <Markdown style={markdownStyles}> 169 + {content} 170 + </Markdown> 171 + ); 172 + }; 173 + 174 + export default MessageContent;
+289
src/components/Sidebar.tsx
··· 1 + import React, { useState, useEffect } from 'react'; 2 + import { 3 + View, 4 + Text, 5 + StyleSheet, 6 + TouchableOpacity, 7 + ScrollView, 8 + ActivityIndicator, 9 + RefreshControl, 10 + Alert, 11 + } from 'react-native'; 12 + import lettaApi from '../api/lettaApi'; 13 + import { darkTheme } from '../theme'; 14 + import type { LettaAgent, Project } from '../types/letta'; 15 + 16 + interface SidebarProps { 17 + currentProject: Project | null; 18 + currentAgent: LettaAgent | null; 19 + onAgentSelect: (agent: LettaAgent) => void; 20 + onProjectPress: () => void; 21 + onCreateAgent: () => void; 22 + onLogout: () => void; 23 + isVisible: boolean; 24 + } 25 + 26 + export default function Sidebar({ 27 + currentProject, 28 + currentAgent, 29 + onAgentSelect, 30 + onProjectPress, 31 + onCreateAgent, 32 + onLogout, 33 + isVisible, 34 + }: SidebarProps) { 35 + const [agents, setAgents] = useState<LettaAgent[]>([]); 36 + const [isLoading, setIsLoading] = useState(true); 37 + const [isRefreshing, setIsRefreshing] = useState(false); 38 + 39 + const loadAgents = async (isRefresh = false) => { 40 + if (!currentProject) { 41 + setAgents([]); 42 + setIsLoading(false); 43 + return; 44 + } 45 + 46 + try { 47 + if (!isRefresh) setIsLoading(true); 48 + 49 + const agentList = await lettaApi.listAgentsForProject(currentProject.id, { 50 + sortBy: 'last_run_completion', 51 + limit: 50, 52 + }); 53 + 54 + setAgents(agentList); 55 + console.log('Loaded agents for sidebar:', agentList.length); 56 + } catch (error: any) { 57 + console.error('Failed to load agents:', error); 58 + Alert.alert('Error', 'Failed to load agents: ' + error.message); 59 + } finally { 60 + setIsLoading(false); 61 + setIsRefreshing(false); 62 + } 63 + }; 64 + 65 + const handleRefresh = () => { 66 + setIsRefreshing(true); 67 + loadAgents(true); 68 + }; 69 + 70 + useEffect(() => { 71 + if (currentProject) { 72 + loadAgents(); 73 + } 74 + }, [currentProject]); 75 + 76 + if (!isVisible) return null; 77 + 78 + return ( 79 + <View style={styles.sidebar}> 80 + {/* Project Header */} 81 + <TouchableOpacity style={styles.projectHeader} onPress={onProjectPress}> 82 + <Text style={styles.projectName}> 83 + {currentProject?.name || 'Select Project'} 84 + </Text> 85 + <Text style={styles.projectSubtext}> 86 + {agents.length} agent{agents.length !== 1 ? 's' : ''} 87 + </Text> 88 + </TouchableOpacity> 89 + 90 + {/* Agents List */} 91 + <View style={styles.agentListContainer}> 92 + <Text style={styles.sectionTitle}>Agents</Text> 93 + {isLoading ? ( 94 + <View style={styles.loadingContainer}> 95 + <ActivityIndicator size="small" color="#666" /> 96 + <Text style={styles.loadingText}>Loading agents...</Text> 97 + </View> 98 + ) : ( 99 + <ScrollView 100 + style={styles.agentList} 101 + refreshControl={ 102 + <RefreshControl refreshing={isRefreshing} onRefresh={handleRefresh} /> 103 + } 104 + > 105 + {agents.length === 0 ? ( 106 + <View style={styles.emptyContainer}> 107 + <Text style={styles.emptyText}>No agents found</Text> 108 + <TouchableOpacity style={styles.createButton} onPress={onCreateAgent}> 109 + <Text style={styles.createButtonText}>Create Agent</Text> 110 + </TouchableOpacity> 111 + </View> 112 + ) : ( 113 + agents.map((agent) => ( 114 + <TouchableOpacity 115 + key={agent.id} 116 + style={[ 117 + styles.agentItem, 118 + currentAgent?.id === agent.id && styles.selectedAgentItem 119 + ]} 120 + onPress={() => onAgentSelect(agent)} 121 + > 122 + <Text style={[ 123 + styles.agentName, 124 + currentAgent?.id === agent.id && styles.selectedAgentName 125 + ]}> 126 + {agent.name} 127 + </Text> 128 + <Text style={styles.agentMeta}> 129 + {agent.last_run_completion 130 + ? `Last run: ${new Date(agent.last_run_completion).toLocaleDateString()}` 131 + : 'Never run' 132 + } 133 + </Text> 134 + </TouchableOpacity> 135 + )) 136 + )} 137 + </ScrollView> 138 + )} 139 + </View> 140 + 141 + {/* Bottom Actions */} 142 + <View style={styles.bottomActions}> 143 + <TouchableOpacity style={styles.createButton} onPress={onCreateAgent}> 144 + <Text style={styles.createButtonText}>+ New Agent</Text> 145 + </TouchableOpacity> 146 + <TouchableOpacity style={styles.logoutButton} onPress={onLogout}> 147 + <Text style={styles.logoutButtonText}>Logout</Text> 148 + </TouchableOpacity> 149 + </View> 150 + </View> 151 + ); 152 + } 153 + 154 + const styles = StyleSheet.create({ 155 + sidebar: { 156 + width: darkTheme.layout.sidebarWidth, 157 + backgroundColor: darkTheme.colors.background.secondary, 158 + borderRightWidth: 1, 159 + borderRightColor: darkTheme.colors.border.primary, 160 + flexDirection: 'column', 161 + height: '100%', 162 + }, 163 + projectHeader: { 164 + padding: darkTheme.spacing[2], 165 + borderBottomWidth: 1, 166 + borderBottomColor: darkTheme.colors.border.secondary, 167 + backgroundColor: darkTheme.colors.background.tertiary, 168 + }, 169 + projectName: { 170 + fontSize: darkTheme.typography.agentName.fontSize, 171 + fontWeight: darkTheme.typography.agentName.fontWeight, 172 + fontFamily: darkTheme.typography.agentName.fontFamily, 173 + color: darkTheme.colors.text.primary, 174 + letterSpacing: darkTheme.typography.agentName.letterSpacing, 175 + }, 176 + projectSubtext: { 177 + fontSize: darkTheme.typography.caption.fontSize, 178 + fontFamily: darkTheme.typography.caption.fontFamily, 179 + color: darkTheme.colors.text.secondary, 180 + marginTop: darkTheme.spacing[0.5], 181 + }, 182 + agentListContainer: { 183 + flex: 1, 184 + paddingHorizontal: darkTheme.spacing[2], 185 + }, 186 + sectionTitle: { 187 + fontSize: darkTheme.typography.technical.fontSize, 188 + fontWeight: darkTheme.typography.technical.fontWeight, 189 + fontFamily: darkTheme.typography.technical.fontFamily, 190 + color: darkTheme.colors.text.secondary, 191 + textTransform: darkTheme.typography.technical.textTransform, 192 + letterSpacing: darkTheme.typography.technical.letterSpacing, 193 + marginTop: darkTheme.spacing[2], 194 + marginBottom: darkTheme.spacing[1.5], 195 + }, 196 + loadingContainer: { 197 + flexDirection: 'row', 198 + alignItems: 'center', 199 + justifyContent: 'center', 200 + padding: darkTheme.spacing[3], 201 + }, 202 + loadingText: { 203 + marginLeft: darkTheme.spacing[1], 204 + fontSize: darkTheme.typography.bodySmall.fontSize, 205 + fontFamily: darkTheme.typography.bodySmall.fontFamily, 206 + color: darkTheme.colors.text.secondary, 207 + }, 208 + agentList: { 209 + flex: 1, 210 + }, 211 + emptyContainer: { 212 + alignItems: 'center', 213 + justifyContent: 'center', 214 + paddingVertical: darkTheme.spacing[5], 215 + }, 216 + emptyText: { 217 + fontSize: darkTheme.typography.bodySmall.fontSize, 218 + fontFamily: darkTheme.typography.bodySmall.fontFamily, 219 + color: darkTheme.colors.text.secondary, 220 + marginBottom: darkTheme.spacing[2], 221 + textAlign: 'center', 222 + }, 223 + agentItem: { 224 + paddingVertical: darkTheme.spacing[1.5], 225 + paddingHorizontal: darkTheme.spacing[1.5], 226 + marginBottom: darkTheme.spacing[0.5], 227 + borderRadius: darkTheme.layout.borderRadius.medium, 228 + borderLeftWidth: 0, 229 + borderLeftColor: 'transparent', 230 + }, 231 + selectedAgentItem: { 232 + backgroundColor: darkTheme.colors.background.tertiary, 233 + borderLeftWidth: 3, 234 + borderLeftColor: darkTheme.colors.interactive.primary, 235 + }, 236 + agentName: { 237 + fontSize: darkTheme.typography.bodySmall.fontSize, 238 + fontWeight: '500', 239 + fontFamily: darkTheme.typography.bodySmall.fontFamily, 240 + color: darkTheme.colors.text.primary, 241 + }, 242 + selectedAgentName: { 243 + color: darkTheme.colors.interactive.primary, 244 + fontWeight: '600', 245 + }, 246 + agentMeta: { 247 + fontSize: darkTheme.typography.caption.fontSize, 248 + fontFamily: darkTheme.typography.caption.fontFamily, 249 + color: darkTheme.colors.text.secondary, 250 + marginTop: darkTheme.spacing[0.5], 251 + }, 252 + bottomActions: { 253 + padding: darkTheme.spacing[2], 254 + borderTopWidth: 1, 255 + borderTopColor: darkTheme.colors.border.secondary, 256 + backgroundColor: darkTheme.colors.background.tertiary, 257 + }, 258 + createButton: { 259 + backgroundColor: darkTheme.colors.interactive.secondary, 260 + paddingVertical: darkTheme.spacing[1.5], 261 + paddingHorizontal: darkTheme.spacing[2], 262 + borderRadius: darkTheme.layout.borderRadius.medium, 263 + marginBottom: darkTheme.spacing[1], 264 + shadowColor: darkTheme.colors.interactive.secondary, 265 + shadowOffset: { width: 0, height: 2 }, 266 + shadowOpacity: 0.3, 267 + shadowRadius: 4, 268 + elevation: 4, 269 + }, 270 + createButtonText: { 271 + color: darkTheme.colors.text.inverse, 272 + fontSize: darkTheme.typography.buttonSmall.fontSize, 273 + fontWeight: darkTheme.typography.buttonSmall.fontWeight, 274 + fontFamily: darkTheme.typography.buttonSmall.fontFamily, 275 + textAlign: 'center', 276 + textTransform: darkTheme.typography.buttonSmall.textTransform, 277 + letterSpacing: darkTheme.typography.buttonSmall.letterSpacing, 278 + }, 279 + logoutButton: { 280 + paddingVertical: darkTheme.spacing[1], 281 + paddingHorizontal: darkTheme.spacing[2], 282 + }, 283 + logoutButtonText: { 284 + color: darkTheme.colors.text.secondary, 285 + fontSize: darkTheme.typography.caption.fontSize, 286 + fontFamily: darkTheme.typography.caption.fontFamily, 287 + textAlign: 'center', 288 + }, 289 + });
+134
src/theme/animations.ts
··· 1 + // Letta Brand Animation System 2 + 3 + // Duration constants (in milliseconds) 4 + export const duration = { 5 + instant: 0, 6 + fast: 200, 7 + normal: 300, 8 + slow: 400, 9 + slower: 600, 10 + } as const; 11 + 12 + // Easing functions 13 + export const easing = { 14 + // Standard easing 15 + linear: 'linear', 16 + easeIn: 'ease-in', 17 + easeOut: 'ease-out', 18 + easeInOut: 'ease-in-out', 19 + 20 + // Custom cubic-bezier curves 21 + bounce: 'cubic-bezier(0.68, -0.55, 0.265, 1.55)', 22 + smooth: 'cubic-bezier(0.4, 0, 0.2, 1)', 23 + sharp: 'cubic-bezier(0.4, 0, 0.6, 1)', 24 + } as const; 25 + 26 + // Pre-defined animation configurations 27 + export const animations = { 28 + // Micro-interactions 29 + buttonHover: { 30 + duration: duration.fast, 31 + easing: easing.smooth, 32 + transform: 'scale(1.02)', 33 + }, 34 + buttonPress: { 35 + duration: duration.fast, 36 + easing: easing.sharp, 37 + transform: 'scale(0.98)', 38 + }, 39 + 40 + // Message animations 41 + messageAppear: { 42 + duration: duration.normal, 43 + easing: easing.smooth, 44 + from: { opacity: 0, transform: 'translateY(10px)' }, 45 + to: { opacity: 1, transform: 'translateY(0px)' }, 46 + }, 47 + reasoningReveal: { 48 + duration: duration.slow, 49 + easing: easing.smooth, 50 + from: { opacity: 0, maxHeight: 0 }, 51 + to: { opacity: 1, maxHeight: '200px' }, 52 + }, 53 + 54 + // Loading states 55 + pulse: { 56 + duration: 1500, 57 + easing: easing.easeInOut, 58 + iterationCount: 'infinite', 59 + direction: 'alternate', 60 + from: { opacity: 0.4 }, 61 + to: { opacity: 1 }, 62 + }, 63 + spin: { 64 + duration: 1000, 65 + easing: easing.linear, 66 + iterationCount: 'infinite', 67 + from: { transform: 'rotate(0deg)' }, 68 + to: { transform: 'rotate(360deg)' }, 69 + }, 70 + 71 + // Layout transitions 72 + slideIn: { 73 + duration: duration.normal, 74 + easing: easing.smooth, 75 + from: { transform: 'translateX(-100%)' }, 76 + to: { transform: 'translateX(0%)' }, 77 + }, 78 + slideOut: { 79 + duration: duration.normal, 80 + easing: easing.smooth, 81 + from: { transform: 'translateX(0%)' }, 82 + to: { transform: 'translateX(-100%)' }, 83 + }, 84 + fadeIn: { 85 + duration: duration.normal, 86 + easing: easing.smooth, 87 + from: { opacity: 0 }, 88 + to: { opacity: 1 }, 89 + }, 90 + fadeOut: { 91 + duration: duration.fast, 92 + easing: easing.smooth, 93 + from: { opacity: 1 }, 94 + to: { opacity: 0 }, 95 + }, 96 + 97 + // Focus states 98 + focusGlow: { 99 + duration: duration.fast, 100 + easing: easing.smooth, 101 + boxShadow: '0 0 20px rgba(0, 102, 255, 0.4)', 102 + }, 103 + 104 + // Geometric loading (Letta-specific) 105 + geometricAssembly: { 106 + duration: duration.slower, 107 + easing: easing.bounce, 108 + keyframes: [ 109 + { transform: 'scale(0) rotate(0deg)', opacity: 0 }, 110 + { transform: 'scale(0.5) rotate(180deg)', opacity: 0.5 }, 111 + { transform: 'scale(1) rotate(360deg)', opacity: 1 }, 112 + ], 113 + }, 114 + } as const; 115 + 116 + // Animation utility functions 117 + export const createKeyframes = (keyframes: Record<string, any>) => { 118 + return Object.entries(keyframes) 119 + .map(([key, value]) => `${key} { ${Object.entries(value).map(([prop, val]) => `${prop}: ${val};`).join(' ')} }`) 120 + .join(' '); 121 + }; 122 + 123 + export const createTransition = ( 124 + property: string | string[], 125 + duration: number = 300, 126 + easing: string = 'ease-in-out' 127 + ) => { 128 + const properties = Array.isArray(property) ? property : [property]; 129 + return properties.map(prop => `${prop} ${duration}ms ${easing}`).join(', '); 130 + }; 131 + 132 + export type Duration = typeof duration; 133 + export type Easing = typeof easing; 134 + export type Animations = typeof animations;
+103
src/theme/colors.ts
··· 1 + // Letta Brand Colors 2 + export const LettaColors = { 3 + // Primary Brand Colors 4 + deepBlack: '#0A0A0A', 5 + pureWhite: '#FFFFFF', 6 + neutralGray: '#B8B8B8', 7 + 8 + // Accent Colors 9 + electricBlue: '#0066FF', 10 + vibrantOrange: '#FF5500', 11 + royalBlue: { 12 + start: '#0040CC', 13 + end: '#4080FF', 14 + }, 15 + 16 + // Extended Palette 17 + darkGray: { 18 + 100: '#1A1A1A', 19 + 200: '#0F0F0F', 20 + 300: '#050505', 21 + }, 22 + lightGray: { 23 + 100: '#F8F8F8', 24 + 200: '#E5E5E5', 25 + 300: '#CCCCCC', 26 + }, 27 + 28 + // Semantic Colors 29 + success: '#00CC66', 30 + warning: '#FFAA00', 31 + error: '#FF3366', 32 + info: '#0066FF', 33 + 34 + // Technical Colors (for code, reasoning, etc) 35 + mono: { 36 + bg: '#0F0F0F', 37 + text: '#B8B8B8', 38 + accent: '#0066FF', 39 + } 40 + } as const; 41 + 42 + // Theme-aware color tokens 43 + export const createColorTokens = (isDark: boolean = true) => ({ 44 + // Backgrounds 45 + background: { 46 + primary: isDark ? LettaColors.deepBlack : LettaColors.pureWhite, 47 + secondary: isDark ? LettaColors.darkGray[300] : LettaColors.lightGray[100], 48 + tertiary: isDark ? LettaColors.darkGray[200] : LettaColors.lightGray[200], 49 + surface: isDark ? LettaColors.darkGray[100] : LettaColors.pureWhite, 50 + }, 51 + 52 + // Text Colors 53 + text: { 54 + primary: isDark ? LettaColors.pureWhite : LettaColors.deepBlack, 55 + secondary: isDark ? LettaColors.neutralGray : '#666666', 56 + tertiary: isDark ? '#888888' : '#999999', 57 + inverse: isDark ? LettaColors.deepBlack : LettaColors.pureWhite, 58 + }, 59 + 60 + // Interactive Elements 61 + interactive: { 62 + primary: LettaColors.electricBlue, 63 + primaryHover: '#0052CC', 64 + secondary: LettaColors.vibrantOrange, 65 + secondaryHover: '#E64A00', 66 + disabled: '#666666', 67 + }, 68 + 69 + // Borders & Separators 70 + border: { 71 + primary: isDark ? '#333333' : '#E5E5E5', 72 + secondary: isDark ? '#1A1A1A' : '#F0F0F0', 73 + accent: LettaColors.electricBlue, 74 + }, 75 + 76 + // Status & Feedback 77 + status: { 78 + success: LettaColors.success, 79 + warning: LettaColors.warning, 80 + error: LettaColors.error, 81 + info: LettaColors.info, 82 + }, 83 + 84 + // Gradients 85 + gradients: { 86 + royal: `linear-gradient(135deg, ${LettaColors.royalBlue.start} 0%, ${LettaColors.royalBlue.end} 100%)`, 87 + accent: `linear-gradient(135deg, ${LettaColors.electricBlue} 0%, ${LettaColors.vibrantOrange} 100%)`, 88 + }, 89 + 90 + // Shadows & Effects 91 + shadow: { 92 + small: isDark ? '0 1px 3px rgba(0, 0, 0, 0.5)' : '0 1px 3px rgba(0, 0, 0, 0.1)', 93 + medium: isDark ? '0 4px 12px rgba(0, 0, 0, 0.4)' : '0 4px 12px rgba(0, 0, 0, 0.15)', 94 + large: isDark ? '0 8px 32px rgba(0, 0, 0, 0.3)' : '0 8px 32px rgba(0, 0, 0, 0.1)', 95 + glow: `0 0 20px ${LettaColors.electricBlue}40`, 96 + } 97 + }); 98 + 99 + // Export default dark theme 100 + export const defaultColors = createColorTokens(true); 101 + export const lightColors = createColorTokens(false); 102 + 103 + export type ColorTokens = ReturnType<typeof createColorTokens>;
+55
src/theme/index.ts
··· 1 + // Letta Theme System - Central Export 2 + 3 + export * from './colors'; 4 + export * from './typography'; 5 + export * from './spacing'; 6 + export * from './animations'; 7 + 8 + import { defaultColors, lightColors, createColorTokens, type ColorTokens } from './colors'; 9 + import { typography, type Typography } from './typography'; 10 + import { spacing, layout, breakpoints, type Spacing, type Layout } from './spacing'; 11 + import { duration, easing, animations, createTransition, type Duration, type Easing } from './animations'; 12 + 13 + // Complete theme object 14 + export const createTheme = (isDark: boolean = true) => ({ 15 + colors: createColorTokens(isDark), 16 + typography, 17 + spacing, 18 + layout, 19 + breakpoints, 20 + animations: { 21 + duration, 22 + easing, 23 + presets: animations, 24 + createTransition, 25 + }, 26 + }); 27 + 28 + // Default themes 29 + export const darkTheme = createTheme(true); 30 + export const lightTheme = createTheme(false); 31 + 32 + // Theme type 33 + export type Theme = ReturnType<typeof createTheme>; 34 + 35 + // Theme context interface 36 + export interface ThemeContextType { 37 + theme: Theme; 38 + isDark: boolean; 39 + toggleTheme: () => void; 40 + } 41 + 42 + // Utility function to get responsive values 43 + export const responsive = (values: { [key in keyof typeof breakpoints]?: any }) => { 44 + return values; 45 + }; 46 + 47 + // Media query helpers for React Native Web 48 + export const mediaQuery = { 49 + mobile: `@media (max-width: ${breakpoints.tablet - 1}px)`, 50 + tablet: `@media (min-width: ${breakpoints.tablet}px) and (max-width: ${breakpoints.desktop - 1}px)`, 51 + desktop: `@media (min-width: ${breakpoints.desktop}px)`, 52 + wide: `@media (min-width: ${breakpoints.wide}px)`, 53 + }; 54 + 55 + export default darkTheme;
+80
src/theme/spacing.ts
··· 1 + // Letta Brand Spacing System - Based on 8px Grid 2 + 3 + export const spacing = { 4 + // Base unit (8px) 5 + 1: 8, 6 + 2: 16, 7 + 3: 24, 8 + 4: 32, 9 + 5: 40, 10 + 6: 48, 11 + 7: 56, 12 + 8: 64, 13 + 9: 72, 14 + 10: 80, 15 + 12: 96, 16 + 16: 128, 17 + 20: 160, 18 + 24: 192, 19 + 32: 256, 20 + 21 + // Fractional spacing 22 + 0.5: 4, 23 + 1.5: 12, 24 + 2.5: 20, 25 + 3.5: 28, 26 + 27 + // Component-specific 28 + componentGap: 24, // Gap between major UI components 29 + sectionGap: 40, // Gap between major sections 30 + messageGap: 24, // Gap between message groups 31 + inputPadding: 16, // Padding inside inputs 32 + buttonPadding: 12, // Padding inside buttons 33 + } as const; 34 + 35 + // Responsive breakpoints (following brand guidelines) 36 + export const breakpoints = { 37 + mobile: 0, 38 + tablet: 768, 39 + desktop: 1024, 40 + wide: 1440, 41 + } as const; 42 + 43 + // Layout dimensions 44 + export const layout = { 45 + // Sidebar 46 + sidebarWidth: 280, 47 + sidebarCollapsedWidth: 64, 48 + 49 + // Content 50 + maxContentWidth: 840, // Optimal reading width based on golden ratio 51 + maxInputWidth: 800, 52 + 53 + // Component sizes 54 + headerHeight: 56, 55 + inputHeight: 48, 56 + buttonHeight: 48, 57 + buttonSmallHeight: 32, 58 + 59 + // Border radius (geometric/minimal approach) 60 + borderRadius: { 61 + small: 4, 62 + medium: 8, 63 + large: 12, 64 + round: 24, 65 + } 66 + } as const; 67 + 68 + // Grid system 69 + export const grid = { 70 + columns: { 71 + mobile: 6, 72 + tablet: 8, 73 + desktop: 12, 74 + }, 75 + gutterWidth: spacing[2], // 16px 76 + marginWidth: spacing[3], // 24px on mobile, responsive 77 + } as const; 78 + 79 + export type Spacing = typeof spacing; 80 + export type Layout = typeof layout;
+205
src/theme/typography.ts
··· 1 + // Letta Brand Typography System 2 + 3 + export const fontFamily = { 4 + primary: '-apple-system, "SF Pro Display", Inter, "Helvetica Neue", sans-serif', 5 + mono: 'SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace', 6 + } as const; 7 + 8 + export const fontSize = { 9 + // Headlines (H1-H6) 10 + h1: 64, 11 + h2: 40, 12 + h3: 32, 13 + h4: 24, 14 + h5: 20, 15 + h6: 18, 16 + 17 + // Body Text 18 + body: 16, 19 + bodySmall: 14, 20 + 21 + // UI Elements 22 + button: 14, 23 + input: 16, 24 + label: 12, 25 + caption: 11, 26 + 27 + // Technical 28 + code: 14, 29 + tiny: 10, 30 + } as const; 31 + 32 + export const fontWeight = { 33 + light: '300', 34 + regular: '400', 35 + medium: '500', 36 + semibold: '600', 37 + bold: '700', 38 + } as const; 39 + 40 + export const lineHeight = { 41 + tight: 1.2, 42 + normal: 1.4, 43 + relaxed: 1.6, 44 + loose: 1.8, 45 + } as const; 46 + 47 + export const letterSpacing = { 48 + tight: -0.02, 49 + normal: 0, 50 + wide: 0.08, 51 + } as const; 52 + 53 + // Typography Tokens 54 + export const typography = { 55 + // Headlines 56 + h1: { 57 + fontSize: fontSize.h1, 58 + fontWeight: fontWeight.bold, 59 + lineHeight: lineHeight.tight, 60 + letterSpacing: letterSpacing.tight, 61 + fontFamily: fontFamily.primary, 62 + }, 63 + h2: { 64 + fontSize: fontSize.h2, 65 + fontWeight: fontWeight.medium, 66 + lineHeight: lineHeight.tight, 67 + letterSpacing: letterSpacing.tight, 68 + fontFamily: fontFamily.primary, 69 + }, 70 + h3: { 71 + fontSize: fontSize.h3, 72 + fontWeight: fontWeight.medium, 73 + lineHeight: lineHeight.normal, 74 + letterSpacing: letterSpacing.normal, 75 + fontFamily: fontFamily.primary, 76 + }, 77 + h4: { 78 + fontSize: fontSize.h4, 79 + fontWeight: fontWeight.regular, 80 + lineHeight: lineHeight.normal, 81 + letterSpacing: letterSpacing.normal, 82 + fontFamily: fontFamily.primary, 83 + }, 84 + h5: { 85 + fontSize: fontSize.h5, 86 + fontWeight: fontWeight.regular, 87 + lineHeight: lineHeight.normal, 88 + letterSpacing: letterSpacing.normal, 89 + fontFamily: fontFamily.primary, 90 + }, 91 + h6: { 92 + fontSize: fontSize.h6, 93 + fontWeight: fontWeight.regular, 94 + lineHeight: lineHeight.normal, 95 + letterSpacing: letterSpacing.normal, 96 + fontFamily: fontFamily.primary, 97 + }, 98 + 99 + // Body Text 100 + body: { 101 + fontSize: fontSize.body, 102 + fontWeight: fontWeight.regular, 103 + lineHeight: lineHeight.relaxed, 104 + letterSpacing: letterSpacing.normal, 105 + fontFamily: fontFamily.primary, 106 + }, 107 + bodySmall: { 108 + fontSize: fontSize.bodySmall, 109 + fontWeight: fontWeight.regular, 110 + lineHeight: lineHeight.normal, 111 + letterSpacing: letterSpacing.normal, 112 + fontFamily: fontFamily.primary, 113 + }, 114 + 115 + // UI Components 116 + button: { 117 + fontSize: fontSize.button, 118 + fontWeight: fontWeight.semibold, 119 + lineHeight: lineHeight.tight, 120 + letterSpacing: letterSpacing.normal, 121 + fontFamily: fontFamily.primary, 122 + textTransform: 'none' as const, 123 + }, 124 + buttonSmall: { 125 + fontSize: fontSize.caption, 126 + fontWeight: fontWeight.semibold, 127 + lineHeight: lineHeight.tight, 128 + letterSpacing: letterSpacing.wide, 129 + fontFamily: fontFamily.primary, 130 + textTransform: 'uppercase' as const, 131 + }, 132 + input: { 133 + fontSize: fontSize.input, 134 + fontWeight: fontWeight.regular, 135 + lineHeight: lineHeight.normal, 136 + letterSpacing: letterSpacing.normal, 137 + fontFamily: fontFamily.primary, 138 + }, 139 + label: { 140 + fontSize: fontSize.label, 141 + fontWeight: fontWeight.light, 142 + lineHeight: lineHeight.tight, 143 + letterSpacing: letterSpacing.wide, 144 + fontFamily: fontFamily.primary, 145 + textTransform: 'uppercase' as const, 146 + }, 147 + caption: { 148 + fontSize: fontSize.caption, 149 + fontWeight: fontWeight.regular, 150 + lineHeight: lineHeight.normal, 151 + letterSpacing: letterSpacing.normal, 152 + fontFamily: fontFamily.primary, 153 + }, 154 + 155 + // Technical Elements 156 + code: { 157 + fontSize: fontSize.code, 158 + fontWeight: fontWeight.regular, 159 + lineHeight: lineHeight.relaxed, 160 + letterSpacing: letterSpacing.normal, 161 + fontFamily: fontFamily.mono, 162 + }, 163 + technical: { 164 + fontSize: fontSize.caption, 165 + fontWeight: fontWeight.light, 166 + lineHeight: lineHeight.tight, 167 + letterSpacing: letterSpacing.wide, 168 + fontFamily: fontFamily.primary, 169 + textTransform: 'uppercase' as const, 170 + }, 171 + 172 + // Chat-specific 173 + chatMessage: { 174 + fontSize: fontSize.body, 175 + fontWeight: fontWeight.regular, 176 + lineHeight: lineHeight.relaxed, 177 + letterSpacing: letterSpacing.normal, 178 + fontFamily: fontFamily.primary, 179 + }, 180 + reasoning: { 181 + fontSize: fontSize.bodySmall, 182 + fontWeight: fontWeight.light, 183 + lineHeight: lineHeight.normal, 184 + letterSpacing: letterSpacing.normal, 185 + fontFamily: fontFamily.primary, 186 + fontStyle: 'italic' as const, 187 + }, 188 + agentName: { 189 + fontSize: fontSize.h6, 190 + fontWeight: fontWeight.semibold, 191 + lineHeight: lineHeight.tight, 192 + letterSpacing: letterSpacing.normal, 193 + fontFamily: fontFamily.primary, 194 + }, 195 + timestamp: { 196 + fontSize: fontSize.tiny, 197 + fontWeight: fontWeight.light, 198 + lineHeight: lineHeight.tight, 199 + letterSpacing: letterSpacing.wide, 200 + fontFamily: fontFamily.primary, 201 + textTransform: 'uppercase' as const, 202 + } 203 + } as const; 204 + 205 + export type Typography = typeof typography;
+1
src/types/letta.ts
··· 127 127 sender_id?: string; 128 128 step_id?: string; 129 129 run_id?: string; 130 + reasoning?: string; 130 131 } 131 132 132 133 export interface ToolCall {