A React Native app for the ultimate thinking partner.

fix: deduplicate tool calls in streaming by tracking IDs

Tool calls were being duplicated in the streaming footer when the same
tool call was received across multiple streaming chunks. This caused
spam where the same "(co is updating memory)" block appeared multiple
times.

Changes:
- Add uid=501(cameron) gid=20(staff) groups=20(staff),12(everyone),61(localaccounts),79(_appserverusr),80(admin),81(_appserveradm),701(com.apple.sharepoint.group.1),33(_appstore),98(_lpadmin),100(_lpoperator),204(_developer),250(_analyticsusers),395(com.apple.access_ftp),398(com.apple.access_screensharing),399(com.apple.access_ssh),400(com.apple.access_remote_ae) field to toolCalls array in currentStream state
- Track tool calls by ID and skip duplicates in handleStreamingChunk
- Use toolCall.id as React key instead of array index for stability

This ensures each unique tool call only appears once in the streaming UI.

+16 -8
+16 -8
App.tsx
··· 96 96 // Simplified streaming state - everything in one place 97 97 const [currentStream, setCurrentStream] = useState({ 98 98 reasoning: '', 99 - toolCalls: [] as Array<{name: string, args: string}>, 99 + toolCalls: [] as Array<{id: string, name: string, args: string}>, 100 100 assistantMessage: '', 101 101 }); 102 102 const [isStreaming, setIsStreaming] = useState(false); ··· 468 468 reasoning: prev.reasoning + chunk.reasoning 469 469 })); 470 470 } else if ((chunk.message_type === 'tool_call_message' || chunk.message_type === 'tool_call') && chunk.tool_call) { 471 - // Add tool call to list 471 + // Add tool call to list (deduplicate by ID) 472 472 const callObj = chunk.tool_call.function || chunk.tool_call; 473 473 const toolName = callObj?.name || callObj?.tool_name || 'tool'; 474 474 const args = callObj?.arguments || callObj?.args || {}; 475 + const toolCallId = chunk.id || `tool_${toolName}_${Date.now()}`; 475 476 476 477 const formatArgsPython = (obj: any): string => { 477 478 if (!obj || typeof obj !== 'object') return ''; ··· 482 483 483 484 const toolLine = `${toolName}(${formatArgsPython(args)})`; 484 485 485 - setCurrentStream(prev => ({ 486 - ...prev, 487 - toolCalls: [...prev.toolCalls, { name: toolName, args: toolLine }] 488 - })); 486 + setCurrentStream(prev => { 487 + // Check if this tool call ID already exists 488 + const exists = prev.toolCalls.some(tc => tc.id === toolCallId); 489 + if (exists) { 490 + return prev; // Don't add duplicates 491 + } 492 + return { 493 + ...prev, 494 + toolCalls: [...prev.toolCalls, { id: toolCallId, name: toolName, args: toolLine }] 495 + }; 496 + }); 489 497 } else if (chunk.message_type === 'assistant_message' && chunk.content) { 490 498 // Accumulate assistant message 491 499 let contentText = ''; ··· 2126 2134 )} 2127 2135 2128 2136 {/* Show tool calls if we have any */} 2129 - {currentStream.toolCalls.map((toolCall, idx) => ( 2130 - <View key={idx} style={styles.messageContainer}> 2137 + {currentStream.toolCalls.map((toolCall) => ( 2138 + <View key={toolCall.id} style={styles.messageContainer}> 2131 2139 <ToolCallItem 2132 2140 callText={toolCall.args} 2133 2141 hasResult={false}