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) => {