From a9e24e7071440f789840b2942e41863de4047a93 Mon Sep 17 00:00:00 2001 From: Haileyesus <118998054+blackmammoth@users.noreply.github.com> Date: Mon, 29 Jun 2026 15:41:52 +0300 Subject: [PATCH] fix(chat): group continuous same-tool runs more consistently MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Consecutive tool calls (Edit, Read, Grep, etc.) grouped inconsistently: - The group threshold was 3, so a run of only 2 calls stayed ungrouped while a run of 3 collapsed — making two back-to-back edits look different from three. - A run was broken by any interleaved message, including ones that render nothing (reasoning hidden when showThinking is off). Providers like Codex interleave hidden reasoning between tool calls, so visually continuous edits intermittently failed to group. Lower TOOL_GROUP_THRESHOLD to 2 and skip non-rendered messages when extending a run, so any 2+ consecutive same-tool calls collapse reliably. ChatMessagesPane now passes showThinking into groupConsecutiveTools. --- src/components/chat/utils/toolGrouping.ts | 37 ++++++++++++++----- .../view/subcomponents/ChatMessagesPane.tsx | 5 ++- 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/src/components/chat/utils/toolGrouping.ts b/src/components/chat/utils/toolGrouping.ts index c9d56433..6a964571 100644 --- a/src/components/chat/utils/toolGrouping.ts +++ b/src/components/chat/utils/toolGrouping.ts @@ -1,6 +1,6 @@ import type { ChatMessage } from '../types/types'; -export const TOOL_GROUP_THRESHOLD = 3; +export const TOOL_GROUP_THRESHOLD = 2; export interface ToolGroupItem { _isGroup: true; @@ -19,7 +19,17 @@ function isGroupableToolMessage(message: ChatMessage): message is ChatMessage & return Boolean(message.isToolUse && message.toolName && !message.isSubagentContainer); } -export function groupConsecutiveTools(messages: ChatMessage[]): MessageListItem[] { +// Messages that render nothing (e.g. reasoning hidden when showThinking is off) +// shouldn't split an otherwise-continuous run of the same tool — providers like +// Codex interleave hidden reasoning between consecutive tool calls. +function rendersNothing(message: ChatMessage, showThinking: boolean): boolean { + return Boolean(message.isThinking && !showThinking); +} + +export function groupConsecutiveTools( + messages: ChatMessage[], + showThinking: boolean = true, +): MessageListItem[] { const items: MessageListItem[] = []; let index = 0; @@ -35,13 +45,22 @@ export function groupConsecutiveTools(messages: ChatMessage[]): MessageListItem[ const run: ChatMessage[] = [message]; let nextIndex = index + 1; - while ( - nextIndex < messages.length && - isGroupableToolMessage(messages[nextIndex]) && - messages[nextIndex].toolName === message.toolName - ) { - run.push(messages[nextIndex]); - nextIndex += 1; + while (nextIndex < messages.length) { + const candidate = messages[nextIndex]; + + // Skip invisible interleaved messages so they don't break the run. + if (rendersNothing(candidate, showThinking)) { + nextIndex += 1; + continue; + } + + if (isGroupableToolMessage(candidate) && candidate.toolName === message.toolName) { + run.push(candidate); + nextIndex += 1; + continue; + } + + break; } if (run.length >= TOOL_GROUP_THRESHOLD) { diff --git a/src/components/chat/view/subcomponents/ChatMessagesPane.tsx b/src/components/chat/view/subcomponents/ChatMessagesPane.tsx index bb61096a..55029c58 100644 --- a/src/components/chat/view/subcomponents/ChatMessagesPane.tsx +++ b/src/components/chat/view/subcomponents/ChatMessagesPane.tsx @@ -120,7 +120,10 @@ function ChatMessagesPane({ const messageKeyMapRef = useRef>(new WeakMap()); const allocatedKeysRef = useRef>(new Set()); const generatedMessageKeyCounterRef = useRef(0); - const groupedVisibleMessages = useMemo(() => groupConsecutiveTools(visibleMessages), [visibleMessages]); + const groupedVisibleMessages = useMemo( + () => groupConsecutiveTools(visibleMessages, Boolean(showThinking)), + [visibleMessages, showThinking], + ); // Keep keys stable across prepends so existing MessageComponent instances retain local state. const getMessageKey = useCallback((message: ChatMessage) => {