From 015d0f0e273e080bb3dc658068c88abf90e92e7a Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Wed, 22 Oct 2025 18:17:53 +0200 Subject: [PATCH] feat: merge tool calls with their updates into single unified view MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Instead of showing tool_call and tool_call_update as separate events, now merge them by toolCallId into a single MergedToolCall that updates as new information comes in. **Changes:** - Added MergedToolCall type with start/end timestamps - Renamed groupTextChunks to groupActivities with tool call merging logic - Track tool calls by ID and merge all updates into single entry - Replace ToolCallItem with MergedToolCallItem component - AgentStreamView now uses groupActivities for unified grouping **User Experience:** - Each tool call appears once and updates in place - Status changes from pending → in_progress → completed - Input/output merge as they become available - Cleaner, less noisy activity view Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude Co-Authored-By: Happy --- .../app/src/components/agent-activity.tsx | 61 +++--- .../app/src/components/agent-stream-view.tsx | 6 +- packages/app/src/types/agent-activity.ts | 190 ++++++++++++++---- 3 files changed, 177 insertions(+), 80 deletions(-) diff --git a/packages/app/src/components/agent-activity.tsx b/packages/app/src/components/agent-activity.tsx index b2a100085..8dc0f535d 100644 --- a/packages/app/src/components/agent-activity.tsx +++ b/packages/app/src/components/agent-activity.tsx @@ -1,10 +1,10 @@ import { useState } from 'react'; import { View, Text, Pressable } from 'react-native'; import { StyleSheet } from 'react-native-unistyles'; -import type { AgentActivity, GroupedTextMessage, SessionUpdate } from '@/types/agent-activity'; +import type { AgentActivity, GroupedTextMessage, MergedToolCall, SessionUpdate } from '@/types/agent-activity'; interface AgentActivityItemProps { - item: GroupedTextMessage | AgentActivity; + item: GroupedTextMessage | MergedToolCall | AgentActivity; } function formatTimestamp(date: Date): string { @@ -74,18 +74,9 @@ function GroupedTextItem({ item }: { item: GroupedTextMessage }) { ); } -function ToolCallItem({ update, timestamp }: { update: SessionUpdate; timestamp: Date }) { +function MergedToolCallItem({ item }: { item: MergedToolCall }) { const [isExpanded, setIsExpanded] = useState(false); - if (update.kind !== 'tool_call' && update.kind !== 'tool_call_update') { - return null; - } - - const isUpdate = update.kind === 'tool_call_update'; - const title = update.title || 'Tool Call'; - const status = update.status; - const toolKind = update.toolKind; - return ( - {formatTimestamp(timestamp)} + {formatTimestamp(item.startTimestamp)} - {getToolIcon(toolKind)} - {title} - {status && ( - - {status} - - )} + {getToolIcon(item.toolKind)} + {item.title} + + {item.status} + {isExpanded ? '▼' : '▶'} @@ -116,25 +105,25 @@ function ToolCallItem({ update, timestamp }: { update: SessionUpdate; timestamp: {isExpanded && ( - {update.rawInput && ( + {item.rawInput && ( Input: - {JSON.stringify(update.rawInput, null, 2)} + {JSON.stringify(item.rawInput, null, 2)} )} - {update.rawOutput && ( + {item.rawOutput && ( Output: - {JSON.stringify(update.rawOutput, null, 2)} + {JSON.stringify(item.rawOutput, null, 2)} )} - {!update.rawInput && !update.rawOutput && ( + {!item.rawInput && !item.rawOutput && ( - {isUpdate ? 'Tool call updated' : 'No details available'} + No details available )} @@ -218,15 +207,15 @@ export function AgentActivityItem({ item }: AgentActivityItemProps) { return ; } + // Merged tool call + if ('kind' in item && item.kind === 'merged_tool_call') { + return ; + } + // Individual activity const activity = item as AgentActivity; const update = activity.update; - // Tool calls - if (update.kind === 'tool_call' || update.kind === 'tool_call_update') { - return ; - } - // Plan if (update.kind === 'plan') { return ; diff --git a/packages/app/src/components/agent-stream-view.tsx b/packages/app/src/components/agent-stream-view.tsx index f818d80e7..325c0d3df 100644 --- a/packages/app/src/components/agent-stream-view.tsx +++ b/packages/app/src/components/agent-stream-view.tsx @@ -5,7 +5,7 @@ import { StyleSheet } from 'react-native-unistyles'; import type { AgentStatus } from '@server/server/acp/types'; import type { SessionNotification } from '@agentclientprotocol/sdk'; import { AgentActivityItem } from './agent-activity'; -import { parseSessionUpdate, groupTextChunks, type AgentActivity } from '@/types/agent-activity'; +import { parseSessionUpdate, groupActivities, type AgentActivity } from '@/types/agent-activity'; export interface AgentStreamViewProps { agentId: string; @@ -94,8 +94,8 @@ export function AgentStreamView({ }) .filter((activity): activity is AgentActivity => activity !== null); - // Group consecutive text chunks - return groupTextChunks(activities); + // Group consecutive text chunks and merge tool calls + return groupActivities(activities); }, [updates]); // Auto-scroll to bottom when new updates arrive diff --git a/packages/app/src/types/agent-activity.ts b/packages/app/src/types/agent-activity.ts index 90c9be6e1..62c11cee4 100644 --- a/packages/app/src/types/agent-activity.ts +++ b/packages/app/src/types/agent-activity.ts @@ -102,6 +102,23 @@ export interface GroupedTextMessage { endTimestamp: Date; } +/** + * Merged tool call (initial call + all updates combined) + */ +export interface MergedToolCall { + kind: 'merged_tool_call'; + toolCallId: string; + title: string; + status: 'pending' | 'in_progress' | 'completed' | 'failed'; + toolKind?: 'read' | 'edit' | 'delete' | 'move' | 'search' | 'execute' | 'think' | 'fetch' | 'switch_mode' | 'other'; + rawInput?: Record; + rawOutput?: Record; + content?: unknown[]; + locations?: unknown[]; + startTimestamp: Date; + endTimestamp: Date; +} + /** * Parse SessionNotification into typed SessionUpdate */ @@ -183,21 +200,51 @@ export function parseSessionUpdate(notification: SessionNotification): SessionUp } /** - * Group consecutive text chunks into messages + * Group consecutive text chunks into messages and merge tool calls by ID */ -export function groupTextChunks(activities: AgentActivity[]): Array { - const result: Array = []; - let currentGroup: { +export function groupActivities(activities: AgentActivity[]): Array { + const result: Array = []; + + // Track current text group + let currentTextGroup: { messageType: 'user' | 'agent' | 'thought'; chunks: string[]; startTimestamp: Date; endTimestamp: Date; } | null = null; + // Track tool calls by ID + const toolCallsById = new Map; + rawOutput?: Record; + content?: unknown[]; + locations?: unknown[]; + startTimestamp: Date; + endTimestamp: Date; + insertIndex: number; // Track where to insert in result + }>(); + + function flushTextGroup() { + if (currentTextGroup) { + result.push({ + kind: 'grouped_text', + messageType: currentTextGroup.messageType, + text: currentTextGroup.chunks.join(''), + startTimestamp: currentTextGroup.startTimestamp, + endTimestamp: currentTextGroup.endTimestamp, + }); + currentTextGroup = null; + } + } + for (const activity of activities) { const update = activity.update; - // Check if this is a text chunk + // Handle text chunks if ( update.kind === 'user_message_chunk' || update.kind === 'agent_message_chunk' || @@ -211,56 +258,117 @@ export function groupTextChunks(activities: AgentActivity[]): Array