From 4590e1684f9c71f5be9a795f74b4ca294d8cd247 Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Fri, 17 Apr 2026 14:26:55 +0300 Subject: [PATCH] fix: improve sorted chat activity rendering Show tools and reasoning live in sorted mode Keep ambiguous text deferred until final output Avoid duplicate reasoning outside Activity --- .../chat/lib/turns/projectTurnActivity.ts | 50 +++++++++---------- .../chat/lib/turns/projectTurnRecords.ts | 1 + .../components/chat/message/MessageBody.tsx | 10 ++-- 3 files changed, 30 insertions(+), 31 deletions(-) diff --git a/packages/ui/src/components/chat/lib/turns/projectTurnActivity.ts b/packages/ui/src/components/chat/lib/turns/projectTurnActivity.ts index 585d621c..416efc67 100644 --- a/packages/ui/src/components/chat/lib/turns/projectTurnActivity.ts +++ b/packages/ui/src/components/chat/lib/turns/projectTurnActivity.ts @@ -36,19 +36,6 @@ const getMessageFinish = (message: ChatMessageEntry): string | undefined => { return typeof finish === 'string' ? finish : undefined; }; -const isAssistantMessageCompleted = (message: ChatMessageEntry): boolean => { - const info = message.info as { time?: { completed?: unknown }; status?: unknown }; - const completed = info.time?.completed; - const status = info.status; - if (typeof completed !== 'number' || completed <= 0) { - return false; - } - if (typeof status === 'string') { - return status === 'completed'; - } - return true; -}; - const buildTurnPartRecord = ( turnId: string, messageId: string, @@ -69,6 +56,7 @@ interface ProjectActivityInput { turnId: string; assistantMessages: ChatMessageEntry[]; summarySourceMessageId?: string; + summarySourcePartId?: string; showTextJustificationActivity: boolean; } @@ -84,35 +72,42 @@ export const projectTurnActivity = (input: ProjectActivityInput): ProjectActivit let hasTools = false; let hasReasoning = false; + input.assistantMessages.forEach((message) => { + message.parts.forEach((part) => { + if (part.type === 'tool') { + hasTools = true; + return; + } + + if (part.type === 'reasoning' && getPartText(part)) { + hasReasoning = true; + } + }); + }); + const taskMessageById = new Map(); const taskOrder: string[] = []; const partsByAfterTool = new Map(); let currentAfterToolPartId: string | null = null; input.assistantMessages.forEach((message) => { - const messageCompleted = isAssistantMessageCompleted(message); const finish = getMessageFinish(message); + const messageHasTool = message.parts.some((part) => part.type === 'tool'); message.parts.forEach((part, partIndex) => { const isTool = part.type === 'tool'; - if (isTool) { - hasTools = true; - } const text = part.type === 'reasoning' || part.type === 'text' ? getPartText(part) : undefined; - - if (part.type === 'reasoning' && text) { - hasReasoning = true; - } + const partId = part.id ?? `${message.info.id}-part-${partIndex}-${part.type}`; const toolName = isTool ? (part as { tool?: unknown }).tool : undefined; const standaloneTool = isTool && isStandaloneTool(toolName); if (standaloneTool) { - const toolPartId = part.id ?? `${message.info.id}-part-${partIndex}-${part.type}`; + const toolPartId = partId; if (!taskMessageById.has(toolPartId)) { taskMessageById.set(toolPartId, message.info.id); taskOrder.push(toolPartId); @@ -120,6 +115,12 @@ export const projectTurnActivity = (input: ProjectActivityInput): ProjectActivit currentAfterToolPartId = toolPartId; } + const isConfirmedSummaryText = part.type === 'text' + && typeof text === 'string' + && finish === 'stop' + && input.summarySourceMessageId === message.info.id + && input.summarySourcePartId === partId; + let kind: TurnActivityRecord['kind'] | null = null; if (isTool) { kind = 'tool'; @@ -130,10 +131,9 @@ export const projectTurnActivity = (input: ProjectActivityInput): ProjectActivit } else if ( input.showTextJustificationActivity && part.type === 'text' - && messageCompleted - && typeof finish === 'string' - && finish !== 'stop' && text + && !isConfirmedSummaryText + && (messageHasTool || (typeof finish === 'string' && finish !== 'stop')) ) { kind = 'justification'; } diff --git a/packages/ui/src/components/chat/lib/turns/projectTurnRecords.ts b/packages/ui/src/components/chat/lib/turns/projectTurnRecords.ts index d4faaa5b..33da93b1 100644 --- a/packages/ui/src/components/chat/lib/turns/projectTurnRecords.ts +++ b/packages/ui/src/components/chat/lib/turns/projectTurnRecords.ts @@ -168,6 +168,7 @@ export const projectTurnRecords = ( turnId: turn.turnId, assistantMessages: turn.assistantMessages, summarySourceMessageId: turn.summary.sourceMessageId, + summarySourcePartId: turn.summary.sourcePartId, showTextJustificationActivity: effectiveOptions.showTextJustificationActivity, }); turn.activityParts = activity.activityParts; diff --git a/packages/ui/src/components/chat/message/MessageBody.tsx b/packages/ui/src/components/chat/message/MessageBody.tsx index d68a0957..25d4eb63 100644 --- a/packages/ui/src/components/chat/message/MessageBody.tsx +++ b/packages/ui/src/components/chat/message/MessageBody.tsx @@ -1106,6 +1106,8 @@ const AssistantMessageBody: React.FC> = ({ && hasAnchoredActivitySegments && Boolean(toggleActivityGroup); + const shouldDeferSortedInlineText = isSortedRenderMode && !hasStopFinish; + const renderedParts = React.useMemo(() => { const rendered: React.ReactNode[] = []; @@ -1152,7 +1154,7 @@ const AssistantMessageBody: React.FC> = ({ if (part.type === 'text') { const activity = activityByPart.get(part); - if (isSortedRenderMode && !hasStopFinish) { + if (shouldDeferSortedInlineText) { i += 1; continue; } @@ -1177,10 +1179,6 @@ const AssistantMessageBody: React.FC> = ({ if (part.type === 'reasoning') { const activity = activityByPart.get(part); - if (isSortedRenderMode && !hasStopFinish) { - i += 1; - continue; - } if (activity?.kind === 'reasoning') { i += 1; continue; @@ -1293,7 +1291,6 @@ const AssistantMessageBody: React.FC> = ({ chatRenderMode, collapsedPreviewCount, expandedTools, - hasStopFinish, isMobile, isActivityOwnerMessage, isSortedRenderMode, @@ -1306,6 +1303,7 @@ const AssistantMessageBody: React.FC> = ({ shouldShowTool, streamPhase, showReasoningTraces, + shouldDeferSortedInlineText, syntaxTheme, toggleActivityGroup, turnGroupingContext,