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
This commit is contained in:
Bohdan Triapitsyn
2026-04-17 14:26:55 +03:00
parent 767f423500
commit 4590e1684f
3 changed files with 30 additions and 31 deletions
@@ -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<string, string>();
const taskOrder: string[] = [];
const partsByAfterTool = new Map<string | null, TurnActivityRecord[]>();
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';
}
@@ -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;
@@ -1106,6 +1106,8 @@ const AssistantMessageBody: React.FC<Omit<MessageBodyProps, 'isUser'>> = ({
&& hasAnchoredActivitySegments
&& Boolean(toggleActivityGroup);
const shouldDeferSortedInlineText = isSortedRenderMode && !hasStopFinish;
const renderedParts = React.useMemo(() => {
const rendered: React.ReactNode[] = [];
@@ -1152,7 +1154,7 @@ const AssistantMessageBody: React.FC<Omit<MessageBodyProps, 'isUser'>> = ({
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<Omit<MessageBodyProps, 'isUser'>> = ({
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<Omit<MessageBodyProps, 'isUser'>> = ({
chatRenderMode,
collapsedPreviewCount,
expandedTools,
hasStopFinish,
isMobile,
isActivityOwnerMessage,
isSortedRenderMode,
@@ -1306,6 +1303,7 @@ const AssistantMessageBody: React.FC<Omit<MessageBodyProps, 'isUser'>> = ({
shouldShowTool,
streamPhase,
showReasoningTraces,
shouldDeferSortedInlineText,
syntaxTheme,
toggleActivityGroup,
turnGroupingContext,