A React Native app for the ultimate thinking partner.

fix(streaming): preserve completed blocks and fix tool call padding

- Add completedStreamBlocks array to store finished message blocks
- When transitioning between message types, push current content to completed blocks
- Display completed blocks + current stream content in chronological order
- Remove messageContainer padding from streaming tool calls (fixes indent issue)
- Ensures all streamed content remains visible until server reload completes
- Messages now properly accumulate and display during multi-step streaming

+55 -10
+55 -10
App.tsx
··· 108 108 toolCalls: [] as Array<{id: string, name: string, args: string}>, 109 109 assistantMessage: '', 110 110 }); 111 + // Store completed stream blocks (reasoning, assistant messages, tool calls that have finished) 112 + const [completedStreamBlocks, setCompletedStreamBlocks] = useState<Array<{ 113 + type: 'reasoning' | 'assistant_message', 114 + content: string 115 + }>>([]); 111 116 const [isStreaming, setIsStreaming] = useState(false); 112 117 const [lastMessageNeedsSpace, setLastMessageNeedsSpace] = useState(false); 113 118 const spacerHeightAnim = useRef(new Animated.Value(0)).current; ··· 470 475 setIsStreaming(false); 471 476 setIsSendingMessage(false); 472 477 setCurrentStream({ reasoning: '', toolCalls: [], assistantMessage: '' }); 478 + setCompletedStreamBlocks([]); 473 479 return; 474 480 } 475 481 ··· 479 485 return; 480 486 } 481 487 482 - // Simple accumulation - reset accumulators when switching to a new message type 488 + // Accumulate content - when switching message types, save previous content to completed blocks 483 489 if (chunk.message_type === 'reasoning_message' && chunk.reasoning) { 484 - // Accumulate reasoning, but reset if we're coming from assistant message or tool call 485 490 setCurrentStream(prev => { 486 - // If we have assistant message or tool calls, this is a NEW reasoning block - reset 487 - if (prev.assistantMessage || prev.toolCalls.length > 0) { 491 + // If we have assistant message, this is a NEW reasoning block - save assistant message first 492 + if (prev.assistantMessage) { 493 + setCompletedStreamBlocks(blocks => [...blocks, { 494 + type: 'assistant_message', 495 + content: prev.assistantMessage 496 + }]); 488 497 return { 489 498 reasoning: chunk.reasoning, 490 499 toolCalls: [], ··· 543 552 544 553 if (contentText) { 545 554 setCurrentStream(prev => { 546 - // If we have reasoning but no assistant message, this is a NEW assistant message - clear reasoning 555 + // If we have reasoning, this is a NEW assistant message - save reasoning first 547 556 if (prev.reasoning && !prev.assistantMessage) { 557 + setCompletedStreamBlocks(blocks => [...blocks, { 558 + type: 'reasoning', 559 + content: prev.reasoning 560 + }]); 548 561 return { 549 562 reasoning: '', 550 563 toolCalls: [], ··· 639 652 setLastMessageNeedsSpace(true); 640 653 // Clear streaming state 641 654 setCurrentStream({ reasoning: '', toolCalls: [], assistantMessage: '' }); 655 + setCompletedStreamBlocks([]); 642 656 643 657 // Make status indicator immediately visible 644 658 statusFadeAnim.setValue(1); ··· 769 783 // Clear streaming state after attempting to load 770 784 setIsStreaming(false); 771 785 setCurrentStream({ reasoning: '', toolCalls: [], assistantMessage: '' }); 786 + setCompletedStreamBlocks([]); 772 787 } 773 788 }, 500); 774 789 }, ··· 803 818 804 819 setIsStreaming(false); 805 820 setCurrentStream({ reasoning: '', toolCalls: [], assistantMessage: '' }); 821 + setCompletedStreamBlocks([]); 806 822 807 823 // Create detailed error message 808 824 let errorMsg = 'Failed to send message'; ··· 2227 2243 <> 2228 2244 {isStreaming && ( 2229 2245 <Animated.View style={[styles.assistantFullWidthContainer, { minHeight: spacerHeightAnim, opacity: statusFadeAnim }]}> 2230 - {/* Streaming Block - show all current stream content */} 2246 + {/* Streaming Block - show completed blocks + current stream content */} 2247 + 2248 + {/* Show completed blocks first (in chronological order) */} 2249 + {completedStreamBlocks.map((block, index) => { 2250 + if (block.type === 'reasoning') { 2251 + return ( 2252 + <React.Fragment key={`completed-${index}`}> 2253 + <LiveStatusIndicator status="thought" /> 2254 + <View style={styles.reasoningStreamingContainer}> 2255 + <Text style={styles.reasoningStreamingText}>{block.content}</Text> 2256 + </View> 2257 + </React.Fragment> 2258 + ); 2259 + } else if (block.type === 'assistant_message') { 2260 + return ( 2261 + <React.Fragment key={`completed-${index}`}> 2262 + <LiveStatusIndicator status="saying" /> 2263 + <View style={{ flex: 1 }}> 2264 + <MessageContent 2265 + content={block.content} 2266 + isUser={false} 2267 + isDark={colorScheme === 'dark'} 2268 + /> 2269 + </View> 2270 + <View style={styles.messageSeparator} /> 2271 + </React.Fragment> 2272 + ); 2273 + } 2274 + return null; 2275 + })} 2231 2276 2232 - {/* Show reasoning with status indicator if we have it */} 2277 + {/* Show current reasoning being accumulated */} 2233 2278 {currentStream.reasoning && ( 2234 2279 <> 2235 2280 <LiveStatusIndicator status="thought" /> ··· 2241 2286 2242 2287 {/* Show tool calls if we have any */} 2243 2288 {currentStream.toolCalls.map((toolCall) => ( 2244 - <View key={toolCall.id} style={styles.messageContainer}> 2289 + <View key={toolCall.id}> 2245 2290 <ToolCallItem 2246 2291 callText={toolCall.args} 2247 2292 hasResult={false} ··· 2249 2294 </View> 2250 2295 ))} 2251 2296 2252 - {/* Show assistant message if we have it */} 2297 + {/* Show current assistant message being accumulated */} 2253 2298 {currentStream.assistantMessage && ( 2254 2299 <> 2255 2300 <View style={currentStream.toolCalls.length > 0 ? { marginTop: 16 } : undefined}> ··· 2267 2312 )} 2268 2313 2269 2314 {/* Show thinking indicator if nothing else to show */} 2270 - {!currentStream.reasoning && !currentStream.assistantMessage && currentStream.toolCalls.length === 0 && ( 2315 + {completedStreamBlocks.length === 0 && !currentStream.reasoning && !currentStream.assistantMessage && currentStream.toolCalls.length === 0 && ( 2271 2316 <LiveStatusIndicator status="thinking" /> 2272 2317 )} 2273 2318 </Animated.View>