A React Native app for the ultimate thinking partner.

feat(rn): star indicators and toggle in agent lists\n\n- Add star button to each agent row in sidebar (both tabs)\n- Add star indicator + toggle to AgentCard in drawer list\n- Uses Zustand favorites; prevents parent press via nested touchable

+107 -49
+30 -7
src/components/AgentCard.tsx
··· 8 8 agent: LettaAgent; 9 9 isSelected: boolean; 10 10 onPress: () => void; 11 + isFavorited?: boolean; 12 + onToggleFavorite?: () => void; 11 13 } 12 14 13 - const AgentCard: React.FC<AgentCardProps> = ({ agent, isSelected, onPress }) => { 15 + const AgentCard: React.FC<AgentCardProps> = ({ agent, isSelected, onPress, isFavorited, onToggleFavorite }) => { 14 16 const formatDate = (dateString: string) => { 15 17 const date = new Date(dateString); 16 18 return date.toLocaleDateString('en-US', { ··· 35 37 /> 36 38 </View> 37 39 <View style={styles.textContainer}> 38 - <Text 39 - style={[styles.name, isSelected && styles.selectedText]} 40 - numberOfLines={1} 41 - > 42 - {agent.name} 43 - </Text> 40 + <View style={styles.rowHeader}> 41 + <Text 42 + style={[styles.name, isSelected && styles.selectedText]} 43 + numberOfLines={1} 44 + > 45 + {agent.name} 46 + </Text> 47 + {onToggleFavorite && ( 48 + <TouchableOpacity 49 + accessibilityLabel={isFavorited ? 'Unfavorite agent' : 'Favorite agent'} 50 + onPress={onToggleFavorite} 51 + style={styles.starBtn} 52 + > 53 + <Ionicons name={isFavorited ? 'star' : 'star-outline'} size={16} color={isFavorited ? '#ffd166' : (isSelected ? darkTheme.colors.text.inverse : darkTheme.colors.text.secondary)} /> 54 + </TouchableOpacity> 55 + )} 56 + </View> 44 57 <Text 45 58 style={[styles.date, isSelected && styles.selectedSubtext]} 46 59 > ··· 120 133 alignItems: 'center', 121 134 marginBottom: 8, 122 135 }, 136 + rowHeader: { 137 + flexDirection: 'row', 138 + alignItems: 'center', 139 + justifyContent: 'space-between', 140 + }, 123 141 avatarContainer: { 124 142 width: 40, 125 143 height: 40, ··· 137 155 fontWeight: '600', 138 156 color: darkTheme.colors.text.primary, 139 157 marginBottom: 2, 158 + }, 159 + starBtn: { 160 + paddingHorizontal: 6, 161 + paddingVertical: 4, 162 + marginLeft: 8, 140 163 }, 141 164 selectedText: { 142 165 color: darkTheme.colors.text.inverse,
+4
src/components/AgentsDrawerContent.tsx
··· 26 26 setCurrentAgent, 27 27 createAgent, 28 28 fetchAgents, 29 + toggleFavorite, 30 + isFavorite, 29 31 } = useAppStore(); 30 32 31 33 useEffect(() => { ··· 92 94 agent={agent} 93 95 isSelected={agent.id === currentAgentId} 94 96 onPress={() => handleAgentPress(agent.id)} 97 + isFavorited={isFavorite(agent.id)} 98 + onToggleFavorite={() => toggleFavorite(agent.id)} 95 99 /> 96 100 ))} 97 101
+73 -42
src/components/Sidebar.tsx
··· 167 167 <Text style={styles.emptyText}>Loading favorites…</Text> 168 168 </View> 169 169 ) : ( 170 - favoriteAgents.map((agent) => ( 170 + favoriteAgents.map((agent) => { 171 + const starred = favorites.includes(agent.id) 172 + return ( 173 + <TouchableOpacity 174 + key={agent.id} 175 + style={[ 176 + styles.agentItem, 177 + currentAgent?.id === agent.id && styles.selectedAgentItem 178 + ]} 179 + onPress={() => onAgentSelect(agent)} 180 + > 181 + <View style={styles.rowHeader}> 182 + <Text style={[ 183 + styles.agentName, 184 + currentAgent?.id === agent.id && styles.selectedAgentName 185 + ]}> 186 + {agent.name} 187 + </Text> 188 + <TouchableOpacity 189 + style={styles.starBtn} 190 + onPress={() => useAppStore.getState().toggleFavorite(agent.id)} 191 + accessibilityLabel={starred ? 'Unfavorite agent' : 'Favorite agent'} 192 + > 193 + <Ionicons name={starred ? 'star' : 'star-outline'} size={16} color={starred ? '#ffd166' : darkTheme.colors.text.secondary} /> 194 + </TouchableOpacity> 195 + </View> 196 + <Text style={styles.agentMeta}> 197 + {agent.last_run_completion 198 + ? `Last run: ${new Date(agent.last_run_completion).toLocaleDateString()}` 199 + : 'Never run'} 200 + </Text> 201 + </TouchableOpacity> 202 + ) 203 + }) 204 + ) 205 + ) : agents.length === 0 ? ( 206 + <View style={styles.emptyContainer}> 207 + <Text style={styles.emptyText}>No agents found</Text> 208 + <TouchableOpacity style={styles.createButton} onPress={onCreateAgent}> 209 + <Text style={styles.createButtonText}>Create Agent</Text> 210 + </TouchableOpacity> 211 + </View> 212 + ) : ( 213 + agents.map((agent) => { 214 + const starred = favorites.includes(agent.id) 215 + return ( 171 216 <TouchableOpacity 172 217 key={agent.id} 173 218 style={[ ··· 176 221 ]} 177 222 onPress={() => onAgentSelect(agent)} 178 223 > 179 - <Text style={[ 180 - styles.agentName, 181 - currentAgent?.id === agent.id && styles.selectedAgentName 182 - ]}> 183 - {agent.name} 184 - </Text> 224 + <View style={styles.rowHeader}> 225 + <Text style={[ 226 + styles.agentName, 227 + currentAgent?.id === agent.id && styles.selectedAgentName 228 + ]}> 229 + {agent.name} 230 + </Text> 231 + <TouchableOpacity 232 + style={styles.starBtn} 233 + onPress={() => useAppStore.getState().toggleFavorite(agent.id)} 234 + accessibilityLabel={starred ? 'Unfavorite agent' : 'Favorite agent'} 235 + > 236 + <Ionicons name={starred ? 'star' : 'star-outline'} size={16} color={starred ? '#ffd166' : darkTheme.colors.text.secondary} /> 237 + </TouchableOpacity> 238 + </View> 185 239 <Text style={styles.agentMeta}> 186 240 {agent.last_run_completion 187 241 ? `Last run: ${new Date(agent.last_run_completion).toLocaleDateString()}` 188 - : 'Never run' 189 - } 242 + : 'Never run'} 190 243 </Text> 191 244 </TouchableOpacity> 192 - )) 193 - ) 194 - ) : agents.length === 0 ? ( 195 - <View style={styles.emptyContainer}> 196 - <Text style={styles.emptyText}>No agents found</Text> 197 - <TouchableOpacity style={styles.createButton} onPress={onCreateAgent}> 198 - <Text style={styles.createButtonText}>Create Agent</Text> 199 - </TouchableOpacity> 200 - </View> 201 - ) : ( 202 - agents.map((agent) => ( 203 - <TouchableOpacity 204 - key={agent.id} 205 - style={[ 206 - styles.agentItem, 207 - currentAgent?.id === agent.id && styles.selectedAgentItem 208 - ]} 209 - onPress={() => onAgentSelect(agent)} 210 - > 211 - <Text style={[ 212 - styles.agentName, 213 - currentAgent?.id === agent.id && styles.selectedAgentName 214 - ]}> 215 - {agent.name} 216 - </Text> 217 - <Text style={styles.agentMeta}> 218 - {agent.last_run_completion 219 - ? `Last run: ${new Date(agent.last_run_completion).toLocaleDateString()}` 220 - : 'Never run' 221 - } 222 - </Text> 223 - </TouchableOpacity> 224 - )) 245 + ) 246 + }) 225 247 )} 226 248 </ScrollView> 227 249 )} ··· 380 402 borderColor: darkTheme.colors.border.primary, 381 403 borderRadius: 0, 382 404 }, 405 + rowHeader: { 406 + flexDirection: 'row', 407 + alignItems: 'center', 408 + justifyContent: 'space-between', 409 + }, 383 410 agentName: { 384 411 fontSize: darkTheme.typography.body.fontSize, 385 412 fontWeight: '600', 386 413 fontFamily: darkTheme.typography.body.fontFamily, 387 414 color: darkTheme.colors.text.primary, 415 + }, 416 + starBtn: { 417 + paddingHorizontal: 6, 418 + paddingVertical: 4, 388 419 }, 389 420 selectedAgentName: { 390 421 color: darkTheme.colors.text.primary,